Guia de estilos de shell do Google (em russo)

Prefácio


Qual Shell usar


Bash única linguagem de script de shell que pode ser usada para arquivos executáveis.


Os scripts devem começar com #!/bin/bash com um conjunto mínimo de sinalizadores. Use set para definir opções de shell para que a chamada do script como bash <script_name> não viole sua funcionalidade.


Limitar todos os scripts de shell ao bash fornece uma linguagem consistente de shell instalada em todas as nossas máquinas.


A única exceção é se você estiver limitado pelas condições para as quais está programando. Um exemplo seria os pacotes Solaris SVR4, que requerem o uso do shell Bourne usual para qualquer script.


Quando usar o Shell


O shell deve ser usado apenas para pequenos utilitários ou wrappers de script simples.


Embora o script de shell não seja uma linguagem de desenvolvimento, ele é usado para escrever vários utilitários em todo o Google. Este guia de estilo é mais um reconhecimento de seu uso, em vez de uma proposta para usá-lo em uso generalizado.


Algumas recomendações:


  • Se você costuma chamar outros utilitários e faz relativamente pouca manipulação de dados, o shell é uma opção aceitável para a tarefa.
  • Se o desempenho importa, use outra coisa, mas não o shell.
  • Se você achar que precisa usar matrizes para mais do que atribuir ${PIPESTATUS} , use Python.
  • Se você estiver escrevendo um script com mais de 100 linhas, provavelmente deverá escrevê-lo em Python. Lembre-se de que os scripts estão crescendo. Reescreva seu script em outro idioma anteriormente para evitar reescrições demoradas mais tarde.

Arquivos de shell e chamada de intérprete


Extensões de arquivo


Os arquivos executáveis ​​não devem ter a extensão (fortemente preferida) ou a extensão .sh . As bibliotecas devem ter a extensão .sh e não devem ser executáveis.


Não é necessário saber em que idioma o programa foi escrito durante sua execução, e o shell não requer uma extensão, portanto, preferimos não usá-lo para arquivos executáveis.


No entanto, é importante que as bibliotecas saibam em que idioma está escrito e, às vezes, é necessário ter bibliotecas semelhantes em diferentes idiomas. Isso permite que você tenha arquivos de biblioteca com nomes idênticos com objetivos idênticos, mas escritos em idiomas diferentes devem ter o mesmo nome, exceto por um sufixo específico do idioma.


SUID / SGID


SUID e SGID são proibidos em scripts de shell.


Existem muitos problemas de segurança, tornando quase impossível fornecer proteção SUID / SGID suficiente. Embora o bash complique o lançamento do SUID, ele ainda é possível em algumas plataformas, por isso proibimos explicitamente seu uso.


Use o sudo para acesso aprimorado, se necessário.


O meio ambiente


STDOUT vs STDERR


Todas as mensagens de erro devem ser enviadas para STDERR .


Isso ajuda a separar o estado normal dos problemas reais.


