Shell script é uma linguagem de script, ou seja, assim como JavaScript, PHP e Python, é uma linguagem de programação executada do interior de programas e/ou de outras linguagens de programação, não se restringindo a esses ambientes. É usada em vários sistemas operacionais, com diferentes dialetos, dependendo do interpretador de comandos utilizado. Esse interpretador de comandos é uma espécie de tradutor entre o sistema operacional e o usuário, normalmente conhecido como shell. Um exemplo de interpretador de comandos é o bash (GNU Bourne-Again SHell), usado na grande maioria das distribuições GNU/Linux, porém há versões para o Windows (como o do projeto Cygwin).
Caso não tenha muita experiência em Linux, leia também os artigos sobre Linux, Terminal de texto e Permissões de arquivos.
O bash permite a execução de sequências de comandos direto no prompt do sistema ou escritas em arquivos de texto, conhecidos como shell scripts. Uma variável é onde o shell armazena determinados valores para utilização posterior. Sua definição é realizada da seguinte forma:
$ site=MonolitoNimbus
Para utilizar o valor de uma variável, é só colocar um sinal de “$” seguido do nome da variável. Veja como ficaria para imprimir na tela o valor da variável “site”:
$ echo $site
Esses dois comandos podem ser guardados em um arquivo de texto (script) para ser executado de uma só vez, na sequência. A primeira linha de todo shell script deve começar com algo do tipo “#!/bin/bash”, a qual indica com qual shell deverá ser executado o script. Devemos escrever a rotina usando um editor de texto simples, e salvaremos o arquivo com o nome “teste.sh”. Assim, o conteúdo do nosso arquivo seria – caso os comandos estivessem em uma mesma linha, deveria ter um ponto e vírgula no final do comando da primeira linha para separá-lo do comando da segunda linha:
#!/bin/bash site=MonolitoNimbus echo $site
Antes de executar o arquivo, devemos mudar sua permissão de execução usando o comando chmod (veja artigo sobre permissões de arquivos). Assim, basta executar a rotina digitando “./teste.sh” – o ponto indica que a rotina está no mesmo diretório onde você executou o comando; caso não esteja, basta escrever o endereço completo em vez do ponto.
Caso queira digitar o conteúdo da variável na hora da execução da rotina, troque “$site” por $1″, salve o arquivo e execute novamente incluindo o texto a ser impresso depois do nome do arquivo, separado por espaço. Quando o bash lê a variável “$1”, ele a substitui pelo primeiro parâmetro passado na linha de comando para o nosso script.
Caso esteja executando um shell script e alterar um trecho dele que ainda não tenha sido executado, esse trecho novo será executado da mesma forma. Ele é executado linha por linha, ou seja, se der erro em uma linha, ele vai para a seguinte e não aborta todo o script.
Quando executamos um programa, o shell fica esperando o mesmo terminar para depois nos devolver a linha de comando. Para executar um programa e retornar a linha de comando sem ficar esperando programa terminar sua execução, basta colocar um “&” (e comercial) no final da linha de comando que executará a rotina – ou executar como “bg nome_ou_pid_da_rotina”. Ou seja, o programa deixou de rodar em primeiro plano (fg – foreground) e foi rodar em segundo plano (bg – background). Podemos fazer isso também durante a execução do programa, digitando Ctrl+z; para passar a tarefa novamente ao foreground, execute o comando “fg” (ou “fg nome_ou_pid_da_tarefa). É possível listar as tarefas ativas usando “jobs -l”.
Redirecionadores de entrada e saída
Toda rotina/programa executado tem uma saída de dados, cujo padrão é que os resultados (STDOUT, saída 1) e os erros (STDERR, saída 2) serem mostrados na tela. Para isso, utiliza-se o sinal “>” (maior que), como se fosse uma seta indicando o fluxo da informação. Se estiver direcionando a saída para um arquivo e for só um sinal, ele cria/substitui o conteúdo do arquivo, e se forem dois (“>>”), a informação será incluída no fim do arquivo existente. O mesmo vale para o redirecionamento da entrada (STDIN), cujo sinal é “<" (menor que) e para o redirecionamento da saída de erros ("2>” ou “2>>”). Veja esses exemplos:
$ comando > saida.txt #redirecionar saída para arquivo $ cat teste1.txt teste2.txt > teste #copiar o conteúdo dos arquivos teste1.txt e teste2.txt para o arquivo teste $ cat teste3.txt >> teste #adiciona o conteúdo do arquivo teste3.txt no final do arquivo teste $ comando 2>&1 #Redirecionar stderr para stdout $ comando 2>&1 > saida.txt #redirecionar stderr para stdout e então para arquivo $ comando &> arquivo.txt #redirecionar stderr e stdout direto para arquivo $ comando 1> saida.log1 2> saida.log2 #redirecionar stderr e stdout para arquivos separados
O sinal “|” (pipe) redireciona a saída de um comando para a entrada de um outro comando. Abaixo temos três comandos aninhados: primeiro, o comando cat exibe o conteúdo do arquivo teste.txt; segundo, o resultado do comando cat é usado como entrada do comando wc que conta o número de linhas do arquivo; e terceiro, o número de linhas do arquivo teste.txt é gravado no arquivo resultado.txt.
$ cat teste.txt | wc -l > resultado.txt
Um pipe nomeado (também chamado de named pipe ou FIFO, acrônimo de ‘First In First Out’) é uma extensão do conceito de encadeamento do sistema Unix e dos seus métodos de comunicação entre processos. Ao contrário do pipe convencional, ele é criado explicitamente utilizando-se os comandos mkfifo ou mknod (no caso de sistemas mais antigos): “mkfifo pipe1” (por exemplo). Nesse exemplo, abra dois terminais: em um deles, é executado o comando “ls -l” com a saída direcionada para o “pipe1” (ª linha), permanecendo em “wait”; no segundo terminal, é executado o comando da segunda linha, onde o “cat” recebe o pipe gerado anteriormente:
$ ls -l > pipe1 $ cat < pipe1
Os arquivos tipo named pipes são exibidos pelo comando ls como qualquer outro, com poucas diferenças: antes das permissões, aparece a letra ‘p’. Como um ‘named pipe’ é um arquivo no sistema, ele estará disponível após a vida dos processos. A comunicação do FIFO é dita half-duplex, o que significa que os dois processos utilizam o mesmo canal de comunicação, porém um processo somente lê ou escreve, nunca os dois ao mesmo tempo. O kernel garante que todos os dados serão sincronizados – a ordem em que eles são enviados será a ordem em que eles serão lidos. Uma aplicação muito útil dos ‘named pipes’ é permitir que programas sem nenhuma relação possam se comunicar entre si.
A crase (ou “backtick” em inglês) funciona da seguinte forma: tudo o que você digita entre crases é executado pelo shell antes do comando principal, e a saída da execução é usada pelo restante do comando exatamente como se você tivesse digitado a saída naquele lugar da linha de comando. Outra forma de realizar essa tarefa é através de parênteses com cifrão na frente: $(comando). Veja como extrair o conteúdo de uma célula de uma tabela gravada em arquivo CSV (colunas separadas por vírgulas) usando o que foi visto até aqui e outro dois comandos a serem melhor explicados mais adiante.
var=$(cat dados.csv | sed -n `echo $linha`p | awk -F "," '{print $1}')
O exemplo acima lê o arquivo “dados.csv”, redireciona sua saída para o comando SED (para selecionar uma determinada linha), que por sua vez redireciona para o comando AWK (para selecionar a coluna 1). Note o uso da crase para imprimir a variável “linha” como argumento do comando SED, e depois o uso do parênteses para definir a saída de toda a linha de comandos como a variável “var”.
Uma notação mais indicada para esse caso é o subshell, que equivale usar o conteúdo a ser executado primeiro entre dois parênteses e um cifrão na frente. Veja o seguinte exemplo:
parent=$(dirname $PWD)
Nesse caso, a variável “parent” recebe a execução do comando “dirname” sobre o diretório corrente (pwd), retornando o nome do diretório um nível acima do atual.
Codificação de caracteres
Às vezes acontece de abrir um arquivo e aparecerem uns caracteres estranhos devido uma diferença entre a codificação em que foi escrito e a de visualização do sistema. A codificação de caracteres de um sistema é um padrão entre um conjunto de caracteres e bits. No Brasil, as mais utilizadas são UTF-8 (preferencialmente) e iso-8859-1 (iso-latin1).
Segue um comando que permite ver a codificação de um ou mais arquivos e outro para converter a codificação:
$ file *.txt --mime-encoding #ver codificação de caracteres de todos os arquivos .txt da pasta $ iconv -f utf-8 -t iso-8859-1 arquivo > novo_arquivo #converter codificação de caracteres de UTF-8 pra ISO-8859-1
Para descobrir a codificação do sistema, utilize o comando “locale”. Caso queira escrever um script em codificação diferente, defina LANG=pt_BR.UTF-8 (por exemplo) no início e escolha no menu do terminal (ou do programa em que estiver escrevendo) a opção “Definir codificação de caractere” e escolha sua opção.
Edição de arquivos (dos2unix, grep e diff)
Os arquivos podem ser escritos em diferentes codificações, conforme o idioma ou sistema operacional. Por exemplo, se editar no Windows um arquivo do Linux, o caractere de quebra de linha é diferente. O Linux adota o LF (line feed), enquanto que o Windows adota a combinação CRLF (onde CR é carriage return). Para converter o arquivo de texto que passou pelo Windows e substituir essa quebra de linha no Linux, existe o comando “dos2unix nome_do_arquivo”, e o inverso é usando o comando “unix2dos”. Para obter informações do arquivo, use o parâmetro “-i”, conforme o exemplo a seguir:
$ dos2unix -i exemplo.txt 0 14240 0 no_bom text exemplo.txt
As colunas apresentam (respectivamente) o número de quebras de linha do DOS, do Unix e do Mac, além da marca de ordem de byte, “text” ou “binary” e o nome de arquivo. Observação: somente com o lançamento da versão de “2014-09-11” que apareceu a flag “-i”. Se não tiver essa opção, atualize usando “sudo apt install –only-upgrade dos2unix”.
Para fazer a conversão em vários arquivos ao mesmo tempo, usar uma combinação de comandos como essa:
find . -type f -print0 | xargs -0 dos2unix
O comando diff permite comparar o conteúdo de dois arquivos, exibindo apenas as diferenças entre eles. Linhas precedidas pelo sinal de < indicam que o conteúdo existe somente em arquivo1. Linhas precedidas pelo sinal de > indicam que o conteúdo existe somente em arquivo2. A sintaxe é:
$ diff arquivo1.txt arquivo2.txt
O comando grep seleciona somente as linhas que contenham o texto determinado pelo usuário. Veja esse exemplo, ele redireciona para o arquivo arq_destino.ext as linhas do arquivo nome_arquivo.ext que contem as strings “termo 1” e “termo 2”:
$ grep "termo 1" nome_arquivo.ext | grep "termo 2" > arq_destino.ext
Nesse outro exemplo do grep, são utilizadas expressões regulares. O comando a seguir pesquisa no arquivo ‘myfile’ todas as ocorrências da string com a restrição utilizada como parâmetro. A ocorrência deve conter pelo menos cinco caracteres, onde o caractere de número 2 e 5 são ‘a’ e o caractere de número 3 não é ‘b’:
grep .a[^b].a myfile
Cada ponto indica um caractere qualquer (um antes da letra ‘a’, para indicar que essa letra será a segunda, e outro antes do outro ‘a’ para indicar que esse será o quinto caractere). Os colchetes indicam uma lista de caracteres para serem pesquisados (por exemplo, [Mm] inclui as letras ‘m’ maiúscula e minúscula e [0-9] indica para buscar caracteres entre 0 e 9). No entanto, no exemplo existe um acento circunflexo, indicando uma negação de lista: pode ser qualquer coisa EXCETO o que vier depois do sinal gráfico. Se fosse só o acento circunflexo (por exemplo ^d), ele indica para pegar somente as ocorrência que iniciem com d – um cifrão no final, como em ‘foo$’, indica para buscar linhas que terminem com ‘foo’. Ainda se existisse um número (ou dois, separados por vírgula) dentro de de chaves, como em {1,3}, isso indica a quantidade de dígitos que os números devem ter na pesquisa, ou seja, de 1 a 3 dígitos. Mais exemplos do uso de expressões regulares no grep podem ser vistas clicando no link e no site guru99.
O comando “cut” recorta trechos conforme sua posição ou separador comum – veja mais no artigo “exemplos do comando ‘cut’“.
AWK, SED, find e XARGS
O AWK é uma linguagem de programação interpretada que é, geralmente, usada para processar dados nos textos e operações com arquivos em conjunto com o shell script. Por exemplo, para imprimir na tela a mensagem “Monolito Nimbus”, o comando é o seguinte:
$ awk '{ print "Monolito Nimbus" }'
Outro exemplo: caso queira selecionar somente as colunas 5 e 4 do arquivo nome_do_arquivo.txt e gravar em novo_arquivo.txt (nessa ordem) e alterar o delimitador de leitura alterado para ponto e virgula (o padrão é espaço), temos o seguinte comando:
$ awk -F';' '{ print $5,$4}' nome_do_arquivo.txt > novo_arquivo.txt
Veja mais sobre awk no Viva o Linux.
O comando SED tem como objetivo substituir e “casar” padrões, sempre por meio de Expressões Regulares. O SED, assim como o AWK, lê um arquivo, linha por linha, e aplica a expressão do parâmetro a cada uma delas. Sua sintaxe é s/ER/sub/[modificadores] (veja exemplos do comando SED no Blog do Beraldo). Caso queira substituir a primeira ocorrência “sed” por “awk” no texto abaixo, veja como ficará o comando:
$ echo "Tutorial sobre sed: aprenda a trabalhar com sed" | sed 's/sed/awk'
Para apagar linhas inteiras, “1d” corresponde ao nº da linha q será eliminada; “1,2d” para eliminar as duas primeiras linhas. Se quiser remover linhas que contenham um termo, pode usar a estrutura “/palavras que a linha deve conter/d”. Veja um exemplo de cada caso:
$ sed -i 1d nome_do_arquivo.txt $ sed -i '/exemplo/d' .bash_history
Também é possível realizar uma busca de arquivos usando o comando find e redirecionar sua saída para o comando SED. Veja esse exemplo para encontrar todos os arquivos “.php” do diretório atual (por isso o ponto) e substituir (s) 192.168.1.148 por monolitonimbus.com.br em todas as ocorrências (g):
$ find . -iname "*.php" | xargs sed -i 's/192.168.1.148/monolitonimbus.com.br/g' $ find . -iname "tar_*" | xargs sed -i 's/\./,/g' # outro exemplo, substitui ponto por virgula em todos os arquivos que começam com "tar_"
Uma alternativa ao SED é o comando XARGS, cujo objetivo é facilitar a repetição de um certo comando para cada entrada fornecida para ele. Veja um exemplo para substituir a ocorrência “viniroger” por “monolitonimbus” em vários documentos:
$ find . * -print | xargs perl -pi -e 's/viniroger/monolitonimbus/ig' *
Caso a substituição envolva renomear arquivos, utilize a opção “rename”, conforme segue:
$ find . -name "*.png" -print0 | xargs -0 rename "s/viniroger/monolitonimbus/"
Clique na imagem para abrir/baixar o arquivo com um resumo para consulta dos principais comandos e parâmetros – apêndice do livro Shell Script Profissional:
Canivete Suíço do Shell (Bash)
Leia também as continuações desse post para aprender mais:
Visitando a tag Shell Script é possível ver alguns exemplos de rotinas e alguns “truques”, além de outros assuntos ligados ao tema como “usuários e permissões”, “arquivos e diretórios”, “comandos de rede”, “agendamento de tarefas e backup”, etc.
Fontes
2 comments