La forma más eficaz de cambiar 1 línea en un archivo

La forma más eficaz de cambiar 1 línea en un archivo

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/basha #!/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 -ilee 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 .gituno, 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 el Dcalificador 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í, zshya que otros shells no pueden manejar valores de bytes arbitrarios).
  • Estamos usando #!/bin/sh -como reemplazo cuál es el shebang correcto para /bin/shlos scripts ( por cierto, #!/bin/bash -sería el shebang correcto para los scripts). /bin/bashVer¿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 bashcomo 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-efush-Oextglobsh

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/shshebang, 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 bashpor shesencialmente 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 shdeberá 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.

información relacionada