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, então eles 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 backquote. Eu tentei filtrar a saída de find e cercar cada nome de arquivo com aspas, mas no momento em que o bash os vê, é tarde demais para retirá-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 contornar isso?

Isso é embaraçoso, mas não estou conseguindo chegar à abordagem correta. Eu finalmente descobri como fazer isso com xargs -0 -n 10000 ... mas é um hack tão feio que eu ainda quero perguntar: como cito os resultados da expansão do backquote, ou obtenho o mesmo efeito de outra maneira?

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

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

    
por alexis 19.01.2014 / 23:38

5 respostas

12

Você pode fazer o seguinte usando algumas implementações de find e xargs desta forma.

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

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

3 directories, 4 files

Agora digamos que eu tenha 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 a segunda forma assim:

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

Detalhes

encontre + xargs

Os 2 métodos acima, apesar de parecerem diferentes, são essencialmente os mesmos. A primeira é pegar a saída de find, dividindo-a usando NULLs ( -print0 ) através da opção xargs -0 para localizar. O find é especificamente projetado para receber entradas divididas usando NULLs. Essa sintaxe não padrão foi introduzida pelo GNU xargs e -r , mas também é encontrada hoje em dia em alguns outros como os BSDs mais recentes. A opção myscript é necessária para evitar chamar find se find não encontrar nada com o GNU ./myscript , mas não com os BSDs.

OBSERVAÇÃO: Essa abordagem toda depende do fato de que você nunca passará uma string que é excessivamente longa. Se for, então uma segunda invocação de find será iniciada com o restante dos resultados subseqüentes encontrados.

encontre com +

Essa é a maneira padrão (embora tenha sido adicionada recentemente (2005) à implementação GNU de xargs ). A capacidade de fazer o que estamos fazendo com find é literalmente incorporada em find . Portanto, -exec encontrará uma lista de arquivos e passará essa lista com tantos argumentos quantos couberem ao comando especificado após {} (observe que + pode ser apenas anterior a xargs neste caso), 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 as citações, usando NULLs para separar os argumentos. Quando find recebe essa lista, ela é instruída a dividir os NULLs efetivamente protegendo nossos átomos de comando individuais.

No segundo exemplo, estamos mantendo os resultados internos em xargs e, portanto, sabemos qual é o átomo de cada arquivo e garantiremos que eles sejam manipulados adequadamente, 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, estou adicionando a essa resposta, principalmente para poder encontrá-la no futuro. Você pode usar %code% para ver o limite do ambiente assim:

$ 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
    
por 20.01.2014 / 00:49
3
find . -name something.txt -exec myscript {} +

Acima, find localiza todos os nomes de arquivos correspondentes e os fornece como argumentos para myscript . Isso funciona com nomes de arquivos, independentemente dos espaços ou de quaisquer outros caracteres estranhos.

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

MORE: Quantos arquivos cabem em uma linha de comando? man find diz que find constrói as linhas de comando "da mesma maneira que o xargs constrói". E man xargs que os limites dependem do sistema e que você pode determiná-los executando xargs --show-limits . ( getconf ARG_MAX também é uma possibilidade). No Linux, o limite é normalmente (mas nem sempre) cerca de 2 milhões de caracteres por linha de comando.

    
por 20.01.2014 / 00:49
2

Alguns acréscimos à resposta do @ slm.

A limitação no tamanho dos argumentos está na chamada do sistema execve(2) (na verdade, é no tamanho cumulativo das cadeias de caracteres e argumentos e ponteiros do ambiente). Se myscript é escrito em uma linguagem que seu shell pode interpretar, então talvez você não precise executar , você poderia ter seu shell apenas interpretando-o sem ter que executar outro interpretador.

Se você executar o script como:

(. myscript x y)

É como:

myscript x y

