Bashスクリプトの引数を繰り返し処理する方法

シェル/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
解説 (7)

VonCで削除されたanswerを書き直したものです。 Robert Gamble氏の簡潔な回答は、質問に直接対応しています。 こちらの回答は、スペースを含むファイル名の問題点について補足しています。 こちらもご覧ください。${1:+"$@"} in /bin/sh を参照してください。 基本的な考え方: "$@" は正しく、$* (unquoted) はほとんどの場合間違っています。 これは、"$@" は、引数にスペースが含まれていても問題なく動作し、$* と同じように動作するからです。 は、スペースが含まれていない場合は、$*と同じように動作するからです。 状況によっては、"$*"もOKですが、"$@"は通常(常にではありませんが)同じ場所で動作します。 は通常(常にではありませんが)同じ場所で動作します。 引用されていない、$@$*は同等です(そしてほとんどの場合間違っています)。 では、$*, $@, "$*", "$@" の違いは何でしょうか? これらはすべて、'シェルへのすべての引数'に関連していますが、それぞれ異なることをします。引用されていないとき、$*$@は同じことをします。 それぞれの 'word'(空白以外の連続した文字)を個別の引数として扱います。 一方、 "$@" は、コマンドラインで指定されたときとほぼ同じように引数を扱います。 "$@"は、位置引数がないときは何もない状態に展開され、"$*"は、空の文字列に展開されます — と、そう、違いがあるのですが、それを認識するのは難しいかもしれません。 詳しくは以下の、(非標準の)コマンド al の導入後にご覧ください。 第二のテーゼ: スペースを含む引数を処理して、他のコマンドに渡す必要がある場合は 他のコマンドに渡す必要がある場合、それを支援する非標準のツールが必要になることがあります。 助けるための非標準的なツールが必要になることがあります。(あるいは、慎重に配列を使うべきです。"${array[@]}"は、"$@"と似たような動作をします)。 Example:

    $ 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 を使わなければなりません。

    $ 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.のようなファイル名がある場合に非常に厄介です。 のようなファイル名があると、本当に厄介です。

    $ 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'以外の文字を含むことができます。 しかし、シェルではパス名のどこにもスペースや改行、タブを入れないことを強く推奨しています。 パス名のどこにもスペースや改行、タブを入れないことを強く推奨しています。 標準的なUnixのファイル名にスペースなどが含まれていないのもそのためです。 スペースやその他の厄介な文字を含む可能性のあるファイル名を扱う際には ファイル名を扱う際には、細心の注意を払わなければなりません。 私はずっと前に、Unixでは標準ではないプログラムが必要であることに気づきました。 私はこれをescapeと呼んでいます(バージョン1.1の日付は1989-08-23T16:01:45Z)。 ここでは、SCCS制御システムでのescapeの使用例を紹介します。 これは、delta (check-inと考えてください) と get (check-outと考えてください) の両方を行うカバースクリプトです。 get(check-outと考えてください)の両方を行うカバースクリプトです。 様々な引数、特に -y (変更を行った理由) にはブランクや改行が含まれます。 なお、このスクリプトは1992年のものなので、以下のようにバックティックを使用しています。 また、1 行目に #!/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が行うように、単にその引数を出力します。 しかし、escapeプログラムは、echoがするように、単に引数を出力しますが、その引数が しかし、evalで使用するために、引数が保護されていることを保証します(evalの1レベル。 しかし、引数はeval` で使用するために保護されています。)

    $ 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"
    $

[Added: さて、さまざまな "$@" 表記の違いを示すために、もうひとつ例を挙げてみましょう。

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

コマンドラインの **/* の間のオリジナルのブランクは何も保存されていないことに注意してください。 また、シェルで 'コマンドライン引数'を変更するには次のようにすることに注意してください。

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

これは4つのオプション、 '-new'、 '-opt'、 'and'、 'arg with space'を設定します。 となっています。 ] うーん、かなり長い回答ですね。 リクエストがあれば、escapeのソースコードを提供します (e-mail to firstname dot lastname at gmail dot com)。 al`のソースコードは信じられないほどシンプルです。

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

以上である。 これは、Robert Gamble氏が示したtest.shスクリプトと同等のもので、シェル関数として書くこともできます(ただし、私が最初にalを書いたときには、ローカル版のBourne shellにはシェル関数は存在していませんでした)。 また、alを単純なシェルスクリプトとして書くことができることにも注意してください。

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

引数を渡されなかったときに何も出力しないようにするために、条件付きが必要です。 printf` コマンドはフォーマット文字列の引数だけで空行を生成しますが、C プログラムは何も生成しません。

解説 (1)

なお、Robert'の答えは正しく、shでも動作します。 さらに(移植可能な形で)単純化することもできます。

for i in "$@"

は次のようになります。

for i

I.e. あなたは何も必要ありません!

テスト($はコマンドプロンプト)。

$ 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では、help for`にこのことが書かれています。

for NAME [in WORDS ... ;] do COMMANDS; done. となっています。 'in WORDS ...;'が存在しない場合、'in "$@"'が想定されます。

解説 (10)