Leer y escribir un archivo: comando tee

10

Es bien sabido que un comando como este:

cat filename | some_sed_command >filename

borra el nombre del archivo, ya que la redirección de salida, que se ejecuta antes del comando, hace que el nombre del archivo se trunca.

Se podría resolver el problema de la siguiente manera:

cat file | some_sed_command | tee file >/dev/null

pero no estoy seguro de que esto funcione en cualquier caso: ¿qué sucede si el archivo (y el resultado del comando sed) es muy grande? ¿Cómo puede el sistema operativo evitar sobrescribir algún contenido que aún no se lee? Veo que también hay un comando de esponja que debería funcionar en cualquier caso: ¿es "más seguro" que el tee?

VeryHardCoder
fuente
¿Cuál es tu objetivo principal? (en términos simples)
Sergiy Kolodyazhnyy
@Serg simplemente entiende cómo funcionan las cosas ... La respuesta escrita por kos aclara el asunto
VeryHardCoder

Respuestas:

10

Se podría resolver el problema de la siguiente manera:

cat file | some_sed_command | tee file >/dev/null

No se .

Las posibilidades fileserán truncadas, pero no hay garantía de cat file | some_sed_command | tee file >/dev/nullque no se truncarán file.

Todo depende de qué comando se procese primero, a diferencia de lo que uno puede esperar, los comandos en una tubería no se procesan de izquierda a derecha . No hay garantía sobre qué comando se elegirá primero, por lo que uno podría pensar que se eligió al azar y nunca confiar en que el caparazón no elija al infractor.

Dado que las posibilidades de que el comando ofensivo se elija primero entre tres comandos son menores que las posibilidades de que el comando ofensivo se elija primero entre dos comandos, es menos probable que filese trunque, pero aún así sucederá .

script.sh:

#!/bin/bash
for ((i=0; i<100; i++)); do
    cat >file <<-EOF
    foo
    bar
    EOF
    cat file |
        sed 's/bar/baz/' |
        tee file >/dev/null
    [ -s file ] &&
        echo 'Not truncated' ||
        echo 'Truncated'
done |
    sort |
    uniq -c
rm file
% bash script.sh
 93 Not truncated
  7 Truncated
% bash script.sh
 98 Not truncated
  2 Truncated
% bash script.sh
100 Not truncated

Así que nunca uses algo como eso cat file | some_sed_command | tee file >/dev/null. Use spongecomo lo sugirió Oli.

Como alternativa, para entornos más exigentes y / o archivos relativamente pequeños, se puede usar una cadena here y una sustitución de comando para leer el archivo antes de ejecutar cualquier comando:

$ cat file
foo
bar
$ for ((i=0; i<100; i++)); do <<<"$(<file)" sed 's/bar/baz/' >file; done
$ cat file
foo
baz
kos
fuente
9

Para sedconcreto, puede utilizar su -iargumento en el lugar. Simplemente guarda de nuevo en el archivo que abrió, por ejemplo:

sed -i 's/ /-/g' filename

Si desea hacer algo más robusto, suponiendo que está haciendo más que sed, sí, puede almacenar todo el contenido sponge(desde el moreutilspaquete) que "absorberá" todo el stdin antes de escribirlo en el archivo. Es como teepero con menos funcionalidad. Sin embargo, para un uso básico, es prácticamente un reemplazo directo:

cat file | some_sed_command | sponge file >/dev/null

¿Es eso más seguro? Seguro. Probablemente tiene límites, por lo que si está haciendo algo colosal (y no puede editar en el lugar con sed), es posible que desee hacer sus ediciones en un segundo archivo y luego mvese archivo nuevamente al nombre de archivo original. Eso debería ser atómico (por lo que todo lo que dependa de estos archivos no se romperá si necesitan acceso constante).

Oli
fuente
0

Puede usar Vim en modo Ex:

ex -sc '%!some_sed_command' -cx filename
  1. % seleccione todas las líneas

  2. ! Ejecutar comando

  3. x Guardar y Salir

Steven Penny
fuente
0

Ah, pero spongeno es la única opción; no tiene que obtener moreutilspara que esto funcione correctamente. Cualquier mecanismo funcionará siempre que satisfaga los siguientes dos requisitos:

  1. Acepta el nombre del archivo de salida como parámetro.
  2. Solo crea el archivo de salida una vez que se ha procesado toda la entrada.

Verá, el problema bien conocido al que se refiere el OP es que el shell creará todos los archivos que son necesarios para que las tuberías funcionen incluso antes de comenzar a ejecutar los comandos en la tubería, por lo que es el shell el que realmente se trunca el archivo de salida (que desafortunadamente también es el archivo de entrada) antes de que cualquiera de los comandos haya tenido la oportunidad de comenzar a ejecutarse.

El teecomando no funciona, aunque cumple con el primer requisito, porque no cumple con el segundo requisito: siempre creará el archivo de salida inmediatamente después del inicio, por lo que es esencialmente tan malo como crear una tubería directamente en el archivo de salida. (En realidad, es peor, porque su uso introduce un retraso aleatorio no determinista antes de que el archivo de salida se trunca, por lo que podría pensar que funciona, mientras que de hecho no lo hace).

Entonces, todo lo que necesitamos para resolver este problema es algún comando que almacenará todas sus entradas antes de producir cualquier salida, y que sea capaz de aceptar el nombre de archivo de salida como un parámetro, de modo que no tengamos que canalizar su salida a El archivo de salida. Uno de esos comandos es shuf. Entonces, lo siguiente logrará lo mismo que spongehace:

    shuf --output=file --random-source=/dev/zero 

La --random-source=/dev/zeroparte engaña shufpara hacer lo suyo sin hacer ningún movimiento aleatorio, por lo que amortiguará su entrada sin alterarla.

Mike Nakis
fuente