AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • Início
  • system&network
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • Início
  • system&network
    • Recentes
    • Highest score
    • tags
  • Ubuntu
    • Recentes
    • Highest score
    • tags
  • Unix
    • Recentes
    • tags
  • DBA
    • Recentes
    • tags
  • Computer
    • Recentes
    • tags
  • Coding
    • Recentes
    • tags
Início / unix / Perguntas / 754193
Accepted
terdon
terdon
Asked: 2023-08-17 00:08:44 +0800 CST2023-08-17 00:08:44 +0800 CST 2023-08-17 00:08:44 +0800 CST

Agora é seguro analisar a saída de GNU ls?

  • 772

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 lstê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 lstradicionalmente são ruins. Por exemplo:

  1. 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"
    
  2. 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 lse um awktruque inteligente e, se somarmos --zeroa ls, 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:

  1. Existe um caso que falharia em um dos dois exemplos acima? Talvez alguma esquisitice local?
  2. Se não, isso significa que as versões modernas do GNU lspodem realmente ser usadas com segurança com nomes de arquivo arbitrários?
shell
  • 3 3 respostas
  • 3946 Views

3 respostas

  • Voted
  1. Best Answer
    ilkkachu
    2023-08-17T00:59:19+08:002023-08-17T00:59:19+08:00

    Agora é seguro analisar a saída de GNU ls? (com --zero)

    --zeroajuda, e muito, mas ainda não é seguro do jeito que foi usado aqui. Existem problemas com o lspró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 ( --zeronão importa aqui):

    $ ls -l --time-style=long-iso foo.txt
    -rw-rw-r-- 1 foo bar users 0 2023-08-16 16:45 foo.txt
    

    No mínimo, você precisa --numeric-uid-gidde / -n, que imprime UIDs e GIDs como números ou -goos omite completamente. Ambos incluem os outros campos de formato longo também.

    lstambé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

    ls -dgo --time-style=long-iso --zero -- *
    

    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 --zeroe a omissão do UID e GID não ajudam, pois o tamanho do arquivo e a contagem de links podem variar em largura:

    $ ls -dgo --zero --time-style=long-iso -- *.txt |tr '\0' '\n'
    -rw-rw-r-- 21    0 2023-08-16 17:24 bar.txt
    -rw-rw-r--  1 1234 2023-08-16 17:30  leading space.txt
    

    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-isonã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. ( lsainda 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 explicitamente TZ=UTC0tornaria as datas mais fáceis de comparar como strings:

    $ TZ=Europe/Helsinki ls -dgo --time-style=long-iso -- *
    -rw-rw-r-- 1 0 2023-10-29 03:30 first
    -rw-rw-r-- 1 0 2023-10-29 03:20 second
    
    $ TZ=UTC0 ls -dgo --full-time -- *
    -rw-rw-r-- 1 0 2023-10-29 00:30:00.000000000 +0000 first
    -rw-rw-r-- 1 0 2023-10-29 01:20:00.000000000 +0000 second
    
    $ TZ=UTC0 ls -dgo --time-style=+%FT%T.%NZ -- *
    -rw-rw-r-- 1 0 2023-10-29T00:30:00.000000000Z first
    -rw-rw-r-- 1 0 2023-10-29T01:20:00.000000000Z second
    

    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, lsnã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.

    $ ls -dgo --zero --time-style=long-iso -- /dev/null somefile.txt |tr '\0' '\n'
    crw-rw-rw- 1  1, 3 2023-07-16 15:37 /dev/null
    -rw-rw-r-- 1 12345 2023-08-17 06:14 somefile.txt
    

    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 ->...

    $ ls -dgo --zero --time-style=long-iso -- how* what* |tr '\0' '\n'
    lrwxrwxrwx 1 14 2023-08-17 06:05 how -> about -> this?
    lrwxrwxrwx 1  5 2023-08-17 05:54 what -> is -> this?
    

    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-alwaysseria realmente melhor que --zero, pois imprime os dois citados individualmente com alguns caracteres especiais ou não imprimíveis escapados dentro $'':

    $ ls -dgo --quoting-style=shell-escape-always --time-style=long-iso -- how* what*  |cat
    lrwxrwxrwx 1 14 2023-08-17 06:05 'how' -> 'about -> this?'
    lrwxrwxrwx 1  5 2023-08-17 05:54 'what -> is' -> 'this?'
    

    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 lspara isso. O GNU find tem -printfo que eu acho que poderia ser feito para produzir uma saída segura e, se você quiser apenas lsclassificar por hora, não precisa imprimir o carimbo de data / hora, apenas ls --zerocom -t/ -u/ -cdeve 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 -pruneao final. Também --não ajuda com find, então você precisa do ./prefixo.

    Talvez stat --printffosse mais fácil.


    Existe um caso que falharia em um dos dois exemplos acima? Talvez alguma esquisitice local?

    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 segura lsseja 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ênuo split/ +/, $_, 6comeria 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 --zerodeve fornecer apenas nomes de arquivos brutos terminados por NULs, portanto, a saída deve ser segura e mais simples de analisar.

    Para $narquivos mais antigos, a página wiki tem:

    readarray -t -d '' -n 5 sorted < <(ls --zero -tr)
    # check the number of elements you got
    

    e para apenas um, você pode usar read -rd ''faria, como foi mencionado em um comentário:

    IFS= read -rd '' newest < <(ls -t --zero)
    # check the exit status or make sure "$newest" is not empty
    
    • 18
  2. Kaz
    2023-08-17T00:53:47+08:002023-08-17T00:53:47+08:00

    Se você vai depender da saída do GNU lsespecificamente, isso significa que você depende do pacote GNU Coreutils. Isso significa que você pode usar outro utilitário Coreutils, ou seja stat, . 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:

    $ echo $(date -d @$(stat --format="%Y" .) +"%b %m %H:%M")
    Aug 08 07:57
    

    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 -dargumento de date(um recurso do GNU Coreutils date) e, em seguida, usamos strftimecódigos para obter a hora no formato desejado.

    É uma pena que statnão haja uma maneira de formatar datas usando strftimeo built-in. Se quisermos obter vários campos de informações, incluindo o tempo de modificação, sem fazer várias chamadas para stat, temos que imprimir uma linha de vários campos que precisamos analisar. Esta ainda é uma medida melhor do que raspar a saída de arquivos ls. 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 de stat.

    Foi feita uma reclamação nos comentários que statnão pode ser usada para descobrir o arquivo com o tempo de modificação mais antigo. É verdade que statsozinho não pode fazê-lo, mas, de fato, statcombinado com a expansão do curinga do shell, pode fazê-lo tão bem quanto confiar no ls -1t.

    $ for x in *.txt ; do stat --format="%Y %n" "$x" ; done | sort -n | head -1
    1328379315 readme-mt.txt
    

    Esse arquivo remonta um pouco:

    $ date -d @1328379315
    Sat Feb  4 10:15:15 PST 2012
    

    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 -1obtemos 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 lscodificou espaços e novas linhas de alguma forma que temos que analisar.

    $ array=(*.txt)
    $ for x in ${!array[@]}; do 
    >   printf "%s %s\n" $(stat --format="%Y" "${array[$x]}") $x 
    > done | sort -n | head -1
    1328379315 29
    $ echo "${array[29]}"
    readme-mt.txt
    

    array[29]conterá o 30º arquivo encontrado por *.txt, não importa de quais caracteres esse nome seja feito. Nosso sorttrabalho é 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 para ls, e lógica de análise correta.

    A regra "não raspe a saída de ls" está no contexto do script.

    • 9
  3. Ed Morton
    2023-08-17T01:09:24+08:002023-08-17T01:09:24+08:00

    Dado este código do exemplo final na pergunta:

    ls -l --zero --time-style=long-iso -- * | 
        awk 'BEGIN{RS="\0"}{ t=index($0,$7); print substr($0,t+6), $6 }'
    

    e esta saída de amostra postada desse lscomando (com #<newline>a substituição dos NULs para visibilidade):

    $ 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#
    

    Parece que $7se 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:

    -rw-r--r--+ 1 terdon Domain Users 0 2023-08-15 19:16 file#
    

    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:

    ls -l --zero --time-style=long-iso -- * | 
        awk -v RS='\0' 'p=index($0,":") { print substr($0,p+4), substr($0,p-13,10) }'
    

    ou com GNU awk (que você provavelmente está usando de qualquer maneira para RS='\0') para o terceiro argumento para match():

    ls -l --zero --time-style=long-iso -- * | 
        awk -v RS='\0' 'match($0,/(.{10}) ..:.. (.*)/,a) { print a[2], a[1] }'
    
    • 4

relate perguntas

  • Como funciona este comando? mkfifo /tmp/f; cat /tmp/f | /bin/sh -i 2>&1 | nc -l 1234 > /tmp/f

  • FreeBSD's sh: funções de lista

  • Existe uma maneira de fazer ls mostrar arquivos ocultos apenas para determinados diretórios?

  • o que grep -v grep faz

  • Como salvar um caminho com ~ em uma variável?

Sidebar

Stats

  • Perguntas 205573
  • respostas 270741
  • best respostas 135370
  • utilizador 68524
  • Highest score
  • respostas
  • Marko Smith

    Possível firmware ausente /lib/firmware/i915/* para o módulo i915

    • 3 respostas
  • Marko Smith

    Falha ao buscar o repositório de backports jessie

    • 4 respostas
  • Marko Smith

    Como exportar uma chave privada GPG e uma chave pública para um arquivo

    • 4 respostas
  • Marko Smith

    Como podemos executar um comando armazenado em uma variável?

    • 5 respostas
  • Marko Smith

    Como configurar o systemd-resolved e o systemd-networkd para usar o servidor DNS local para resolver domínios locais e o servidor DNS remoto para domínios remotos?

    • 3 respostas
  • Marko Smith

    apt-get update error no Kali Linux após a atualização do dist [duplicado]

    • 2 respostas
  • Marko Smith

    Como ver as últimas linhas x do log de serviço systemctl

    • 5 respostas
  • Marko Smith

    Nano - pule para o final do arquivo

    • 8 respostas
  • Marko Smith

    erro grub: você precisa carregar o kernel primeiro

    • 4 respostas
  • Marko Smith

    Como baixar o pacote não instalá-lo com o comando apt-get?

    • 7 respostas
  • Martin Hope
    user12345 Falha ao buscar o repositório de backports jessie 2019-03-27 04:39:28 +0800 CST
  • Martin Hope
    Carl Por que a maioria dos exemplos do systemd contém WantedBy=multi-user.target? 2019-03-15 11:49:25 +0800 CST
  • Martin Hope
    rocky Como exportar uma chave privada GPG e uma chave pública para um arquivo 2018-11-16 05:36:15 +0800 CST
  • Martin Hope
    Evan Carroll status systemctl mostra: "Estado: degradado" 2018-06-03 18:48:17 +0800 CST
  • Martin Hope
    Tim Como podemos executar um comando armazenado em uma variável? 2018-05-21 04:46:29 +0800 CST
  • Martin Hope
    Ankur S Por que /dev/null é um arquivo? Por que sua função não é implementada como um programa simples? 2018-04-17 07:28:04 +0800 CST
  • Martin Hope
    user3191334 Como ver as últimas linhas x do log de serviço systemctl 2018-02-07 00:14:16 +0800 CST
  • Martin Hope
    Marko Pacak Nano - pule para o final do arquivo 2018-02-01 01:53:03 +0800 CST
  • Martin Hope
    Kidburla Por que verdadeiro e falso são tão grandes? 2018-01-26 12:14:47 +0800 CST
  • Martin Hope
    Christos Baziotis Substitua a string em um arquivo de texto enorme (70 GB), uma linha 2017-12-30 06:58:33 +0800 CST

Hot tag

linux bash debian shell-script text-processing ubuntu centos shell awk ssh

Explore

  • Início
  • Perguntas
    • Recentes
    • Highest score
  • tag
  • help

Footer

AskOverflow.Dev

About Us

  • About Us
  • Contact Us

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve