105.2 Lição 1
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: |
1 de 2 |
Introdução
O ambiente de shell do Linux permite o uso de arquivos — chamados scripts — contendo comandos de qualquer programa disponível no sistema combinados a comandos internos do shell para automatizar as tarefas de um usuário e/ou do sistema. Com efeito, muitas das tarefas de manutenção do sistema operacional são executadas por scripts que consistem em sequências de comandos, estruturas de decisão e loops condicionais. Embora os scripts sejam na maioria das vezes destinados a tarefas relacionadas ao próprio sistema operacional, eles também são úteis para tarefas orientadas ao usuário, como renomeação em massa de arquivos, coleta e análise de dados ou qualquer outra atividade repetitiva de linha de comando.
Scripts nada mais são do que arquivos de texto que se comportam como programas. Um programa de verdade — o interpretador — lê e executa as instruções listadas no arquivo de script. O interpretador também pode iniciar uma sessão interativa na qual os comandos — incluindo scripts — são lidos e executados à medida que são inseridos, como é o caso das sessões de shell do Linux. Os arquivos de script podem agrupar essas instruções e comandos quando se tornam complexos demais para serem implementados como um alias ou uma função de shell personalizada. Além disso, os arquivos de script podem ser mantidos como programas convencionais e, sendo apenas arquivos de texto, podem ser criados e modificados com qualquer editor de texto simples.
Estrutura e execução de scripts
Basicamente, um arquivo de script é uma seqüência ordenada de comandos que devem ser executados por um interpretador de comandos. Um interpretador pode ler um arquivo de script de vários modos, e há maneiras distintas de fazer isso em uma sessão de shell Bash, mas o interpretador padrão para um arquivo de script será o indicado na primeira linha do script, logo após os caracteres #!
(conhecidos como shebang). Em um script com instruções para o shell Bash, a primeira linha deve ser #!/bin/bash
. Quando essa linha é indicada, o interpretador de todas as instruções do arquivo será /bin/bash
. Tirando a primeira linha, todas as outras linhas que começam com o caractere hash #
serão ignoradas e, assim, podem ser usadas para lembretes e comentários. As linhas em branco também são ignoradas. Um arquivo muito conciso de script do shell pode, portanto, ser escrito da seguinte maneira:
#!/bin/bash # A very simple script echo "Cheers from the script file! Current time is: " date +%H:%M
Este script tem apenas duas instruções para o interpretador /bin/bash
: o comando interno echo
e o comando date
. A maneira mais básica de rodar um arquivo de script é executar o interpretador com o caminho do script como argumento. Assim, supondo que o exemplo anterior foi salvo em um arquivo de script chamado script.sh
no diretório atual, ele será lido e interpretado pelo Bash com o seguinte comando:
$ bash script.sh Cheers from the script file! Current time is: 10:57
O comando echo
adiciona automaticamente uma nova linha após exibir o conteúdo, mas a opção -n
suprime esse comportamento. Portanto, usar echo -n
no script fará com que a saída de ambos os comandos apareça na mesma linha:
$ bash script.sh Cheers from the script file! Current time is: 10:57
Embora não seja obrigatório, o sufixo .sh
ajuda a identificar os scripts do shell ao listar e pesquisar arquivos.
Tip
|
O Bash chama qualquer comando indicado após o |
Se o arquivo de script se destina a ser executado por outros usuários do sistema, é importante verificar se as permissões de leitura adequadas estão definidas. O comando chmod o+r script.sh
concede permissão de leitura a todos os usuários do sistema, permitindo-lhes executar script.sh
com o caminho para o arquivo de script como o argumento do comando bash
. Como alternativa, o arquivo de script pode ter a permissão do bit de execução definida para que o arquivo possa ser executado como um comando convencional. O bit de execução é ativado no arquivo de script com o comando chmod
:
$ chmod +x script.sh
Com o bit de execução habilitado, o arquivo de script chamado script.sh
no diretório atual pode ser executado diretamente com o comando ./script.sh
. Os scripts colocados em um diretório listado na variável de ambiente PATH
também estarão acessíveis sem seu caminho completo.
Warning
|
Um script que executa ações restritas pode ter sua permissão SUID ativada e, portanto, os usuários comuns também podem executar o script com privilégios de root. Nesse caso, é muito importante garantir que nenhum usuário além do root tenha permissão para escrever no arquivo. Caso contrário, um usuário comum poderá modificar o arquivo para realizar operações arbitrárias e potencialmente prejudiciais. |
O posicionamento e o recuo dos comandos em arquivos de script não são muito rígidos. Cada linha de um script de shell será executada como um comando de shell comum, na mesma sequência em que a linha aparece no arquivo de script, e as mesmas regras que se aplicam ao prompt do shell também se aplicam a cada linha de script individualmente. É possível colocar dois ou mais comandos na mesma linha, separados por ponto e vírgula:
echo "Cheers from the script file! Current time is:" ; date +%H:%M
Embora esse formato possa ser conveniente às vezes, seu uso é opcional, pois os comandos sequenciais podem ser postos em linhas separadas, sendo executados exatamente como se estivessem separados por ponto-e-vírgula. Em outras palavras, o ponto-e-vírgula pode ser substituído por um caractere de nova linha nos arquivos de script do Bash.
Quando um script é executado, os comandos nele contidos não são executados diretamente na sessão atual, mas sim por um novo processo do Bash, chamado sub-shell. Isso evita que o script sobrescreva as variáveis de ambiente da sessão atual e faça modificações indesejadas nela. Se o objetivo é executar o conteúdo do script na sessão atual do shell, ele deve ser executado com source script.sh
ou . script.sh
(note que há um espaço entre o ponto e o nome do script).
Assim como acontece com a execução de qualquer outro comando, o prompt do shell só estará disponível novamente quando o script finalizar sua execução e seu código de status de saída estiver disponível na variável $?
. Para mudar esse comportamento, de forma que o shell atual também se encerre quando o script for concluído, o script — ou qualquer outro comando — pode ser precedido pelo comando exec
. Esse comando também substitui o código de status de saída da sessão do shell atual pelo seu próprio.
Variáveis
As variáveis nos scripts de shell se comportam da mesma maneira que nas sessões interativas, visto que o interpretador é o mesmo. Por exemplo, o formato SOLUTION=42
(sem espaços ao redor do sinal de igual) atribuirá o valor 42
à variável de nome SOLUTION
. Por convenção, letras maiúsculas são usadas para nomes de variáveis, mas não é obrigatório. Os nomes das variáveis não podem, entretanto, começar com caracteres não alfabéticos.
Além das variáveis comuns criadas pelo usuário, os scripts do Bash também possuem um conjunto de variáveis especiais chamadas parâmetros. Ao contrário das variáveis comuns, os nomes dos parâmetros começam com um caractere não alfabético que designa sua função. Os argumentos passados para um script e outras informações úteis são armazenados em parâmetros como $0
, $*
, $?
etc., onde o caractere após o cifrão indica a informação a ser obtida:
$*
-
Todos os argumentos passados para o script.
$@
-
Todos os argumentos passados para o script. Se usado com aspas duplas, como em
"$@"
, todos os argumentos serão colocados entre aspas duplas. $#
-
O número de argumentos.
$0
-
O nome do arquivo de script.
$!
-
PID do último programa executado.
$$
-
PID do shell atual.
$?
-
Código de status de saída numérico do último comando concluído. Para processos POSIX padrão, um valor numérico de
0
indica que o último comando foi executado com sucesso, o que também se aplica a scripts do shell.
Um parâmetro posicional é um parâmetro denotado por um ou mais dígitos diferentes do dígito único 0
. Por exemplo, a variável $1
corresponde ao primeiro argumento dado ao script (parâmetro posicional um), $2
corresponde ao segundo argumento e assim por diante. Se a posição de um parâmetro for maior que nove, ele deve ser referenciado com chaves, como em ${10}
, ${11}
etc.
As variáveis comuns, por outro lado, têm como função armazenar valores inseridos manualmente ou a saída gerada por outros comandos. O comando read
, por exemplo, pode ser usado dentro do script para solicitar informações ao usuário durante a execução do script:
echo "Do you want to continue (y/n)?" read ANSWER
O valor retornado será armazenado na variável ANSWER
. Se o nome da variável não for fornecido, o nome da variável REPLY
será usado por padrão. Também é possível usar o comando read
para ler mais de uma variável simultaneamente:
echo "Type your first name and last name:" read NAME SURNAME
Neste caso, cada termo separado por espaços será atribuído às variáveis NAME
e SURNAME
respectivamente. Se o número de termos dados for maior que o número de variáveis, os termos excedentes serão armazenados na última variável. O próprio read
pode exibir a mensagem para o usuário com a opção -p
, tornando o comando echo
redundante nesse caso:
read -p "Type your first name and last name:" NAME SURNAME
Os scripts que executam tarefas do sistema geralmente requerem informações fornecidas por outros programas. A notação crase (backtick) pode ser usada para armazenar a saída de um comando em uma variável:
$ OS=`uname -o`
No exemplo, a saída do comando uname -o
será armazenada na variável OS
. Um resultado idêntico será produzido com $()
:
$ OS=$(uname -o)
O comprimento de uma variável, ou seja, a quantidade de caracteres que ela contém, é retornado acrescentando-se um hash #
antes do nome da variável. Esse recurso, no entanto, requer o uso da sintaxe das chaves para indicar a variável:
$ OS=$(uname -o) $ echo $OS GNU/Linux $ echo ${#OS} 9
O Bash também apresenta variáveis de matriz (array) unidimensionais, de forma que um conjunto de elementos relacionados pode ser armazenado com um único nome de variável. Cada elemento de uma matriz possui um índice numérico, que deve ser usado para escrever e ler valores no elemento correspondente. Ao contrário das variáveis comuns, as matrizes devem ser declaradas com o comando interno do Bash declare
. Por exemplo, para declarar uma variável chamada SIZES
como uma matriz:
$ declare -a SIZES
As matrizes também podem ser declaradas implicitamente quando preenchidas a partir de uma lista predefinida de itens, usando a notação de parênteses:
$ SIZES=( 1048576 1073741824 )
No exemplo, os dois grandes valores inteiros foram armazenados na matriz SIZES
. Os elementos da matriz devem ser referenciados usando chaves e colchetes, caso contrário o Bash não alterará nem exibirá o elemento corretamente. Como os índices da matriz começam em 0, o conteúdo do primeiro elemento está em ${SIZES[0]}
, o segundo em ${SIZES[1]}
e assim por diante:
$ echo ${SIZES[0]} 1048576 $ echo ${SIZES[1]} 1073741824
Diferente da leitura, a alteração do conteúdo de um elemento da matriz é realizada sem as chaves (por exemplo, SIZES[0]=1048576
). Como no caso das variáveis comuns, o comprimento de um elemento em uma matriz é retornado com o caractere hash (por exemplo, ${#SIZES[0]}
para o comprimento do primeiro elemento da matriz SIZES
). O número total de elementos em uma matriz é retornado se @
ou *
forem usados como o índice:
$ echo ${#SIZES[@]} 2 $ echo ${#SIZES[*]} 2
As matrizes também podem ser declaradas usando-se, como elementos iniciais, a saída de um comando, por meio da substituição de comando. O exemplo a seguir mostra como criar uma matriz do Bash cujos elementos são os sistemas de arquivos suportados pelo sistema atual:
$ FS=( $(cut -f 2 < /proc/filesystems) )
O comando cut -f 2 < /proc/filesystems
exibe todos os sistemas de arquivos atualmente suportados pelo kernel em execução (listados na segunda coluna do arquivo /proc/filesystems
), de forma que a matriz FS
agora contém um elemento para cada sistema de arquivos suportado. Qualquer conteúdo de texto pode ser usado para inicializar uma matriz, já que, por padrão, quaisquer termos delimidados por caracteres de espaço, tabulação ou nova linha se tornarão um elemento de matriz.
Tip
|
O Bash trata cada caractere do |
Expressões aritméticas
O Bash oferece um método prático para realizar operações aritméticas de números inteiros com o comando interno expr
. Duas variáveis numéricas, $VAL1
e $VAL2
, por exemplo, podem ser adicionadas junto com o seguinte comando:
$ SUM=`expr $VAL1 + $VAL2`
O valor resultante do exemplo estará disponível na variável $SUM
. O comando expr
pode ser substituído por $(())
, de forma que o exemplo anterior pode ser reescrito como SUM=$(( $VAL1 + $VAL2 ))
. Expressões com potenciação são igualmente permitidas com o operador de duplo asterisco, de forma que a declaração de matriz anterior SIZES=( 1048576 1073741824)
poderia ser reescrita como SIZES=( $((1024**2)) $((1024**3)) )
.
A substituição de comandos também pode ser usada em expressões aritméticas. Por exemplo, o arquivo /proc/meminfo
contém informações detalhadas sobre a memória do sistema, incluindo o número de bytes livres na RAM:
$ FREE=$(( 1000 * `sed -nre '2s/[^[:digit:]]//gp' < /proc/meminfo` ))
O exemplo mostra como o comando sed
pode ser usado para analisar o conteúdo de /proc/meminfo
dentro da expressão aritmética. A segunda linha do arquivo /proc/meminfo
contém a quantidade de memória livre em milhares de bytes, então a expressão aritmética multiplica esse valor por 1000 para obter o número de bytes livres na RAM.
Execução condicional
Alguns scripts geralmente não se destinam a executar todos os comandos no arquivo de script, mas apenas aqueles que correspondem a critérios predefinidos. Por exemplo, um script de manutenção pode enviar uma mensagem de aviso ao email do administrador somente se a execução de um comando falhar. O Bash fornece métodos específicos para avaliar o sucesso da execução de comandos, além de estruturas condicionais gerais, mais semelhantes às encontradas nas linguagens de programação populares.
Ao separar os comandos com &&
, o comando à direita será executado apenas se o comando à esquerda não encontrar um erro, ou seja, se seu status de saída for igual a 0
:
COMMAND A && COMMAND B && COMMAND C
O comportamento oposto ocorre se os comandos estiverem separados por ||
. Nesse caso, o comando a seguir será executado apenas se o comando anterior encontrar um erro, ou seja, se seu código de status de retorno for diferente de 0.
Um dos recursos mais importantes de todas as linguagens de programação é a capacidade de executar comandos dependendo de condições previamente definidas. A maneira mais direta de executar comandos condicionalmente é usar o comando interno do Bash if
, que executa um ou mais comandos somente se o comando fornecido como argumento retornar um código de status 0 (sucesso). Outro comando, test
, pode ser usado para avaliar diversos critérios especiais diferentes, sendo assim usado principalmente em conjunto com if
. No exemplo a seguir, a mensagem Confirmed: /bin/bash is executable.
será exibida se o arquivo /bin/bash
existir e for executável:
if test -x /bin/bash ; then echo "Confirmed: /bin/bash is executable." fi
A opção -x
faz com que o comando test
retorne um código de status 0 apenas se o caminho fornecido for um arquivo executável. O exemplo a seguir mostra outra maneira de obter exatamente o mesmo resultado, já que os colchetes podem ser usados como substitutos para test
:
if [ -x /bin/bash ] ; then echo "Confirmed: /bin/bash is executable." fi
A instrução else
é opcional para a estrutura if
e pode, se presente, definir um comando ou sequência de comandos a serem executados se a expressão condicional não for verdadeira:
if [ -x /bin/bash ] ; then echo "Confirmed: /bin/bash is executable." else echo "No, /bin/bash is not executable." fi
As estruturas if
sempre devem terminar com fi
, de forma que o interpretador Bash saiba onde os comandos condicionais terminam.
Saída do script
Mesmo quando a finalidade de um script envolve apenas operações orientadas a arquivos, é importante exibir mensagens relacionadas ao progresso na saída padrão, para que o usuário seja informado sobre quaisquer problemas e possa, eventualmente, usar essas mensagens para gerar logs de operação.
O comando interno do Bash echo
é comumente usado para exibir strings de texto simples, mas ele também oferece alguns recursos estendidos. Com a opção -e
, o comando echo
é capaz de exibir caracteres especiais usando sequências de escape (uma sequência de barra invertida designando um caractere especial). Por exemplo:
#!/bin/bash # Get the operating system's generic name OS=$(uname -o) # Get the amount of free memory in bytes FREE=$(( 1000 * `sed -nre '2s/[^[:digit:]]//gp' < /proc/meminfo` )) echo -e "Operating system:\t$OS" echo -e "Unallocated RAM:\t$(( $FREE / 1024**2 )) MB"
Embora o uso de aspas seja opcional ao se usar echo
sem opções, é necessário adicioná-las ao incluir a opção -e
; caso contrário, os caracteres especiais podem não ser lidos corretamente. No script anterior, ambos os comandos echo
usam o caractere de tabulação \t
para alinhar o texto, resultando na seguinte saída:
Operating system: GNU/Linux Unallocated RAM: 1491 MB
O caractere de nova linha \n
pode ser usado para separar as linhas da saída, de forma que exatamente a mesma saída é obtida combinando-se os dois comandos echo
em um só:
echo -e "Operating system:\t$OS\nUnallocated RAM:\t$(( $FREE / 1024**2 )) MB"
Embora adequado para exibir a maioria das mensagens de texto, o comando echo
pode não ser o melhor para padrões de texto mais específicos. O comando interno do Bash printf
oferece mais controle sobre a exibição das variáveis. O comando printf
usa o primeiro argumento como formato da saída, onde os marcadores serão substituídos pelos argumentos seguintes na ordem em que aparecem na linha de comando. Assim, a mensagem do exemplo anterior poderia ser gerada com o seguinte comando printf
:
printf "Operating system:\t%s\nUnallocated RAM:\t%d MB\n" $OS $(( $FREE / 1024**2 ))
O espaço reservado %s
destina-se ao conteúdo de texto (será substituído pela variável $OS
) e o espaço reservado %d
destina-se a números inteiros (será substituído pelo número resultante de megabytes livres na RAM). O printf
não acrescenta um caractere de nova linha no final do texto, então o caractere de nova linha \n
deve ser posto ao fim do padrão, se necessário. Todo o padrão deve ser interpretado como um único argumento e, portanto, deve ser posto entre aspas.
Tip
|
O formato de substituição do espaço reservado realizada por |
Com printf
, as variáveis são postas fora do padrão de texto, o que torna possível armazenar o padrão de texto em uma variável separada:
MSG='Operating system:\t%s\nUnallocated RAM:\t%d MB\n' printf "$MSG" $OS $(( $FREE / 1024**2 ))
Este método é particularmente útil para exibir formatos de saída distintos, dependendo dos requisitos do usuário. Fica mais fácil, por exemplo, produzir um script que use um padrão de texto distinto se o usuário precisar de uma lista CSV (valores separados por vírgula) em vez de uma mensagem de saída padrão.
Exercícios Guiados
-
A opção
-s
para o comandoread
é útil para inserir senhas, pois não mostra o conteúdo que está sendo digitado na tela. Como o comandoread
pode ser usado para armazenar os dados inseridos pelo usuário na variávelPASSWORD
enquanto oculta o conteúdo digitado? -
A única finalidade do comando
whoami
é exibir o nome do usuário que o chamou, de modo que ele é usado principalmente dentro de scripts para identificar o usuário que o está executando. Dentro de um script Bash, como a saída do comandowhoami
pode ser armazenada na variável chamadaWHO
? -
Qual operador do Bash deve estar entre os comandos
apt-get dist-upgrade
esystemctl reboot
se o usuário root quiser executarsystemctl reboot
apenas seapt-get dist-upgrade
for concluído com sucesso?
Exercícios Exploratórios
-
Depois de tentar executar um script Bash recém-criado, um usuário recebe a seguinte mensagem de erro:
bash: ./script.sh: Permission denied
Considerando que o arquivo
./script.sh
foi criado pelo mesmo usuário, qual seria a provável causa desse erro? -
Suponha que um arquivo de script chamado
do.sh
seja executável e o link simbólicoundo.sh
aponte para ele. De dentro do script, como seria possível identificar se o nome do arquivo de chamada erado.sh
ouundo.sh
? -
Em um sistema com um serviço de email configurado corretamente, o comando
mail -s "Maintenance Error" root <<<"Scheduled task error"
envia o email de aviso ao usuário root. Esse comando pode ser usado em tarefas autônomas, como cronjobs, para informar o administrador do sistema sobre um problema inesperado. Escreva uma instrução com if para executar o comandomail
acima mencionado somente se o status de saída do comando anterior — seja lá qual for — não for bem-sucedido.
Resumo
Esta lição cobre os conceitos básicos para compreender e escrever scripts de shell Bash. Os scripts de shell são uma parte essencial de qualquer distribuição Linux, pois oferecem uma maneira muito flexível de automatizar as tarefas do usuário e do sistema executadas no ambiente do shell. A lição trata dos seguintes tópicos:
-
Estrutura dos scripts de shell e permissões corretas dos arquivos de script
-
Parâmetros de script
-
Uso de variáveis para ler as informações fornecidas pelo usuário e para armazenar a saída dos comandos
-
Matrizes do Bash
-
Testes simples e execução condicional
-
Formatação da saída
Os comandos e procedimentos abordados foram:
-
Notação interna do Bash para substituição de comandos, expansão de matrizes e expressões aritméticas
-
Execução condicional de comandos com os operadores
||
e&&
-
echo
-
chmod
-
exec
-
read
-
declare
-
test
-
if
-
printf
Respostas aos Exercícios Guiados
-
A opção
-s
para o comandoread
é útil para inserir senhas, pois não mostra o conteúdo que está sendo digitado na tela. Como o comandoread
pode ser usado para armazenar os dados inseridos pelo usuário na variávelPASSWORD
enquanto oculta o conteúdo digitado?read -s PASSWORD
-
A única finalidade do comando
whoami
é exibir o nome do usuário que o chamou, de modo que ele é usado principalmente dentro de scripts para identificar o usuário que o está executando. Dentro de um script Bash, como a saída do comandowhoami
pode ser armazenada na variável chamadaWHO
?WHO=`whoami`
ouWHO=$(whoami)
-
Qual operador do Bash deve estar entre os comandos
apt-get dist-upgrade
esystemctl reboot
se o usuário root quiser executarsystemctl reboot
apenas seapt-get dist-upgrade
for concluído com sucesso?O operador
&&
, como emapt-get dist-upgrade && systemctl reboot
.
Respostas aos Exercícios Exploratórios
-
Depois de tentar executar um script Bash recém-criado, um usuário recebe a seguinte mensagem de erro:
bash: ./script.sh: Permission denied
Considerando que o arquivo
./script.sh
foi criado pelo mesmo usuário, qual seria a provável causa desse erro?O arquivo
./script.sh
não está com a permissão de execução habilitada. -
Suponha que um arquivo de script chamado
do.sh
seja executável e o link simbólicoundo.sh
aponte para ele. De dentro do script, como seria possível identificar se o nome do arquivo de chamada erado.sh
ouundo.sh
?A variável especial
$0
contém o nome de arquivo usado para chamar o script. -
Em um sistema com um serviço de email configurado corretamente, o comando
mail -s "Maintenance Error" root <<<"Scheduled task error"
envia o email de aviso ao usuário root. Esse comando pode ser usado em tarefas autônomas, como cronjobs, para informar o administrador do sistema sobre um problema inesperado. Escreva uma instrução com if para executar o comandomail
acima mencionado somente se o status de saída do comando anterior — seja lá qual for — não for bem-sucedido.if [ "$?" -ne 0 ]; then mail -s "Maintenance Error" root <<<"Scheduled task error"; fi