协慌网

登录 贡献 社区

如何在 Bash 脚本中迭代参数

我有一个复杂的命令,我想制作一个 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

重写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
    $

为什么不起作用?它不起作用,因为 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 (思考签到 )和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"

(这些天我可能不会非常彻底地使用 escape - 例如, -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 ”。
]

嗯,这是一个相当长的答案 - 也许解释是更好的术语。可根据要求提供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 编程环境中读到过这个。

bashhelp for文档:

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

如果'in WORDS ...;'不存在,然后'in "$@"'被假设。