Problema con la redirección de salida Bash


10

Estaba intentando eliminar todas las líneas de un archivo excepto la última línea, pero el siguiente comando no funcionaba, aunque file.txt no está vacío.

$cat file.txt |tail -1 > file.txt 

$cat file.txt 

¿Por qué es así?

19

redireccionamiento desde un archivo a través de una tubería de vuelta al mismo archivo es insegura; Si file.txt es sobreescrito por el armazón cuando configura la última etapa de la tubería antes de que tail comience a leer la primera etapa, termina con salida vacía.

Haga lo siguiente en su lugar:

tail -1 file.txt >file.txt.new && mv file.txt.new file.txt 

... bueno, en realidad, no hacer que en el código de producción; sobre todo si estás en un entorno sensible a la seguridad y funcionando como root, el siguiente es más apropiado:

tempfile="$(mktemp file.txt.XXXXXX)" 
chown --reference=file.txt -- "$tempfile" 
chmod --reference=file.txt -- "$tempfile" 
tail -1 file.txt >"$tempfile" && mv -- "$tempfile" file.txt 

Otro enfoque (evitando los archivos temporales, a menos que <<< implícitamente los crea en su plataforma) es la siguiente:

lastline="$(tail -1 file.txt)"; cat >file.txt <<<"$lastline" 

(La implementación anterior es específica de bash, pero funciona en casos en que echo no lo hace, como cuando la última línea contiene "version ", por ejemplo).

Por último, se puede utilizar una esponja de moreutils:

tail -1 file.txt | sponge file.txt 
  0

Tenga en cuenta que cola acepta un nombre de archivo como argumento: "cola -1 archivo.txt> archivo.txt.nuevo && mv archivo.txt.nuevo archivo.txt" 23 sep. 082008-09-23 19:35:56

  0

@Marcel Levy - muy bien, y tiene el potencial de ejecutarse de manera más eficiente de esa manera; actualizado. 23 sep. 082008-09-23 20:47:58

  0

Estimado @CharlesDuffy, Me gustaría señalar que su variante "segura" requiere que el usuario haga eso será root. En cuanto a cómo es seguro, puedo decir que en el acceso a "archivo.txt" no es suficiente para aplicar sus permisos y propietario. Considera ese "archivo".txt "tiene permisos 0666 y se coloca dentro de la carpeta '~/.ssh' que tiene permisos 0700. En este caso, mi solución será más segura ya que no expondrá ese archivo al mundo. ¿Debo rechazar su respuesta debido a eso? ?;) 14 may. 132013-05-14 13:02:27

  0

@CharlesDuffy, Oh ... He olvidado por completo que puede perder datos si alguien agrega una línea a ese archivo porque lo expone durante todo el tiempo de 'tail -1 file.txt'. Como resultado puede sobrescribir 'archivo.txt' existente con contenido anterior. ¿Debo desestimar su respuesta debido a eso?;) 14 may. 132013-05-14 13:15:35

  0

@ony El uso de' chown' es potencialmente útil aquí si el archivo es propiedad de un grupo secundario, y no _not_ requieren privilegios en ese caso. 14 may. 132013-05-14 13:18:36

  0

@ony En cuanto a la condición de la carrera, si fuera evitable, seguro, me rechazó. Me encantaría saber cómo se haría esa elusión (particularmente dado como 'sed -i' , 'ed',' ex' y kin tampoco son atómicos). 14 may. 132013-05-14 13:19:43

  0

@Char lesDuffy, en el archivo '~/.ssh/file.txt' hay varios niveles de permisos: primero' ~ '(y todas sus carpetas principales), luego' .ssh' (que generalmente tienen 0700) y luego 'file.txt '. Copia solo permisos para el último elemento, por lo que posiblemente disminuya el nivel de seguridad. En mi solución, esto se evita usando el mismo archivo (sin mover/copiar/sobreescribir). Entonces, este problema es evitable. ¿Esto significa que puedo rechazarlo? Por supuesto, mi solución tiene otro problema: el tamaño del archivo puede diferir con su contenido, pero este es otro problema. En mi respuesta original evito el problema con los permisos también. 14 may. 132013-05-14 13:33:32

  0

