`find` コマンドを使用してシェルのメタ文字を自動的にエスケープするにはどうすればよいですか?

`find` コマンドを使用してシェルのメタ文字を自動的にエスケープするにはどうすればよいですか?

ディレクトリ ツリーの下に多数の XML ファイルがあり、それらを同じディレクトリ ツリー内の同じ名前の対応するフォルダーに移動したいと考えています。

サンプル構造(シェル内)は次のとおりです。

touch foo.xml bar.xml "[ foo ].xml" "( bar ).xml"
mkdir -p foo bar "foo/[ foo ]" "bar/( bar )"

したがって、私のアプローチは次のとおりです。

find . -name "*.xml" -exec sh -c '
  DST=$(
    find . -type d -name "$(basename "{}" .xml)" -print -quit
  )
  [ -d "$DST" ] && mv -v "{}" "$DST/"' ';'

次のような出力が得られます。

‘./( bar ).xml’ -> ‘./bar/( bar )/( bar ).xml’
mv: ‘./bar/( bar )/( bar ).xml’ and ‘./bar/( bar )/( bar ).xml’ are the same file
‘./bar.xml’ -> ‘./bar/bar.xml’
‘./foo.xml’ -> ‘./foo/foo.xml’

しかし、角括弧 ( ) が付いたファイルは[ foo ].xml無視されたかのように移動されていません。

確認したところ、basename(例basename "[ foo ].xml" ".xml") はファイルを正しく変換しますが、find括弧に問題があります。例:

find . -name '[ foo ].xml'

ファイルを正しく見つけることができません。ただし、括弧 ( '\[ foo \].xml') をエスケープすると正常に動作しますが、スクリプトの一部であり、どのファイルに特殊な (シェルの?) 文字が含まれているかわからないため、問題は解決されません。BSD と GNU の両方でテスト済みですfind

findのパラメータを使用するときにファイル名をエスケープする普遍的な方法はありますか-name? そうすれば、メタ文字を含むファイルをサポートするようにコマンドを修正できますか?

答え1

ここでは glob を使用すると非常に簡単になりますzsh

for f (**/*.xml(.)) (mv -v -- $f **/$f:r:t(/[1]))

または、隠し xml ファイルを含めて隠しディレクトリ内を検索する場合は、次のようにfindします。

for f (**/*.xml(.D)) (mv -v -- $f **/$f:r:t(D/[1]))

.xmlただし、、..xmlまたはと呼ばれるファイルは...xml問題になる可能性があるので、それらを除外することをお勧めします。

setopt extendedglob
for f (**/(^(|.|..)).xml(.D)) (mv -v -- $f **/$f:r:t(D/[1]))

GNU ツールでは、各ファイルのディレクトリ ツリー全体をスキャンしなくても済むようにする別の方法として、一度スキャンしてすべてのディレクトリとxmlファイルを検索し、それらの場所を記録して、最後に移動を実行するという方法があります。

(export LC_ALL=C
find . -mindepth 1 -name '*.xml' ! -name .xml ! \
  -name ..xml ! -name ...xml -type f -printf 'F/%P\0' -o \
  -type d -printf 'D/%P\0' | awk -v RS='\0' -F / '
  {
    if ($1 == "F") {
      root = $NF
      sub(/\.xml$/, "", root)
      F[root] = substr($0, 3)
    } else D[$NF] = substr($0, 3)
  }
  END {
    for (f in F)
      if (f in D) 
        printf "%s\0%s\0", F[f], D[f]
  }' | xargs -r0n2 mv -v --
)

