Rinomina ricorsivamente i file usando find e sed


87

Voglio passare attraverso un mucchio di directory e rinominare tutti i file che terminano con _test.rb per finire invece in _spec.rb. È qualcosa che non ho mai capito come fare con bash, quindi questa volta ho pensato di fare un po 'di sforzo per farlo inchiodare. Finora sono arrivato a breve, il mio miglior sforzo è:

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

NB: c'è un eco in più dopo exec in modo che il comando venga stampato invece di essere eseguito mentre lo sto testando.

Quando lo eseguo, l'output per ogni nome di file corrispondente è:

mv original original

cioè la sostituzione con sed è stata persa. Qual è il trucco?


A proposito, sono consapevole del fatto che esiste un comando di ridenominazione, ma mi piacerebbe davvero capire come farlo usando sed in modo da poter eseguire comandi più potenti in futuro.
opsb

2
Per favore non cross-post .
Dennis Williamson

Risposte:


32

Questo accade perché sedriceve la stringa {}come input, come si può verificare con:

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

che stampa foofooper ogni file nella directory, in modo ricorsivo. La ragione di questo comportamento è che la pipeline viene eseguita una volta, dalla shell, quando espande l'intero comando.

Non c'è modo di citare la sedpipeline in modo tale findda eseguirla per ogni file, poiché findnon esegue comandi tramite la shell e non ha la nozione di pipeline o backquote. Il manuale findutils GNU 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 -ce un sacco di virgolette per fare tutto questo in un unico comando, ma non ho intenzione di provare.)


27
Per coloro che si interrogano sull'uso perverso di sh -c eccolo qui: find spec -name "* _test.rb" -exec sh -c 'echo mv "$ 1" "$ (echo" $ 1 "| sed s / test.rb \ $ / spec.rb /) "'_ {} \;
opsb

1
@opsb a cosa diavolo è questo _? ottima soluzione - ma mi piace di più la risposta ramtam :)
iRaS

Saluti! Mi ha risparmiato un sacco di mal di testa. Per motivi di completezza, ecco come lo installo in uno script: find. -name "file" -exec sh /path/to/script.sh {} \;
Sven M.

131

Per risolverlo in un modo più vicino al problema originale sarebbe probabilmente utilizzare l'opzione xargs "args per command line":

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 quindi a un nome modificato ( s/test/spec/) e invia il tutto a mvcoppie ( xargs -n2). Attenzione che in questo caso il percorso stesso non dovrebbe contenere una stringa test.


9
Purtroppo questo ha problemi di spazio bianco. Quindi l'utilizzo con cartelle che hanno spazi nel nome lo interromperà in xargs (confermare con -p per la modalità dettagliata / interattiva)
cde

