Tutorial Linux – Parte 3

Foto de um teclado de computador com retroiluminação branca

Capítulo 3. O Shell

Como já dissemos anteriormente, o shell é um programa independente que roda na camada do usuário dosistema. Por ser independente do kernel do sistema surgiram diversas versões distintas deshell. O primeiro foi o Bourne Shell criado por Stephen Bourne na década de 70. Essa versãopode ser executada nos UNIX com o comando sh. A partir da década de 70, novas versõesde shell foram sendo criadas, um deles é o C Shell (csh) que foi desenvolvido porBill Joy para ser usada na versão BSD do UNIX em meados de 1980. Existem ainda Tenex/Tops C Shell (tcsh), o Bourne Against Shell (bash) e o Korn Shell (ksh). Neste livro usaremos o bash pois essaé a versão mais comum em distribuições Linux. Para saber qual é a versão de shell que estamos utilizando bastaimprimir o conteúdo da variável de ambiente $SHELL:

$ echo $SHELL
bash
$ _

3.1. O prompt

Quando efetuamos o login no sistema o UNIX irá buscar no arquivo /etc/passwd qual versão do shell é a utilizada pelo usuário. Ao terminar a busca o sistema irá executar um shell para o usuário que entrará no modo ‘interativo’. Apresentando o prompt de comandos:

$ _

O $ geralmente é o prompt exibido para usuários comuns do sistema. No caso do usuário root o prompt será:

# _

Esses prompts indicam que o sistema está pronto e aguardando algum comando. É possível alterar o prompt mudando o conteúdo da variável $PS1:

$ export PS1='prompt: '
prompt: export PS1="$(hostname) > "
maquina1 > _

Notem que alguns caracteres especiais como o ‘>‘ precisam ser precedidos de um caractere ‘‘ para forçar o shell a usar o caractere literal. Essa técnica é conhecida como escape de um caracter.

3.2. Executando comandos

Para executarmos um comando basta digitá-lo no prompt de comandos teclar Enter. Quando fazemos isso, o shell irá verificar se esse comando não é parte de seus comandos internos (veremos mais sobre isso no próximo capítulo), depois disso ele vai procurar por um arquivo com permissão de execução nos diretórios listados na variável de ambiente $PATH. Quando encontrado o shell irá verificar se o arquivo é um binário ou um script. Se o arquivo for um binário será solicitado que o kernel carregue e execute esse arquivo. Caso o arquivo seja um script o shell irá solicitar ao kernel que execute o interpretador apropriado passando como parâmetro o nome do script em questão. A forma que o shell utiliza para identificar qual é o interpretador apropriado para executar o script será vista no próximo capítulo.

Muitos programas requerem que sejam passados parâmetros para eles. Para passarmos esses parâmetros podemos separá-los por um espaço em branco (pode ser um espaço, uma tabulação ou uma quebra de linha). Como já dissemos anteriormente é o shell que fica encarregado de expandir a lista de arquivos e passar para o programa quando passamos nomes de arquivos com globs como parâmetro.

$ ls -l

3.3. Concatenando comandos

Você pode executar vários comandos no prompt, para isso basta separá-los por ‘;’:

$ date; ls -l
Thu Dec 13 15:28:16 GMT-2 2001
total 0
-rw-rw-r--  1  osvaldo  osvaldo  0  Dec 13  15:28  arquivo1
-rw-rw-r--  1  osvaldo  osvaldo  0  Dec 13  15:28  arquivo2
$ _

Como vocês viram no exemplo acima, o shell executou o comando date e em seguida o comando ls -l.

3.4. Evaluation

Nós podemos também executar um segundo comando apenas se o primeiro retornar sucesso usando && (pronuncia-se “and”ou “e”). Por exemplo:

$ grep -q usuario /etc/passwd && finger usuario

O comando acima procura pela string no passwd. Caso ela seja encontrada, o grep retorna sucesso e o comando finger é executado.

Também é possível executar um comando caso outro falhe, usando || (pronuncia-se “or“ou “ou”). O seguinte comando cria um usuário caso ele não exista:

$ grep -q usuario /etc/passwd || adduser usuario

3.5. Redireção

Como foi dito anteriormente o UNIX possui um dispositivo padrão de entrada (stdin), de saída (stdout) e para saída de mensagens de erro (stderr) que, por padrão estão associados ao teclado e ao monitor. Felizmente o UNIX oferece um mecanismo para modificar esses padrões possibilitando que a entrada seja a saída de um outro programa (pipe) ou que a saída de erro seja enviada para um arquivo de log de erros. Esse mecanismo é conhecido como redireção.

É uma prática comum, e recomendada, para todos os programas que rodam em UNIX que eles enviem mensagens para stdout, recebam entradas de stdin e enviem suas mensagens de erro para stderr. Veja o exemplo abaixo:

$ ls -l /etc/passwd
-rw-r--r--  1  root  root  987  Dec 4  08:39  /etc/passwd
$ ls -l /etc/foo
/etc/foo not found

No primeiro caso, a saída foi enviada para stdout. No segundo caso, para stderr. Nós podemos usar os indicadores de redireção para verificar isso. O indicador de redireção > envia a saída padrão (stdout para um arquivo determinado. Veja o exemplo:

$ ls -l /etc/passwd > /dev/null
$ ls -l /etc/foo > /dev/null
/etc/foo not found

Apenas a saída padrão foi redirecionada para /dev/null e, portanto descartada. Lembre-se que um programa pode enviar dados para as duas saídas durante a mesma execução. O ls poderia ter retornado uma mensagem de erro para a saída de erro e uma listagem de arquivos para a saída padrão ao mesmo tempo.

Da mesma forma como podemos descartar a saída enviando-a para /dev/null, também podemos usá-la enviando-a para um arquivo para uso posterior. Veja o exemplo:

$ ls -l /etc > lista

Esse comando envia a listagem de arquivos em /etc para o arquivo lista. Se o arquivo tinha algum conteúdo anterior, ele é eliminado. Para evitar isso, podemos usar o redirecionador duplo ».

$ ls -l /etc >> lista

Nesse caso, o resultado da saída do comando é acrescentado ao fim do arquivo lista. Assim como acontece com o redirecionador simples >, se o arquivo não existia, ele é criado.

Da mesma forma como é possível enviar dados para um arquivo, é possível ler dados provenientes de um arquivo. Para isso, usa-se o redirecionador <. O seguinte comando lê o arquivo lista e passa seus conteúdos para o comando grep:

$ grep passwd < lista

Ainda, a saída de erro pode ser redirecionada com o redirecionador 2>. Veja o exemplo:

$ ls -l /etc/foo > /dev/null
/etc/foo not found
$ ls -l /etc/foo 2> /dev/null

Confira na tabela 3.1 os redirecionadores mais usados.

Redirecionador Significado
> Envia saída padrão para arquivo especificado zerando o arquivo
>> Envia saída padrão para arquivo especificado adicionando os conteúdos ao arquivo
2> Envia saída de erro para arquivo especificado zerando o arquivo
2>> Envia saída de erro para arquivo especificado adicionando o conteúdo ao arquivo
< Usa arquivo especificado como entrada
<< STRING Lê da entrada padrão até encontrar string (ver próximo capítulo)

Tabela 3.1: Redirecionadores.

 

Se você desejar redirecionar a sua saída padrão para outra saída, tal como a saída de erro, basta redirecioná-la para &2:

$ echo "Erro: Esse programa encontrou um erro ao abrir o arquivo" >&2
Erro: Esse programa encontrou um erro ao abrir o arquivo

A mensagem acima foi enviada para stderr. Da mesma forma é possível redirecionar a saída de erro para a saída normal, para isso basta usar 2>&1:

$ ls -l > lista 2>&1

O comando acima envia a saída do comando ls para o arquivo lista. As mensagens de erro também serão enviadas para o arquivo lista.

3.6. Pipe

Uma das operações mais frequente na informática é a análise de dados. Uma das formas mais indicadas para fazer essa análise no UNIX é combinar os diversos comandos disponíveis no ambiente. A maneira de ‘conectar’ os comandos no UNIX é utilizando o | (pipe).

Quando o shell encontra um | na linha de comando ele sabe que deve enviar a saída padrão de um programa para a entrada padrão de outro aplicativo. Esse recurso é um dos mais poderosos recursos disponíveis em ambientes UNIX. O comando grep por exemplo mostra a linha em que ele encontrou um texto que precisamos procurar:

$ ls -l | grep passwd
-r--r--r--  1  root  root  3903  Dec 5  16:38  passwd

O comando ls -l envia a lista dos arquivos disponíveis no diretório para a saída padrão. Essa saída é enviada para o pipe que por sua vez a envia para a entrada do comando grep que está instruído para procurar pela string ‘passwd’. Quando o grep encontra a string ele a imprime na tela.

3.7. Ambiente

O shell também nos possibilita a criação de variáveis. Essas variáveis, quando criadas, estão disponíveis para o ambiente em que ela foi criada. Por esse motivo elas são conhecidas como variáveis de ambiente. Quando o seu shell script termina ou o shell em funcionamento termina a execução (exit) as variáveis são restauradas ou removidas do ambiente de forma a devolvê-lo ao mesmo estágio em que se encontrava. O shell é sensível ao caso com nomes de variáveis, assim como para comandos, ou seja, VARIAVEL é diferente de Variavel. Existem algumas variáveis que são definidas por padrão pelo próprio shell. Essas variáveis muitas vezes armazenam dados do sistema para serem usados pelos scripts. A tabela 3.2 mostra algumas das mais comuns (uma lista completa pode ser obtida com o comando set).

Variável Conteúdo
$HOME Caminho para o diretório do usuário
$PATH Caminhos de busca de comandos
$PS1 Conteúdo do prompt do shell
$PWD Diretório atual
$TERM Tipo de terminal
$USER Usuário atual
$? Código de retorno do último comando executado
$$ PID do shell em execução

Tabela 3.2: Variáveis padrão.

 

Para atribuir um valor a uma variável basta utilizar o operador =:

$ var=foo
$ echo $var
foo
$ _

O shell utiliza o método de substituição do valor da variável pelo conteúdo dela sempre que encontra o símbolo $ precedendo um nome de variável. Isso pode ser observado no exemplo abaixo:

$ var='ls -l'
$ $var
total 23872
-rwxr-xr-x  1  root  root  1825  Sep 3  10:27  usuario

Um erro muito comum encontrado em scripts é a utilização de espaços para separar o operador =. O operador = deve ser usado imediatamente depois do nome da variável e seguido, imediatamente, pelo valor a ser atribuído à variável.

$ var = 'ok'
bash: var: command not found
$ _

Quando queremos verificar o conteúdo da variável podemos imprimí-lo com o comando echo:

$ echo $HOME
/home/usuario

As variáveis, além de ser válidas no shell atual, também podem ser passadas para todos os programas executados a partir desse shell. Para que isso aconteça, porém, essas variáveis devem ser exportadas com o comando export:

$ export var

Ainda, é possível exportar uma variável ao mesmo tempo em que ela é criada:

$ export var=oi

Note, porém, que o ambiente só é herdado por processos filhos. Se você abre um shell secundário e cria uma variável, ela vai aparecer para os processos filhos do shell secundário, mas não vai aparecer no shell primário. O ambiente só é copiado ‘para baixo’, nunca ‘para cima’ entre os processos.

Para eliminar variáveis, use o comando unset:

$ set | grep var
var=foo
$ unset var
$ set | grep var
$ _

3.8. Controle de trabalhos

Um processo pode ser iniciado em background adicionando o caractere & ao fim da linha de comando. Por exemplo:

$ kermit &
[1] 6185
$ _

Os números mostrados logo depois da execução do processo em background são respectivamente o número do job no shell e o ID do processo (PID) no sistema. O PID é um número global de identificação do sistema enquanto o número do job só vale para o shell atual. A sua utilização é mais prática ser um número pequeno, incremental e independente dos demais processos do sistema.

O exemplo 3.8 vai iniciar o kermit e voltar imediatamente ao prompt do shell para que o usuário possa continuar executando comandos. Esse comando é útil para disparar programas demorados e que não mostrem saída no terminal (por padrão, o programa pode imprimir texto no terminal livremente, o que pode “embaralharťť a tela do usuário). Caso durante a execução do programa ele tente ler alguma entrada do teclado (como o kermit faz), o processo será interrompido.

O shell pode eventualmente mostrar mensagens sobre o status de seus jobs. A mensagem de que o processo foi interrompido por tentar ler dados do teclado é:

[1] + Stopped (tty input) ftp &

Por padrão, esse tipo de mensagem aparece apenas quando o prompt vai ser redesenhado na tela (ficar com o shell parado esperando a mensagem não funciona. Apertar Enter uma vez pode forçar o aparecimento da mensagem). Novamente, o primeiro número é o número do job no shell, seguido pela explicação do que aconteceu com o processo e por que motivo e, finalmente, a linha de comando usada para lançar o processo.

O usuário pode abrir vários processos em background. O gerenciamento dos jobs é feito, no shell, com o comando jobs. Veja o exemplo:

$ jobs
[3] + Stopped (tty output) vi &
[2] - Stopped (tty output) kermit&
[1] Stopped (tty input) ftp &

Aqui o usuário tem três processos, parados, em background. Para trazer para a frente um dos processos, usa-se o comando fg.

$ fg %3
<traz o vi para a frente>

Note como o número do job deve ser precedido por %.

Uma das operações mais comuns, porém, é o usuário ter que interromper seu programa atual para verificar alguma outra informação em outra parte do sistema. O shell, por padrão, aceita que se digite no teclado a seqüência CTRL+Z. Essa seqüência faz com que o processo atual seja jogado em background exatamente como teria sido se o programa tivesse sido iniciado com &. O usuário pode voltar ao programa usando o comando fg seguido pelo número do processo. Adicionalmente, pode executar apenas fg para voltar automaticamente ao último processo manipulado.

Um inconveniente às vezes indesejado acontece quando o processo deveria continuar rodando enquanto o usuário volta ao shell. Quando o processo é enviado interativamente para background, ele é parado. Um processo parado em background pode ser continuado usando o comando bg, que funciona como o comando fg. Assim como o fg, bg aceita um número de job precedido por % e, se não tiver parâmetros, usa o último job manipulado.

O comando kill pode ser usado para mandar sinais para os jobs. O seguinte comando envia o sinal TERM para o processo vi iniciado em background.

$ jobs
[1] + Stopped (tty output) vi&
$ kill %1
[1] + Terminated vi&

O sinal TERM é o padrão enviado pelo kill. Poderia-se usar, por exemplo, kill -KILL %1 para enviar o sinal KILL, ou qualquer outro sinal. kill -l lista os sinais disponíveis no sistema. Note que, apesar do nome, o kill não serve somente para matar os processos.

Finalmente, caso o usuário tente sair do sistema com jobs que não foram interrompidos, a mensagem You have stopped jobs vai aparecer. O usuário terá, então, mais uma chance para fechar seus programas rodando. Se o usuário tentar sair do sistema mais uma vez imediatamente após essa mensagem aparecer, os jobs rodando serão interrompidos.

3.9. Aliases

Aliases são uma forma de encurtar comandos longos e muito usados, e são muito úteis e largamente usados por usuários experientes. O bash vem com vários aliases definidos por padrão. O usuário pode consultar a lista de aliases com o comando alias.

Veja, por exemplo, o alias stop. Ele está definido como

stop=kill -STOP

Caso o usuário rode stop, internamente o comando será substituído por kill -STOP. Parâmetros podem ser usados normalmente.

Novos aliases podem ser usados executando-se o comando alias nomedoalias='comando a ser executado'.

Continua…