A sabedoria aceita nas últimas décadas é que nunca é uma boa ideia analisar a saída de ls
( [1] , [2] ). Por exemplo, se eu quiser salvar a data de modificação de um arquivo junto com seu nome em uma variável shell, esta não é a maneira correta de fazer isso:
$ ls -l file
-rw-r--r-- 1 terdon terdon 0 Aug 15 19:16 file
$ foo=$(ls -l file | awk '{print $9,$6,$7,$8}')
$ echo "$foo"
file Aug 15 19:16
Assim que o nome do arquivo for ligeiramente diferente, a abordagem falha:
$ ls -l file*
-rw-r--r-- 1 terdon terdon 0 Aug 15 19:16 'file with spaces'
$ foo=$(ls -l file* | awk '{print $9,$6,$7,$8}')
$ echo "$foo"
file Aug 15 19:16
Fica pior se a data de modificação do arquivo não for próxima de hoje, pois isso pode alterar o formato da hora:
$ ls -l
total 0
-rw-r--r-- 1 terdon terdon 0 Aug 15 19:21 file
-rw-r--r-- 1 terdon terdon 0 Aug 15 2018 'file with spaces'
No entanto, as versões mais recentes do GNU coreutils ls
têm duas opções que podem ser combinadas para definir um formato de hora específico e produzir uma saída delimitada por NULL:
--time-style=TIME_STYLE
time/date format with -l; see TIME_STYLE below
[...]
--zero end each output line with NUL, not newline
[...]
The TIME_STYLE argument can be full-iso, long-iso, iso, locale, or
+FORMAT. FORMAT is interpreted like in date(1). If FORMAT is FOR‐
MAT1<newline>FORMAT2, then FORMAT1 applies to non-recent files and
FORMAT2 to recent files. TIME_STYLE prefixed with 'posix-' takes ef‐
fect only outside the POSIX locale. Also the TIME_STYLE environment
variable sets the default style to use.
Aqui estão os arquivos novamente, com essas opções definidas (o zero no final de cada linha de saída é substituído por #
e uma nova linha aqui para melhorar a legibilidade):
$ ls -l --zero --time-style=long-iso -- *
-rw-r--r--+ 1 terdon terdon 0 2023-08-16 21:35 a file with a
newline#
-rw-r--r--+ 1 terdon terdon 0 2023-08-15 19:16 file#
-rw-r--r--+ 1 terdon terdon 0 2018-08-15 12:00 file with spaces#
Com essas opções disponíveis, posso fazer muitas das coisas que ls
tradicionalmente são ruins. Por exemplo:
Obtenha o nome do arquivo modificado mais recentemente em uma variável:
$ touch 'a file with a'$'\n''newline' $ last=$(ls -tr --zero | tail -z -n1) bash: warning: command substitution: ignored null byte in input $ printf -- 'LAST: "%s"\n' "$last" LAST: "a file with a newline"
O exemplo que gerou essa pergunta. Outra pergunta, no Ask Ubuntu, onde o OP queria imprimir o nome do arquivo e a data de modificação. Alguém postou uma resposta usando
ls
e umawk
truque inteligente e, se somarmos--zero
als
, parece ser bem robusto:$ output=$(ls -l --zero --time-style=long-iso -- * | awk 'BEGIN{RS="\0"}{ t=index($0,$7); print substr($0,t+6), $6 }') $ printf 'Output: "%s"\n' "$output" Output: "a file with a newline 2023-08-16"
Não consigo encontrar um nome que quebre qualquer um desses dois exemplos. Então, minhas perguntas são:
- Existe um caso que falharia em um dos dois exemplos acima? Talvez alguma esquisitice local?
- Se não, isso significa que as versões modernas do GNU
ls
podem realmente ser usadas com segurança com nomes de arquivo arbitrários?
--zero
ajuda, e muito, mas ainda não é seguro do jeito que foi usado aqui. Existem problemas com ols
próprio formato de saída e com os comandos usados na pergunta para analisar a saída.--zero
é realmente mencionado na página wiki ParsingLs, mas eles não usam o formato longo nos exemplos lá (talvez por causa dos problemas aqui!). Várias questões nesta resposta foram levantadas por Stéphane Chazelas nos comentários.Para começar,
ls -l
é um problema, pois ainda imprime nomes de usuários/grupos que contêm espaços em branco como estão, bagunçando a contagem de colunas (--zero
não importa aqui):No mínimo, você precisa
--numeric-uid-gid
de /-n
, que imprime UIDs e GIDs como números ou-go
os omite completamente. Ambos incluem os outros campos de formato longo também.ls
também listará o conteúdo de todos os diretórios que aparecem entre os argumentos, então você provavelmente vai querer-d
, também.Não acho que as outras colunas possam conter espaços ou NULs, então
pode ser seguro. Talvez.
Ainda não é o mais fácil de analisar, pois se houver vários arquivos, ele preencherá as colunas com espaços, em vez de usar apenas um como separador de campo, para que você não possa usar, por exemplo, na saída
cut
. Isso acontece mesmo quando a saída para um canal--zero
e a omissão do UID e GID não ajudam, pois o tamanho do arquivo e a contagem de links podem variar em largura:O nome do arquivo não é preenchido à direita (e fazer isso seria estranho), então provavelmente é seguro assumir que há apenas um espaço entre o carimbo de data/hora e o nome do arquivo.
--time-style=long-iso
não inclui o deslocamento UTC, o que significa que as datas podem ser ambíguas. Na pior das hipóteses, dois arquivos criados próximo ao término do horário de verão podem ser exibidos com datas que parecem estar na ordem errada. (ls
ainda os classificaria corretamente se solicitado, mas a saída seria confusa.)--full-time
/--time-style=full-iso
(ou um formato personalizado) seria melhor nisso e definir explicitamenteTZ=UTC0
tornaria as datas mais fáceis de comparar como strings:Fica pior se você tiver qualquer coisa além de arquivos regulares. Pode não ser um problema em muitos casos, mas de qualquer maneira:
Para arquivos de dispositivo,
ls
não imprime seu tamanho, mas sim os números de dispositivo principal/secundário. Separados por uma vírgula e um espaço, tornando a contagem da coluna diferente dos outros arquivos. Você pode distinguir as duas variantes pela vírgula, mas isso torna a análise mais dolorosa.Depois, há links simbólicos, que em formato longo são impressos como
link name -> link target
, mas não há nada a dizer que o link ou o nome do destino possam conter->
...Bem, acho que tecnicamente o campo de tamanho informa o comprimento (em bytes, não em caracteres) do nome do link...
Este é um caso em que
--quoting-style=shell-escape-always
seria realmente melhor que--zero
, pois imprime os dois citados individualmente com alguns caracteres especiais ou não imprimíveis escapados dentro$''
:Não que seja divertido analisar isso também, mesmo com um shell.
Seria melhor se pudéssemos selecionar explicitamente os campos que queremos, mas não vejo uma opção
ls
para isso. O GNU find tem-printf
o que eu acho que poderia ser feito para produzir uma saída segura e, se você quiser apenasls
classificar por hora, não precisa imprimir o carimbo de data / hora, apenasls --zero
com-t
/-u
/-c
deve fazer. Veja abaixo. (zsh poderia fazer isso sozinho, mas o Bash não é tão bom.)Se você quiser os carimbos de data/hora e os nomes dos arquivos, algo como
find ./* -printf '%TY-%Tm-%Td %TT %p\0'
deve ser feito, embora, é claro, recurse aos subdiretórios por padrão, então você terá que fazer algo a respeito se não quiser. Talvez apenas adicione-prune
ao final. Também--
não ajuda comfind
, então você precisa do./
prefixo.Talvez
stat --printf
fosse mais fácil.Dos comandos usados na questão,
last=$(ls -tr --zero | tail -z -n1)
por si só não é seguro no Bash, pois a substituição do comando remove as novas linhas à direita, após ignorar o NL final. E como Ed Morton aponta , pelo menos aquele comando AWK em particular está apenas quebrado, independentemente de quão segurals
seja a saída dele mesmo.Não acho que o AWK seja adequado para entradas onde há um número fixo de campos onde o último pode conter separadores de campo. O Perl
split()
tem um argumento extra para limitar o número de campos a serem produzidos, exceto que não é muito fácil usá-lo quando alguns (não todos) dos separadores de campo podem ser vários espaços. Um ingênuosplit/ +/, $_, 6
comeria espaços iniciais de nomes de arquivos. Você poderia construir um regex para lidar com isso e com o problema do nó do dispositivo, mas isso está começando a ser como forçar um pino redondo em um orifício quadrado e não corrige o problema de saída do link simbólico.Sem a saída de formato longo,
ls --zero
deve fornecer apenas nomes de arquivos brutos terminados por NULs, portanto, a saída deve ser segura e mais simples de analisar.Para
$n
arquivos mais antigos, a página wiki tem:e para apenas um, você pode usar
read -rd ''
faria, como foi mencionado em um comentário:Se você vai depender da saída do GNU
ls
especificamente, isso significa que você depende do pacote GNU Coreutils. Isso significa que você pode usar outro utilitário Coreutils, ou sejastat
, . Stat tem strings de formato para obter as informações sobre o objeto da maneira necessária.Por exemplo, imprima a hora de modificação do diretório atual no formulário
MMM DD HH:MM
:O comando
stat --format=%Y .
nos dá o tempo de modificação do.
objeto como um inteiro decimal representando os segundos familiares desde a época.Interpolamos isso com um
@
prefixo como-d
argumento dedate
(um recurso do GNU Coreutilsdate
) e, em seguida, usamosstrftime
códigos para obter a hora no formato desejado.É uma pena que
stat
não haja uma maneira de formatar datas usandostrftime
o built-in. Se quisermos obter vários campos de informações, incluindo o tempo de modificação, sem fazer várias chamadas parastat
, temos que imprimir uma linha de vários campos que precisamos analisar. Esta ainda é uma medida melhor do que raspar a saída de arquivosls
. Se a eficiência máxima não for importante (e se for, por que estamos codificando em Bash), podemos sofrer várias invocações destat
.Foi feita uma reclamação nos comentários que
stat
não pode ser usada para descobrir o arquivo com o tempo de modificação mais antigo. É verdade questat
sozinho não pode fazê-lo, mas, de fato,stat
combinado com a expansão do curinga do shell, pode fazê-lo tão bem quanto confiar nols -1t
.Esse arquivo remonta um pouco:
Agora temos o problema de que, se o nome contiver novas linhas, isso atrapalhará a classificação. Poderíamos contornar isso de maneiras que não são fáceis com
ls
.Por exemplo, podemos ler os nomes em uma matriz Bash e, em vez dos nomes, imprimir os carimbos de data/hora junto com os índices da matriz. Da saída de
sort -n | head -1
obtemos um item cujo segundo campo nos dá o índice do array do nome do arquivo modificado menos recentemente.Podemos evitar totalmente a questão de lidar com a saída que
ls
codificou espaços e novas linhas de alguma forma que temos que analisar.array[29]
conterá o 30º arquivo encontrado por*.txt
, não importa de quais caracteres esse nome seja feito. Nossosort
trabalho é imune a isso porque não vê esse nome.Portanto, para responder à pergunta, o GNU ls possui alguns recursos que tornam mais seguro analisar sua saída, mas ainda não é fácil analisar a saída com segurança na linguagem shell.
GNU ls pode ser usado com segurança por, digamos, um programa C que faz
popen("ls ...", "r")
com as opções certas parals
, e lógica de análise correta.A regra "não raspe a saída de
ls
" está no contexto do script.Dado este código do exemplo final na pergunta:
e esta saída de amostra postada desse
ls
comando (com#<newline>
a substituição dos NULs para visibilidade):Parece que
$7
se destina a ser o carimbo de data/hora. Nesse caso,t=index($0,$7)
falharia para nomes/grupos de usuários com mais de 1 palavra, por exemplo:desde então, seu timestamp estaria em
$8
(ou algum número maior, dependendo de quantas palavras estão no nome de usuário e/ou grupo), não$7
.Dado que os nomes/grupos de usuários não podem incluir
:
, você pode resolver isso apenas procurando o primeiro:
na linha em vez de procurar um campo específico:ou com GNU awk (que você provavelmente está usando de qualquer maneira para
RS='\0'
) para o terceiro argumento paramatch()
: