Cambiar el nombre recursivamente de los archivos usando find y sed

Quiero ir a través de un grupo de directorios y renombrar todos los archivos que terminan en _test.rb para terminar en _spec.rb. Es algo que nunca he descifrado cómo hacerlo con bash, así que esta vez pensé en esforzarme para lograrlo. Hasta ahora me he quedado corto, mi mejor esfuerzo es:

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

NB: hay un eco extra después de la ejecución para que el comando se imprima en lugar de ejecutar mientras lo estoy probando.

Cuando lo ejecuto, la salida para cada nombre de archivo coincidente es:

 mv original original 

es decir, la sustitución por sed se ha perdido. ¿Cuál es el truco?

Esto sucede porque sed recibe la cadena {} como entrada, como se puede verificar con:

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

que imprime foofoo para cada archivo en el directorio, recursivamente. La razón de este comportamiento es que la tubería se ejecuta una vez, por el shell, cuando expande todo el comando.

No hay forma de citar la tubería sed de tal manera que find ejecute para cada archivo, ya que find no ejecuta comandos a través del shell y no tiene noción de canalizaciones o comillas inversas. El manual de GNU findutils explica cómo realizar una tarea similar colocando la tubería en un script de shell separado:

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

(Puede haber alguna forma perversa de usar sh -c y un montón de comillas para hacer todo esto en un comando, pero no voy a intentarlo).

Para resolverlo de una manera más cercana al problema original, probablemente usaría la opción xargs “args por línea de comando”:

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

Encuentra los archivos en el directorio de trabajo actual recursivamente, repite el nombre del archivo original ( p ) y luego un nombre modificado ( s/test/spec/ ) y lo envía todo a mv en pares ( xargs -n2 ). Tenga en cuenta que en este caso la ruta en sí no debe contener una test cadena.

es posible que desee considerar de otra manera como

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

Encuentro este más corto

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

Puedes hacerlo sin sed, si quieres:

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

${var%%suffix} elimina el suffix del valor de var .

o, para hacerlo usando sed:

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

Mencionas que estás usando bash como tu caparazón, en cuyo caso no necesitas find y sed para lograr el cambio de nombre del lote que estás buscando …

Asumiendo que estás usando bash como tu caparazón:

 $ echo $SHELL /bin/bash $ _ 

… y suponiendo que haya activado la llamada opción de shell globstar :

 $ shopt -p globstar shopt -s globstar $ _ 

… y finalmente, suponiendo que haya instalado la utilidad de rename (que se encuentra en el paquete util-linux-ng )

 $ which rename /usr/bin/rename $ _ 

