始终在变量替换和命令替换周围使用双引号:"$foo"
,"$(foo)"
如果使用无$foo
引号,则脚本将阻塞$(foo)
包含空格或的输入或参数(或命令输出,带有)\[*?
。
在那里,您可以停止阅读。好吧,这里还有更多:
read
— 要read
使用内置命令逐行读取输入,请使用while IFS= read -r line; do …
Plain read
特殊对待反斜杠和空格。
xargs
— 避免xargs
。如果必须使用xargs
,那就做xargs -0
。相反find … | xargs
,宁愿find … -exec …
。特殊
xargs
对待空格和字符\"'
。
这个答案适用于伯恩/ POSIX风格的贝壳(sh
,ash
,dash
,bash
,ksh
,mksh
,yash
...)。Zsh用户应该跳过它,并阅读何时需要双引号?代替。如果您想了解所有细节,请阅读标准或外壳手册。
请注意,以下说明包含一些近似值(在大多数情况下正确的陈述,但可能会受到周围环境或配置的影响)。
我为什么需要写"$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外壳程序(如灰或破折号)没有数组变量。
myfiles=("$someprefix"*.txt)
process "${myfiles[@]}"
Ksh88的数组变量具有不同的赋值语法set -A myfiles "someprefix"*.txt
(如果需要ksh88 / bash可移植性,请参见不同ksh环境下的赋值变量)。Bourne / POSIX样式的外壳具有一个单一的数组,"$@"
即您使用set
其设置的位置参数数组,并且该位置参数对函数而言是局部的:
set -- "$someprefix"*.txt
process -- "$@"
以开头的文件名-
呢?
请注意,文件名可以以-
(破折号/减号)开头,大多数命令将其解释为表示选项。如果您的文件名以可变部分开头,请确保--
在其前面通过,如上面的代码片段所示。这表明命令已到达选项的末尾,因此此后的任何内容都是文件名,即使它以开头-
。
或者,您可以确保文件名以以外的其他字符开头-
。绝对文件名以开头/
,您可以./
在相对名称的开头添加。以下代码段将变量的内容f
转换为“安全”的方式,以引用保证不以开头的同一文件-
。
case "$f" in -*) "f=./$f";; esac
最后,请注意-
,即使在之后,某些命令也将其解释为标准输入或标准输出--
。如果您需要引用一个名为的实际文件-
,或者正在调用此类程序,而又不想从stdin读取该文件或将其写入stdout,请确保-
按上述方法进行重写。请参见“ du -sh *”和“ du -sh ./*”之间的区别是什么?有待进一步讨论。
如何将命令存储在变量中?
“命令”可能意味着三件事:命令名称(作为可执行文件的名称,带有或不带有完整路径,或者函数的名称,内置或别名),带有参数的命令名称或一段外壳代码。因此,存在将它们存储在变量中的不同方式。
如果您有命令名称,则只需存储它,然后照常使用带双引号的变量即可。
command_path="$1"
…
"$command_path" --option --message="hello world"
如果您有带有参数的命令,则问题与上述文件名列表相同:这是字符串列表,而不是字符串。您不能仅将参数填充到单个字符串中,并在其之间留有空格,因为如果这样做,您将无法分辨出作为参数一部分的空格与分隔参数的空格之间的区别。如果您的外壳中有数组,则可以使用它们。
cmd=(/path/to/executable --option --message="hello world" --)
cmd=("${cmd[@]}" "$file1" "$file2")
"${cmd[@]}"
如果您使用的是不带阵列的外壳怎么办?如果您不介意修改位置参数,则仍然可以使用它们。
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
值为string /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
为空字符串可避免任何修整。请参阅为什么经常使用“ IFS =读取时”而不是“ IFS =”。在阅读时。详细的解释。
这有什么错xargs
?
输入格式xargs
是用空格分隔的字符串,可以选择用单引号或双引号引起来。没有标准工具输出此格式。
xargs -L1
或xargs -l
几乎是行列表的输入,但不完全是-如果行尾有空格,则下一行是续行。
您可以xargs -0
在适用的地方使用(以及可用的地方: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或shell-script。(单击“了解更多…”以查看一些一般性提示以及一些常见问题的手动选择列表。)如果您已搜索但找不到答案,请提出。
shellcheck
帮助您提高程序质量。