Por que escrever um script bash inteiro em funções?

52

No trabalho, escrevo scripts comuns 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 de "legibilidade", que eu acho que poderia ser igualmente bem estabelecido com mais alguns comentários e algum espaçamento entre linhas?

Isso faz o script ser executado com mais eficiência (na verdade, eu esperaria o contrário, se houver alguma) ou facilita a modificação do código além do potencial de legibilidade mencionado anteriormente? Ou é realmente apenas uma preferência estilística?

Por favor, note 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 das coisas que i_am_foo fez e do_baz age sobre coisas configuradas por walk_into_bar - então, ser capaz de trocar arbitrariamente a ordem de execução não é algo que geralmente estaríamos fazendo. Por exemplo, você não iria querer colocar declare_variables após walk_into_bar , isso quebraria 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
    
por Doktor J 29.09.2016 / 20:45

11 respostas

33

Eu comecei a usar esse mesmo estilo de programação depois de ler o blog de Kfir Lavi postar "Defensive Bash Programming" . Ele dá algumas boas razões, mas pessoalmente considero estas 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 parede de código, você vê "Oh, a função find_log_errors lê esse arquivo de log para erros". Compare com encontrar muitas linhas awk / grep / sed que usam deus sabe que tipo de regex no meio de um longo script - você não tem idéia do que está fazendo lá a menos que haja comentários.

  • você pode depurar funções colocando em set -x e set +x . Depois de saber que o resto do código funciona bem, você pode usar esse truque para se concentrar em depurar apenas essa função específica. Claro, você pode incluir partes do script, mas e se for uma parte longa? É mais fácil fazer algo assim:

     set -x
     parse_process_list
     set +x
    
  • imprimindo o uso com cat <<- EOF . . . EOF . Eu usei isso algumas vezes para tornar meu código muito mais profissional. Além disso, a função parse_args() with getopts é bastante conveniente. Novamente, isso ajuda na legibilidade, em vez de empurrar tudo para o 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 experiência bash limitada. No que diz respeito à eficiência, não há muito do que você pode fazer - o bash em si não é a linguagem mais eficiente e as pessoas preferem o perl e o python quando se trata de velocidade e eficiência. No entanto, você pode usar nice de uma função:

nice -10 resource_hungry_function

Comparada a chamar bem em cada linha de código, isso diminui muito a digitação E pode ser convenientemente usado quando você quer 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 este estilo:

por 01.10.2016 / 03:47
65

Readability é uma coisa. Mas há mais para modularização do que apenas isso. ( Semi-modularização é talvez mais correta para funções.)

Nas funções, você pode manter algumas variáveis locais, o que aumenta a confiabilidade , diminuindo a chance de as coisas ficarem confusas.

Outro pro de funções é 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ê pode entrar no reino de multi-threading , ou multi-processamento no mundo Bash. Uma vez que você aprende a fazer as coisas em funções, você estará bem equipado para entrar no paralelo.

Mais um ponto para adicionar. Como o Etsitpab Nioliv observa no comentário abaixo, é fácil redirecionar as funções como uma entidade coerente. Mas há mais um aspecto de redirecionamentos com funções. Ou seja, os redirecionamentos podem ser definidos ao longo da definição da função. Por exemplo:

f () { echo something; } > log

Agora, nenhum redirecionamento explícito é necessário para as chamadas de função.

$ f

Isso pode poupar muitas repetições, o que aumenta a confiabilidade e ajuda a manter as coisas em ordem.

Veja também

por 29.09.2016 / 20:55
38

No meu comentário, mencionei três vantagens das funções:

  1. Eles são mais fáceis de testar e verificar a exatidão.

  2. As funções podem ser facilmente reutilizadas (originadas) em futuros scripts

  3. Seu chefe gosta deles.

E nunca subestime a importância do número 3.

Gostaria de abordar mais um problema:

... so being able to arbitrarily swap the run order isn't something we would generally be doing. For example, you wouldn't suddenly want to put declare_variables after walk_into_bar, that would break things.

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 exigir uma variável que não seja usada em outro lugar, essa variável deverá ser definida e tornada local para walk_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 simples.

Idealmente, as funções devem ser fáceis de testar individualmente. Se, por causa das interações, eles não são fáceis de testar, então isso é um sinal de que eles podem se beneficiar da refatoração.

    
por 29.09.2016 / 23:44
12

Você divide o código em funções pela mesma razão que faria para C / C ++, python, perl, ruby ou qualquer código de linguagem de programação. A razão mais profunda é a abstração - você encapsula tarefas de nível mais baixo em primitivos de nível superior (funções) para que você não precise se preocupar sobre 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.

    
por 30.09.2016 / 09:28
12

Embora eu concorde totalmente com a capacidade de reutilização , legibilidade , e delicadamente beijando os chefes, mas há uma outra vantagem de funções em : escopo da variável . Como LDP mostra :

#!/bin/bash
# ex62.sh: Global and local variables inside a function.

func ()
{
  local loc_var=23       # Declared as local variable.
  echo                   # Uses the 'local' builtin.
  echo "\"loc_var\" in function = $loc_var"
  global_var=999         # Not declared as local.
                         # Therefore, defaults to global. 
  echo "\"global_var\" in function = $global_var"
}  

func

# Now, to see if local variable "loc_var" exists outside the function.

echo
echo "\"loc_var\" outside function = $loc_var"
                                      # $loc_var outside function = 
                                      # No, $loc_var not visible globally.
echo "\"global_var\" outside function = $global_var"
                                      # $global_var outside function = 999
                                      # $global_var is visible globally.
echo                      

exit 0
#  In contrast to C, a Bash variable declared inside a function
#+ is local ONLY if declared as such.

Eu não vejo isso com muita frequência em scripts de shell do mundo real, mas parece uma boa idéia para scripts mais complexos. Reduzir a coesão ajuda a evitar erros nos locais em que você está atingindo uma variável esperada em outra parte do código.

Reusabilidade geralmente significa criar uma biblioteca comum de funções e source dessa biblioteca em todos os seus scripts. Isso não os ajudará a correr mais rápido, mas ajudará você a escrevê-los mais rapidamente.

    
por 30.09.2016 / 19:45
10

Um motivo completamente diferente daqueles já fornecidos em outras respostas: uma razão pela qual essa técnica é usada algumas vezes, em que a única instrução de definição de não-função no nível superior é uma chamada para main , é certificar-se de que o script não faz nada acidentalmente 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 terminar por qualquer motivo antes de terminar de escrever o script inteiro. Isso é especialmente provável se o processo A buscar o script de um recurso remoto. Embora, por razões de segurança, isso não seja uma boa ideia, é algo que é feito e alguns scripts foram modificados para antecipar o problema.

    
por 30.09.2016 / 15:36
7

Um processo requer uma sequência. A maioria das tarefas é seqüencial. Não faz sentido mexer com o pedido.

Mas a grande coisa sobre programação - que inclui scripts - é testar. Testando, testando, testando. Quais scripts de teste você tem atualmente para validar a correção de seus scripts?

Seu chefe está tentando guiá-lo de ser um script infantil para ser um programador. Essa é uma boa direção para entrar. As pessoas que vierem depois de você vão gostar de você.

MAS. Lembre-se sempre das suas raízes orientadas pelo processo. Se faz sentido ter as funções ordenadas na seqüência em que são tipicamente executadas, faça isso, pelo menos como uma primeira passagem.

Mais tarde, você verá que algumas de suas funções estão manipulando dados, outras produzindo, outras processando, outras modelando dados e outras manipulando os dados, por isso pode ser inteligente agrupar métodos semelhantes, talvez até mesmo removê-los. em arquivos separados.

Mais tarde, você pode perceber que agora escreveu bibliotecas de pequenas funções auxiliares que você usa em muitos de seus scripts.

    
por 30.09.2016 / 07:54
6

Os comentários e o espaçamento podem não chegarem 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 panorama geral. Isso pode não ser óbvio em um roteiro curto; contanto que permaneça curto, pode ser legível o suficiente. O software se torna 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:

Place your hands on your desk.
Tense your arm muscles.
Extend your knee and hip joints.
Relax your arms.
Move your arms backwards.
Move your left leg backwards.
Move your right leg backwards.
(continue for 10,000 more lines)

No momento em que você chegou na metade, ou mesmo em 5%, você teria esquecido quais foram os primeiros passos. Você não poderia identificar a maioria dos problemas, porque você não podia ver a floresta para as árvores. Compare com funções:

stand_up();
walk_to(break_room);
pour(coffee);
walk_to(office);

Isso é muito mais compreensível, não importa quantos comentários você possa colocar na versão sequencial linha a linha. Também torna muito mais provável que você perceba que esqueceu de fazer o café e provavelmente esqueceu-se de sit_down () no final. Quando a sua mente está pensando nos detalhes do greex e awk regexes, você não pode estar pensando em um quadro geral - "e se não houver café feito"?

