reemplazar - sustituir bash
Cambiar el nombre recursivamente de los archivos usando find y sed (18)
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?
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.
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
Encuentro este más corto
find . -name ''*_test.rb'' -exec bash -c ''echo mv $0 ${0/test.rb/spec.rb}'' {} /;
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/`"'' {} /;
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).
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:]]+$//''`'' {} +
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 anidados en la jerarquía del directorio ... use help shopt
para descubrir cómo establecer la opción)
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 compila 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 <<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: 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 compilar una/( -path PATTERN -exec rm -rf /{/} /)
parafind
hacerlo programáticamente, pero ese es todo suyo.
- Intencionalmente dejé fuera cualquier llamada funcional que pudiera eliminar o destruir datos de cualquier tipo. Usted menciona que
-
_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 desed
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.
- Declare sus argumentos y llame a la función de trabajador.
-
mv -n $1 $2
- Todo el árbol se mueve desde el principio. Le ahorrará muchos dolores de cabeza; créame. El resto 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 paramv
; como está escrito, esta función no pondrá${SRC_DIR}
donde ya existe${TGT_DIR}
.
- Todo el árbol se mueve desde el principio. Le ahorrará muchos dolores de cabeza; créame. El resto 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
-
read -R SED <<HEREDOC
- 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
. Confind
solo buscamos cualquier cosa que necesite cambiar de nombre porque ya hicimos todas las operaciones demv
lugar a lugar con el primer comando de la función. En lugar de tomar cualquier acción directa confind
, como una llamadaexec
, por ejemplo, en su lugar la usamos para construir la línea de comando de forma dinámica con-printf
.
- Comenzamos el proceso de
-
%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 ...
- Después de
-
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 lamv
de archivos a ubicaciones inexistentes, y minimiza la necesidad de bucles recursivos. ( de hecho, es posible que tenga dificultades para encontrar un bucle )
- Ordenamos todo el resultado de
-
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 desed
, 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 astdout
(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 demv
.
- Creo que este es el único bucle en todo el script, y solo gira sobre el segundo
-
sed -ex...-ex search|%dir_depth(save*)${sed_sep}|(only_saved)|out
- Las dos llamadas a
-exec
ocurren aquí sin una segundafork
. En el primero, como hemos visto, modificamos el comandomv
como lo proporciona el comandofind
''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 quesed
finaliza todo lo que necesita hacer, le ordenamos que borre sus puntos de referencia del buffer de retención antes de pasarlo.
- Las dos llamadas a
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
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 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 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
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.
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
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
Su pregunta parece ser sobre sed, pero para lograr su objetivo 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 .
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
si tienes Ruby (1.9+)
ruby -e ''Dir["**/*._test.rb"].each{|x|test(?f,x) and File.rename(x,x.gsub(/_test/,"_spec") ) }''
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/
$ 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