@ony Ese es un problema apremiante: el archivo temporal debe crearse en la ubicación final en lugar de usar TEMPDIR. Haciendo ese cambio; gracias por el puntero. 14 may. 132013-05-14 13:38:47

  0

Creo que TEMPFILE = $ (tempfile) es mucho más fácil ... por supuesto (foot-to-mouth) no todas las distribuciones tienen esto todavía? ¿Por qué si es "más seguro" me pregunto .. 16 jul. 142014-07-16 15:12:40

  0

@osirisgothra, de hecho, 'tempfile' es específico de la distribución. No está disponible en OS X, no está disponible en el último Arch Linux, & c. Más allá de eso, es útil para un usuario conocer el software al que está asociado un archivo o directorio temporal determinado, lo que hace que el parámetro de la plantilla sea una característica valiosa. 16 jul. 142014-07-16 15:57:52


0

Parece que no le gusta el hecho de que está escribiendo de nuevo en el mismo nombre de archivo. Si la siguiente funciona:

$cat file.txt | tail -1 > anotherfile.txt 
  0

No hay necesidad de que "parece" especulación: No hay garantía de ordenar el inicio entre los componentes de canalización , y las redirecciones (por lo tanto, la apertura de 'anotherfile.txt' en modo truncado) ocurre * antes * de la ejecución (en este caso, de' cola'; si sucede antes de la ejecución de 'cat' no está definido, pero desde un programa como como 'cat' requiere tiempo para iniciarse, es muy probable que el truncamiento de' anotherfile.txt' ya haya sucedido antes de que 'cat' termine de cargarse y esté listo para abrir su argumento para la entrada). 10 mar. 162016-03-10 19:13:22


1

Como dice Lewis Baumstark, que no le gusta que se está escribiendo con el mismo nombre de archivo.

Esto se debe a que el shell abre "file.txt" y lo trunca para hacer la redirección antes de que se ejecute "cat file.txt". Por lo tanto, usted tiene que

tail -1 file.txt > file2.txt; mv file2.txt file.txt 

0

tail -1 > file.txt se sobreponen a su archivo, causando gato para leer un archivo vacío porque la re-escritura va a pasar antes de cualquiera de los comandos en su tubería se ejecutan.


3

Antes de que se ejecute 'cat', Bash ya ha abierto 'file.txt' para escribir, borrando sus contenidos.

En general, no escriba en los archivos que está leyendo en la misma declaración. Esto puede solucionarse escribiendo en un archivo diferente, como el anterior:

$cat file.txt | tail -1 >anotherfile.txt 
$mv anotherfile.txt file.txt
o usando una utilidad como esponja de moreutils:
$cat file.txt | tail -1 | sponge file.txt
Esto funciona porque la esponja espera hasta que su flujo de entrada haya finalizado antes de abrir su archivo de salida.

  0

¿'cat' le da algún valor sobre' tail -1 file.txt'? Ciertamente hace las cosas ** mucho menos eficientes si su 'archivo.txt' es grande - si' cola' tiene un descriptor de archivo de búsqueda directa, puede saltar directamente al último kb del archivo, solo leer eso, y intente encontrar la última línea, retrocediendo si la última línea es más grande que 1kb; si todo lo que tiene es una tubería de 'cat', tiene que leer el archivo desde el principio, sin importar qué tan grande sea, para llegar al final. 21 jul. 162016-07-21 14:42:36

  0

... 'tail -1 <file.txt' también sería un descriptor de archivo real y buscable, y en stdin (por lo tanto, los argumentos idénticos en la línea de comandos pasan a su enfoque actual' cat file.txt | tail -1') . 21 jul. 162016-07-21 14:43:19


2

Cuando envíe su cadena de comandos para golpear, hace lo siguiente:

  1. Crea un tubo de E/S.
  2. Inicia "/ usr/bin/tail -1", leyendo de la tubería y escribiendo en file.txt.
  3. Inicia "/ usr/bin/cat file.txt", escribiendo en la tubería.

