Redirección de E / S y el comando principal

9

.hgignoreHoy estaba tratando de editar rápidamente un archivo desde el shell de Cygwin bash, y agregué una línea que fue un error. No estoy seguro de si esta era la mejor manera de hacerlo, pero rápidamente pensé en usar head -1 .hgignorepara eliminar la línea ofensiva (anteriormente solo tenía una línea en el archivo). Efectivamente, cuando se ejecuta, da la primera línea como la única salida.

Pero cuando intenté redirigir la salida y reescribir el archivo usando head -1 .hgignore > .hgignore, el archivo estaba vacío. ¿Por qué pasó esto? Si trato de agregar en su lugar, head -1 .hgignore >> .hgignorese agrega correctamente, pero obviamente este no es el resultado deseado. ¿Por qué una redirección truncada no funciona en este caso?

Voithos
fuente

Respuestas:

10

Cuando el shell obtiene una línea de comando como: command > file.outel shell se abre (y quizás crea) el archivo llamado file.out. El shell establece el descriptor de archivo 0 en el descriptor de archivo que obtuvo desde la apertura. Así es como funciona la redirección de E / S: cada proceso conoce los descriptores de archivo 0, 1 y 2.

La parte difícil de esto es cómo abrir file.out. La mayoría de las veces, desea file.outabrir para escritura en el desplazamiento 0 (es decir, truncado) y esto es lo que hizo el shell por usted. Truncó .hgignore, lo abrió para escribir, duplicó el descriptor de archivo a 0, luego lo ejecutó head. Clobbering instantáneo de archivos.

En bash shell, haces una set noclobberpara cambiar este comportamiento.

Bruce Ediger
fuente
Ajá ya veo. Pensé que el shell estaba truncando el archivo antes de ejecutar el comando, pero no sabía por qué. ¡Gracias por la explicación!
voithos
10

Creo que Bruce responde lo que está pasando aquí con la tubería de shell.

Una de mis pequeñas utilidades favoritas es el spongecomando de moreutils . Resuelve exactamente este problema al "absorber" todas las entradas disponibles antes de abrir el archivo de salida de destino y escribir los datos. Le permite escribir tuberías exactamente como esperaba:

$ head -1 .hgignore | sponge .hgignore

La solución del pobre es canalizar la salida a un archivo temporal, luego, una vez que se termina la canalización (por ejemplo, el siguiente comando que ejecuta) es mover el archivo temporal nuevamente a la ubicación del archivo original.

$ head -1 .hgingore > .hgignore.tmp
$ mv .hgignore{.tmp,}
Caleb
fuente
Al mirar esto unos años más tarde, se me ocurrió una idea: ¿no podríamos simplemente hacerlo head -1 .hgignore | tee .hgignore? teeestá en coreutils, y como beneficio adicional / efecto secundario, esto también escribe en STDOUT
voithos
@voithos Que yo sepa, teeabre y trunca el archivo en el que está escribiendo cuando se instancia como todo lo demás, por lo que no resuelve el problema principal aquí de la condición de carrera al leer el contenido del archivo antes de truncarlo con la escritura.
Caleb
Traes un punto del que no estaba al tanto, en realidad, a saber, que los comandos canalizados se inician de inmediato, en lugar de secuencialmente. ¿Es eso exacto? Sin embargo, lo probé y tee parece hacer lo deseado. Tengo la versión 8.13en mi máquina.
voithos
1
@voithos Yes ordena en una canalización y todos los canales de entrada / salida involucrados se inician en orden inverso, por lo que la canalización está lista para recibir datos cuando el primero comienza a proporcionarlos. Sospecho que su prueba es defectuosa porque probablemente usó una porción demasiado pequeña de datos y lo almacenó en caché en un búfer de lectura antes de que lo necesitara. El teeprograma truncará sus archivos, no está configurado para duplicarlos.
Caleb
3

En

head -n 1 file > file

filese trunca antes de headcomenzar, pero si lo escribe:

