cómo `seguir` el último archivo en un directorio

20

En shell, ¿cómo puedo tailcrear el último archivo en un directorio?

Itay Moav -Malimovka
fuente
1
vamos cerradores, los programadores deben seguir!
amit
El cierre es solo para pasar al superusuario o al servidor por defecto. La pregunta vivirá allí, y más personas que puedan estar interesadas la encontrarán.
Mnementh
El verdadero problema aquí es encontrar el archivo de actualización más reciente en el directorio y creo que ya se ha respondido (ya sea aquí o en Super User, no puedo recordarlo).
dmckee

Respuestas:

24

¡ No analice la salida de ls! Analizar la salida de ls es difícil y poco confiable .

Si debe hacer esto, le recomiendo usar find. Originalmente tenía aquí un ejemplo simple simplemente para darle una idea general de la solución, pero dado que esta respuesta parece algo popular, decidí revisarla para proporcionar una versión segura para copiar / pegar y usar con todas las entradas. ¿Estás sentado cómodamente? Comenzaremos con un oneliner que le dará el último archivo en el directorio actual:

tail -- "$(find . -maxdepth 1 -type f -printf '%T@.%p\0' | sort -znr -t. -k1,2 | while IFS= read -r -d '' -r record ; do printf '%s' "$record" | cut -d. -f3- ; break ; done)"

No es un buen línea ahora, ¿verdad? Aquí está nuevamente como una función de shell y formateada para facilitar la lectura:

latest-file-in-directory () {
    find "${@:-.}" -maxdepth 1 -type f -printf '%T@.%p\0' | \
            sort -znr -t. -k1,2 | \
            while IFS= read -r -d '' -r record ; do
                    printf '%s' "$record" | cut -d. -f3-
                    break
            done
}

Y ahora eso como línea:

tail -- "$(latest-file-in-directory)"

Si todo lo demás falla, puede incluir la función anterior en su .bashrcy considerar el problema resuelto, con una advertencia. Si solo desea hacer el trabajo, no necesita leer más.

La advertencia con esto es que un nombre de archivo que termina en una o más líneas nuevas todavía no se pasará tailcorrectamente. Evitar este problema es complicado y considero que es suficiente que si se encuentra un nombre de archivo malicioso de este tipo, se produzca un comportamiento relativamente seguro de encontrar un error "No existe ese archivo" en lugar de algo más peligroso.

Detalles jugosos

Para los curiosos, esta es la tediosa explicación de cómo funciona, por qué es seguro y por qué otros métodos probablemente no lo son.

Peligro, Will Robinson

En primer lugar, el único byte que es seguro para delimitar las rutas de archivos es nulo porque es el único byte universalmente prohibido en las rutas de archivos en sistemas Unix. Cuando se maneja cualquier lista de rutas de archivos, es importante utilizar solo nulo como delimitador y, al entregar incluso una sola ruta de archivo de un programa a otro, hacerlo de una manera que no ahogue bytes arbitrarios. Hay muchas formas aparentemente correctas de resolver este y otros problemas que fallan al suponer (incluso accidentalmente) que los nombres de los archivos no tendrán nuevas líneas o espacios en ellos. Ninguna de las suposiciones es segura.

Para los propósitos de hoy, el primer paso es obtener una lista de archivos delimitada por nulos fuera de búsqueda. Esto es bastante fácil si tiene un findsoporte -print0como GNU:

find . -print0

Pero esta lista todavía no nos dice cuál es la más nueva, por lo que debemos incluir esa información. Elijo usar el -printfinterruptor de find que me permite especificar qué datos aparecen en la salida. No todas las versiones de findsoporte -printf(no es estándar) pero GNU find sí. Si se encuentra sin -printfusted, necesitará confiar -exec stat {} \;en qué momento debe renunciar a toda esperanza de portabilidad, ya statque tampoco es estándar. Por ahora voy a seguir asumiendo que tienes herramientas GNU.

find . -printf '%T@.%p\0'

Aquí solicito el formato printf, %T@que es el tiempo de modificación en segundos desde el comienzo de la época de Unix seguido de un punto y luego un número que indica fracciones de segundo. Agrego a esto otro período y luego %p(que es la ruta completa al archivo) antes de terminar con un byte nulo.

