O Shell e as linguagens de Script

As linguagens de script podem parecer um pouco estranhas para os programadores de linguagens mais tradicionais, como programadores C ou C++. A exemplo, eu cito, falta de tipos distintos para as variáveis, ou mesmo o fato de estas linguagens usarem algum mecanismo obscuro para gerenciamento dinâmico de memória que consome mais recursos da máquina do que os estritamente necessários. Por outro lado, se pensarmos que em linguagens como C e C++ cerca de 40% do tempo de desenvolvimento é gasto com técnicas de armazenamento e gerenciamento de memória (Hans Boehm. Advantages and Disadvantages of Conservative Garbage Collection. ) essas linguagens podem ser utilizadas em projetos cujo desempenho possa ser “rápido o bastante” e não “o mais rápido possível” ou mesmo em sistemas de prova de conceito.

O termo script, que provém de mecanismos antigos onde usavam-se arquivos de texto (estes sim scripts) para gerar entradas de programas iterativos, não mais faz sentido. Perceba, as linguagens de script modernas como Perl5, TCL e Python cresceram em capacidade e poder que o termo “linguagem de script” não passa de mera palavra técnica que não mais expressa a atual capacidade destas linguagens sendo usado ainda por que não temos um termo melhor para elas. Hoje elas possuem estruturas de dados complexas, como referências, hashs ou dicionários e aceitam até mesmo as modernas técnicas de programação orientada a objetos além de frameworks complexos.

E para sermos programadores ou administradores de sistemas proficientes nos ambientes Linux o conhecimento de ao menos uma ou duas linguagens de Scripts e conhecimentos mínimos de C ou C++ fazem-se necessários. Dentre as linguagens de script, minha preferida é o Perl5, porém hoje vamos falar do Shell.

O Shell foi uma das, se não a primeira interface de desenvolvimento de scripts no mundo Unix. Mas com certeza foi a primeira interface capaz de produzir código interpretado portável (Addison Wesley - The Art of Unix Programming). Apesar de ser muito utilizada como mera interface de comando, o Shell é uma linguagem de programação simples e natural. Por não possuir estruturas de dados complexas o Shell utiliza de forma muito inteligente outros programas em modo texto (como sort, sed, awk e perl -e), suprindo assim esta fraqueza.

A primeira versão do netnews era um shellscript com 150 linhas apenas. De acordo com Steven M. Bellovin, idealizador do programa, a versão em shellscript era muito lento para produção, mas muito eficiente para provas de conceito e testes.

O primeiro Shell disponível para Unix foi o Bourne Shell (sh), hoje substituído em muitos sistemas pelo Bourne Again Shell (bash). Depois do Bourne Shell muitos outros shells foram desenvolvidos, como o Korn Shell (ksh) e o C Shell (csh) entre outros.

Como funciona o Shell

Scripts em Shell são excelentes em sistemas onde não podemos assumir a pré existência de outras linguagens, como Python ou Perl ou mesmo em scripts que rodam durante o start-up dos sistemas operacionais para levantar os serviços necessários e fazer as configurações dos usuários. Também são uma ótima pedida em situações simples para administradores, instaladores e como ferramenta didática para programadores iniciantes. Sistemas Shell mais complexos podem arriscar sua portabilidade, pois existem diferenças de implementação entre os shells existentes (Bash, Korn, C) além da possibilidade da não existência de um determinado comando no sistema que executará sua aplicação.

É muito importante saber operar o Shell no mundo Unix e Linux. No mundo Linux o shell mais popular é o Bash. No mundo Unix o Ksh ainda tem muita importância. Historicamente os principais Shells são: Bourn Shell ou sh; C Shell ou csh; Korn Shell ou ksh; Bourn Again Shell ou bash;

Inicialmente os sistemas Unix usavam o sh como linguagem de programação e interpretação de comandos. O sh introduziu a possibilidade de declaração de variáveis e estruturas de decisão em um interpretador de comandos. Posteriormente foi desenvolvido o csh, cuja a estrutura de programação era semelhante ao C, muito utilizado no desenvolvimento Unix até os dias de hoje.

Exemplos de código em sh e csh para comparação:

#!/bin/sh
  if [ $days -gt 365 ]
  then
     echo This is over a year.
  fi
  #!/bin/csh
  if ( $days > 365 ) then
     echo This is over a year.
  endif
  #!/bin/sh
  i=2
  j=1
  while [ $j -le 10 ]; do
     echo '2 **' $j = $i
     i=`expr $i '*' 2`
     j=`expr $j + 1`
  done
  #!/bin/csh
  set i = 2
  set j = 1
  while ( $j <= 10 )
     echo '2 **' $j = $i
     @ i *= 2
     @ j++
  end

O ksh surgiu de uma implementação onde David Korn tentou unir as qualidades do csh e do sh em um único shell além de adicionar aritmética de ponto flutuante e estrutura de arrays associativos, tornando-se muito popular em sistemas Unix como Solaris, True64 e AIX.

O bash surgiu como uma implementação mais atual do Bourn Shell, com aritmética em diversas bases, arrays de tamanho ilimitado, funções e diversas outras facilidades ao programador/administrador de sistemas.

Você pode aprender mais sobre o bash em http://www.gnu.org/software/bash/ e sobre o ksh em http://www.kornshell.com/.

Todo shell moderno deve atender ao padrão POSIX 1003.2 "Shell and Utilities Language Committee". As principais características dos modernos Shells são:

  • Controle de Jobs;
  • Funções;
  • Alias;
  • Redirecionamento;
  • Histórico de comandos;
  • Editor de comandos;
  • Auto-Completar;

O Bash, como hell padrão do projeto GNU atende a todos estes requisitos do padrão Posix e as qualidades de um shell moderno listadas acima.

Usando o shell em sistemas Linux

Apesar do Bash ser o shell padrão do Linux, diversos outros podem existir na instalação. O shell padrão de cada usuário é definido no arquivo /etc/passwd:

$ cat /etc/passwd
  root:x:0:0:root:/root:/bin/bash
  bin:x:1:1:bin:/bin:/sbin/nologin
  daemon:x:2:2:daemon:/sbin:/sbin/nologin
  thiago:x:500:500::/home/thiago:/bin/bash
  $

E também pode ser visto na variável de ambiente SHELL:

$ echo "Minha shell eh: $SHELL"
  Minha shell eh: /bin/bash
  $

Se você, por alguma razão, preferir outro shell ao bash pode alterar o shell padrão do seu usuário com o comando chsh:

$chsh thiago
  Alterando o interpretador de comandos para o usuário thiago.
  Senha:
  Novo interpretador de comandos [/bin/bash]: /bin/ksh
  Interpretador de comandos alterado.

Depois saia do shell com o comando logout e log no sistema novamente. Veja o seu novo shell:

$ echo $SHELL
  /bin/ksh
  $

E no arquivo /etc/passwd

$ grep thiago /etc/passwd
  thiago:x:500:500::/home/thiago:/bin/ksh
  $

Arquivos de Configuração do BASH:

Os arquivos de configuração do bash definem os alias, atalhos, prompt e variáveis de ambientes da sua sessão dentre outras coisas. Estes arquivos são divididos em globais, que influenciam as sessões de todos os usuários e os arquivos locais que alteram apenas o seu usuário. Os arquivos globais são /etc/profile e /etc/bashrc. Os arquivos locais são $HOME/.profile; $HOME/.bash_login; $HOME/.bashrc; $HOME/.bash_profile; $HOME/.bash_logout;

Exceção feita ao arquivo local $HOME/.bash_logout, os outros arquivos de configuração local são equivalentes e não irão necessariamente coexistir, bastando apenas um dos arquivos de configuração local. O arquivo que estará na sua pasta home dependerá da distribuição. As configurações feitas nos arquivos locais sobrescrevem as configurações locais. Quando você utilizar um shell que não seja de login, apenas os arquivos locais serão lidos para configurar este shell e o $HOME/.bahs_logout não é executado ao sair do shell.

Nota: $HOME e ~ são formas de referenciar o diretório home do usuário atual, no caso /home/thiago

