ファイル名とディレクトリ名の文字列を再帰的に置き換える方法は?

ファイル名とディレクトリ名の文字列を再帰的に置き換える方法は?

私はetckeeperのgitレプリカを持っており、etckeeper名前はusrkeeper./foo-etckeeper-bar./foo-usrkeeper-bar

関連ファイルを見つけるのは簡単です。

% find . -path '*etckeeper*' -print

しかし、実際に名前を変更する方法がわかりません。私は次のものxargsと組み合わせようとしましたmv

% find . -path '*etckeeper*' -print0 | xargs -0 -n 1 -J % bash -c mv % '$(echo' % \| sed \"s/etckeeper/usrkeeper/\" \)

読みやすくするためにエスケープされていない後半は、次のように表示されますxargs -0 -n 1 -J % bash -c mv % $(echo % | sed "s/etckeeper/usrkeeper/" )。これに関するアイデアは、置き換えのために$()ファイル名をパイプすることですsed

ここでの問題は、bash -c実行に必要なコマンドが単一の文字列であることです。その後、パラメータを位置パラメータとして解釈し始めます。私はすべての内容を引用することができます:

% find . -path '*etckeeper*' -print0 | xargs -0 -n 1 -J % bash -c 'mv % $(echo % | sed "s/etckeeper/usrkeeper/g" )'

しかし、今はxargs交換されません%。この問題をどのように解決できますか? (また、参照として、etckeeper名前を含むファイルが name を含むディレクトリに存在する場合、ディレクトリがファイルの前に移動されるetckeeperため、上記の操作は失敗します。)

答え1

名前変更コマンドを使用できます(参照:編集1)。

ソリューション1

合理的な数のファイル/ディレクトリの場合は、bash 4オプションを設定してグローバルスター(再帰名には適用されません。編集3):

shopt -s globstar
rename -n 's/etckeeper/userkeeper/g' **

ソリューション2

ファイル/ディレクトリの数が多い場合は、名前変更と検索を2つの手順で使用して、名前を変更したばかりのディレクトリでファイルの名前変更が失敗するのを防ぎます(参照)。編集2):

find . -type f -exec rename 's/etckeeper/userkeeper/g' {} \;
find . -type d -exec rename 's/etckeeper/userkeeper/g' {} \;

編集1:

2つの名前変更コマンドがあります。。この答えは、Debianベースのディストリビューションで利用可能なPerlベースの名前変更コマンドを使用します。 Debian ベースではなくディストリビューションにインストールするには、次のものが必要です。cpanからインストール可能またはキャッチ。

編集2:

-depthJeff Schallerがコメントでfindオプションを指摘したように、オプションProcess each directory's contents before the directory itselfごとに「ソートされた」ルックアップで-depth十分です。

find . -depth -exec rename 's/etckeeper/userkeeper/g' {} \;

編集3

etckeeper/etckeeper/etckeeper解決策1は、外部レベルが内部レベルよりも前に処理され、内部レベルへのポインタが役に立たなくなるため、再帰的な名前変更ターゲット(たとえば)には機能しません。 (最初の名前の後にetckeeper/etckeeper/etckeeper名前が付けられるため、usrkeeper/etckeeper/etckeeper名前をetckeeper/etckeeper/およびに置き換えるとetckeeper/etckeeper/etckeeper失敗します)。オプションはfind同じ問題を解決します-depth

編集4

Casがコメントで指摘したように、{}\;代わりに {}+ を使用します。 - Perlスクリプトをフォークすると(たとえば、ファイル/ディレクトリごとに1回)、費用がかかります。

答え2

あなたの元の質問は実際に答えやすいです。 (xargs少なくともOS Xでは)-I以下を行うオプションもあります。いいえ代替文字列は唯一のパラメータでなければなりません。したがって、呼び出しは次のようになります。

% find . -path '*etckeeper*' -print0 | xargs -0 -n 1 -I % bash -c 'echo mv % $(echo % | sed "s/etckeeper/usrkeeper/g" )'

とても簡単です。それでは、正しい順序で名前を変更しましょう。ディレクトリが最初に印刷されることがわかりましたfind(下降する前にディレクトリを処理する必要があるため)。したがって、私たちがしなければならないのは、ファイル名の順序を変更することだけです。

% find . -path '*etckeeper*' -print | tail -r | xargs -n 1 -I % bash -c 'echo mv % $(echo % | sed "s/etckeeper/usrkeeper/g" )'

find -print0 | xargs -0に切り替えましたfind -print | xargs。なぜですか?tail -r反転はヌル文字ではなく改行文字に基づいているためです。切り替えないとtail -rパイプしたのと同じ内容が印刷されます!したがって、スクリプトはより正確になりましたが、改行文字を含むファイル名でも中断されます。

お持ちでない場合は、tail -r一度お試しくださいtac。望むより:ファイルの行の順序を変更するには?スタックオーバーフローに。

今問題は、そのsed命令があまりにも攻撃的だという点だ。たとえば、次のツリーがあります。

.
├── etckeeper-foo
│   └── etckeeper-bar.md
└── some-other-directory
    └── etckeeper-baz.md

mv次のような電話がかかります。

mv ./some-other-directory/etckeeper-baz.md ./some-other-directory/usrkeeper-baz.md
mv ./etckeeper-foo/etckeeper-bar.md ./usrkeeper-foo/usrkeeper-bar.md
mv ./etckeeper-foo ./usrkeeper-foo

最初のものは素晴らしかったが、2番目のものは明らかではなかった。まだ終わっていないからですmv ./etckeeper-foo ./usrkeeper-foo

私たちは交換するだけです最後パス名コンポーネント。basenameこれを使用してこれを行うことができますdirname

% find . -name '*etckeeper*' -print | tail -r | xargs -n 1 -I % bash -c 'echo mv % $(dirname %)/$(basename % | sed "s/etckeeper/usrkeeper/g" )'

私が変更したもう一つのことはfind -namefind -path

これにより、正しいmv呼び出しが生成されます。

mv ./some-other-directory/etckeeper-baz.md ./some-other-directory/usrkeeper-baz.md
mv ./etckeeper-foo/etckeeper-bar.md ./etckeeper-foo/usrkeeper-bar.md
mv ./etckeeper-foo ./usrkeeper-foo

ついに。もう一度覚えてください。失敗する難解なファイル名に関して。また、ファイル名が特に長い場合、パラメータが長すぎてxargs機能しません。mvこの場合、(少なくともOS Xでは)toを渡して-xすぐにxargs失敗するように指示できます。

答え3

for別の解決策は、検索結果を繰り返し、mv見つかったファイルでbash文字列の置き換えを実行する小さなスクリプトを使用することです。

IFS=$'\n'
for files in $(find . -name "*etckeeper*"); 
do 
  mv "$files ${files/etckeeper/usrkeeper}"
done

スクリプトでそれを使用しない場合は、元のIFSを保存し、ループの終わりに復元することをお勧めします。

関連情報