Ahora tengo

find . -maxdepth 1 \! -type d -printf '%T@.%p\0'

Puede ser obvio, pero por el hecho de estar completo -maxdepth 1evita que se findenumeren los contenidos de los subdirectorios y \! -type domite los directorios que es poco probable que desee tail. Hasta ahora tengo archivos en el directorio actual con información de tiempo de modificación, así que ahora necesito ordenar por ese tiempo de modificación.

Conseguirlo en el orden correcto

De forma predeterminada, sortespera que su entrada sean registros delimitados por nueva línea. Si tiene GNU sort, puede pedirle que espere registros delimitados por nulos utilizando el -zinterruptor .; para estándar sortno hay solución. Solo estoy interesado en ordenar por los dos primeros números (segundos y fracciones de segundo) y no quiero ordenar por el nombre real del archivo, así que digo sortdos cosas: Primero, que debería considerar el punto ( .) como un delimitador de campo y segundo, que solo debe usar los campos primero y segundo al considerar cómo ordenar los registros.

| sort -znr -t. -k1,2

En primer lugar, estoy agrupando tres opciones cortas que no tienen valor juntas; -znres solo una forma concisa de decir -z -n -r). Después de eso -t .(el espacio es opcional) le dice sortal carácter delimitador de campo y -k 1,2especifica los números de campo: primero y segundo ( sortcuenta los campos de uno, no cero). Recuerde que un registro de muestra para el directorio actual se vería así:

1000000000.0000000000../some-file-name

Esto significa que sortse verá primero 1000000000y luego 0000000000al ordenar este registro. La -nopción le indica sortque use la comparación numérica al comparar estos valores, porque ambos valores son números. Esto puede no ser importante ya que los números son de longitud fija pero no hace daño.

El otro interruptor dado sortes -rpara "reversa". Por defecto, la salida de una ordenación numérica será primero los números más bajos, los -rcambia para que enumere los números más bajos al final y los números más altos primero. Dado que estos números son marcas de tiempo más altas, significará más nuevos y esto coloca el registro más nuevo al comienzo de la lista.

Solo las partes importantes

A medida que emerge la lista de rutas de archivo sort, ahora tiene la respuesta deseada que estamos buscando en la parte superior. Lo que queda es encontrar una manera de descartar los otros registros y eliminar la marca de tiempo. Desafortunadamente, incluso GNU heady tailno aceptan interruptores para que funcionen con entradas delimitadas por nulos. En cambio, uso un bucle while como una especie de hombre pobre head.

| while IFS= read -r -d '' record

Primero lo desarmo IFSpara que la lista de archivos no esté sujeta a división de palabras. A continuación, digo readdos cosas: no interprete secuencias de escape en la entrada ( -r) y la entrada se delimita con un byte nulo ( -d); aquí la cadena vacía ''se usa para indicar "sin delimitador", también delimitado por nulo. Cada registro se leerá en la variable recordpara que cada vez que el whileciclo se repita, tenga una sola marca de tiempo y un solo nombre de archivo. Tenga en cuenta que -des una extensión GNU; Si solo tiene un estándar, readesta técnica no funcionará y tendrá pocos recursos.

Sabemos que la recordvariable tiene tres partes, todas delimitadas por caracteres de punto. Usando la cututilidad es posible extraer una parte de ellos.

printf '%s' "$record" | cut -d. -f3-

Aquí se pasa todo el registro hacia printfy desde allí canalizado a cut; en bash se podía simplificar este adicionalmente utilizando una cadena de aquí a cut -d. -3f- <<<"$record"un mejor rendimiento. Decimos cutdos cosas: primero con -deso, debe un delimitador específico para identificar campos (como con sortel delimitador .se utiliza). El segundo cutrecibe instrucciones -fpara imprimir solo valores de campos específicos; la lista de campos se proporciona como un rango 3-que indica el valor del tercer campo y de todos los campos siguientes. Esto significa que cutleerá e ignorará todo, incluido el segundo .que encuentre en el registro, y luego imprimirá el resto, que es la parte de la ruta del archivo.

Después de imprimir la ruta del archivo más reciente, no hay necesidad de continuar: breaksale del bucle sin dejar que se mueva a la segunda ruta del archivo.

Lo único que queda es ejecutarse tailen la ruta del archivo que devuelve esta canalización. Puede haber notado en mi ejemplo que hice esto al encerrar la tubería en una subshell; lo que quizás no haya notado es que incluí la subshell entre comillas dobles. Esto es importante porque al final, incluso con todo este esfuerzo para estar seguro para cualquier nombre de archivo, una expansión de subshell sin comillas podría romper las cosas. Una explicación más detallada está disponible si está interesado. El segundo aspecto importante pero fácil de pasar por alto para la invocación tailes que le proporcioné la opción --antes de expandir el nombre del archivo. Esto instruirátailque no se están especificando más opciones y que todo lo que sigue es un nombre de archivo, lo que hace que sea seguro manejar nombres de archivos que comienzan con -.

phogg
fuente
1
@AakashM: porque puede obtener resultados "sorprendentes", por ejemplo, si un archivo tiene caracteres "inusuales" en su nombre (casi todos los caracteres son legales).
John Zwinck
66
Las personas que usan caracteres especiales en sus nombres de archivo merecen todo lo que obtienen :-)
66
Ver a Paxdiablo hacer ese comentario fue bastante doloroso, ¡pero luego dos personas lo votaron! Las personas que escriben software con errores intencionalmente merecen todo lo que obtienen.
John Zwinck
44
Entonces, la solución anterior no funciona en osx debido a la falta de la opción -printf en find, pero lo siguiente solo funciona en osx debido a las diferencias en el comando stat ... tal vez todavía ayude a alguientail -f $(find . -type f -exec stat -f "%m {}" {} \;| sort -n | tail -n 1 | cut -d ' ' -f 2)
audio.zoom
2
"Desafortunadamente, incluso GNU heady tailno aceptan interruptores para que funcionen con entradas delimitadas por valores nulos". Mi reemplazo para head: … | grep -zm <number> "".
Kamil Maciorowski
22
tail `ls -t | head -1`

Si te preocupan los nombres de archivos con espacios,

tail "`ls -t | head -1`"
Puntiagudo
fuente
1
Pero, ¿qué sucede cuando su último archivo tiene espacios o caracteres especiales? Use $ () en lugar de `` y cite su subshell para evitar este problema.
phogg
Me gusta esto. Limpio y sencillo. Como debería ser.
66
Es fácil ser limpio y simple si sacrificas lo robusto y lo correcto.
phogg
2
Bueno, realmente depende de lo que estés haciendo. Una solución que siempre funciona en todas partes, para todos los nombres de archivo posibles, es muy buena, pero en una situación restringida (archivos de registro, por ejemplo, con nombres conocidos no extraños) podría ser innecesaria.
Esta es la solución más limpia hasta ahora. ¡Gracias!
Demisx
4

Puedes usar:

tail $(ls -1t | head -1)

La $()construcción inicia un sub-shell que ejecuta el comando ls -1t(enumerando todos los archivos en orden de tiempo, uno por línea) y canalizándolo head -1para obtener la primera línea (archivo).

La salida de ese comando (el archivo más reciente) se pasa a tailprocesar.

Tenga en cuenta que esto corre el riesgo de obtener un directorio si esa es la entrada de directorio más reciente creada. He usado ese truco en un alias para editar el archivo de registro más reciente (de un conjunto rotativo) en un directorio que contenía solo esos archivos de registro.


fuente
No -1es necesario, lo lshace por ti cuando está en una tubería. Compara lsy ls|cat, por ejemplo.
Pausado hasta nuevo aviso.
Ese puede ser el caso en Linux. En "verdadero" Unix, los procesos no cambiaron su comportamiento en función del destino de su producción. Eso haría que la tubería de depuración muy molesto :-)
Hmmm, no estoy seguro de que sea correcto: ISTR tiene que emitir "ls -C" para obtener una salida con formato de columna bajo 4.2BSD cuando canaliza la salida a través de un filtro, y estoy bastante seguro de que ls bajo Solaris funciona de la misma manera. ¿Cuál es el "uno, verdadero Unix" de todos modos?
¡Citas! ¡Citas! ¡Los nombres de archivo tienen espacios en ellos!
Norman Ramsey
@ TMN: La única forma verdadera de Unix es no confiar en ls para consumidores no humanos. "Si la salida es a una terminal, el formato está definido por la implementación". - Esta es la especificación. Si quiere estar seguro, debe decir ls -1 o ls -C.
phogg
4

