「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メタ文字を含むファイルをサポートするようにコマンドを変更できるようにwith引数を使用するときにファイル名をエスケープする一般的な方法はありますか?

答え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名前がorのファイルは...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さらに多くのファイルを探します。元の場所に移動してみてください)。xmlfind

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

  • 最初のケースでは、zshファイルリストはディレクトリ名とファイル名でソートされるため、ターゲットディレクトリはある程度一貫して予測可能です。の場合は、findディレクトリ内のファイルの元の順序に基づいています。
  • を使用してくださいzsh。ファイルを移動するための一致するディレクトリがない場合は、find上記の方法を使用する代わりにエラーメッセージが表示されます。
  • を使用しているときにfind一部のディレクトリを参照できない場合はエラーメッセージが表示されますが、使用するときはそうではありませんzsh

最後の警告です。信頼できないファイル名を持つ一部のファイルを取得する理由が攻撃者がディレクトリツリーに書き込む可能性があるため、攻撃者がコマンドの下でファイル名を変更できる場合は、上記の回避策のいずれも安全ではないことに注意してください。

たとえば、LXDEを使用すると、攻撃者は悪意のあるファイルを作成しfoo/lxde-rc.xmllxde-rcフォルダを作成し、コマンドの実行時を検出し、レースウィンドウ(必要に応じて大きくすることができます)中にlxde-rcそれをシンボリックリンクに置き換えることができます。~/.config/openbox/findそれを見つけて実行するlxde-rc間(シンボリックリンクに変更して他の場所に移動することもできます)mvrename("foo/lxde-rc.xml", "lxde-rc/lxde-rc.xml")foolxde-rc.xml

標準またはGNUユーティリティを使用してこの問題を解決することはおそらく不可能です。適切なプログラミング言語で作成し、安全なディレクトリナビゲーションを実行し、renameat()システムコールを使用する必要があります。

ディレクトリツリーがシステムコールのパス長制限に達するのに十分深くなると、上記rename()の解決策もすべて失敗します(表示エラーが発生しました)。使用されたソリューションでも問題を解決できます。mvrename()ENAMETOOLONGrenameat()

答え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 +シェルは含まれません。

シェルの出力を処理するには、そのコードの前に呼び出してシェルでファイル名ワイルドカードを無効にし、後で呼び出して再度有効にすることをお勧めしますfindset -fset +f

答え4

以下は比較的簡単なPOSIX準拠のパイプラインです。階層を2回スキャンします。まずディレクトリを検索してから、通常の* .xmlファイルを検索します。スキャン間の空白行は、変換されたAWK信号を表します。

AWKコンポーネントは、ベース名をターゲットディレクトリにマッピングします(同じベース名を持つ複数のディレクトリがある場合は、最初の巡回のみが記憶されます)。各* .xmlファイルに対して2つのフィールド、つまり1)ファイルパスと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

関連情報