web-dev-qa-db-ja.com

findおよびsedを使用してファイルの名前を再帰的に変更する

たくさんのディレクトリを調べて、_test.rbで終わるすべてのファイルの名前を_spec.rbで終わるように変更します。それは私がbashをどのように扱うべきかをまったく理解していなかったので、今回はそれを釘付けにするために少し努力を払うと思った。私はこれまでのところ、不足している、私の最善の努力は次のとおりです。

find spec -name "*_test.rb" -exec echo mv {} `echo {} | sed s/test/spec/` \;

注意:execの後に余分なエコーがあるため、テスト中にコマンドが実行される代わりに出力されます。

実行すると、一致した各ファイル名の出力は次のとおりです。

mv original original

つまり、sedによる代替が失われました。トリックは何ですか?

77
opsb

これは、sedが入力として文字列{}を受け取るために発生します。

find . -exec echo `echo "{}" | sed 's/./foo/g'` \;

これは、ディレクトリ内の各ファイルに対してfoofooを再帰的に出力します。この動作の理由は、コマンド全体を展開するときにパイプラインがシェルによって1回実行されるためです。

sedはシェル経由でコマンドを実行せず、すべてのファイルに対してfindがパイプラインを実行するような方法でfindパイプラインを引用する方法はありません。パイプラインまたはバッククォートの概念。 GNU findutilsマニュアルは、パイプラインを別のシェルスクリプトに入れることで同様のタスクを実行する方法を説明しています。

#!/bin/sh
echo "$1" | sed 's/_test.rb$/_spec.rb/'

sh -cと大量の引用符を使用して、これらすべてを1つのコマンドで実行するための逆方向の方法があるかもしれませんが、私は試しません。)

32
Fred Foo

元の問題に最も近い方法で解決するには、おそらくxargsの「コマンドラインごとの引数」オプションを使用します。

find . -name *_test.rb | sed -e "p;s/test/spec/" | xargs -n2 mv

現在の作業ディレクトリ内のファイルを再帰的に検索し、元のファイル名(p)をエコーし​​てから、変更された名前(s/test/spec/)をすべてペアでmvに送ります(xargs -n2)。この場合、パス自体に文字列testを含めないでください。

113
ramtam

あなたは次のような他の方法を検討したいかもしれません

for file in $(find . -name "*_test.rb")
do 
  echo mv $file `echo $file | sed s/_test.rb$/_spec.rb/`
done
23
ajreal

これはもっと短い

find . -name '*_test.rb' -exec bash -c 'echo mv $0 ${0/test.rb/spec.rb}' {} \;
17
csg

シェルとしてbashを使用していると言いますが、この場合、実際にfindsedは必要ありません。

bashをシェルとして使用していると仮定します。

$ echo $Shell
/bin/bash
$ _

...いわゆるglobstarシェルオプションを有効にしたと仮定します。

$ shopt -p globstar
shopt -s globstar
$ _

...最後に、renameユーティリティ(util-linux-ngパッケージに含まれています)をインストールしたと仮定します

$ which rename
/usr/bin/rename
$ _

...その後、次のようにbash one-linerでバッチ名を変更できます:

