web-dev-qa-db-ja.com

Bash:サブシェルで引用符をエスケープ

次のコマンドを実行すると:

#!/bin/bash
while IFS= read -r -d '' file; do
    files+=$file
done < <(find -type f -name '*.c' -print0)
echo "${files[@]}"

これと同じ結果は得られません:

#!/bin/bash
find_args="-type f '*.c' -print0"
while IFS= read -r -d '' file; do
    files+=$file
done < <(find $find_args)
echo "${files[@]}"

2番目のシナリオを最初のシナリオと同等に修正するにはどうすればよいですか?

私の理解では、二重引用符には一重引用符があるため、一重引用符はエスケープされ、次のような悪い展開が生成されます。

find -type f -name ''\''*.c'\'' -print0
3
Rogozhin

(タイプミスがあることに注意してください。2番目の例では-nameフラグを省略しました。)

1つのアプローチは、配列に引数を配置し、配列をfind ..に適切に渡すことです。

#!/bin/bash
find_args=(-type f -name '*.c' -print0)
while IFS= read -r -d '' file; do
    files+=$file
done < <(find "${find_args[@]}")
echo "${files[@]}"

形式${foo[@]}は、配列のすべての要素に展開され、それぞれが(単一の文字列に展開されるのではなく)個々の単語に展開されます。これは、元のスクリプトに近い意図です。

3
B Layer

BLayerの答え は正しいですが、ここで実際に起こっていることを分解するには(欠落している-nameプライマリのタイプミスを無視して):

#!/bin/bash
while IFS= read -r -d '' file; do
    files+=$file
done < <(find -type f -name '*.c' -print0)
echo "${files[@]}"

プロセス置換(<(...))によって開始されたシェルでは、次のコマンドがbashによって解析されます。

find -type f -name '*.c' -print0

Glob *.cが引用されているため、bashはそれを展開しませんnot。ただし、一重引用符は削除されます。したがって、findプロセスが開始されると、引数リストとして表示されるのは次のとおりです。

-type
f
-name
*.c
-print0

これらの引数は、スペースや改行ではなく、nullバイトで区切られていることに注意してください。これは、シェルレベルではなく、経営幹部レベルです。これは、Cでexecve()を使用してプログラムを実行する方法と関係があります。

対照的に、次のスニペットでは次のようになります。

#!/bin/bash
find_args="-type f -name '*.c' -print0"
while IFS= read -r -d '' file; do
    files+=$file
done < <(find $find_args)
echo "${files[@]}"

変数find_argsの値は次のように設定されます。

-type f -name '*.c' -print0

(二重引用符は値の一部ではありませんが、一重引用符はあります。

コマンドfind $find_argsが実行されると、man bashに従って、トークン$find_argsはパラメーター拡張の後にワード分割が続きます。続いてパス名展開(別名glob展開)。

パラメータ展開後、-type f -name '*.c' -print0があります。これはafter引用符の削除であることに注意してください。したがって、一重引用符は削除されません。

単語分割後、次の単語が別々の単語になります。

-type
f
-name
'*.c'
-print0

次にパス名の展開が行われます。もちろん、ファイル名に通常は一重引用符を入れないため、'*.c'は何にも一致しない可能性が高いため、結果は可能性が高い'*.c'はリテラルパターンとしてfindに渡されるため、-nameプライマリはすべてのファイルで失敗します。 (名前が一重引用符で始まり、3文字の.c'で終わるファイルがある場合にのみ成功します)


編集:実際には、そのようなファイルがある場合、glob '*.c'はそのファイルと他のそのようなファイルに一致するように展開され、次に展開[実際のファイル名]はパターンとしてfindに渡されます。したがって、-print0プライマリに到達するかどうかは、(a)のみが存在するかどうかによって異なります。 oneそのようなファイル名、および(b)globとして解釈されるそのファイル名がそれ自体と一致するかどうか。

例:

touch "'something.c'"を実行すると、glob'*.c''something.c'に展開され、findプライマリ-name 'something.c'もそのファイルと一致し、出力されます。

touch "'namewithcharset[a].c'"を実行すると、グロブ'*.c'はシェルによって拡張されますが、findプライマリ-name 'namewithcharset[a].c'はそれ自体と一致しませんnot-それは'namewithcharseta.c'のみと一致します、存在しないため、-print0に到達しません。

touch "'x.c'" "'y.c'"を実行すると、glob '*.c'bothファイル名に展開されます。これにより、'y.c'は有効なプライマリではないため、findからエラーが出力されます。 (そして、ハイフンで始まらないので、そうすることはできません)。


nullglobオプションが設定されている場合、異なる動作が発生します。

参照:

4
Wildcard

すでに言われていることに加えて、あなたはする必要があります:

  • _$files_変数を配列として宣言します。デフォルトではスカラーになり、スカラーの_var+=something_は文字列の連結(またはスカラーにinteger属性が指定されている場合は算術加算)を行います。 )。または、var+=(something)構文を使用します(これにより、変数が自動的に配列に変換されます)。
  • 変数を(未設定または空のリストとして)初期化します。そうしないと、環境から初期値を継承する可能性があります。

行うこと:

_files=()
while ...
  files+=$file # or files+=("$file")
done
_

files変数が以前にスクリプトの前半で連想配列として宣言されていない限り、十分です(この場合、_files+=something_は_files["0"]+=something_のようになり、files+=("$files")はエラーになります)。

スクリプトの前半でfilesが連想配列として定義されていないことを保証できない場合は、次のことが必要になる場合があります。

_typeset -a files=()
_

代わりに、変数のスコープを囲んでいる関数に制限するという副作用があります。 typeset -ga files=()は、グローバルスコープで変数を宣言するため、bashでの回避策として適切に機能しません。 unset files; files=()は、_unset files_が設定を解除する代わりに、外部スコープ(連想配列の場合もある)からfiles変数を明らかにする場合があるため機能しない場合があります。

1

2番目のスクリプトでいくつかの変更を行う必要があります。欠落している_-name_を挿入し、一重引用符を削除し、set -fを使用して、アスタリスクの展開を回避します。スクリプトの開始時または(示されているように)サブシェル内のいずれか:

_<(set -f; find $find_args)
_

エコーをprintfに変更し、実際に値の配列を持つようにfiles+=("$file")として割り当てます。あなたが持っているものは文字列を構築するだけです。

_#!/bin/bash
find_args="-type f -name *.c -print0"
while IFS= read -r -d '' file; do
    files+=("$file")
done < <(set -f; find $find_args)
printf '<%s> ' "${files[@]}"; echo
_
0
Isaac