¿Cómo puedo lograr la portabilidad con sed -i (edición in situ)?

42

Estoy escribiendo scripts de shell para mi servidor, que es un alojamiento compartido que ejecuta FreeBSD. También quiero poder probarlos localmente, en mi PC con Linux. Por lo tanto, estoy tratando de escribirlos de forma portátil, pero sedno veo forma de hacerlo.

Parte de mi sitio web utiliza archivos HTML estáticos generados, y esta línea de sed inserta DOCTYPE correcto después de cada regeneración:

sed -i '1s/^/<!DOCTYPE html> \n/' ${file_name.html}

Funciona con GNU seden Linux, pero FreeBSD sedespera que el primer argumento después de la -iopción sea una extensión para la copia de seguridad. Así es como se vería:

sed -i '' '1s/^/<!DOCTYPE html> \n/' ${file_name.html}

Sin embargo, GNU seda su vez espera que la expresión siga inmediatamente después -i. (También requiere correcciones con el manejo de nueva línea, pero eso ya se respondió aquí )

Por supuesto, puedo incluir este cambio en la copia del script de mi servidor, pero eso podría causar problemas, es decir, mi uso de VCS para el control de versiones. ¿Hay alguna manera de lograr esto con sed de una manera totalmente portátil?

rojo
fuente
Los dos fragmentos de sed que proporcionó son idénticos, ¿está seguro de que no hay un error tipográfico? Además, puedo ejecutar GNU sed suministrando la extensión de copia de seguridad justo después-i
iruvar
Duh, gracias por ver esto. He arreglado mi pregunta. La segunda línea produce un error en mi sed, espera que '1s / ^ / <! DOCTYPE html> \ n /' sea un archivo y se queja de que no puede encontrarlo.
Rojo

Respuestas:

41

GNU sed acepta una extensión opcional después -i. La extensión debe estar en el mismo argumento sin espacio intermedio. Esta sintaxis también funciona en BSD sed.

sed -i.bak -e '…' SOMEFILE

Tenga en cuenta que en BSD, -itambién cambia el comportamiento cuando hay varios archivos de entrada: se procesan de forma independiente (por ejemplo, $coincide con la última línea de cada archivo). Además, esto no funcionará en BusyBox.

Si no desea utilizar archivos de copia de seguridad, puede verificar qué versión de sed está disponible.

case $(sed --help 2>&1) in
  *GNU*) set sed -i;;
  *) set sed -i '';;
esac
"$@" -e '…' "$file"

O, alternativamente, para evitar golpear los parámetros posicionales, defina una función.

case $(sed --help 2>&1) in
  *GNU*) sed_i () { sed -i "$@"; };;
  *) sed_i () { sed -i '' "$@"; };;
esac
sed_i -e '…' "$file"

Si no quieres molestarte, usa Perl.

perl -i -pe '…' "$file"

Si desea escribir un script portátil, no lo use -i, no está en POSIX. Haga manualmente lo que sed hace debajo del capó: es solo una línea más de código.

sed -e '…' "$file" >"$file.new"
mv -- "$file.new" "$file"
Gilles 'SO- deja de ser malvado'
fuente
2
GNU sed -itambién implica -s. Y la forma más fácil de verificar si hay GNU sedes con el sed vcomando que es un noop válido para GNU pero falla en cualquier otro lugar.
mikeserv
Inspirado por los consejos anteriores, aquí hay una versión portátil de una sola línea (aunque fea) para aquellos que realmente quieren una, aunque genera una subshell: sed -i$(sed v < /dev/null 2> /dev/null || echo -n " ''") -e '...' "$file"si no es GNU sed, inserta un espacio seguido de dos comillas después -ipara que trabaja en BSD. GNU sed obtiene solo -i.
Ivan X
2
@IvanX Soy cuidadoso de usar la presencia del vcomando para probar GNU sed. ¿Qué pasa si FreeBSD decide implementarlo?
Gilles 'SO- deja de ser malvado'
@Gilles Fair point, pero la página de manual de GNU sed describe v como exactamente para ese propósito (detectando que es GNU sed y no otra cosa), por lo que uno esperaría que * BSD honrara eso. No puedo pensar en otra prueba, sin previo aviso, que no realice ninguna acción en GNU sed, mientras que causa un error en BSD sed (o viceversa), aparte de usar -i, pero eso requeriría crear primero un archivo ficticio. Su prueba para sed arriba está bien pero es difícil de manejar para en línea. Evitar completamente -i, como sugieres, ciertamente parece ser la apuesta más segura, pero estoy de acuerdo con usar sed vdado que ese es su propósito para existir.
Ivan X
1
@lapo Una alternativa es definir una función, vea mi edición.
Gilles 'SO- deja de ser malvado'
10

Si no encuentras un truco para hacer que el sedjuego sea agradable, puedes intentar:

  1. No utilizar -i:

    sed '1s/^/<!DOCTYPE html> \n/' "${file_name.html}" > "${file_name.html}.tmp" &&
      mv "${file_name.html}.tmp" "${file_name.html}"
  2. Usa Perl

    perl -i -pe 'print "<!DOCTYPE html> \n" if $.==1;' "${file_name.html}"
terdon
fuente
8

ed

Siempre puede usar edpara anteponer una línea a un archivo existente.

