我有一个复杂的命令,我想做一个shell/bash脚本。我可以很容易地用1美元表示

foo $1 args -o $1.ext

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

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


当前回答

由VonC重写现已删除的答案。

罗伯特·甘伯简洁的回答直接处理了这个问题。 这一点放大了包含空格的文件名的一些问题。

参见/bin/sh中的${1:+"$@"}

基本论点:“$@”是正确的,$*(未引用)几乎总是错的。 这是因为当参数包含空格时,“$@”可以正常工作 如果没有,则与$*的工作相同。 在某些情况下,“$*”也可以,但“$@”通常(但不是 总是)在相同的地方工作。 不加引号的话,$@和$*是等价的(而且几乎总是错的)。

So, what is the difference between $*, $@, "$*", and "$@"? They are all related to 'all the arguments to the shell', but they do different things. When unquoted, $* and $@ do the same thing. They treat each 'word' (sequence of non-whitespace) as a separate argument. The quoted forms are quite different, though: "$*" treats the argument list as a single space-separated string, whereas "$@" treats the arguments almost exactly as they were when specified on the command line. "$@" expands to nothing at all when there are no positional arguments; "$*" expands to an empty string — and yes, there's a difference, though it can be hard to perceive it. See more information below, after the introduction of the (non-standard) command 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
    $ 

当你的文件名是“他说, “别这样!”(加上引号、双引号和空格)。

    $ 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(所有的shell)并不是特别容易处理这种情况 的东西,所以(足够有趣的是)许多Unix程序并没有做得很好 处理它们。 在Unix上,文件名(单个组件)可以包含除 斜杠和NUL '\0'。 但是,shell强烈建议不使用空格、换行符或制表符 路径名称中的任意位置。 这也是为什么标准的Unix文件名不包含空格等。

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

下面是一个转义在SCCS控制系统中的应用示例。 它是一个封面脚本,同时执行delta(考虑签入)和 获得(考虑结帐)。 各种参数,特别是-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'。 ]

嗯,这是一个相当长的答案-也许注释是更好的术语。 源代码转义可请求(电子邮件到firstname点 姓氏在gmail.com)。 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命令将产生一个只有format字符串参数的空行,但是C程序什么也不产生。

其他回答

使用“$@”来表示所有的参数:

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

这将遍历每个参数,并将其打印在单独的行上。$@的行为类似于$*,除了当引用参数时,如果参数中有空格,则会被正确拆分:

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

由VonC重写现已删除的答案。

罗伯特·甘伯简洁的回答直接处理了这个问题。 这一点放大了包含空格的文件名的一些问题。

参见/bin/sh中的${1:+"$@"}

基本论点:“$@”是正确的,$*(未引用)几乎总是错的。 这是因为当参数包含空格时,“$@”可以正常工作 如果没有,则与$*的工作相同。 在某些情况下,“$*”也可以,但“$@”通常(但不是 总是)在相同的地方工作。 不加引号的话,$@和$*是等价的(而且几乎总是错的)。

So, what is the difference between $*, $@, "$*", and "$@"? They are all related to 'all the arguments to the shell', but they do different things. When unquoted, $* and $@ do the same thing. They treat each 'word' (sequence of non-whitespace) as a separate argument. The quoted forms are quite different, though: "$*" treats the argument list as a single space-separated string, whereas "$@" treats the arguments almost exactly as they were when specified on the command line. "$@" expands to nothing at all when there are no positional arguments; "$*" expands to an empty string — and yes, there's a difference, though it can be hard to perceive it. See more information below, after the introduction of the (non-standard) command 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
    $ 

当你的文件名是“他说, “别这样!”(加上引号、双引号和空格)。

    $ 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(所有的shell)并不是特别容易处理这种情况 的东西,所以(足够有趣的是)许多Unix程序并没有做得很好 处理它们。 在Unix上,文件名(单个组件)可以包含除 斜杠和NUL '\0'。 但是,shell强烈建议不使用空格、换行符或制表符 路径名称中的任意位置。 这也是为什么标准的Unix文件名不包含空格等。

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

下面是一个转义在SCCS控制系统中的应用示例。 它是一个封面脚本,同时执行delta(考虑签入)和 获得(考虑结帐)。 各种参数,特别是-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'。 ]

嗯,这是一个相当长的答案-也许注释是更好的术语。 源代码转义可请求(电子邮件到firstname点 姓氏在gmail.com)。 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命令将产生一个只有format字符串参数的空行,但是C程序什么也不产生。

对参数数量变量$#进行循环也可以。

#! /bin/bash

for ((i=1; i<=$#; i++))
do
  printf "${!i}\n"
done
./test.sh 1 2 '3 4'

增长:

1
2
3 4

请注意,Robert的答案是正确的,它也适用于sh。你可以(可移植地)进一步简化它:

for i in "$@"

等价于:

for i

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

测试($ is命令提示符):

$ 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中,帮助文档如下:

对于名字[用语言来说…]do COMMANDS;完成 如果'in WORDS…;'不存在,则假定'in "$@"'。

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

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