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
- Contato: blau@debxp.org
- Lista de vídeos e textos para você estudar
- Curso permanante: Técnicas do Shell
Formas de apoio
- Apoio regular mensal no Apoia.se
- Doações pelo PicPay
- Chave PIX: pix@blauaraujo.com
- Versão impressa do Pequeno Manual do Programador GNU/Bash
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.
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. |
É 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 ($
).
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.
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
.
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.
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
é 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 oset
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.
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
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.
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
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.
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
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
eBASH_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
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
.
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.
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
- Contato: blau@debxp.org
- Lista de vídeos e textos para você estudar
- Curso permanante: Técnicas do Shell
Formas de apoio
- Apoio regular mensal no Apoia.se
- Doações pelo PicPay
- Chave PIX: pix@blauaraujo.com
- Versão impressa do Pequeno Manual do Programador GNU/Bash