web-dev-qa-db-ja.com

Bashスクリプトの引数を反復処理する方法

Shell/bashスクリプトを作成したいという複雑なコマンドがあります。 $1という言葉で簡単に書くことができます。

foo $1 args -o $1.ext

スクリプトに複数の入力名を渡すことができるようにしたいです。それをする正しい方法は何ですか?

そしてもちろん、ファイル名をスペースで処理したいのです。

772
Thelema

すべての引数を表すには"$@"を使用します。

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

これは各引数を反復して別々の行に表示します。 $ @は$ *と同じように振る舞いますが、引用符で囲まれたときにスペースがあると引数が正しく分割される点が異なります。

sh test.sh 1 2 '3 4'
1
2
3 4
1264
Robert Gamble

今削除された 回答 による VonC の書き換え

ロバートギャンブルの簡潔な答えは直接質問に対処します。これはスペースを含むファイル名に関するいくつかの問題を増幅します。

/ bin/shの $ {1:+ "$ @"}も参照してください

基礎論文: "$@"は正しく、$*(引用符なし)はほとんど間違いです。これは、引数にスペースが含まれていると"$@"が正常に機能し、含まれていない場合は$*と同じように機能するためです。状況によっては、"$*"でも構いませんが、"$@"は通常(常にではありません)同じ場所で機能します。引用符で囲まれていない、$@$*は同等です(そしてほとんどいつも間違っています)。

それで、$*$@"$*"、および"$@"の違いは何ですか?それらはすべて「シェルへのすべての引数」に関連していますが、異なることをします。引用符で囲まないと、$*$@は同じことをします。それらはそれぞれの 'Word'(空白でないシーケンス)を別々の引数として扱います。ただし、引用符で囲まれた形式はまったく異なります。"$*"は引数リストを単一のスペース区切り文字列として扱いますが、"$@"は引数をコマンドラインで指定されたときとほぼ同じように扱います。位置引数がない場合、"$@"はまったく何も展開しません。 "$*"は空の文字列に展開されます - そして、確かに違いがありますが、認識するのは難しい場合があります。 (非標準の)コマンド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では、ファイル名(単一の構成要素)はスラッシュとNUL '\0'以外の任意の文字を含むことができます。ただし、シェルはパス名のどこにもスペースや改行やタブを付けないことを強く推奨します。それはまた標準的なUnixファイル名がスペースなどを含まない理由でもあります。

スペースや他の厄介な文字を含むかもしれないファイル名を扱うとき、あなたは細心の注意を払わなければなりません、そして私は私がUnixで標準的でないプログラムを必要とすることをずっと前に見つけました。私はそれをescapeと呼びます(バージョン1.1は1989-08-23T16:01:45Zの日付です)。

これは、SCCS制御システムで使用されているescapeの例です。これはdelta(think check-in)とget(think check-out)の両方を行うカバースクリプトです。さまざまな引数、特に-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を使用した私の簡単なスクリプトの1つです。)

escapeプログラムは、echoとは異なり、単にその引数を出力しますが、eval(1つのレベルのevalで使用するために引数が保護されていることを保証します。 escape)。

    $ escape $var
    '"./my' 'dir"' '"./anotherdir"'
    $ escape "$var"
    '"./my dir" "./anotherdir"'
    $ escape x y z
    x y z
    $ 

私はその引数を1行に1つずつリストするalという別のプログラムを持っています(そしてそれはもっと古く、バージョン1987-01-27T14:35:49)。スクリプトをデバッグするときに最も便利です。コマンドラインにプラグインして、実際にどの引数がコマンドに渡されるのかを確認できるからです。

    $ echo "$var"
    "./my dir" "./anotherdir"
    $ al $var
    "./my
    dir"
    "./anotherdir"
    $ al "$var"
    "./my dir" "./anotherdir"
    $

[Added:そして、さまざまな"$@"表記法の違いを示すために、もう1つ例を示します。

$ 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'を設定します。
]

うーん、それはかなり長いanswer - おそらくexegesisがより良い用語です。 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スクリプトと同等であり、シェル関数として書くことができます(ただし、最初にalを書いたときには、シェル関数はBourne Shellのローカルバージョンにはありませんでした)。

alは単純なシェルスクリプトとして書くこともできます。

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

引数が渡されなかったときに出力が生成されないように、条件式が必要です。 printfコマンドはformat文字列引数のみを持つ空白行を生成しますが、Cプログラムは何も生成しません。

216

Robertの答えは正しいことに注意してください、そしてそれは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プログラミング環境 でこれについて読みました。

bashでは、help forがこれを文書化しています。

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

'in WORDS ...;'が存在しない場合、'in "$@"'が想定されます。

114
Alok Singhal

単純な場合にはshiftを使うこともできます。引数リストをキューのように扱い、それぞれのshiftは最初の引数を送出し、残っている各引数の数は減らされます。

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

たとえば、それらすべてを反復処理したくない場合は、配列要素としてアクセスすることもできます。

argc=$#
argv=($@)

for (( j=0; j<argc; j++ )); do
    echo ${argv[j]}
done
11
baz
aparse() {
while [[ $# > 0 ]] ; do
  case "$1" in
    --arg1)
      varg1=${2}
      shift
      ;;
    --arg2)
      varg2=true
      ;;
  esac
  shift
done
}

aparse "$@"
3
g24l