… entonces puedes lograr el cambio de nombre del lote en un bash one-liner de la siguiente manera:

 $ rename _test _spec **/*_test.rb 

(La opción de shell globstar asegurará que bash encuentre todos los archivos *_test.rb que *_test.rb , sin importar cuán profundamente estén nesteds en la jerarquía del directorio … use help shopt para descubrir cómo establecer la opción)

La forma más fácil :

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

La manera más rápida (suponiendo que tienes 4 procesadores):

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

Si tiene que procesar una gran cantidad de archivos, es posible que la lista de nombres de archivos conectados a xargs haga que la línea de comando resultante exceda la longitud máxima permitida.

Puede verificar el límite de su sistema usando getconf ARG_MAX

En la mayoría de los sistemas Linux, puede usar free -b o cat /proc/meminfo para encontrar la cantidad de RAM con la que tiene que trabajar; De lo contrario, utilice la aplicación del monitor de actividad top o de su sistema.

Una forma más segura (suponiendo que tenga 1000000 bytes de RAM para trabajar):

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

si tienes Ruby (1.9+)

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

En la respuesta de Ramtam que me gusta, la parte de búsqueda funciona bien, pero el rest no, si la ruta tiene espacios. No estoy muy familiarizado con sed, pero pude modificar esa respuesta a:

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

Realmente necesitaba un cambio como este porque en mi caso de uso el comando final se parece más a

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

No tengo corazón para volver a hacerlo, pero escribí esto en respuesta a Commandline Find Sed Exec . Allí, el asker quería saber cómo mover un árbol completo, posiblemente excluyendo un directorio o dos, y cambiar el nombre de todos los archivos y directorios que contenían la cadena “OLD” para contener “NEW” en su lugar .

Además de describir cómo con minuciosa verbosidad a continuación, este método también puede ser único en el sentido de que incorpora una depuración incorporada. Básicamente, no hace nada como está escrito excepto que comstack y guarda en una variable todos los comandos que cree que debería hacer para realizar el trabajo solicitado.

También evita explícitamente los bucles tanto como sea posible. Además de la búsqueda recursiva sed para más de una coincidencia del patrón, no hay otra recursión hasta donde yo sé.

Y, por último, esto está completamente delimitado por null : no se dispara en ningún carácter en ningún nombre de archivo excepto el null . No creo que debas tener eso.

Por cierto, esto es REALMENTE rápido. Mira:

 % _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 anterior probablemente requiera versiones de GNU de sed y find para manejar adecuadamente las :;recursive regex test;t find printf y sed -z -e y :;recursive regex test;t . Si no están disponibles para usted, es probable que la funcionalidad se duplique con algunos ajustes menores.

Esto debería hacer todo lo que quisieras de principio a fin con muy poco alboroto. Hice fork con sed , pero también practicaba algunas técnicas de ramificación recursiva sed , por eso estoy aquí. Es como obtener un corte de pelo con descuento en una escuela de peluquería, supongo. Aquí está el flujo de trabajo:

  • rm -rf ${UNNECESSARY}
    • Intencionalmente dejé fuera cualquier llamada funcional que pudiera eliminar o destruir datos de cualquier tipo. Usted menciona que ./app podría no ser deseado. Elimínelo o muévalo a otro lugar de antemano, o, como alternativa, podría comstackr una \( -path PATTERN -exec rm -rf \{\} \) para find hacerlo programáticamente, pero ese es todo suyo.
  • _mvnfind "${@}"
    • Declare sus argumentos y llame a la función de trabajador. ${sh_io} es especialmente importante ya que guarda el rendimiento de la función. ${sed_sep} viene en un segundo ${sed_sep} ; esta es una cadena arbitraria utilizada para hacer referencia a la recursión de sed en la función. Si ${sed_sep} se establece en un valor que podría encontrarse en cualquiera de tus nombres de ruta o de archivo sobre los que ${sed_sep} … bueno, simplemente no lo dejes.
  • mv -n $1 $2
    • Todo el árbol se mueve desde el principio. Le ahorrará muchos dolores de cabeza; créame. El rest de lo que quieres hacer, el cambio de nombre, es simplemente una cuestión de metadatos del sistema de archivos. Si estuvieras, por ejemplo, moviendo esto de una unidad a otra, o a través de los límites del sistema de archivos de cualquier tipo, será mejor que lo hagas de inmediato con un comando. También es más seguro. Tenga en cuenta la opción -noclobber establecida para mv ; como está escrito, esta función no pondrá ${SRC_DIR} donde ya existe ${TGT_DIR} .
  • read -R SED <
    • Localicé todos los comandos de sed aquí para ahorrar en problemas de escape y los leí en una variable para alimentar a sed a continuación. Explicación a continuación.
  • find . -name ${OLD} -printf
    • Comenzamos el proceso de find . Con find solo buscamos cualquier cosa que necesite cambiar de nombre porque ya hicimos todas las operaciones de mv lugar a lugar con el primer comando de la función. En lugar de tomar cualquier acción directa con find , como una llamada exec , por ejemplo, en su lugar la usamos para construir la línea de comando de forma dinámica con -printf .
  • %dir-depth :tab: 'mv '%path-to-${SRC}' '${sed_sep}'%path-again :null delimiter:'
    • Después de find ubica los archivos que necesitamos, los construye e imprime directamente (la mayoría ) del comando que necesitaremos para procesar su cambio de nombre. El %dir-depth pegado al principio de cada línea ayudará a asegurar que no estamos intentando cambiar el nombre de un archivo o directorio en el árbol con un objeto principal que aún no se ha cambiado de nombre. find utiliza todo tipo de técnicas de optimización para recorrer su árbol de sistema de archivos y no es seguro que devuelva los datos que necesitamos en un orden de seguridad para las operaciones. Es por eso que ahora ...
  • sort -general-numerical -zero-delimited
    • Ordenamos todo el resultado de find basado en %directory-depth para que las rutas más cercanas en relación con $ {SRC} se trabajen primero. Esto evita posibles errores que impliquen la mv de archivos a ubicaciones inexistentes, y minimiza la necesidad de bucles recursivos. ( de hecho, es posible que tenga dificultades para encontrar un bucle )
  • sed -ex :rcrs;srch|(save${sep}*til)${OLD}|\saved${SUBSTNEW}|;til ${OLD=0}
    • Creo que este es el único bucle en todo el script, y solo gira sobre el segundo %Path impreso para cada cadena en caso de que contenga más de un valor $ {OLD} que pueda necesitar ser reemplazado. Todas las otras soluciones que imaginé involucraron un segundo proceso de sed , y aunque un ciclo corto puede no ser deseable, ciertamente supera el proceso de desove y bifurcación.
    • Básicamente, lo que sed hace aquí es buscar $ {sed_sep}, luego, al encontrarlo, lo guarda y todos los caracteres que encuentra hasta que encuentra $ {OLD}, que luego reemplaza con $ {NEW}. Luego vuelve a $ {sed_sep} y busca nuevamente $ {OLD}, en caso de que ocurra más de una vez en la cadena. Si no se encuentra, imprime la cadena modificada a stdout (que luego vuelve a capturar) y finaliza el ciclo.
    • Esto evita tener que analizar toda la cadena y garantiza que la primera mitad de la cadena de comandos mv , que debe incluir $ {OLD} por supuesto, la incluye, y la segunda mitad se modifica tantas veces como sea necesario para borrar el $ {OLD} nombre de la ruta de destino de mv .
  • sed -ex...-ex search|%dir_depth(save*)${sed_sep}|(only_saved)|out
    • Las dos llamadas a -exec ocurren aquí sin una segunda fork . En el primero, como hemos visto, modificamos el comando mv como lo proporciona el comando find 's -printf function según sea necesario para alterar correctamente todas las referencias de $ {OLD} a $ {NEW}, pero para ello tuvo que usar algunos puntos de referencia arbitrarios que no deberían incluirse en el resultado final. Así que una vez que sed finaliza todo lo que necesita hacer, le ordenamos que borre sus puntos de referencia del buffer de retención antes de pasarlo.

Y AHORA ESTAMOS VOLVIENDO ALREDEDOR

read recibirá un comando que se ve así:

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

Lo read en ${msg} como ${sh_io} que se puede examinar a voluntad fuera de la función.

Guay.

-Micro

Pude manejar nombres de archivo con espacios siguiendo los ejemplos sugeridos por onitake.

Esto no se rompe si la ruta contiene espacios o la test cadena:

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

Este es un ejemplo que debería funcionar en todos los casos. Funciona de forma recursiva, necesita solo shell y admite nombres de archivos con espacios.

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

Esto es lo que funcionó para mí cuando los nombres de los archivos tenían espacios en ellos. El ejemplo siguiente recursivamente cambia el nombre de todos los archivos .dar a archivos .zip:

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

Para esto no necesitas sed . Puede perfectamente quedarse solo con un ciclo while alimentado con el resultado de find través de una sustitución de proceso .

Entonces, si tiene una expresión de find que selecciona los archivos necesarios, entonces use 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") 

Esto find archivos y les _test.rb el nombre a todos ellos, _test.rb la cadena _test.rb del final y anexando _spec.rb .

Para este paso, utilizamos la expansión del parámetro Shell, donde ${var%string} elimina el patrón de coincidencia más corto "cadena" de $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 

Vea un ejemplo:

 $ 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 

Su pregunta parece ser sobre sed, pero para lograr su objective de renombrar recursivamente, sugeriría lo siguiente, desvergonzadamente arrancado de otra respuesta que di aquí: cambio de nombre recursivo en 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 . 

Manera más segura de renombrar con find utils y sed tipo de expresión regular:

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

Elimine la extensión “.txt.txt” de la siguiente manera:

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

Si usa + en lugar de; para trabajar en el modo por lotes, el comando anterior cambiará el nombre del primer archivo coincidente, pero no la lista completa de coincidencias de archivos por ‘buscar’.

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

Aquí hay un buen oneliner que hace el truco. Sed no puede manejar este derecho, especialmente si xargs pasa múltiples variables con -n 2. Una substición de bash manejaría esto fácilmente como:

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

Agregar -type -f limitará las operaciones de movimiento a archivos solamente, -print 0 manejará espacios vacíos en las rutas.