Recomenda-se que a função de exibição de mensagens de erro seja usada junto com outras informações de status.


 err() { echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $@" >&2 } if ! do_something; then err "Unable to do_something" exit "${E_DID_NOTHING}" fi 

Comentários


Cabeçalho do arquivo


Inicie cada arquivo com uma descrição do seu conteúdo.


Cada arquivo deve ter um título do comentário, incluindo uma breve descrição de seu conteúdo. Informações sobre direitos autorais e autor são opcionais.


Um exemplo:


 #!/bin/bash # # Perform hot backups of Oracle databases. 

Comentários dos Recursos


Qualquer função que não seja óbvia e curta deve ser comentada. Qualquer função na biblioteca deve ser comentada, independentemente de seu comprimento ou complexidade.


Você precisa garantir que alguém entenda como usar seu programa ou como usar a função em sua biblioteca, simplesmente lendo os comentários (e a necessidade de auto-aperfeiçoamento) sem ler o código.


Todos os comentários dos recursos devem incluir:


  • Descrição da Função
  • Variáveis ​​globais usadas e modificadas
  • Argumentos recebidos
  • Retorne valores diferentes dos códigos de saída padrão no último comando.

Um exemplo:


 #!/bin/bash # # Perform hot backups of Oracle databases. export PATH='/usr/xpg4/bin:/usr/bin:/opt/csw/bin:/opt/goog/bin' ######################################## # Cleanup files from the backup dir # Globals: # BACKUP_DIR # ORACLE_SID # Arguments: # None # Returns: # None ######################################## cleanup() { ... } 

Comentários de implementação


Comente partes complexas, não óbvias, interessantes ou importantes do seu código.


Presume-se que seja a prática usual de comentar código no Google. Não comente tudo. Se houver um algoritmo complicado ou você estiver fazendo algo incomum, adicione um breve comentário.


TODO Comentários


Use os comentários do TODO para códigos temporários, de curto prazo ou muito bons, mas não perfeitos.


Isso é consistente com a convenção no manual C ++ .


Os comentários do TODO devem incluir a palavra TODO em maiúsculas, seguida do seu nome entre parênteses. Dois pontos é opcional. Também é preferível indicar o número do bug / ticket próximo ao elemento TODO.


Um exemplo:


 # TODO(mrmonkey): Handle the unlikely edge cases (bug ####) 

Formatação


Embora você deva seguir o estilo que já é usado nos arquivos que você está editando, o seguinte é necessário para qualquer novo código.


Indentação


Recuar 2 espaços. Sem guias.


Use linhas em branco entre os blocos para melhorar a legibilidade. Recuo são dois espaços. Não importa o que você faça, não use guias. Para arquivos existentes, permaneça fiel ao recuo atual.


Comprimento da String e Comprimento do Valor


O comprimento máximo da linha é de 80 caracteres.


Se você precisar escrever linhas com mais de 80 caracteres, isso deve ser feito usando o here document ou, se possível, a newline . Valores literais que podem ter mais de 80 caracteres e não podem ser separados são razoavelmente permitidos, mas é altamente recomendável que você encontre uma maneira de reduzi-los.


 #  'here document's cat <<END; I am an exceptionally long string. END #  newlines   long_string="I am an exceptionally long string." 

Tubulações


Os pipelines devem ser divididos cada um em uma linha, se não couberem em uma linha.


Se um pipeline se encaixa em uma linha, ele deve estar em uma linha.


Caso contrário, ele deve ser dividido para que cada seção esteja em uma nova linha e indentada por 2 espaços para a próxima seção. Refere-se à cadeia de comandos combinada usando '|' bem como conexões lógicas usando '||' e '&&'.


 #      command1 | command2 #   command1 \ | command2 \ | command3 \ | command4 

Ciclos


Local ; do ; do e ; then ; then na mesma linha que while , for ou if .


Os ciclos no shell são um pouco diferentes, mas seguimos os mesmos princípios que as chaves ao declarar funções. Isto é ; then ; then e ; do ; do deve estar na mesma linha que if / for / while . else deve estar em uma linha separada e as instruções de fechamento devem estar em sua própria linha, alinhadas verticalmente com a instrução de abertura.


Um exemplo:


 for dir in ${dirs_to_cleanup}; do if [[ -d "${dir}/${ORACLE_SID}" ]]; then log_date "Cleaning up old files in ${dir}/${ORACLE_SID}" rm "${dir}/${ORACLE_SID}/"* if [[ "$?" -ne 0 ]]; then error_message fi else mkdir -p "${dir}/${ORACLE_SID}" if [[ "$?" -ne 0 ]]; then error_message fi fi done 

Declaração de caso


  • Opções separadas em 2 espaços.
  • As opções de linha única requerem um espaço após o colchete de fechamento do modelo e antes ;; .
  • Opções longas ou com vários comandos devem ser divididas em várias linhas com um modelo, ações e ;; em linhas separadas.

Expressões correspondentes retrocedem um nível de case e esac . As ações multilinhas também têm recuos em um nível separado. Não há necessidade de colocar expressões entre aspas. Os padrões de expressão não devem ser precedidos por parênteses abertos. Evite usar o &; e notação.


 case "${expression}" in a) variable="..." some_command "${variable}" "${other_expr}" ... ;; absolute) actions="relative" another_command "${actions}" "${other_expr}" ... ;; *) error "Unexpected expression '${expression}'" ;; esac 

