我有一个复杂的命令,我想制作一个 shell/bash 脚本。我可以很容易地用 $1
来写它:
foo $1 args -o $1.ext
我希望能够将多个输入名称传递给脚本。正确的方法是什么?
而且,当然,我想处理带有空格的文件名。
使用 "$@"
表示所有参数:
for var in "$@"
do
echo "$var"
done
这将遍历每个参数并将其打印在单独的行上。 $@ 的行为类似于 $* ,只是在引用时,如果参数中有空格,则它们会被正确分解:
sh test.sh 1 2 '3 4'
1
2
3 4
Robert Gamble 的简洁回答直接涉及该问题。这一个放大了包含空格的文件名的一些问题。
基本论点: "$@"
是正确的,而 $*
(未引用)几乎总是错误的。这是因为当参数包含空格时,"$@"
可以正常工作,而当参数不包含空格时,其工作方式与 $*
相同。在某些情况下,"$*"
也可以,但 "$@"
通常(但并非总是)在相同的地方工作。未加引号的 $@
和 $*
是等价的(而且几乎总是错误的)。
那么,$*
、$@
、"$*"
和 "$@"
之间有什么区别?它们都与“shell 的所有参数”有关,但它们做不同的事情。不加引号时,$*
和 $@
做同样的事情。他们将每个“单词”(非空白序列)视为一个单独的参数。但是,带引号的形式完全不同:"$*"
将参数列表视为单个空格分隔的字符串,而 "$@"
将参数几乎与在命令行中指定时完全相同。当没有位置参数时,"$@"
完全没有展开; "$*"
扩展为一个空字符串——是的,有区别,尽管很难察觉。在引入(非标准)命令 al
之后,请参阅下面的更多信息。
次要论文:如果您需要处理带有空格的参数,然后将它们传递给其他命令,那么您有时需要非标准工具来协助。 (或者您应该小心地使用数组:"${array[@]}"
的行为类似于 "$@"
。)
例子:
$ mkdir "my dir" anotherdir
$ ls
anotherdir my dir
$ cp /dev/null "my dir/my file"
$ cp /dev/null "anotherdir/myfile"
$ ls -Fltr
total 0
drwxr-xr-x 3 jleffler staff 102 Nov 1 14:55 my dir/
drwxr-xr-x 3 jleffler staff 102 Nov 1 14:55 anotherdir/
$ ls -Fltr *
my dir:
total 0
-rw-r--r-- 1 jleffler staff 0 Nov 1 14:55 my file
anotherdir:
total 0
-rw-r--r-- 1 jleffler staff 0 Nov 1 14:55 myfile
$ ls -Fltr "./my dir" "./anotherdir"
./my dir:
total 0
-rw-r--r-- 1 jleffler staff 0 Nov 1 14:55 my file
./anotherdir:
total 0
-rw-r--r-- 1 jleffler staff 0 Nov 1 14:55 myfile
$ var='"./my dir" "./anotherdir"' && echo $var
"./my dir" "./anotherdir"
$ ls -Fltr $var
ls: "./anotherdir": No such file or directory
ls: "./my: No such file or directory
ls: dir": No such file or directory
$
为什么那行不通?它不起作用,因为 shell 在扩展变量之前会处理引号。因此,要让 shell 注意到 $var
中嵌入的引号,您必须使用 eval
:
$ eval ls -Fltr $var
./my dir:
total 0
-rw-r--r-- 1 jleffler staff 0 Nov 1 14:55 my file
./anotherdir:
total 0
-rw-r--r-- 1 jleffler staff 0 Nov 1 14:55 myfile
$
当您有诸如“He said, "Don't do this!"
”之类的文件名(带有引号、双引号和空格)时,这会变得非常棘手。
$ cp /dev/null "He said, \"Don't do this!\""
$ ls
He said, "Don't do this!" anotherdir my dir
$ ls -l
total 0
-rw-r--r-- 1 jleffler staff 0 Nov 1 15:54 He said, "Don't do this!"
drwxr-xr-x 3 jleffler staff 102 Nov 1 14:55 anotherdir
drwxr-xr-x 3 jleffler staff 102 Nov 1 14:55 my dir
$
shell(所有这些)并没有使处理这些东西特别容易,所以(很有趣)许多 Unix 程序不能很好地处理它们。在 Unix 上,文件名(单个组件)可以包含除斜杠和 NUL '\0'
之外的任何字符。但是,shell 强烈建议在路径名中的任何位置都不要使用空格、换行符或制表符。这也是标准 Unix 文件名不包含空格等的原因。
在处理可能包含空格和其他麻烦字符的文件名时,您必须非常小心,而且我很久以前就发现我需要一个在 Unix 上不标准的程序。我称之为 escape
(版本 1.1 的日期为 1989-08-23T16:01:45Z)。
以下是使用中的 escape
示例 - 与 SCCS 控制系统一起使用。它是一个封面脚本,同时执行 delta
(想想 check-in)和 get
(想想 check-out)。各种参数,尤其是 -y
(您进行更改的原因)将包含空格和换行符。请注意,该脚本可追溯到 1992 年,因此它使用反引号而不是 $(cmd ...)
表示法,并且在第一行不使用 #!/bin/sh
。
: "@(#)$Id: delget.sh,v 1.8 1992/12/29 10:46:21 jl Exp $"
#
# Delta and get files
# Uses escape to allow for all weird combinations of quotes in arguments
case `basename $0 .sh` in
deledit) eflag="-e";;
esac
sflag="-s"
for arg in "$@"
do
case "$arg" in
-r*) gargs="$gargs `escape \"$arg\"`"
dargs="$dargs `escape \"$arg\"`"
;;
-e) gargs="$gargs `escape \"$arg\"`"
sflag=""
eflag=""
;;
-*) dargs="$dargs `escape \"$arg\"`"
;;
*) gargs="$gargs `escape \"$arg\"`"
dargs="$dargs `escape \"$arg\"`"
;;
esac
done
eval delta "$dargs" && eval get $eflag $sflag "$gargs"
(这些天我可能不会如此彻底地使用转义 - 例如,-e
参数不需要它 - 但总的来说,这是我使用 escape
的更简单的脚本之一。)
escape
程序只是输出它的参数,就像 echo
那样,但它确保参数受到保护以与 eval
一起使用(一级 eval
;我确实有一个执行远程 shell 执行的程序,并且需要转义 escape
的输出)。
$ escape $var
'"./my' 'dir"' '"./anotherdir"'
$ escape "$var"
'"./my dir" "./anotherdir"'
$ escape x y z
x y z
$
我有另一个名为 al
的程序,它每行列出一个参数(它甚至更古老:1.1 版,日期为 1987-01-27T14:35:49)。它在调试脚本时最有用,因为它可以插入命令行以查看实际传递给命令的参数。
$ echo "$var"
"./my dir" "./anotherdir"
$ al $var
"./my
dir"
"./anotherdir"
$ al "$var"
"./my dir" "./anotherdir"
$
[已添加: 现在为了展示各种 "$@"
符号之间的区别,这里还有一个示例:
$ cat xx.sh
set -x
al $@
al $*
al "$*"
al "$@"
$ sh xx.sh * */*
+ al He said, '"Don'\''t' do 'this!"' anotherdir my dir xx.sh anotherdir/myfile my dir/my file
He
said,
"Don't
do
this!"
anotherdir
my
dir
xx.sh
anotherdir/myfile
my
dir/my
file
+ al He said, '"Don'\''t' do 'this!"' anotherdir my dir xx.sh anotherdir/myfile my dir/my file
He
said,
"Don't
do
this!"
anotherdir
my
dir
xx.sh
anotherdir/myfile
my
dir/my
file
+ al 'He said, "Don'\''t do this!" anotherdir my dir xx.sh anotherdir/myfile my dir/my file'
He said, "Don't do this!" anotherdir my dir xx.sh anotherdir/myfile my dir/my file
+ al 'He said, "Don'\''t do this!"' anotherdir 'my dir' xx.sh anotherdir/myfile 'my dir/my file'
He said, "Don't do this!"
anotherdir
my dir
xx.sh
anotherdir/myfile
my dir/my file
$
请注意,没有任何内容会保留命令行上 *
和 */*
之间的原始空白。另外,请注意,您可以使用以下命令更改 shell 中的“命令行参数”:
set -- -new -opt and "arg with space"
这设置了 4 个选项,“-new
”、“-opt
”、“and
”和“arg with space
”。
]
嗯,这是一个相当长的答案 - 也许exegesis 是更好的术语。 escape
的源代码可应要求提供(通过电子邮件发送至 gmail dot com 的 firstname dot lastname)。 al
的源代码非常简单:
#include <stdio.h>
int main(int argc, char **argv)
{
while (*++argv != 0)
puts(*argv);
return(0);
}
就这样。它相当于 Robert Gamble 展示的 test.sh
脚本,并且可以编写为 shell 函数(但当我第一次编写 al
时,本地版本的 Bourne shell 中不存在 shell 函数)。
另请注意,您可以将 al
编写为简单的 shell 脚本:
[ $# != 0 ] && printf "%s\n" "$@"
需要条件,以便在不传递任何参数时不产生输出。 printf
命令将生成一个仅包含格式字符串参数的空行,但 C 程序不生成任何内容。
请注意,罗伯特的答案是正确的,它也适用于 sh
。您可以(可移植地)进一步简化它:
for i in "$@"
相当于:
for i
即,你不需要任何东西!
测试($
是命令提示符):
$ set a b "spaces here" d
$ for i; do echo "$i"; done
a
b
spaces here
d
$ for i in "$@"; do echo "$i"; done
a
b
spaces here
d
我首先在 Kernighan 和 Pike 的 Unix Programming Environment 中读到了这一点。
在 bash
中,help for
记录了这一点:
for NAME [in WORDS ... ;] 执行命令;完成如果'在单词中......;'不存在,则假定为 'in "$@"'。
"$@"
是什么意思,一旦你知道 for i
是什么意思,它的可读性不亚于 for i in "$@"
。
for i
更好,因为它节省了击键。我比较了 for i
和 for i in "$@"
的可读性。
对于简单的情况,您也可以使用 shift
。它将参数列表视为队列。每个 shift
都会抛出第一个参数,并且每个剩余参数的索引都会递减。
#this prints all arguments
while test $# -gt 0
do
echo "$1"
shift
done
shift
,则该元素仍然是数组的一部分,而使用此 shift
可以按预期工作。
echo "$1"
来保留参数字符串值中的间距。此外,(一个小问题)这是破坏性的:如果你把它们全部移开,你就不能重用它们。通常,这不是问题——无论如何你只处理一次参数列表。
--file myfile.txt
因此,$1 是参数,$2 是值,当您需要跳过另一个参数时调用 shift 两次
您还可以将它们作为数组元素访问,例如,如果您不想遍历所有这些元素
argc=$#
argv=("$@")
for (( j=0; j<argc; j++ )); do
echo "${argv[j]}"
done
./my-script.sh param1 "param2 is a quoted param"
的东西: - 使用 argv=($@) 你会得到 [param1, param2], - 使用 argv=("$@"),你会得到 [param1, param2 是一个带引号的参数]
对 $#(参数变量的数量)进行循环也可以。
#! /bin/bash
for ((i=1; i<=$#; i++))
do
printf "${!i}\n"
done
./test.sh 1 2 '3 4'
输出:
1
2
3 4
放大 baz 的答案,如果您需要使用索引枚举参数列表(例如搜索特定单词),您可以在不复制列表或对其进行变异的情况下执行此操作。
假设您想在双破折号(“--”)处拆分参数列表,并将破折号之前的参数传递给一个命令,并将破折号之后的参数传递给另一个命令:
toolwrapper() {
for i in $(seq 1 $#); do
[[ "${!i}" == "--" ]] && break
done || return $? # returns error status if we don't "break"
echo "dashes at $i"
echo "Before dashes: ${@:1:i-1}"
echo "After dashes: ${@:i+1:$#}"
}
结果应如下所示:
$ toolwrapper args for first tool -- and these are for the second
dashes at 5
Before dashes: args for first tool
After dashes: and these are for the second
aparse() {
while [[ $# > 0 ]] ; do
case "$1" in
--arg1)
varg1=${2}
shift
;;
--arg2)
varg2=true
;;
esac
shift
done
}
aparse "$@"
[ $# != 0 ]
更好。在上面,#$
应该是 $#
,如 while [[ $# > 0 ]] ...
getopt 在脚本中使用命令来格式化任何命令行选项或参数。
#!/bin/bash
# Extract command line options & values with getopt
#
set -- $(getopt -q ab:cd "$@")
#
echo
while [ -n "$1" ]
do
case "$1" in
-a) echo "Found the -a option" ;;
-b) param="$2"
echo "Found the -b option, with parameter value $param"
shift ;;
-c) echo "Found the -c option" ;;
--) shift
break ;;
*) echo "$1 is not an option";;
esac
shift
"$@"
的其他优点之一是它在没有位置参数时扩展为空,而"$*"
扩展为空字符串 - 是的,没有参数和一个空参数之间存在差异。请参阅${1:+"$@"} in
/bin/sh`。shift
丢弃$1
,并向下移动所有后续元素。bash -c 'echo "$@"' a b
只显示b
时,这可能会让您感到惊讶。