findとsedを使用してファイルの名前を再帰的に変更します


87

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

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

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

実行すると、一致した各ファイル名の出力は次のようになります。

mv original original

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


ところで、renameコマンドがあることは知っていますが、将来、より強力なコマンドを実行できるように、sedを使用してそれを実行する方法を本当に理解したいと思います。
opsb 2011年

2
クロスポストしないでください。
デニスウィリアムソン

回答:


32

これsedは、次の{}ように確認できるように、入力として文字列を受け取るために発生します。

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

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

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

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

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


27
sh -cのひねくれた使用法について疑問に思っている人は、次のようになります。findspec -name "* _test.rb" -exec sh -c'echo mv "$ 1" "$(echo" $ 1 "| sed s / test.rb \ $ / spec.rb /) "'_ {} \;
opsb 2011年

1
@opsb _は一体何のためですか?素晴らしい解決策-しかし、私はラムタムの答えがもっと好きです:)
iRaS 2015年

乾杯!私は多くの頭痛の種を救った。完全を期すために、これは私がそれをスクリプトにパイプする方法です:find。-name "file" -exec sh /path/to/script.sh {} \;
Sven M.

131

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

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

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


9
残念ながら、これには空白の問題があります。したがって、名前にスペースが含まれているフォルダーで使用すると、xargsで壊れます(詳細/インタラクティブモードの場合は-pで確認してください)
cde 2014

1
それがまさに私が探していたものです。空白の問題にはあまりにも悪いです(私はそれをテストしませんでしたが)。しかし、私の現在のニーズには完璧です。最初に、「xargs」のパラメーターとして「mv」ではなく「echo」を使用してテストすることをお勧めします。
Michele Dall'Agata 2016年

5
パス内の空白を処理する必要があり、GNU sed> = 4.2.2を使用している-z場合は、finds-print0およびxargsとともにオプションを使用できます-0find -name '*._test.rb' -print0 | sed -ze "p;s/test/spec/" | xargs -0 -n2 mv
Evan Purkhiser 2016

最良の解決策。find-execよりもはるかに高速です。ありがとう
ミゲルA.バルディHörlle

test1つのパスに複数のフォルダーがある場合、これは機能しません。sed最初の名前を変更するだけで、mvコマンドはNo such file or directoryエラー時に失敗します。
ケーシー

22

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

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

それはそれを行うための良い方法のように見えます。何よりも知識を向上させるために、私は本当にワンライナーをクラックしたいと思っています。
opsb 2011年

2
$(find .-name "* _test.rb");内のファイルの場合 echo mv $ fileを実行しますecho $file | sed s/_test.rb$/_spec.rb/; 完了、ワンライナーでそれはないですか?
Bretticus 2013

5
スペースを含むファイル名がある場合、これは機能しません。forそれらを別々の単語に分割します。forループに改行でのみ分割するように指示することで、これを機能させることができます。例については、cyberciti.biz / Tips /handling-filenames-with-spaces-in-bash.htmlを参照してください。
onitake 2014

私は@onitakeに同意し-execますが、findのオプションを使用したいと思います。
ShellFish 2015年

18

これは短いと思います

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

こんにちは、私は「考える_test.rb」でなければなりません『 _test.rb』(単一引用符を二重引用符を)。私はそれがように私には思えるとき、あなたは$ 1の位置決めしたい引数をプッシュするアンダースコアを使用している理由を求めることができfind . -name '*_test.rb' -exec bash -c 'echo mv $0 ${0/test.rb/spec.rb}' {} \;た作品?同様にfind . -name '*_test.rb' -exec bash -c 'echo mv $1 ${1/test.rb/spec.rb}' iAmArgumentZero {} \;
agtb 2011年

提案をありがとう、修正
csg 2011年

それをクリアしてくれてありがとう-私は_の意味を考えてしばらく過ごしたのでコメントしただけですそれはおそらく$ _のいくつかのトリック使用でした( '_'はドキュメントで検索するのはかなり難しいです!)
agtb 2011年

9

必要に応じて、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

sed受け入れられた答えで説明されているように、これは機能しません(1つ)。
アリ

@アリ、それは機能します-私が答えを書いたときに私はそれを自分でテストしました。@ larsmanの説明はには適用されませんfor i in... ; do ... ; doneシェル経由でコマンドを実行している、ないバッククォート理解しています。
ウェインコンラッド

