web-dev-qa-db-ja.com

シェルスクリプトで空白やその他の特殊文字が詰まるのはなぜですか?

または、堅牢なファイル名の処理とシェルスクリプトで渡されるその他の文字列の入門ガイド。

私はほとんどの場合うまくいくシェルスクリプトを書きました。しかし、一部の入力(ファイル名など)で窒息します。

次のような問題が発生しました。

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

何が起こっているのですか、どうすれば修正できますか?

Gillesの回答はすばらしいですが、私は彼の要点で問題を取り上げています

変数の置換とコマンドの置換は常に二重引用符で囲みます: "$ foo"、 "$(foo)"

Word分割を行うBashのようなシェルから始める場合、もちろん安全なアドバイスは常に引用符を使用することです。ただし、ワード分割は常に実行されるわけではありません

§単語分割

これらのコマンドはエラーなしで実行できます

foo=$bar
bar=$(a command)
logfile=$logdir/foo-$(date +%Y%m%d)
PATH=/usr/local/bin:$PATH ./myscript
case $foo in bar) echo bar ;; baz) echo baz ;; esac

私はユーザーにこの動作を採用するように勧めていませんが、Wordの分割がいつ発生するかを誰かがしっかりと理解していれば、引用符をいつ使用するかを自分で決定できるはずです。

26
Steven Penny

私の知る限り、展開を二重引用符で囲む必要があるのは2つの場合だけです。これらの場合は、二重引用符で囲むと異なる展開を指定する2つの特別なシェルパラメーター"$@""$*"が含まれます。他のすべての場合では、(おそらく、シェル固有の配列実装を除く)拡張の動作は構成可能なものです-そのためのオプションがあります。

もちろん、これは二重引用符を避けなければならないということではありません。逆に、シェルが提供する必要がある拡張を区切る最も便利で堅牢な方法です。しかし、私は代替案がすでに専門的に説明されているので、シェルが値を拡張したときに何が起こるかを議論するための素晴らしい場所だと思います。

シェルは、その心と魂(そのようなものを持っている人のために)であり、コマンドインタープリターです。これは、大きなインタラクティブなsedのようなパーサーです。 Shellステートメントが窒息 on whitespaceまたは類似している場合は、シェルの解釈プロセスを完全に理解していない可能性が高いです-特に、入力ステートメントをアクション可能なものに変換する方法と理由コマンド。シェルの仕事は次のとおりです。

  1. 入力を受け入れる

  2. 解釈してsplit正しくトークン化された入力に変換words

    • input wordsは、$Wordecho $words 3 4* 5などのシェル構文項目です。

    • 単語は常に空白で分割されます。これは単なる構文ですが、入力ファイルでシェルに提供されるリテラルの空白文字のみです。

  3. 必要に応じて、それらを複数のフィールドに展開します

    • フィールドWord展開の結果-最終的な実行可能コマンドを構成します

    • "$@"$IFSフィールド分割、およびパス名展開入力を除くWordは常に単一のフィールドに評価される必要があります。

  4. そして、結果のコマンドを実行します

    • ほとんどの場合、これには何らかの解釈の結果を何らかの形で渡すことが含まれます

シェルはglueであるとよく言われます。これがtrueの場合、1つのプロセスに対するstickingは引数のリスト、またはfieldsです。または、それがそれらをexecsしたときの別のもの。ほとんどのシェルは、NULバイトをうまく処理しません-たとえあったとしても-これは、それらがすでに分割しているためです。シェルはexecたくさんを使用する必要があり、NUL時にシステムカーネルに渡す引数のexec区切り配列でこれを行う必要があります。シェルのデリミタとデリミタ付きデータを混在させる場合、シェルはおそらくそれを台無しにするでしょう。ほとんどのプログラムと同様に、その内部データ構造はその区切り文字に依存しています。特に、zshはこれを台無しにしません。

そして、それが$IFSの出番です。$IFSは常に存在します-同様に設定可能-シェルがシェル拡張をWordからフィールドに分割する方法を定義するシェルパラメータ-特にこれらの値fieldsで区切る必要があります。 $IFSは、NUL以外の区切り文字でシェル拡張を分割します。つまり、シェルは、内部データ配列の$IFSの値と一致する拡張から得られたバイトをNULで置き換えます。そのように見ると、すべてのfield-splitシェル展開が$IFSで区切られたデータ配列であることがわかり始めます。

$IFSのみdelimits展開されているnot展開は、他の方法で区切られていることを理解することが重要です。これは"double-quotesで実行できます。展開を引用するときは、展開を先頭で区切り、少なくともをその値の末尾に区切ります。それらの場合、分離するフィールドがないため、$IFSは適用されません。実際、二重引用符で囲まれた展開は、IFS=が空の値に設定されている場合、引用符で囲まれていない展開と同じfield-splitting動作を示します。

引用されていない限り、$IFSはそれ自体、$IFSで区切られたシェル拡張です。デフォルトでは、<space><tab><newline>の指定された値に設定されます。これらの3つはすべて、$IFS内に含まれると特別なプロパティを示します。 $IFSの他の値は、展開ごとに単一のフィールドに評価されるように指定されますが、オカレンス$IFS空白-これら3つのうちのいずれか-は、展開ごとに1つのフィールドsequenceおよび先頭/末尾のシーケンスは完全に省略されます。これは、例を使用して理解するのがおそらく最も簡単です。

slashes=///// spaces='     '
IFS=/; printf '<%s>' $slashes$spaces
<><><><><><     >
IFS=' '; printf '<%s>' $slashes$spaces
</////>
IFS=; printf '<%s>' $slashes$spaces
</////     >
unset IFS; printf '<%s>' "$slashes$spaces"
</////     >

