E agora, quem poderá nos proteger?

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.

Como o shell processa as linhas de comandos

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:

  1. Decomposição da linha do comando;
  2. Classificação de comandos simples, complexos e compostos;
  3. Expansões diversas;
  4. Redirecionamentos;
  5. 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.

Decomposição da linha do comando

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.

'Tokens' (componentes léxicos)

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.

Regras de citação

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.

O que é uma expansão

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.

O nome é tudo para alguém de palavra

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 à palavra ameixamadura.
  • 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?

As expansões da terceira etapa de processamento

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:

  1. Expansão de chaves (outras chaves, não as "protetoras");
  2. Expansão do til;
  3. Expansão de parâmetros, substituições de comandos, substituições de processos e expansões aritméticas;
  4. Separação de palavras;
  5. Expansão de nomes de arquivos;
  6. 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.

Então, por que você escreve sem chaves?

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".

E nas concatenações de strings?

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.

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.

Aí vêm as chaves, chaves, chaves...

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

Conclusões

  • 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!

Músico, programador, designer e apaixonado pelos ideais do Software Livre.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Post comment