1
È esattamente quello che stavo cercando. Peccato per il problema dello spazio bianco (però non l'ho provato). Ma per le mie esigenze attuali è perfetto. Suggerirei di provarlo prima con "echo" invece di "mv" come parametro in "xargs".
Michele Dall'Agata

5
Se hai bisogno di gestire gli spazi bianchi nei percorsi e stai usando GNU sed> = 4.2.2, puoi usare l' -zopzione insieme a -print0find e xargs -0:find -name '*._test.rb' -print0 | sed -ze "p;s/test/spec/" | xargs -0 -n2 mv
Evan Purkhiser

Soluzione migliore. Molto più veloce di find -exec. Grazie
Miguel A. Baldi Hörlle

Questo non funzionerà, se ci sono più testcartelle in un percorso. sedrinominerà solo il primo e il mvcomando fallirà in caso di No such file or directoryerrore.
Casey

22

potresti voler considerare un altro modo come

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

Sembra un buon modo per farlo. Sto davvero cercando di rompere l'unico liner, per migliorare la mia conoscenza più di ogni altra cosa.
opsb

2
per il file in $ (trova. -name "* _test.rb"); fare echo mv $ file echo $file | sed s/_test.rb$/_spec.rb/; fatto è un one-liner, non è vero?
Bretticus

5
Questo non funzionerà se hai nomi di file con spazi. forli dividerà in parole separate. Puoi farlo funzionare istruendo il ciclo for a dividere solo sulle nuove righe. Vedi cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html per esempi.
onitake

Sono d'accordo con @onitake, anche se preferirei utilizzare l' -execopzione da find.
ShellFish

18

Trovo questo più corto

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

Ciao, ' _test.rb" dovrebbe essere ' _test.rb'(virgolette a singolo apice). Posso chiedere perché si sta utilizzando la sottolineatura per spingere l'argomento che si desidera posizionare $ 1 quando mi sembra che find . -name '*_test.rb' -exec bash -c 'echo mv $0 ${0/test.rb/spec.rb}' {} \;le opere ? Come sarebbefind . -name '*_test.rb' -exec bash -c 'echo mv $1 ${1/test.rb/spec.rb}' iAmArgumentZero {} \;
agtb

Grazie per i tuoi suggerimenti, risolti
csg

Grazie per aver
chiarito questo aspetto

9

Puoi farlo senza sed, se vuoi:

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

${var%%suffix}toglie suffixil valore di var.

oppure, per farlo usando sed:

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

questo non funziona ( sedquello) come spiegato dalla risposta accettata.
Ali

@ Ali, funziona - l'ho testato io stesso quando ho scritto la risposta. @ spiegazione di larsman non si applica a for i in... ; do ... ; done, che esegue i comandi attraverso il guscio e non capisce backtick.
Wayne Conrad

9

Dici che stai usando bashcome shell, nel qual caso non ne hai effettivamente bisogno finde sedper ottenere la ridenominazione batch che stai cercando ...

Supponendo che tu stia usando bashcome shell:

$ echo $SHELL
/bin/bash
$ _

... e supponendo che tu abbia abilitato la cosiddetta globstaropzione di shell:

$ shopt -p globstar
shopt -s globstar
$ _

... e infine supponendo che tu abbia installato l' renameutilità (che si trova nel util-linux-ngpacchetto)

$ which rename
/usr/bin/rename
$ _

... quindi puoi ottenere il batch rinominare in un bash one-liner come segue:

$ rename _test _spec **/*_test.rb

(l' globstaropzione shell garantirà che bash trovi tutti i *_test.rbfile corrispondenti , non importa quanto profondamente siano annidati nella gerarchia di directory ... usa help shoptper scoprire come impostare l'opzione)


7

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 dispone di un numero elevato di file da elaborare, è possibile che l'elenco dei nomi di file reindirizzati a xargs causi il superamento della lunghezza massima consentita della riga di comando risultante.

Puoi controllare il limite del tuo sistema usando getconf ARG_MAX

Sulla maggior parte dei sistemi Linux puoi usare free -bo cat /proc/meminfoper trovare la quantità di RAM con cui lavorare; Altrimenti, usa topo la tua app di monitoraggio dell'attività dei sistemi.

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

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

2

Ecco cosa ha funzionato per me quando i nomi dei file contenevano spazi. L'esempio seguente rinomina in modo ricorsivo tutti i file .dar in file .zip:

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

2

Per questo non hai bisogno sed. Puoi perfettamente stare da solo con un whileciclo alimentato con il risultato di finduna sostituzione di processo .

Quindi, se hai findun'espressione che seleziona i file necessari, usa la sintassi:

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

Questo findarchivierà e rinominerà tutti loro eliminando la stringa _test.rbdalla fine e aggiungendola _spec.rb.

Per questo passaggio utilizziamo Shell Parameter Expansion in cui ${var%string}rimuove la "stringa" del 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

Molte grazie! Mi ha aiutato a rimuovere facilmente .gz finale da tutti i nomi di file in modo ricorsivo. while IFS= read -r file; do mv $file ${file%.gz}; done < <(find -type f -name "*.gz")
Vinay Vissh

1
@CasualCoder bello da leggere :) Nota che puoi dire direttamente find .... -exec mv .... Inoltre, fai attenzione $filepoiché fallirà se contiene spazi. Usa meglio le citazioni "$file".
fedorqui 'SO stop harming'

1

se hai Ruby (1.9+)

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

1

Nella risposta di ramtam che mi piace, la parte di ricerca funziona bene ma il resto no se il percorso ha spazi. Non ho molta familiarità con sed, ma sono stato in grado di modificare quella risposta a:

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

Avevo davvero bisogno di una modifica come questa 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

1

Non ho il coraggio di rifare 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 come con minuziosa verbosità di seguito, questo metodo può anche essere unico in quanto incorpora il debug integrato. Fondamentalmente non fa nulla come scritto tranne che per compilare e salvare in una variabile tutti i comandi che crede di dover eseguire per eseguire il lavoro richiesto.

Inoltre evita esplicitamente i loop il più possibile. Oltre alla sedricerca ricorsiva di più di una corrispondenza del modello, non c'è altra ricorsione per quanto ne so.

Infine, questo è completamente nulldelimitato: non inciampa su alcun carattere in nessun nome di file tranne il null. Non penso che dovresti averlo.

A proposito, è DAVVERO veloce. Guarda:

% _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        

NOTA: quanto sopra functionrichiederà probabilmente le GNUversioni di sede findper gestire correttamente le chiamate find printfe sed -z -ee :;recursive regex test;t. Se questi non sono disponibili, la funzionalità può essere probabilmente duplicata con alcune piccole modifiche.

Questo dovrebbe fare tutto quello che volevi dall'inizio alla fine con pochissime storie. L'ho fatto forkcon sed, ma mi è stato anche praticato alcune sedtecniche di ramificazione ricorsiva Ecco, questo è il motivo per cui sono qui. È un po 'come farsi un taglio di capelli scontato in una scuola di barbiere, immagino. Ecco il flusso di lavoro:

  • rm -rf ${UNNECESSARY}
    • Ho intenzionalmente omesso qualsiasi chiamata funzionale che potrebbe eliminare o distruggere dati di qualsiasi tipo. Hai detto che ./apppotrebbe essere indesiderato. Eliminalo o spostalo altrove in anticipo o, in alternativa, potresti creare una \( -path PATTERN -exec rm -rf \{\} \)routine findper farlo in modo programmatico, ma è 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 in un secondo vicino; questa è una stringa arbitraria utilizzata per fare riferimento alla sedricorsione nella funzione. Se ${sed_sep}è impostato su un valore che potrebbe essere potenzialmente trovato in uno qualsiasi dei nomi di file o percorsi su cui si è agito ... beh, non lasciarlo.
  • mv -n $1 $2
    • L'intero albero viene spostato dall'inizio. Risparmierà un sacco di mal di testa; credimi. Il resto di quello che vuoi fare - la ridenominazione - è semplicemente una questione di metadati del filesystem. Se, ad esempio, lo spostassi da un'unità a un'altra, o oltre i confini del filesystem di qualsiasi tipo, faresti meglio a farlo subito con un comando. È anche più sicuro. Notare l' -noclobberopzione impostata per mv; come scritto, questa funzione non verrà inserita ${SRC_DIR}dove ${TGT_DIR}esiste già.
  • read -R SED <<HEREDOC
    • Ho trovato tutti i comandi di sed qui per risparmiare sui problemi di fuga e li ho letti in una variabile da alimentare a sed di seguito. Spiegazione di seguito.
  • find . -name ${OLD} -printf
    • Iniziamo il findprocesso. Con findcerchiamo solo tutto ciò che deve essere rinominato perché abbiamo già eseguito tutte le operazioni da luogo a luogo mvcon il primo comando della funzione. Piuttosto che intraprendere qualsiasi azione diretta con find, come una execchiamata, ad esempio, la usiamo invece per costruire dinamicamente la riga di comando con -printf.
  • %dir-depth :tab: 'mv '%path-to-${SRC}' '${sed_sep}'%path-again :null delimiter:'
    • Dopo aver findindividuato i file di cui abbiamo bisogno, crea e stampa direttamente (la maggior parte ) del comando che ci servirà per elaborare la ridenominazione. Il %dir-depthvirato all'inizio di ogni riga aiuterà a garantire che non stiamo cercando di rinominare un file o una directory nell'albero con un oggetto genitore che deve ancora essere rinominato. findutilizza tutti i tipi di tecniche di ottimizzazione per percorrere il tuo albero del filesystem e non è una cosa sicura che restituirà i dati di cui abbiamo bisogno in un ordine sicuro per le operazioni. Questo è il motivo per cui la prossima volta ...
  • sort -general-numerical -zero-delimited
    • Ordiniamo tutto findl'output di in base a in %directory-depthmodo che i percorsi più vicini in relazione a $ {SRC} vengano elaborati per primi. Ciò evita possibili errori che coinvolgono mvfile ing in posizioni inesistenti e riduce al minimo la necessità di cicli ricorsivi. ( 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 si ripete solo sul secondo %Pathstampato per ogni stringa nel caso in cui contenga più di un valore $ {OLD} che potrebbe essere necessario sostituire. Tutte le altre soluzioni che immaginavo prevedevano un secondo sedprocesso e, sebbene un ciclo breve possa non essere desiderabile, certamente batte la generazione e il fork di un intero processo.
    • Quindi fondamentalmente quello che sedfa qui è cercare $ {sed_sep}, quindi, dopo averlo trovato, lo salva e tutti i caratteri 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 stdout(che poi riprende di nuovo) e termina il ciclo.
    • Ciò evita di dover analizzare l'intera stringa e garantisce che la prima metà della mvstringa di comando, che deve includere $ {OLD} ovviamente, la includa e la seconda metà venga modificata tutte le volte che è necessario cancellare il $ {OLD} nome dal mvpercorso di destinazione di.
  • sed -ex...-ex search|%dir_depth(save*)${sed_sep}|(only_saved)|out
    • Le due -execchiamate qui avvengono senza un secondo fork. Nel primo, come abbiamo visto, abbiamo modificare il mvcomando come fornito da find's -printfcomando funzione come necessario modificare correttamente tutti i riferimenti di $ {OLD} a $ {NEW}, ma per farlo abbiamo dovuto usare un po' punti di riferimento arbitrari che non dovrebbero essere inclusi nell'output finale. Quindi, una volta sedterminato tutto ciò che deve fare, gli chiediamo di cancellare i suoi punti di riferimento dal buffer di blocco prima di passarlo.

E ORA SIAMO TORNATI INTORNO

read riceverà un comando simile a questo:

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

Sarà readin ${msg}quanto ${sh_io}può essere esaminato a piacimento al di fuori della funzione.

Freddo.

-Mike


1

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

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

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

1

Questo è un esempio che dovrebbe funzionare in tutti i casi. Funziona in modo ricorsivo, necessita solo di 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

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

Ah..non sono a conoscenza di un modo per usare sed diverso dal mettere la logica in uno script di shell e chiamarla in exec. inizialmente non ha visto l'obbligo di utilizzare sed
Damodharan R

0

La tua domanda sembra riguardare sed, ma per raggiungere il tuo obiettivo di rinominare ricorsivamente, suggerirei quanto segue, spudoratamente strappato da un'altra risposta che ho dato qui: rinomina ricorsiva 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 .

Come sedfunziona senza scappare ()se non si imposta l' -ropzione?
mikeserv

0

Modo più sicuro per rinominare con find utils e tipo di espressione regolare 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 lavorare in modalità batch, il comando precedente rinominerà solo il primo file corrispondente, ma non l'intero elenco di file corrispondenti con "trova".

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

0

Ecco un bel oneliner che fa il trucco. Sed non può gestirlo correttamente, specialmente se più variabili vengono passate da xargs con -n 2. Una sostituzione bash lo gestirà facilmente come:

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

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



0

Questa è la mia soluzione di lavoro:

for FILE in {{FILE_PATTERN}}; do echo ${FILE} | mv ${FILE} $(sed 's/{{SOURCE_PATTERN}}/{{TARGET_PATTERN}}/g'); done
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.