105.2 Lição 2
Certificação: |
LPIC-1 |
---|---|
Versão: |
5.0 |
Tópico: |
105 Shells e scripts do Shell |
Objetivo: |
105.2 Personalizar ou criar scripts simples |
Lição: |
2 de 2 |
Introdução
Os scripts de shell destinam-se geralmente a automatizar operações relacionadas a arquivos e diretórios — as mesmas operações que podem ser executadas manualmente na linha de comando. Porém, o alcance dos scripts de shell não se restringe apenas aos documentos do usuário, já que a configuração e interação com muitos aspectos de um sistema operacional Linux também são realizadas por meio de arquivos de script.
O shell Bash oferece muitos comandos internos úteis para criar scripts de shell, mas para aproveitar totalmente o poder desses scripts temos de combinar os comandos internos do Bash com os diversos utilitários de linha de comando disponíveis em um sistema Linux.
Testes ampliados
O Bash, como linguagem de script, é orientado sobretudo a arquivos, de forma que o comando interno do Bash test
tem muitas opções para avaliar as propriedades dos objetos do sistema de arquivos (essencialmente arquivos e diretórios). Os testes que se concentram em arquivos e diretórios são úteis, por exemplo, para verificar se os arquivos e diretórios necessários para executar uma tarefa específica estão presentes e podem ser lidos. A seguir, associado a uma construção condicional if, o conjunto apropriado de ações é executado caso o teste seja bem-sucedido.
O comando test
avalia as expressões usando duas sintaxes diferentes: as expressões de teste podem ser dadas como um argumento para o comando test
ou podem ser postas entre colchetes, caso em que o comando test
é dado implicitamente. Assim, o teste para avaliar se /etc
é um diretório válido pode ser escrito como test -d /etc
ou como [ -d /etc]
:
$ test -d /etc $ echo $? 0 $ [ -d /etc ] $ echo $? 0
Como confirmam os códigos de status de saída na variável especial $?
— um valor de 0 significa que o teste foi bem-sucedido — ambas as formas avaliaram /etc
como um diretório válido. Supondo-se que o caminho para um arquivo ou diretório foi armazenado na variável $VAR
, as seguintes expressões podem ser usadas como argumentos para test
ou entre colchetes:
-a "$VAR"
-
Avalia se o caminho em
VAR
existe no sistema de arquivos e é um arquivo. -b "$VAR"
-
Avalia se o caminho em
VAR
é um arquivo de bloco especial. -c "$VAR"
-
Avalia se o caminho em
VAR
é um arquivo de caractere especial. -d "$VAR"
-
Avalia se o caminho em
VAR
é um diretório. -e "$VAR"
-
Avalia se o caminho em
VAR
existe no sistema de arquivos. -f "$VAR"
-
Avalia se o caminho em
VAR
existe e é um arquivo regular. -g "$VAR"
-
Avalia se o caminho em
VAR
tem permissão SGID. -h "$VAR"
-
Avalia se o caminho em
VAR
é um link simbólico. -L "$VAR"
-
Avalia se o caminho em
VAR
é um link simbólico (como-h
). -k "$VAR"
-
Avalia se o caminho em
VAR
tem a permissão sticky bit. -p "$VAR"
-
Avalia se o caminho em
VAR
é um arquivo pipe. -r "$VAR"
-
Avalia se o caminho em
VAR
é legível pelo usuário atual. -s "$VAR"
-
Avalia se o caminho em
VAR
existe e não está vazio. -S "$VAR"
-
Avalia se o caminho em
VAR
é um arquivo de socket. -t "$VAR"
-
Avalia se o caminho em
VAR
está aberto em um terminal. -u "$VAR"
-
Avalia se o caminho em
VAR
tem permissão SUID. -w "$VAR"
-
Avalia se o caminho em
VAR
é gravável pelo usuário atual. -x "$VAR"
-
Avalia se o caminho em
VAR
é executável pelo usuário atual. -O "$VAR"
-
Avalia se o caminho em
VAR
é de propriedade do usuário atual. -G "$VAR"
-
Avalia se o caminho em
VAR
pertence ao grupo efetivo do usuário atual. -N "$VAR"
-
Avalia se o caminho em
VAR
foi modificado desde o último acesso. "$VAR1" -nt "$VAR2"
-
Avalia se o caminho em
VAR1
é mais recente que o caminho emVAR2
, de acordo com as datas de modificação respectivas. "$VAR1" -ot "$VAR2"
-
Avalia se o caminho em
VAR1
é mais antigo queVAR2
. "$VAR1" -ef "$VAR2"
-
Esta expressão avalia como True (Verdadeiro) se o caminho em
VAR1
é um link físico paraVAR2
.
Recomenda-se usar aspas duplas em torno de uma variável testada porque, se a variável por acaso estiver vazia, isso pode causar um erro de sintaxe para o comando test
. As opções de teste requerem um argumento operando, e uma variável vazia sem aspas causaria um erro devido à falta de um argumento obrigatório. Também existem testes para variáveis de texto arbitrárias, descritos a seguir:
-z "$TXT"
-
Avalia se a variável
TXT
está vazia (tamanho zero). -n "$TXT"
ortest "$TXT"
-
Avalia se a variável
TXT
não está vazia. "$TXT1" = "$TXT2"
or"$TXT1" == "$TXT2"
-
Avalia se
TXT1
eTXT2
são iguais. "$TXT1" != "$TXT2"
-
Avalia se
TXT1
eTXT2
não são iguais. "$TXT1" < "$TXT2"
-
Avalia se
TXT1
vem antes deTXT2
, em ordem alfabética. "$TXT1" > "$TXT2"
-
Avalia se
TXT1
vem depois deTXT2
, em ordem alfabética.
Linguagens diferentes podem ter regras diferentes para a ordenação alfabética. Para obter resultados consistentes, independentemente das configurações de localização do sistema no qual o script está sendo executado, é recomendável definir a variável de ambiente LANG
como C
, como em LANG=C
, antes de fazer operações que envolvam ordem alfabética. Essa definição também manterá as mensagens do sistema no idioma original e, portanto, deve ser usada apenas no escopo do script.
As comparações numéricas têm seu próprio conjunto de opções de teste:
$NUM1 -lt $NUM2
-
Avalia se
NUM1
é menor queNUM2
. $NUM1 -gt $NUM2
-
Avalia se
NUM1
é maior queNUM2
. $NUM1 -le $NUM2
-
Avalia se
NUM1
é menor ou igual aNUM2
. $NUM1 -ge $NUM2
-
Avalia se
NUM1
é maior ou igual aNUM2
. $NUM1 -eq $NUM2
-
Avalia se
NUM1
é igual aNUM2
. $NUM1 -ne $NUM2
-
Avalia se
NUM1
não é igual aNUM2
.
Todos os testes podem receber os seguintes modificadores:
! EXPR
-
Avalia se a expressão
EXPR
é falsa. EXPR1 -a EXPR2
-
Avalia se tanto
EXPR1
quantoEXPR2
são verdadeiras. EXPR1 -o EXPR2
-
Avalia se ao menos uma das duas expressões é verdadeira.
Outra construção condicional, case
, pode ser vista como uma variação da construção if. A instrução case
executa uma lista de comandos dados se um item especificado — por exemplo, o conteúdo de uma variável — puder ser encontrado em uma lista de itens separados por pipes (a barra vertical |
) e encerrado por )
. O exemplo de script a seguir mostra como a construção case
pode ser usada para indicar o formato de pacote de software correspondente para uma determinada distribuição Linux:
#!/bin/bash DISTRO=$1 echo -n "Distribution $DISTRO uses " case "$DISTRO" in debian | ubuntu | mint) echo -n "the DEB" ;; centos | fedora | opensuse ) echo -n "the RPM" ;; *) echo -n "an unknown" ;; esac echo " package format."
Cada lista de padrões e comandos associados deve terminar com ;;
, ;&
, ou ;;&
. O último padrão, um asterisco, será usado se não for encontrada uma correspondência para nenhum outro padrão anterior. A instrução esac
(case de trás pra frente) conclui a construção case
. Supondo que o script de amostra anterior se chame script.sh
e seja executado com opensuse
como primeiro argumento, a seguinte saída será gerada:
$ ./script.sh opensuse Distribution opensuse uses the RPM package format.
Tip
|
O Bash tem uma opção chamada |
O item pesquisado e os padrões sofrem expansão de til, expansão de parâmetro, substituição de comando e expansão aritmética. Se o item pesquisado for especificado com aspas, elas serão removidas antes do script tentar encontrar uma correspondência.
Construções de loop
Os scripts são freqüentemente usados como ferramenta para automatizar tarefas repetitivas, executando o mesmo conjunto de comandos até que seja verificado um critério de interrupção. O Bash tem três instruções de loop — for
, until
e while
— projetadas para construções de loop ligeiramente distintas.
A construção for
percorre uma lista dada de itens — geralmente uma lista de palavras ou quaisquer outros segmentos de texto separados por espaços — executando o mesmo conjunto de comandos em cada um desses itens. Antes de cada iteração, a instrução for
atribui o item atual a uma variável, que pode então ser usada pelos comandos incluídos. O processo é repetido até que não restem mais itens. A sintaxe da construção for
é:
for VARNAME in LIST do COMMANDS done
VARNAME
é um nome de variável arbitrária do shell e LIST
é qualquer sequência de termos separados. Os caracteres delimitadores válidos que dividem os itens na lista são definidos pela variável de ambiente IFS
, que são os caracteres espaço, tabulação e nova linha por padrão. A lista de comandos a serem executados é delimitada pelas instruções do
e done
, de modo que os comandos podem ocupar tantas linhas quantas forem necessárias.
No exemplo a seguir, o comando for
pega cada item da lista fornecida — uma sequência de números — e os atribui à variável NUM
, um de cada vez:
#!/bin/bash for NUM in 1 1 2 3 5 8 13 do echo -n "$NUM is " if [ $(( $NUM % 2 )) -ne 0 ] then echo "odd." else echo "even." fi done
No exemplo, uma construção aninhada if
é usada em conjunto com uma expressão aritmética para avaliar se o número na variável NUM
atual é par ou ímpar. Supondo-se que o script do exemplo anterior se chama script.sh
e está no diretório atual, a seguinte saída será gerada:
$ ./script.sh 1 is odd. 1 is odd. 2 is even. 3 is odd. 5 is odd. 8 is even. 13 is odd.
O Bash também suporta um formato alternativo para construções for
, com a notação de parênteses duplos. Essa notação se assemelha à sintaxe da instrução for
da linguagem de programação C e é particularmente útil para trabalhar com matrizes:
#!/bin/bash SEQ=( 1 1 2 3 5 8 13 ) for (( IDX = 0; IDX < ${#SEQ[*]}; IDX++ )) do echo -n "${SEQ[$IDX]} is " if [ $(( ${SEQ[$IDX]} % 2 )) -ne 0 ] then echo "odd." else echo "even." fi done
Este script gera exatamente a mesma saída do exemplo anterior. No entanto, em vez de usar a variável NUM
para armazenar um item por vez, a variável IDX
é empregada para rastrear o índice da matriz atual em ordem crescente, começando de 0 e continuando a adicionar enquanto esse número permanecer abaixo do número de itens na matriz SEQ
. O item em si é recuperado de sua posição na matriz com ${SEQ[$IDX]}
.
Da mesma forma, a construção until
executa uma sequência de comandos até que um comando de teste — como o próprio comando test
— seja encerrado com o status 0 (sucesso). Por exemplo, a mesma estrutura de loop do exemplo anterior pode ser implementada com until
da seguinte forma:
#!/bin/bash SEQ=( 1 1 2 3 5 8 13 ) IDX=0 until [ $IDX -eq ${#SEQ[*]} ] do echo -n "${SEQ[$IDX]} is " if [ $(( ${SEQ[$IDX]} % 2 )) -ne 0 ] then echo "odd." else echo "even." fi IDX=$(( $IDX + 1 )) done
As construções until
podem exigir mais instruções do que as construções for
, mas podem ser mais adequadas para critérios de parada não-numéricos fornecidos pelas expressões test
ou qualquer outro comando. É importante incluir ações que garantam um critério de parada válido, como o incremento de uma variável de contador, caso contrário o loop pode acabar sendo executado indefinidamente.
A instrução while
é semelhante à instrução until
, mas while
continua repetindo o conjunto de comandos se o comando de teste terminar com o status 0 (sucesso). Portanto, a instrução until [ $IDX -eq ${#SEQ[*]} ]
do exemplo anterior é equivalente a while [ $IDX -lt ${#SEQ[*]} ]
, já que o loop deve ser repetido enquanto o índice da matriz for menor que o total de itens na matriz.
Um exemplo mais elaborado
Imagine que um usuário deseja sincronizar periodicamente uma coleção de seus arquivos e diretórios com outro dispositivo de armazenamento, montado em um ponto de montagem arbitrário no sistema de arquivos, e considera-se que um sistema de backup completo seria um exagero. Como esta é uma atividade que deve ser realizada periodicamente, trata-se de uma aplicação que vale a pena automatizar com um script de shell.
A tarefa é simples: sincronizar todos os arquivos e diretórios contidos em uma lista, de um diretório de origem informado como primeiro argumento do script para um diretório de destino informado como segundo argumento do script. Para ser mais fácil adicionar ou remover itens da lista, ela será mantida em um arquivo separado, ~/.sync.list
, um item por linha:
$ cat ~/.sync.list Documents To do Work Family Album .config .ssh .bash_profile .vimrc
O arquivo contém uma mistura de arquivos e diretórios, alguns deles com espaços em branco em seus nomes. Este é um cenário adequado para o comando interno do Bash mapfile
, que analisa qualquer conteúdo de texto dado e cria uma variável de matriz a partir dele, colocando cada linha como um item de matriz individual. O arquivo do script será denominado sync.sh
e conterá o seguinte script:
#!/bin/bash set -ef # List of items to sync FILE=~/.sync.list # Origin directory FROM=$1 # Destination directory TO=$2 # Check if both directories are valid if [ ! -d "$FROM" -o ! -d "$TO" ] then echo Usage: echo "$0 <SOURCEDIR> <DESTDIR>" exit 1 fi # Create array from file mapfile -t LIST < $FILE # Sync items for (( IDX = 0; IDX < ${#LIST[*]}; IDX++ )) do echo -e "$FROM/${LIST[$IDX]} \u2192 $TO/${LIST[$IDX]}"; rsync -qa --delete "$FROM/${LIST[$IDX]}" "$TO"; done
A primeira ação realizada pelo script é redefinir dois parâmetros do shell com o comando set
: a opção -e
sai da execução imediatamente se um comando resultar em um status diferente de zero e a opção -f
desabilita o globbing do nome do arquivo. Ambas as opções podem ser encurtadas como -ef
. Esta não é uma etapa obrigatória, mas ajuda a diminuir a probabilidade de um comportamento inesperado.
As instruções reais do arquivo de script orientadas para a aplicação podem ser divididas em três partes:
-
Coletar e verificar os parâmetros do script
A variável
FILE
é o caminho para o arquivo que contém a lista de itens a serem copiados:~/.sync.list
. As variáveisFROM
eTO
são os caminhos de origem e destino, respectivamente. Como esses dois últimos parâmetros são fornecidos pelo usuário, eles passam por um teste de validação simples realizado pela construçãoif
: se algum dos dois não for um diretório válido — determinado pelo teste[ ! -d "$FROM" -o ! -d "$TO" ]
— o script mostrará uma breve mensagem de ajuda e se encerrará com um status de saída 1. -
Carregar lista de arquivos e diretórios
Após todos os parâmetros serem definidos, uma matriz contendo a lista de itens a serem copiados é criada com o comando
mapfile -t LIST < $FILE
. A opção-t
domapfile
remove o caractere final de nova linha de cada linha antes de incluí-lo na variável de matriz chamadaLIST
. O conteúdo do arquivo indicado pela variávelFILE
—~/.sync.list
— é lido via redirecionamento de entrada. -
Realizar a cópia e informar o usuário
Um loop
for
usando a notação de parênteses duplo percorre a matriz de itens, com a variávelIDX
controlando o incremento do índice. O comandoecho
informa o usuário sobre cada item que está sendo copiado. O caractere Unicode de escape —\u2192
— para o caractere seta direita está presente na mensagem de saída, por isso opção-e
do comandoecho
deve ser usada. O comandorsync
copia seletivamente apenas as partes modificadas do arquivo de origem, portanto seu uso é recomendado para esse tipo de tarefa. As opções-q
e-a
dorsync
, condensadas em-qa
, inibem as mensagens dorsync
e ativam o modo arquivar, no qual todas as propriedades do arquivo são preservadas. A opção--delete
faz com que orsync
exclua um item do destino que não exista mais na origem e, portanto, deve ser usada com cuidado.
Supondo-se que todos os itens da lista existem no diretório inicial da usuária carol
, /home/carol
, e que o diretório de destino /media/carol/backup
aponta para um dispositivo de armazenamento externo montado, o comando sync.sh /home/carol /media/carol/backup
gera a seguinte saída:
$ sync.sh /home/carol /media/carol/backup /home/carol/Documents → /media/carol/backup/Documents /home/carol/"To do" → /media/carol/backup/"To do" /home/carol/Work → /media/carol/backup/Work /home/carol/"Family Album" → /media/carol/backup/"Family Album" /home/carol/.config → /media/carol/backup/.config /home/carol/.ssh → /media/carol/backup/.ssh /home/carol/.bash_profile → /media/carol/backup/.bash_profile /home/carol/.vimrc → /media/carol/backup/.vimrc
O exemplo também supõe que o script é executado pelo root ou pela usuária carol
, já que a maioria dos arquivos seria ilegível por outros usuários. Se script.sh
não estiver dentro de um diretório listado na variável de ambiente PATH
, ele deve ser especificado com seu caminho completo.
Exercícios Guiados
-
Como o comando
test
pode ser usado para verificar se o caminho do arquivo armazenado na variávelFROM
é mais recente do que um arquivo cujo caminho está armazenado na variávelTO
? -
O script a seguir deveria imprimir uma sequência numérica de 0 a 9 mas, em vez disso, imprime 0 eternamente. O que deve ser feito para se obter a saída esperada?
#!/bin/bash COUNTER=0 while [ $COUNTER -lt 10 ] do echo $COUNTER done
-
Suponha que um usuário escreveu um script que requer uma lista ordenada de nomes de usuário. A lista resultante é apresentada da seguinte forma em seu computador:
carol Dave emma Frank Grace henry
No entanto, a mesma lista é ordenada assim no computador de seu colega:
Dave Frank Grace carol emma henry
O que poderia explicar as diferenças entre as duas listas ordenadas?
Exercícios Exploratórios
-
Como todos os argumentos de linha de comando do script podem ser usados para inicializar uma matriz Bash?
-
Porque é que, contraintuitivamente, o comando
test 1 > 2
é avaliado como verdadeiro? -
Como um usuário poderia alterar temporariamente o separador de campo padrão apenas para o caractere de nova linha, sem deixar de ser capaz de revertê-lo ao conteúdo original?
Resumo
Esta lição discorre mais profundamente sobre os testes disponíveis para o comando test
e outras construções condicionais e de loop, necessárias para escrever scripts de shell mais elaborados. Um script de sincronização de arquivo simples é fornecido como exemplo de aplicação prática para um script de shell. A lição trata dos seguintes tópicos:
-
Testes estendidos para as construções condicionais
if
ecase
. -
Construções de loop do shell:
for
,until
ewhile
. -
Iterações por meio de matrizes e parâmetros.
Os comandos e procedimentos abordados foram:
test
-
Realiza uma comparação entre os itens fornecidos ao comando.
if
-
Uma construção lógica usada em scripts para avaliar algo como verdadeiro ou falso e em seguida lançar a execução do comando com base nos resultados.
case
-
Avalia diversos valores em relação a uma única variável. A execução do comando do script é então realizada dependendo do resultado do comando
case
. for
-
Repete a execução de um comando com base em um determinado critério.
until
-
Repete a execução de um comando até que uma expressão seja avaliada como falsa.
while
-
Repete a execução de um comando enquanto uma dada expressão for avaliada como verdadeira.
Respostas aos Exercícios Guiados
-
Como o comando
test
pode ser usado para verificar se o caminho do arquivo armazenado na variávelFROM
é mais recente do que um arquivo cujo caminho está armazenado na variávelTO
?O comando
test "$FROM" -nt "$TO"
retorna um código de status 0 se o arquivo na variávelFROM
for mais recente que o arquivo na variávelTO
. -
O script a seguir deveria imprimir uma sequência numérica de 0 a 9 mas, em vez disso, imprime 0 eternamente. O que deve ser feito para se obter a saída esperada?
#!/bin/bash COUNTER=0 while [ $COUNTER -lt 10 ] do echo $COUNTER done
A variável
COUNTER
deve ser incrementada, o que pode ser feito com a expressão aritméticaCOUNTER=$(( $COUNTER + 1 ))
, até atingir os critérios de parada e encerrar o loop. -
Suponha que um usuário escreveu um script que requer uma lista ordenada de nomes de usuário. A lista resultante é apresentada da seguinte forma em seu computador:
carol Dave emma Frank Grace henry
No entanto, a mesma lista é ordenada assim no computador de seu colega:
Dave Frank Grace carol emma henry
O que poderia explicar as diferenças entre as duas listas ordenadas?
A classificação baseia-se na localidade (idioma) do sistema atual. Para evitar inconsistências, as tarefas de classificação devem ser realizadas com a variável de ambiente
LANG
definida comoC
.
Respostas aos Exercícios Exploratórios
-
Como todos os argumentos de linha de comando do script podem ser usados para inicializar uma matriz Bash?
Os comandos
PARAMS=( $* )
ouPARAMS=( "$@" )
criam uma matriz chamadaPARAMS
com todos os argumentos. -
Porque é que, contraintuitivamente, o comando
test 1 > 2
é avaliado como verdadeiro?O operador
>
deve ser usado com testes de string, e não testes numéricos. -
Como um usuário poderia alterar temporariamente o separador de campo padrão apenas para o caractere de nova linha, sem deixar de ser capaz de revertê-lo ao conteúdo original?
Uma cópia da variável
IFS
pode ser armazenada em outra variável:OLDIFS=$IFS
. Em seguida o novo separador de linhas é definido comIFS=$'\n'
e a nova variável IFS pode ser revertida comIFS=$OLDIFS
.