Cuando 'gato' comienza a leer, 'archivo.txt' ya ha sido truncado por 'cola'.

Todo esto forma parte del diseño de Unix y del entorno de shell, y se remonta hasta el shell Bourne original. Es una característica, no un error.

  0

¿Se especifica el orden entre (2) y (3)? Mi impresión es que están sucediendo al mismo tiempo, lo que efectivamente ocurre es una carrera, aunque 'archivo.txt' se abre para escritura antes de ejecutar'/usr/bin/tail', lo que hace que el truncamiento sea muy probable que gane raza (ya que '/ usr/bin/cat' requiere un ejecutivo, con todo el impacto en el rendimiento del vinculador/cargador que implica). 21 jul. 162016-07-21 14:46:14


2

tmp = $ (cola -1 archivo.txt); echo $ tmp> file.txt;

+1

muy bien evita archivos temporales, pero debe ser citado para evitar todos los problemas estándar. 23 sep. 082008-09-23 20:31:58


5

Puede usar sed para borrar todas las líneas, pero el último de un archivo:

sed -i '$!d' file 
  • -i le indica a sed para reemplazar el archivo en su lugar; de lo contrario, el resultado escribiría en STDOUT.
  • $ es la dirección que coincide con la última línea del archivo.
  • d es el comando de eliminar. En este caso, es negado por !, por lo que todas las líneas no que coincidan con la dirección serán eliminadas.
  0

Lo único desafortunado de esto es que 'sed -i' no es compatible con POSIX, pero es una extensión de GNU. Usar 'ed' o' ex' para editar in situ evitaría esas advertencias. 14 may. 132013-05-14 13:16:13


2

Esto funciona muy bien en un shell de Linux:

replace_with_filter() { 
    local filename="$1"; shift 
    local dd_output byte_count filter_status dd_status 
    dd_output=$("[email protected]" <"$filename" | dd conv=notrunc of="$filename" 2>&1; echo "${PIPESTATUS[@]}") 
    { read; read; read -r byte_count _; read filter_status dd_status; } <<<"$dd_output" 
    ((filter_status > 0)) && return "$filter_status" 
    ((dd_status > 0)) && return "$dd_status" 
    dd bs=1 seek="$byte_count" if=/dev/null of="$filename" 
} 

replace_with_filter file.txt tail -1 

dd 's 'notrunc' opción se utiliza para escribir los contenidos filtrados hacia atrás, en su lugar, mientras que dd se necesita de nuevo (con una cuenta de bytes) para truncar realmente el archivo. Si el tamaño del archivo nuevo es mayor o igual que el tamaño del archivo anterior, no es necesaria la segunda invocación dd.

Las ventajas de esto sobre un método de copia de archivo son: 1) no es necesario espacio en disco adicional, 2) rendimiento más rápido en archivos grandes, y 3) shell puro (que no sea dd).

  0

Me gusta eso, es una idea innovadora. No estoy votando en este momento porque el uso de '$ FILTER' se ejecuta en http://mywiki.wooledge.org/BashFAQ/050 (depender de la división de cadenas para formar correctamente un comando es extremadamente propenso a errores) 13 may. 132013-05-13 14:14:29

  0

Espero que no te molesten mis ediciones pesadas; esta solución ahora debería ser considerablemente más robusta. 14 may. 132013-05-14 13:14:09

  0

No lo llamaría 'replace_with_filter'. Esto funcionará solo para los filtros que con cada fragmento siguiente producen datos no más que el fragmento original. De lo contrario, la información leída por el filtro podría sobrescribirse por su resultado. Tenga en cuenta que en lugar de la última 'dd' puede usar' truncate -s $ byte_count "$ filename" '. 14 may. 132013-05-14 16:00:28

  0

@ony 'truncate' no es una herramienta estándar POSIX, se encuentra en coreutils modernos, pero hay muchas plataformas donde bash está disponible pero no lo es. 14 may. 132013-05-14 17:23:21


1

Solo para este caso es posible usar