head -n 1 file 1<> file

no filees como se abre en modo lectura-escritura. Sin embargo, cuando headtermina de escribir, no trunca el archivo, por lo que la línea de arriba sería no operativa ( headsimplemente reescribiría la primera línea sobre sí misma y dejaría las otras intactas).

Sin embargo, después de que headhaya regresado y mientras fdtodavía esté abierto, puede llamar a otro comando que haga el truncate.

Por ejemplo:

{ head -n 1 file; perl -e 'truncate STDOUT, tell STDOUT'; } 1<> file

Lo que importa aquí es que truncatearriba, headsolo mueve el cursor para fd 1 dentro del archivo justo después de la primera línea. Reescribe la primera línea que no necesitábamos, pero eso no es dañino.

Con una cabeza POSIX, podríamos escapar sin reescribir esa primera línea:

{ head -n 1 > /dev/null
  perl -e 'truncate STDIN, tell STDIN'
} <> file

Aquí, estamos usando el hecho de que headmueve la posición del cursor en su stdin. Si bien headnormalmente leería su entrada en grandes fragmentos para mejorar el rendimiento, POSIX requeriría (cuando sea posible) seekretroceder justo después de la primera línea si hubiera ido más allá. Sin embargo, tenga en cuenta que no todas las implementaciones lo hacen.

Alternativamente, puede usar el readcomando de shell en este caso:

{ read -r dummy; perl -e 'truncate STDIN, tell STDIN'; } <> file
Stéphane Chazelas
fuente
1
Stéphane, ¿sabes de una norma o coreutils comando que se puede truncar STDINsimilar a lo que has logrado usando perlanterior
Iruvar
2
@ 1_CR, no. sin embargo, ddpuede truncarse en cualquier desplazamiento absoluto arbitrario en el archivo. Por lo que puede determinar el desplazamiento de la segunda línea y truncado a partir de ahí con el bytedd bs=1 seek="$offset" of=file
Stéphane Chazelas
1

La solución del hombre real es

ed .hgignore
$d
wq

o como una sola línea

printf '%s\n' '$d' 'wq' | ed .hgignore

O con GNU sed:

sed -i '$d' .hgignore

(No, estoy bromeando. Usaría un editor interactivo vi .hgignore GddZZ) .

Gilles 'SO- deja de ser malvado'
fuente
Me he preguntado, ¿hay alguna ventaja de utilizar :wqmás ZZ?
voithos
También, :xque es lo que hacen los dedos de forma automática
Glenn Jackman
y ZQes lo mismo que:q!
glenn jackman
ZZ y: x solo escriben si hay algo que escribir ...: w siempre sincroniza el archivo en el disco, independientemente de si lo necesita. Yo uso: xa porque uso pestañas.
xenoterracide
1

Puede usar Vim en modo Ex:

ex -sc '2,d|x' .hgignore
  1. 2, seleccione las líneas 2 hasta el final

  2. d Eliminar

  3. x guardar y cerrar

Steven Penny
fuente
0

Para la edición de archivos en el lugar, también puede usar el truco de manejo de archivos abiertos como se muestra por Jürgen Hötzel en la salida Redirect de sed 's / c / d /' myFile a myFile .

exec 3<.hgignore
rm .hgignore  # prevent open file from being truncated
head -1 <&3 > .hgignore

ls -l .hgignore  # note that permissions may have changed
dan55
fuente
2
Y justo después de que rm .hgignoresu energía falla, quitando horas de duro trabajo. Ok, no importa .hgignore, pero ¿por qué harías algo tan complicado de todos modos? Por lo tanto, mi voto negativo: técnicamente correcto pero una muy mala idea.
Gilles 'SO- deja de ser malvado'
@Gilles, tal vez no sea una buena idea, pero eso es, por ejemplo, lo que hace perl -i(para la edición in situ), y no me sorprendería si algunas implementaciones de lo sed -ihicieran también (aunque sedparece que la última versión de GNU no lo hace).
Stéphane Chazelas