Curso: interfaces para scripts em shell – Aula 2

Nas interfaces CLI, toda interação possível com o script se dá pela linha de comando que resulta na sua invocação: em outras palavras, é pela linha de comando que nós passamos dados e instruções para o script. Nesta aula, nós veremos os principais mecanismos envolvidos na captura de dados passados na forma de argumentos na linha do comando.


Para aprender mais sobre o shell

Formas de apoio


Aula 2 - Recebendo dados pela linha de comando

Como vimos, nas interfaces CLI, toda interação possível com o script se dá pela linha de comando que resulta na sua invocação: em outras palavras, é pela linha de comando que nós passamos dados e instruções para o script.

Por exemplo, observe o script nome.sh, abaixo:

#!/usr/bin/env bash

echo "Seu nome é $1."

Apesar de simples, existem vários conceitos importantes aqui, dos quais, o que merece a nossa atenção no momento é a expansão de uma variável que está sendo representada por $1.

Nós sabemos que o cifrão ($), quando precede o identificador válido de uma variável, indica que o shell deverá expandir essa variável antes de executar a linha do comando. Porém, apesar de nomes de variáveis não poderem começar com caracteres numéricos, lá está uma variável de nome 1 para ser expandida. Será que aprendemos errado ou se trata de uma condição especial?

Vamos testar:

:~$ 3=teste
bash: 3=teste: comando não encontrado

De fato, como 3 não é um nome válido, o shell sequer interpreta nossa linha de comando como uma atribuição de um valor a uma variável. Em vez disso, toda a sequência de caracteres (3=teste) é vista como uma palavra que o shell tenta interpretar, sem sucesso, como um comando, o que nos leva a concluir que, se a construção $1 for válida, existe algo de especial nela.

2.1 - Parâmetros especiais

Na verdade, além de variáveis identificadas por números, existem diversas outras com nomes que nós não poderíamos utilizar, mas que o próprio shell utiliza para registrar uma série de informações úteis: são os parâmetros especiais.

A tabela abaixo descreve os parâmetros especiais do shell:

Parâmetro Nome Descrição
$0...$n Parâmetros posicionais Expandem cada uma das palavras passadas como argumentos em uma linha de comando segundo sua ordem de aparição. O primeiro parâmetro, $0, sempre receberá o nome do executável que deu início à sessão do shell.
$# Quantidade de argumentos Expande um inteiro correspondente à quantidade de argumentos passados para a sessão do shell, o que não inclui na contagem o parâmetro $0.
$* Todos os argumentos Expande uma lista de todos os argumentos passados para a sessão do shell. Entre aspas, expande tudo como uma palavra só com os argumentos separados pelo primeiro caractere definido na variável IFS.
$@ Todos os argumentos Sem aspas, não há diferença da expansão $*. Entre aspas, porém, a expansão de $@ resultará em todos os argumentos separados segundo as regras de citação aplicadas.
$_ Último argumento do comando anterior Expande o último parâmetro posicional do último comando executado: que pode ser, inclusive, o valor em $0.
$- Parâmetros do shell Expande todas as opções ativas (flags) passadas na inicialização do shell.
$? Estado de saída Expande o estado de saída do último comando executado.
$$ PID da sessão Expande o identificador do processo (PID) da sessão corrente do shell.
$! PID do job Expande o PID do último processo executado em segundo plano na sessão.

Variáveis ou parâmetros?

É muito comum vermos a expansão dos identificadores acima serem chamados de "variáveis especiais". Embora não seja de todo errado, trata-se de uma imprecisão que nos dá a oportunidade de falarmos sobre esses conceitos:

  • Uma variável é um identificador associado a um dado na memória que pode variar ao longo da execução do programa.
  • Um parâmetro é o dado que um programa (ou uma sub-rotina) espera receber para a sua execução.
  • Complementarmente, os argumentos são os dados passados para um programa (ou sub-rotina), no momento da sua invocação.

Em muitas linguagens, quando queremos especificar os parâmetros esperados pelo programa ou suas sub-rotinas (uma função, por exemplo), nós definimos as variáveis que receberão esses dados. No shell, isso não é possível, porque o programa que será executado a partir da invocação dos nossos scripts, na verdade, sempre será o shell e, consequentemente, toda passagem de argumentos será para a sessão do shell, e não exatamente para o nosso script.

Sendo assim, tudo que podemos fazer é acessar os parâmetros especiais que o shell utiliza para registrar os dados que recebe, o que é feito, nos nossos scipts, expandindo seus identificadores prefixados pelo cifrão ($).

2.2 - Parâmetros posicionais