Comandos simples podem ser colocados em uma linha com o padrão e ;; enquanto a expressão permanece legível. Isso geralmente é adequado para lidar com opções de letra única. Quando as ações não couberem em uma linha, deixe o modelo na sua linha; a próxima ação será ;; também na própria linha. Quando essa é a mesma linha das ações, use um espaço após o colchete de fechamento do modelo e outro antes ;; .


 verbose='false' aflag='' bflag='' files='' while getopts 'abf:v' flag; do case "${flag}" in a) aflag='true' ;; b) bflag='true' ;; f) files="${OPTARG}" ;; v) verbose='true' ;; *) error "Unexpected option ${flag}" ;; esac done 

Expansão variável


Em ordem de prioridade: observe o que já está em uso; coloque variáveis ​​entre aspas; prefira "${var}" mais que "$var" , mas com atenção ao contexto de uso.


Essas são recomendações, uma vez que o tópico é bastante contraditório para a regulamentação obrigatória. Eles são listados em ordem de prioridade.


  1. Use o mesmo estilo que você encontra no código existente.
  2. Coloque variáveis ​​entre aspas, consulte a seção Cotação abaixo.
  3. Não coloque caracteres únicos específicos para parâmetros de shell / posicionais entre aspas e chaves, a menos que estritamente necessário e para evitar confusão profunda.
    Prefira chaves para todas as outras variáveis.


      #    #    '' : echo "Positional: $1" "$5" "$3" echo "Specials: !=$!, -=$-, _=$_. ?=$?, #=$# *=$* @=$@ \$=$$ ..." #   : echo "many parameters: ${10}" #    : # Output is "a0b0c0" set -- abc echo "${1}0${2}0${3}0" #     : echo "PATH=${PATH}, PWD=${PWD}, mine=${some_var}" while read f; do echo "file=${f}" done < <(ls -l /tmp) #    #   ,    , #     ,   shell echo a=$avar "b=$bvar" "PID=${$}" "${1}" #  : #    "${1}0${2}0${3}0",   "${10}${20}${30} set -- abc echo "$10$20$30" 

    Citações


    • Sempre use aspas para valores que contêm variáveis, substituições de comandos, espaços ou metacaracteres de shell, até precisar expor com segurança valores que não estejam entre aspas.
    • Prefira aspas para valores que são "palavras" (ao contrário de parâmetros de comando ou nomes de caminho)
    • Nunca cite números inteiros.
    • Saiba como as aspas funcionam para padrões de correspondência em [[ .
    • Use "$@" se você não tiver um motivo específico para usar $* .


 # ''  ,     . # ""  ,   /. #   # "   " flag="$(some_command and its args "$@" 'quoted separately')" # "  " echo "${flag}" # "      " value=32 # "    ",      number="$(generate_number)" # "   ",    readonly USE_INTEGER='true' # "    - shell" echo 'Hello stranger, and well met. Earn lots of $$$' echo "Process $$: Done making \$\$\$." # "    " # ( ,  $1  ) grep -li Hugo /dev/null "$1" #    # "   ,    ": ccs     git send-email --to "${reviewers}" ${ccs:+"--cc" "${ccs}"} #   : $1    #       . grep -cP '([Ss]pecial|\|?characters*)$' ${1:+"$1"} #   , # "$@"   ,  # $*    # # * $*  $@   ,   #      ; # * "$@"     ,   #       ; #     ,      #   # * "$*"    ,    #     () , #        # ( 'man bash'  nit-grits ;-) set -- 1 "2 two" "3 three tres"; echo $# ; set -- "$*"; echo "$#, $@") set -- 1 "2 two" "3 three tres"; echo $# ; set -- "$@"; echo "$#, $@") 

Recursos e erros


Substituição de comando


Use $(command) vez de backticks.


Os backticks aninhados requerem o escape de aspas internas com \ . O formato $ (command) não muda dependendo do aninhamento e é mais fácil de ler.


Um exemplo:


 #  : var="$(command "$(command1)")" #  : var="`command \`command1\``" 

Cheques, [ e [[


[[ ... ]] mais preferível que [ , test ou /usr/bin/[ .


[[ ... ]] reduz a possibilidade de erro, porque não há resolução de caminho ou separação de palavras entre [[ e ]] , e [[ ... ]] permite que você use uma expressão regular onde [ ... ] não [ ... ] .


 #  ,       #  `alnum`,     . #  ,       #  .  ,  # E14   https://tiswww.case.edu/php/chet/bash/FAQ if [[ "filename" =~ ^[[:alnum:]]+name ]]; then echo "Match" fi #     "f*" (    ) if [[ "filename" == "f*" ]]; then echo "Match" fi #    "too many arguments",   f*   #     if [ "filename" == f* ]; then echo "Match" fi 

Verificar valores


Use aspas em vez de caracteres extras sempre que possível.


O Bash é inteligente o suficiente para trabalhar com uma string vazia em um teste. Portanto, o código resultante é muito mais fácil de ler; use verificações de valores vazios / não vazios ou valores vazios, sem usar caracteres adicionais.


 #  : if [[ "${my_var}" = "some_string" ]]; then do_something fi # -z (   ),  -n (    ): #      if [[ -z "${my_var}" ]]; then do_something fi #   ( ),   : if [[ "${my_var}" = "" ]]; then do_something fi #   : if [[ "${my_var}X" = "some_stringX" ]]; then do_something fi 

Para evitar confusão sobre o que você está verificando, use explicitamente -z ou -n .


 #   if [[ -n "${my_var}" ]]; then do_something fi #  ,    ,  ${my_var} #     . if [[ "${my_var}" ]]; then do_something fi 

Expressões de substituição para nomes de arquivo


Use o caminho explícito ao criar expressões curinga para nomes de arquivos.


Como os nomes dos arquivos podem começar com o caractere - , é muito mais seguro ./* expressão curinga como ./* vez de * .


 #   : # -f -r somedir somefile #        force psa@bilby$ rm -v * removed directory: `somedir' removed `somefile' #   : psa@bilby$ rm -v ./* removed `./-f' removed `./-r' rm: cannot remove `./somedir': Is a directory removed `./somefile' 

Eval


eval deve ser evitada.


O Eval permite expandir as variáveis ​​passadas na entrada, mas também pode definir outras variáveis, sem a possibilidade de verificá-las.


 #   ? #   ?   ? eval $(set_my_variables) #  ,         ? variable="$(eval some_function)" 

Canos em While


Use substituição de comando ou o loop for , em vez de canalizar while . As variáveis ​​alteradas no while não se propagam para o pai, porque os comandos do loop são executados em um sub-shell.


Uma subcasca implícita no canal while pode dificultar o rastreamento de erros.


 last_line='NULL' your_command | while read line; do last_line="${line}" done #   'NULL' echo "${last_line}" 

Use um loop for se tiver certeza de que a entrada não conterá espaços ou caracteres especiais (geralmente isso não implica na entrada do usuário).


 total=0 #  ,       . for value in $(command); do total+="${value}" done 

O uso da substituição de comando permite redirecionar a saída, mas executa comandos em um sub-shell explícito, diferente do sub-shell implícito, que cria o bash para o while .


 total=0 last_file= while read count filename; do total+="${count}" last_file="${filename}" done < <(your_command | uniq -c) #         # . echo "Total = ${total}" echo "Last one = ${last_file}" 

Use while loops onde não há necessidade de passar resultados complexos para o shell pai - isso é típico quando é necessária uma "análise" mais complexa. Lembre-se de que exemplos simples às vezes são muito mais fáceis de resolver usando uma ferramenta como o awk. Também pode ser útil quando você não deseja modificar especificamente as variáveis ​​do ambiente pai.


 #    awk: # awk '$3 == "nfs" { print $2 " maps to " $1 }' /proc/mounts cat /proc/mounts | while read src dest type opts rest; do if [[ ${type} == "nfs" ]]; then echo "NFS ${dest} maps to ${src}" fi done 

Convenção de nomenclatura


Nomes de Função


Minúsculas com sublinhados para separar as palavras. Separe as bibliotecas com :: . Os colchetes são necessários após o nome da função. A palavra-chave function é opcional, mas se usada, é consistente em todo o projeto.


Se você escrever funções separadas, use minúsculas e palavras separadas com sublinhados. Se você estiver escrevendo um pacote, separe os nomes dos pacotes com :: . Os colchetes devem estar na mesma linha que o nome da função (como em outros idiomas do Google) e não devem ter espaço entre o nome da função e o colchete.


 #   my_func() { ... } #   mypackage::my_func() { ... } 

Quando "()" vem após o nome da função, a palavra-chave da função parece redundante, mas melhora a identificação rápida de funções.


Nome da variável


Em relação aos nomes das funções.


Os nomes de variáveis ​​para loops devem ser igualmente nomeados para qualquer variável sobre a qual você itere.


 for zone in ${zones}; do something_with "${zone}" done 

Nomes de constante da variável de ambiente


Todas as letras maiúsculas, separadas por sublinhados, são declaradas na parte superior do arquivo.


As constantes e tudo o que é exportado para o ambiente devem estar em maiúsculas.


 #  readonly PATH_TO_FILES='/some/path' # ,   declare -xr ORACLE_SID='PROD' 

Algumas coisas permanecem constantes quando são instaladas pela primeira vez (por exemplo, via getopts ). Portanto, é bastante normal definir uma constante através de getopts ou com base em uma condição, mas isso deve ser feito readonly após a readonly . Observe que declare não funciona com variáveis ​​globais dentro de funções, portanto, readonly ou export recomendada.


 VERBOSE='false' while getopts 'v' flag; do case "${flag}" in v) VERBOSE='true' ;; esac done readonly VERBOSE 

Nomes dos arquivos de origem


Letras minúsculas, com sublinhado para separar palavras, se necessário.


Isso se aplica à correspondência com outros estilos de código no Google: maketemplate ou make_template , mas não o make-template .


Variáveis ​​somente leitura


Use readonly ou declare -r para garantir que sejam somente leitura.


shell, . , , .


 zip_version="$(dpkg --status zip | grep Version: | cut -d ' ' -f 2)" if [[ -z "${zip_version}" ]]; then error_message else readonly zip_version fi 


, , local . .


, , local . , .


, ; local exit code .


 my_func2() { local name="$1" #      : local my_var my_var="$(my_func)" || return #   : $?  exit code  'local',   my_func local my_var="$(my_func)" [[ $? -eq 0 ]] || return ... } 


. .


, . , set , .


. .


main


, main , , .


, main . , ( , ). main:


 main "$@" 

, , , main — , .




.


$? if , .


:


 if ! mv "${file_list}" "${dest_dir}/" ; then echo "Unable to move ${file_list} to ${dest_dir}" >&2 exit "${E_BAD_MOVE}" fi #  mv "${file_list}" "${dest_dir}/" if [[ "$?" -ne 0 ]]; then echo "Unable to move ${file_list} to ${dest_dir}" >&2 exit "${E_BAD_MOVE}" fi Bash    `PIPESTATUS`,         .           ,   : ```bash tar -cf - ./* | ( cd "${dir}" && tar -xf - ) if [[ "${PIPESTATUS[0]}" -ne 0 || "${PIPESTATUS[1]}" -ne 0 ]]; then echo "Unable to tar files to ${dir}" >&2 fi 

, PIPESTATUS , - , , PIPESTATUS ( , [ PIPESTATUS ).


 tar -cf - ./* | ( cd "${DIR}" && tar -xf - ) return_codes=(${PIPESTATUS[*]}) if [[ "${return_codes[0]}" -ne 0 ]]; then do_something fi if [[ "${return_codes[1]}" -ne 0 ]]; then do_something_else fi 


shell , .


, bash, ( , sed ).


:


 #  : addition=$((${X} + ${Y})) substitution="${string/#foo/bar}" #  : addition="$(expr ${X} + ${Y})" substitution="$(echo "${string}" | sed -e 's/^foo/bar/')" 

Conclusão


.


Reserve alguns minutos para ler a seção Palavras de despedida, na parte inferior do manual do C ++ .

Source: https://habr.com/ru/post/pt413155/


All Articles