Cambiar múltiples archivos

194

El siguiente comando está cambiando correctamente el contenido de 2 archivos.

sed -i 's/abc/xyz/g' xaa1 xab1 

Pero lo que tengo que hacer es cambiar dinámicamente varios de estos archivos y no sé los nombres de los archivos. Quiero escribir un comando que lea todos los archivos del directorio actual comenzando xa*y que seddebería cambiar el contenido del archivo.

shantanuo
fuente
61
Quieres decir sed -i 's/abc/xyz/g' xa*?
Paul R
3
Las respuestas aquí no son suficientes. Ver unix.stackexchange.com/questions/112023/…
Isaac

Respuestas:

135

Mejor todavía:

for i in xa*; do
    sed -i 's/asd/dfg/g' $i
done

porque nadie sabe cuántos archivos hay y es fácil romper los límites de la línea de comandos.

Esto es lo que sucede cuando hay demasiados archivos:

# grep -c aaa *
-bash: /bin/grep: Argument list too long
# for i in *; do grep -c aaa $i; done
0
... (output skipped)
#
lenik
fuente
18
Si hay tantos archivos, romperá el límite de la línea de comando en el forcomando. Para protegerte de eso, deberías usarlofind ... | xargs ...
Glenn Jackman
1
No sé la implementación, pero el patrón "xa *" tiene que expandirse en algún momento. ¿El shell hace la expansión de manera diferente forpara echoo grep?
Glenn Jackman
44
ver la respuesta actualizada si necesita más información, haga una pregunta oficial para que la gente pueda ayudarlo.
lenik
55
En el comando sed, debe usarlo en "$i"lugar de $ievitar la división de palabras en nombres de archivos con espacios. De lo contrario, esto es muy agradable.
Comodín el
44
Con respecto a la lista, creo que la diferencia es que fores parte de la sintaxis del lenguaje, no solo una versión incorporada. Para sed -i 's/old/new' *, la expansión de *TODO debe pasar como un arglista para sed, y estoy bastante seguro de que esto tiene que suceder antes de que el sedproceso pueda comenzar. Usando el forbucle, el arglista completo (la expansión de *) nunca se pasa como un comando, solo se almacena en la memoria del shell y se repite. Sin embargo, no tengo ninguna referencia para esto, solo parece probable que esa sea la diferencia. (Me encantaría saber de alguien más conocedor ...)
Comodín el
165

Me sorprende que nadie haya mencionado el argumento -exec para buscar, que está destinado a este tipo de casos de uso, aunque comenzará un proceso para cada nombre de archivo coincidente:

find . -type f -name 'xa*' -exec sed -i 's/asd/dsg/g' {} \;

Alternativamente, uno podría usar xargs, que invocará menos procesos:

find . -type f -name 'xa*' | xargs sed -i 's/asd/dsg/g'

O más simplemente use la + variante exec en lugar de ;en find para permitir que find proporcione más de un archivo por llamada de subproceso:

find . -type f -name 'xa*' -exec sed -i 's/asd/dsg/g' {} +
ealfonso
fuente
77
Tuve que modificar el comando en esta respuesta de la siguiente manera: find ./ -type f -name 'xa*' -exec sed -i '' 's/asd/dsg/g' {} \;esa es la ubicación del comando find ./y un par de comillas simples -ipara OSX.
shelbydz
El comando find funciona como lo proporciona ealfonso, ./es igual .y -isolo tiene el parámetro backupsuffix.
uhausbrand
La -execopción de buscar junto con {} +es suficiente para resolver el problema como se indicó, y debería estar bien para la mayoría de los requisitos. Pero xargses una mejor opción en general porque también permite el procesamiento paralelo con la -popción. Cuando su expansión global es lo suficientemente grande como para desbordar la longitud de su línea de comando, es probable que también se beneficie de una aceleración en una ejecución secuencial.
Amit Naidu
78

Podrías usar grep y sed juntos. Esto le permite buscar subdirectorios de forma recursiva.

Linux: grep -r -l <old> * | xargs sed -i 's/<old>/<new>/g'
OS X: grep -r -l <old> * | xargs sed -i '' 's/<old>/<new>/g'

For grep:
    -r recursively searches subdirectories 
    -l prints file names that contain matches
For sed:
    -i extension (Note: An argument needs to be provided on OS X)
Raj
fuente
3
La ventaja de este método para mí fue que podía deslizarme grep -vpara evitar las carpetas gitgrep -rl <old> . | grep -v \.git | xargs sed -i 's/<old>/<new>/g'
Martin Lyne
La mejor solución para Mac
Marquis Blount
30

Esos comandos no funcionarán en el valor predeterminado sed que viene con Mac OS X.

De man 1 sed:

-i extension
             Edit files in-place, saving backups with the specified
             extension.  If a zero-length extension is given, no backup 
             will be saved.  It is not recommended to give a zero-length
             extension when in-place editing files, as you risk corruption
             or partial content in situations where disk space is exhausted, etc.

Intentó

sed -i '.bak' 's/old/new/g' logfile*

y

for i in logfile*; do sed -i '.bak' 's/old/new/g' $i; done

Ambos funcionan bien.