cat < file.txt | (rm file.txt; tail -1 > file.txt)
Esto abrirá "file.txt" justo antes de la conexión "cat" con subshell en "(...)". "rm file.txt" eliminará la referencia del disco antes de que subshell lo abra para escribir para "cola", pero el contenido seguirá estando disponible a través del descriptor abierto que se pasa a "cat" hasta que se cierre stdin. Así que es mejor estar seguro de que este comando terminará o contenidos de "archivo.txt" se perderá

  0

Los agresores anónimos deben ser castigados. Esta respuesta funciona y tiene lógica (es decir, eliminar el archivo después de abrirlo para leerlo y mantener el acceso a los contenidos). 22 abr. 132013-04-22 09:54:36

  0

Tengo problemas para estar en desacuerdo con la proposición (presunta) de que debe evitarse una solución en la que los casos de falla conducen a la pérdida de datos, incluso cuando esa advertencia esté claramente etiquetada; una subshell innecesaria es la guinda del pastel. Y "castigado"? De Verdad? 13 may. 132013-05-13 14:12:47

  0

@CharlesDuffy, "debe ser castigado" es para el anonimato. Debido a que downvote sin ningún razonamiento es irritante. ¿No lo crees? ... En cuanto a la corrección, ¿estás seguro de que se requiere una gran seguridad y fiabilidad para obtener una respuesta correcta? A veces puedes sacrificar algo por archivar otra cosa. Por lo tanto, desde diferentes puntos de vista puede tratar las soluciones de manera diferente. Si proyectas mi respuesta a tu vista, encontrarás que la precondición para esta respuesta no se puede cumplir, así que esa es una lógica incorrecta para decir que está mal. 14 may. 132013-05-14 12:57:46

  0

... por cierto, para ser claros, yo no era el votante anónimo; Simplemente considero sus acciones defendibles. 14 may. 132013-05-14 13:21:23

  0

@CharlesDuffy, bien, esa es su suposición acerca de downvoter. Pero la razón real puede ser diferente. Y ese es el problema. 14 may. 132013-05-14 15:34:09

  0

Para citar un poco: "Si los constructores construyen casas de la misma forma en que los desarrolladores construyen el software, el primer pájaro carpintero que venga destruiría la civilización". Sugerir que las personas usen enfoques innecesariamente frágiles como una cuestión de rutina (sin una razón específica para hacer lo contrario) conduce a software y sistemas plagados de modos de falla inesperados e indocumentados y, por lo tanto, perpetúa el problema. 14 may. 132013-05-14 15:51:39

  0

@CharlesDuffy, el hecho de reemplazar el contenido del archivo con su última línea ya es una ruta incorrecta. Esto debería ser dos archivos separados (si alguien necesita contenidos completos) o debería ser solo una línea en ese archivo (es decir, una línea producida en lugar de contenidos completos), por lo que no será necesario hacer 'tail -1' en ella. Y estoy bastante seguro de que la variedad de variantes proviene del hecho de que esto se parece más a un desafío para la mente. 14 may. 132013-05-14 17:00:59

  0

Las modificaciones en línea son algo que las personas hacen. 'tail -1' puede no ser un caso de uso particularmente bueno (de hecho, ciertamente no lo es), pero cualquiera con una carrera en administración de sistemas va a utilizar prácticas similares a las que se encuentran aquí en entornos de producción reales y scripts con frecuencia no trivial. Como tal, nos corresponde a nosotros - "rompecabezas" o no - alentar la adopción de buenas prácticas y desalentar la adopción de las malas. 14 may. 132013-05-14 17:28:49


1
echo "$(tail -1 file.txt)" > file.txt 
  0

¿Qué sucede si la última línea contiene solo '-n'? ¿Qué pasa si contiene literales de barra diagonal inversa y su 'echo' es compatible con las extensiones XSI POSIX (que requieren que se formen los escapes que esos literales se formen, incluso sin nada parecido a' -e')? Sería más seguro usar 'printf '% s \ n'" $ (tail -1 file.txt) "' en lugar de confiar en echo 21 jul. 162016-07-21 14:47:42