Rinominare ricorsivamente i file usando find e sed

Voglio passare attraverso un gruppo di directory e rinominare tutti i file che terminano in _test.rb per terminare invece in _spec.rb. È qualcosa che non ho mai capito bene come fare con bash quindi questa volta ho pensato di mettere un po ‘di impegno per farlo inchiodare. Sono arrivato così lontano però, il mio massimo sforzo è:

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

NB: c’è un echo extra dopo exec in modo che il comando venga stampato invece di correre mentre lo sto testando.

Quando lo eseguo, l’output per ogni nome file abbinato è:

 mv original original 

cioè la sostituzione con sed è andata perduta. Qual è il trucco?

Questo accade perché sed riceve la stringa {} come input, come può essere verificata con:

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

che stampa foofoo per ogni file nella directory, in modo ricorsivo. Il motivo di questo comportamento è che la pipeline viene eseguita una volta, dalla shell, quando espande l’intero comando.

Non c’è modo di citare la pipeline sed in modo tale che find lo eseguirà per ogni file, dal momento che find non esegue comandi tramite la shell e non ha alcuna nozione di pipeline o backquote. Il manuale GNU findutils spiega come eseguire un’attività simile inserendo la pipeline in uno script di shell separato:

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

(Potrebbe esserci un modo perverso di usare sh -c e una tonnellata di virgolette per fare tutto questo in un unico comando, ma non ci proverò.)

Per risolverlo in un modo il più vicino al problema originale probabilmente userebbe l’opzione “args per riga di comando” di xargs:

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

Trova i file nella directory di lavoro corrente in modo ricorsivo, fa eco al nome del file originale ( p ) e poi a un nome modificato ( s/test/spec/ ) e lo xargs -n2 tutto a mv a coppie ( xargs -n2 ). Attenzione che in questo caso il percorso stesso non dovrebbe contenere un test stringa.

potresti volerlo considerare in un altro modo

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

Lo trovo più corto

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

Puoi farlo senza sed, se vuoi:

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

${var%%suffix} rimuove il suffix dal valore di var .

o, per farlo usando sed:

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

Tu dici che stai usando bash come shell, nel qual caso non hai davvero bisogno di find e sed per raggiungere il rinominare batch che stai find

Supponendo che stai usando bash come shell:

 $ echo $SHELL /bin/bash $ _ 

… e assumendo che tu abbia abilitato la cosiddetta opzione della shell globstar :

 $ shopt -p globstar shopt -s globstar $ _ 

… e infine assumendo che tu abbia installato l’utility rename (che si trova nel util-linux-ng )

 $ which rename /usr/bin/rename $ _ 

