Google's Shell Style Guide (in Russian)

Foreword


Which shell to use


Bash only shell script language that is allowed to use for executable files.


Scripts should start with #!/bin/bash with a minimum set of flags. Use set to set shell options so that calling your script as bash <script_name> does not violate its functionality.


Limiting all shell scripts to bash gives us a consistent shell language that is installed on all of our machines.


The only exception is if you are limited by the terms of what you are programming. One example would be the Solaris SVR4 packages, which require the use of the usual Bourne shell for any scripts.


When to use shell


Shell should be used only for small utilities or simple wrapper skrptov.


Although shell scripting is not a development language, it is used to write various utilities throughout Google. This style guide is more of a recognition of its use, rather than an offer to use it in a wide application.


Some recommendations:



Shell files and interpreter call


File extensions


Executable files should not have an extension (strongly preferred) or the .sh extension. Libraries must have the .sh extension and must not be executable.


There is no need to know in which language the program is written when it is executed, and the shell does not require an extension, so we prefer not to use it for executable files.


However, it is important for libraries to know in which language it is written, and sometimes it is necessary to have similar libraries in different languages. This allows you to have identically named library files with identical targets, but the names of different languages ​​should be identical, except for the language-specific suffix.


SUID / SGID


SUID and SGID are not allowed on shell scripts.


There are too many security issues here, which make it almost impossible to provide sufficient protection for SUID / SGID. Although bash makes it difficult to launch a SUID, it is still possible on some platforms, so we explicitly prohibit its use.


Use sudo to provide enhanced access if you need it.


Environment


STDOUT vs STDERR


All error messages should be sent to STDERR .


It helps to separate the normal state from the actual problems.


The feature for displaying error messages is recommended to be used with other state information.


 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 

Comments


File header


Start each file with a description of its contents.


Each file should have a comment title that includes a brief description of its contents. Copyright notice and author information are optional.


Example:


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

Feature Comments


Any function that is not obvious or short should be commented. Any function in the library should be commented regardless of its length or complexity.


You need to make someone else understand how to use your program or how to use the function in your library, simply by reading the comments (and the need for self-improvement) without reading the code.


All comments on features should include:



Example:


 #!/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() { ... } 

Implementation Comments


Comment on complex, non-obvious, interesting or important parts of your code.


This is assumed to be the usual practice of commenting code in Google. Do not comment on everything. If there is a complicated algorithm or you are doing something unusual, add a short comment.


TODO Comments


Use TODO comments for code that is a temporary, short-term solution or a fairly good, but not perfect.


This is in agreement with the C ++ manual .


TODO comments should include the word TODO in capital letters and then your name in parentheses. The colon is optional. It is also preferable to indicate the bug / ticket number next to the TODO element.


Example:


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

Formatting


Although you must follow a style that is already used in the files you are modifying, for any new code, the following is required.


Indentation


Indent 2 space. No tabs.


Use blank lines between blocks to improve readability. Indenting is two spaces. No matter what you do, do not use tabs. For existing files, stay true to the current indents.


String length and length of values


The maximum string length is 80 characters.


If you need to write lines longer than 80 characters, this should be done with the help of here document or, if possible, the built-in newline . Literal values ​​that can be longer than 80 characters and cannot be divided reasonably allowed, but it is strongly recommended to find a way to make them shorter.


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

Pipelines


Pipelines should be divided into one line each, if they do not fit on one line.


If plyplayn fit on one line, it should be on one line.


If not, it should be divided so that each section is on a new line and indented by 2 spaces for the next section. This refers to a chain of commands combined using '|' and also to logical connections using '||' and '&&'.


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

Cycles


Put ; do ; do and ; then ; then on the same line as while , for or if .


The cycles in the shell are slightly different, but we follow the same principles as with curly brackets when declaring functions. That is ; then ; then and ; do ; do must be in the same line as if / for / while . else must be on a separate line, and the closing statements must be on their own line, vertically aligned with the opening instruction.


Example:


 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 

Case statement



The corresponding expressions esac back one level from case and esac . Multiline actions also have indentation on a separate level. No need to put expressions in quotes. Expression templates should not precede open parentheses. Avoid using &; and ;;& notation.


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

Simple commands can be put on one line with the pattern and ;; while the expression remains readable. This is often suitable for handling single-letter options. When actions do not fit on one line, leave the template in its line, the next action, then ;; also in own line. When it is the same line as with the action, use the space after the closing bracket of the template and the other before ;; .


 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 