しかし、それは単に$IFSです-尋ねられたとおりの単語分割または空白なので、特殊文字は何ですか?

シェルは、デフォルトでは、引用符で囲まれていない特定のトークン(ここで別の場所に記載されている?*[など)をリストに出現すると、それらを複数のフィールドに展開します。これはパス名展開またはグロビングと呼ばれます。これは非常に便利なツールであり、シェルの解析順序でフィールド分割の後に発生するため、生成される$ IFS-フィールドの影響を受けませんパス名展開は、ファイルの内容に現在$IFSに含まれる文字が含まれているかどうかに関係なく、ファイル名自体の先頭/末尾で区切られます。この動作はデフォルトでオンに設定されていますが、それ以外の場合は非常に簡単に設定できます。

set -f

これは、シェルにnotglobに指示します。パス名の展開は、少なくともその設定が元に戻されるまで発生しません-現在のシェルが別の新しいシェルプロセスに置き換えられた場合など。

set +f

...シェルに発行されます二重引用符は、$IFSfield-splittingの場合と同様に、このグローバル設定を展開ごとに不要にします。そう:

echo "*" *

...パス名の拡張が現在有効になっている場合、引数ごとに非常に異なる結果が生成される可能性があります-最初の拡張はリテラル値(単一のアスタリスク文字、つまり、まったくではない)にのみ拡張されるため現在の作業ディレクトリにと一致する可能性のあるファイル名が含まれていない場合(そしてそれらのほとんどすべてに一致する場合)の場合は、2番目のみが同じです。ただし、次の場合:

set -f; echo "*" *

...両方の引数の結果は同じです-その場合、*は拡張されません。

22
mikeserv

ファイル名にスペースが含まれ、ディレクトリ名にスペースが含まれる大規模なビデオプロジェクトがありました。 find -type f -print0 | xargs -0はいくつかの目的でさまざまなシェルで機能しますが、bashを使用している場合は、カスタムIFS(入力フィールド区切り記号)を使用すると柔軟性が高まることがわかります。以下のスニペットはbashを使用し、IFSを改行に設定しています。ファイル名に改行がない場合:

(IFS=$'\n'; for i in $(find -type f -print) ; do
    echo ">>>$i<<<"
done)

括弧を使用してIFSの再定義を分離することに注意してください。 IFSを回復する方法について他の投稿を読んだことがありますが、これは簡単です。

さらに、IFSを改行に設定すると、シェル変数を事前に設定して簡単に印刷できます。たとえば、区切り文字として改行を使用して変数Vを段階的に大きくすることができます。

V=""
V="./Ralphie's Camcorder/STREAM/00123.MTS,04:58,05:52,-vf yadif"
V="$V"$'\n'"./Ralphie's Camcorder/STREAM/00111.MTS,00:00,59:59,-vf yadif"
V="$V"$'\n'"next item goes here..."

それに応じて:

(IFS=$'\n'; for v in $V ; do
    echo ">>>$v<<<"
done)

これで、二重引用符を使用してecho "$V"でVの設定を「リスト」し、改行を出力できます。 ($'\n'の説明については this thread にクレジットしてください。)

3
Russ

_find directory -print0 | xargs -0_を使用するメソッドは、すべてのスペシャルを処理する必要があります。ただし、ファイルまたはディレクトリごとに1つのPIDが必要であり、パフォーマンスの問題にマウントされる可能性があります。

私が最近遭遇した堅牢な(およびパフォーマンスの高い)ファイル処理の別の方法について説明しましょう。これは、find出力をタブ区切りCSVデータとして後処理する必要がある場合に適しています。 AWKによる。このような処理では、実際にはファイル名のタブと改行だけが混乱を招きます。

ディレクトリは_find directory -printf '%P\t///\n'_を介してスキャンされます。パスにタブまたは改行が含まれていない場合、これにより、パス自体と_///_を含むフィールドの2つのCSVフィールドを持つ1つのレコードが作成されます。

パスにタブが含まれている場合、3つのフィールドがあります。パスフラグメント1、パスフラグメント2、および_///_を含むフィールドです。

改行が含まれている場合、2つのレコードがあります。最初のレコードにはパスfragment1が含まれ、2番目のレコードにはパスfragment2と_///_を含むフィールドが含まれます。

ここで重要なのは、_///_がパスで自然に発生することはないということです。また、それは一種の防水エスケープまたはターミネーターです。

findの出力をスキャンする(AWK)プログラムを記述して、untilが_///_を見つけると、新しいフィールドがパスのタブと新しいレコードはパスの改行です。

タブは_///t_として安全にエスケープすることができ、改行は_///n_として安全にエスケープすることができます。これは、_///_がファイルパスで自然に発生することはないことを知っているためです。 _///t_および_///n_をタブに変換すると、処理から出力が生成されるときに、最後に改行が発生する可能性があります。

はい、複雑に聞こえますが、手掛かりは、2つのPIDだけが必要であるということです:findと、記述されたアルゴリズムを実行するawkインスタンス。そしてそれは速いです。

アイデアは私のものではありません。ディレクトリ同期用のこの新しい(2019)bashスクリプトに実装されていることがわかりました: Zaloha.sh 。実際には、アルゴリズムを説明するドキュメントがあります。

ファイル名の特殊文字によってそのプログラムを壊したり、止めたりすることができませんでした。改行とタブのみという名前のディレクトリも正しく処理しました...

0
user400462

上記のすべてのセキュリティへの影響を考慮し、変数を信頼して制御できると想定すると、evalを使用して空白を含む複数のパスを作成できます。しかし、注意してください!

$ FILES='"a b" c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
$ FILES='a\ b c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
0
Mattias Wadman