Usando uma lista gerada de nomes de arquivos como lista de argumentos - com espaços

16

Estou tentando invocar um script com uma lista de nomes de arquivos coletados por find. Nada de especial, apenas algo assim:

$ myscript `find . -name something.txt`

O problema é que alguns dos nomes de caminho contêm espaços e, portanto, são divididos em dois nomes inválidos na expansão do argumento. Normalmente, eu colocaria os nomes entre aspas, mas aqui eles são inseridos pela expansão de aspas. Eu tentei filtrar a saída finde cercar cada nome de arquivo com aspas, mas quando o bash os vê, é tarde demais para removê-los e eles são tratados como parte do nome do arquivo:

$ myscript `find . -name something.txt | sed 's/.*/"&"/'`
No such file or directory: '"./somedir/something.txt"'

Sim, essas são as regras de como a linha de comando é processada, mas como posso contorná-la?

Isso é embaraçoso, mas não estou conseguindo apresentar a abordagem correta. Finalmente descobri como fazê-lo xargs -0 -n 10000... mas é um truque tão feio que ainda quero perguntar: como cito os resultados da expansão de cotações anteriores ou obtenho o mesmo efeito de outra maneira?

Edit: Eu estava confuso sobre o fato que xargs faz coleta todos os argumentos em uma única lista de argumentos, a menos que seja dito de outra forma ou limites do sistema pode ser excedida. Obrigado a todos por me esclarecer! Outros, lembre-se disso ao ler a resposta aceita, porque ela não é apontada diretamente.

Aceitei a resposta, mas minha pergunta permanece: não existe uma maneira de proteger espaços na $(...)expansão de backtick (ou )? (Observe que a solução aceita é uma resposta não-bash).

alexis
fonte
Eu acho que você precisaria alterar o que o shell usa como separadores de nome de arquivo (por exemplo, jogando com o valor do IFS, uma maneira possível é IFS="newline "). Mas é necessário executar o script em todos os nomes de arquivos? Caso contrário, considere usar-se para executar o script para cada arquivo.
Njsg
Mudar o IFS é uma ótima idéia, não tinha pensado nisso! Não é prático para o uso da linha de comando, mas ainda assim. :-) E sim, o objetivo é passar todos os argumentos para a mesma invocação do meu script.
Alexis20

Respostas:

12

Você pode fazer o seguinte usando algumas implementações finde xargsassim.

$ find . -type f -print0 | xargs -r0 ./myscript

ou, normalmente, apenas find:

$ find . -type f -exec ./myscript {} +

Exemplo

Digamos que eu tenha o seguinte diretório de exemplo.

$ tree
.
|-- dir1
|   `-- a\ file1.txt
|-- dir2
|   `-- a\ file2.txt
|-- dir3
|   `-- a\ file3.txt
`-- myscript

3 directories, 4 files

Agora, digamos que tenho isso para ./myscript.

#!/bin/bash

for i in "$@"; do
    echo "file: $i"
done

Agora, quando eu executo o seguinte comando.

$ find . -type f -print0 | xargs -r0 ./myscript 
file: ./dir2/a file2.txt
file: ./dir3/a file3.txt
file: ./dir1/a file1.txt
file: ./myscript

Ou quando eu uso o segundo formulário da seguinte forma:

$ find . -type f -exec ./myscript {} +
file: ./dir2/a file2.txt
file: ./dir3/a file3.txt
file: ./dir1/a file1.txt
file: ./myscript

Detalhes

find + xargs

Os 2 métodos acima, embora pareçam diferentes, são essencialmente os mesmos. O primeiro é pegar a saída de find, dividindo-a usando NULLs ( \0) através do -print0switch para encontrar. O xargs -0foi projetado especificamente para receber entradas divididas usando NULLs. Essa sintaxe não-padrão foi introduzida pelo GNU finde xargstambém é encontrada atualmente em alguns outros, como os BSDs mais recentes. A -ropção é necessária para evitar chamadas myscriptse findnão encontrar nada no GNU, findmas não nos BSDs.

NOTA: Toda essa abordagem depende do fato de você nunca passar uma string excessivamente longa. Se for, uma segunda invocação de ./myscriptserá iniciada com o restante dos resultados subsequentes da localização.

encontre com +

