Gil*_*il' 341 shell bash shell-script quoting whitespace
或者,有关强大的文件名处理和在 shell 脚本中传递的其他字符串的介绍性指南。
我写了一个 shell 脚本,它在大多数情况下运行良好。但它在某些输入(例如某些文件名)上窒息。
我遇到了如下问题:
hello world,它被视为两个单独的文件hello和world.\[*?,它们会被一些文本替换,这实际上是文件的名称。'(或双引号"),在那之后事情变得很奇怪。\分隔符)。这是怎么回事,我该如何解决?
Gil*_*il' 437
"$foo","$(foo)"如果您使用$foo未加引号,您的脚本将在$(foo)包含空格或\[*?.
在那里,您可以停止阅读。好吧,还有一些:
read—要read使用内置函数逐行读取输入,请使用while IFS= read -r line; do …read特殊对待反斜杠和空格。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。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)
-呢?在相关说明中,请记住文件名可以以-(破折号/减号)开头,大多数命令将其解释为表示选项。某些命令(如sh、set或sort)也接受以 开头的选项+。如果您有一个以可变部分开头的文件名,请确保--在它之前传递,如上面的代码片段所示。这向命令表明它已到达选项的末尾,因此之后的任何内容都是文件名,即使它以-或开头+。
或者,您可以确保您的文件名以除-. 绝对文件名以 开头/,您可以./在相对名称的开头添加。以下代码段将变量的内容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)
浏览此站点上的引用标记,或shell或shell-script。(单击“了解更多...”可查看一些一般提示和手动选择的常见问题列表。)如果您进行了搜索但找不到答案,请离开。
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)
我不鼓励用户采用这种行为,但如果有人清楚地理解何时发生分词,那么他们应该能够自己决定何时使用引号。
mik*_*erv 24
据我所知,只有两种情况需要双引号扩展,这些情况涉及两个特殊的 shell 参数"$@"和"$*"- 当用双引号括起来时,它们被指定为不同的扩展。在所有其他情况下(可能不包括特定于 shell 的数组实现),扩展的行为是可配置的 - 有一些选项。
当然,这并不是说应该避免双引号——相反,它可能是 shell 必须提供的最方便和最可靠的分隔扩展的方法。但是,我认为,由于已经熟练地阐述了替代方案,这是讨论 shell 扩展值时会发生什么的绝佳场所。
shell,在其核心和灵魂中(对于那些拥有它的人),是一个命令解释器 - 它是一个解析器,就像一个大的、交互式的sed. 如果您的 shell 语句因空格或类似内容而阻塞,那么很可能是因为您还没有完全理解 shell 的解释过程 - 特别是它将输入语句转换为可操作命令的方式和原因。shell 的工作是:
接受输入
将其正确解释并拆分为标记化的输入词
输入词是 shell 语法项,例如$word或echo $words 3 4* 5
单词总是在空格上分割 - 这只是语法 - 但只有在其输入文件中提供给 shell 的文字空白字符
如有必要,将这些扩展到多个领域
字段由单词扩展产生 - 它们构成最终的可执行命令
除了"$@",$IFS 字段拆分和路径名扩展,输入单词必须始终计算为单个字段。
然后执行结果命令
人们经常说 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)
这指示shell不给glob的。至少在该设置以某种方式撤消之前不会发生路径名扩展 - 例如,如果当前 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 次 |
| 最近记录: |