…或者是关于强大的文件名处理和 shell 脚本中其他字符串传递的介绍性指南。
我写了一个 shell 脚本,大部分时间都运行良好。但它会因某些输入而阻塞(例如某些文件名)。
我遇到了如下问题:
- 我有一个包含空格的文件名
hello world
,它被视为两个单独的hello
文件world
。 - 我有一个带有两个连续空格的输入行,它们在输入中缩小为一个。
- 输入行中的前导和尾随空格消失。
- 有时,当输入包含其中一个字符时
\[*?
,它们会被一些文本替换,这些文本实际上是某些文件的名称。 - 输入中有一个撇号
'
(或双引号),在那之后事情变得很奇怪。"
- 输入中有一个反斜杠(或者:我正在使用 Cygwin 并且我的一些文件名具有 Windows 样式的
\
分隔符)。
这是怎么回事?我该如何解决这个问题?
答案1
始终在变量替换和命令替换周围使用双引号:"$foo"
,"$(foo)"
如果您使用不带引号的内容,您的脚本将因包含空格或 的输入或参数(或命令输出,带有 )而$foo
阻塞。$(foo)
\[*?
在那里,你可以停止阅读。好吧,这里还有一些:
read
—要使用内置函数逐行读取输入read
,请使用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
。 - 文件名生成:将每个字段视为一个全局变量,即作为通配符模式,并将其替换为与该模式匹配的文件名列表。如果该模式与任何文件都不匹配,则不会对其进行修改。在我们的示例中,这会导致列表包含
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 个字符的字符串*.txt
,并且当您写入时$myfiles
,通配符会展开。这个示例实际上会起作用,直到您将脚本更改为myfiles="$someprefix*.txt"; … process $myfiles
.如果someprefix
设置为final report
,则此操作不起作用。
要处理任何类型的列表(例如文件名),请将其放入数组中。这需要 mksh、ksh93、yash 或 bash(或 zsh,它不存在所有这些引用问题);普通的 POSIX shell(例如 ash 或 dash)没有数组变量。
myfiles=("$someprefix"*.txt)
process "${myfiles[@]}"
Ksh88 具有具有不同赋值语法的数组变量set -A myfiles "someprefix"*.txt
(请参阅不同ksh环境下的赋值变量如果您需要 ksh88/bash 可移植性)。 Bourne/POSIX 风格的 shell 有一个数组,"$@"
即您设置的位置参数数组set
,并且它是函数的本地参数:
set -- "$someprefix"*.txt
process -- "$@"
以 开头的文件名怎么样-
?
在相关说明中,请记住文件名可以以-
(破折号/减号)开头,大多数命令将其解释为表示选项。某些命令(如sh
、set
或sort
)也接受以 开头的选项+
。如果您的文件名以可变部分开头,请务必--
在其前面传递,如上面的代码片段所示。这向命令表明它已到达选项末尾,因此之后的任何内容都是文件名,即使它以-
或开头+
。
或者,您可以确保文件名以 . 以外的字符开头-
。绝对文件名以 开头/
,您可以./
在相对名称的开头添加。以下代码片段将变量的内容转换f
为引用同一文件的“安全”方式,保证不以-
nor开头+
。
case "$f" in -* | +*) "f=./$f";; esac
关于此主题的最后一点是,请注意某些命令会解释-
为标准输入或标准输出,即使在--
.如果您需要引用名为 的实际文件-
,或者如果您正在调用这样的程序并且您不希望它从 stdin 读取或写入 stdout,请确保-
按上述方式重写。看“du -sh *”和“du -sh ./*”有什么区别?以供进一步讨论。
如何将命令存储在变量中?
“命令”可以表示三件事:命令名称(可执行文件的名称,带或不带完整路径,或函数名称,内置或别名),带参数的命令名称,或一段 shell 代码。因此有不同的方式将它们存储在变量中。
如果您有命令名称,只需存储它并像平常一样使用带有双引号的变量即可。
command_path="$1"
…
"$command_path" --option --message="hello world"
如果您有一个带参数的命令,则问题与上面的文件名列表相同:这是一个字符串列表,而不是一个字符串。您不能将参数填充到一个中间有空格的字符串中,因为如果这样做,您将无法区分作为参数一部分的空格和分隔参数的空格之间的区别。如果您的 shell 有数组,则可以使用它们。
cmd=(/path/to/executable --option --message="hello world" --)
cmd=("${cmd[@]}" "$file1" "$file2")
"${cmd[@]}"
如果您使用的 shell 没有数组怎么办?如果您不介意修改位置参数,您仍然可以使用它们。
set -- /path/to/executable --option --message="hello world" --
set -- "$@" "$file1" "$file2"
"$@"
如果您需要存储复杂的 shell 命令(例如重定向、管道等)怎么办?或者如果您不想修改位置参数?然后您可以构建一个包含该命令的字符串,并使用eval
内置命令。
code='/path/to/executable --option --message="hello world" -- /path/to/file1 | grep "interesting stuff"'
eval "$code"
请注意 定义中的嵌套引号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%.}'"
将变量扩展保留在代码内部,以便在计算代码时而不是在构建代码片段时查找它。这更简单,但只有在执行代码时变量仍然具有相同值的情况下才有效,例如如果代码是在循环中构建的,则不起作用。
code="$code \"\$filename\""
最后,您真的需要一个包含代码的变量吗?为代码块命名的最自然的方法是定义一个函数。
怎么了read
?
没有-r
,read
允许连续行——这是输入的单个逻辑行:
hello \
world
read
将输入行拆分为由 中的字符分隔的字段$IFS
(如果没有-r
,反斜杠也会转义这些字段)。例如,如果输入是包含三个单词的行,则read first second third
设置first
为输入的第一个单词、second
第二个单词和third
第三个单词。如果还有更多单词,则最后一个变量包含设置前面的单词后剩下的所有内容。前导和尾随空白被修剪。
设置IFS
为空字符串可以避免任何修剪。看为什么如此频繁地使用“while IFS= read”,而不是“IFS=;”在阅读时..`?以获得更长的解释。
有什么问题吗xargs
?
的输入格式xargs
是空格分隔的字符串,可以选择单引号或双引号。没有标准工具输出这种格式。
xargs -L1
或者xargs -l
不分割输入线,但每行输入运行一个命令(该行仍然拆分以组成参数,如果以空格结尾,则继续下一行)。
xargs -I PLACEHOLDER
确实使用一行输入来替换,PLACEHOLDER
但仍会处理引号和反斜杠并修剪前导空格。
您可以xargs -r0
在适用的情况下使用(以及可用的情况:GNU(Linux、Cygwin)、BusyBox、BSD、OSX,但不在 POSIX 中)。这是安全的,因为空字节不能出现在大多数数据中,特别是在文件名和外部命令参数中。要生成以空分隔的文件名列表,请使用find … -print0
(或者您可以find … -exec …
按如下所述使用)。
如何处理 找到的文件find
?
find … -exec some_command a_parameter another_parameter {} +
some_command
需要是外部命令,不能是 shell 函数或别名。如果需要调用 shell 来处理文件,请sh
显式调用。
find … -exec sh -c '
for x do
… # process the file "$x"
done
' find-sh {} +
我还有其他问题
浏览引用在此网站上标记,或壳或者shell脚本。 (单击“了解更多...”查看一些一般提示和手动选择的常见问题列表。)如果您进行了搜索但找不到答案,问走。
答案2
虽然吉尔斯的回答非常好,但我对他的主要观点有异议
始终在变量替换和命令替换周围使用双引号:“$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
我并不鼓励用户采用这种行为,但如果有人坚定地理解何时发生分词,那么他们应该能够自己决定何时使用引号。
答案3
据我所知,只有两种情况需要双引号扩展,这些情况涉及两个特殊的 shell 参数"$@"
和"$*"
- ,它们被指定在用双引号括起来时以不同的方式扩展。在所有其他情况下(也许排除特定于 shell 的数组实现)扩展的行为是可配置的——有一些选项。
当然,这并不是说应该避免双引号 - 相反,它可能是 shell 必须提供的界定扩展的最方便、最可靠的方法。但是,我认为,由于已经熟练地阐述了替代方案,因此这是讨论 shell 扩展值时会发生什么的绝佳场所。
外壳,在它的心脏和灵魂中(对于那些有这样的人), 是一个命令解释器 - 它是一个解析器,就像一个大型的交互式sed
.如果你的 shell 语句是窒息在空白或类似的情况,那么很可能是因为您还没有完全理解 shell 的解释过程 - 特别是它如何以及为何将输入语句转换为可操作的命令。 shell 的工作是:
接受输入
解释和分裂它正确地进入标记化输入字
输入字是 shell 语法项,例如
$word
orecho $words 3 4* 5
字总是在空格上分割 - 这只是语法 - 但只有在其输入文件中提供给 shell 的文字空白字符
如有必要,将其扩展为多个领域
领域结果来自单词扩展 - 它们构成最终的可执行命令
除了
"$@"
,$IFS
场分裂, 和路径名扩展一个输入单词必须始终评估为单个场地。
然后执行结果命令
- 在大多数情况下,这涉及以某种形式传递其解释结果
人们常说外壳是胶水,并且,如果这是真的,那么它是什么粘着是参数列表 - 或领域- 一个或另一个进程(当它exec
是它们时)。大多数 shell 都不能NUL
很好地处理字节——如果有的话——这是因为它们已经在字节上进行了分裂。外壳必须exec
很多并且它必须使用NUL
当时传递给系统内核的分隔参数数组来完成此操作exec
。如果您将 shell 的分隔符与其分隔数据混合在一起,那么 shell 可能会把它搞砸。它的内部数据结构 - 像大多数程序一样 - 依赖于该分隔符。zsh
值得注意的是,这并没有搞砸。
这就是$IFS
出现的地方。$IFS
是一个始终存在且同样可设置的 shell 参数,它定义 shell 应如何将 shell 扩展从单词到场地- 特别是关于什么价值这些领域应划定。$IFS
在除NUL
- 之外的分隔符上分割 shell 扩展,或者换句话说,shell 替换由与其内部数据数组中的$IFS
with值相匹配的扩展产生的字节。NUL
当你这样看时,你可能会开始发现每一个场分裂shell 扩展是一个$IFS
- 分隔的数据数组。
重要的是要明白,$IFS
只有划定界限扩展是不是已经以其他方式分隔 - 您可以使用"
双引号来完成。当您引用扩展时,您会在头部对其进行定界,并且至少到其值的尾部。在这些情况下$IFS
不适用,因为没有可分隔的字段。事实上,双引号展开式表现出相同的效果场分裂IFS=
当设置为空值时,行为为不带引号的扩展。
除非引用,否则$IFS
它本身就是一个$IFS
定界 shell 扩展。它默认为指定值<space><tab><newline>
- 所有三个值包含在 中时都表现出特殊属性$IFS
。而 的任何其他值$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"
<///// >
但这只是$IFS
- 只是分词或空白正如所问,那么特殊字符?
shell - 默认情况下 - 也会扩展某些未加引号的标记(如此?*[
处其他地方所述)分成多个领域当它们出现在列表中时。这就是所谓的路径名扩展, 或者通配。它是一个非常有用的工具,而且,因为它发生在场分裂在 shell 的解析顺序中,它不受$IFS-领域由路径名扩展生成的文件名在文件名本身的头/尾部进行分隔,无论其内容是否包含当前$IFS
.此行为默认设置为打开 - 但也可以很容易地以其他方式进行配置。
set -f
这指示外壳不是到全局。至少在该设置以某种方式撤消之前,路径名扩展不会发生 - 例如,如果当前 shell 被另一个新的 shell 进程替换,或者......
set +f
...被发送到 shell。双引号 - 就像它们一样$IFS
场分裂- 每次扩展都不需要此全局设置。所以:
echo "*" *
...如果当前启用路径名扩展,每个参数可能会产生非常不同的结果 - 因为第一个参数只会扩展为其字面值(单个星号字符,也就是说,根本没有)如果当前工作目录不包含可能匹配的文件名,则第二个仅相同(它几乎匹配所有这些)。但是,如果您这样做:
set -f; echo "*" *
...两个参数的结果是相同的 -*
在这种情况下不会扩展。
答案4
考虑到上面提到的所有安全隐患,并假设您信任并控制您扩展的变量,则可以使用eval
.不过要小心!
$ FILES='"a b" c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
$ FILES='a\ b c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory