Dúvida sobre boas práticas em Shell Script

1. Dúvida sobre boas práticas em Shell Script

Fabio Siqueira
fsiqueira

(usa Ubuntu)

Enviado em 03/03/2017 - 21:04h

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á


  


2. Re: Dúvida sobre boas práticas em Shell Script

thinomar
thinomar

(usa Linux Mint)

Enviado em 06/03/2017 - 16:17h

http://tldp.org/LDP/abs/html/


3. Re: Dúvida sobre boas práticas em Shell Script

Paulo
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.


4. Re: Dúvida sobre boas práticas em Shell Script

Guilherme
Ghost_Shell

(usa Arch Linux)

Enviado em 06/03/2017 - 17:43h

paulo1205 escreveu:

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 da variável de ambiente PATH. Um bom script sempre ajusta o valor da variável PATH para conter [b]apenas aquilo de que ele precisa para funcionar. 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.


Caraca, as respostas do paulo deveriam sempre ser postadas como artigos,

Keep it simple stupid!


5. Re: Dúvida sobre boas práticas em Shell Script

Perfil removido
removido

(usa Nenhuma)

Enviado em 07/03/2017 - 01:52h


Mesma regra para HTML,C e JAVA.

Especificar qual SHELL
Nome - mantenha nomes coerentes
Data
Comentários e descrição
Declarar variáveis - no início do script. Isso ajuda a não fixar valores, podendo reaproveitar o script posteriormente.
Identação (muito importante)
Comentários fase-a-fase
LOGs - Para análise posterior em execuções será muito mais fácil encontrar possíveis erros.
Valide as execuções de seus scripts
Pense complexo mas seja simples
Documente sempre
Pense no futuro
Utilize funções quando necessário
Pense no recursos do Sistema


Leia
http://cesarakg.freeshell.org/tips-shell-programming.html












Patrocínio

Site hospedado pelo provedor RedeHost.
Linux banner

Destaques

Artigos

Dicas

Tópicos

Top 10 do mês

Scripts