スペースやその他の特殊文字が原因でシェルスクリプトが停止するのはなぜですか?

スペースやその他の特殊文字が原因でシェルスクリプトが停止するのはなぜですか?

...または強力なファイル名の処理とシェルスクリプトから渡されるその他の文字列の紹介ガイドです。

私はシェルスクリプトを書いていて、ほとんどの場合うまくいきます。ただし、特定の入力(特定のファイル名など)ではブロックされます。

次の問題が発生しました。

  • スペースを含むファイル名があり、2つの別々のファイルhello worldとして扱われます。helloworld
  • 入力から 1 つに縮小される 2 つの連続した空白の入力行があります。
  • 入力行の前後のスペースは消えます。
  • 場合によっては、入力にこれらの文字の1つが含まれている場合、\[*?実際にはいくつかのファイルの名前であるいくつかのテキストに置き換えられます。
  • '入力にアポストロフィ(または二重引用符)があり、その後は状況が奇妙になります。"
  • 入力にバックスラッシュがあります(代わりに:私はCygwinを使用しており、一部のファイル名にはWindowsスタイルの\区切り文字があります)。

どうなりますか?この問題をどのように解決できますか?

ベストアンサー1

変数の置換とコマンドの置換には常に二重引用符を使用してください"$foo""$(foo)"

引用符なしで使用すると、スクリプトは$fooスペースまたはを含む入力または引数(またはコマンド出力、)をブロックします。$(foo)\[*?

そこから読み取りを中止できます。まあ、ここにいくつかあります:

  • read組み込み関数を使用して入力を1行ずつ読み取るには、次のようにreadします。while IFS= read -r line; do …
    一般はreadバックスラッシュとスペースを特別に扱います。
  • xargs避けるxargs。必ず使用する必要がある場合は、xargsそうしてくださいxargs -0。変えるfind … | xargs好むfind … -exec …
    xargsスペースと文字を特に扱います\"'

この回答はBourne / POSIXスタイルのシェル(、、、、、、、、... )shに適用されます。Zshユーザーはスキップして最後を読んでください。ashdashbashkshmkshyashいつ二重引用符が必要ですか?代わりに。詳しくは、読み取り基準またはシェルマニュアル。


以下の説明にはいくつかのおおよその内容が含まれています(ほとんどの場合、正確ですが周囲の状況や構成によって影響を受ける可能性があります)。

なぜ書くのですか"$foo"?引用符がない場合はどうなりますか?

$foo「変数の値を取得するfoo」という意味ではありません。これはより複雑なことを意味します。

  • まず、変数の値を取得します。
  • フィールド分割:値をスペースで区切られたフィールドのリストとして処理し、結果のリストを作成します。たとえば、変数に以下が含まれている場合、foo * bar ​このステップの結果は3つの要素のリストfoo、、、*ですbar
  • ファイル名の生成:各フィールドをグローバル変数、つまりワイルドカードパターンとして処理し、そのパターンと一致するファイル名のリストに置き換えます。パターンがどのファイルとも一致しない場合は変更されません。この例では、を含むリストが生成され、fooその後に現在のディレクトリのファイルのリストが続きますbar。現在のディレクトリが空の場合、結果はfoo、、、*ですbar

結果は文字列のリストです。シェル構文には、リストコンテキストと文字列コンテキストという2種類のコンテキストがあります。フィールド分割とファイル名の生成はリストコンテキストでのみ発生しますが、ほとんどの場合に当てはまります。二重引用符で区切られた文字列コンテキスト:二重引用符で囲まれた文字列全体は単一の文字列であるため、分割できません。 (例外:"$@"位置引数のリストに展開されます。たとえば、3つの位置引数がある場合"$@"と同じです。)"$1" "$2" "$3"$*と$@の違いは何ですか?)

$(foo)交換コマンドを使用または使用する場合も同様です`foo`。しかし、使用しないでください`foo`。引用規則は奇妙で移植性がなく、すべての最新のシェルは$(foo)直感的な引用規則に加えてまったく同じ引用規則をサポートしています。

算術置換の出力も同じ拡張を経るが拡張できない文字のみを含むので、一般的に問題にはなりません(IFS数字またはがないと仮定-)。

バラよりいつ二重引用符が必要ですか?引用符を省略できる状況の詳細。

これらのすべての乱れが発生したくない限り、変数とコマンドの置換には常に二重引用符を使用することを忘れないでください。注:引用符を省略するとエラーが発生するだけでなく、セキュリティの脆弱性

ファイル名のリストを処理する方法は?

スペースを使用してファイルを区切って作成すると、myfiles="file1 file2"スペースを含むファイル名には機能しません。 Unixファイル名には、/(常にディレクトリ区切り文字)とヌルバイト(ほとんどのシェルのシェルスクリプトでは使用できません)を除く任意の文字を含めることができます。

同じ質問myfiles=*.txt; … process $myfilesです。これにより、変数にmyfiles5文字の文字列が含まれ、ワイルドカード文字が*.txt作成時に拡張されます。$myfilesこの例は、スクリプトをに変更するまで実際に機能しますmyfiles="$someprefix*.txt"; … process $myfilessomeprefixに設定すると機能final reportしません。

すべての種類のリスト(ファイル名など)を処理するには、リストを配列に配置します。これには、mksh、ksh93、yash、bash(またはこれらの引用問題のないzsh)が必要です。通常のPOSIXシェル(ashまたはdashなど)には配列変数はありません。

myfiles=("$someprefix"*.txt)
process "${myfiles[@]}"

Ksh88には、割り当て構文の異なる配列変数がありますset -A myfiles "someprefix"*.txt(参照:各種ksh環境の代入変数ksh88/bash 移植性が必要な場合)。 Bourne / POSIXスタイルのシェルには、"$@"ユーザーが設定し、関数のローカル引数setである位置引数の配列である配列があります。

set -- "$someprefix"*.txt
process -- "$@"

で始まるファイル名はどうですか-

関連する注意では、ファイル名は-(ダッシュ/マイナス記号)で始まり、ほとんどのコマンドはオプションを表すと解釈されます。一部のコマンド(たとえばshsetまたはsort)では、で始まるオプションも許可されています+。ファイル名が変数部分で始まる場合は、--上記のスニペットに示すように変数部分の前に渡す必要があります。これはオプションの終わりに達したことをコマンドに示すため、それ以降のすべての内容はまたはで始まる-ファイル名です+

または、ファイル名が以外の文字で始まるかどうかを確認できます-。絶対ファイル名はで始まり、相対名の先頭に追加/できます。./次のコードスニペットは、変数の内容をf同じファイルを参照する「安全な」方法に変換し、-norで始まらないようにします+

case "$f" in -* | +*) "f=./$f";; esac

