paulo1205
(usa Ubuntu)
Enviado em 06/03/2017 - 17:21h
fsiqueira escreveu:
Opa... Na verdade queria saber se vocês tem algum material um pouco mais avançado em Shell Script para me passar ou indicar? Queria pegar os macetes, aprender alguns padrões e boas práticas. Todo material que encontro na internet é bem superficial, basicamente o que ensinado é lógica de programação em Shell Script. Agradeço desde já
A primeira boa prática tem a ver com o fato de que existem várias variantes de shell, com conjuntos de recursos diferentes entre si, mas que às vezes se apresentam com os mesmos nomes.
É um erro muito comum, sobretudo de que vem da linha Red Hat, assumir que
/bin/sh se refere ao Bash, e aí o cara faz um script que começa com “
#!/bin/sh ”, mas que contém uma porção de recursos que existem no Bash mas não existem em nenhuma outra variante de shell ou -- o que talvez seja ainda pior -- funcionam de modo diferente em algumas dessas variações. Aí o script do cara pára de funcionar quando ele muda de RHEL/CentOS/Fedora para o Debian/Ubuntu/Mint, ou para outro UNIX, como Solaris ou FreeBSD.
Então a
primeira dica é
usar uma linha shebang que seja específica em chamar o shell correto, que implemente os recursos que você se propôs a usar .
Só que isso nem sempre é suficiente. Problema semelhante ocorre com muitos shells que tentam imitar o Korn Shell (ksh) da AT&T, e que usam o nome
/bin/ksh , mas que não implementam todos os recursos da versão oficial ou o fazem com limitações ou diferenças.
Suponha que o cara desenvolve um script em que usa computação de ponto flutuante -- só para citar um recurso que não existe no Bash --, que funciona no ksh93 original mas, quando se vai rodar o script na máquina do cliente, acontece uma das seguintes coisas:
- a máquina não tem ksh instalado (caso da instalação padrão de muitos Linuxes);
- a máquina possui mais de uma versão de ksh, mas o ksh93 se chama
/bin/ksh93 , e o
/bin/ksh se refere ao ksh88, que não possuía computação em ponto flutuante, nem um monte de outros recursos da versão 93 (comum em alguns Solaris e AIX);
- a máquina não usa nenhuma das duas versões oficiais do ksh, mas sim algum clone, como o mirksh ou pdksh, que possuem várias discrepâncias em relação a qualquer das versões originais de ksh (caso de algumas distribuições de Linux, como o Arch, e de alguns BSDs; diferentes versões do RHEL também possuem versões diferentes de ksh, com algumas trazendo o pdksh e outras usando a versão AT&T, depois que teve o fonte liberado).
Então o cara quis usar o ksh para poder ter ponto flutuante, e acabou se deparando com discrepâncias de implementação. Será que ele deveria ter usado shell?
A
segunda regra é esta:
se você quiser ter absoluto controle sobre o que o script deve fazer, sem depender de detalhes de implementação do interpretador,
provavelmente você não quer um script em shell, mas sim um programa feito numa linguagem de programação com comportamento mais rigidamente padronizado , e o resultado de fazer seu programa nessa linguagem será tanto melhor quanto menos você depender de bibliotecas externas ou aspectos do sistema operacional que também possam ter muita variação entre máquinas diferentes (tais como locales, localização ou formato de arquivos do sistema etc.).
Muitas tarefas executadas dentro de scipts em shell dependem de comandos que são externos ao shell. Executar comandos externos é uma operação consideravelmente lenta, com um custo de criação de um processo externo para cada tarefa e o acompanhamento do ciclo de vida desse processo, até seu encerramento. Após a criação do processo, existe também um custo de invocar o comando externo, e esse custo pode depender de fatores variáveis, tais como em qual diretório o executável se encontra, se ele já está presente no cache de disco feito na memória, do tamanho desse executável, da carga da máquina, entre outros.
A
terceira regra , que vale para o shell como também em qualquer linguagem de programação, é que
se deve tomar cuidado a repetição de tarefas custosas , mesmo que -- ou princiapalmente se -- esse custo ficar oculto por uma sintaxe aparentemente inocente.
Compare as duas variações abaixo para a realização da tarefa de tirar a assinatura SHA-256 de todos os arquivos do /etc (saída suprimida, porque o que interessa nesta discussão são o modo e o tempo de execução).
$ sudo time sh -c 'find /etc -type f -exec sha256sum \{\} \; >/dev/null'
0.07user 0.27system 0:02.22elapsed 15%CPU (0avgtext+0avgdata 1760maxresident)k
29951inputs+0outputs (0major+224957minor)pagefaults 0swaps
$ sudo time sh -c 'find /etc -type f -print0 | xargs -r0 sha256sum >/dev/null'
0.09user 0.04system 0:00.14elapsed 95%CPU (0avgtext+0avgdata 1704maxresident)k
22533inputs+0outputs (0major+1305minor)pagefaults 0swaps
Note que a primeira versão é alguns caracteres mais curta do que a segunda, o que possivelmente a faz mais tentadora de escrever. Contudo, a primeira versão chama o executável
sha256sum uma vez para cada arquivo. No caso, havia 2252 arquivos, o que implicou 2252 novos processos criados pelo shell, com consequentes 2252 execuções do
sha256sum . No fim das contas, isso levou 2,22 segundos. Já a segunda versão criou, em princípio, apenas um processo além do
find , para o programa
xargs , que recebe a lista de arquivos gerada pelo
find e forma uma lista de argumentos, que depois é passada em lotes para o comando especificado (
sha256sum ), executado apenas quando a lista fica cheia. No meu caso, os 2252 não foram suficientes para encher prematuramente a lista, então o executável
sha256sum foi chamado apenas uma vez, com 2252 argumentos (um para cada arquivo), e isso permitiu a execução em apenas 0,14 segundo.
Compare também a ocupação de CPU em cada cenário: no primeiro caso, a construção deixou a CPU mais ociosa do que no segundo (15% × 95% de utilização), pois o
find tinha de ficar esperando pelo sistema em cada execução do
sha256sum , ao passo que, na segunda forma, o
find simplesmente saia colhendo dados e enviando-os ao
xargs , e este, após montar sua lista, simplesmente mandou-a de uma vez so
sha256sum , sem tanta espera. Mesmo assim, o consumo efetivo total de CPU foi maior no primeiro caso que no segundo (2,22s*15%=0,333s, contra 0,14s*95%=0,133s). Outra coisa a ser comparada é o número de
page faults (ligada a operações de alocação de memória ou transferência de/para o cache): são 224957 no primeiro caso contra 1305 no segundo.
O exemplo acima tem, além da questão de desempenho, outro problema: eu pedi ao shell que executasse o
find , o
xargs e o
sha256sum , mas não disse onde eles estavam. Quando isso acontece, o shell tenta executar cada programa no primeiro diretório constante na variável de ambiente
PATH . Se isso falhar, vê se existe um segundo diretório e tenta nesse segundo. Se falhar, vê se existe um terceiro diretório e tenta nesse terceiro, e assim por diante, até ou encontrar o executável ou chegar ao fim da lista de diretórios especificados em
PATH , caso em que ele decide que o executável “não existe” (note que, ao contrário do Windows, o executável não é procurado no diretório corrente, a não ser que o diretório corrente seja um dos listados como parte do valor de
PATH ).
Isso é um duplo problema. O primeiro problema, que salta aos olhos de quem leu esta postagem até aqui, é de desempenho, principalmente em situações repetidas (nos códigos mostrados anteriormente, se eu trocar
sha256sum por
/usr/bin/sha256sum , o primeiro exemplo conseguiu ficar entre 0,05s e 0,09s mais rápido, e a ocupação de CPU subiu de 15% para 16% -- um ganho marginal, porém consistente em várias observações de execuções sucessivas). O segundo problema, no entanto, é mais sério: estar sujeito ao valor da variável de ambiente
PATH pode levar um script a não funcionar, a funcionar mal, o a funcionar de modo perigoso.
Imagine o seguinte cenário: você dá acesso ao seu usuário a um determinado script
x.sh através de
sudo . Ele percebe que, no meio do script, você chama o comando
grep , e resolve fazer o seguinte teste: ele cria um arquivo vazio chamado
grep dentro do diretório dele, e coloca esse diretório como primeiro na lista da variável
PATH . Com isso, ele nota que o
x.sh parou de funcionar direito. Pronto: ele descobriu que o seu script chama o
grep sem especificar o caminho completo, e que o
sudo não foi configurado para sobrescrever o valor da variável
PATH . O próximo passo, então, é ele preencher aquele
grep que ele criou no seu
home com coisas divertidas, tais como criar uma conta privilegiadas com acesso para ele, remover a conta de algum desafeto, remover acesso do administrador
de jure da máquina, destruir, roubar ou sequestrar algum dado importante armazenado na máquina (incluindo chaves privadas de SSH e PGP de outros usuários), entre outras coisas.
Então a
quarta regra é
nunca confiar no valor herdado da variável de ambiente PATH , mas ajustá-lo, de modo a conter apenas aquilo de que ele precisa para funcionar , de preferência arrumando os diretórios de modo a diminuir o número de buscas por executáveis. Uma abordagem complementar é
procurar usar caminhos completos ao chamar programas externos , o que favorece o desempenho ao economizar tempo tentando executar comandos que não existam.
Note que usar caminhos completos simplesmente, mas deixar a variável
PATH intocada, pode não ser suficiente para eliminar riscos de segurança. Em algum ponto do seu script pode haver uma chamada a outro script ou programa externo que não é controlado por você, e esse outro programa pode tentar executar comandos externos sem usar caminhos completos. Então convém, em benefício desses outros programas, ter valores seguros e bem pensados para
PATH .
A
quinta regra é semelhante à anterior:
qualquer outra variável que afete o comportamento do sistema deve ter um valor que assegure um bom comportamento dos comandos invocados pelo script . Um primeiro exemplo seriam variáveis ligadas a idiomas e locales, tais como
LANG ,
LC_MESSAGES ou
LC_ALL . Se eu fizer um script que tentar listar as conexões TCP abertas fazendo algo como “
netstat -nat | grep ESTABELECIDA ”, e vejo que ele funciona quando eu estou logado pelo ambiente gráfico, configurado para funionar em Português, não poderei ficar surpreso se o script parar de funcionar quando eu tentar executável automaticamente através do
cron , pois eu fiz o script contando que o
netstat produziria saída em Português, mas o default do
cron é trabalhar com a língua default do sistema, chamada “
C ”, que é um Inglês limitado a caracteres comuns da norma ISO-646. Por uma questão de resiliência, bons scripts devem preferir trabalhar com as locales “C” ou “POSIX” internamente, e eventualmente usar outras locales apenas na hora de interagir com o usuário, se for o caso. Outras variáveis que podem ser relevantes, inclusive por razões de segurança, são aquelas que podem afetar o disparo de ferramentas externas, tais como
PAGER ,
EDITOR e
SHELL , entre outras.