… quindi è ansible raggiungere la ridenominazione del batch in una bash one-liner come segue:

 $ rename _test _spec **/*_test.rb 

(l’opzione shell globstar garantirà che bash trovi tutti i file *_test.rb corrispondenti, indipendentemente da quanto siano nidificati nella gerarchia delle directory … usa help shopt per scoprire come impostare l’opzione)

Il modo più semplice :

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

Il modo più veloce (supponendo che tu abbia 4 processori):

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

Se si desidera elaborare un numero elevato di file, è ansible che l’elenco di nomi di file collegati a xargs causi il superamento della lunghezza massima consentita dalla riga di comando risultante.

Puoi controllare il limite del tuo sistema usando getconf ARG_MAX

Sulla maggior parte dei sistemi Linux è ansible utilizzare free -b o cat /proc/meminfo per trovare la quantità di RAM con cui lavorare; In caso contrario, utilizzare l’app di monitoraggio dell’attività del sistema in top o in top .

Un modo più sicuro (supponendo che abbiate 1000000 byte di ram con cui lavorare):

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

se hai Ruby (1.9+)

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

Nella risposta di ramtam che mi piace, la porzione di ricerca funziona correttamente ma il resto non funziona se il percorso contiene spazi. Non ho molta familiarità con sed, ma sono stato in grado di modificare la risposta a:

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

Avevo davvero bisogno di un cambiamento come questo perché nel mio caso d’uso il comando finale sembra più simile

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

Non ho il coraggio di rifarlo tutto da capo, ma l’ho scritto in risposta a Commandline Find Sed Exec . Lì il richiedente voleva sapere come spostare un intero albero, escludendo eventualmente una o due directory, e rinominare tutti i file e le directory contenenti la stringa “OLD” per contenere invece “NEW” .

Oltre a descrivere il modo con scrupolosa verbosità di seguito, questo metodo può anche essere unico in quanto incorpora il debug integrato. Fondamentalmente non fa nulla di niente come scritto tranne la compilazione e salva su una variabile tutti i comandi che crede dovrebbe fare per eseguire il lavoro richiesto.

Evita anche esplicitamente i loop il più ansible. Oltre alla ricerca ricorsiva sed di più di una corrispondenza del modello, non c’è altra ricorsione per quanto ne so.

E per ultimo, questo è del tutto delimitato da null – non incide su nessun carattere in nessun nome di file tranne il null . Non penso che dovresti averlo.

A proposito, questo è VERAMENTE veloce. Guarda:

 % _mvnfind() { mv -n "${1}" "${2}" && cd "${2}" > read -r 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 <  Prepared commands saved in variable: ${6} > To view do: printf ${6} | tr "\000" "\n" > To run do: sh <  $(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 )  0.06s user 0.03s system 106% cpu 0.090 total  Lines Words Bytes 115 362 20691 -  mv .config/replacement_word-chrome-beta/Default/.../googlestars \ .config/replacement_word-chrome-beta/Default/.../replacement_wordstars 

NOTA: La function sopra richiederà probabilmente le versioni GNU di sed e find per gestire correttamente i risultati find printf e sed -z -e e :;recursive regex test;t chiamate. Se questi non sono disponibili, è probabile che la funzionalità venga duplicata con alcune modifiche minori.

Questo dovrebbe fare tutto ciò che volevi dall’inizio alla fine con pochissimo rumore. Ho fatto un fork con sed , ma stavo anche praticando alcune tecniche di branching ricorsivo sed , ecco perché sono qui. È come avere un taglio di capelli scontato in una scuola di barbiere, immagino. Ecco il stream di lavoro:

  • rm -rf ${UNNECESSARY}
    • Ho intenzionalmente escluso qualsiasi chiamata funzionale che potrebbe eliminare o distruggere dati di qualsiasi tipo. Dici che ./app potrebbe non essere desiderato. Eliminalo o spostalo altrove, o, in alternativa, potresti creare una \( -path PATTERN -exec rm -rf \{\} \) per find di farlo a livello di programmazione, ma questo è tutto tuo.
  • _mvnfind "${@}"
    • Dichiara i suoi argomenti e chiama la funzione worker. ${sh_io} è particolarmente importante in quanto salva il ritorno dalla funzione. ${sed_sep} arriva al secondo; questa è una stringa arbitraria utilizzata per fare riferimento alla ricorsione di sed nella funzione. Se ${sed_sep} è impostato su un valore che potrebbe potenzialmente essere trovato in uno qualsiasi dei tuoi nomi di percorso o file, agisci su … beh, non lasciartelo fare.
  • mv -n $1 $2
    • L’intero albero viene spostato dall’inizio. Risparmierà un sacco di mal di testa; credimi. Il resto di ciò che si vuole fare – la ridenominazione – è semplicemente una questione di metadati del filesystem. Se, ad esempio, lo stessi spostando da un’unità all’altra o attraverso i confini del file system di qualsiasi tipo, è meglio farlo subito con un solo comando. È anche più sicuro. Nota l’opzione -noclobber impostata per mv ; come scritto, questa funzione non inserirà ${SRC_DIR} dove esiste già ${TGT_DIR} .
  • read -R SED <
    • Ho individuato tutti i comandi di sed qui per risparmiare sulla scocciatura dei problemi e leggerli in una variabile da inserire in sed sotto. Spiegazione sotto.

  • find . -name ${OLD} -printf
    • Iniziamo il processo di find . Con find cerchiamo solo qualcosa che ha bisogno di ridenominazione perché abbiamo già fatto tutte le operazioni mv place-to-place con il primo comando della funzione. Piuttosto che prendere qualsiasi azione diretta con find , ad esempio, come una chiamata exec , la usiamo invece per build dynamicmente la riga di comando con -printf .
  • %dir-depth :tab: 'mv '%path-to-${SRC}' '${sed_sep}'%path-again :null delimiter:'
    • Dopo la find trova i file che ci servono direttamente costruisce e stampa (la maggior parte ) del comando di cui avremo bisogno per elaborare la tua rinomina. La directory %dir-depth aggiunta all'inizio di ogni riga aiuterà a garantire che non stiamo cercando di rinominare un file o una directory nell'albero con un object padre che deve ancora essere rinominato. find utilizza tutti i tipi di tecniche di ottimizzazione per percorrere l'albero del filesystem e non è sicuro che restituirà i dati di cui abbiamo bisogno in un ordine sicuro per le operazioni. Questo è il motivo per cui ...
  • sort -general-numerical -zero-delimited
    • Ordiniamo tutto l'output di find base a %directory-depth modo che i percorsi più vicini in relazione a $ {SRC} vengano elaborati per primi. Ciò evita possibili errori che coinvolgono i file mv in posizioni inesistenti e riduce al minimo la necessità di ricorrere al ciclo ricorsivo. ( in effetti, potresti avere difficoltà a trovare un loop )
  • sed -ex :rcrs;srch|(save${sep}*til)${OLD}|\saved${SUBSTNEW}|;til ${OLD=0}
    • Penso che questo sia l'unico ciclo dell'intero script e circoli solo sul secondo %Path stampato per ogni stringa nel caso in cui contenga più di un valore $ {OLD} che potrebbe essere necessario sostituire. Tutte le altre soluzioni che immaginavo implicavano un secondo processo di sed , e anche se un ciclo breve può non essere desiderabile, certamente batte spawning e bifora un intero processo.
    • Quindi, in pratica, ciò che sed fa è la ricerca di $ {sed_sep}, quindi, dopo averlo trovato, lo salva e tutti i personaggi che incontra finché non trova $ {OLD}, che poi sostituisce con $ {NEW}. Quindi torna a $ {sed_sep} e cerca di nuovo $ {OLD}, nel caso si verifichi più di una volta nella stringa. Se non viene trovato, stampa la stringa modificata su stdout (che poi cattura di nuovo) e termina il ciclo.
    • Questo evita di dover analizzare l'intera stringa e assicura che la prima metà della stringa di comando mv , che deve includere $ {OLD} ovviamente, la includa, e la seconda metà viene modificata tante volte quanto è necessario per cancellare il nome $ {OLD} dal percorso di destinazione di mv .
  • sed -ex...-ex search|%dir_depth(save*)${sed_sep}|(only_saved)|out
    • Le due chiamate -exec qui avvengono senza una seconda fork . Nel primo, come abbiamo visto, modifichiamo il comando mv come fornito dal comando find 's -printf function necessario per modificare correttamente tutti i riferimenti di $ {OLD} a $ {NEW}, ma per fare ciò noi ha dovuto usare alcuni punti di riferimento arbitrari che non dovrebbero essere inclusi nell'output finale. Quindi, una volta che sed terminato tutto ciò che deve fare, lo istruiamo a cancellare i suoi punti di riferimento dal buffer di attesa prima di passarlo.

E ORA STIAMO INDIETRO INTORNO

read riceverà un comando simile al seguente:

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

Lo read in ${msg} come ${sh_io} che può essere esaminato a volontà al di fuori della funzione.

Freddo.

-Mike

Sono stato in grado di gestire nomi di file con spazi seguendo gli esempi suggeriti da onitake.

Questo non si interrompe se il percorso contiene spazi o il test della stringa:

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

Questo è un esempio che dovrebbe funzionare in tutti i casi. Funziona in modo ricorsivo, richiede solo shell e supporta nomi di file con spazi.

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

Ecco cosa ha funzionato per me quando i nomi dei file contenevano degli spazi. L’esempio seguente rinomina ricorsivamente tutti i file .dar in file .zip:

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

Per questo non hai bisogno di sed . Si può perfettamente stare da soli con un ciclo while alimentato con il risultato di find attraverso una sostituzione di processo .

Quindi se hai un’espressione find che seleziona i file necessari, usa la syntax:

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

Ciò find file e li rinominerà tutti _test.rb la stringa _test.rb dalla fine e aggiungendo _spec.rb .

Per questo passo usiamo Shell Parameter Expansion dove ${var%string} rimuove la "stringa" di pattern di corrispondenza più breve da $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 

Guarda un esempio:

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

La tua domanda sembra essere su sed, ma per raggiungere il tuo objective di rinominare ricorsivamente, ti suggerirei quanto segue, spudoratamente strappato da un’altra risposta che ho dato qui: rinominare in modo ricorsivo in 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 . 

Modo più sicuro di rinominare con i programmi di utilità di ricerca e il tipo di espressione regolare di sed:

  mkdir ~/practice cd ~/practice touch classic.txt.txt touch folk.txt.txt 

Rimuovi l’estensione “.txt.txt” come segue:

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

Se usi il + al posto di; per poter lavorare in modalità batch, il comando precedente rinominerà solo il primo file di corrispondenza, ma non l’intero elenco di corrispondenze di file di “trova”.

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

Ecco una bella oneliner che fa il trucco. Sed non può gestire questo diritto, specialmente se più variabili vengono passate da xargs con -n 2. Una sottostruttura bash gestirà questo facilmente come:

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

Aggiungendo -type -f limiterà le operazioni di spostamento solo ai file, -print 0 gestirà gli spazi vuoti nei percorsi.