关于命令行:如何迭代bash脚本中的参数

How to iterate over arguments in a Bash script

我有一个复杂的命令,我想制作一个shell/bash脚本。我可以很容易地用$1来写:

1
foo $1 args -o $1.ext

我希望能够向脚本传递多个输入名称。正确的方法是什么?

当然,我想处理文件名中包含空格。


使用"$@"表示所有参数:

1
2
3
4
for var in"$@"
do
    echo"$var"
done

这将迭代每个参数,并在单独的行上打印出来。$@的行为与$*类似,但在引用时,如果参数中有空格,则这些参数将被正确地分解:

1
2
3
4
sh test.sh 1 2 '3 4'
1
2
3 4


重写VONC现在删除的答案。好的。

罗伯特·甘布尔简洁的回答直接涉及到这个问题。这篇文章进一步阐述了文件名中包含空格的一些问题。好的。

另请参见:$1:+"$@"in/bin/sh好的。

基础理论:"$@"是正确的,$*几乎总是错误的。这是因为当参数包含空格时,"$@"工作正常,并且当它们不工作时,工作原理与$*相同。在某些情况下,"$*"也可以,但"$@"通常(但不是始终)在相同的地方工作。未经引用,$@$*是等效的(几乎总是错误的)。好的。

那么,$*$@"$*""$@"之间有什么区别?它们都与"外壳的所有论据"有关,但它们的作用不同。当没有引用时,$*$@也做同样的事情。他们将每个"单词"(非空白序列)视为单独的参数。不过,引用的形式有很大的不同:"$*"将参数列表视为一个单独的空格分隔字符串,而"$@"则将参数视为命令行上指定的参数。当没有位置参数时,"$@"完全不展开;"$*"展开为空字符串—是的,有区别,尽管很难察觉。在引入(非标准)命令al之后,请参阅下面的更多信息。好的。

第二篇论文:如果你需要用空格来处理参数,然后将它们传递给其他命令,然后您有时需要非标准的辅助工具。(或者您应该小心地使用数组:"${array[@]}"的行为与"$@"相似。)好的。

例子:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    $ 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在展开前处理引号变量。因此,为了让壳牌公司注意嵌入在$var中的报价,您必须使用eval:好的。

1
2
3
4
5
6
7
8
9
    $ 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
    $

当您有诸如"EDOCX1"〔23〕之类的文件名(带引号、双引号和空格)时,这会变得非常棘手。好的。

1
2
3
4
5
6
7
8
9
    $ 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
    $

贝壳(所有的)并不特别容易处理一些东西,所以(有趣的是)许多Unix程序不能很好地完成处理它们。在UNIX上,文件名(单个组件)可以包含除Slash和Nul '\0'。然而,外壳强烈地鼓励不使用空格、换行符或制表符。路径名中的任何位置。这也是标准UNIX文件名不包含空格等的原因。好的。

处理可能包含空格和其他麻烦的人物,你必须非常小心,我发现很久以前,我需要一个在Unix上不是标准的程序。我把它叫做escape(版本1.1是1989-08-23t16:01:45z)。好的。

下面是一个使用中的escape与SCCS控制系统的示例。它是一个封面脚本,可以同时执行delta(think check-in)和get(考虑退房)。各种各样的论据,特别是-y(你做出改变的原因)将包含空格和换行符。请注意,脚本的日期是1992年,因此它使用反勾号而不是$(cmd ...)符号,第一行不使用#!/bin/sh。好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
:  "@(#)$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执行,这需要避开EDOCX1的输出(25)。好的。

1
2
3
4
5
6
7
    $ escape $var
    '"./my' 'dir"' '"./anotherdir"'
    $ escape"$var"
    '"./my dir""./anotherdir"'
    $ escape x y z
    x y z
    $

我还有一个叫做al的程序,它每行列出一个参数。(更古老的是:1987-01-27T14:35:49的版本1.1)。它在调试脚本时最有用,因为它可以插入命令行查看实际传递给命令的参数。好的。

1
2
3
4
5
6
7
8
9
    $ echo"$var"
   "./my dir""./anotherdir"
    $ al $var
   "./my
    dir"

   "./anotherdir"
    $ al"$var"
   "./my dir""./anotherdir"
    $

[补充:为了说明不同的"$@"符号之间的区别,下面再举一个例子:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
$ 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中的"命令行参数":好的。

1
set -- -new -opt and"arg with space"

这设置了4个选项:"-new、"-opt、"and和"arg with space。]好的。

嗯,这是一个相当长的答案-也许释义是更好的术语。根据要求提供escape的源代码(电子邮件至firstname dot姓gmail.com)。al的源代码非常简单:好的。

1
2
3
4
5
6
7
#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脚本:好的。

1
2
[ $# != 0 ] && printf"%s
"
"$@"

需要条件,以便在不传递参数时不生成输出。printf命令将生成一个只有格式字符串参数的空行,但C程序不生成任何内容。好的。好啊。


请注意,罗伯特的回答是正确的,它在sh中也有效。您可以(便携地)进一步简化它:

1
for i in"$@"

相当于:

1
for i

也就是说,你什么都不需要!

测试($为命令提示):

1
2
3
4
5
6
7
8
9
10
11
$ 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

我第一次在Unix编程环境中读到这篇文章,作者是Kernighan和Pike。

bash中,help for记录了:

for NAME [in WORDS ... ;] do COMMANDS; done

If 'in WORDS ...;' is not present, then 'in"$@"' is assumed.


对于简单的情况,也可以使用shift。它将参数列表视为一个队列,每个shift抛出第一个参数,左边的每个参数的数目是递减的。

1
2
3
4
5
6
#this prints all arguments
while test $# -gt 0
do
    echo $1
    shift
done


您还可以作为数组元素访问它们,例如,如果您不想遍历所有这些元素

1
2
3
4
5
6
argc=$#
argv=($@)

for (( j=0; j<argc; j++ )); do
    echo ${argv[j]}
done


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
aparse() {
while [[ $# > 0 ]] ; do
  case"$1" in
    --arg1)
      varg1=${2}
      shift
      ;;
    --arg2)
      varg2=true
      ;;
  esac
  shift
done
}

aparse"$@"