En los sistemas POSIX, no hay forma de obtener la entrada del directorio "última creación". Cada entrada de directorio tiene atime, mtimey ctime, a diferencia de Microsoft Windows, ctimeno significa CreationTime, sino "Hora del último cambio de estado".

Entonces, lo mejor que puede obtener es "seguir el último archivo modificado recientemente", que se explica en las otras respuestas. Yo iría por este comando:

cola -f "$ (ls -tr | sed 1q)"

Tenga en cuenta las comillas alrededor del lscomando. Esto hace que el fragmento funcione con casi todos los nombres de archivo.

Roland Illig
fuente
Buen trabajo. Directo al grano. +1
Norman Ramsey
4

Si solo desea ver el cambio de tamaño de archivo, puede usar watch.

watch -d ls -l
tpal
fuente
3

En zsh:

tail *(.om[1])

Ver: http://zsh.sourceforge.net/Doc/Release/Expansion.html#Glob-Qualifiers , aquí mdenota el tiempo de modificación m[Mwhms][-|+]n, y lo anterior osignifica que está ordenado de una manera (lo Oordena de la otra manera). Los .medios solo archivos regulares. Dentro de los corchetes se [1]recoge el primer elemento. Para elegir tres usos [1,3], para obtener el uso más antiguo [-1].

Es bonito corto y no se usa ls.

Anne van Rossum
fuente
1

Probablemente hay un millón de formas de hacer esto, pero la forma en que lo haría es esta:

tail `ls -t | head -n 1`

Los bits entre los backticks (la comilla como caracteres) se interpretan y el resultado vuelve a la cola.

ls -t #gets the list of files in time order
head -n 1 # returns the first line only
iblamefish
fuente
2
Los backticks son malvados. Use $ () en su lugar.
William Pursell
1

Un simple:

tail -f /path/to/directory/*

funciona bien para mí

El problema es obtener archivos que se generan después de iniciar el comando de cola. Pero si no necesita eso (ya que todas las soluciones anteriores no le importan), el asterisco es una solución más simple, en mi opinión.

bruno.braga
fuente
0
tail`ls -tr | tail -1`
usuario22644
fuente
¡Olvidaste un espacio allí!
Blacklight Shining
0

Alguien lo publicó y luego lo borró por alguna razón, pero este es el único que funciona, así que ...

tail -f `ls -tr | tail`
Itay Moav -Malimovka
fuente
tienes que excluir directorios, ¿no?
amit
1
Publiqué esto originalmente, pero lo eliminé, ya que estoy de acuerdo con Sorpigal en que analizar la salida lsno es lo más inteligente ...
ChristopheD
Lo necesito rápido y sucio, sin directorios. Entonces, si agrega su respuesta, aceptaré esa
Itay Moav -Malimovka
0
tail -f `ls -lt | grep -v ^d | head -2 | tail -1 | tr -s " " | cut -f 8 -d " "`

Explicación:

  • ls -lt: lista de todos los archivos y directorios ordenados por hora de modificación
  • grep -v ^ d: excluye directorios
  • cabeza -2 en adelante: analizando el nombre de archivo necesario
amit
fuente
1
+1 para inteligente, -2 para analizar la salida de ls, -1 para no citar la subshell, -1 para una suposición mágica de "campo 8" (¡no es portátil!) Y finalmente -1 para demasiado inteligente . Puntuación general: -4.
phogg
@Sorpigal De acuerdo. Aunque feliz de ser el mal ejemplo.
Amit
Si no se imaginó que sería mal en muchos conteos
Amit
0
tail "$(ls -1tr|tail -1)"
usuario31894
fuente