Ordem de leitura dos arquivos:

  1. Ao carregar um shell de login o Linux carrega inicialmente o arquivo /etc/bashrc ou /etc/profile.
  2. Depois o sistema procura executar o script $HOME/.bash_profile.
  3. Se ele não existir o sistema irá procurar pelo $HOME/.bash_login.
  4. Se ele não existir o sistema irá procurar pelo $HOME/.profile.
  5. O sistema executa $HOME/.bashrc se houver.
  6. O shell está pronto para interagir com o usuário e apresenta o prompt.
  7. Quando o usuário sair do shell o script $HOME/.bash_logout é executado.

Executando comandos no Bash

No bash a estrutura básica de um comando é:

comando [opções] [argumentos]

Exemplo:

$ ls -la $HOME
  total 40
  drwx------ 3 thiago thiago 4096 Mai  4 11:28 .
  drwxr-xr-x 4 root root 4096 Mai  3 16:15 ..
  -rw------- 1 thiago thiago 2155 Mai  4 13:02 .bash_history
  -rw-r--r-- 1 thiago thiago   33 Mai  3 16:15 .bash_logout
  -rw-r--r-- 1 thiago thiago  176 Mai  3 16:15 .bash_profile
  -rw-r--r-- 1 thiago thiago  124 Mai  3 16:15 .bashrc
  drwxr-xr-x 4 thiago thiago 4096 Mai  3 16:15 .mozilla
  -rw------- 1 thiago thiago  134 Mai  4 13:02 .sh_history
  -rw-r--r-- 1 thiago thiago  658 Mai  3 16:15 .zshrc
   
  $ cd /etc
  $ pwd
  /etc
  $

Quando você precisar executar uma sequência muito grande de comandos podemos utilizar o caractere \ para fazer quebra de linha e facilitar a leitura da linha ou script:

# ls -lR /etc   > | wc -l
  2570
  #

Outra facilidade importante do Bash é sua capacidade de autocompletar comandos com a tecla <tab>. Quando você iniciar a digitação de um comando e não tiver certeza do nome completo do comando basta digitar tab duas vezes e ver as possibilidades:

$ ca <tab><tab>
  cadaver             cancel              captoinfo
  cal                 cancel.cups         case
  caller              capifax             cat
  callgrind_annotate  capifaxrcvd         catchsegv
  callgrind_control   capiinfo
  $ca

Se houver apenas uma possibilidade de comando, ela irá aparecer na tela após o tab:

$ calle <tab>
  $ caller

Caracteres Especiais do Shell

Para atender sua necessidade de ser tanto um interpretador de comandos quanto uma linguagem de programação, o Bash possuí diversos caracteres especiais:

Caractere Finalidade/Significado Exemplo
~ Home do usuário atual cd ~ ls -la ~
Escape do caractere seguinte: touch my\*file myfile my1file (cria um arquivo my*file)
/ Separação de Diretórios cd /etc/apache2 ls /var/log
$ Variável echo $PATH
? Existe um caractere qualquer ls my?file
' Cota absoluta echo 'não substitui: $PATH'
` Executa comando/Substituição NOW=`date` ; echo $NOW
Cota dupla echo “Este é o PATH: $PATH”
* Qualquer caractere, nenhuma ou várias vezes ls m*file
& Envia processo para o background xeyes&
&& Operador AND curto-circuito: se cmd1 executar com sucesso executa o segundo, senão encerra echo 1 && echo 2 fake && echo 2
| Pipe: Redireciona a saída de um comando para outro comando ls ~ | xargs wc
|| Operador OR curto-circuito: Se primeiro comando com sucesso não executa o segundo comando echo 1 || echo 2 fake || echo 2
; Executa os comandos em sequência cmd1; cmd2; cmd3... echo “O path é: “; echo $PATH echo “O home é: “; echo $HOME
[] Range de caracteres $ touch file1 file2 file3 file4 $ ls file[1-4]
> Redireciona saída Cmd > out_file.txt
< Redireciona entrada Cmd < in_file.txt

Valor retornado por comandos

Como as funções em linguagens de programação, os comandos do GNU/Linux retornam um valor para o Shell. O valor retornado para o Shell indica o status da execução. O valor de retorno zero indica que o comando foi executado com sucesso. Qualquer outro valor indica um erro na execução do comando. O valor retornado fica armazenado na variável de ambiente $?.

exemplo:

$ echo "Usuario:home_dir:login_shell";grep thiago /etc/passwd 
  |cut -d: -f1,6-7 
  && echo "estatus do comando: $?"
  Usuario:home_dir:login_shell
  thiago:/home/thiago:/bin/bash
  estatus do comando: 0
  $

Perceba: uma vez que a execução do comando anterior teve sucesso o valor de $? foi setado para zero. Caso eu utilize um comando incorreto, inexistente ou que eu não tenho permissão $? receberá um valor diferente de 0.

exemplo:

$ comando_inexistente ; echo "Erro $?"
  -bash: comando_inexistente: command not found
  Erro 127
   
  $ rm / || echo "Erro. $?"
  rm: cannot remove directory `/': um diretório
  Erro. 1
   
  $ rm -rf /root || echo "Erro: $?"
  rm: cannot chdir from `.' to `/root': Permissão negada
  Erro: 1