$ rename _test _spec **/*_test.rb

globstarシェルオプションは、ディレクトリ階層内のネストの深さに関係なく、bashが一致するすべての*_test.rbファイルを検出することを保証します... help shoptを使用して設定方法を確認しますオプション)

9
pvandenberk

必要に応じて、sedを使用せずに実行できます。

for i in `find -name '*_test.rb'` ; do mv $i ${i%%_test.rb}_spec.rb ; done

${var%%suffix}は、suffixの値からvarを取り除きます。

または、sedを使用して実行するには:

for i in `find -name '*_test.rb'` ; do mv $i `echo $i | sed 's/test/spec/'` ; done
9
Wayne Conrad

最も簡単な方法

find . -name "*_test.rb" | xargs rename s/_test/_spec/

最速の方法(4つのプロセッサがあると仮定):

find . -name "*_test.rb" | xargs -P 4 rename s/_test/_spec/

処理するファイルの数が多い場合、xargsにパイプされたファイル名のリストにより、結果のコマンドラインが許可されている最大長を超える可能性があります。

getconf ARG_MAXを使用してシステムの制限を確認できます

ほとんどのLinuxシステムでは、free -bまたはcat /proc/meminfoを使用して、作業する必要があるRAMを確認できます。それ以外の場合は、topまたはシステムアクティビティモニターアプリを使用します。

より安全な方法(使用するRAMが1000000バイトあると仮定):

find . -name "*_test.rb" | xargs -s 1000000 rename s/_test/_spec/
6
l3x

このためには、sedは必要ありません。 process substitution を介してwhileの結果が渡されるfindループで完全に独り立ちできます。

したがって、必要なファイルを選択するfind式がある場合は、次の構文を使用します。

while IFS= read -r file; do
     echo "mv $file ${file%_test.rb}_spec.rb"  # remove "echo" when OK!
done < <(find -name "*_test.rb")

これにより、findファイルが作成され、すべての名前が文字列_test.rbの末尾から削除され、_spec.rbが追加されます。

このステップでは、 Shell Parameter Expansion を使用します。ここで、${var%string}は、$varから最短一致パターン「string」を削除します。

$ file="HELLOa_test.rbBYE_test.rb"
$ echo "${file%_test.rb}"          # remove _test.rb from the end
HELLOa_test.rbBYE
$ echo "${file%_test.rb}_spec.rb"  # remove _test.rb and append _spec.rb
HELLOa_test.rbBYE_spec.rb

例を参照してください。

$ tree
.
├── ab_testArb
├── a_test.rb
├── a_test.rb_test.rb
├── b_test.rb
├── c_test.hello
├── c_test.rb
└── mydir
    └── d_test.rb

$ while IFS= read -r file; do echo "mv $file ${file/_test.rb/_spec.rb}"; done < <(find -name "*_test.rb")
mv ./b_test.rb ./b_spec.rb
mv ./mydir/d_test.rb ./mydir/d_spec.rb
mv ./a_test.rb ./a_spec.rb
mv ./c_test.rb ./c_spec.rb
2
fedorqui

ここに、ファイル名にスペースが含まれていたときに機能したものがあります。以下の例は、すべての.darファイルを再帰的に.Zipファイルに名前変更します。

find . -name "*.dar" -exec bash -c 'mv "$0" "`echo \"$0\" | sed s/.dar/.Zip/`"' {} \;
2
rskengineer

これは、すべての場合に機能する例です。再帰的に動作し、シェルのみが必要で、スペースを含むファイル名をサポートします。

find spec -name "*_test.rb" -print0 | while read -d $'\0' file; do mv "$file" "`echo $file | sed s/test/spec/`"; done
1
eldy

やり直すつもりはありませんが、 Commandline Find Sed Exec に対する答えでこれを書きました。 1つまたは2つのディレクトリを除外する可能性のあるツリー全体。文字列 "OLD"を含むすべてのファイルおよびディレクトリの名前を、代わりに "NEW"に変更します。

以下の骨の折れる冗長性howを記述することに加えて、このメソッドは組み込みのデバッグを組み込むという点でもユニークかもしれません。基本的に、コンパイルされ、要求された作業を実行するために実行する必要があると思われるすべてのコマンドを変数に保存することを除いて、書かれているとおりには何もしません。

また、可能な限り明示的に loops を回避します。 patternの複数の一致に対するsed再帰検索に加えて、私が知る限り他の再帰はありません。

最後に、これは完全にnullで区切られています-null以外のファイル名の文字にはつまずきません。私はあなたがそれを持つべきではないと思います。

ところで、これは[〜#〜] really [〜#〜]高速です。見て:

_% _mvnfind() { mv -n "${1}" "${2}" && cd "${2}"
> read -r SED <<SED
> :;s|${3}\(.*/[^/]*${5}\)|${4}\1|;t;:;s|\(${5}.*\)${3}|\1${4}|;t;s|^[0-9]*[\t]\(mv.*\)${5}|\1|p
> SED
> find . -name "*${3}*" -printf "%d\tmv %P ${5} %P\000" |
> sort -zg | sed -nz ${SED} | read -r ${6}
> echo <<EOF
> Prepared commands saved in variable: ${6}
> To view do: printf ${6} | tr "\000" "\n"
> To run do: sh <<EORUN
> $(printf ${6} | tr "\000" "\n")
> EORUN
> EOF
> }
% rm -rf "${UNNECESSARY:=/any/dirs/you/dont/want/moved}"
% time ( _mvnfind ${SRC=./test_tree} ${TGT=./mv_tree} \
> ${OLD=google} ${NEW=replacement_Word} ${sed_sep=SsEeDd} \
> ${sh_io:=sh_io} ; printf %b\\000 "${sh_io}" | tr "\000" "\n" \
> | wc - ; echo ${sh_io} | tr "\000" "\n" |  tail -n 2 )

   <actual process time used:>
    0.06s user 0.03s system 106% cpu 0.090 total

   <output from wc:>

    Lines  Words  Bytes
    115     362   20691 -

    <output from tail:>

    mv .config/replacement_Word-chrome-beta/Default/.../googlestars \
    .config/replacement_Word-chrome-beta/Default/.../replacement_wordstars        
_

注:上記のfunctionは、_find printf_を適切に処理するために、GNUおよびsedfindバージョンを必要とする可能性が高いおよび_sed -z -e_および_:;recursive regex test;t_呼び出し。これらが利用できない場合、いくつかの小さな調整で機能が複製される可能性があります。