Essa é a maneira padrão (embora tenha sido adicionada apenas relativamente recentemente (2005) à implementação do GNU find). A capacidade de fazer o que estamos fazendo xargsé literalmente incorporada find. Assim, findvocê encontrará uma lista de arquivos e passará a lista o maior número possível de argumentos para o comando especificado depois -exec(observe que {}só pode ser a última +neste momento), executando os comandos várias vezes, se necessário.

Por que não citar?

No primeiro exemplo, estamos pegando um atalho, evitando completamente os problemas com a citação, usando NULLs para separar os argumentos. Quando xargsé fornecida essa lista, ela é instruída a dividir os NULLs, protegendo efetivamente nossos átomos de comando individuais.

No segundo exemplo, mantemos os resultados internos finde, portanto, ele sabe o que é cada átomo de arquivo e garantirá o tratamento adequado deles, evitando assim o negócio de citá-los.

Tamanho máximo da linha de comando?

Essa pergunta surge de tempos em tempos, então, como bônus, eu a adiciono a essa resposta, principalmente para que eu possa encontrá-la no futuro. Você pode usar xargspara ver como é o limite do ambiente:

$ xargs --show-limits
Your environment variables take up 4791 bytes
POSIX upper limit on argument length (this system): 2090313
POSIX smallest allowable upper limit on argument length (all systems): 4096
Maximum length of command we could actually use: 2085522
Size of command buffer we are actually using: 131072
slm
fonte
11
Obrigado, mas preciso passar todos os argumentos para a mesma invocação do meu script. Isso está na descrição do problema, mas acho que não deixei claro que não é incidental.
alexis
@alexis - leia as respostas novamente, eles estão passando todos os argumentos para uma única chamada do seu script.
slm
Eu serei amaldiçoado! Eu não sabia sobre o +argumento find(e você também usa +prosa, então perdi sua explicação na primeira vez). Mas mais ao ponto, eu tinha entendido mal o que xargsfaz por padrão !!! Em três décadas de usar Unix Eu nunca tive um uso para ele até agora, mas eu pensei que eu sabia que a minha caixa de ferramentas ...
alexis
@alexis - imaginei que você tivesse perdido o que estávamos dizendo. Sim xargsé um diabo de um comando. Você precisa lê-lo e findas páginas de manual muitas vezes para entender o que eles podem fazer. Muitas das opções são contra-positivas uma da outra, o que aumenta a confusão.
slm
@alexis - também mais uma coisa a ser adicionada à caixa de ferramentas, não use aspas / bastões para executar comandos aninhados, use $(..)agora. Ele lida automaticamente com o aninhamento de cotações etc. Os backticks estão sendo descontinuados.
slm
3
find . -name something.txt -exec myscript {} +

No exemplo acima, findlocaliza todos os nomes de arquivos correspondentes e os fornece como argumentos para myscript. Isso funciona com nomes de arquivos, independentemente de espaços ou qualquer outro caractere ímpar.

Se todos os nomes de arquivos couberem em uma linha, o myscript será executado uma vez. Se a lista for muito longa para o shell manipular, o find irá executar o myscript várias vezes, conforme necessário.

MAIS: Quantos arquivos cabem em uma linha de comando? man finddiz que findconstrói linhas de comando "da mesma maneira que o xargs constrói suas". E man xargsque os limites dependem do sistema e que você pode determiná-los executando xargs --show-limits. ( getconf ARG_MAXtambém é uma possibilidade). No Linux, o limite é tipicamente (mas nem sempre) em torno de 2 milhões de caracteres por linha de comando.

John1024
fonte
2

Mais um acréscimo à excelente resposta de @ slm.

A limitação no tamanho dos argumentos está na execve(2)chamada do sistema (na verdade, está no tamanho cumulativo das seqüências de caracteres do argumento e do ambiente e ponteiros). Se myscriptestiver escrito em uma linguagem que seu shell possa interpretar, talvez você não precise executá- lo; você pode fazer com que seu shell o interprete sem ter que executar outro intérprete.

Se você executar o script como:

(. myscript x y)

É como:

myscript x y

Exceto que ele está sendo interpretado por um filho do shell atual, em vez de executá- lo (o que eventualmente envolve a execução sh (ou o que a linha she-bang especificar, se houver) com ainda mais argumentos).

Agora, obviamente, você não pode usar find -exec {} +com o .comando, como .sendo um comando interno do shell, ele deve ser executado pelo shell, não por find.

Com zsh, é fácil:

IFS=$'\0'
(. myscript $(find ... -print0))

Ou:

(. myscript ${(ps:\0:)"$(find ... -print0)"}

Embora com zsh, você não precisaria findem primeiro lugar, pois a maioria de seus recursos é incorporada ao zshglobbing.

bashvariáveis, no entanto, não podem conter caracteres NUL; portanto, você precisa encontrar outra maneira. Uma maneira poderia ser:

files=()
while IFS= read -rd '' -u3 file; do
  files+=("$file")
done 3< <(find ... -print0)
(. myscript "${files[@]}")

Você também pode usar globbing recursivo no estilo zsh com a globstaropção bash4.0 e posterior:

shopt -s globstar failglob dotglob
(. myscript ./**/something.txt)

Observe que os **links simbólicos foram seguidos até os diretórios até serem corrigidos no bash4.3. Observe também que bashnão implementa zshqualificadores de globbing para que você não obtenha todos os recursos find.

Outra alternativa seria usar o GNU ls:

eval "files=(find ... -exec ls -d --quoting-style=shell-always {} +)"
(. myscript "${files[@]}")

Os métodos acima também podem ser usados ​​se você quiser ter certeza de que myscripté executado apenas uma vez (falhando se a lista de argumentos for muito grande). Nas versões recentes do Linux, você pode aumentar e até elevar essa limitação na lista de argumentos com:

ulimit -s 1048576

(Tamanho da pilha de 1GiB, um quarto do qual pode ser usado para a lista arg + env).

ulimit -s unlimited

(sem limite)

Stéphane Chazelas
fonte
1

Na maioria dos sistemas, há um limite no comprimento de uma linha de comando passada para qualquer programa, usando xargsou -exec command {} +. De man find:

-exec command {} +
      This  variant  of the -exec action runs the specified command on
      the selected files, but the command line is built  by  appending
      each  selected file name at the end; the total number of invoca
      tions of the command will  be  much  less  than  the  number  of
      matched  files.   The command line is built in much the same way
      that xargs builds its command lines.  Only one instance of  `{}'
      is  allowed  within the command.  The command is executed in the
      starting directory.

As invocações serão muito menores, mas não garantidas. O que você deve fazer é ler a NUL separados nomes no script de stdin, possível com base em um argumento de linha de comando -o -. Eu faria algo como:

$ find . -name something.txt -print0 | myscript -0 -o -

e implemente os argumentos das opções de myscriptacordo.

Timo
fonte
Sim, o sistema operacional impõe um limite no número / tamanho dos argumentos que podem ser passados. Nos sistemas Linux modernos, isso é (gigantesco) ( linux.die.net/man/2/execve ) (1/4 do tamanho da pilha, argumentos 0x7FFFFFFF). O AFAIK bash em si não impõe nenhum limite. Minhas listas são muito menores e meu problema foi causado por mal-entendidos ou lembranças erradas de como xargsfunciona. Sua solução é realmente a mais robusta, mas é um exagero nesse caso.
22414 alexis
0

Não existe uma maneira de proteger espaços na expansão de backtick (ou $ (...))?

Não, não existe. Por que é que?

Bash não tem como saber o que deve ser protegido e o que não deve.

Não há matrizes no arquivo / canal unix. É apenas um fluxo de bytes. O comando dentro do ``ou $()gera um fluxo, que bash engole e trata como uma única sequência. Nesse ponto, você só tem duas opções: colocá-lo entre aspas, mantê-lo como uma sequência ou nu, para que o bash o divida de acordo com o comportamento configurado.

Então, o que você deve fazer se quiser uma matriz é definir um formato de bytes que tenha uma matriz, e é isso que as ferramentas gostam xargse findfazem: se você as executa com o -0argumento, elas funcionam de acordo com um formato de matriz binária que termina os elementos com o byte nulo, adicionando semântica ao fluxo de bytes opaco.

Infelizmente, bashnão pode ser configurado para dividir seqüências de caracteres no byte nulo. Agradecemos a /unix//a/110108/17980 por nos mostrar o que zshpode.

xargs

Você deseja que seu comando seja executado uma vez e disse que xargs -0 -n 10000resolve o seu problema. Não, garante que, se você tiver mais de 10000 parâmetros, seu comando será executado mais de uma vez.

Se você deseja executá-lo estritamente uma vez ou falhar, é necessário fornecer o -xargumento e um -nargumento maior que o -sargumento (realmente: grande o suficiente para que um monte de argumentos de comprimento zero mais o nome do comando não se encaixem o -stamanho). ( homem xargs , veja trecho bem abaixo)

O sistema em que estou atualmente tem uma pilha limitada a cerca de 8 milhões, então aqui está o meu limite:

$ printf '%s\0' -- {1..1302582} | xargs -x0n 2076858 -s 2076858 /bin/true
xargs: argument list too long
$ printf '%s\0' -- {1..1302581} | xargs -x0n 2076858 -s 2076858 /bin/true
(no output)

bater

Se você não deseja envolver um comando externo, o loop while-read que alimenta uma matriz, conforme mostrado em /unix//a/110108/17980 , é a única maneira de o bash dividir as coisas em o byte nulo.

A ideia de criar o script ( . ... "$@" )para evitar o limite de tamanho da pilha é legal (tentei, funciona!), Mas provavelmente não é importante para situações normais.

Usar um fd especial para o pipe de processo é importante se você quiser ler algo mais do stdin, mas, caso contrário, não precisará dele.

Portanto, a maneira "nativa" mais simples, para as necessidades domésticas diárias:

files=()
while IFS= read -rd '' file; do
    files+=("$file")
done <(find ... -print0)

myscriptornonscript "${files[@]}"

Se você deseja que sua árvore de processos seja limpa e agradável de ver, esse método permite exec mynonscript "${files[@]}", o que remove o processo bash da memória, substituindo-o pelo comando chamado. xargssempre permanecerá na memória enquanto o comando chamado é executado, mesmo se o comando for executado apenas uma vez.


O que fala contra o método bash nativo é o seguinte:

$ time { printf '%s\0' -- {1..1302581} | xargs -x0n 2076858 -s 2076858 /bin/true; }

real    0m2.014s
user    0m2.008s
sys     0m0.172s

$ time {
  args=()
  while IFS= read -rd '' arg; do
    args+=( "$arg" )
  done < <(printf '%s\0' -- $(echo {1..1302581}))
  /bin/true "${args[@]}"
}
bash: /bin/true: Argument list too long

real    107m51.876s
user    107m38.532s
sys     0m7.940s

O bash não é otimizado para manipulação de array.


homem xargs :

-n max-args

Use no máximo argumentos max-args por linha de comando. Argumentos menores que max-args serão usados ​​se o tamanho (consulte a opção -s) for excedido, a menos que a opção -x seja fornecida, nesse caso o xargs será encerrado.

-s max-chars

Use no máximo caracteres max-chars por linha de comando, incluindo o comando e os argumentos iniciais e os nulos finais no final das sequências de argumentos. O maior valor permitido depende do sistema e é calculado como o limite de comprimento do argumento para exec, menos o tamanho do seu ambiente, menos 2048 bytes de altura livre. Se esse valor for maior que 128KiB, 128Kib será usado como o valor padrão; caso contrário, o valor padrão é o máximo. 1KiB é 1024 bytes.

-x

Saia se o tamanho (consulte a opção -s) for excedido.

clacke
fonte
Obrigado por todo o problema, mas sua premissa básica ignora o fato de que o bash normalmente usa um sistema elaborado de processamento de cotações. Mas não na expansão de cotações. Comparar o seguinte (erros que ambos dão, mas mostrar a diferença): ls "what is this"vs. ls `echo '"what is this"'` . Alguém esqueceu de implementar o processamento de cotações para o resultado de aspas.
Alexis 22/05
Fico feliz que as aspas não processem as cotações. O fato de eles até dividirem palavras causou bastante aparência confusa, arranhões na cabeça e falhas de segurança na história moderna da computação.
Clacke 22/05
A pergunta é "Não existe uma maneira de proteger espaços na $(...)expansão de backtick (ou )?", Portanto, parece apropriado ignorar o processamento que não é feito nessa situação.
Clacke 22/05
O formato de matriz de elemento com terminação nula é a maneira mais simples e, portanto, mais segura de expressar uma matriz. É uma pena que bashnão o apóie nativamente como aparentemente o apóia zsh.
Clacke 22/05
De fato, apenas nesta semana eu usei printf "%s\0"e resolvi xargs -0uma situação de cotação em que uma ferramenta intermediária passava parâmetros através de uma string analisada por um shell. A citação sempre volta para te morder.
Clacke 22/05