9

あなたが使用していることを言及bash、あなたのシェルとして、その場合には、あなたが実際に必要としないfindと、sedあなたは後にしている名前を変更するバッチを達成するために...

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

$ echo $SHELL
/bin/bash
$ _

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

$ shopt -p globstar
shopt -s globstar
$ _

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

$ which rename
/usr/bin/rename
$ _

...次に、bashワンライナーで次のようにバッチの名前変更を行うことができます。

$ rename _test _spec **/*_test.rb

globstarシェルオプションは*_test.rb、ディレクトリ階層にネストされている深さに関係なく、bashが一致するすべてのファイルを確実help shoptに検出するようにします...オプションの設定方法を見つけるために使用します)


7

最も簡単な方法

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 -bcat /proc/meminfo、使用する必要のあるRAMの量を見つけることができます。それ以外の場合は、topまたはシステムアクティビティモニターアプリを使用してください。

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

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

2

ファイル名にスペースが含まれている場合、これが私にとってうまくいったことです。以下の例では、すべての.darファイルの名前を.zipファイルに再帰的に変更します。

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

2

このためにあなたは必要ありませんsedプロセス置換whilefind介した結果が供給されるループで完全に孤独になることができます。

したがって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ます。

このステップのために我々は使用シェルパラメータ展開${var%string}から最短一致パターン「文字列」を削除します$var

$ 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

どうもありがとう!これは、すべてのファイル名から末尾の.gzを再帰的に簡単に削除するのに役立ちました。while IFS= read -r file; do mv $file ${file%.gz}; done < <(find -type f -name "*.gz")
Vinay Vissh 2017

1
@CasualCoderそれを読んでうれしいです:)あなたが直接言うことができることに注意してくださいfind .... -exec mv ...。また、$fileスペースが含まれていると失敗しますのでご注意ください。引用符を使用することをお勧めします"$file"
fedorqui'SOは害をやめる '2017

1

Ruby(1.9+)をお持ちの場合

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

1

私が好きな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

もう一度やり直す気はありませんが、Commandline Find SedExecに答えてこれを書きました。そこで質問者は、おそらくディレクトリを1つか2つ除いて、ツリー全体を移動し、文字列「OLD」を含むすべてのファイルとディレクトリの名前を「NEW」を含むように変更する方法を知りたがっていました。

ほかに記述する方法冗長性を骨の折れると、それはデバッグビルトイン組み込んだで以下を、この方法にも固有のものとすることができます。基本的に、要求された作業を実行するために実行する必要があると思われるすべてのコマンドをコンパイルして変数に保存する以外は、記述されているとおりには何も実行しません。

また、ループを可能な限り明示的に回避しますパターンのsed複数の一致を再帰的に検索する以外に、私が知る限り、他の再帰はありません。

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

ちなみに、これは本当に速いです。見てください:

% _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意志がおそらく必要GNUのバージョンをsedし、find適切に処理するようにfind printfし、sed -z -eかつ:;recursive regex test;t呼び出し。これらが利用できない場合は、いくつかの小さな調整を加えるだけで機能が複製される可能性があります。

これにより、最初から最後まで、ほとんど手間をかけずにすべてを実行できます。私はforkでやりましたがsedsed再帰的な分岐手法も練習していたので、ここにいます。理髪学校で割引ヘアカットをするようなものだと思います。ワークフローは次のとおりです。

  • rm -rf ${UNNECESSARY}
    • あらゆる種類のデータを削除または破壊する可能性のある機能呼び出しを意図的に省略しました。あなたはそれ./appが望ましくないかもしれないと言います。事前に削除するか、別の場所に移動するか、\( -path PATTERN -exec rm -rf \{\} \)ルーチンfindを組み込んでプログラムで実行することもできますが、それはすべてあなたのものです。
  • _mvnfind "${@}"
    • 引数を宣言し、worker関数を呼び出します。${sh_io}関数からの戻り値を保存するという点で特に重要です。${sed_sep}すぐに来ます。これはsed、関数内のの再帰を参照するために使用される任意の文字列です。${sed_sep}が作用するパス名またはファイル名のいずれかに含まれる可能性のある値に設定されている場合は、そのままにしないでください。
  • mv -n $1 $2
    • ツリー全体が最初から移動されます。それは多くの頭痛を救うでしょう。私を信じてください。あなたがしたい残りのこと-名前の変更-は単にファイルシステムのメタデータの問題です。たとえば、これをあるドライブから別のドライブに移動したり、あらゆる種類のファイルシステムの境界を越えて移動したりする場合は、1つのコマンドで一度に移動することをお勧めします。また、より安全です。;に-noclobber設定されたオプションに注意してくださいmv。書かれているように、この関数はすでに存在する${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突き止め我々はそれが直接構築し、(プリントアウトし必要なファイルほとんどのコマンドのは)私たちはあなたの名前の変更を処理する必要があります。%dir-depth各行の先頭にタックは、私たちが名前を変更するまだ親オブジェクトとツリー内のファイルやディレクトリの名前を変更しようとしていないことを保証するのに役立ちます。findあらゆる種類の最適化手法を使用してファイルシステムツリーをウォークしますが、必要なデータが安全な順序で返されるかどうかは定かではありません。これが私たちが次にする理由です...
  • sort -general-numerical -zero-delimited
    • $ {SRC}に最も近いパスが最初に機能findする%directory-depthように、すべての出力をに基づいて並べ替えます。これにより、mvファイルを存在しない場所に移動することで発生する可能性のあるエラーが回避され、再帰的なループの必要性が最小限に抑えられます。(実際、ループを見つけるのは難しいかもしれません
  • sed -ex :rcrs;srch|(save${sep}*til)${OLD}|\saved${SUBSTNEW}|;til ${OLD=0}
    • これはスクリプト全体で唯一のループで%Pathあり、置換が必要な可能性のある複数の$ {OLD}値が含まれている場合は、文字列ごとに出力された2番目のループのみをループすると思います。私が想像した他のすべてのソリューションには2番目のsedプロセスが含まれ、短いループは望ましくないかもしれませんが、プロセス全体の生成とフォークに勝るものは確かです。
    • つまり、基本的にsedここで行うことは、$ {sed_sep}を検索し、それを見つけたら、$ {OLD}が見つかるまでそれと遭遇したすべての文字を保存し、それを$ {NEW}に置き換えます。次に、$ {sed_sep}に戻り、文字列内で複数回発生した場合に備えて、$ {OLD}を再度検索します。見つからない場合は、変更された文字列をに出力しstdout(次に再びキャッチします)、ループを終了します。
    • これにより、文字列全体を解析する必要がなくなり、mvコマンド文字列の前半(もちろん$ {OLD}を含める必要があります)にそれが含まれ、後半はワイプに必要な回数だけ変更されます。mvの宛先パスからの$ {OLD}名。
  • sed -ex...-ex search|%dir_depth(save*)${sed_sep}|(only_saved)|out
    • ここでの2つの-exec呼び出しは、秒なしで発生しforkます。最初に、これまで見てきたように、$ {OLD}から$ {NEW}へのすべての参照を適切に変更するために、必要に応じて's function commandmvによって提供されるコマンドを変更しますが、そうするためにいくつかを使用する必要がありました最終出力に含めるべきではない任意の参照点。したがって、必要な作業がすべて終了したら、保持バッファから参照ポイントを消去してから渡すように指示します。 find-printfsed

そして今、私たちは戻ってきました

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

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

これは、意志readにそれを${msg}として${sh_io}機能する意志の外側で検査することができます。

涼しい。

-マイク


1

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

パスにスペースまたは文字列が含まれている場合、これ壊れませんtest

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

1

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

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

0
$ 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

ああ..私は、ロジックをシェルスクリプトに入れて、それをexecで呼び出す以外に、sedを使用する方法を認識していません。最初にsedを使用する要件が
わかりませんでした

0

あなたの質問は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 .

オプションを設定しない場合、sedエスケープせずにどのように機能しますか?()-r
mikeserv 2013

0

findutilsと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

これがそのトリックを行う素晴らしいワンライナーです。特に、複数の変数が-n 2を指定してxargsによって渡される場合、Sedはこの権利を処理できません。bash置換はこれを次のように簡単に処理します。

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

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



0

これは私の実用的な解決策です:

for FILE in {{FILE_PATTERN}}; do echo ${FILE} | mv ${FILE} $(sed 's/{{SOURCE_PATTERN}}/{{TARGET_PATTERN}}/g'); done
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.