如何遍历Bash脚本中的参数


899

我有一个复杂的命令,我想制作一个shell / bash脚本。我可以$1轻松地编写它:

foo $1 args -o $1.ext

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

而且,当然,我想处理文件名中带有空格的文件名。


67
仅供参考,始终将参数放在引号中是一个好主意。您永远不知道何时parm可能包含嵌入式空间。
David R Tribble,

Answers:


1478

使用"$@"来代表所有的参数:

for var in "$@"
do
    echo "$var"
done

这将遍历每个参数并将其打印在单独的行上。$ @的行为与$ *相似,不同之处在于,如果使用引号将参数括起来,则它们之间如果有空格,则会被正确分解:

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

38
顺便说一句,的另一个优点"$@"是,当没有位置参数时,它扩展为零,而"$*"扩展为空字符串-是的,没有参数和一个空参数之间是有区别的。参见${1:+"$@"} in / bin / sh`
Jonathan Leffler 2014年

9
注意,在shell函数中也应使用此符号来访问函数的所有参数。
乔纳森·莱夫勒

通过删除双引号:$var我能够执行参数。
eonist

我如何从第二个参数开始?我的意思是,我需要这个,但总是从第二个参数开始,即跳过$ 1。
m4l490n

4
@ m4l490n:shift抛弃$1,并向下移动所有后续元素。
MSalters,

239

重写现在缺失的答案VonC

Robert Gamble的简洁回答直接涉及该问题。这在文件名中包含空格的某些问题上有所放大。

另请参见:/ bin / sh中的$ {1:+“ $ @”}

基本论点: "$@"是正确的,并且$*(未加引号)几乎总是错误的。这是因为"$@"当参数包含空格时,效果很好,而当参数不包含空格时,效果相同$*。在某些情况下"$*"也可以,但是"$@"通常(但并非总是)在相同的地方工作。未报价,$@并且$*是等效的(并且几乎总是错误的)。

那么,是什么样的区别$*$@"$*",和"$@"?它们都与“ 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
    $

为什么不起作用?它不起作用,因为外壳程序在扩展变量之前先处理引号。因此,要使外壳程序注意嵌入在其中的引号$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
    $ 

外壳程序(所有外壳程序)并没有使其特别容易处理,因此(很有趣)许多Unix程序在处理它们方面做得不好。在Unix上,文件名(单个组件)可以包含除slash和NUL之外的任何字符'\0'。但是,shell强烈建议在路径名中的任何地方都不包含空格,换行符或制表符。这也是为什么标准Unix文件名不包含空格等的原因。

在处理可能包含空格和其他麻烦字符的文件名时,您必须格外小心,我很久以前发现我需要一个在Unix上不是标准的程序。我称之为escape(1.1版的日期为1989-08-23T16:01:45Z)。

这是escapeSCCS控制系统正在使用的示例。它是一个封面脚本,它同时执行delta(认为签入)和 get(认为签出)。各种参数,特别是-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,该程序每行列出一个参数(甚至更古老:版本1987-01-27T14:35:49的1.1版)。它在调试脚本时最有用,因为可以将其插入命令行以查看实际将哪些参数传递给命令。

    $ 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”。
]

嗯,这是一个很长的答案 -也许训ege是更好的说法。escape可根据要求提供源代码(在gmail dot com上发送电子邮件至firstname dot lastname)。的源代码al非常简单:

#include <stdio.h>
int main(int argc, char **argv)
{
    while (*++argv != 0)
        puts(*argv);
    return(0);
}

就这样。它等效于test.shRobert Gamble展示的脚本,可以作为shell函数编写(但是,当我第一次编写时,shell函数在本地版本的Bourne shell中并不存在al)。

另请注意,您可以将其编写al为简单的Shell脚本:

[ $# != 0 ] && printf "%s\n" "$@"

需要该条件,以便在不传递任何参数时不产生任何输出。该printf命令将只使用格式字符串参数产生空白行,但是C程序不产生任何内容。


132

请注意,罗伯特的答案是正确的,它也适用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编程环境中读到的。

在中bashhelp for记录以下内容:

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

如果'in WORDS ...;'不存在,则'in "$@"'假定为。


4
我不同意。人们必须知道神秘的"$@"含义,一旦知道了什么for i意思,它的可读性就不比for i in "$@"
阿洛克·辛哈尔

10
我没有断言这样做for i会更好,因为它可以节省击键次数。我比较了for i和的可读性for i in "$@"
Alok Singhal

这就是我一直在寻找的东西-在不需要显式引用$ @的循环中,他们如何称呼$ @的假设?不使用$ @引用有不利之处吗?
qodeninja

58

对于简单的情况,您也可以使用shift。它将参数列表视为队列。每个shift参数都将第一个参数抛出,并且其余每个参数的索引都会递减。

#this prints all arguments
while test $# -gt 0
do
    echo "$1"
    shift
done

我认为这个答案更好,因为如果shift在for循环中执行操作,则该元素仍然是数组的一部分,而shift按预期工作。
DavidG 2013年

2
请注意,您应该使用echo "$1"保留参数字符串值中的间距。另外,(一个小问题)这是破坏性的:如果您已将所有参数都移开了,则无法重用这些参数。通常,这不是问题-无论如何,您只处理一次参数列表。
乔纳森·勒夫勒

1
同意这种转换会更好,因为这样您就可以轻松地在切换案例中获取两个参数,以处理诸如“ -o outputfile”之类的标志参数。
Baxissimo

另一方面,如果您开始解析标志参数,则可能需要考虑使用一种比bash更为强大的脚本语言:)
nuoritoveri

4
如果您需要一些参数值,--file myfile.txt 那么此解决方案会更好,例如,$ 1是参数,$ 2是值,并且需要跳过另一个参数时调用shift两次
Daniele Licitra

16

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

argc=$#
argv=("$@")

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

5
我认为argv =($ @)行应为argv =(“ $ @”)其他带有空格的明智args不能正确处理
kdubs

我确认正确的语法为argv =(“ $ @”)。除非您有带引号的参数,否则不会获得最后的值。例如,如果您使用以下命令./my-script.sh param1 "param2 is a quoted param":-使用argv =($ @),您将获得[param1,param2],-使用argv =(“ $ @”),您将获得[param1,param2是带引号的参数]
jseguillon

2
aparse() {
while [[ $# > 0 ]] ; do
  case "$1" in
    --arg1)
      varg1=${2}
      shift
      ;;
    --arg2)
      varg2=true
      ;;
  esac
  shift
done
}

aparse "$@"

1
如下所述[ $# != 0 ],更好。在上面,#$$#while [[ $# > 0 ]] ...
hute37 '16

1

扩大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

1

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

棘手的人,我将对此进行彻底的研究。示例:file:///usr/share/doc/util-linux/examples/getopt-parse.bash,手册:man.jimmylandstudios.xyz/?man=getopt
JimmyLandStudios
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.