Voltando ao script nome.sh:

#!/usr/bin/env bash

echo "Seu nome é $1."

Agora nós sabemos que $1 expande a primeira palavra passada após o nome do script na linha do comando que o invocar. Então, vamos passar alguns argumentos:

# Sem argumentos...

:~$ ./nome.sh
Seu nome é .   <-- Nada foi expandido.

# Com um argumento...

:~$ ./nome.sh João
Seu nome é João.   <-- A primeira palavra era 'João'.

:~$ ./nome.sh João da Silva
Seu nome é João.   <-- Apena a primeira palavra é expandida.

O conceito de 'palavra'

Numa linha de comando, alguns caracteres terão significado especial para o shell, como espaços, tabulações, quebras de linha (nos scripts), além de: |, &, ;, (, ), < e >. Esses caracteres (ou metacaracteres, para ser mais exato) irão compor os chamados operadores do shell e, quando aparecerem na linha do comando, eles delimitarão palavras. Portanto, para o shell, uma palavra é qualquer sequência contínua de caracteres delimitada por um operador.

Objetivamente, nós podemos afirmar que cada palavra na linha de comando que invoca um script será um argumento passado para a sessão do shell iniciada por esse script: inclusive, o próprio nome do script, que será registrado no parâmetro especial $0.

Regras de citação

Todavia, nós podemos mudar o contexto em que os caracteres são analisados pelo shell ao longo da primeira etapa de processamento da linha do comando, fazendo com que, aqueles que tiverem algum, percam seu significado especial e sejam tratados como caracteres textuais comuns.

Essa mudança de contexto é feita pelas citações (quoting, em inglês) através de:

