
ディレクトリツリーの下に複数の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
さらに多くのファイルを探します。元の場所に移動してみてください)。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ファイルに対して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