Linux: ¿Cómo usar un archivo como entrada y salida al mismo tiempo?

55

Acabo de ejecutar lo siguiente en bash:

uniq .bash_history > .bash_history

y mi archivo de historial terminó completamente vacío.

Supongo que necesito una forma de leer todo el archivo antes de escribirlo. ¿Cómo se hace eso?

PD: obviamente pensé en usar un archivo temporal, pero estoy buscando una solución más elegante.

MilliaLover
fuente
Es porque los archivos se abren de derecha a izquierda. Consulte también stackoverflow.com/questions/146435/…
WheresAlice el
Debe escribir la salida en un nuevo archivo en el mismo directorio y cambiarle el nombre al archivo anterior. Cualquier otro enfoque correrá el riesgo de perder sus datos si se interrumpe a la mitad. Algunas herramientas pueden ocultar este paso de usted.
kasperd
O bashno colocará engaños consecutivos en su historial si configura HISTCONTROL para incluir ignoredups; ver la página de manual.
dave_thompson_085
por favor considere cambiar la respuesta a esta. serverfault.com/a/557566/130392
23inhouse hace

Respuestas:

49

Recomiendo usar spongede moreutils . Desde la página del manual:

DESCRIPTION
  sponge  reads  standard  input  and writes it out to the specified file. Unlike
  a shell redirect, sponge soaks up all its input before opening the output file.
  This allows for constructing pipelines that read from and write to the same 
  file.

Para aplicar esto a su problema, intente:

uniq .bash_history | sponge .bash_history
jldugger
fuente
66
Es como un gato, pero con capacidades de succión: D
MilliaLover
77

Solo quería aportar otra respuesta que sea simple y no use esponja (porque a menudo no se incluye en entornos livianos).

echo "$(uniq .bash_history)" > .bash_history

debería tener el resultado deseado. La subshell se ejecuta antes de que .bash_history se abra para escribir. Como se explica en la respuesta de Phil P, cuando se lee .bash_history en el comando original, el operador '>' ya lo ha truncado.

Hart Simha
fuente
15
Normalmente no soy fanático de las respuestas que desentierran una pregunta antigua que ya tiene una respuesta válida y aceptada, pero esta es elegante, bien escrita y es un fuerte argumento para su necesidad (entornos livianos); para mí, realmente agrega algo al conjunto existente de respuestas. Bienvenido a SF, Hart (has estado aquí un mes, pero creo que esta es tu primera publicación importante). ¡Espero leer más respuestas tuyas como esta!
MadHatter
44
Esta es la mejor solución. Tuve que usar una subshell en $()lugar de backticks debido a algunos problemas de escape.
CMCDragonkai
3
Me pregunto si esta solución escala a archivos grandes, digamos ... 20 o 50 GB.
Amit Naidu el
1
Esta realmente debería ser la respuesta de aceptación.
maxywb
1
Usé esta respuesta para hacer echo "$(fmt -p '# ' -w 50 readme.txt)" > readme.txthoy. Estuve buscando por mucho tiempo una solución elegante. Muchas gracias, @Hart Simha!
Shredalert
12

El problema es que su shell está configurando la canalización de comandos antes de ejecutar los comandos. No es una cuestión de "entrada y salida", es que el contenido del archivo ya desapareció antes de que uniq incluso se ejecute. Va algo como:

  1. El shell abre el >archivo de salida para escribir, truncándolo
  2. El shell se configura para que el descriptor de archivo 1 (para stdout) se use para esa salida
  3. El shell ejecuta uniq, quizás algo así como execlp ("uniq", "uniq", ".bash_history", NULL)
  4. uniq se ejecuta, abre .bash_history y no encuentra nada allí

Hay varias soluciones, incluida la edición en el lugar y el uso de archivos temporales que otros mencionan, pero la clave es comprender el problema, qué está realmente mal y por qué.

Phil P
fuente
10

Otro truco para hacer esto, sin usar sponge, es el siguiente comando:

{ rm .bash_history && uniq > .bash_history; } < .bash_history

Este es uno de los trucos descritos en el excelente artículo de edición "in situ" de archivos en backreference.org.

Básicamente abre el archivo para leerlo, luego lo "elimina". Sin embargo, en realidad no se elimina: hay un descriptor de archivo abierto que apunta hacia él, y mientras permanezca abierto, el archivo todavía está disponible. Luego crea un nuevo archivo con el mismo nombre y escribe las líneas únicas en él.

Desventaja de esta solución: si uniqfalla por algún motivo, su historial desaparecerá.

guadaña
fuente
6

usar esponja de moreutils

uniq .bash_history | sponge .bash_history
Justin
fuente
3

Este sedscript elimina los duplicados adyacentes. Con la -iopción, realiza la modificación en el lugar. Es del sed infoarchivo:

sed -i 'h;:b;$b;N;/^\(.*\)\n\1$/ {g;bb};$b;P;D' .bash_history
Dennis Williamson
fuente
sed todavía usa el archivo temporal, agregó una respuesta con straceilustración (no es que realmente importe) :-)
Kyle Brandt
3
@Kyle: Cierto, pero "fuera de la vista, fuera de la mente". Personalmente, usaría el archivo temporal explícito ya que algo así process input > tmp && mv tmp inputes mucho más simple y más legible que usar sedtrucos simplemente para evitar un archivo temporal y no sobrescribirá mi original si falla (no sé si sed -ifalla con gracia, lo haría creo que sí) Además, hay muchas cosas que puede hacer con el método de salida a archivo temporal que no se pueden hacer en el lugar sin algo aún más complicado que este sedscript. Sé que sabes todo esto, pero puede beneficiar a algunos espectadores.
Dennis Williamson el
3

Como un dato interesante, sed también usa un archivo temporal (esto solo lo hace por usted):

$ strace sed -i 's/foo/bar/g' foo    
open("foo", O_RDONLY|O_LARGEFILE)       = 3
...
open("./sedPmPv9z", O_RDWR|O_CREAT|O_EXCL|O_LARGEFILE, 0600) = 4
...
read(3, "foo\n"..., 4096)               = 4
write(4, "bar\n"..., 4)                 = 4
read(3, ""..., 4096)                    = 0
close(3)                                = 0
close(4)                                = 0
rename("./sedPmPv9z", "foo")            = 0
close(1)                                = 0
close(2)                                = 0

Descripción:
el archivo temporal se ./sedPmPv9zconvierte en fd 4, y los fooarchivos se convierten en fd 3. Las operaciones de lectura están en fd 3 y las escrituras en fd 4 (el archivo temporal). El archivo foo se sobrescribe con el archivo temporal en la llamada de cambio de nombre.

Kyle Brandt
fuente
1

Otra solución:

uniq file 1<> líneas.txt
Antonio Lebrón
fuente
0

Un archivo temporal es más o menos, a menos que el comando en cuestión sea compatible con la edición in situ ( uniqno, algunos seds hacen ( sed -i)).

Douglas Leeder
fuente
0

Puede usar Vim en modo Ex:

ex -sc '%!uniq' -cx .bash_history
  1. % seleccione todas las líneas

  2. ! ejecutar comando

  3. x guardar y cerrar

Steven Penny
fuente
-1

También puede usar tee, usando la salida uniq como entrada:

uniq .bash_history | tee .bash_history
Saul Vargas
fuente
No, no puedes hacer esto askubuntu.com/a/752451
Steven Penny