このトピックに関する最後の注意点は、実際のファイルを参照する必要がある場合、またはそのようなプログラムを呼び出してそのファイルを読み取らない場合でも、一部の-コマンドはstdinまたはstdoutとして解釈されることです。 stdin stdoutを読み書きするには、上記のように上書きする必要があります。バラより----「du -sh *」と「du -sh ./*」の違いは何ですか?さらなる議論のために。

コマンドを変数に保存するには?

「コマンド」とは、3つのことを意味します。コマンド名(フルパスのある/なしの実行可能ファイルの名前、組み込みまたはエイリアス付きの関数名)、パラメータ付きのコマンド名、またはシェルコードの断片。したがって、変数に保存する方法はいくつかあります。

コマンド名がある場合は、保存して通常どおり二重引用符を含む変数を使用してください。

command_path="$1"
"$command_path" --option --message="hello world"

引数を取るコマンドがある場合、問題は上記のファイル名のリストと同じです。つまり、文字列ではなく文字列のリストです。途中にスペースがある文字列にはパラメータを入力できません。これは、パラメータの一部であるスペースとパラメータを区切るスペースの違いがわからないためです。シェルに配列がある場合はそれを使用できます。

cmd=(/path/to/executable --option --message="hello world" --)
cmd=("${cmd[@]}" "$file1" "$file2")
"${cmd[@]}"

使用しているシェルに配列がない場合はどうなりますか?位置パラメータを変更しても問題ない場合は引き続き使用できます。

set -- /path/to/executable --option --message="hello world" --
set -- "$@" "$file1" "$file2"
"$@"

複雑なシェルコマンド(リダイレクト、パイプなど)を保存する必要がある場合はどうすればよいですか?または、場所パラメータを変更したくない場合は?その後、そのコマンドを含む文字列を作成し、組み込みevalコマンドを使用できます。

code='/path/to/executable --option --message="hello world" -- /path/to/file1 | grep "interesting stuff"'
eval "$code"

定義でネストされた引用符を参照してくださいcode。一重引用符は'…'文字列リテラルを区切るため、変数の値は文字列codeです/path/to/executable --option --message="hello world" -- /path/to/file1。組み込み関数は、eval引数として渡された文字列をスクリプトに表示されているかのように解析するようにシェルに指示するため、引用符、パイプなどが解析されます。

使いにくいですeval。いつ何を分析するかを慎重に考えてください。特にファイル名をコードに入れることはできません。ソースコードファイルにあるように参照する必要があります。これを行う直接的な方法はありません。code="$code $filename"ファイル名にシェル特殊文字(スペース、、、、、、、、など)が含まれていると、$このような内容が壊れます。まだオンになっていてオフになっています。ファイル名に。;|<>code="$code \"$filename\"""$\`code="$code '$filename'"'

  • ファイル名の周りに引用符を追加します。最も簡単な方法は、周囲に一重引用符を追加し、一重引用符を'\''

     quoted_filename=$(printf %s. "$filename" | sed "s/'/'\\\\''/g")
     code="$code '${quoted_filename%.}'"
    
  • コードスニペットが構築されるのではなく、コードが評価されたときにルックアップされるように、コード内の変数拡張を維持します。これはより簡単ですが、コードが実行されたときに変数がまだ同じ値を持つ場合にのみ機能します。たとえば、コードがループに埋め込まれている場合は機能しません。

     code="$code \"\$filename\""
    

最後に、コードを含む変数は本当に必要ですか?コードブロックの名前を指定する最も自然な方法は、関数を定義することです。

何が問題ですかread

いいえ-rread連続した行は許可されます。これは入力の単一の論理行です。

hello \
world

read入力行を文字で区切られたフィールドに分割します$IFS(そうでない場合、-rバックスラッシュはこれらのフィールドもエスケープします)。たとえば、入力が3つの単語を含む行の場合、read first second third設定は入力のfirst最初の単語、second2番目の単語、3番目の単語です。thirdより多くの単語がある場合、最後の変数には前の単語を設定した後に残った内容が含まれます。先行および末尾の空白が切り捨てられます。

IFSクリーンアップを防ぐには、空の文字列に設定してください。バラより"IFS=;"代わりに「IFS = Read」が頻繁に使用されるのはなぜですか?より長い説明のために。

質問がありますかxargs

入力形式xargsはスペースで区切られた文字列で、単一引用符または二重引用符を選択できます。この形式を出力する標準ツールはありません。

xargs -L1またはxargs -l入力を分割しないでください。ワイヤーしかし、入力ラインごとに1つのコマンドを実行します(ラインはまだ分割されて引数を形成し、スペースで終わる場合は次のラインに進みます)。

xargs -I PLACEHOLDER置換には1行の入力を使用しますPLACEHOLDERが、引用符とバックスラッシュは引き続き処理され、先行スペースは切り捨てられます。

xargs -r0該当する場合(使用可能な場合:GNU(Linux、Cygwin)、BusyBox、BSD、OSXを使用できますが、POSIXでは使用できません)、ほとんどのデータ、特にファイル名と外部コマンド引数にNULLバイトが表示されないため安全です。ヌルで区切られたファイル名のリストを生成するには、以下を使用しますfind … -print0(またはfind … -exec …以下の説明に従って使用できます)。

見つかったファイルで何をすべきですかfind

find … -exec some_command a_parameter another_parameter {} +

some_commandシェル関数やエイリアスではなく、外部コマンドでなければなりません。ファイルを処理するためにシェルを呼び出す必要がある場合は、sh明示的に呼び出してください。

find … -exec sh -c '
  for x do
    … # process the file "$x"
  done
' find-sh {} +

他に質問があります

検索このサイトにタグを追加するか、または。 (「詳細を見る...」をクリックすると、いくつかの一般的なヒントと直接選択したよくある質問のリストが表示されます。)検索したが答えが見つからない場合は、私に与えるように頼む

おすすめ記事