As funções permitem principalmente que você veja a foto maior e observe que 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.

Existem também outros benefícios discutidos em outras respostas, é claro. Outro benefício que não está 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 você só precisa olhar para 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), qualquer coisa e tudo em todo o sistema pode ser uma causa de $ foo estar incorreto, e qualquer coisa e tudo pode ser afetado por $ foo. Portanto, você não pode fixar com segurança $ foo sem reexaminar todas as linhas do programa. Se $ foo é local para uma função, você pode garantir que todas as alterações sejam seguras e corretas, verificando apenas essa função.

    
por 03.10.2016 / 07:20
5

Alguns truísmos relevantes sobre programação:

  • Seu programa mudará, mesmo que seu chefe insista que esse não é o caso.
  • Apenas código e entrada afetam o comportamento do programa.
  • A nomeação é difícil.

Os comentários começam como uma lacuna por não serem capazes de expressar suas idéias claramente no código *, e pioram (ou simplesmente estão errados) com a mudança. Portanto, se for possível, expresse conceitos, estruturas, raciocínio, semântica, fluxo, tratamento de erros e qualquer outra coisa pertinente à compreensão do código como código.

Dito isto, as funções Bash têm alguns problemas não encontrados na maioria das linguagens:

  • O namespace é terrível no Bash. Por exemplo, esquecer de usar a palavra-chave local resulta em poluir o espaço de nomes global.
  • Usando local foo="$(bar)" resulta em perdendo o código de saída de bar .
  • Não há parâmetros nomeados, portanto, você deve ter em mente o que "$@" significa em diferentes contextos.

* Me desculpe se isso ofende, mas depois de usar comentários por alguns anos e desenvolver sem eles ** por mais anos, fica claro que é superior.

** Usando comentários para licenciamento, a documentação da API e afins ainda é necessária.

    
por 01.10.2016 / 17:16
4

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 < em> um grupo de pessoas e não apenas para seu próprio uso.

Eu quero focar em uma expectativa: em um ambiente de trabalho "tempo é dinheiro" . Assim, a ausência de bugs e os desempenhos do seu código são avaliados em conjunto com readability , capacidade de teste, capacidade de manutenção, capacidade de refatoração, reutilização ...

Escrever em "módulos" um código irá diminuir o tempo de leitura necessário não apenas pelo codificador , mas até mesmo o tempo usado por os testadores ou pelo chefe. Além disso, observe que o tempo de um chefe geralmente é pago mais do que o tempo de um programador e que seu chefe avaliará a qualidade de seu trabalho.

Além disso, escrever em independente "módulos" um código (mesmo um script bash) permitirá que você trabalhe em "paralelo" com outros componente de sua equipe encurtando o tempo total de produção e usando na melhor das hipóteses a expertise do single, para revisar ou reescrever uma parte sem nenhum efeito colateral sobre os outros, para reciclar o código que você acabou de escrever "como está" para outro programa / script, para criar bibliotecas (ou bibliotecas de trechos), para reduzir o tamanho geral e a probabilidade relacionada de erros, para depurar e testar cada parte ... e, claro, organizará em uma seção lógica seu programa / script e melhorar a sua legibilidade. Todas as coisas que economizarão tempo e dinheiro. A desvantagem é que você tem que se ater aos padrões e comentar suas funções (que você deve, no entanto, fazer em um ambiente de trabalho).

Para aderir a um padrão , seu trabalho será mais lento no começo, mas isso 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. Então, por exemplo, mesmo que eu acredite que as variáveis globais tenham que ser definidas globalmente e não em uma função, eu posso entender um padrão que as inicializa em uma função chamada declare_variables() chamada sempre no primeira linha do main() one ...

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 irá manter o código compacto e focou o usuário salvando novamente o tempo.

Aqui acima, você pode ver como está desdobrado somente a função walk_into_bar() . Mesmo os outros tinham 1000 linhas de comprimento cada, você ainda poderia manter sob controle todo o código em uma única página. Note que é dobrado até a seção onde você vai declarar / inicializar as variáveis.

    
por 03.10.2016 / 10:43
1

Além das razões dadas em outras respostas:

  1. Psychology: A programmer whose productivity is being measured in lines of code will have an incentive to write unnecessarily verbose code. The more management is focusing on lines of code, the more incentive the programmer has to expand his code with unneeded complexity. This is undesirable since increased complexity can lead to increased cost of maintenance and increased effort required for bug fixing.
    
por 01.10.2016 / 05:51