Por que meu script de shell engasga com espaços em branco ou outros caracteres especiais?

284

Ou, um guia introdutório para manipulação robusta de nome de arquivo e outras strings que passam em scripts de shell.

Eu escrevi um script de shell que funciona bem na maioria das vezes. Mas ele engasga com algumas entradas (por exemplo, em alguns nomes de arquivos).

Eu encontrei um problema como o seguinte:

  • Eu tenho um nome de arquivo que contém um espaço hello worlde foi tratado como dois arquivos separados helloe world.
  • Eu tenho uma linha de entrada com dois espaços consecutivos e eles encolheram para um na entrada.
  • Os espaços em branco à esquerda e à direita desaparecem das linhas de entrada.
  • Às vezes, quando a entrada contém um dos caracteres \[*?, eles são substituídos por algum texto que é, na verdade, o nome dos arquivos.
  • Há um apóstrofo '(ou aspas duplas ") na entrada e as coisas ficaram estranhas após esse ponto.
  • Há uma barra invertida na entrada (ou: estou usando o Cygwin e alguns dos meus nomes de arquivos têm \separadores no estilo do Windows ).

O que está acontecendo e como corrigi-lo?

Gilles
fonte
16
shellcheckajudá-lo a melhorar a qualidade dos seus programas.
aurelien
3
Além das técnicas de proteção descritas nas respostas e, embora provavelmente seja óbvio para a maioria dos leitores, acho que vale a pena comentar que quando os arquivos devem ser processados ​​usando ferramentas de linha de comando, é uma boa prática evitar caracteres sofisticados no nomes em primeiro lugar, se possível.
bli
2
Agora também existem ferramentas para reescrever scripts de shell com citações apropriadas .
precisa saber é o seguinte
1
@bli Não, isso faz com que apenas os bugs demorem mais para aparecer. Está escondendo insetos hoje. E agora, você não conhece todos os nomes de arquivos usados ​​posteriormente com seu código.
Volker Siegel
Primeiro, se seus parâmetros contiverem espaços, eles deverão ser citados na entrada (na linha de comando). No entanto, você pode pegar a linha de comando inteira e analisá-la. Dois espaços não se transformam em um espaço; qualquer quantidade de espaço informa ao seu script que é a próxima variável; portanto, se você fizer algo como "echo $ 1 $ 2", é o seu script colocando um espaço no meio. Também use "find (-exec)" para iterar sobre arquivos com espaços em vez de um loop for; você pode lidar com os espaços mais facilmente.
Patrick Taylor

Respostas:

352

Sempre use aspas em torno substituições de variáveis e substituições de comando: "$foo","$(foo)"

Se você usar sem $fooaspas, seu script bloqueará a entrada ou parâmetros (ou saída de comando, com $(foo)) contendo espaços em branco ou \[*?.

Lá, você pode parar de ler. Bem, ok, aqui estão mais alguns:

  • read- Para ler linha de entrada por linha com o readembutido, usarwhile IFS= read -r line; do …
    Plain readtrata invertidas e espaços em branco especialmente.
  • xargs- Evitexargs . Se você deve usar xargs, faça isso xargs -0. Em vez de find … | xargs, prefirafind … -exec … .
    xargstrata espaço em branco e os caracteres \"'especialmente.

Essa resposta se aplica a cascas de Bourne / estilo POSIX ( sh, ash, dash, bash, ksh, mksh, yash...). Os usuários do Zsh devem ignorá-lo e ler o final de Quando é necessário citar duas vezes? em vez de. Se você quiser todo o âmago da questão, leia o padrão ou o manual do seu shell.


Observe que as explicações abaixo contêm algumas aproximações (declarações verdadeiras na maioria das condições, mas podem ser afetadas pelo contexto circundante ou pela configuração).

Por que preciso escrever "$foo"? O que acontece sem as aspas?

$foonão significa "pegue o valor da variável foo". Significa algo muito mais complexo:

  • Primeiro, pegue o valor da variável.
  • Divisão de campo: trate esse valor como uma lista de campos separados por espaços em branco e crie a lista resultante. Por exemplo, se a variável contém foo * bar ​, em seguida, o resultado deste passo é a lista de 3-elemento foo, *, bar.
  • Geração de nome de arquivo: trate cada campo como um glob, ou seja, como um padrão curinga e substitua-o pela lista de nomes de arquivos que correspondem a esse padrão. Se o padrão não corresponder a nenhum arquivo, ele não será modificado. No nosso exemplo, isso resulta em uma lista contendo foo, seguida da lista de arquivos no diretório atual e, finalmente bar. Se o diretório atual está vazio, o resultado é foo, *, bar.

Observe que o resultado é uma lista de seqüências de caracteres. Existem dois contextos na sintaxe do shell: contexto de lista e contexto de cadeia. A divisão de campos e a geração de nome de arquivo ocorrem apenas no contexto da lista, mas na maioria das vezes. As aspas duplas delimitam um contexto de string: a string com aspas duplas é uma string única, que não deve ser dividida. (Exceção: "$@"expandir para a lista de parâmetros posicionais, por exemplo, "$@"é equivalente a "$1" "$2" "$3"se houver três parâmetros posicionais. Consulte Qual é a diferença entre $ * e $ @? )

O mesmo acontece com a substituição de comando com $(foo)ou com `foo`. Em uma nota lateral, não use `foo`: suas regras de cotação são estranhas e não portáveis, e todos os shells modernos suportam o $(foo)que é absolutamente equivalente, exceto por ter regras intuitivas de cotação.

A saída da substituição aritmética também sofre as mesmas expansões, mas isso normalmente não é uma preocupação, pois contém apenas caracteres não expansíveis (supondo IFSque não contenha dígitos ou -).

Consulte Quando é necessária a citação dupla? para obter mais detalhes sobre os casos em que você pode deixar de fora as aspas.

A menos que você queira que todo esse rigmarole aconteça, lembre-se de sempre usar aspas duplas em torno das substituições de variáveis ​​e comandos. Cuidado: deixar de fora as aspas pode levar não apenas a erros, mas a falhas de segurança .

Como processar uma lista de nomes de arquivos?

Se você escrever myfiles="file1 file2", com espaços para separar os arquivos, isso não funcionará com nomes de arquivos que contenham espaços. Os nomes de arquivo Unix podem conter qualquer caractere que não seja /(que é sempre um separador de diretório) e bytes nulos (que você não pode usar em shell scripts com a maioria dos shells).

Mesmo problema com myfiles=*.txt; … process $myfiles. Quando você faz isso, a variável myfilescontém a sequência de 5 caracteres *.txte é quando você escreve $myfilesque o curinga é expandido. Este exemplo realmente funcionará, até que você altere seu script myfiles="$someprefix*.txt"; … process $myfiles. Se someprefixestiver definido como final report, isso não funcionará.

Para processar uma lista de qualquer tipo (como nomes de arquivo), coloque-a em uma matriz. Isso requer mksh, ksh93, yash ou bash (ou zsh, que não possui todos esses problemas de citação); um shell POSIX comum (como ash ou dash) não possui variáveis ​​de matriz.

myfiles=("$someprefix"*.txt)
process "${myfiles[@]}"

O Ksh88 possui variáveis ​​de array com uma sintaxe de atribuição diferente set -A myfiles "someprefix"*.txt(consulte a variável de atribuição em um ambiente ksh diferente, se você precisar da portabilidade do ksh88 / bash). Os shells no estilo Bourne / POSIX têm um único array, o array de parâmetros posicionais com os "$@"quais você define sete que é local para uma função:

set -- "$someprefix"*.txt
process -- "$@"

E os nomes de arquivos que começam com -?

Em uma nota relacionada, lembre-se de que os nomes de arquivos podem começar com um -(traço / menos), que a maioria dos comandos interpreta como denotando uma opção. Se você tiver um nome de arquivo que comece com uma parte variável, passe --antes dele, como no snippet acima. Isso indica ao comando que chegou ao final das opções; portanto, qualquer coisa depois disso é um nome de arquivo, mesmo que comece com -.

Como alternativa, você pode garantir que os nomes dos arquivos comecem com um caractere diferente de -. Os nomes absolutos dos arquivos começam com /e você pode adicionar ./no início dos nomes relativos. O seguinte snippet transforma o conteúdo da variável fem uma maneira "segura" de se referir ao mesmo arquivo que é garantido para não começar -.

case "$f" in -*) "f=./$f";; esac

Em uma observação final sobre este tópico, observe que alguns comandos interpretam -como significando entrada ou saída padrão, mesmo depois --. Se você precisar se referir a um arquivo real chamado -, ou se estiver chamando esse programa e não quiser que ele leia de stdin ou escreva para stdout, reescreva -como acima. Consulte Qual é a diferença entre "du -sh *" e "du -sh ./*"? para uma discussão mais aprofundada.

Como guardo um comando em uma variável?

“Comando” pode significar três coisas: um nome de comando (o nome como um executável, com ou sem o caminho completo, ou o nome de uma função, embutida ou alias), um nome de comando com argumentos ou um código de shell. Existem diferentes maneiras de armazená-las em uma variável.

Se você tiver um nome de comando, armazene-o e use a variável entre aspas duplas, como de costume.

command_path="$1"

"$command_path" --option --message="hello world"

Se você possui um comando com argumentos, o problema é o mesmo que com uma lista de nomes de arquivos acima: esta é uma lista de cadeias, não uma cadeia. Você não pode simplesmente agrupar os argumentos em uma única sequência com espaços no meio, porque se fizer isso, não poderá dizer a diferença entre espaços que fazem parte de argumentos e espaços que separam argumentos. Se seu shell tiver matrizes, você poderá usá-las.

cmd=(/path/to/executable --option --message="hello world" --)
cmd=("${cmd[@]}" "$file1" "$file2")
"${cmd[@]}"

E se você estiver usando um shell sem matrizes? Você ainda pode usar os parâmetros posicionais, se não se importar em modificá-los.

set -- /path/to/executable --option --message="hello world" --
set -- "$@" "$file1" "$file2"
"$@"

E se você precisar armazenar um comando shell complexo, por exemplo, com redirecionamentos, pipes, etc.? Ou se você não deseja modificar os parâmetros posicionais? Em seguida, você pode criar uma string contendo o comando e usar o evalbuiltin.

code='/path/to/executable --option --message="hello world" -- /path/to/file1 | grep "interesting stuff"'
eval "$code"

Observe as aspas aninhadas na definição de code: as aspas simples '…'delimitam um literal de sequência, para que o valor da variável codeseja a sequência /path/to/executable --option --message="hello world" -- /path/to/file1. O evalbuiltin diz ao shell para analisar a cadeia passada como um argumento como se ela aparecesse no script; portanto, nesse ponto, as aspas e o pipe são analisados, etc.

Usar evalé complicado. Pense cuidadosamente sobre o que é analisado quando. Em particular, você não pode simplesmente inserir um nome de arquivo no código: é necessário citá-lo, como faria se estivesse em um arquivo de código-fonte. Não há maneira direta de fazer isso. Algo como code="$code $filename"quebras se o nome do arquivo contém qualquer caractere especial shell (espaços, $, ;, |, <, >, etc.). code="$code \"$filename\""ainda quebra "$\`. Até code="$code '$filename'"quebra se o nome do arquivo contiver a '. Existem duas soluções.

  • Adicione uma camada de aspas ao redor do nome do arquivo. A maneira mais fácil de fazer isso é adicionar aspas simples e substituir aspas simples por '\''.

    quoted_filename=$(printf %s. "$filename" | sed "s/'/'\\\\''/g")
    code="$code '${quoted_filename%.}'"
    
  • Mantenha a expansão da variável dentro do código, para que ela seja consultada quando o código for avaliado, não quando o fragmento do código for criado. Isso é mais simples, mas só funciona se a variável ainda estiver com o mesmo valor no momento em que o código for executado, não por exemplo, se o código for construído em um loop.

    code="$code \"\$filename\""

Finalmente, você realmente precisa de uma variável que contenha código? A maneira mais natural de dar um nome a um bloco de código é definir uma função.

O que há com isso read?

Sem -r, readpermite linhas de continuação - esta é uma única linha lógica de entrada:

hello \
world

readdivide a linha de entrada em campos delimitados por caracteres em $IFS(sem -r, a barra invertida também os escapa). Por exemplo, se a entrada for uma linha contendo três palavras, read first second thirddefina firsta primeira palavra de entrada, seconda segunda e thirda terceira palavra. Se houver mais palavras, a última variável conterá tudo o que resta depois de definir as anteriores. Os espaços em branco à esquerda e à direita são aparados.

A configuração IFSpara a sequência vazia evita qualquer corte. Veja Por que `while IFS = read` é usado com tanta frequência, em vez de` IFS =; enquanto lê ...? para uma explicação mais longa.

O que há de errado xargs?

O formato de entrada xargsé de cadeias separadas por espaços em branco que podem opcionalmente ser citadas simples ou duplas. Nenhuma ferramenta padrão gera esse formato.

A entrada para xargs -L1ou xargs -lé quase uma lista de linhas, mas não exatamente - se houver um espaço no final de uma linha, a seguinte linha é uma linha de continuação.

Você pode usar xargs -0onde aplicável (e onde disponível: GNU (Linux, Cygwin), BusyBox, BSD, OSX, mas não está no POSIX). Isso é seguro, porque bytes nulos não podem aparecer na maioria dos dados, principalmente nos nomes de arquivos. Para produzir uma lista separada por nulos de nomes de arquivos, use find … -print0(ou você pode usar find … -exec …como explicado abaixo).

Como eu processo os arquivos encontrados por find?

find  -exec some_command a_parameter another_parameter {} +

some_commandprecisa ser um comando externo, não pode ser uma função ou alias do shell. Se você precisar chamar um shell para processar os arquivos, chame shexplicitamente.

find  -exec sh -c '
  for x do
    … # process the file "$x"
  done
' find-sh {} +

Eu tenho outra pergunta

Navegue pela tag de neste site, ou ou . (Clique em "saiba mais ..." para ver algumas dicas gerais e uma lista selecionada manualmente de perguntas comuns.) Se você pesquisou e não conseguiu encontrar uma resposta, pergunte .

Gilles
fonte
6
@ John1024 É apenas um recurso GNU, então eu continuarei com “nenhuma ferramenta padrão”.
Gilles
2
Você também precisa de aspas $(( ... ))(também $[...]em algumas conchas), exceto em zsh(mesmo em emulação de sh) e mksh.
Stéphane Chazelas
3
Observe que xargs -0não é POSIX. Exceto no FreeBSD xargs, você geralmente deseja, em xargs -r0vez de xargs -0.
Stéphane Chazelas
2
@ John1024, não, ls --quoting-style=shell-alwaysnão é compatível com xargs. Tentetouch $'a\nb'; ls --quoting-style=shell-always | xargs
Stéphane Chazelas
3
Outro recurso interessante (somente GNU) é xargs -d "\n"que você pode executar, por exemplo, locate PATTERN1 |xargs -d "\n" grep PATTERN2procurar nomes de arquivos correspondentes a PATTERN1 com conteúdo correspondente a PATTERN2 . Sem o GNU, você pode fazê-lo, por exemplo, comolocate PATTERN1 |perl -pne 's/\n/\0/' |xargs -0 grep PATTERN1
Adam Katz
26

Embora a resposta de Gilles seja excelente, eu discuto seu ponto principal

Sempre use aspas duplas em torno das substituições de variáveis ​​e substituições de comandos: "$ foo", "$ (foo)"

Quando você está começando com um shell do tipo Bash que divide palavras, sim, é claro que o conselho seguro é sempre usar aspas. No entanto, a divisão de palavras nem sempre é executada

§ Divisão de palavras

Esses comandos podem ser executados sem erros

foo=$bar
bar=$(a command)
logfile=$logdir/foo-$(date +%Y%m%d)
PATH=/usr/local/bin:$PATH ./myscript
case $foo in bar) echo bar ;; baz) echo baz ;; esac

Não estou incentivando os usuários a adotar esse comportamento, mas se alguém entender com firmeza quando ocorrer a divisão de palavras, poderá decidir por si próprio quando usar aspas.

Steven Penny
fonte
19
Como mencionei na minha resposta, consulte unix.stackexchange.com/questions/68694/… para obter detalhes. Observe a pergunta - “Por que meu script shell engasga?”. O problema mais comum (de anos de experiência neste site e em outros lugares) está faltando aspas duplas. “Sempre use aspas duplas” é mais fácil de lembrar do que “sempre use aspas duplas, exceto nos casos em que não são necessários”.
Gilles
14
Regras são difíceis de entender para iniciantes. Por exemplo, foo=$baré OK, mas export foo=$barou env foo=$varnão são (pelo menos em algumas conchas). Um conselho para iniciantes: sempre cite suas variáveis, a menos que você saiba o que está fazendo e tenha um bom motivo para não fazê-lo .
Stéphane Chazelas
5
@StevenPenny É realmente mais correto? Existem casos razoáveis ​​em que aspas quebrariam o script? Em situações em que em casos meia citações deve ser usado, e em outras citações meio pode ser usado opcionalmente - então uma recomendação "sempre usar aspas, just in case" é a que deve ser pensado, uma vez que é verdade, simples e menos arriscada. Ensinar essas listas de exceções para iniciantes é bem conhecido por ser ineficaz (sem contexto, eles não se lembram delas) e contraproducente, pois confundem citações necessárias / desnecessárias, quebrando seus scripts e desmotivando-as para aprender mais.
Peteris 25/05
6
Meu $ 0,02 seria que recomendar citar tudo é um bom conselho. Citar erroneamente algo que não precisa é inofensivo, falhar erroneamente em citar algo que precisa é prejudicial. Portanto, para a maioria dos autores de scripts de shell que nunca entenderão os meandros de quando ocorre exatamente a divisão de palavras, citar tudo é muito mais seguro do que tentar citar apenas quando necessário.
25414 godlygeek
5
@Peteris e godlygeek: "Existem casos razoáveis ​​em que citações quebrariam o script?" Depende da sua definição de "razoável". Se um script é definido criteria="-type f", find . $criteriafunciona, mas find . "$criteria"não.
G-Man
22

Até onde eu sei, existem apenas dois casos em que é necessário cotar duas expansões, e esses casos envolvem os dois parâmetros especiais do shell "$@"e "$*"- que são especificados para expandir diferentemente quando colocados entre aspas duplas. Em todos os outros casos (excluindo, talvez, implementações de matrizes específicas do shell), o comportamento de uma expansão é algo configurável - existem opções para isso.

Isso não quer dizer, é claro, que as aspas duplas devam ser evitadas - pelo contrário, é provavelmente o método mais conveniente e robusto de delimitar uma expansão que o shell tem a oferecer. Mas acho que, como as alternativas já foram habilmente expostas, esse é um excelente lugar para discutir o que acontece quando o shell expande um valor.

A concha, em seu coração e alma (para aqueles que a possuem) , é um interpretador de comandos - é um analisador, como um grande e interativo sed. Se sua instrução shell estiver bloqueada em espaço em branco ou semelhante, é muito provável que você não tenha entendido completamente o processo de interpretação do shell - especialmente como e por que ele traduz uma instrução de entrada em um comando acionável. O trabalho do shell é:

  1. aceitar entrada

  2. interpretar e dividi- lo corretamente em palavras de entrada tokenizadas

    • As palavras de entrada são os itens da sintaxe do shell, como $wordouecho $words 3 4* 5

    • as palavras são sempre divididas no espaço em branco - isso é apenas sintaxe - mas apenas os caracteres literais do espaço em branco servidos ao shell em seu arquivo de entrada

  3. expanda-os se necessário em vários campos

    • campos resultam de expansões de palavras - eles compõem o comando executável final

    • com exceção "$@", $IFS campo-splitting , e expansão de nome de uma entrada palavra sempre deve avaliar a um único campo .

  4. e depois executar o comando resultante

    • na maioria dos casos, isso envolve transmitir os resultados de sua interpretação de uma forma ou de outra

As pessoas costumam dizer que a casca é uma cola e, se isso for verdade, o que está aderindo são as listas de argumentos - ou campos - para um processo ou outro quando é execo caso. A maioria dos shells não lida NULbem com o byte - se é que existe - e isso ocorre porque eles já estão divididos nele. O shell tem exec muito e deve fazer isso com uma NULmatriz delimitada de argumentos que ele entrega ao kernel do sistema no execmomento. Se você misturasse o delimitador do shell com seus dados delimitados, o shell provavelmente estragaria tudo. Suas estruturas de dados internas - como a maioria dos programas - contam com esse delimitador. zsh, notavelmente, não estraga tudo.

E é aí que $IFSentra. $IFSÉ um parâmetro de shell sempre presente - e igualmente configurável - que define como o shell deve dividir as expansões de shell de palavra em campo - especificamente sobre quais valores esses campos devem delimitar. $IFSdivide expansões de shell em delimitadores que não sejam NUL- ou, em outras palavras, o shell substitui bytes resultantes de uma expansão que corresponde àqueles no valor de $IFScom NULem suas matrizes de dados internas. Quando você olha dessa maneira, pode começar a ver que toda expansão de shell dividida em campo é uma $IFSmatriz de dados delimitada.

É importante entender que $IFSapenas delimita expansões que ainda não foram delimitadas - o que você pode fazer com "aspas duplas. Ao citar uma expansão, você a delimita na cabeça e pelo menos na cauda de seu valor. Nesses casos, $IFSnão se aplica, pois não há campos a serem separados. De fato, uma expansão com aspas duplas exibe um comportamento de divisão de campo idêntico a uma expansão sem aspas quando IFS=definida como um valor vazio.

A menos que citado, $IFSele próprio é uma $IFSexpansão de shell delimitada. O padrão é um valor especificado de <space><tab><newline>- todos os três exibem propriedades especiais quando contidos $IFS. Enquanto qualquer outro valor para $IFSé especificado para avaliar um único campo por ocorrência de expansão , o $IFS espaço em branco - qualquer um desses três - é especificado para eleger um único campo por sequência de expansão e as sequências à esquerda / à direita são totalmente eliminadas. Provavelmente é mais fácil de entender por exemplo.

slashes=///// spaces='     '
IFS=/; printf '<%s>' $slashes$spaces
<><><><><><     >
IFS=' '; printf '<%s>' $slashes$spaces
</////>
IFS=; printf '<%s>' $slashes$spaces
</////     >
unset IFS; printf '<%s>' "$slashes$spaces"
</////     >

Mas isso é apenas $IFS- apenas a divisão de palavras ou o espaço em branco, conforme solicitado, e os caracteres especiais ?

O shell - por padrão - também expandirá certos tokens não citados ( ?*[como observado em outro lugar aqui) em vários campos quando eles ocorrem em uma lista. Isso é chamado de expansão do nome do caminho ou globbing . É uma ferramenta incrivelmente útil e, como ocorre após a divisão do campo na ordem de análise do shell, não é afetada pelo $ IFS - os campos gerados por uma expansão do nome do caminho são delimitados na cabeça / cauda dos próprios nomes de arquivos, independentemente de se seu conteúdo contém caracteres atualmente $IFS. Esse comportamento é ativado por padrão - mas é configurado com muita facilidade.

set -f

Que instrui o shell não para glob . A expansão do nome do caminho não ocorrerá pelo menos até que essa configuração seja desfeita de alguma forma - como se o shell atual fosse substituído por outro novo processo ou ....

set +f

... é emitido para o shell. As aspas duplas - como também fazem para $IFS a divisão de campos - tornam essa configuração global desnecessária por expansão. Assim:

echo "*" *

... se a expansão do nome do caminho estiver ativada no momento, provavelmente produzirá resultados muito diferentes por argumento - já que o primeiro se expandirá apenas para seu valor literal (o único caractere de asterisco, ou seja, de modo algum) e o segundo apenas para o mesmo se o diretório de trabalho atual não contiver nomes de arquivos que possam corresponder (e corresponde a quase todos) . No entanto, se você fizer:

set -f; echo "*" *

... os resultados de ambos os argumentos são idênticos - *nesse caso, não se expandem.

mikeserv
fonte
Na verdade, eu concordo com o @ StéphaneChazelas de que (na maior parte) confunde as coisas mais do que ajudar ... mas eu achei isso útil, pessoalmente, então eu votei. Agora tenho uma ideia melhor (e alguns exemplos) de como IFSrealmente funciona. O que eu não entendo é por que ele iria sempre ser uma boa idéia para definir IFSpara algo diferente de padrão.
Curinga
1
@Wildcard - é um delimitador de campo. se você tiver um valor em uma variável que deseja expandir para vários campos, você o dividirá $IFS. cd /usr/bin; set -f; IFS=/; for path_component in $PWD; do echo $path_component; donecópias \n, em seguida, usr\nem seguida bin\n. O primeiro echoestá vazio porque /é um campo nulo. Os path_components podem ter novas linhas ou espaços ou o que quer que seja - não importaria porque os componentes foram divididos /e não o valor padrão. as pessoas fazem isso awko tempo todo, de qualquer maneira. seu shell também
mikeserv
3

Eu tinha um grande projeto de vídeo com espaços nos nomes de arquivos e espaços nos nomes de diretório. Embora find -type f -print0 | xargs -0funcione para vários propósitos e em diferentes shells, acho que o uso de um IFS personalizado (separador de campos de entrada) oferece mais flexibilidade se você estiver usando o bash. O snippet abaixo usa bash e define o IFS como apenas uma nova linha; desde que não haja novas linhas nos seus nomes de arquivos:

(IFS=$'\n'; for i in $(find -type f -print) ; do
    echo ">>>$i<<<"
done)

Observe o uso de parênteses para isolar a redefinição do IFS. Eu li outros posts sobre como recuperar o IFS, mas isso é apenas mais fácil.

Além disso, configurar o IFS como nova linha permite definir variáveis ​​de shell com antecedência e imprimi-las facilmente. Por exemplo, eu posso aumentar uma variável V incrementalmente usando novas linhas como separadores:

V=""
V="./Ralphie's Camcorder/STREAM/00123.MTS,04:58,05:52,-vf yadif"
V="$V"$'\n'"./Ralphie's Camcorder/STREAM/00111.MTS,00:00,59:59,-vf yadif"
V="$V"$'\n'"next item goes here..."

e correspondentemente:

(IFS=$'\n'; for v in $V ; do
    echo ">>>$v<<<"
done)

Agora eu posso "listar" a configuração de V echo "$V"usando aspas duplas para gerar as novas linhas. (Agradecemos a esta discussão pela $'\n'explicação.)

Russ
fonte
3
Mas você ainda terá problemas com nomes de arquivos que contêm caracteres de nova linha ou glob. Veja também: Por que o loop sobre a prática inadequada de saída do find? . Se estiver usando zsh, você pode usar IFS=$'\0'e usar -print0( zshnão faz globs nas expansões para que os caracteres glob não sejam um problema lá).
Stéphane Chazelas
1
Isso funciona com nomes de arquivos que contêm espaços, mas não funciona com nomes de arquivos potencialmente hostis ou nomes de arquivos "sem sentido" acidentais. Você pode corrigir facilmente o problema de nomes de arquivos contendo caracteres curinga adicionando set -f. Por outro lado, sua abordagem falha fundamentalmente com nomes de arquivos contendo novas linhas. Ao lidar com dados que não sejam nomes de arquivos, ele também falha com itens vazios.
Gilles
Certo, minha ressalva é que não funcionará com novas linhas nos nomes de arquivos. No entanto, eu acredito que nós temos que traçar a linha apenas tímido de loucura ;-)
Russ
E não sei por que isso recebeu um voto negativo. Este é um método perfeitamente razoável para iterar nomes de arquivos com espaços. Usar -print0 requer xargs, e há coisas difíceis de usar nessa cadeia. Lamento que alguém não concorde com a minha resposta, mas isso não é motivo para rebaixá-la.
Russ
0

Considerando todas as implicações de segurança mencionadas acima e assumindo que você confia e tem controle sobre as variáveis ​​que está expandindo, é possível ter vários caminhos com espaços em branco usando eval. Mas tenha cuidado!

$ FILES='"a b" c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
$ FILES='a\ b c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
Mattias Wadman
fonte