Só que está sendo interpretado por um filho do shell atual, em vez de executá-lo (o que eventualmente envolve executar sh (ou qualquer que seja a linha she-bang) especifica 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=$'
(. myscript ${(ps:
files=()
while IFS= read -rd '' -u3 file; do
  files+=("$file")
done 3< <(find ... -print0)
(. myscript "${files[@]}")
:)"$(find ... -print0)"}
' (. myscript $(find ... -print0))

Ou:

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

Embora com zsh , você não precisaria de find , já que a maioria de seus recursos são incorporados em zsh globbing.

No entanto, as variáveis

bash não podem conter caracteres NUL, por isso você precisa encontrar outro caminho. Um caminho poderia ser:

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

Você também pode usar o recurso de globalização recursiva de estilo zsh com a opção globstar em bash 4.0 e posterior:

ulimit -s 1048576

Observe que ** seguiu links simbólicos para diretórios até que foi corrigido em bash 4.3. Observe também que bash não implementa os qualificadores zsh globbing, portanto você não obterá todos os recursos de find .

Outra alternativa seria usar o GNU ls :

ulimit -s unlimited

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

(. myscript x y)

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

myscript x y

(sem limite)

    
por 20.01.2014 / 11:54
1

Na maioria dos sistemas, há um limite no comprimento de uma linha de comando passada para qualquer programa, usando xargs ou -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 como uma. O que você deve fazer é ler os nomes de arquivos separados NUL no script de stdin, possíveis 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 da opção para myscript de acordo.

    
por 20.01.2014 / 09:19
0

Isn't there some way to protect spaces in backtick (or $(...)) expansion?

Não, não há. Por que isso?

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

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

Então, o que você precisa fazer se quiser que uma matriz defina um formato de byte que tenha uma matriz, e é isso que ferramentas como xargs e find do: se você executá-las com o argumento -0 , eles funcionam de acordo com um formato de matriz binária que termina elementos com o byte nulo, adicionando semântica ao fluxo de bytes opaco.

Infelizmente, bash não pode ser configurado para dividir strings no byte nulo. Graças ao link para nos mostrar que zsh pode.

xargs

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

Se você quiser que seja estritamente executado uma vez ou falhe, você deve fornecer o argumento -x e um argumento -n maior que o argumento -s (na verdade: grande o suficiente para que um monte de argumentos de comprimento mais o nome do comando não se encaixam no -s size). ( man xargs , veja o trecho bem abaixo)

O sistema no qual estou atualmente tem uma pilha limitada a cerca de 8M, então aqui está meu limite:

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

myscriptornonscript "${files[@]}"
' -- {1..1302582} | xargs -x0n 2076858 -s 2076858 /bin/true xargs: argument list too long $ printf '%s
$ time { printf '%s
$ printf '%s
files=()
while IFS= read -rd '' file; do
    files+=("$file")
done <(find ... -print0)

myscriptornonscript "${files[@]}"
' -- {1..1302582} | xargs -x0n 2076858 -s 2076858 /bin/true xargs: argument list too long $ printf '%s
$ time { printf '%s%pre%' -- {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%pre%' -- $(echo {1..1302581}))
  /bin/true "${args[@]}"
}
bash: /bin/true: Argument list too long

real    107m51.876s
user    107m38.532s
sys     0m7.940s
' -- {1..1302581} | xargs -x0n 2076858 -s 2076858 /bin/true (no output)
' -- {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%pre%' -- $(echo {1..1302581})) /bin/true "${args[@]}" } bash: /bin/true: Argument list too long real 107m51.876s user 107m38.532s sys 0m7.940s
' -- {1..1302581} | xargs -x0n 2076858 -s 2076858 /bin/true (no output)

bash

Se você não quiser envolver um comando externo, o loop while-reading alimentando uma matriz, como mostrado no link , é a única maneira de o bash dividir as coisas no byte nulo.

A ideia de criar o script ( . ... "$@" ) para evitar o limite de tamanho de 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 diferente do stdin, mas caso contrário você não precisará dele.

Assim, o caminho "nativo" mais simples, para as necessidades domésticas diárias:

%pre%

Se você gosta da sua árvore de processos limpa e agradável de se ver, esse método permite fazer exec mynonscript "${files[@]}" , o que remove o processo bash da memória, substituindo-o pelo comando chamado. xargs permanecerá sempre 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 é este:

%pre%

o bash não é otimizado para manipulação de matrizes.

homem xargs :

-n max-args

Use at most max-args arguments per command line. Fewer than max-args arguments will be used if the size (see the -s option) is exceeded, unless the -x option is given, in which case xargs will exit.

-s max-chars

Use at most max-chars characters per command line, including the command and initial-arguments and the terminating nulls at the ends of the argument strings. The largest allowed value is system-dependent, and is calculated as the argument length limit for exec, less the size of your environment, less 2048 bytes of headroom. If this value is more than 128KiB, 128Kib is used as the default value; otherwise, the default value is the maximum. 1KiB is 1024 bytes.

-x

Exit if the size (see the -s option) is exceeded.

    
por 21.05.2015 / 20:58