Cambiar el nombre de los archivos de forma recursiva usando find y sed

85

Quiero revisar varios directorios y cambiar el nombre de todos los archivos que terminan en _test.rb para que terminen en _spec.rb. Es algo que nunca he descubierto del todo cómo hacer con bash, así que esta vez pensé en poner un poco de esfuerzo para lograrlo. Sin embargo, 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 adicional después del exec para que el comando se imprima en lugar de ejecutarse mientras lo estoy probando.

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

mv original original

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

opsb
fuente
Por cierto, soy consciente de que hay un comando de cambio de nombre, pero realmente me gustaría descubrir cómo hacerlo usando sed para poder hacer comandos más poderosos en el futuro.
opsb
2
No realices publicaciones cruzadas .
Pausado hasta nuevo aviso.

Respuestas:

32

Esto sucede porque sedrecibe 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, de forma recursiva. 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 sedcanalización de tal manera que la findejecute para cada archivo, ya findque 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 -cy un montón de comillas para hacer todo esto en un comando, pero no lo intentaré).

Fred Foo
fuente
27
Para aquellos que se preguntan sobre el uso perverso de sh -c aquí está: find spec -name "* _test.rb" -exec sh -c 'echo mv "$ 1" "$ (echo" $ 1 "| sed s / test.rb \ $ / spec.rb /) "'_ {} \;
opsb
1
@opsb ¿para qué diablos es eso _? gran solución, pero me gusta la respuesta ramtam más :)
iRaS
¡Salud! Me ahorró muchos dolores de cabeza. En aras de la integridad, así es como lo canalizo a un script: find. -nombre "archivo" -exec sh /path/to/script.sh {} \;
Sven M.
128

Para resolverlo de la manera más cercana al problema original, probablemente se usaría la opción xargs "argumentos 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 de forma recursiva, repite el nombre del archivo original ( p) y luego un nombre modificado ( s/test/spec/) y lo alimenta todo mven pares ( xargs -n2). Tenga en cuenta que, en este caso, la ruta en sí no debería contener una cadena test.

ramtam
fuente
9
Desafortunadamente, esto tiene problemas de espacios en blanco. Entonces, usar con carpetas que tienen espacios en el nombre lo romperá en xargs (confirme con -p para el modo detallado / interactivo)
cde
1
Eso es exactamente lo que estaba buscando. Lástima por el problema de los espacios en blanco (aunque no lo probé). Pero para mis necesidades actuales es perfecto. Sugeriría probarlo primero con "echo" en lugar de "mv" como parámetro en "xargs".
Michele Dall'Agata
5
Si necesita lidiar con espacios en blanco en las rutas y está usando GNU sed> = 4.2.2, entonces puede usar la -zopción junto con -print0find y xargs -0:find -name '*._test.rb' -print0 | sed -ze "p;s/test/spec/" | xargs -0 -n2 mv
Evan Purkhiser
Mejor solución. Mucho más rápido que find -exec. Gracias
Miguel A. Baldi Hörlle
Esto no funcionará si hay varias testcarpetas en una ruta. sedsolo cambiará el nombre del primero y el mvcomando fallará por No such file or directoryerror.
Casey
22

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
ajreal
fuente
Parece una buena forma de hacerlo. Sin embargo, realmente estoy buscando romper el trazador de líneas, para mejorar mi conocimiento más que cualquier otra cosa.
opsb
2
para el archivo en $ (buscar. -nombre "* _test.rb"); hacer echo mv $ file echo $file | sed s/_test.rb$/_spec.rb/; hecho es una sola línea, ¿no?
Bretticus
5
Esto no funcionará si tiene nombres de archivo con espacios. forlos dividirá en palabras separadas. Puede hacer que funcione indicando al bucle for que se divida solo en líneas nuevas. Consulte cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html para ver ejemplos.
onitake
Estoy de acuerdo con @onitake, aunque preferiría usar la -execopción de buscar.
ShellFish
18

Encuentro este más corto

find . -name '*_test.rb' -exec bash -c 'echo mv $0 ${0/test.rb/spec.rb}' {} \;
csg
fuente
Hola, creo que ' _test.rb "debería ser' _test.rb '(comillas dobles a comillas simples). ¿Puedo preguntar por qué está usando el guión bajo para empujar el argumento que desea colocar $ 1 cuando me parece que find . -name '*_test.rb' -exec bash -c 'echo mv $0 ${0/test.rb/spec.rb}' {} \;funciona ? Como lo haríafind . -name '*_test.rb' -exec bash -c 'echo mv $1 ${1/test.rb/spec.rb}' iAmArgumentZero {} \;
agtb
Gracias por sus sugerencias, corregido
CSG
Gracias por aclarar eso, solo comenté porque pasé un tiempo reflexionando sobre el significado de _ pensando que tal vez fue un uso engañoso de $ _ (¡'_' es bastante difícil de buscar en documentos!)
agtb
9

Puedes hacerlo sin sed, si quieres:

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

${var%%suffix}se quita suffixdel 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
Wayne Conrad
fuente
esto no funciona (el seduno) como se explica en la respuesta aceptada.
Ali
@Ali, funciona. Lo probé yo mismo cuando escribí la respuesta. @ explicación de larsman no se aplica a for i in... ; do ... ; done, que ejecuta comandos a través de la cáscara y no entiende de acento grave.
Wayne Conrad
9

Mencionas que estás usando bashcomo tu caparazón, en cuyo caso realmente no necesitas findysed para lograr el cambio de nombre por lotes que buscas ...

Suponiendo que está usando bashcomo su shell:

$ echo $SHELL
/bin/bash
$ _

... y suponiendo que haya habilitado la llamada globstaropción de shell:

$ shopt -p globstar
shopt -s globstar
$ _

... y finalmente asumiendo que ha instalado la renameutilidad (que se encuentra en el util-linux-ngpaquete)

$ which rename
/usr/bin/rename
$ _

... entonces puede lograr el cambio de nombre del lote en una sola línea de bash de la siguiente manera:

$ rename _test _spec **/*_test.rb

(la globstaropción de shell asegurará que bash encuentre todos los *_test.rbarchivos coincidentes , sin importar cuán profundamente estén anidados en la jerarquía de directorios ... utilícelo help shoptpara averiguar cómo configurar la opción)

pvandenberk
fuente
7

La forma más sencilla :

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

La forma más rápida (suponiendo que tenga 4 procesadores):

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

Si tiene una gran cantidad de archivos para procesar, es posible que la lista de nombres de archivos enviados 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 -bo cat /proc/meminfopara encontrar la cantidad de RAM con la que tiene que trabajar; De lo contrario, utilice topla aplicación de monitorización de actividad 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/
l3x
fuente
2

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

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

Para esto no es necesario sed. Puede quedarse perfectamente solo con un whilebucle alimentado con el resultado de finduna sustitución de proceso .

Entonces, si tiene una findexpresión que selecciona los archivos necesarios, use la sintaxis:

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

Esto findarchivará y cambiará el nombre de todos ellos separando la cadena _test.rbdesde el final y agregando _spec.rb.

Para este paso usamos Shell Parameter Expansion donde ${var%string}elimina la "cadena" de patrón coincidente más corto $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
fedorqui 'ASÍ que deja de hacer daño'
fuente
¡Muchas gracias! Me ayudó a eliminar fácilmente el .gz final de todos los nombres de archivo de forma recursiva. while IFS= read -r file; do mv $file ${file%.gz}; done < <(find -type f -name "*.gz")
Vinay Vissh
1
@CasualCoder es bueno leer eso :) Tenga en cuenta que puede decir directamente find .... -exec mv .... Además, tenga cuidado con $fileya que fallará si contiene espacios. Mejor use las comillas "$file".
fedorqui 'SO deja de dañar'
1

si tienes Ruby (1.9+)

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

En la respuesta de ramtam que me gusta, la parte de búsqueda funciona bien, pero el resto 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
dzs0000
fuente
1

No tengo el corazón para hacerlo de nuevo, pero escribí esto en respuesta a Commandline Find Sed Exec . Allí, el autor de la pregunta quería saber cómo mover un árbol completo, posiblemente excluyendo uno o dos directorios, y cambiar el nombre de todos los archivos y directorios que contienen la cadena "OLD" para que en su lugar contengan "NEW" .

Además de describir el cómo con minuciosa verbosidad continuación, este método también puede ser único en el sentido de que incorpora la depuración incorporada. Básicamente, no hace nada en absoluto como está escrito, excepto compilar y guardar 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 sedbúsqueda recursiva de más de una coincidencia del patrón, no hay otra recursividad que yo sepa.

Y por último, esto está completamente nulldelimitado: no se tropieza con ningún carácter en ningún nombre de archivo excepto en 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 <<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: Lo anterior functiones probable que requieren GNUversiones de sedy findpara manejar apropiadamente la find printfe sed -z -ey:;recursive regex test;t llamadas. Si estos no están disponibles para usted, es probable que la funcionalidad se pueda duplicar con algunos ajustes menores.

Esto debería hacer todo lo que quería de principio a fin con muy poco alboroto. Lo hice forkcon sed, pero también estaba practicando algunas sedtécnicas recursivas de ramificación y por eso estoy aquí. Es como conseguir un corte de pelo con descuento en una escuela de peluquería, supongo. Aquí está el flujo de trabajo:

  • rm -rf ${UNNECESSARY}
    • Dejé de lado intencionalmente cualquier llamada funcional que pudiera borrar o destruir datos de cualquier tipo. Mencionas que eso ./apppodría no ser deseado. Elimínelo o muévalo a otro lugar de antemano o, alternativamente, puede crear una \( -path PATTERN -exec rm -rf \{\} \)rutina findpara hacerlo de forma programática, pero esa es toda suya.
  • _mvnfind "${@}"
    • Declare sus argumentos y llame a la función de trabajo. ${sh_io}es especialmente importante porque guarda el retorno de la función. ${sed_sep}viene en un segundo cercano; esta es una cadena arbitraria utilizada para hacer referencia a sedla recursividad en la función. Si ${sed_sep}se establece en un valor que podría encontrarse en cualquiera de sus rutas o nombres de archivo sobre los que se actúa ... bueno, simplemente no lo permita.
  • mv -n $1 $2
    • Todo el árbol se mueve desde el principio. Ahorrará muchos dolores de cabeza; créame. El resto de lo que quiere hacer, el cambio de nombre, es simplemente una cuestión de metadatos del sistema de archivos. Si, por ejemplo, estuviera moviendo esto de una unidad a otra, o cruzando los límites del sistema de archivos de cualquier tipo, es mejor que lo haga de una vez con un comando. También es más seguro. Tenga en cuenta la -noclobberopción establecida para mv; como está escrito, esta función no pondrá ${SRC_DIR}donde ${TGT_DIR}ya existe.
  • read -R SED <<HEREDOC
    • Ubiqué todos los comandos de sed aquí para evitar 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 findproceso. Con findsolo buscamos cualquier cosa que necesite cambiar de nombre porque ya hicimos todas las mvoperaciones de lugar a lugar con el primer comando de la función. En lugar de realizar una acción directa con find, como una execllamada, por ejemplo, la usamos para construir la línea de comandos dinámicamente con -printf.
  • %dir-depth :tab: 'mv '%path-to-${SRC}' '${sed_sep}'%path-again :null delimiter:'
    • Después de findubicar los archivos que necesitamos, compila e imprime directamente (la mayoría ) del comando que necesitaremos para procesar su cambio de nombre. La %dir-depthtachuela al principio de cada línea ayudará a asegurarnos de 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. findutiliza todo tipo de técnicas de optimización para recorrer el árbol del sistema de archivos y no es seguro que devuelva los datos que necesitamos en un orden seguro para las operaciones. Es por eso que a continuación ...
  • sort -general-numerical -zero-delimited
    • Ordenamos todos findlos resultados en función de %directory-depthpara que se trabajen primero las rutas más cercanas en relación a $ {SRC}. Esto evita posibles errores que involucren mvarchivos en 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 recorre el segundo %Pathimpreso para cada cadena en caso de que contenga más de un valor $ {OLD} que pueda necesitar ser reemplazado. Todas las demás soluciones que imaginé implicaban un segundo sedproceso, y aunque un ciclo corto puede no ser deseable, sin duda es mejor que generar y bifurcar un proceso completo.
    • Entonces, básicamente, lo que sedhace aquí es buscar $ {sed_sep}, luego, habiéndolo encontrado, lo guarda y todos los caracteres que encuentra hasta que encuentra $ {OLD}, que luego reemplaza con $ {NEW}. Luego regresa 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 stdout(que luego captura de nuevo a continuación) y finaliza el ciclo.
    • Esto evita tener que analizar toda la cadena y asegura que la primera mitad de la mvcadena de comando, que debe incluir $ {OLD} por supuesto, sí la incluya, y la segunda mitad se modifica tantas veces como sea necesario para borrar el $ {OLD} nombre de mvla ruta de destino.
  • sed -ex...-ex search|%dir_depth(save*)${sed_sep}|(only_saved)|out
    • Las dos -execllamadas aquí ocurren sin un segundo fork. En el primero, como hemos visto, modificamos el mvcomando proporcionado por findel -printfcomando de función según sea necesario para alterar correctamente todas las referencias de $ {OLD} a $ {NEW}, pero para hacerlo tuvimos que usar algunos puntos de referencia arbitrarios que no deben incluirse en el resultado final. Así que una vezsed termina todo lo que necesita hacer, le indicamos que borre sus puntos de referencia del búfer de retención antes de pasarlo.

Y ahora estamos de vuelta

read recibirá un comando que se ve así:

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

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

Frio.

-Miguel

mikeserv
fuente
1

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

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

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

Este es un ejemplo que debería funcionar en todos los casos. Funciona de forma recursiva, solo necesita 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
eldy
fuente
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
Damodharan R
fuente
Ah ... no conozco otra forma de usar sed que no sea poner la lógica en un script de shell y llamarlo en exec. no vio el requisito de usar sed inicialmente
Damodharan R
0

Su pregunta parece ser sobre sed, pero para lograr su objetivo de cambio de nombre recursivo, sugeriría lo siguiente, extraído descaradamente 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 .
Dreynold
fuente
¿Cómo sedfunciona sin escapar ()si no configura la -ropción?
mikeserv
0

Una forma más segura de hacer el cambio de nombre con find utils y tipo de expresión regular sed:

  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 modo por lotes, el comando anterior cambiará el nombre solo del primer archivo coincidente, pero no de la lista completa de coincidencias de archivos por 'buscar'.

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

Aquí hay un buen delineador que funciona. Sed no puede manejar esto correctamente, especialmente si xargs pasa múltiples variables con -n 2. Una sustitució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.

Orsiris de Jong
fuente
0

Esta es mi solución de trabajo:

for FILE in {{FILE_PATTERN}}; do echo ${FILE} | mv ${FILE} $(sed 's/{{SOURCE_PATTERN}}/{{TARGET_PATTERN}}/g'); done
Antonio Petricca
fuente