En shell, ¿cómo puedo tail
crear el último archivo en un directorio?
En shell, ¿cómo puedo tail
crear el último archivo en un directorio?
Respuestas:
¡ 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 .bashrc
y 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á tail
correctamente. 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.
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.
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 find
soporte -print0
como 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 -printf
interruptor de find que me permite especificar qué datos aparecen en la salida. No todas las versiones de find
soporte -printf
(no es estándar) pero GNU find sí. Si se encuentra sin -printf
usted, necesitará confiar -exec stat {} \;
en qué momento debe renunciar a toda esperanza de portabilidad, ya stat
que 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 1
evita que se find
enumeren los contenidos de los subdirectorios y \! -type d
omite 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.
De forma predeterminada, sort
espera que su entrada sean registros delimitados por nueva línea. Si tiene GNU sort
, puede pedirle que espere registros delimitados por nulos utilizando el -z
interruptor .; para estándar sort
no 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 sort
dos 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; -znr
es solo una forma concisa de decir -z -n -r
). Después de eso -t .
(el espacio es opcional) le dice sort
al carácter delimitador de campo y -k 1,2
especifica los números de campo: primero y segundo ( sort
cuenta 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 sort
se verá primero 1000000000
y luego 0000000000
al ordenar este registro. La -n
opción le indica sort
que 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 sort
es -r
para "reversa". Por defecto, la salida de una ordenación numérica será primero los números más bajos, los -r
cambia 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.
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 head
y tail
no 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 IFS
para que la lista de archivos no esté sujeta a división de palabras. A continuación, digo read
dos 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 record
para que cada vez que el while
ciclo se repita, tenga una sola marca de tiempo y un solo nombre de archivo. Tenga en cuenta que -d
es una extensión GNU; Si solo tiene un estándar, read
esta técnica no funcionará y tendrá pocos recursos.
Sabemos que la record
variable tiene tres partes, todas delimitadas por caracteres de punto. Usando la cut
utilidad es posible extraer una parte de ellos.
printf '%s' "$record" | cut -d. -f3-
Aquí se pasa todo el registro hacia printf
y 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 cut
dos cosas: primero con -d
eso, debe un delimitador específico para identificar campos (como con sort
el delimitador .
se utiliza). El segundo cut
recibe instrucciones -f
para 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 cut
leerá 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: break
sale del bucle sin dejar que se mueva a la segunda ruta del archivo.
Lo único que queda es ejecutarse tail
en 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 tail
es que le proporcioné la opción --
antes de expandir el nombre del archivo. Esto instruirátail
que 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 -
.
tail -f $(find . -type f -exec stat -f "%m {}" {} \;| sort -n | tail -n 1 | cut -d ' ' -f 2)
head
y tail
no aceptan interruptores para que funcionen con entradas delimitadas por valores nulos". Mi reemplazo para head
: … | grep -zm <number> ""
.
tail `ls -t | head -1`
Si te preocupan los nombres de archivos con espacios,
tail "`ls -t | head -1`"
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 -1
para obtener la primera línea (archivo).
La salida de ese comando (el archivo más reciente) se pasa a tail
procesar.
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.
-1
es necesario, lo ls
hace por ti cuando está en una tubería. Compara ls
y ls|cat
, por ejemplo.
En los sistemas POSIX, no hay forma de obtener la entrada del directorio "última creación". Cada entrada de directorio tiene atime
, mtime
y ctime
, a diferencia de Microsoft Windows, ctime
no 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 ls
comando. Esto hace que el fragmento funcione con casi todos los nombres de archivo.
En zsh
:
tail *(.om[1])
Ver: http://zsh.sourceforge.net/Doc/Release/Expansion.html#Glob-Qualifiers , aquí m
denota el tiempo de modificación m[Mwhms][-|+]n
, y lo anterior o
significa que está ordenado de una manera (lo O
ordena 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
.
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
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.
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`
ls
no es lo más inteligente ...
tail -f `ls -lt | grep -v ^d | head -2 | tail -1 | tr -s " " | cut -f 8 -d " "`
Explicación:
tail "$(ls -1tr|tail -1)"