Citação Descrição
Contra-barra (\) Remove o significado especial do caractere que vier imediatamente em seguida.
Aspas simples ('...') Remove o significado especial de todos os caracteres entre elas, sem exceção.
Aspas duplas ("...") Remove o significado especial de todos os caracteres entre elas, exceto o acento grave ( ` ), o cifrão ($), a contra-barra (\) e, no modo interativo, a exclamação (!).

Revendo o script nome.sh em ação, observe:

# Com mais de um argumento...

:~$ ./nome.sh João da Silva  <-- São três palavras.
Seu nome é João.  <------------- Só expande o primeiro parâmetro!

# Com o nome completo citado entre aspas...

:~$ ./nome.sh 'João da Silva' <-- É um argumento só!
Seu nome é João da Silva.  <----- Ainda é o primeiro parâmetro posicional.

Para efeito do nosso estudo de interfaces CLI, este é o ponto mais relevante no momento: como as citações afetam os argumentos que estão sendo passados na invocação do script e como o shell expandirá os parâmetros recebidos.

2.3 - Expandindo parâmetros

Para facilitar as demonstrações de como os parâmetros especiais funcionam, em vez de utilizarmos um script, nós trabalharemos no modo interativo alterando os parâmetros da sessão do shell com o comando interno set. Neste caso, em vez do nome de um script, o parâmetro especial $0 estará associado ao nome do executável que deu início à sessão do shell: bash, portanto.

O comando 'set'

O comando set é utilizado para definir opções de execução do shell, mas também pode definir os argumentos que serão passados para a sessão.

Para mais informações, consulte help set, porque nós vamos nos concentrar apenas na possibilidade de modificar os parâmetros posicionais da sessão em curso que o set nos dá.

O comando set permite o uso do argumento -- (dois traços) para separar as opções do shell de uma lista de palavras: tudo que vier depois de -- será tratado como parâmetro posicional do shell; se não houver nada depois de --, os parâmetros posicionais da sessão corrente do shell serão destruídos.

Isso vale tanto para o modo interativo quanto para os nossos scripts.

Expandindo argumentos

Observe o exemplo:

:~$ set -- João Maria Luis Carlos

Com este comando, nós definimos quatro palavras que serão passadas para a sessão do shell como argumentos. Internamente, o shell recebe esses argumentos e os associa aos parâmetros posicionais a partir de 1:

:~$ echo $1
João
:~$ echo $2
Maria
:~$ echo $3
Luis
:~$ echo $4
Carlos

Mas também podemos expandir o nome do executável que deu início à sessão do shell:

:~$ echo $0
bash

O número de argumentos recebidos pode ser obtido com a expansão do parâmetro especial #, que não inclui na contagem o parâmetro identificado pelo 0:

:~$ echo $#
4

Também podemos expandir todos os argumentos de uma vez com o parâmetro especial *:

:~$ echo $*
João Maria Luis Carlos

Ou com o parâmetro especial @:

:~$ echo $@
João Maria Luis Carlos

Alterando a citação de caracteres (com as aspas), a quantidade de argumentos muda:

:~$ set -- João Maria 'Luis Carlos'
:~$ echo $#
3

Bem como os dados associados a cada parâmetro posicional:

:~$ echo $1
João
:~$ echo $2
Maria
:~$ echo $3
Luis Carlos
:~$ echo $4
            <-- Não há mais um parâmetro '4'!

Expandindo o * ou o @ como argumentos do comando echo, não teremos como saber onde começam ou terminam as palavras:

# Com o parâmetro especial '*'...

:~$ echo $*
João Maria Luis Carlos

# Com o parâmetro especial '@'...

:~$ echo $@
João Maria Luis Carlos

Neste caso, o comando printf pode ser mais útil:

# Inserindo uma quebra de linha depois de cada argumento...

:~$ printf '%s\n' $*
João
Maria
Luis
Carlos

Da forma que a expansão foi feita, acima, não há diferença entre o * e o @:

# Inserindo uma quebra de linha depois de cada argumento...

:~$ printf '%s\n' $@
João
Maria
Luis
Carlos

A diferença aparece quando expandimos * ou @ entre aspas duplas. O * expande uma lista de palavras que, entre aspas, torna-se uma palavra só:

:~$ printf '%s\n' "$*"
João Maria Luis Carlos

Por sua vez, o @ expande uma lista de palavras que, entre aspas, mantém sua separação original:

:~$ printf '%s\n' "$@"
João
Maria
Luis Carlos

Expandindo parâmetros com '@' e '*'

Dentre as diversas transformações possíveis através das expansões do shell, nós podemos extrair substrings de variáveis escalares ou faixas de elementos de vetores. Em termos de implementação, o parâmetro especial @ é um vetor, porém, não é possível utilizá-lo diretamente como tal:

:~$ echo ${@[2]}
bash: ${@[2]}: substituição incorreta

Para o efeito desejado nos exemplos, nós teremos que tratá-lo como uma variável escalar. Observe:

:~$ echo ${@:2:1}
Maria
:~$ echo ${@:3:1}
Luis Carlos
:~$ echo ${@:0}
bash João Maria Luis Carlos

O mais interessante, porém, é que as citações dos argumentos são mantidas mesmo quando expandimos o * desta forma:

:~$ echo ${*:1:1}
João
:~$ echo ${*:2:1}
Maria
:~$ echo ${*:3:1}
Luis Carlos

O parâmetro 0 também pode ser expandido assim:

:~$ echo ${*:0:1}
bash

Para aprender mais sobre este tipo de expansão, estude o tópico 7.12 do curso Shell GNU.

2.4 - Expandindo argumentos em 'loops'

Nós podemos acessar individualmente os argumentos passados na invocação dos nossos scripts pela expansão do parâmetro posicional correspondente à sua ordem de aparição na linha do comando:

:~$ set -- a b 'c d' e
:~$ echo $2
b
:~$ echo $4
e
:~$ echo $3
c d

Também podemos expandir todos os argumentos de uma vez com os parâmetros especiais @ e *:

:~$ set -- a b 'c d' e
:~$ echo $@
a b c d e
:~$ echo $*
a b c d e

Neste último caso, o resultado da expansão será uma lista de palavras, que é exatamente a forma mais comum de controle da estrutura de repetição for:

# Em uma linha...

for VAR in LISTA_DE_PALAVRAS; do COMANDOS; done

# Em várias linhas...

for VAR in LISTA_DE_PALAVRAS; do
    COMANDOS
done

Sendo assim, nós podemos expandir sequencialmente (e na ordem de aparição na expansão) cada um dos parâmetros posicionais:

:~$ set -- a b 'c d' e
:~$ for arg in $@; do echo $arg; done
a
b
c
d
e

Como o @ foi expandido sem aspas, as citações dos argumentos foram ignoradas, o que resulta numa lista contendo cinco palavras distintas e em cinco ciclos de execução do bloco de comandos.

Se utilizado entre aspas, o @ expandirá os parâmetros respeitando as citações aplicadas aos argumentos na linha do comando:

:~$ set -- a b 'c d' e
:~$ for arg in "$@"; do echo $arg; done
a
b
c d
e

Por outro lado, como vimos, a expansão do parâmetro especial * entre aspas resultará em apenas uma palavra contendo todos os argumentos passados para o script e, portanto, na execução de apenas um ciclo do loop for:

:~$ set -- a b 'c d' e
:~$ for arg in "$*"; do echo $arg; done
a b c d e

No Bash, a expansão do @ entre aspas é tão comum (e útil) que, quando for este o caso, a sintaxe do for pode ser simplificada com a omissão da parte in "$@":

:~$ set -- a b 'c d' e
:~$ for arg; do echo $arg; done
a
b
c d
e

O loop 'for' no estilo C

Além da sintaxe comum, onde o controle das repetições é feito por uma lista de palavras, o for também pode ser controlado por uma expressão lógica e aritmética tripla, tal como sua contraparte na linguagem C. A sintaxe é um pouco diferente, mas é bem mais familiar para quem vem de outras linguagens:

# Em uma linha...

for ((EXP1; EXP2; EXP3)) { COMANDOS; }

for ((EXP1; EXP2; EXP3)); do COMANDOS; done

# Em várias linhas...

for ((EXP1; EXP2; EXP3)) {
    COMANDOS
}

for ((EXP1; EXP2; EXP3)); do
    COMANDOS
done

Desta forma, é possível percorrer os argumento a partir da expansão dos elementos nos parâmetros especiais @ ou *, por exemplo:

:~$ set -- a b 'c d' e
:~$ for ((i = 1; i <= $#; i++)) { echo ${@:$i:1}; }
a
b
c d
e

Para entender como essa estrutura de repetição funciona, nós teremos que pensar em etapas:

Primeira etapa:

A expressão EXP1, que geralmente é a atribuição de um valor inicial a uma variável de referência, é avaliada: esta avaliação ocorre apenas uma vez, antes do primeiro ciclo.

No exemplo, nós atribuímos o valor 1 à variável i:

for ((i = 1; i <= $#; i++)) { echo ${@:$i:1}; }
      -----
        ↑
      EXP1

Segunda etapa:

A expressão EXP2 é avaliada quanto à verdade da sua afirmação. Geralmente, é uma comparação entre o valor na variável de referência e um valor inteiro utilizado como limite: se EXP2 for avaliada como verdadeira, haverá um ciclo de execução do bloco de comandos; caso seja avaliada como falsa, nenhum novo ciclo será executado e o for será encerrado.

No exemplo, nós comparamos o valor corrente de i com o número total de argumentos no parâmetro especial #:

for ((i = 1; i <= $#; i++)) { echo ${@:$i:1}; }
             -------
                ↑
              EXP2

Terceira etapa:

Após executar um ciclo do bloco de comandos, a expressão em EXP3, que costuma ser uma alteração no valor da variável de referência, será avaliada.

No caso do exemplo, nós incrementamos em uma unidade o valor de i:

for ((i = 1; i <= $#; i++)) { echo ${@:$i:1}; }
                      ---
                       ↑
                     EXP3

Quarta etapa:

Completadas as etapas anteriores, EXP2 é novamente avaliada para confirmar se a sua afirmação ainda é verdadeira: se for, o bloco de comandos é executado novamente e EXP3 é avaliada; caso contrário, nenhum novo ciclo é executado e o for é encerrado.

O problema é que, apesar de bastante prático, o estilo C não é portável. Se isso for um requisito, nossa única opção será trabalhar com a expansão de uma lista de palavras. Mas, considerando que estamos escrevendo scripts para o shell interativo padrão de sistemas GNU/Linux, o Bash, vale a pena considerá-lo na implementação de uma iteração pelos valores em parâmetros posicionais.

Expandindo parâmetros com os loops 'while' e 'until'

Observe o exemplo com o loop while:

:~$ set -- a b 'c d' e
:~$ i=1; while [[ $i -le $# ]]; do echo ${@:$i:1}; ((i++)); done
a
b
c d
e

O comando composto while avalia o estado de saída de um comando para permitir (ou não) a execução de um ciclo do bloco de comandos:

# Em uma linha...

while COMANDO_TESTADO; do COMANDOS; done

# Em várias linhas...

while COMANDO_TESTADO; do
    COMANDOS
done

No nosso exemplo, COMANDO_TESTADO é o comando composto [[, o test do Bash, que avalia expressões assertivas: no exemplo, nós afirmamos que o valor corrente de i é menor ou igual ao número de argumentos no parâmetro especial #:

:~$ i=1; while [[ $i -le $# ]]; do echo ${@:$i:1}; ((i++)); done
               ---------------
                      ↑
               COMANDO_TESTADO

A variável i foi iniciada com o valor 1 antes do while ser executado, o que fez com que a avaliação da expressão resultasse em verdadeiro e o comando [[ terminasse com estado de sucesso. Sendo assim, o bloco de comandos é executado: primeiro, expandindo o parâmetro de número 1:

:~$ i=1; while [[ $i -le $# ]]; do echo ${@:$i:1}; ((i++)); done
                                        ---------
                                            ↑
                                  EXPANSÃO DO PARÂMETRO 'i'

Em seguida, incrementando o valor em i:

:~$ i=1; while [[ $i -le $# ]]; do echo ${@:$i:1}; ((i++)); done
                                                   -------
                                                      ↑
                                      INCREMENTO DO VALOR EM 'i'

Os ciclos continuarão até que o valor em i seja maior do que a quantidade de argumentos passados para o script, o que fará com que o comando [[ termine com estado de erro.

Se optássemos pelo loop until, a técnica seria a mesma, com a diferença de que o comando testado precisaria terminar com erro para que o bloco de comandos fosse executado.

Portanto, afirmando que o valor de i é maior do que a quantidade de argumentos, nós temos a condição de erro necessária para a execução do bloco de comandos:

:~$ set -- a b 'c d' e
:~$ i=1; until [[ $i -gt $# ]]; do echo ${@:$i:1}; ((i++)); done
a
b
c d
e

2.5 - Parâmetros em pilhas de execução do Bash

Quando falamos em pilhas de execução, estamos nos referindo a duas coisas:

  • Cada um dos fluxos de execução do shell: o fluxo principal e as sub-rotinas (funções).
  • Os parâmetros relativos a cada um desses fluxos registrados na forma de pilhas nos vetores BASH_ARGC e BASH_ARGV.

Os vetores 'BASH_ARGC' e 'BASH_ARGV'

Uma pilha é uma estrutura em que os dados mais recentes são registrados acima (metaforicamente) dos dados mais antigos. No caso do vetor BASH_ARGV, o conceito de pilha se aplica à ordem dos parâmetros posicionais: os últimos parâmetros são "empilhados" sobre os primeiros:

Pilha de parâmetros

   +-------------+          ---+
$N | Parâmetro N | Índice 0    |
   +-------------+             |
          ·                    |
          ·                    |
          ·                    +-- BASH_ARGV
   +-------------+             |
$3 | Parâmetro 3 | Índice N-3  |
   +-------------+             |
$2 | Parâmetro 2 | Índice N-2  |
   +-------------+             |
$1 | Parâmetro 1 | Índice N-1  |
   +-------------+          ---+

Por sua vez, o vetor BASH_ARGC empilhará a quantidade de parâmetros recebidos na chamada de cada fluxo de execução ao longo da sessão:

Pilha da contagem de parâmetros

  Momento 1 (início do script)
  +-------------------+        ---+
  |  Fluxo Principal  | Índice 0  |-- BASH_ARGC
  +-------------------+        ---+

  Momento 2 (chamada da função 1)
  +-------------------+        ---+
  | Fluxo da Função 1 | Índice 0  |
  +-------------------+           +-- BASH_ARGC
  |  Fluxo Principal  | Índice 1  |
  +-------------------+        ---+

  Momento 3 (retorno da função 1)
  +-------------------+        ---+
  |  Fluxo Principal  | Índice 0  |-- BASH_ARGC
  +-------------------+        ---+

Enquanto BASH_ARGC é atualizado para registrar a quantidade de parâmetros de cada fluxo, BASH_ARGV é atualizado com os parâmetros de todos os fluxos em execução. Por padrão, porém, os vetores BASH_ARGC e BASH_ARGV só são atualizados no início da sessão do Bash.

Quando no modo interativo, ambos os vetores estão definidos, mas não possuem elementos (BASH_ARGV) e a quantidade de parâmetros do fluxo principal de execução será 0 (BASH_ARGC):

:~$ declare -p BASH_ARGV
declare -a BASH_ARGV=()
:~$ declare -p BASH_ARGC
declare -a BASH_ARGC=([0]="0")

Mesmo que novos parâmetros sejam definidos ao longo da sessão, BASH_ARGC e BASH_ARGV não serão atualizados:

:~$ set -- a b 'c d' e
:~$ printf '%s\n' "${BASH_ARGV[@]}"

:~$

Numa sessão não-interativa, se ela for invocada com a passagem de argumentos, cada um deles será "empilhado" em BASH_ARGV, ou seja, o último argumento passado será o primeiro elemento do vetor.

Considerando o script args.sh, abaixo:

#!/usr/bin/env bash

for i in "${!BASH_ARGC[@]}"; do
    printf 'Pilha %s → Qtd: %s\n' $i ${BASH_ARGC[i]}
done

for i in "${!BASH_ARGV[@]}"; do
    printf 'Índice %s → %s\n' $i "${BASH_ARGV[i]}"
done

Observe o que acontece na execução:

:~$ ./args.sh a b 'c d' e
Pilha 0 → Qtd: 4
Índice 0 → e
Índice 1 → c d
Índice 2 → b
Índice 3 → a

Entretanto, ainda por padrão, se houver alterações de parâmetros ao longo da execução do script, pela chamada de uma função, por exemplo, BASH_ARGC e BASH_ARGV não serão atualizados.

Considere o script args2.sh:

#!/usr/bin/env bash

# Uma função que receberá argumentos...

func() {
    for i in "${!BASH_ARGC[@]}"; do
        printf 'Pilha %s → Qtd: %s\n' $i ${BASH_ARGC[i]}
    done
    for i in "${!BASH_ARGV[@]}"; do
        printf 'Índice %s → %s\n' $i "${BASH_ARGV[i]}"
    done
}

printf 'PARÂMETROS DO FLUXO PRINCIPAL...\n\n'

for i in "${!BASH_ARGC[@]}"; do
    printf 'Pilha %s → Qtd: %s\n' $i ${BASH_ARGC[i]}
done
for i in "${!BASH_ARGV[@]}"; do
    printf 'Índice %s → %s\n' $i "${BASH_ARGV[i]}"
done

printf '\nPARÂMETROS DA SUB-ROTINA...\n\n'

func x y z

printf '\nPARÂMETROS DO FLUXO PRINCIPAL (DEPOIS DA FUNÇÃO)...\n\n'

for i in "${!BASH_ARGC[@]}"; do
    printf 'Pilha %s → Qtd: %s\n' $i ${BASH_ARGC[i]}
done
for i in "${!BASH_ARGV[@]}"; do
    printf 'Índice %s → %s\n' $i "${BASH_ARGV[i]}"
done

Observe o que acontece na sua execução:

:~$ ./args2.sh a b 'c d' e
PARÂMETROS DO FLUXO PRINCIPAL...

Pilha 0 → Qtd: 4
Índice 0 → e
Índice 1 → c d
Índice 2 → b
Índice 3 → a

PARÂMETROS DA SUB-ROTINA...

Pilha 0 → Qtd: 4  -+
Índice 0 → e       |
Índice 1 → c d     +--- Sem mudanças!
Índice 2 → b       |
Índice 3 → a      -+

PARÂMETROS DO FLUXO PRINCIPAL (DEPOIS DA FUNÇÃO)...

Pilha 0 → Qtd: 4
Índice 0 → e
Índice 1 → c d
Índice 2 → b
Índice 3 → a

A opção 'extdebug'

O que precisamos compreender é que, apesar de sua utilidade para outros fins, tudo indica que BASH_ARGC e BASH_ARGV visam, essencialmente, o rastreio de parâmetros em uma situação de busca por erros (debug) em scripts, o que podemos depreender do fato deles serem atualizados somente se, no início da sessão, a opção extdebug for habilitada com o comando shopt.

Habilitando e desabilitando extdebug:

# Estado inicial...

:~$ shopt extdebug
extdebug       	off

# Habilitando...

:~$ shopt -s extdebug
:~$ shopt extdebug
extdebug       	on

# Desabilitando...

:~$ shopt -u extdebug
:~$ shopt extdebug
extdebug       	off

Numa sessão interativa, mesmo que extdebug seja habilitado, não haverá atualizações relativas ao fluxo principal:

:~$ shopt extdebug
extdebug       	off
:~$ shopt -s extdebug
:~$ set -- a b 'c d' e
:~$ printf '%s\n' "${BASH_ARGV[@]}"

:~$

Contudo, com extdebug habilitado, se uma função for executada, seus parâmetros serão atualizados nos vetores:

:~$ shopt extdebug
extdebug       	on
:~$ func() { printf '%s\n' "${BASH_ARGV[@]}"; }
:~$ func a b c
c
b
a

Para ver como isso afeta uma sessão não-interativa, observe o que acontece quando habilitamos extdebug no script args3.sh:

#!/usr/bin/env bash

# Habilitando 'extdebug'...
shopt -s extdebug

# Uma função que receberá argumentos...
func() {
    for i in "${!BASH_ARGC[@]}"; do
        printf 'Pilha %s → Qtd: %s\n' $i ${BASH_ARGC[i]}
    done
    for i in "${!BASH_ARGV[@]}"; do
        printf 'Índice %s → %s\n' $i "${BASH_ARGV[i]}"
    done
}

printf 'PARÂMETROS DO FLUXO PRINCIPAL...\n\n'

for i in "${!BASH_ARGC[@]}"; do
    printf 'Pilha %s → Qtd: %s\n' $i ${BASH_ARGC[i]}
done
for i in "${!BASH_ARGV[@]}"; do
    printf 'Índice %s → %s\n' $i "${BASH_ARGV[i]}"
done

printf '\nPARÂMETROS DA SUB-ROTINA...\n\n'

func x y z

printf '\nPARÂMETROS DO FLUXO PRINCIPAL (DEPOIS DA FUNÇÃO)...\n\n'

for i in "${!BASH_ARGC[@]}"; do
    printf 'Pilha %s → Qtd: %s\n' $i ${BASH_ARGC[i]}
done
for i in "${!BASH_ARGV[@]}"; do
    printf 'Índice %s → %s\n' $i "${BASH_ARGV[i]}"
done

Executando...

:~$ ./args3.sh a b 'c d' e
PARÂMETROS DO FLUXO PRINCIPAL...

Pilha 0 → Qtd: 4
Índice 0 → e
Índice 1 → c d
Índice 2 → b
Índice 3 → a

PARÂMETROS DA SUB-ROTINA...

Pilha 0 → Qtd: 3  <-- Função foi para o topo da pilha.
Pilha 1 → Qtd: 4  <-- Fluxo principal foi para o final.
Índice 0 → z    --+
Índice 1 → y      |
Índice 2 → x    --+--- Parâmetros recebidos na função.
Índice 3 → e    --+--- Parâmetros recebidos no fluxo principal.
Índice 4 → c d    |
Índice 5 → b      |
Índice 6 → a    --+

PARÂMETROS DO FLUXO PRINCIPAL (DEPOIS DA FUNÇÃO)...

Pilha 0 → Qtd: 4 -+
Índice 0 → e      |
Índice 1 → c d    +-- Fluxo principal volta para o topo.
Índice 2 → b      |
Índice 3 → a     -+

Vale destacar que, em scripts, é muito comum que o bloco principal de comandos seja executado a partir da chamada de uma função (geralmente chamada de main()), o que interfere nos valores associados aos vetores BASH_ARGC e BASH_ARGV.

Quando 'BASH_ARGC' e 'BASH_ARGV' recebem valores

Observe o script args4.sh:

#!/usr/bin/env bash

# Habilitando 'extdebug'...
shopt -s extdebug

# Uma função que receberá argumentos...

func() {
    for i in "${!BASH_ARGC[@]}"; do
        printf 'Pilha %s → Qtd: %s\n' $i ${BASH_ARGC[i]}
    done
    for i in "${!BASH_ARGV[@]}"; do
        printf 'Índice %s → %s\n' $i "${BASH_ARGV[i]}"
    done
}

printf 'CHAMADA SEM ARGUMENTOS...\n\n'

func

printf '\nCHAMADA COM ARGUMENTOS...\n\n'

func x y z

printf '\nNOVA CHAMADA SEM ARGUMENTOS...\n\n'

func

Executando...

:~$ ./args4.sh a b 'c d' e
CHAMADA SEM ARGUMENTOS...

Pilha 0 → Qtd: 0 <-- Pilha da função não recebeu parâmetros.
Pilha 1 → Qtd: 4 <-- Argumentos do fluxo principal.
Índice 0 → e
Índice 1 → c d
Índice 2 → b
Índice 3 → a

CHAMADA COM ARGUMENTOS...

Pilha 0 → Qtd: 3 <-- Parâmetros na pilha da função.
Pilha 1 → Qtd: 4 <-- Argumentos do fluxo principal.
Índice 0 → z    --+
Índice 1 → y      |
Índice 2 → x    --+--- Parâmetros recebidos na função.
Índice 3 → e    --+--- Parâmetros recebidos no fluxo principal.
Índice 4 → c d    |
Índice 5 → b      |
Índice 6 → a    --+

NOVA CHAMADA SEM ARGUMENTOS...

Pilha 0 → Qtd: 0 <-- Pilha da função não recebeu parâmetros.
Pilha 1 → Qtd: 4 <-- Argumentos do fluxo principal.
Índice 0 → e
Índice 1 → c d
Índice 2 → b
Índice 3 → a

Comentando a linha de habilitação do extdebug, ainda no script args4.sh, não acontece a atualização dos vetores no contexto da função:

:~$ ./args4.sh a b 'c d' e
CHAMADA SEM ARGUMENTOS...


CHAMADA COM ARGUMENTOS...


NOVA CHAMADA SEM ARGUMENTOS...

:~$

O que nos permite demonstrar que, embora tenham um comportamento de escopo global, com extdebug desabilitado, BASH_ARGC e BASH_ARGV são atualizados apenas no início da sessão, que é um momento em que a função ainda não foi chamada.

Todavia, nós poderíamos imaginar que, mesmo assim, se os argumentos foram passados para o script, eles deveriam constar nos vetores (como no script args.sh). Porém, observe que nem a quantidade de argumentos chegou a ser expandida, o que pode indicar que BASH_ARGC sequer recebeu valores.

Para confirmar, vejamos o script args5.sh, que tem extdebug desabilitado:

#!/usr/bin/env bash

# Descomente para ver a diferença que faz sobre os vetores...
# printf "Nós temos ${BASH_ARGC[@]} argumentos.\n\n"

# Uma função que receberá argumentos...

func() {
    # Consultando o estado de 'BASH_ARGC'...
    declare -p BASH_ARGC
}

printf 'CHAMADA SEM ARGUMENTOS...\n\n'

func

printf '\nCHAMADA COM ARGUMENTOS...\n\n'

func x y z

printf '\nNOVA CHAMADA SEM ARGUMENTOS...\n\n'

func

Executando...

:~$ ./args5.sh a b 'c d' e
CHAMADA SEM ARGUMENTOS...

declare -a BASH_ARGC=() <-- Vetor sem elementos!

CHAMADA COM ARGUMENTOS...

declare -a BASH_ARGC=() <-- Vetor sem elementos!

NOVA CHAMADA SEM ARGUMENTOS...

declare -a BASH_ARGC=() <-- Vetor sem elementos!

:~$

Portanto, nós podemos assumir outra condição para que BASH_ARGC e BASH_ARGV sejam iniciados e recebam valores: se extdebug estiver desabilitado, eles terão que ser referenciados no fluxo principal de execução.

Para comprovar, vamos descomentar esta linha no início de args5.sh:

#!/usr/bin/env bash

# Descomente para ver a diferença que faz sobre os vetores...
printf "Nós temos ${BASH_ARGC[@]} argumentos.\n\n"

...

Executando...

$ ./args5.sh a b 'c d' e
Nós temos 4 argumentos.  <--- Confirmado!

CHAMADA SEM ARGUMENTOS...

declare -a BASH_ARGC=([0]="4") <-- Definido e com valores.

CHAMADA COM ARGUMENTOS...

declare -a BASH_ARGC=([0]="4") <-- Definido e com valores.

NOVA CHAMADA SEM ARGUMENTOS...

declare -a BASH_ARGC=([0]="4") <-- Definido e com valores.

Habilitando 'extdebug' no corpo da função

Mas, o que aconteceria se extdebug fosse habilidada no corpo da função? É o que veremos com o script args6.sh:

#!/usr/bin/env bash

# Descomente para ver a diferença que faz sobre os vetores...
# printf "Nós temos ${BASH_ARGC[@]} argumentos.\n\n"

# Uma função que receberá argumentos...

func() {
    shopt -s extdebug

    echo ARGC "${BASH_ARGC[@]}"
    echo ARGV "${BASH_ARGV[@]}"
}

printf 'CHAMADA SEM ARGUMENTOS...\n\n'

func

printf '\nCHAMADA COM ARGUMENTOS...\n\n'

func x y z

printf '\nNOVA CHAMADA SEM ARGUMENTOS...\n\n'

func

Executando:

$ ./args6.sh a b 'c d' e
CHAMADA SEM ARGUMENTOS...

ARGC 0  <--- A pilha do fluxo principal existe
ARGV         mas está vazia!

CHAMADA COM ARGUMENTOS...

ARGC 3 0  <-- Apenas a pilha da função é atualizada.
ARGV z y x

NOVA CHAMADA SEM ARGUMENTOS...

ARGC 0 0  <-- Ambas as pilhas existem e estão vazias.
ARGV

É somente referenciando BASH_ARGC ou BASH_ARGV que os valores da pilha do fluxo principal serão atualizados, o que podemos ver removendo o comentário da linha no início de arg6.sh:

# Descomente para ver a diferença que faz nos vetores...
printf "Nós temos ${BASH_ARGC[@]} argumentos.\n\n"

Executando:

:~$ ./args6.sh a b 'c d' e
Nós temos 4 argumentos. <--- A pilha do fluxo principal
                             contém dados!
CHAMADA SEM ARGUMENTOS...

ARGC 4
ARGV e c d b a

CHAMADA COM ARGUMENTOS...

ARGC 3 4
ARGV z y x e c d b a

NOVA CHAMADA SEM ARGUMENTOS...

ARGC 0 4
ARGV e c d b a

Para aprender mais sobre o shell

Formas de apoio


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