E agora, quem poderá nos proteger?
Essa, eu ainda não descobri de onde vem, mas é com uma frequência absurda que eu esbarro com alguém afirmando que, no shell, as chaves nas expansões de parâmetros servem para "proteger as variáveis". Independente da origem da pérola, neste artigo, nós veremos um pouco do mecanismo das expansões e do que realmente se trata o uso das chaves ao redor de nomes de variáveis.
O shell pode receber comandos de três formas:
- Das linhas de comandos digitados pelo usuário no terminal.
- De um arquivo de script.
- Ou da string de uma lista de comandos passada como argumento da invocação de seu executável (
bash -c 'LISTA DE COMANDOS'
).
A partir daí, o shell incia um procedimento de análise e interpretação antes de, de fato, proceder a execução dos comandos. Esse mecanismo pode ser resumido em cinco etapas gerais:
- Decomposição da linha do comando;
- Classificação de comandos simples, complexos e compostos;
- Expansões diversas;
- Redirecionamentos;
- Captura do estado de saída (após a execução).
Desta sequência, o que nos interessa, para efeito de argumentação neste artigo, são a primeira e a terceira etapas.
A primeira coisa que o shell faz com o que digitamos é percorrer os caracteres da linha para localizar e identificar, de acordo com as regras de citação (quoting), a ocorrência de palavras e operadores.
Os operadores são sequências de um ou mais caracteres especiais (também chamados de "metacaracteres") com diversas finalidades, desde a simples separação de palavras à definição de como os encadeamentos de comandos e redirecionamento deverão ser processados (operadores de controle, pipes e operadores de redirecionamento). Uma palavra, por sua vez, é qualquer sequência contínua de caracteres que for encontrada na linha do comando delimitada por operadores.
No processo de decomposição da linha do comando, também entram em cena dois conceitos fundamentais para o entendimento de como o shell processa os comandos: as regras de citação e os tokens
.
Além dos metacaracteres que formam operadores, existem outros caracteres que, quando surgem nas palavras, alteram seus significados para o shell. Por exemplo:
:~$ echo Eu tenho um objeto
Eu tenho um objeto
Na linha acima, o shell encontra cinco palavras: echo
, Eu
, tenho
, um
e objeto
, delimitadas entre si pelo operador espaço, cuja função é apenas a de separar palavras.
Mas, observe o que acontece com o comando abaixo:
:~$ echo Eu tenho um $objeto
Eu tenho um
O caractere $
, parte da palavra $objeto
tem um significado especial para o shell, que é informar que a palavra de que ele faz parte, em dado momento, deverá ser expandida. Especificamente no contexto do exemplo, pela simples presença do caractere $
, o shell saberá que objeto
é o nome de uma variável: como nenhuma variável de nome objeto
foi definida, nada é expandido na linha do comando, mas nós acabamos de descobrir o que é um token.
No shell, os tokens são seus componentes léxicos: símbolos, palavras e estruturas que, para ele, possuem um significado e devem ser processados de algum modo especial. Sendo assim, nós também podemos entender que os tokens são os componentes do shell que definem a linguagem pela qual nós podemos comunicar os nossos comandos.
Alguns exemplos de tokens do shell:
- Operadores
- Palavras reservadas
- Caracteres de citação
- Cifrão (
$
), colchetes ([...]
) e chaves ({...}
) - O til (
~
) - Metacaracteres formadores de padrões
Além disso, alguns desses símbolos e estruturas podem tornar especiais os significados de palavras, que foi o que vimos acontecer no exemplo anterior com a palavra objeto
: por ser prefixada pelo $
, a palavra ganhou o significado de "nome de variável" e indicou para o shell que ali deveria ser procedida uma expansão.
Com exceção das palavras reservadas (porque são palavras) e de algumas expansões, todos os tokens que não formam estruturas predefinidas separam palavras: e isso é um fato muito importante para a argumentação neste artigo.
Na direção oposta do que vimos acontecer com o $
, as citações (quoting, em inglês) removem o significado especial de elementos na linha de comando. Quem cumpre esse papel são:
Citação | Descrição |
---|---|
\ |
Remove o significado especial do caractere seguinte. |
'...' |
Remove o significado especial de tudo que estiver entre elas. |
"..." |
Remove o significado especial de tudo que estiver entre elas, menos o do cifrão ($ ) e do acento grave. Condicionalmente, também mantém o significado da contra-barra (\ ) e, no modo interativo, da exclamação (! , busca de eventos no histórico). |
# |
No início da linha, ou antecedida de espaços, impede que qualquer coisa, a partir da sua ocorrência, seja vista pelo shell como um comando ou parte de um comando. |
Portanto...
:~$ echo Eu tenho um '$objeto'
Resulta em...
Eu tenho um $objeto
Porque o significado especial do $
foi removido pela citação (aspas simples).
Não há necessidade, neste momento, de nos aprofundarmos nas citações nem em outros detalhes sobre o processamento da linha de comando: tudo isso já foi exaustivamente detalhado em texto e vídeo na aula 5 do curso Shell GNU. Contudo, ainda precisamos entender melhor o mecanismo das expansões do shell.
A expansão é o mecanismo pelo qual o shell transforma a nossa linha de comando antes de, efetivamente, executá-la. Logo na primeira etapa de processamento, ocorre a primeira expansão do shell: a expansão de apelidos (aliases).
A rigor, os apelidos são como variáveis que armazenam strings que expressam comandos, com algumas diferenças:
- Apelidos são armazenados e gerenciados em uma tabela própria na memória.
- São expandidos a partir de seus nomes sem o uso de
$
. - Só são expandidos se forem invocados no começo da linha do comando.
- Antecedidos pela contra-barra (
\
), ou entre aspas, deixam de ser apelidos. - Seus identificadores (nomes) podem conter qualquer combinação de caracteres que não sejam tokens.
Na expansão de um apelido, na primeira etapa do processamento do comando, seu identificador é substituído pela string a ele associado.
Observe o que acontece com o apelido ameixa
, criado desta forma:
:~$ alias ameixa='echo Eu sou uma fruta'
Quando invocado, antes do shell executar a linha do comando, acontece a transformação:
:~$ ameixa
↓
Transformação da linha de comando (expansão)...
↓
:~$ echo Eu sou uma fruta
↓
Resultando em...
↓
Eu sou uma fruta
O mesmo mecanismo é aplicado às demais expansões do shell, porém, as transformações só acontecerão na terceira etapa de processamento da linha do comando.
Ainda utilizando o apelido ameixa
como exemplo, observe o comando abaixo:
:~$ ameixa madura
Como vimos, identificado o apelido, o shell procederá a expansão na primeira etapa de processamento, transformando nosso comando em:
:~$ echo Eu sou uma fruta madura
Que resulta na saída...
Eu sou uma fruta madura
Porém, observe e deduza o que acontecerá com esta linha de comando:
:~$ ameixamadura
Como não há separação entre ameixa
e madura
, o shell não tem como saber onde termina o identificador do apelido e tentará executar o comando inexistente ameixamadura
, o que causará um erro:
:~$ ameixamadura
bash: ameixamadura: comando não encontrado
Nós podemos analisar o problema da seguinte forma:
- Para o shell,
ameixamadura
é apenas uma palavra. - O shell verifica se essa palavra está na tabela de apelidos.
- Não encontrando, nada será feito com a palavra na primeira etapa de processamento.
- Na segunda etapa, o shell buscará por funções, comandos internos e binários executáveis no
PATH
que correspondam à palavraameixamadura
. - Não encontrando, o shell interrompe o processamento e produz um erro.
No caso dos apelidos, nós temos dois agravantes: primeiro, o identificador pode conter qualquer combinação de caracteres que não sejam tokens; segundo, o apelido tem que ser invocado no início da linha do comando. Isso significa que a única forma de separar o identificador de um apelido dos demais elementos é através do uso de um operador (de controle, pipe, redirecionamento, etc...).
Daí minha pergunta: você já viu alguém dizendo que temos que colocar um espaço (ou qualquer operador do shell) depois do identificador "para proteger o apelido"?
Eu espero que não! Uma, porque não é assim que as coisas funcionam; outra, porque os apelidos sabem se defender muito bem sozinhos. Então, que história é essa de expandir um nome entre chaves para "proteger a variável"? Por que alguém diria uma coisa dessas?
Depois da primeira etapa, o shell só voltará a processar expansões na terceira etapa, o que também obedecerá a uma sequência rígida:
- Expansão de chaves (outras chaves, não as "protetoras");
- Expansão do til;
- Expansão de parâmetros, substituições de comandos, substituições de processos e expansões aritméticas;
- Separação de palavras;
- Expansão de nomes de arquivos;
- Remoção de caracteres de citação.
Como podemos ver, as expansões que envolvem os nomes de variáveis (expansões de parâmetros) só acontecem na terceira etapa da terceira etapa de processamento de uma linha de comando. Isso tem uma série de implicações que não vêm ao caso neste artigo. Para nós, o que interessa agora é saber como o shell expande as variáveis. Entretanto, existe um ponto que precisa ser destacado: todas as expansões da terceira etapa, menos a substituição de processos, são iniciadas pelo caractere especial $
.
Observe a tabela:
Expansão | Descrição |
---|---|
${identificador} |
Expande o valor associado ao identificador . |
$(COMANDOS) |
Executa COMANDOS e expande as saídas produzidas. |
$(< ARQUIVO) |
Expande o conteúdo de ARQUIVO . |
$((EXPRESSÃO)) |
Expande o valor avaliado de EXPRESSÃO . |
Reparou em algo interessante logo na primeira linha?
Exatamente! A sintaxe padrão para expandir valores associados a variáveis é ${identificador}
! Não tem nada a ver com proteger o pobrezinho do nome da variável, é apenas o jeito "oficial" de invocar a expansão de uma variável.
No shell, nós temos dois tipos de variáveis: escalares e vetoriais, cujas expansões seriam invocadas "oficialmente" da seguinte forma:
Variável escalar : ${nome}
Variável vetorial: ${nome[subscrito]}
Nas variáveis escalares (que apontam para apenas um valor), nós temos um identificador simples, apenas um nome, mas os vetores trazem um subscrito (o índice do elemento) entre colchetes como parte do identificador.
Além disso, as expansões dos valores associados a variáveis podem ser transformados de várias formas, incluindo modificadores antes ou depois dos identificadores.
Por exemplo:
:~$ var=pitanga
# Expandindo a quantidade de caracteres do valor...
:~$ echo ${#var}
7
# Removendo o padrão 'pi' do início do valor...
:~$ echo ${var#pi}
tanga
# O valor em si permanece inalterado...
:~$ echo ${var}
pitanga
Tendo em vista essas três situações:
- Variáveis escalares
- Vetores
- Transformações de expansões
Para qual delas um nome bastaria para identificar o que se quer expandir?
Se você respondeu "variáveis escalares", é isso mesmo! Para a expansão de variáveis escalares sem transformações, o nome da variável é tudo de que o shell precisa para saber o que expandir. Portanto, tanto faz escrevermos ${nome}
ou $nome
: a nossa variável estará sã e salva!
Aliás, como a dispensa das chaves para a expansão de variáveis escalares sem transformações está explicitamente documentada,
$nome
também é uma forma "oficial".
Observe o exemplo:
:~$ echo "Eu gosto de $var."
Eu gosto de pitanga.
Algo chamou a sua atenção? Observe com atenção a expansão de var
. Reparou que existe um ponto ligado ao nome? Em tese, $var.
seria uma palavra só na linha do comando, já que o ponto (.
) não tem significado especial para o shell e não há uma separação de palavras ali.
Ou será que tem?
Na verdade, não existe um separador, mas existe uma borda de palavra.
Diferente o conceito geral de "palavra", utilizado quando falamos do processamento da linha de um comando, os identificadores válidos para a definição de nomes de variáveis no shell são aqueles que casam com a expressão regular:
^[A-Za-z_][0-9A-Za-z_]*$
Ou seja: sequências de caracteres maiúsculos e minúsculos não acentuados, o sublinhado (underscore) e dígitos numéricos, desde que não sejam o primeiro caractere do identificador.
Sendo assim, como o ponto não faz parte do conjunto de caracteres válidos, o shell é esperto o suficiente para saber que ali, antes dele, existe uma borda de palavra, separando o que deverá ser expandido do restante da string.
Observe outro exemplo:
:~$ var='Caro '
:~$ echo $varÁlvaro
Caro Álvaro
Aqui, a borda foi definida porque Á
não é um caractere válido em identificadores de variáveis e, portanto, nada mais foi necessário para delimitar o fim do nome a ser expandido.
Contudo, pode ocorrer de o caractere seguinte, na concatenação com uma expansão, ser um caractere válido para a formação de identificadores de variáveis. Nessas situações, não se trata de "proteger" uma variável, trata-se de escrever a expansão na sua forma completa, com as chaves, pelo simples fato de que, obviamente, acontecerá a invocação de outro nome:
:~$ var='Cara '
:~$ echo $varLina <--- 'varLina' é um nome válido
<--- Mas é uma variável não definida.
:~$
Aqui, utilizar as chaves não é uma questão de "proteção", mas de suspensão da dispensa de uma formalidade em nome da clareza, da explicitação dos limites, o que reflete, sem qualquer dúvida, uma decisão consciente e embasada pelo conhecimento e domínio do funcionamento do shell.
:~$ var='Cara '
:~$ echo ${var}Lina
Cara Lina
- Variáveis sabem se defender sozinhas, não precisam da nossa proteção.
- Utilizar ou não chaves não é uma regra, mas o resultado direto de saber o que estamos fazendo.
- Quem fala coisas como "proteger variáveis", não sabe do que está falando ou não quer que você saiba.
- Nenhum conceito é trivial e seu estudo pode trazer grandes descobertas e novas curiosidades.
- Não existe atalho para aprender shell: tem que estudar e praticar muito!