No trabalho, escrevo scripts bash com frequência. Meu supervisor sugeriu que o script inteiro fosse dividido em funções, semelhante ao seguinte exemplo:
#!/bin/bash
# Configure variables
declare_variables() {
noun=geese
count=three
}
# Announce something
i_am_foo() {
echo "I am foo"
sleep 0.5
echo "hear me roar!"
}
# Tell a joke
walk_into_bar() {
echo "So these ${count} ${noun} walk into a bar..."
}
# Emulate a pendulum clock for a bit
do_baz() {
for i in {1..6}; do
expr $i % 2 >/dev/null && echo "tick" || echo "tock"
sleep 1
done
}
# Establish run order
main() {
declare_variables
i_am_foo
walk_into_bar
do_baz
}
main
Existe alguma razão para fazer isso além da "legibilidade", que eu acho que poderia estar igualmente bem estabelecida com mais alguns comentários e espaçamento entre linhas?
Isso faz com que o script seja executado com mais eficiência (eu realmente esperaria o oposto, se houver) ou facilita a modificação do código além do potencial de legibilidade acima mencionado? Ou é realmente apenas uma preferência estilística?
Observe que, embora o script não o demonstre bem, a "ordem de execução" das funções em nossos scripts reais tende a ser muito linear - walk_into_bar
depende do que i_am_foo
foi feito e do_baz
atua no que foi configurado walk_into_bar
- A capacidade de trocar arbitrariamente a ordem de execução não é algo que geralmente estaríamos fazendo. Por exemplo, você não iria de repente querem colocar declare_variables
depois walk_into_bar
, que iria quebrar as coisas.
Um exemplo de como eu escreveria o script acima seria:
#!/bin/bash
# Configure variables
noun=geese
count=three
# Announce something
echo "I am foo"
sleep 0.5
echo "hear me roar!"
# Tell a joke
echo "So these ${count} ${noun} walk into a bar..."
# Emulate a pendulum clock for a bit
for i in {1..6}; do
expr $i % 2 >/dev/null && echo "tick" || echo "tock"
sleep 1
done
fonte
main()
na parte superior e adicionomain "$@"
na parte inferior para chamá-lo. Isso permite que você veja a lógica do script de alto nível quando você a abre.local
- isso fornece um escopo variável que é incrivelmente importante em qualquer script não trivial.Respostas:
Comecei a usar esse mesmo estilo de programação bash depois de ler a postagem no blog de Kfir Lavi, "Defensive Bash Programming" . Ele dá algumas boas razões, mas pessoalmente acho essas as mais importantes:
Os procedimentos se tornam descritivos: é muito mais fácil descobrir o que uma parte específica do código deve fazer. Em vez de código de parede, você vê "Oh, a
find_log_errors
função lê esse arquivo de log para erros". Compare-o com encontrar muitas linhas awk / grep / sed que usam deus sabe que tipo de regex no meio de um script longo - você não tem idéia do que está fazendo lá, a menos que haja comentários.você pode depurar funções anexando
set -x
eset +x
. Depois de saber que o restante do código funciona bem, você pode usar esse truque para se concentrar na depuração apenas dessa função específica. Claro, você pode incluir partes do script, mas e se for uma parte longa? É mais fácil fazer algo assim:uso de impressão com
cat <<- EOF . . . EOF
. Eu o usei algumas vezes para tornar meu código muito mais profissional. Além disso,parse_args()
com agetopts
função é bastante conveniente. Novamente, isso ajuda na legibilidade, em vez de colocar tudo no script como uma parede gigante de texto. Também é conveniente reutilizá-los.E, obviamente, isso é muito mais legível para alguém que conhece C ou Java ou Vala, mas tem uma experiência limitada no bash. Quanto à eficiência, não há muito o que você pode fazer - o bash em si não é a linguagem mais eficiente e as pessoas preferem perl e python quando se trata de velocidade e eficiência. No entanto, você pode
nice
uma função:Comparado a chamar agradável em cada linha de código, isso diminui muita digitação E pode ser usado convenientemente quando você deseja que apenas uma parte do seu script seja executada com prioridade mais baixa.
A execução de funções em segundo plano, na minha opinião, também ajuda quando você deseja que várias instruções sejam executadas em segundo plano.
Alguns dos exemplos em que usei esse estilo:
fonte
local
e chamando tudo por meio damain()
função. Isso torna as coisas muito mais fáceis de gerenciar e você pode evitar uma situação potencialmente confusa.Legibilidade é uma coisa. Mas há mais na modularização do que apenas isso. (A semi-modularização talvez seja mais correta para as funções.)
Nas funções, você pode manter algumas variáveis locais, o que aumenta a confiabilidade , diminuindo a chance de as coisas ficarem bagunçadas.
Outro profissional de funções é a reutilização . Depois que uma função é codificada, ela pode ser aplicada várias vezes no script. Você também pode portá-lo para outro script.
Seu código agora pode ser linear, mas, no futuro, você poderá entrar no mundo da multithreading ou multiprocessing no mundo Bash. Depois de aprender a fazer as coisas em funções, você estará bem equipado para o passo em paralelo.
Mais um ponto a acrescentar. Como o Etsitpab Nioliv observa no comentário abaixo, é fácil redirecionar das funções como uma entidade coerente. Mas há mais um aspecto dos redirecionamentos com funções. Ou seja, os redirecionamentos podem ser definidos ao longo da definição da função. Por exemplo.:
Agora, nenhum redirecionamento explícito é necessário pelas chamadas de função.
Isso pode poupar muitas repetições, o que novamente aumenta a confiabilidade e ajuda a manter as coisas em ordem.
Veja também
fonte
source
ou. scriptname.sh
, e usar essas funções como-se que eles estavam em seu novo script.No meu comentário, mencionei três vantagens de funções:
Eles são mais fáceis de testar e verificar a correção.
As funções podem ser facilmente reutilizadas (originadas) em scripts futuros
Seu chefe gosta deles.
E nunca subestime a importância do número 3.
Gostaria de abordar mais uma questão:
Para obter o benefício de dividir o código em funções, deve-se tentar tornar as funções o mais independentes possível. Se
walk_into_bar
requer uma variável que não é usada em outro lugar, essa variável deve ser definida e tornada local parawalk_into_bar
. O processo de separar o código em funções e minimizar suas interdependências deve tornar o código mais claro e mais simples.Idealmente, as funções devem ser fáceis de testar individualmente. Se, por causa das interações, elas não são fáceis de testar, isso é um sinal de que elas podem se beneficiar da refatoração.
fonte
;-)
Você divide o código em funções pelo mesmo motivo que faria para C / C ++, python, perl, ruby ou qualquer outro código de linguagem de programação. A razão mais profunda é a abstração - você encapsula tarefas de nível inferior em primitivas (funções) de nível superior, para que você não precise se preocupar em como as coisas são feitas. Ao mesmo tempo, o código se torna mais legível (e passível de manutenção) e a lógica do programa se torna mais clara.
No entanto, olhando para o seu código, acho bastante estranho ter uma função para declarar variáveis; isso realmente me faz levantar uma sobrancelha.
fonte
main
função / método, então?Embora eu concorde totalmente com a reutilização , a legibilidade e o beijo delicado dos chefes, mas há outra vantagem das funções no bash : escopo variável . Como mostra o LDP :
Não vejo isso com muita frequência em scripts shell do mundo real, mas parece uma boa ideia para scripts mais complexos. Reduzir a coesão ajuda a evitar erros nos quais você está enfrentando uma variável esperada em outra parte do código.
Reutilização geralmente significa criar uma biblioteca comum de funções e
source
inserir essa biblioteca em todos os seus scripts. Isso não os ajudará a correr mais rápido, mas ajudará você a escrevê-los mais rapidamente.fonte
local
, mas acho que a maioria das pessoas que escreve scripts divididos em funções ainda segue o princípio do design. Usignlocal
apenas torna mais difícil a introdução de bugs.local
disponibiliza variáveis para a função e seus filhos, portanto é muito bom ter uma variável que possa ser transmitida da função A, mas indisponível para a função B, que pode querer ter variável com o mesmo nome, mas com finalidade diferente. Então isso é bom para definir o escopo e, como disse o Voo - menos bugsUm motivo completamente diferente do que já foi fornecido em outras respostas: um motivo pelo qual essa técnica é usada algumas vezes, em que a única declaração de não definição de função no nível superior é uma chamada para
main
, é garantir que o script não faça acidentalmente nada desagradável se o script estiver truncado. O script pode ser truncado se for canalizado do processo A para o processo B (o shell) e o processo A for encerrado por qualquer motivo antes de terminar de escrever o script inteiro. É provável que isso aconteça se o processo A buscar o script de um recurso remoto. Embora, por razões de segurança, não seja uma boa ideia, é algo que é feito e alguns scripts foram modificados para antecipar o problema.fonte
main()
padrão é comum no Python, onde se usaif __name__ == '__main__': main()
no final do arquivo.import
o script atual sem executarmain
. Suponho que um guarda semelhante possa ser colocado em um script bash.Um processo requer uma sequência. A maioria das tarefas é seqüencial. Não faz sentido mexer com a ordem.
Mas o grande problema da programação - que inclui scripts - é o teste. Teste, teste, teste. Quais scripts de teste você possui atualmente para validar a correção de seus scripts?
Seu chefe está tentando guiá-lo de um roteirista para um programador. Esta é uma boa direção a seguir. As pessoas que vierem depois de você irão gostar de você.
MAS. Lembre-se sempre de suas raízes orientadas ao processo. Se fizer sentido ordenar as funções na sequência em que são executadas, faça isso, pelo menos como primeira passagem.
Mais tarde, você verá que algumas de suas funções estão lidando com entradas, outras saídas, outras processando, outras modelando dados e outras manipulando os dados; portanto, pode ser inteligente agrupar métodos semelhantes, talvez até movê-los para arquivos separados .
Mais tarde, você pode perceber que agora escreveu bibliotecas de pequenas funções auxiliares usadas em muitos de seus scripts.
fonte
Comentários e espaçamento não podem chegar nem perto da legibilidade que as funções podem, como demonstrarei. Sem funções, você não pode ver a floresta para as árvores - grandes problemas se escondem entre muitas linhas de detalhes. Em outras palavras, as pessoas não podem se concentrar simultaneamente nos detalhes e no quadro geral. Isso pode não ser óbvio em um script curto; contanto que permaneça curto, pode ser legível o suficiente. O software fica maior, porém, não menor, e certamente faz parte de todo o sistema de software da sua empresa, que é certamente muito maior, provavelmente milhões de linhas.
Considere se eu lhe dei instruções como esta:
Quando você chegasse na metade ou até 5%, já teria esquecido quais foram os primeiros passos. Você não conseguiu identificar a maioria dos problemas, porque não podia ver a floresta para as árvores. Compare com funções:
Isso é certamente muito mais compreensível, não importa quantos comentários você possa colocar na versão sequencial linha por linha. Também torna muito mais provável que você notará que esqueceu de fazer o café e provavelmente esqueceu de sit_down () no final. Quando sua mente está pensando nos detalhes das expressões regulares grep e awk, você não pode estar pensando em um cenário geral - "e se não houver café feito"?
As funções permitem principalmente que você veja o quadro geral e observe que se esqueceu de fazer o café (ou que alguém pode preferir chá). Em outro momento, em um estado de espírito diferente, você se preocupa com a implementação detalhada.
Também há outros benefícios discutidos em outras respostas, é claro. Outro benefício não claramente indicado nas outras respostas é que as funções fornecem uma garantia importante na prevenção e correção de bugs. Se você descobrir que alguma variável $ foo na função apropriada walk_to () estava errada, você sabe que precisa apenas olhar as outras 6 linhas dessa função para encontrar tudo o que poderia ter sido afetado por esse problema e tudo o que poderia fizeram com que estivesse errado. Sem funções (apropriadas), tudo e qualquer coisa no sistema inteiro pode ser uma causa de $ foo estar incorreta, e tudo e qualquer coisa e tudo podem ser afetados por $ foo. Portanto, você não pode corrigir com segurança $ foo sem reexaminar todas as linhas do programa. Se $ foo for local para uma função,
fonte
bash
sintaxe. É uma pena, no entanto; Eu não acho que haja uma maneira de passar informações para funções como essa. (ou seja,pour();
<coffee
). Parece maisc++
ouphp
(acho).Alguns truques relevantes sobre programação:
Os comentários começam como uma brecha por não serem capazes de expressar suas idéias claramente no código * e pioram (ou simplesmente erram) com a mudança. Portanto, se possível, expresse conceitos, estruturas, raciocínio, semântica, fluxo, tratamento de erros e qualquer outra coisa pertinente ao entendimento do código como código.
Dito isto, as funções do Bash têm alguns problemas não encontrados na maioria dos idiomas:
local
palavra - chave resulta em poluir o espaço para nome global.local foo="$(bar)"
resultados na perda do código de saída debar
."$@"
significa em diferentes contextos.* Sinto muito se isso ofende, mas depois de usar os comentários por alguns anos e desenvolvê-los sem eles por mais anos, fica bem claro o que é superior.
** Ainda é necessário o uso de comentários para licenciamento, documentação da API e similares.
fonte
local foo=""
Em seguida, colocá-los usando a execução de comandos para agir sobre o resultado ...foo="$(bar)" || { echo "bar() failed"; return 1; }
. Isso nos tira da função rapidamente quando um valor necessário não pode ser definido. As chaves são necessárias para garantir que areturn 1
execução seja feita apenas em caso de falha.Tempo é dinheiro
Existem outras boas respostas que esclarecem as razões técnicas para escrever modularmente um script, potencialmente longo, desenvolvido em um ambiente de trabalho, desenvolvido para ser usado por um grupo de pessoas e não apenas para seu próprio uso.
Quero me concentrar em uma expectativa: em um ambiente de trabalho "tempo é dinheiro" . Portanto, a ausência de bugs e o desempenho do seu código são avaliados em conjunto com legibilidade , capacidade de teste, manutenção, refactorabilidade, reutilização ...
Escrever em "módulos" um código diminuirá o tempo de leitura necessário não apenas pelo codificador em si, mas também o tempo usado pelos testadores ou pelo chefe. Além disso, observe que o tempo de um chefe geralmente é pago mais do que o tempo de um codificador e que seu chefe avaliará a qualidade do seu trabalho.
Além disso, escrevendo em "módulos" independentes um código (mesmo um script bash) permitirá que você trabalhe "paralelamente" com outro componente de sua equipe, reduzindo o tempo total de produção e utilizando, na melhor das hipóteses, a experiência do single, para revisar ou reescrever uma peça com nenhum efeito colateral nos outros, para reciclar o código que você acabou de escrever "como está"para outro programa / script, criar bibliotecas (ou bibliotecas de trechos), reduzir o tamanho geral e a probabilidade de erros relacionados, depurar e testar minuciosamente cada parte ... e, claro, organizará na seção lógica seu programa / script e aprimore sua legibilidade. Tudo o que economizará tempo e dinheiro. A desvantagem é que você precisa seguir os padrões e comentar suas funções (que você ainda precisa fazer em um ambiente de trabalho).
Aderir a um padrão atrasará seu trabalho no início, mas acelerará o trabalho de todos os outros (e também de você) posteriormente. De fato, quando a colaboração cresce em número de pessoas envolvidas, isso se torna uma necessidade inevitável. Assim, por exemplo, mesmo que eu acredite que as variáveis globais precisam ser definidas globalmente e não em uma função, posso entender um padrão que as inicialize em uma função
declare_variables()
denominada sempre chamada na primeira linha damain()
primeira ...Por último, mas não menos importante, não subestime a possibilidade nos editores modernos de código fonte de mostrar ou ocultar rotinas seletivamente separadas ( dobragem de código ). Isso manterá o código compacto e concentrará o usuário economizando novamente.
Aqui acima, você pode ver como é desdobrado apenas a
walk_into_bar()
função. Mesmo os outros tinham 1000 linhas cada, você ainda podia controlar todo o código em uma única página. Observe que está dobrada até a seção em que você vai declarar / inicializar as variáveis.fonte
Além dos motivos dados em outras respostas:
fonte
Outro motivo que geralmente é esquecido é a análise de sintaxe do bash:
Obviamente, esse script contém um erro de sintaxe e o bash não deve executá-lo, certo? Errado.
Se empacotássemos o código em uma função, isso não aconteceria:
fonte