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
ロバートギャンブルの簡潔な答えは直接質問に対処します。これはスペースを含むファイル名に関するいくつかの問題を増幅します。
/ 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プログラムは何も生成しません。
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 "$@"'
が想定されます。
単純な場合にはshift
を使うこともできます。引数リストをキューのように扱い、それぞれのshift
は最初の引数を送出し、残っている各引数の数は減らされます。
#this prints all arguments
while test $# -gt 0
do
echo $1
shift
done
たとえば、それらすべてを反復処理したくない場合は、配列要素としてアクセスすることもできます。
argc=$#
argv=($@)
for (( j=0; j<argc; j++ )); do
echo ${argv[j]}
done
aparse() {
while [[ $# > 0 ]] ; do
case "$1" in
--arg1)
varg1=${2}
shift
;;
--arg2)
varg2=true
;;
esac
shift
done
}
aparse "$@"