これにより、最初から最後まで必要なすべての操作が非常に簡単に行えます。 forksedを実行しましたが、いくつかのsed再帰的分岐テクニックも練習していたので、ここに来ました。理髪師の学校で割引ヘアカットをするようなものです。ワークフローは次のとおりです。

  • _rm -rf ${UNNECESSARY}_
    • あらゆる種類のデータを削除または破壊する可能性のある関数呼び出しを意図的に省略しました。 _./app_は望ましくない可能性があることに言及しています。事前に削除するか、他の場所に移動するか、または\( -path PATTERN -exec rm -rf \{\} \)ルーチンをfindに組み込んでプログラムで実行することもできますが、それはすべてあなた次第です。
  • __mvnfind "${@}"_
    • 引数を宣言し、ワーカー関数を呼び出します。 _${sh_io}_は、関数からの戻り値を保存するという点で特に重要です。 _${sed_sep}_はすぐに来ます。これは、関数でsedの再帰を参照するために使用される任意の文字列です。 _${sed_sep}_が、実行されたパス名またはファイル名のいずれかで潜在的に見つかる可能性のある値に設定されている場合...まあ、それをさせないでください。
  • _mv -n $1 $2_
    • ツリー全体が最初から移動されます。それは多くの頭痛の種を救います。私を信じてください。やりたいことの残り-名前の変更-は、単にファイルシステムのメタデータの問題です。たとえば、これをあるドライブから別のドライブに移動する場合、またはあらゆる種類のファイルシステムの境界を越えて移動する場合は、1つのコマンドで一度に実行することをお勧めします。また、より安全です。 mvに設定された_-noclobber_オプションに注意してください。書かれているように、この関数は_${SRC_DIR}_が既に存在する場所に_${TGT_DIR}_を配置しません。
  • _read -R SED <<HEREDOC_
    • Sedのすべてのコマンドをここに配置して、面倒なエスケープを省き、それらを変数に読み込んでsedにフィードします。以下の説明。
  • _find . -name ${OLD} -printf_
    • findプロセスを開始します。 findを使用すると、関数の最初のコマンドですべての場所から場所へのmv操作が既に行われているため、名前の変更が必要なもののみを検索します。たとえば、find呼び出しのようにexecを使用して直接アクションを実行するのではなく、代わりに_-printf_を使用してコマンドラインを動的に構築します。
  • _%dir-depth :tab: 'mv '%path-to-${SRC}' '${sed_sep}'%path-again :null delimiter:'_
    • findが必要なファイルを見つけたら、名前の変更を処理するために必要なコマンドを直接ビルドして(most)出力します。各行の先頭に付けられた_%dir-depth_は、ツリー内のファイルまたはディレクトリの名前を、まだ名前が変更されていない親オブジェクトに変更しようとしていないことを確認するのに役立ちます。 findは、あらゆる種類の最適化手法を使用してファイルシステムツリーを探索しますが、必要なデータを安全な操作の順序で返すかどうかは不明です。これが次の理由です...
  • _sort -general-numerical -zero-delimited_
    • findのすべての出力を_%directory-depth_に基づいてソートし、$ {SRC}との関係で最も近いパスが最初に機能するようにします。これにより、存在しない場所へのmvingファイルに関するエラーを回避でき、再帰ループの必要性を最小限に抑えます。 (実際、ループを見つけるのは難しいかもしれません
  • sed -ex :rcrs;srch|(save${sep}*til)${OLD}|\saved${SUBSTNEW}|;til ${OLD=0}
    • これはスクリプト全体で唯一のループであり、置換が必要な可能性のある$ {OLD}値が複数含まれる場合、各文字列に対して出力される2番目の_%Path_のみをループします。私が想像した他のすべてのソリューションは、2番目のsedプロセスを必要とし、短いループは望ましくないかもしれませんが、確かにプロセス全体の生成と分岐を打ち負かします。
    • 基本的にここでsedが行うことは$ {sed_sep}の検索であり、それを見つけたら、$ {OLD}が見つかるまでそれと遭遇したすべての文字を保存し、$ {NEW}に置き換えます。次に、$ {sed_sep}に戻り、文字列内で複数回発生した場合に再び$ {OLD}を探します。見つからない場合は、変更された文字列をstdoutに出力し(次に次にキャッチします)、ループを終了します。
    • これにより、文字列全体を解析する必要がなくなり、もちろん$ {OLD}を含める必要があるmvコマンド文字列の前半が含まれるようになり、後半は何度も変更されます。 mvの宛先パスから$ {OLD}名を消去するために必要です。
  • sed -ex...-ex search|%dir_depth(save*)${sed_sep}|(only_saved)|out
    • ここでの2つの_-exec_呼び出しは、2番目のforkなしで発生します。最初に見てきたように、必要に応じてmvの_-printf_関数コマンドによって提供されるfindコマンドを変更して、$ {OLD}のすべての参照を$ {NEWに適切に変更します}、しかし、そうするために、最終出力に含まれてはならない任意の参照ポイントを使用する必要がありました。したがって、sedが必要なことをすべて終えたら、参照バッファーを渡す前に参照バッファーを消去するように指示します。

そして今すぐ戻ってきました

readは、次のようなコマンドを受け取ります。

_% mv /path2/$SRC/$OLD_DIR/$OLD_FILE /same/path_w/$NEW_DIR/$NEW_FILE \000
_

readを_${msg}_として_${sh_io}_に変換し、関数の外部で自由に調べることができます。

クール。

-マイク

1
mikeserv

私が好きなramtamの答えでは、検索部分は正常に機能しますが、パスにスペースが含まれている場合は残りは機能しません。私はsedにあまり精通していませんが、その答えを次のように修正することができました。

find . -name "*_test.rb" | Perl -pe 's/^((.*_)test.rb)$/"\1" "\2spec.rb"/' | xargs -n2 mv

私のユースケースでは最終的なコマンドが次のように見えるため、このような変更が本当に必要でした

find . -name "olddir" | Perl -pe 's/^((.*)olddir)$/"\1" "\2new directory"/' | xargs -n2 mv
1
dzs0000

onitakeが提案することで、スペースを含むファイル名を処理できました。

パスにスペースまたは文字列testが含まれる場合、これはブレークしません

find . -name "*_test.rb" -print0 | while read -d $'\0' file
do
    echo mv "$file" "$(echo $file | sed s/test/spec/)"
done
1
James

Ruby(1.9+)がある場合

Ruby -e 'Dir["**/*._test.rb"].each{|x|test(?f,x) and File.rename(x,x.gsub(/_test/,"_spec") ) }'
1
kurumi

トリックを行うニースワンライナーを紹介します。 Sedは、特に-n 2を指定したxargsによって複数の変数が渡される場合、この権利を処理できません。

find ./spec -type f -name "*_test.rb" -print0 | xargs -0 -I {} sh -c 'export file={}; mv $file ${file/_test.rb/_spec.rb}'

-type -fを追加すると、移動操作はファイルのみに制限され、-print 0はパス内の空のスペースを処理します。

0
Orsiris de Jong
$ find spec -name "*_test.rb"
spec/dir2/a_test.rb
spec/dir1/a_test.rb

$ find spec -name "*_test.rb" | xargs -n 1 /usr/bin/Perl -e '($new=$ARGV[0]) =~ s/test/spec/; system(qq(mv),qq(-v), $ARGV[0], $new);'
`spec/dir2/a_test.rb' -> `spec/dir2/a_spec.rb'
`spec/dir1/a_test.rb' -> `spec/dir1/a_spec.rb'

$ find spec -name "*_spec.rb"
spec/dir2/b_spec.rb
spec/dir2/a_spec.rb
spec/dir1/a_spec.rb
spec/dir1/c_spec.rb
0
Damodharan R

Find utilsおよびsed正規表現タイプで名前変更を行うより安全な方法:

  mkdir ~/practice

  cd ~/practice

  touch classic.txt.txt

  touch folk.txt.txt

次のように「.txt.txt」拡張子を削除します-

  cd ~/practice

  find . -name "*txt" -execdir sh -c 'mv "$0" `echo "$0" | sed -r 's/\.[[:alnum:]]+\.[[:alnum:]]+$//'`' {} \;

;の代わりに+を使用する場合バッチモードで動作するために、上記のコマンドは、最初に一致したファイルのみを名前変更しますが、「find」によって一致するファイルのリスト全体は名前変更しません。

  find . -name "*txt" -execdir sh -c 'mv "$0" `echo "$0" | sed -r 's/\.[[:alnum:]]+\.[[:alnum:]]+$//'`' {} +
0
Sathish

あなたの質問はsedに関するもののようですが、再帰的な名前変更の目標を達成するために、私がここで与えた別の答えから恥知らずに引き裂かれた次のものをお勧めします: bashの再帰的な名前変更

#!/bin/bash
IFS=$'\n'
function RecurseDirs
{
for f in "$@"
do
  newf=echo "${f}" | sed -e 's/^(.*_)test.rb$/\1spec.rb/g'
    echo "${f}" "${newf}"
    mv "${f}" "${newf}"
    f="${newf}"
  if [[ -d "${f}" ]]; then
    cd "${f}"
    RecurseDirs $(ls -1 ".")
  fi
done
cd ..
}
RecurseDirs .
0
dreynold

質問に少し関係があるので、この投稿を共有します。詳細を提供しないで申し訳ありません。それが他の誰かを助けることを願っています。 http://www.peteryu.ca/tutorials/shellscripting/batch_rename

0
Breton F.