如何迭代 Bash 脚本中的参数

Posted

技术标签:

【中文标题】如何迭代 Bash 脚本中的参数【英文标题】:How to iterate over arguments in a Bash script 【发布时间】:2010-09-20 07:58:08 【问题描述】:

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

foo $1 args -o $1.ext

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

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

【问题讨论】:

仅供参考,始终将参数放在引号内是个好主意。你永远不知道什么时候一个参数可能包含一个嵌入的空间。 【参考方案1】:

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

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

这将遍历每个参数并将其打印在单独的行上。 $@ 的行为与 $* 类似,只是在引用时,如果参数中有空格,则它们会被正确分解:

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

【讨论】:

顺便说一下,"$@" 的其他优点之一是它在没有位置参数时扩展为空,而 "$*" 扩展为空字符串 - 是的,两者之间存在差异没有参数和一个空参数。见$1:+"$@" in /bin/sh`。 请注意,这个符号也应该在 shell 函数中用于访问函数的所有参数。 如何从第二个参数开始?我的意思是,我需要这个,但总是从第二个参数开始,即跳过 $1。 @m4l490n: shift 抛弃$1,并向下移动所有后续元素。 应该注意(根据 bash 手册页)第一个位置参数 ($0) 不包含在 $@ 或 "$@" 中。这通常是您在执行脚本 file 时想要的,但在执行 bash -c 'echo "$@"' a b 时可能会让您感到惊讶,只显示 b【参考方案2】:

用VonC重写现在删除的answer。

Robert Gamble 的简洁回答直接解决了这个问题。 这一个放大了包含空格的文件名的一些问题。

另请参阅:$1:+"$@" in /bin/sh

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

那么,$*$@"$*""$@" 之间有什么区别?它们都与“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 注意到 $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 上,文件名(单个组件)可以包含任何字符,除了 斜线和 NUL '\0'。 但是,shell 强烈建议不要使用空格、换行符或制表符 路径名称中的任何位置。 这也是为什么标准 Unix 文件名不包含空格等原因。

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

以下是使用中的escape 示例 - 与 SCCS 控制系统一起使用。 这是一个封面脚本,它同时执行 delta(想想 check-in)和 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 确实如此,但它确保参数受到保护以便与 evaleval 的一级;我确实有一个执行远程 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 的姓氏)。 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 程序什么也不产生。

【讨论】:

【参考方案3】:

请注意,罗伯特的答案是正确的,它也适用于 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 中读到了这一点。

bashhelp for 中记录了这个:

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

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

【讨论】:

我不同意。你必须知道神秘的"$@" 是什么意思,一旦你知道for i 是什么意思,它的可读性不亚于for i in "$@" 我没有断言for i 更好,因为它节省了击键次数。我比较了for ifor i in "$@"的可读性。 这就是我一直在寻找的——他们在不必明确引用它的循环中将这种假设称为 $@ 是什么?不使用 $@ 引用有缺点吗?【参考方案4】:

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

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

【讨论】:

我认为这个答案更好,因为如果您在 for 循环中执行 shift,则该元素仍然是数组的一部分,而使用此 shift 可以按预期工作。 请注意,您应该使用echo "$1" 来保留参数字符串值中的间距。此外,(一个小问题)这是破坏性的:如果你把它们全部移开,你就不能重用它们。通常,这不是问题——无论如何你只处理一次参数列表。 同意 shift 更好,因为这样您就可以轻松地在 switch case 中获取两个参数来处理诸如“-o outputfile”之类的标志参数。 另一方面,如果您开始解析标志参数,您可能需要考虑比 bash 更强大的脚本语言 :) 如果你需要一些参数值,这个解决方案会更好,例如--file myfile.txt 所以,$1 是参数,$2 是值,当你需要跳过另一个参数时,你调用 shift 两次 【参考方案5】:

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

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

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

【讨论】:

我相信行 argv=($@) 应该是 argv=("$@") 其他明智的带空格的 args 没有正确处理 我确认正确的语法是 argv=("$@")。除非如果您有带引号的参数,您将不会获得最后一个值。例如,如果你使用类似./my-script.sh param1 "param2 is a quoted param" 的东西: - 使用 argv=($@) 你会得到 [param1, param2], - 使用 argv=("$@"),你会得到 [param1, param2 是一个带引号的参数]【参考方案6】:

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

#! /bin/bash

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

输出:

1
2
3 4

【讨论】:

是什么!意思是?我看到它调用了存储在第 i 个参数中的内容,而不是打印一个数字……它是间接引用吗? @SkyScraper 它被称为indirect expansion 并且意味着参数被扩展了两次,例如。从$!i$3'3 4'【参考方案7】:

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

【讨论】:

【参考方案8】:
aparse() 
while [[ $# > 0 ]] ; do
  case "$1" in
    --arg1)
      varg1=$2
      shift
      ;;
    --arg2)
      varg2=true
      ;;
  esac
  shift
done


aparse "$@"

【讨论】:

如下所述,[ $# != 0 ] 更好。在上面,#$ 应该是 $#,如 while [[ $# &gt; 0 ]] ...【参考方案9】:

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

以上是关于如何迭代 Bash 脚本中的参数的主要内容,如果未能解决你的问题,请参考以下文章

从 bash 中的 $@ 中删除第一个元素 [重复]

如何计算以下 #Vus、AVG 事务时间、loadrunner 中多个脚本的迭代?

¿ 如何使用 bash 迭代西班牙的邮政编码? [复制]

为啥我需要另一个迭代器作为 std::copy() 中的参数?

性能测试迭代与检查点(loadrunner12.55)

没有参数/可迭代的函数的多处理池?