任意のファイル名を許可したい場合、このアプローチにはいくつかの問題があります。

  • {}シェルコードに埋め込むのはいつも$(rm -rf "$HOME").xml間違いです。たとえば、というファイルがあったらどうなるでしょうか? 正しい方法は、それらを{}引数としてインライン シェル スクリプト ( -exec sh -c 'use as "$1"...' sh {} \;) に渡すことです。
  • GNU ではfind( を使用しているため、ここでは暗黙的に-quit)、*.xmlは、有効な文字のシーケンスの後に が続くファイルのみに一致する.xmlため、現在のロケールで無効な文字を含むファイル名 (たとえば、間違った文字セットのファイル名) は除外されます。これを修正するには、すべてのバイトが有効な文字になるようにロケールを修正しますC(ただし、エラー メッセージは英語で表示されます)。
  • これらのファイルのいずれかがディレクトリまたはシンボリックリンクのタイプである場合、問題が発生します (ディレクトリのスキャンに影響したり、移動時にシンボリックリンクが壊れたりする)。 を追加して、通常のファイルのみを移動することxmlをお勧めします。-type f
  • コマンド置換($(...))ストリップ全て末尾に改行文字が付きます。これは、たとえば というファイルで問題を引き起こしますfoo␤.xml。回避策は可能ですが面倒です: base=$(basename "$1" .xml; echo .); base=${base%??}。少なくとも演算子basenameで置き換えることができます${var#pattern}。また、可能であればコマンド置換は避けてください。
  • ?ワイルドカード文字 ( 、、およびバックスラッシュ。これらはシェルに特有のものではなく、シェルのパターン マッチングと非常によく似た によって実行されるパターン マッチング () 特有のもの[です)を含むファイル名に関する問題です。バックスラッシュを使用してそれらをエスケープする必要があります。*fnmatch()find
  • 前述の.xml、、..xmlの問題。...xml

したがって、上記のすべてに対処すると、次のようになります。

LC_ALL=C find . -type f -name '*.xml' ! -name .xml ! -name ..xml \
  ! -name ...xml -exec sh -c '
  for file do
    base=${file##*/}
    base=${base%.xml}
    escaped_base=$(printf "%s\n" "$base" |
      sed "s/[[*?\\\\]/\\\\&/g"; echo .)
    escaped_base=${escaped_base%??}
    find . -name "$escaped_base" -type d -exec mv -v "$file" {\} \; -quit
  done' sh {} +

ふう…

これですべてではありません。 では-exec ... {} +、できるだけ少ない数を実行しますsh。運が良ければ 1 つだけ実行しますが、そうでない場合は、最初のsh呼び出しの後に多数のファイルを移動し xml、さらにfind検索を続行して、最初のラウンドで移動したファイルを再び見つける可能性が高くなります (そして、おそらくそれらを元の場所に移動しようとします)。

それ以外は、基本的に zsh のものと同じアプローチです。その他の注目すべき違いは次のとおりです。

  • ではzsh、ファイル リストが (ディレクトリ名とファイル名で) ソートされるため、宛先ディレクトリはほぼ一貫性があり、予測可能です。 ではfind、ディレクトリ内のファイルの生の順序に基づきます。
  • を使用するzshと、ファイルを移動するための一致するディレクトリが見つからない場合にエラー メッセージが表示されますが、find上記の方法ではそうではありません。
  • ではfind、一部のディレクトリをトラバースできない場合にエラー メッセージが表示されますが、 ではそうではありませんzsh

最後に警告です。ディレクトリ ツリーが攻撃者によって書き込み可能であるために、疑わしいファイル名を持つファイルがいくつか取得される場合、攻撃者がそのコマンドの背後でファイル名を変更する可能性がある場合は、上記の解決策はどれも安全ではないことに注意してください。

たとえば、LXDE を使用している場合、攻撃者は悪意のある を作成しfoo/lxde-rc.xmllxde-rcフォルダーを作成し、コマンドの実行を検出して、その検出との実行の間の競合ウィンドウ (さまざまな方法で必要なだけ大きくすることができます) 中に、そのlxde-rcを へのシンボリック リンクに置き換えることができます(をそのシンボリック リンクに変更して、 を別の場所に移動させることもできます)。~/.config/openbox/findlxde-rcmvrename("foo/lxde-rc.xml", "lxde-rc/lxde-rc.xml")foolxde-rc.xml

これを回避するのは、標準ユーティリティや GNU ユーティリティを使用してもおそらく不可能であり、適切なプログラミング言語で記述し、安全なディレクトリ トラバーサルを実行し、renameat()システム コールを使用する必要があります。

ディレクトリ ツリーが深く、rename()によって実行されるシステム コールに渡されるパスの長さの制限にmv達した場合 ( でrename()が失敗するENAMETOOLONG)、上記のすべてのソリューションも失敗します。 を使用するソリューションrenameat()でもこの問題を回避できます。

答え2

インライン スクリプトを で使用する場合find ... -exec sh -c ...、位置パラメータを介して結果をシェルに渡す必要があります。そうすれば、インライン スクリプト内のどこでもfindを使用する必要はありません。{}

bashまたは がある場合は、出力を にzsh渡すことができます。basenameprintf '%q'

find . -name "*.xml" -exec bash -c '
  for f do
    BASENAME="$(printf "%q" "$(basename -- "$f" .xml)")"
    DST=$(find . -type d -name "$BASENAME" -print -quit)
    [ -d "$DST" ] && mv -v -- "$f" "$DST/"
  done
' bash {} +

では をbash使用できますがprintf -v BASENAME、ファイル名に制御文字または非 ASCII 文字が含まれている場合、この方法は正しく機能しません。

正しく動作させたい場合は、、、およびバックスラッシュのみをエスケープするシェル関数を記述する必要があり[ます。*?

答え3

良いニュース:

find . -name '[ foo ].xml'

シェルによって解釈されないため、この方法で find プログラムに渡されます。ただし、find は引数をパターン-nameとして解釈するglobため、これを考慮する必要があります。

find -exec \;コールまたはそれ以上を好む場合はfind -exec +、シェルは必要ありません。

findシェルで出力を処理する場合は、set -f問題のコードの前に を呼び出してシェルでのファイル名のグロビングを無効にし、後で を呼び出して再度オンにすることをお勧めしますset +f

答え4

以下は、比較的単純な POSIX 準拠のパイプラインです。階層を 2 回スキャンし、最初にディレクトリを検索し、次に *.xml の通常ファイルを検索します。スキャン間の空白行は、AWK に遷移を通知します。

AWK コンポーネントは、ベース名を宛先ディレクトリにマップします (同じベース名を持つディレクトリが複数ある場合は、最初のトラバーサルのみが記憶されます)。各 *.xml ファイルに対して、1) ファイルのパスと 2) 対応する宛先ディレクトリの 2 つのフィールドを含むタブ区切りの行を出力します。

{
    find . -type d
    echo
    find . -type f -name \*.xml
} |
awk -F/ '
    !NF { ++i; next }
    !i && !($NF".xml" in d) { d[$NF".xml"] = $0 }
    i { print $0 "\t" d[$NF] }
' |
while IFS='     ' read -r f d; do
    mv -- "$f" "$d"
done

読み取り直前に IFS に割り当てられる値は、スペースではなく、リテラルのタブ文字です。

以下は、元の質問の touch/mkdir スケルトンを使用したトランスクリプトです。

$ touch foo.xml bar.xml "[ foo ].xml" "( bar ).xml"
$ mkdir -p foo bar "foo/[ foo ]" "bar/( bar )"
$ find .
.
./foo
./foo/[ foo ]
./bar.xml
./foo.xml
./bar
./bar/( bar )
./[ foo ].xml
./( bar ).xml
$ ../mv-xml.sh
$ find .
.
./foo
./foo/[ foo ]
./foo/[ foo ]/[ foo ].xml
./foo/foo.xml
./bar
./bar/( bar )
./bar/( bar )/( bar ).xml
./bar/bar.xml

関連情報