Variable expansion


In order of priority: observe what is already used; enclose variables in quotes; prefer "${var}" more than "$var" , but with an eye to the context of use.


These are rather recommendations as the topic is controversial enough for mandatory regulation. They are listed in order of priority.


  1. Use the same style that you find in the existing code.
  2. Put variables in quotes, see the Quotes section below.
  3. Do not put single characters for quotes and curly brackets for shell / positional parameters unless it is strictly necessary and to avoid confusion.
    Prefer braces for all other variables.


      #    #    '' : 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" 

    Quotes


    • Always use quotes for values ​​that contain variables, command substitutions, spaces, or shell metacharacters, until safe values ​​are not shown in quotes.
    • Prefer quotes for values ​​that are “words” (as opposed to command parameters or path names)
    • Never quote whole numbers in quotes.
    • Know how quotes work for match patterns in [[ .
    • Use "$@" if you have no special reason to use $* .


 # ''  ,     . # ""  ,   /. #   # "   " 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 "$#, $@") 

Features and errors


Command Substitution


Use $(command) instead of backquotes.


Nested back quotes require shielding of internal quotes using \ . The $ (command) format does not change depending on nesting and is easier to read.


Example:


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

Checks, [ and [[


[[ ... ]] is preferable to [ , test or /usr/bin/[ .


[[ ... ]] reduces the possibility of error, since the resolution of the path or the division of words between [[ and ]] and [[ ... ]] does not allow the use of a regular expression, where [ ... ] not.


 #  ,       #  `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 

Value check


Use quotation marks, not additional characters, where possible.


Bash is smart enough to work with an empty string in the test. Therefore, the resulting code is much easier to read, use checks for empty / non-empty values ​​or empty values, without using additional characters.


 #  : 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 

To avoid confusion in what you are testing, use -z or -n .


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

Wildcard Expressions for File Names


Use an explicit path when creating substitution expressions for file names.


Since file names can begin with the - character, it is much safer to ./* lookup expression as ./* instead of * .


 #   : # -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 should be avoided.


Eval allows you to expand the variables passed in the input, but it can also set other variables without being able to check them.


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

While in


Use command substitution or for loop, preferably in while . Variables changed in a while do not apply to the parent, because the loop commands are executed in a sabershell.


Implicit sables in pipe while can make it difficult to track errors.


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

Use a for loop if you are sure that the input will not contain spaces or special characters (this usually does not imply user input).


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

Using command substitution allows you to redirect the output, but executes commands in an explicit sub-variable, in contrast to the implicit sub-variable, which creates a bash for the 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 where there is no need to pass complex results to the parent shell — this is typical when more complex parsing is required. Remember that simple examples are sometimes much easier to solve using a tool like awk. This can also be useful when you do not specifically want to change the variables of the parent environment.


 #    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 

Name Agreement


Function names


In lower case, with underscores for word separation. Separate libraries with :: . After the function name, parentheses are required. The function keyword is optional, but if used, it is consistent throughout the project.


If you are writing individual functions, use lowercase and single words with underscores. If you are writing a package, separate the package names with :: . The brackets must be on the same line as the function name (as in other languages ​​in Google), and not have a space between the function name and the bracket.


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

When "()" comes after the function name, then the function keyword looks redundant, but it improves the quick identification of functions.


Variable name


As for function names.


Variable names for cycles must be named the same for any variable that you iterate over.


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

Names of environment variable constants


All capital letters, separated by underscores, are declared at the top of the file.


Constants and everything that is exported to the environment must be in upper case.


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

Some things remain constant when they are first installed (for example, via getopts ). Thus, it is quite normal to set a constant via getopts or based on a condition, but it must be made readonly immediately after that. Note that declare does not work with global variables inside functions, so readonly or export recommended instead.


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

Source file naming


Lower case, with an underscore for word separation, if necessary.


This applies to matching other styles of code in Google: maketemplate or make_template , but not make-template .


Read Only Variables


Use readonly or declare -r to ensure that they are read-only.


Since global is widely used in the shell, it is important to catch errors when working with them. When you declare a read-only variable, make it explicit.


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


Example:


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


Example:


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

Conclusion


.


, Parting Words C++ .

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


All Articles