funroll
fuente
2
@sumek Aquí hay una sesión de terminal de ejemplo en OS X que muestra sed reemplazando todas las ocurrencias: GitHub Gist
funroll
Utilicé esto para reemplazar dos líneas diferentes en todos los archivos de configuración de mi sitio web con una línea de abajo. sed -i.bak "s / supercache_proxy_config / proxy_includes \ / supercache_config / g; s / basic_proxy_config / proxy_include \ / basic_proxy_config / g" sites-available / * No olvide eliminar los archivos * .bak cuando haya terminado el archivo sistema de higiene sake.
Josiah
19

@PaulR publicó esto como un comentario, pero la gente debería verlo como una respuesta (y esta respuesta funciona mejor para mis necesidades):

sed -i 's/abc/xyz/g' xa*

Esto funcionará para una cantidad moderada de archivos, probablemente del orden de decenas, pero probablemente no del orden de millones .

palswim
fuente
Suponga que tiene barras diagonales en sus reemplazos. Otro ejemplo con rutas de archivos sed -i 's|auth-user-pass nordvpn.txt|auth-user-pass /etc/openvpn/nordvpn.txt|g' *.ovpn.
Léo Léopold Hertz 준영
10

Otra forma más versátil es usar find:

sed -i 's/asd/dsg/g' $(find . -type f -name 'xa*')
dkinzer
fuente
1
la salida de ese comando find se expande, por lo que esto no soluciona el problema. En su lugar, debe usar -exec
ealfonso
@erjoalgo esto funciona porque el comando sed puede manejar múltiples archivos de entrada. La expansión del comando find es exactamente necesaria para que funcione.
dkinzer
funciona siempre y cuando el número de archivos no llegue a los límites de la línea de comandos.
ealfonso
Ese límite depende solo de los recursos de memoria disponibles para la máquina y es exactamente el mismo que el límite para exec.
dkinzer
44
Eso simplemente no es cierto. En su comando anterior, $ (find. ...) se expande en un solo comando, que podría ser muy largo si hay muchos archivos coincidentes. Si es demasiado largo (por ejemplo, en mi sistema, el límite es de alrededor de 2097152 caracteres), puede recibir un error: "Lista de argumentos demasiado larga" y el comando fallará. Google este error para obtener algunos antecedentes sobre esto.
ealfonso
2

Estoy usando findpara una tarea similar. Es bastante simple: tienes que pasarlo como argumento para sedesto:

sed -i 's/EXPRESSION/REPLACEMENT/g' `find -name "FILE.REGEX"`

De esta manera, no tiene que escribir bucles complejos, y es fácil ver qué archivos va a cambiar, solo ejecute findantes de ejecutar sed.

Bluesboy
fuente
1
Esto es exactamente lo mismo que la respuesta de @ dkinzer .
Sr. Tao
0

puedes hacer

el texto ' xxxx ' busca y lo reemplazará con ' aaaa '

grep -Rn '**xxxx**' /path | awk -F: '{print $1}' | xargs sed -i 's/**xxxx**/**yyyy**/'
Mohamed Galal
fuente
0

Si puede ejecutar un script, esto es lo que hice para una situación similar:

Usando un diccionario / hashMap (matriz asociativa) y variables para el sedcomando, podemos recorrer la matriz para reemplazar varias cadenas. La inclusión de un comodín en el name_patternpermitirá reemplazar en el lugar en los archivos con un patrón (esto podría ser algo así name_pattern='File*.txt') en un directorio específico ( source_dir). Todos los cambios están escritos logfileen eldestin_dir

#!/bin/bash
source_dir=source_path
destin_dir=destin_path
logfile='sedOutput.txt'
name_pattern='File.txt'

echo "--Begin $(date)--" | tee -a $destin_dir/$logfile
echo "Source_DIR=$source_dir destin_DIR=$destin_dir "

declare -A pairs=( 
    ['WHAT1']='FOR1'
    ['OTHER_string_to replace']='string replaced'
)

for i in "${!pairs[@]}"; do
    j=${pairs[$i]}
    echo "[$i]=$j"
    replace_what=$i
    replace_for=$j
    echo " "
    echo "Replace: $replace_what for: $replace_for"
    find $source_dir -name $name_pattern | xargs sed -i "s/$replace_what/$replace_for/g" 
    find $source_dir -name $name_pattern | xargs -I{} grep -n "$replace_for" {} /dev/null | tee -a $destin_dir/$logfile
done

echo " "
echo "----End $(date)---" | tee -a $destin_dir/$logfile

Primero, se declara la matriz de pares, cada par es una cadena de reemplazo, luego WHAT1se reemplazará por FOR1y OTHER_string_to replacese reemplazará por string replaceden el archivo File.txt. En el bucle se lee la matriz, el primer miembro del par se recupera como replace_what=$iy el segundo como replace_for=$j. El findcomando busca en el directorio el nombre del archivo (que puede contener un comodín) y el sed -icomando reemplaza en el mismo archivo (s) lo que se definió previamente. Finalmente agregué un greparchivo redirigido al archivo de registro para registrar los cambios realizados en los archivos.

Esto funcionó para mí GNU Bash 4.3 sed 4.2.2y se basó en la respuesta de VasyaNovikov para Loop over tuples in bash .

Lejuanjowski
fuente