ディレクトリ ツリーの下に多数の 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.xml
、lxde-rc
フォルダーを作成し、コマンドの実行を検出して、その検出との実行の間の競合ウィンドウ (さまざまな方法で必要なだけ大きくすることができます) 中に、そのlxde-rc
を へのシンボリック リンクに置き換えることができます(をそのシンボリック リンクに変更して、 を別の場所に移動させることもできます)。~/.config/openbox/
find
lxde-rc
mv
rename("foo/lxde-rc.xml", "lxde-rc/lxde-rc.xml")
foo
lxde-rc.xml
これを回避するのは、標準ユーティリティや GNU ユーティリティを使用してもおそらく不可能であり、適切なプログラミング言語で記述し、安全なディレクトリ トラバーサルを実行し、renameat()
システム コールを使用する必要があります。
ディレクトリ ツリーが深く、rename()
によって実行されるシステム コールに渡されるパスの長さの制限にmv
達した場合 ( でrename()
が失敗するENAMETOOLONG
)、上記のすべてのソリューションも失敗します。 を使用するソリューションrenameat()
でもこの問題を回避できます。
答え2
インライン スクリプトを で使用する場合find ... -exec sh -c ...
、位置パラメータを介して結果をシェルに渡す必要があります。そうすれば、インライン スクリプト内のどこでもfind
を使用する必要はありません。{}
bash
または がある場合は、出力を にzsh
渡すことができます。basename
printf '%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