
Quiero cambiar la primera línea de cientos de archivos de forma recursiva de la forma más eficiente posible. Un ejemplo de lo que quiero hacer es cambiar #!/bin/bash
a #!/bin/sh
, así que se me ocurrió este comando:
find ./* -type f -exec sed -i '1s/^#!\/bin\/bash/#!\/bin\/sh/' {} \;
Pero, según tengo entendido, al hacerlo de esta manera, sed tiene que leer el archivo completo y reemplazar el original. ¿Existe una forma más eficiente de hacer esto?
Respuesta1
Sí, sed -i
lee y reescribe el archivo en su totalidad y, dado que la longitud de la línea cambia, tiene que hacerlo, ya que mueve las posiciones de todas las demás líneas.
...pero en este caso, no es necesario cambiar la longitud de la línea. Podemos reemplazar la línea hashbang #!/bin/sh␣␣
con dos espacios finales. El sistema operativo los eliminará al analizar la línea hashbang. (Como alternativa, utilice dos nuevas líneas o un signo de nueva línea + almohadilla, los cuales crean líneas adicionales que el shell eventualmente ignorará).
Todo lo que tenemos que hacer es abrir el archivo para escribirlo desde el principio, sin truncarlo. Las redirecciones habituales >
no >>
pueden hacer eso, pero en Bash, la redirección de lectura y escritura <>
parece funcionar:
echo '#!/bin/sh ' 1<> foo.sh
o usando dd
(estas deberían ser opciones POSIX estándar):
echo '#!/bin/sh ' | dd of=foo.sh conv=notrunc
Tenga en cuenta que, estrictamente hablando, ambos también reescriben la nueva línea al final de la línea, pero no importa.
Por supuesto, lo anterior sobrescribe incondicionalmente el inicio del archivo dado. Agregar una verificación de que el archivo original tenga el hashbang correcto se deja como ejercicio... De todos modos, probablemente no haría esto en producción y, obviamente, esto no funcionará si necesita cambiar la línea a unamás extensouno.
Respuesta2
Una optimización sería utilizar {} +
en lugar de {} \;
.
find . -type f -exec sed -i '1s|^#!/bin/bash|#!/bin/sh|' {} +
En lugar de invocar un proceso sed para cada archivo encontrado, proporciona los archivos como argumentos para un único proceso sed.
Especificación POSIX de buscar en{} +
(mi negrita):
Si la expresión primaria está puntuada por un <signo más>, la primaria siempre se evaluará como verdadera y los nombres de ruta para los cuales se evalúa la primaria se agregarán en conjuntos.La utilidad nombre_utilidad se invocará una vez para cada conjunto de nombres de ruta agregados.
Respuesta3
Lo haría:
#! /bin/zsh -
LC_ALL=C # work with bytes instead of characters.
shebang_to_replace=$'#!/bin/bash\n'
new_shebang=$'#!/bin/sh -\n'
length=$#shebang_to_replace
ret=0
for file in **/*(N.L+$((length - 1)));do
if
read -u0 -k $length shebang < $file &&
[[ $shebang = $shebang_to_replace ]]
then
print -rn -- $new_shebang 1<> $file || ret=$?
fi
done
exit $ret
ComoEl enfoque de @ilkkachu, el archivo se sobrescribe en su lugar con una cadena que es exactamente del mismo tamaño. Las diferencias son:
- ignoramos los archivos ocultos y los archivos en directorios ocultos (piense en
.git
uno, por ejemplo), ya que es poco probable que desee considerarlos (usófind ./*
los que habrían omitido los archivos y directorios ocultos del directorio actual, pero no los de los subdirectorios). Agregue elD
calificador global si los desea. - No nos molestamos en buscar archivos que no sean lo suficientemente grandes como para contener el shebang original a reemplazar (lo usamos
.
como equivalente a-type f
, por lo que ya estamos recuperando la información del inodo del archivo, por lo que también podríamos verificar el tamaño allí). ). - en realidad estamos verificando que el archivo comience con el shebang correcto para reemplazar, leyendo tan pocos bytes como sea necesario (aquí tiene que ser así,
zsh
ya que otros shells no pueden manejar valores de bytes arbitrarios). - Estamos usando
#!/bin/sh -
como reemplazo cuál es el shebang correcto para/bin/sh
los scripts ( por cierto,#!/bin/bash -
sería el shebang correcto para los scripts)./bin/bash
Ver¿Por qué el "-" en el tinglado "#! /bin/sh -"?para detalles.
Los errores al sobrescribir archivos se informan en el estado de salida, pero no los errores al recorrer el árbol de directorios ni los errores al leer los archivos, aunque se podrían agregar.
En cualquier caso, sólo reemplaza los tinglados que sonexactamente #!/bin/bash
, no otros tinglados que utilizan bash
como intérprete como #! /bin/bash
,,, . Para esos, necesitarías decidir qué hacer. son opciones pero no tienen equivalente, por ejemplo.#! /bin/bash -Oextglob
#! /usr/bin/env bash
#! /bin/bash -efu
-efu
sh
-Oextglob
sh
Puede ampliarlo para admitir los casos más sencillos como:
#! /bin/zsh -
LC_ALL=C # work with bytes instead of characters.
zmodload zsh/system || exit
minlength=11 # length of "#!/bin/bash"
maxlength=1024 # arbitrary here.
ret=0
for file in **/*(N.L+$minlength);do
if
sysread -s $maxlength buf < $file &&
[[ $buf =~ $'(^#![\t ]*((/usr)?/bin/env[ \t]+bash|/bin/bash)([ \t]+-([aCefux]*))?[ \t]*)\n' ]]
then
shebang=$match[1] newshebang="#!/bin/sh -$match[5]"
print -r -- ${(r[$#shebang])newshebang} 1<> $file || ret=$?
fi
done
exit $ret
Aquí se permiten varios shebangs diferentes con una serie de opciones admitidas que se reproducen en el nuevo /bin/sh
shebang, relleno a la derecha (con el r[length]
indicador de expansión de parámetros) al mismo tamaño que el original.
Respuesta4
Los archivos son un largo rango contiguo de bytes. Su reemplazo bash
por sh
esencialmente necesitará eliminar los dos bytes (suponiendo UTF-8 o similar) que componen ba
. Los archivos no pueden tener agujeros, por lo que todo lo que comience sh
deberá escribirse dos bytes antes en el archivo.
Esto requiere reescribir todo el archivo, o al menos comenzar desde la parte modificada.
Hay maneras dereemplazarbytes en un archivo, por ejemplo con espacios inocentes si el formato lo permite, sin tener que reescribir el archivo completo, ver la respuesta aceptada.