$ printf '0a\n<!DOCTYPE html>\n.\nw\n' | ed my.html

Detalles

Los bits alrededor del <!DOCTYPE html>son comandos para edindicarle que agregue esa línea al archivo my.html.

sed

Creo que este comando sedtambién se puede usar:

$ sed -i '1i<!DOCTYPE html>\n` testfile.csv
slm
fuente
Finalmente recurrí a Perl, pero usar ed es una buena alternativa que no es popular entre los usuarios de Unix como debería ser.
Rojo
@Red: me alegra saber que resolvió su problema. Sí, no lo había visto antes, googlear apareció y realmente parecía la forma más portátil y adecuada de hacer esto.
slm
7

También puede hacer manualmente lo que perl -ihace debajo del capó:

{ rm -f file && { echo '<!DOCTYPE html>'; cat; } > file;} < file

Como perl -i, no hay copia de seguridad, y como la mayoría de las soluciones dadas aquí, tenga en cuenta que puede afectar los permisos, la propiedad del archivo y puede convertir un enlace simbólico en un archivo normal.

Con:

sed '1i\
<!DOCTYPE html>' file 1<> file

sedsobrescribiría el archivo sobre sí mismo, por lo que no afectaría la propiedad y los permisos o enlaces simbólicos. Funciona con GNU sedporque sednormalmente habrá leído un búfer lleno de datos file(4k en mi caso) antes de sobrescribirlo con el icomando. Eso no funcionaría si el archivo fuera más de 4k, excepto por el hecho de que sedtambién amortigua su salida.

Básicamente sedfunciona en bloques de 4k para leer y escribir. Si la línea a insertar es más pequeña que 4k, sednunca sobrescribirá un bloque que aún no haya leído.

Sin embargo, no contaría con eso.

Stéphane Chazelas
fuente
Debe ser echo '<!DOCTYPE html>'o escapar sin "" comillas.
AD
@AD Buen punto. Tiendo a olvidar ese error ^ Wfeature of interactive bash / zsh ya que generalmente lo desactivo por mí mismo.
Stéphane Chazelas
Esta es una de las muy pocas respuestas aquí que no tiene un agujero de seguridad abierta de redirigir a un "archivo temporal" con una estática, nombre predecible sin comprobar si ya existe. Creo que esta debería ser la respuesta aceptada. Muy buena demostración del uso de comandos de grupo, también.
Comodín
3

FreeBSD sed , que también se usa en Mac OS X, necesita la -eopción después del -icambio para definir y reconocer el siguiente comando (regex) correctamente y sin ambigüedades.

En otras palabras, sed -i -e ...debería funcionar con FreeBSD y GNUsed .

En términos más generales, omitir la extensión de copia de seguridad después de FreeBSD sed -irequiere alguna sedopción explícita o cambiar siguiendo la -ipara evitar confusiones por parte de FreeBSDsed mientras analiza sus argumentos de línea de comandos.

(Tenga en cuenta, sin embargo, que las sedediciones de archivos en el lugar conducen a cambios en el inodo del archivo, consulte la edición "en el lugar" de los archivos ).

(Como pista general, las versiones recientes de FreeBSD sedtienen el -rinterruptor para aumentar la compatibilidad con GNU sed).

echo a > testfile.txt
ls -li testfile.txt
#gsed -i -e 's/a/A/' testfile.txt
#bsdsed -i 's/a/A/' testfile.txt  # does not work
bsdsed -i -e 's/a/A/' testfile.txt
ls -li testfile.txt
cat testfile.txt
carlo
fuente
55
No, nobsdsed -i -e 's/a/A/' es una edición en el lugar, se está editando al guardar el original con un sufijo "-e" ( ). testfile.txt-e
Stéphane Chazelas
tenga en cuenta que GNU sed también admite -E (además de -r) para compatibilidad con FreeBSD. -E es probable que se especifique en la próxima versión POSIX, por lo que todos deberíamos olvidarnos de eso -r sin sentido y pretender que nunca existió.
Stéphane Chazelas
2

Para emular sed -ipara un solo archivo de forma portátil, evitando las condiciones de carrera tanto como sea posible:

sed 'script' <<FILE >file
$(cat file)
FILE

Por cierto, esto también maneja el posible problema que se sed -ipresenta, ya que, dependiendo de los permisos de directorio y archivo, sed -ipodría permitir que un usuario sobrescriba un archivo que ese usuario no tiene permisos para editar.

También puede hacer copias de seguridad como:

sed 'script' <<FILE >file
$(tee file.old <file)
FILE
mikeserv
fuente
2

Puede usar Vim en modo Ex:

ex -sc '1i|<!DOCTYPE html>' -cx file
  1. 1 seleccione primera línea

  2. i insertar texto y nueva línea

  3. x guardar y cerrar

O incluso, como en los comentarios, el viejo estándar ex :

printf '%s\n' 1i '<!DOCTYPE html>' . x | ex file 
Steven Penny
fuente
No hay nada específico de Vim aquí. Esto es totalmente compatible con las especificaciones POSIX paraex , excepto que las implementaciones no son necesarias para admitir varios -cindicadores. Para una portabilidad definitiva, usaríaprintf '%s\n' 1i '<!DOCTYPE html>' . x | ex file
Wildcard