como `tail` o arquivo mais recente em um diretório

20

No shell, como posso tailcriar o arquivo mais recente em um diretório?

Itay Moav-Malimovka
fonte
11
vamos fechar, os programadores precisam seguir!
Amit
O fechamento é apenas para mudar para superusuário ou falha do servidor. A questão vai morar lá, e mais pessoas que possam estar interessadas a encontrarão.
Mnementh
O verdadeiro problema aqui é encontrar o arquivo de atualização mais recente no diretório e acredito que isso já foi respondido (aqui ou no Superusuário, não me lembro).
dmckee

Respostas:

24

Você não analisar a saída de ls! Analisar a saída de ls é difícil e não confiável .

Se você deve fazer isso, recomendo usar o find. Originalmente, eu tinha aqui um exemplo simples apenas para fornecer a essência da solução, mas como essa resposta parece um pouco popular, decidi revisar isso para fornecer uma versão que seja segura para copiar / colar e usar com todas as entradas. Você está sentado confortavelmente? Começaremos com um oneliner que fornecerá o arquivo mais recente no diretório atual:

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)"

Não é exatamente um oneliner agora, é? Aqui está novamente como uma função shell e formatada para facilitar a leitura:

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
}

E agora isso como um delineador:

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

Se tudo mais falhar, você pode incluir a função acima .bashrce considerar o problema resolvido, com uma ressalva. Se você apenas queria fazer o trabalho, não precisa ler mais.

A ressalva disso é que um nome de arquivo que termina em uma ou mais novas linhas ainda não será passado tailcorretamente. A solução do problema é complicada e considero suficiente que, se um nome de arquivo mal-intencionado for encontrado, o comportamento relativamente seguro de encontrar um erro "Nenhum arquivo desse tipo" ocorrerá em vez de algo mais perigoso.

Detalhes suculentos

Para os curiosos, essa é a explicação tediosa de como funciona, por que é seguro e por que outros métodos provavelmente não são.

Danger, Will Robinson

Antes de tudo, o único byte seguro para delimitar os caminhos de arquivo é nulo, pois é o único byte proibido universalmente nos caminhos de arquivo nos sistemas Unix. É importante ao manipular qualquer lista de caminhos de arquivo usar nulo apenas como delimitador e, ao entregar um único caminho de arquivo de um programa para outro, fazê-lo de uma maneira que não engasgue com bytes arbitrários. Existem muitas maneiras aparentemente corretas de resolver esse e outros problemas que falham ao assumir (mesmo que acidentalmente) que os nomes de arquivos não terão novas linhas ou espaços. Nenhuma das suposições é segura.

Para os propósitos de hoje, a primeira etapa é obter uma lista de arquivos delimitada por nulos fora da localização. Isso é muito fácil se você tiver um findsuporte -print0como o GNU:

find . -print0

Mas essa lista ainda não nos diz qual é a mais nova, por isso precisamos incluir essas informações. Eu escolho usar a -printfopção find, que me permite especificar quais dados aparecem na saída. Nem todas as versões de findsuporte -printf(não é padrão), mas o GNU find sim. Se você se encontrar sem -printf, precisará confiar em -exec stat {} \;que ponto deve desistir de toda a esperança de portabilidade, pois isso stattambém não é padrão. Por enquanto, vou seguir assumindo que você tem ferramentas GNU.

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

Aqui, estou solicitando o formato printf, %T@que é o tempo de modificação em segundos desde o início da época do Unix, seguido de um período e depois de um número indicando frações de segundo. Eu adiciono a isso outro período e depois %p(que é o caminho completo para o arquivo) antes de terminar com um byte nulo.

Agora eu tenho

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

Pode ser óbvio, mas por uma questão de conclusão, -maxdepth 1impede a findlistagem do conteúdo dos subdiretórios e \! -type dignora os diretórios que você provavelmente não deseja tail. Até o momento, tenho arquivos no diretório atual com informações de tempo de modificação; agora, preciso classificar por esse horário de modificação.

Obtê-lo na ordem certa

Por padrão, sortespera que sua entrada seja registros delimitados por nova linha. Se você possui o GNU, sortpode pedir para que ele espere registros delimitados por nulo, usando a -zopção .; por padrão, sortnão há solução. Eu só estou interessado em classificar pelos dois primeiros números (segundos e frações de segundo) e não quero classificar pelo nome do arquivo real, então eu digo sortduas coisas: primeiro, que ele deve considerar o período ( .) um delimitador de campo e segundo, que ele deve usar apenas o primeiro e o segundo campos ao considerar como classificar os registros.

| sort -znr -t. -k1,2

Antes de tudo, estou agrupando três opções curtas que não têm valor juntos; -znré apenas uma maneira concisa de dizer -z -n -r). Depois disso -t .(o espaço é opcional) informa sorto caractere delimitador de campo e -k 1,2especifica os números dos campos: primeiro e segundo ( sortconta os campos de um, não zero). Lembre-se de que um registro de amostra para o diretório atual se pareceria com:

1000000000.0000000000../some-file-name

Isso significa que sortvocê analisará primeiro 1000000000e depois 0000000000ao solicitar este registro. A -nopção informa sortpara usar a comparação numérica ao comparar esses valores, porque os dois valores são números. Isso pode não ser importante, pois os números são de comprimento fixo, mas não causam danos.

A outra opção dada sorté -rpara "reverso". Por padrão, a saída de uma classificação numérica será o número mais baixo primeiro, -raltera-o para listar os números mais baixos por último e os números mais altos primeiro. Como esses números são de data e hora mais altos, será mais recente e isso colocará o registro mais recente no início da lista.

Apenas os bits importantes

À medida que a lista de caminhos de arquivos surge sort, agora ela tem a resposta desejada que estamos procurando no topo. O que resta é encontrar uma maneira de descartar os outros registros e retirar o registro de data e hora. Infelizmente, mesmo o GNU heade tailnão aceita switches para fazê-los operar em entradas delimitadas por nulos. Em vez disso, uso um loop while como uma espécie de homem pobre head.

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

Primeiro, desmarco IFSpara que a lista de arquivos não seja sujeita à divisão de palavras. A seguir, digo readduas coisas: não interprete seqüências de escape na entrada ( -r) e a entrada é delimitada com um byte nulo ( -d); aqui a cadeia vazia ''é usada para indicar "sem delimitador", também conhecido como delimitado por null. Cada registro será lido na variável recordpara que cada vez que o whileloop itere, ele tenha um único registro de data e hora e um único nome de arquivo. Observe que -dé uma extensão GNU; se você tiver apenas um padrão, readessa técnica não funcionará e você terá pouco recurso.

Sabemos que a recordvariável possui três partes, todas delimitadas por caracteres de ponto. Usando o cututilitário, é possível extrair uma parte deles.

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

Aqui todo o registro é passado para printfe de lá canalizado para cut; no bash, você pode simplificar ainda mais isso usando uma string here para cut -d. -3f- <<<"$record"obter melhor desempenho. Dizemos cutduas coisas: primeiro, com -disso, um delimitador específico para a identificação de campos (como sortno delimitador .é usado). O segundo cuté instruído com -fpara imprimir apenas valores de campos específicos; a lista de campos é fornecida como um intervalo 3-que indica o valor do terceiro campo e de todos os campos seguintes. Isso significa que cutirá ler e ignorar tudo, inclusive o segundo .encontrado no registro, e imprimirá o restante, que é a parte do caminho do arquivo.

Depois de imprimir o caminho do arquivo mais recente, não há necessidade de continuar: breaksai do loop sem permitir que ele avance para o segundo caminho do arquivo.

A única coisa que resta é a execução tailno caminho do arquivo retornado por este pipeline. Você deve ter notado no meu exemplo que eu fiz isso colocando o pipeline em um subshell; o que você pode não ter notado é que coloquei o subshell entre aspas duplas. Isso é importante porque, finalmente, mesmo com todo esse esforço para ser seguro para qualquer nome de arquivo, uma expansão de sub-shell não citada ainda pode quebrar as coisas. Uma explicação mais detalhada está disponível se você estiver interessado. O segundo aspecto importante, mas facilmente esquecido, da invocação de tailé que forneci a opção --antes de expandir o nome do arquivo. Isso instruirátailque não há mais opções sendo especificadas e tudo o que se segue é um nome de arquivo, o que torna seguro manipular os nomes de arquivos que começam com -.

phogg
fonte
11
@AakashM: porque você pode obter resultados "surpreendentes", por exemplo, se um arquivo tiver caracteres "incomuns" em seu nome (quase todos os caracteres são legais).
John Zwinck
6
As pessoas que usam caracteres especiais em seus nomes de arquivo merecem tudo o que recebem :-) #
6
Ver paxdiablo fazer essa observação já foi doloroso o suficiente, mas duas pessoas votaram nela! As pessoas que escrevem software com bugs intencionalmente merecem tudo o que recebem.
John Zwinck
4
Portanto, a solução acima não funciona no osx devido à falta da opção -printf no find, mas o seguinte funciona apenas no osx devido a diferenças no comando stat ... talvez ainda ajude alguémtail -f $(find . -type f -exec stat -f "%m {}" {} \;| sort -n | tail -n 1 | cut -d ' ' -f 2)
audio.zoom
2
"Infelizmente, mesmo o GNU heade tailnão aceita switches para fazê-los operar em entradas delimitadas por nulos." Meu substituto para head: … | grep -zm <number> "".
Kamil Maciorowski
22
tail `ls -t | head -1`

Se você está preocupado com nomes de arquivos com espaços,

tail "`ls -t | head -1`"
Pontudo
fonte
11
Mas o que acontece quando o seu arquivo mais recente possui espaços ou caracteres especiais? Use $ () em vez de `` e cite seu subshell para evitar esse problema.
Phpg #
Eu gosto disso. Limpo e simples. Como deveria ser.
6
É fácil ser limpo e simples se você sacrificar robusto e correto.
Phpg #
2
Bem, depende do que você está fazendo, realmente. Uma solução que sempre funciona em todos os lugares, com todos os nomes de arquivos possíveis, é muito boa, mas em uma situação restrita (arquivos de log, por exemplo, com nomes não estranhos conhecidos), pode ser desnecessária.
Esta é a solução mais limpa até agora. Obrigado!
demisx
4

Você pode usar:

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

A $()construção inicia um sub-shell que executa o comando ls -1t(listando todos os arquivos na ordem do tempo, um por linha) e canalizando-o head -1para obter a primeira linha (arquivo).

A saída desse comando (o arquivo mais recente) é passada para o tailprocessamento.

Lembre-se de que isso corre o risco de obter um diretório, se essa for a entrada de diretório mais recente criada. Usei esse truque em um alias para editar o arquivo de log mais recente (de um conjunto rotativo) em um diretório que continha apenas esses arquivos de log.


fonte
O -1não é necessário, lsfaz isso para você quando está em um cano. Compare lse ls|cat, por exemplo.
Pausado até novo aviso.
Esse pode ser o caso no Linux. No Unix "verdadeiro", os processos não mudavam de comportamento com base no local onde estavam indo. Isso tornaria a depuração do pipeline realmente muito irritante :-) #
Hmmm, não tenho certeza se isso está correto - o ISTR precisa emitir "ls -C" para obter uma saída formatada em coluna no 4.2BSD ao canalizar a saída através de um filtro, e tenho certeza que o ls no Solaris funciona da mesma maneira. Qual é o "Unix verdadeiro e verdadeiro", afinal?
Citações! Citações! Os nomes de arquivos têm espaços!
Norman Ramsey #
@TMN: A única maneira verdadeira de Unix é não confiar em ls para consumidores não humanos. "Se a saída é para um terminal, o formato é definido pela implementação." - esta é a especificação. Se você quiser ter certeza de que precisa dizer ls -1 ou ls -C.
phogg
4

Nos sistemas POSIX, não há como obter a entrada de diretório "última criação". Cada entrada de diretório possui atime, mtimee ctime, ao contrário do Microsoft Windows, ctimeisso não significa CreationTime, mas "Hora da última alteração de status".

Portanto, o melhor que você pode obter é "ajustar o último arquivo modificado recentemente", o que é explicado nas outras respostas. Eu iria para este comando:

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

Observe as aspas ao redor do lscomando. Isso faz com que o trecho funcione com quase todos os nomes de arquivos.

Roland Illig
fonte
Bom trabalho. Direto ao ponto. +1
Norman Ramsey
4

Eu só quero ver o tamanho do arquivo mudar, você pode usar o watch.

watch -d ls -l
tpal
fonte
3

Em zsh:

tail *(.om[1])

Consulte: http://zsh.sourceforge.net/Doc/Release/Expansion.html#Glob-Qualifiers , aqui mdenota o tempo de modificação m[Mwhms][-|+]ne o anterior osignifica que ele é classificado de uma maneira ( Oclassifica da outra maneira). Os .meios apenas arquivos regulares. Dentro dos colchetes, [1]escolhe o primeiro item. Para escolher três usos [1,3], para obter o uso mais antigo [-1].

É bom curto e não usa ls.

Anne van Rossum
fonte
1

Provavelmente existem milhões de maneiras de fazer isso, mas a maneira como eu faria é:

tail `ls -t | head -n 1`

Os bits entre os backticks (os caracteres semelhantes a aspas) são interpretados e o resultado retornado ao final.

ls -t #gets the list of files in time order
head -n 1 # returns the first line only
iblamefish
fonte
2
Backticks são maus. Use $ () em vez disso.
William Pursell
1

Um simples:

tail -f /path/to/directory/*

funciona muito bem para mim.

O problema é obter arquivos gerados depois que você iniciou o comando tail. Mas se você não precisar disso (como todas as soluções acima não se importam com isso), o asterisco é apenas uma solução mais simples, IMO.

bruno.braga
fonte
0
tail`ls -tr | tail -1`
user22644
fonte
Você esqueceu um espaço lá!
Blacklight Shining
0

Alguém postou e apagou por algum motivo, mas este é o único que funciona, então ...

tail -f `ls -tr | tail`
Itay Moav-Malimovka
fonte
você tem que excluir diretórios, não é?
8243 amit
11
Eu postei isso originalmente, mas eu deletei isso desde que eu concorde com Sorpigal que analisar saída lsnão é a coisa mais inteligente a fazer ...
ChristopheD
Eu preciso rápido e sujo, sem diretórios nele. Então, se você adicionar sua resposta, eu aceitarei essa #
Itay Moav -Malimovka
0
tail -f `ls -lt | grep -v ^d | head -2 | tail -1 | tr -s " " | cut -f 8 -d " "`

Explicação:

  • ls -lt: lista de todos os arquivos e diretórios classificados por hora da modificação
  • grep -v ^ d: excluir diretórios
  • cabeça -2 em diante: analisando o nome do arquivo necessário
amit
fonte
11
+1 para inteligente, -2 para analisar ls output, -1 para não citar o subshell, -1 para uma suposição mágica do "campo 8" (não é portátil!) E, finalmente, -1 para muito inteligente . Pontuação geral: -4.
phogg
@Sorpigal concordou. Feliz por ser o mau exemplo.
Amit
sim não imaginava que seria errado em tantos contagem
Amit
0
tail "$(ls -1tr|tail -1)"
user31894
fonte