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 o read
embutido, usarwhile IFS= read -r line; do …
Plain read
trata 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 …
.
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á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 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ável myfiles
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 script myfiles="$someprefix*.txt"; … process $myfiles
. Se someprefix
estiver 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 set
e 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 f
em 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 eval
builtin.
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 code
seja a sequência /path/to/executable --option --message="hello world" -- /path/to/file1
. O eval
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 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
, read
permite linhas de continuação - esta é uma única linha lógica de entrada:
hello \
world
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
defina first
a primeira palavra de entrada, second
a segunda e third
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
ou 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 -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, 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_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, chame sh
explicitamente.
find … -exec sh -c '
for x do
… # process the file "$x"
done
' find-sh {} +
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 .
shellcheck
ajudá-lo a melhorar a qualidade dos seus programas.