Executando diversos comando na mesma linha

Existem várias maneiras de executar mais de um comando na mesma linha. Existem as maneiras condicionais e não condicionais.

  • ponto-e-vírgula (;): quando colocamos diversos comandos em uma linha separados por ponto e vírgula eles são executados em sequência:
$ echo "cria diversos arquivos: ";touch file1 file2 file3; ls file*
  cria diversos arquivos:
  file1  file2  file3  file4
  $
  • or curto circuito ( || ): se o o primeiro comando não é executado com sucesso, então o shell tenta executar comando seguinte:
$ rm -f /etc/cron.d || echo "ATENCAO: Nao pude remover arquivo. Erro $?"
  rm: impossível remover `/etc/cron.d': Permissão negada
  ATENCAO: Nao pude remover arquivo. Erro 1
   
  $ rm -f /etc/cron.d || mail x8ge -s "Nao pude remover arquivo. Erro $?" 
   
  $ ls $HOME || echo "Nao foi possivel ler os arquivos: $?"
  file1  file2  file3  file4
  $
  • and curto circuíto ( && ): se o primeiro comando é executado com sucesso, então o shell tenta executar o segundo comando.
$ ls $HOME && echo "arquivos lidos em $HOME"
  file1  file2  file3  file4
  arquivos lidos em /home/x8ge
   
  $ ls ~root && echo "arquivos lidos em ~root"
  ls: /root: Permissão negada
  $
  • Substituição de Comandos: Você pode atribuir um comando a uma variável e utilizar esta variável em linhas de comando e scripts:
$ ESTRUTURA=`mkdir -p $HOME/myapp/bin $HOME/myapp/conf $HOME/myapp/help`
  $ echo "Criando estrutura de arquivos"; $ESTRUTURA || 
     echo "WARNING: NAO FOI POSS'IVEL CRIAR ESTRUTURA DE DIRETORIOS 
     PARA MYAPP $?"; ls -R myapp/
  Criando estrutura de arquivos
  myapp/:
  bin  conf  help
   
  myapp/bin:
   
  myapp/conf:
   
  myapp/help:
  $

Neste exemplo criamos uma variável, ESTRUTURA, que na verdade contém o comando `mkdir -p $HOME/myapp/bin $HOME/myapp/help $HOME/myapp/conf`. A substituição de comandos também pode ser utilizada para gerar argumentos para outro comando:

$ wc -c $(ls *.pl)
       117 pingador.pl
       160 usa_ponto.pl
       277 total
  $

Neste exemplo você pode ver o comando pode ser executado dentro de $( ), em $( ls *.pl ). Da mesma maneira que fosse uma variável, mas o valor não é atribuído a nenhuma variável real.

O Histórico de comandos do Shell

O histórico do Gnu/Linux é um arquivo que contém os últimos comandos digitados pelo usuário. Entre suas finalidades podemos citar: Analisar as últimas ações do usuário; Executar comandos repetitivos; Executar comando que tenham pequena variação na lista de argumentos. Por exemplo, eu preciso verificar os arquivos de uma estrutura de diretório que eu não estou familiarizado. Então irei executar o comando:

$ ls /var 
agentx   cache  games  local  log   opt  spool  www 
backups  crash  lib    lock   mail  run  tmp 

Agora que eu sei quais os subdiretórios diretamente abaixo, basta clicar a seta para cima que o shell vai colocar o comando anterior ls /var na linha de comando e eu só preciso digitar a pasta que quero listar, abaixo de /var. Assim eu só vou digitar /www pois ls /var foi posto no meu prompt pelo histórico:

$ ls /var/www 
cgi-bin  htdocs      kumera-0.3.tar.gz  library  news       tools 
data     index.html  lib                media    teste.asp 

Outro atalho interessante é fornecido pela característica de substituição rápida: ^string_anterior^nova_string. Com está técnica eu substituo uma string do comando anterior por uma nova string e executo novamente o código. Por exemplo eu uso o comando ls para verificar o tamanho de um arquivo qualquer, como /var/www/index.html:

# ls -l /var/www/index.html 
-rw-r--r-- 1 root root 2070 2011-03-09 02:17 /var/www/index.html 

E decido compactá-lo. basta substituir ls -l por gzip:


# ^ls -l^gzip 
gzip /var/www/index.html 

Isto se torna extremamente útil quando estamos lidando com comandos muito extensos que possuem diversos pipes, argumentos muito longos, etc... e iremos executar comandos diferentes com argumentos repetidos ou argumentos diferentes com o mesmo comando.

Os comandos digitados pelo usuário ficam armazenados no arquivo $HOME/.bahs_history

$ cat .bash_history | nl | tail 
   466    modprobe -lt net 
   467    locate modules.dep 
   468    cat /lib/modules/2.6.32-31-generic/modules.dep 
   469    lsmod 
   470    lsmod | sort 
   471    grep vboxnetadp /lib/modules/2.6.32-31-generic/modules.dep 
   472    grep vboxdrv /lib/modules/2.6.32-31-generic/modules.dep 
   473    modprob -l 
   474    modprobe -l 
   475    wc -l $(modprobe -l) 
   476    modprobe -l | wc -l 

Configuração do Histórico

As principais configurações do histórico podem ser vistas e alteradas nas variáveis de ambiente HISTSIZE, HISTFILE e HISTCMD. HISTSIZE contém o tamanho do buffer de comandos do history, ou seja, quantos comandos serão armazenados pelo HISTFILE. HISTFILE contém o path do arquivo que armazena o histórico. E HISTCMD contém o número do próximo comando do histórico:

$ echo $HISTSIZE 
500 
$ echo $HISTFILE 
/home/thiago/.bash_history 
$ echo $HISTCMD 
511 

Apesar de HISTSIZE estar configurada para 500 e HISTCMD marcar 511 o meu arquivo de histórico contém apenas 500 comandos, mas seu número inicial não está mais em 1. Você pode ver a lista de todos os comandos do histórico com o comando history:

$ history
   14  ls -R /etc/apache2 
   15  ls -Rd /etc | wc -l 
   16  ls -d /etc 
   17  ls -R /etc | wc -l 
   18  sudo ls -R /etc | wc -l 
   19  sudo ls -R /etc | wc -l 
   20  sudo ls -Ra /etc | wc -l 
   21  sudo ls -Ra /etc 
   22  sudo ls -Rla /etc 
   23  echo `date` 
... saída omitida.

O buffer de comandos do histórico pode ser limpo com o comando history -c

$ history -c 
$ history 
   15  history 

Nota: As variáveis do histórico estão definidas nos arquivos de configuração do seu shell.

Atalhos interessantes do History.

O history nos fornece atalhos muito interessantes, veja:

!! => executa o último comando digitado
!n => executa o comando de número n, exemplo:
$ history 
   15  history 
   16  grep HIST* .profile 
   17  ls .profile 
   18  cat .profile 
   19  cat .bashrc #vamos usar o atalho para executar este comando.
   20  history 

$ !19 
cat .bashrc 
# ~/.bashrc: executed by bash(1) for non-login shells. 
# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc) 
# for examples 
...saída omitida

!string => executa o comando mais recente que inicia com “string”

!?string => executa o comando mais recente que contém “string”

Para obter mais informações sobre o history do Linux:
comandos:

man history
man fc ou info fc
man bash