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 world
e foi tratado como dois arquivos separadoshello
eworld
. - 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?
bash
shell
shell-script
quoting
whitespace
Gilles
fonte
fonte
shellcheck
ajudá-lo a melhorar a qualidade dos seus programas.Respostas:
Sempre use aspas em torno substituições de variáveis e substituições de comando:
"$foo"
,"$(foo)"
Se você usar sem
$foo
aspas, 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 oread
embutido, usarwhile IFS= read -r line; do …
Plain
read
trata invertidas e espaços em branco especialmente.xargs
- Evitexargs
. Se você deve usarxargs
, faça issoxargs -0
. Em vez defind … | xargs
, prefirafind … -exec …
.xargs
trata 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?$foo
não significa "pegue o valor da variávelfoo
". Significa algo muito mais complexo:foo * bar
, em seguida, o resultado deste passo é a lista de 3-elementofoo
,*
,bar
.foo
, seguida da lista de arquivos no diretório atual e, finalmentebar
. 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
IFS
que 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ávelmyfiles
contém a sequência de 5 caracteres*.txt
e é quando você escreve$myfiles
que o curinga é expandido. Este exemplo realmente funcionará, até que você altere seu scriptmyfiles="$someprefix*.txt"; … process $myfiles
. Sesomeprefix
estiver definido comofinal 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.
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ê defineset
e que é local para uma função: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ávelf
em uma maneira "segura" de se referir ao mesmo arquivo que é garantido para não começar-
.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.
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.
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.
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
eval
builtin.Observe as aspas aninhadas na definição de
code
: as aspas simples'…'
delimitam um literal de sequência, para que o valor da variávelcode
seja a sequência/path/to/executable --option --message="hello world" -- /path/to/file1
. Oeval
builtin 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 comocode="$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
'\''
.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.
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
,read
permite linhas de continuação - esta é uma única linha lógica de entrada:read
divide 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 third
definafirst
a primeira palavra de entrada,second
a segunda ethird
a 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
IFS
para 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 -L1
ouxargs -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 -0
onde 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, usefind … -print0
(ou você pode usarfind … -exec …
como explicado abaixo).Como eu processo os arquivos encontrados por
find
?some_command
precisa 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, chamesh
explicitamente.Eu tenho outra pergunta
Navegue pela tag de cotação neste site, ou shell ou shell-script . (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 .
fonte
$(( ... ))
(também$[...]
em algumas conchas), exceto emzsh
(mesmo em emulação de sh) emksh
.xargs -0
não é POSIX. Exceto no FreeBSDxargs
, você geralmente deseja, emxargs -r0
vez dexargs -0
.ls --quoting-style=shell-always
não é compatível comxargs
. Tentetouch $'a\nb'; ls --quoting-style=shell-always | xargs
xargs -d "\n"
que você pode executar, por exemplo,locate PATTERN1 |xargs -d "\n" grep PATTERN2
procurar 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
Embora a resposta de Gilles seja excelente, eu discuto seu ponto principal
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
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.
fonte
foo=$bar
é OK, masexport foo=$bar
ouenv foo=$var
nã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 .criteria="-type f"
,find . $criteria
funciona, masfind . "$criteria"
não.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 é:aceitar entrada
interpretar e dividi- lo corretamente em palavras de entrada tokenizadas
As palavras de entrada são os itens da sintaxe do shell, como
$word
ouecho $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
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 .e depois executar o comando resultante
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 é
exec
o caso. A maioria dos shells não lidaNUL
bem com o byte - se é que existe - e isso ocorre porque eles já estão divididos nele. O shell temexec
muito e deve fazer isso com umaNUL
matriz delimitada de argumentos que ele entrega ao kernel do sistema noexec
momento. 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
$IFS
entra.$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.$IFS
divide expansões de shell em delimitadores que não sejamNUL
- ou, em outras palavras, o shell substitui bytes resultantes de uma expansão que corresponde àqueles no valor de$IFS
comNUL
em 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$IFS
matriz de dados delimitada.É importante entender que
$IFS
apenas 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,$IFS
nã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 quandoIFS=
definida como um valor vazio.A menos que citado,
$IFS
ele próprio é uma$IFS
expansã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.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.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 ....
... é 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:... 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:
... os resultados de ambos os argumentos são idênticos -
*
nesse caso, não se expandem.fonte
IFS
realmente funciona. O que eu não entendo é por que ele iria sempre ser uma boa idéia para definirIFS
para algo diferente de padrão.$IFS
.cd /usr/bin; set -f; IFS=/; for path_component in $PWD; do echo $path_component; done
cópias\n
, em seguida,usr\n
em seguidabin\n
. O primeiroecho
está 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 issoawk
o tempo todo, de qualquer maneira. seu shell tambémEu 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 -0
funcione 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: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:
e correspondentemente:
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.)fonte
zsh
, você pode usarIFS=$'\0'
e usar-print0
(zsh
não faz globs nas expansões para que os caracteres glob não sejam um problema lá).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.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!fonte