为什么我的 shell 脚本会因空格或其他特殊字符而阻塞?

Gil*_*il' 341 shell bash shell-script quoting whitespace

或者,有关强大的文件名处理和在 shell 脚本中传递的其他字符串的介绍性指南。

我写了一个 shell 脚本,它在大多数情况下运行良好。但它在某些输入(例如某些文件名)上窒息。

我遇到了如下问题:

  • 我有一个包含空格的文件名hello world,它被视为两个单独的文件helloworld.
  • 我有一个带有两个连续空格的输入行,它们在输入中缩小为一个。
  • 前导和尾随空格从输入行中消失。
  • 有时,当输入包含其中一个字符时\[*?,它们会被一些文本替换,这实际上是文件的名称。
  • 输入中有一个撇号'(或双引号"),在那之后事情变得很奇怪。
  • 输入中有一个反斜杠(或者:我使用的是 Cygwin 并且我的一些文件名具有 Windows 样式的\分隔符)。

这是怎么回事,我该如何解决?

Gil*_*il' 437

始终在变量替换和命令替换周围使用双引号:"$foo","$(foo)"

如果您使用$foo未加引号,您的脚本将在$(foo)包含空格或\[*?.

在那里,您可以停止阅读。好吧,还有一些:

  • readread使用内置函数逐行读取输入,请使用while IFS= read -r line; do …
    Plainread特殊对待反斜杠和空格。
  • xargs——避免xargs。如果您必须使用xargs,请使用xargs -0。而不是find … | xargs更喜欢find … -exec …。特殊
    xargs对待空格和字符\"'

这个答案适用于 Bourne/POSIX 风格的 shell(sh, ash, dash, bash, ksh, mksh, yash...)。Zsh 用户应该跳过它并阅读何时需要双引号?反而。如果您想要完整的细节,请阅读标准或您的 shell 手册。


请注意,下面的解释包含一些近似值(在大多数情况下为真但可能受周围上下文或配置影响的陈述)。

为什么我需要写"$foo"?没有引号会发生什么?

$foo并不意味着“取变量的值foo”。这意味着更复杂的事情:

  • 首先,获取变量的值。
  • 字段拆分:将该值视为以空格分隔的字段列表,并构建结果列表。例如,如果变量包含,foo * bar ?则此步骤的结果是 3 元素列表foo, *, bar
  • 文件名生成:将每个字段视为一个 glob,即作为通配符模式,并将其替换为与此模式匹配的文件名列表。如果模式与任何文件都不匹配,则不会对其进行修改。在我们的示例中,这会导致列表包含foo,然后是当前目录中的文件列表,最后是bar. 如果当前目录为空,则结果为foo, *, bar

请注意,结果是一个字符串列表。shell 语法中有两种上下文:列表上下文和字符串上下文。字段拆分和文件名生成仅发生在列表上下文中,但大多数情况下都是如此。双引号分隔字符串上下文:整个双引号字符串是单个字符串,不可拆分。(例外:"$@"扩展到位置参数的列表,例如"$@"相当于"$1" "$2" "$3"如果有三个位置参数。请参阅$* 和 $@ 之间的区别是什么?

使用$(foo)或进行命令替换也会发生同样的情况`foo`。附带说明一下,不要使用`foo`: 它的引用规则很奇怪且不可移植,并且所有现代 shell 都支持$(foo)绝对等效,除了具有直观的引用规则。

算术替换的输出也经历了相同的扩展,但这通常不是问题,因为它只包含不可扩展的字符(假设IFS不包含数字或-)。

请参阅何时需要双引号?有关可以省略引号的情况的更多详细信息。

除非您的意思是所有这些繁琐的事情发生,请记住始终在变量和命令替换周围使用双引号。请注意:省略引号不仅会导致错误,还会导致安全漏洞

如何处理文件名列表?

如果你写myfiles="file1 file2", 用空格分隔文件,这不适用于包含空格的文件名。Unix 文件名可以包含除/(始终是目录分隔符)和空字节(您不能在大多数 shell 的 shell 脚本中使用)以外的任何字符。

同样的问题myfiles=*.txt; … process $myfiles。执行此操作时,变量myfiles包含 5 个字符的 string *.txt,并且在您编写时$myfiles扩展了通配符。这个例子实际上会起作用,直到您将脚本更改为myfiles="$someprefix*.txt"; … process $myfiles. 如果someprefix设置为final report,这将不起作用。

要处理任何类型的列表(例如文件名),请将其放入数组中。这需要 mksh、ksh93、yash 或 bash(或 zsh,它没有所有这些引用问题);普通的 POSIX shell(例如 ash 或 dash)没有数组变量。

myfiles=("$someprefix"*.txt)
process "${myfiles[@]}"
Run Code Online (Sandbox Code Playgroud)

ksh88 有不同赋值语法的数组变量set -A myfiles "someprefix"*.txt(如果需要 ksh88/bash 可移植性,请参阅不同 ksh 环境下的赋值变量)。Bourne/POSIX 风格的 shell 有一个单一的数组,"$@"你设置的位置参数数组,set它是函数的局部变量:

set -- "$someprefix"*.txt
process -- "$@"
Run Code Online (Sandbox Code Playgroud)

以 开头的文件名-呢?

在相关说明中,请记住文件名可以以-(破折号/减号)开头,大多数命令将其解释为表示选项。某些命令(如shsetsort)也接受以 开头的选项+。如果您有一个以可变部分开头的文件名,请确保--在它之前传递,如上面的代码片段所示。这向命令表明它已到达选项的末尾,因此之后的任何内容都是文件名,即使它以-或开头+

或者,您可以确保您的文件名以除-. 绝对文件名以 开头/,您可以./在相对名称的开头添加。以下代码段将变量的内容f转换为引用同一文件的“安全”方式,该文件保证不以-nor开头+

case "$f" in -* | +*) "f=./$f";; esac
Run Code Online (Sandbox Code Playgroud)

关于此主题的最后一点,请注意某些命令会解释-为表示标准输入或标准输出,即使在--. 如果您需要引用名为 的实际文件-,或者如果您正在调用这样的程序并且您不希望它从标准输入读取或写入标准输出,请确保-按上述方式重写。请参阅“du -sh *”和“du -sh ./*”有什么区别?供进一步讨论。

如何将命令存储在变量中?

“命令”可以表示三件事:命令名称(作为可执行文件的名称,带或不带完整路径,或函数名称、内置函数或别名)、带参数的命令名称或一段 shell 代码。因此有不同的方式将它们存储在变量中。

如果您有命令名称,只需存储它并像往常一样使用带双引号的变量。

command_path="$1"
…
"$command_path" --option --message="hello world"
Run Code Online (Sandbox Code Playgroud)

如果您有一个带参数的命令,问题与上面的文件名列表相同:这是一个字符串列表,而不是一个字符串。您不能只是将参数填充到一个字符串中,中间有空格,因为如果这样做,您将无法区分作为参数一部分的空格和分隔参数的空格之间的区别。如果你的 shell 有数组,你可以使用它们。

cmd=(/path/to/executable --option --message="hello world" --)
cmd=("${cmd[@]}" "$file1" "$file2")
"${cmd[@]}"
Run Code Online (Sandbox Code Playgroud)

如果您使用没有数组的外壳怎么办?如果您不介意修改它们,您仍然可以使用位置参数。

set -- /path/to/executable --option --message="hello world" --
set -- "$@" "$file1" "$file2"
"$@"
Run Code Online (Sandbox Code Playgroud)

如果您需要存储复杂的 shell 命令,例如重定向、管道等,该怎么办?或者如果您不想修改位置参数?然后你可以构建一个包含命令的字符串,并使用eval内置命令。

code='/path/to/executable --option --message="hello world" -- /path/to/file1 | grep "interesting stuff"'
eval "$code"
Run Code Online (Sandbox Code Playgroud)

注意定义中的嵌套引号code:单引号'…'分隔字符串文字,因此变量的值code是字符串/path/to/executable --option --message="hello world" -- /path/to/file1。该eval内建告诉shell解析作为参数传递,如果它出现在脚本中的字符串,所以在这一点上引号和管道解析等。

使用起来eval很棘手。仔细考虑什么时候解析什么。特别是,您不能只将文件名填充到代码中:您需要引用它,就像在源代码文件中一样。没有直接的方法可以做到这一点。code="$code $filename"如果文件名包含任何 shell 特殊字符(空格$;|<>、 等),则类似于中断。code="$code \"$filename\""仍然休息"$\`。即使code="$code '$filename'"休息,如果文件名包含'。有两种解决方案。

  • 在文件名周围添加一层引号。最简单的方法是在它周围添加单引号,并将单引号替换为'\''.

    quoted_filename=$(printf %s. "$filename" | sed "s/'/'\\\\''/g")
    code="$code '${quoted_filename%.}'"
    
    Run Code Online (Sandbox Code Playgroud)
  • 将变量扩展保留在代码中,以便在评估代码时查找它,而不是在构建代码片段时查找它。这更简单,但只有在代码执行时变量仍然具有相同的值时才有效,例如,如果代码是在循环中构建的。

    code="$code \"\$filename\""
    
    Run Code Online (Sandbox Code Playgroud)

最后,你真的需要一个包含代码的变量吗?给代码块命名最自然的方法是定义一个函数。

怎么了read

没有-r,read允许继续行——这是输入的单个逻辑行:

hello \
world
Run Code Online (Sandbox Code Playgroud)

read将输入行拆分为由 in 字符分隔的字段$IFS(没有-r,反斜杠也会转义这些)。例如,如果输入是包含三个单词的行,则read first second third设置first为输入的第一个单词、second第二个单词和third第三个单词。如果有更多单词,最后一个变量包含设置前面的单词后剩下的所有内容。前导和尾随空格被修剪。

设置IFS为空字符串可避免任何修剪。请参阅为什么经常使用 `while IFS= read`,而不是 `IFS=; 阅读时..`?更长的解释。

怎么了xargs

的输入格式xargs是空格分隔的字符串,可以选择单引号或双引号。没有标准工具输出这种格式。

xargs -L1or的输入xargs -l几乎是一个行列表,但不完全是——如果行尾有空格,则下一行是续行。

您可以xargs -0在适用的情况下使用(如果可用:GNU(Linux、Cygwin)、BusyBox、BSD、OSX,但它不在 POSIX 中)。这是安全的,因为空字节不能出现在大多数数据中,尤其是文件名中。要生成以空分隔的文件名列表,请使用find … -print0(或者您可以使用find … -exec …,如下所述)。

如何处理由 找到的文件find

find … -exec some_command a_parameter another_parameter {} +
Run Code Online (Sandbox Code Playgroud)

some_command必须是外部命令,不能是 shell 函数或别名。如果您需要调用 shell 来处理文件,请sh显式调用。

find … -exec sh -c '
  for x do
    … # process the file "$x"
  done
' find-sh {} +
Run Code Online (Sandbox Code Playgroud)

我还有其他问题

浏览此站点上的标记,或。(单击“了解更多...”可查看一些一般提示和手动选择的常见问题列表。)如果您进行了搜索但找不到答案,请离开

  • @John1024 这只是一个 GNU 功能,所以我会坚持“没有标准工具”。 (6认同)
  • 请注意,`xargs -0` 不是 POSIX。除了 FreeBSD 的 `xargs`,你通常需要 `xargs -r0` 而不是 `xargs -0`。 (3认同)
  • @StéphaneChazelas 我明白了。我想你可能想链接 [here](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_05) 或者至少我应该通读它,不幸的是,明确指定数学扩展将在 `$IFS` 上进行字段拆分。*叹* (3认同)
  • 另一个不错的(仅限 GNU)功能是 `xargs -d "\n"`,这样你就可以运行例如 `locate PATTERN1 |xargs -d "\n" grep PATTERN2` 来搜索匹配 *PATTERN1* 且内容匹配的文件名*模式2*。没有 GNU,你可以这样做,例如`locate PATTERN1 |perl -pne 's/\n/\0/' |xargs -0 grep PATTERN1` (3认同)
  • 除了`zsh`(甚至在sh仿真中)和`mksh`之外,您还需要在`$(( ... ))`(在某些shell中也是`$[...]`)周围的引号。 (2认同)
  • @John1024,不,`ls --quoting-style=shell-always` 与 `xargs` 不兼容。试试 `touch $'a\nb'; ls --quoting-style=shell-always | xargs` (2认同)
  • @John1024 posix `xargs` 规范推荐`INPUT | sed 's/./\\&amp;/g' | xargs cmd` 将保护一行中的每个字符 - 但你需要为 `\newline` 提供其他东西。您可以使用`od` 获得大多数输入的C 风格转义,正如我最近意识到的,使用`INPUT | sed l`。那可以使`INPUT | xargs printf %b\\0` 非常有用。 (2认同)
  • 来自`man find`:`使用 -exec 操作存在不可避免的安全问题;你应该改用 -execdir 选项。`。这篇文章会受益于提及更好的方法而不是教人们如何使用更糟糕的选择。 (2认同)

Ste*_*nny 25

虽然吉尔斯的回答很好,但我对他的主要观点提出异议

始终在变量替换和命令替换周围使用双引号:“$foo”、“$(foo)”

当您开始使用类似 Bash 的 shell 进行分词时,当然安全的建议总是使用引号。然而,并不总是进行分词

§ 分词

这些命令可以无错误地运行

foo=$bar
bar=$(a command)
logfile=$logdir/foo-$(date +%Y%m%d)
PATH=/usr/local/bin:$PATH ./myscript
case $foo in bar) echo bar ;; baz) echo baz ;; esac
Run Code Online (Sandbox Code Playgroud)

我不鼓励用户采用这种行为,但如果有人清楚地理解何时发生分词,那么他们应该能够自己决定何时使用引号。

  • 正如我在回答中提到的,有关详细信息,请参阅 http://unix.stackexchange.com/questions/68694/when-is-double-quoting-necessary。请注意这个问题——“为什么我的 shell 脚本会卡住?”。最常见的问题(来自本网站和其他地方的多年经验)是缺少双引号。“始终使用双引号”比“始终使用双引号,除非在不需要的情况下”更容易记住。 (24认同)
  • 规则对于初学者来说很难理解。例如,`foo=$bar` 是可以的,但 `export foo=$bar` 或 `env foo=$var` 不是(至少在某些 shell 中)。给初学者的建议:_总是引用你的变量,除非你知道你在做什么并且有充分的理由不_。 (16认同)
  • 我的 0.02 美元是建议引用所有内容是个好建议。错误地引用不需要的东西是无害的,错误地不引用确实需要的东西是有害的。因此,对于大多数永远不会理解分词发生的复杂性的 shell 脚本作者来说,引用所有内容比仅在必要时引用要安全得多。 (8认同)
  • @StevenPenny 真的更正确吗?是否存在引号会破坏脚本的合理情况?在一半情况下 * 必须* 使用引号的情况下,在另一半情况下 * 可以* 可选地使用引号 - 那么建议“总是使用引号,以防万一”是应该考虑的,因为它是真实的,简单的并且风险较小。众所周知,向初学者教授此类例外列表是无效的(缺乏上下文,他们不会记住它们)并且适得其反,因为他们会混淆需要/不需要的引用,破坏他们的脚本并削弱他们进一步学习的动力。 (7认同)
  • @Peteris 和 Godlygeek:“在合理的情况下,引号会破坏脚本吗?” 这取决于您对“合理”的定义。如果脚本设置了 `criteria="-type f"`,则 `find 。$criteria` 有效,但 `find 。"$criteria"` 没有。 (6认同)
  • 教,当然,但不要从一开始就被细节淹没。如果引用总是安全的,告诉他们总是引用。一旦他们掌握了这一点,然后继续告诉他们在某些情况下不需要它。想想教物理,例如:我们首先教学生牛顿定律,而不是从广义相对论开始,把牛顿定律当作它们的特殊情况来对待。 (3认同)
  • @Gilles - 这是一个很好的观点,但我认为史蒂文也说过他的话很好。我认为特别重要的 - 尽管这里没有提到 - 是在定义 `$IFS` 不同方式后通过引用然后取消引用 `$*` 来解析 shell 参数数组的能力。而且,虽然可能不太重要,因为你确实涉及到通配符,也许提到 `set [-+]f` 可能是值得的。 (2认同)
  • @Gilles 是的,而 `Always ... except ...` 没有那么简洁,它更正确。 (2认同)
  • @StephaneChazelas - 这是*因为*他们很难理解规则需要*重复* - 而不是边缘化。 (2认同)
  • @Peteris - 了解他们所做的事情风险要小得多。如果有人认为双引号可以保护一切,而他们最终在包含双引号的变量上使用了 `eval` 怎么办? (2认同)

mik*_*erv 24

据我所知,只有两种情况需要双引号扩展,这些情况涉及两个特殊的 shell 参数"$@""$*"- 当用双引号括起来时,它们被指定为不同的扩展。在所有其他情况下(可能不包括特定于 shell 的数组实现),扩展的行为是可配置的 - 有一些选项。

当然,这并不是说应该避免双引号——相反,它可能是 shell 必须提供的最方便和最可靠的分隔扩展的方法。但是,我认为,由于已经熟练地阐述了替代方案,这是讨论 shell 扩展值时会发生什么的绝佳场所。

shell,在其核心和灵魂中(对于那些拥有它的人),是一个命令解释器 - 它是一个解析器,就像一个大的、交互式的sed. 如果您的 shell 语句因空格或类似内容而阻塞,那么很可能是因为您还没有完全理解 shell 的解释过程 - 特别是它将输入语句转换为可操作命令的方式和原因。shell 的工作是:

  1. 接受输入

  2. 其正确解释并拆分为标记化的输入

    • 输入是 shell 语法项,例如$wordecho $words 3 4* 5

    • 单词总是在空格上分割 - 这只是语法 - 但只有在其输入文件中提供给 shell 的文字空白字符

  3. 如有必要,将这些扩展到多个领域

    • 字段单词扩展产生 - 它们构成最终的可执行命令

    • 除了"$@"$IFS 字段拆分路径名扩展,输入单词必须始终计算为单个字段

  4. 然后执行结果命令

    • 在大多数情况下,这涉及以某种形式传递其解释的结果

人们经常说 shell 是一种胶水,如果这是真的,那么它所粘贴的是参数列表——或字段——当它exec是一个或另一个进程时。大多数 shell 不能NUL很好地处理字节 - 如果有的话 - 这是因为它们已经在它上面分裂了。shell 有exec 很多事情要做,它必须使用一个NUL分隔的参数数组来完成这项工作,这些参数是它当时交给系统内核的exec。如果您将 shell 的分隔符与其分隔的数据混合在一起,那么 shell 可能会把它搞砸。它的内部数据结构 - 像大多数程序一样 - 依赖于该分隔符。zsh,值得注意的是,并没有搞砸。

这就是$IFS进来的地方。$IFS是一个始终存在的 - 同样可设置的 - shell 参数,它定义了 shell 应该如何将 shell 扩展从单词拆分到字段- 特别是这些字段应该分隔哪些值。$IFS在除NUL-之外的分隔符上拆分 shell 扩展,或者换言之,shell 替换扩展产生的字节,这些字节与其内部数据数组中的$IFSwith值匹配NUL。当您这样看待它时,您可能会开始看到每个字段拆分壳扩展都是一个带$IFS分隔符的数据数组。

理解这一点很重要$IFS限定那些扩展不是,你可以用做-已另有分隔"双引号。当你引用一个扩展时,你在它的头部和至少在它的值的尾部分隔它。在这些情况下$IFS不适用,因为没有要分隔的字段。事实上,当设置为空值时,双引号扩展表现出与未引用扩展相同的字段拆分行为IFS=

除非引用,否则$IFS它本身就是一个带$IFS分隔符的 shell 扩展。它默认为指定值<space><tab><newline>- 当包含在$IFS. 而任何其他值 for$IFS被指定为每个扩展出现评估单个字段空白- 这三个中的任何一个 - 被指定为每个扩展序列省略单个字段,并且前导/尾随序列被完全省略。这可能是通过示例最容易理解的。$IFS

slashes=///// spaces='     '
IFS=/; printf '<%s>' $slashes$spaces
<><><><><><     >
IFS=' '; printf '<%s>' $slashes$spaces
</////>
IFS=; printf '<%s>' $slashes$spaces
</////     >
unset IFS; printf '<%s>' "$slashes$spaces"
</////     >
Run Code Online (Sandbox Code Playgroud)

但这只是$IFS- 只是所问的分词或空格,那么特殊字符呢?

shell - 默认情况下 - 当某些未加引号的标记(如此?*[处其他地方所述)出现在列表中时,也会将它们扩展为多个字段。这就是所谓的路径扩展,或通配符。这是一个非常有用的工具,而且,因为它发生在shell 的解析顺序中的字段拆分之后,所以它不受$IFS 的影响-由路径名扩展生成的字段在文件名本身的头部/尾部分隔,无论是否它们的内容包含当前在$IFS. 默认情况下,此行为设置为开启 - 但否则很容易配置。

set -f
Run Code Online (Sandbox Code Playgroud)

这指示shellglob的。至少在该设置以某种方式撤消之前不会发生路径名扩展 - 例如,如果当前 shell 被另一个新的 shell 进程替换或......

set +f
Run Code Online (Sandbox Code Playgroud)

...发出给shell。双引号 - 它们也用于$IFS 字段拆分- 使每次扩展都不需要此全局设置。所以:

echo "*" *
Run Code Online (Sandbox Code Playgroud)

...如果当前启用了路径名扩展,每个参数可能会产生非常不同的结果 - 因为第一个只会扩展到它的文字值(单个星号字符,也就是说,根本没有),第二个只会扩展到相同如果当前工作目录不包含可能匹配的文件名(并且几乎匹配所有文件名。但是,如果您这样做:

set -f; echo "*" *
Run Code Online (Sandbox Code Playgroud)

...两个参数的结果是相同的 -*在这种情况下不会扩展。


归档时间:

查看次数:

324437 次

最近记录:

5 年,9 月 前