Статьи

Сценарии оболочки — лучшие практики

Большинство языков программирования имеют набор «лучших практик», которые следует соблюдать при написании кода на этом языке. Тем не менее, я не смог найти исчерпывающего описания сценариев оболочки, поэтому решил написать свой собственный, основываясь на своем опыте написания сценариев оболочки за эти годы.

Замечание о переносимости : так как я в основном пишу сценарии оболочки для запуска в системах, в которых установлен Bash 4.2, мне не нужно сильно беспокоиться о переносимости, но вам может понадобиться! Список ниже написан с учетом Bash 4.2 (и других современных оболочек). Если вы пишете переносимый скрипт, некоторые пункты не будут применяться. Само собой разумеется, вы должны выполнить достаточное тестирование после внесения любых изменений на основе этого списка.

Вот мой список лучших практик для сценариев оболочки (в произвольном порядке):

  1. Используйте функции
  2. Документируйте свои функции
  3. Используйте shift для чтения аргументов функции
  4. Объявите свои переменные
  5. Укажите все расширения параметров
  6. При необходимости используйте массивы
  7. Используйте "$@" для ссылки на все аргументы
  8. Используйте имена переменных в верхнем регистре только для переменных среды
  9. Предпочитают встроенные функции оболочки над внешними программами
  10. Избегайте ненужных трубопроводов
  11. Избегайте разбора ls
  12. Используйте шатание
  13. Используйте вывод с нулевым разделителем, где это возможно
  14. Не используйте спины
  15. Используйте замену процесса вместо создания временных файлов
  16. Используйте mktemp если вам нужно создавать временные файлы
  17. Используйте [[ и (( для условий тестирования
  18. Используйте команды в тестовых условиях вместо состояния выхода
  19. Используйте set -e
  20. Пишите сообщения об ошибках в stderr

Каждый из пунктов выше описан в некоторых деталях ниже.

  1. Используйте функции

    Если вы не пишете очень маленький сценарий, используйте функции для модульного кодирования и сделайте его более читабельным, многоразовым и обслуживаемым. Шаблон, который я использую для всех моих скриптов, показан ниже. Как видите, весь код написан внутри функций. Сценарий начинается с вызова main функции.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    #!/bin/bash
    set -e
     
    usage() {
    }
     
    my_function() {
    }
     
    main() {
    }
     
    main "$@"
  2. Документируйте свои функции

    Добавьте достаточное количество документации к своим функциям, чтобы указать, что они делают и какие аргументы необходимы для их вызова. Вот пример:

    1
    2
    3
    4
    5
    # Processes a file.
    # $1 - the name of the input file
    # $2 - the name of the output file
    process_file(){
    }
  3. Используйте shift для чтения аргументов функции

    Вместо использования $1 , $2 т. Д. Для выбора аргументов функции используйте shift как показано ниже. Это упрощает изменение порядка аргументов, если вы передумаете позже.

    1
    2
    3
    4
    5
    6
    7
    # Processes a file.
    # $1 - the name of the input file
    # $2 - the name of the output file
    process_file(){
        local -r input_file="$1"shift
        local -r output_file="$1"; shift
    }
  4. Объявите свои переменные

    Если ваша переменная является целым числом, объявите ее как таковую. Кроме того, делайте все свои переменные readonly только для readonly если только вы не намереваетесь изменить их значение позже в своем скрипте. Используйте local для переменных, объявленных внутри функций. Это помогает передать ваши намерения . Если переносимость является проблемой, используйте typeset вместо declare . Вот несколько примеров:

    1
    2
    3
    4
    5
    6
    declare -r -i port_number=8080
    declare -r -a my_array=( apple orange )
     
    my_function() {
        local -r name=apple
    }
  5. Укажите все расширения параметров

    Чтобы предотвратить расщепление слов и искажение файлов, вы должны заключить в кавычки все расширения переменных. В частности, вы должны сделать это, если вы имеете дело с именами файлов, которые могут содержать пробелы (или другие специальные символы). Рассмотрим этот пример:

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    # create a file containing a space in its name
    touch "foo bar"
     
    declare -r my_file="foo bar"
     
    # try rm-ing the file without quoting the variable
    rm  $my_file
    # it fails because rm sees two arguments: "foo" and "bar"
    # rm: cannot remove `foo': No such file or directory
    # rm: cannot remove `bar': No such file or directory
     
    # need to quote the variable
    rm "$my_file"
     
    # file globbing example:
    mesg="my pattern is *.txt"
    echo $mesg
    # this is not quoted so *.txt will undergo expansion
    # will print "my pattern is foo.txt bar.txt"
     
    # need to quote it for correct output
    echo "$msg"

    Хорошей практикой является цитирование всех ваших переменных. Если вам нужно разделить слова, рассмотрите возможность использования массива. Смотрите следующий пункт.

  6. При необходимости используйте массивы

    Не храните коллекцию элементов в строке. Вместо этого используйте массив. Например:

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    # using a string to hold a collection
    declare -r hosts="host1 host2 host3"
    for host in $hosts  # not quoting $hosts here, since we want word splitting
    do
        echo "$host"
    done
     
    # use an array instead!
    declare -r -a host_array=( host1 host2 host3 )
    for host in "${host_array[@]}"
    do
        echo "$host"
    done
  7. Используйте "$@" для ссылки на все аргументы

    Не используйте $* . Обратитесь к моему предыдущему сообщению: разница между $ *, $ @, «$ *» и «$ @» . Вот пример:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    main() {
        # print each argument
        for i in "$@"
        do
            echo "$i"
        done
    }
    # pass all arguments to main
    main "$@"
  8. Используйте имена переменных в верхнем регистре только для переменных ENVIRONMENT

    Лично я предпочитаю, чтобы все переменные были строчными, кроме переменных среды. Например:

    1
    2
    3
    4
    declare -i port_number=8080
     
    # JAVA_HOME and CLASSPATH are environment variables
    "$JAVA_HOME"/bin/java -cp "$CLASSPATH" app.Main "$port_number"
  9. Предпочитаю встроенные функции оболочки над внешними программами

    Оболочка имеет возможность манипулировать строками и выполнять простую арифметику, поэтому вам не нужно вызывать такие программы, как cut и sed . Вот несколько примеров:

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    declare -r my_file="/var/tmp/blah"
     
    # instead of dirname, use:
    declare -r file_dir="{my_file%/*}"
     
    # instead of basename, use:
    declare -r file_base="{my_file##*/}"
     
    # instead of sed 's/blah/hello', use:
    declare -r new_file="${my_file/blah/hello}"
     
    # instead of bc <<< "2+2", use:
    echo $(( 2+2 ))
     
    # instead of grepping a pattern in a string, use:
    [[ $line =~ .*blah$ ]]
     
    # instead of cut -d:, use an array:
    IFS=: read -a arr <<< "one:two:three"

    Обратите внимание, что внешняя программа будет работать лучше при работе с большими файлами / вводом.

  10. Избегайте ненужных трубопроводов

    Конвейеры добавляют дополнительную нагрузку на ваш скрипт, поэтому старайтесь, чтобы ваши конвейеры были небольшими. Типичными примерами бесполезных конвейеров являются cat и echo , показанные ниже:

    1. Избегайте ненужных cat

      Если вы не знакомы с печально известной наградой «Бесполезное использование кошек», посмотрите здесь . Команда cat должна использоваться только для объединения файлов, но не для отправки вывода файла другой команде.

      1
      2
      3
      4
      # instead of
      cat file | command
      # use
      command < file
    2. Избегайте ненужного echo

      Вы должны использовать echo только если вы хотите вывести некоторый текст в stdout, stderr, файл и т. Д. Если вы хотите отправить текст другой команде, не echo его через канал! Вместо этого используйте здесь-строку. Обратите внимание, что здесь строки не являются переносимыми (но большинство современных оболочек поддерживают их), поэтому используйте heredoc, если вы пишете переносимый скрипт. (См. Мой предыдущий пост: Бесполезное использование эха .)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      # instead of
      echo text | command
      # use
      command <<< text
       
      # for portability, use a heredoc
      command << END
      text
      END
    3. Избегайте ненужных grep

      Пересылка из grep в awk или sed не нужна. Поскольку awk и sed могут работать с grep , вам не нужен grep в вашем конвейере. (Проверьте мой предыдущий пост: Бесполезное использование Grep .)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      # instead of
      grep pattern file | awk '{print $1}'
      # use
      awk '/pattern/{print $1}'
       
      # instead of
      grep pattern file | sed 's/foo/bar/g'
      # use
      sed -n '/pattern/{s/foo/bar/p}' file
    4. Другие ненужные трубопроводы

      Вот несколько других примеров:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      # instead of
      command | sort | uniq
      # use
      command | sort -u
       
      # instead of
      command | grep pattern | wc -l
      # use
      command | grep -c pattern
  11. Избегайте разбора ls

    Проблема в том, что ls выводит имена файлов, разделенные символами новой строки, поэтому если у вас есть имя файла, содержащее символ новой строки, вы не сможете его правильно проанализировать. Было бы неплохо, если бы ls мог выводить имена файлов, разделенные нулем, но, к сожалению, не может. Вместо ls используйте «globbing» файла или альтернативную команду, которая выводит имена файлов, оканчивающиеся нулем, такие как find -print0 .

  12. Используйте шатание

    Глобализация (или расширение имени файла) — это способ оболочки создать список файлов, соответствующих шаблону. В bash вы можете сделать глобализацию более мощной, включив расширенные операторы сопоставления с шаблоном, используя extglob оболочки extglob . Также включите nullglob чтобы получить пустой список, если совпадений не найдено. Globbing можно использовать вместо find в некоторых случаях и, опять же, не анализировать ls ! Вот пара примеров:

    1
    2
    3
    4
    5
    6
    7
    8
    shopt -s nullglob
    shopt -s extglob
     
    # get all files with a .yyyymmdd.txt suffix
    declare -a dated_files=( *.[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9].txt )
     
    # get all non-zip files
    declare -a non_zip_files=( !(*.zip) )
  13. Используйте вывод с нулевым разделителем, где это возможно

    Чтобы правильно обрабатывать имена файлов, содержащие пробельные символы и символы новой строки, вы должны использовать вывод с разделителями, равными нулю, в результате чего каждая строка заканчивается символом NUL ( 00 ) вместо новой строки. Большинство программ поддерживают это. Например, find -print0 выводит имена файлов, за которыми следует нулевой символ, а xargs -0 читает аргументы, разделенные нулевыми символами.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # instead of
    find . -type f -mtime +5 | xargs rm -f
    # use
    find . -type f -mtime +5 -print0 | xargs -0 rm -f
     
    # looping over files
    find . -type f -print0 | while IFS= read -r -d $'' filename; do
        echo "$filename"
    done
  14. Не используйте спины

    Используйте $(command) вместо `command` потому что это упрощает `command` нескольких команд и делает ваш код более читабельным. Вот простой пример:

    1
    2
    3
    4
    5
    # ugly escaping required when using nested backticks
    a=`command1 \`command2\``
     
    # $(...) is cleaner
    b=$(command1 $(command2))
  15. Используйте замену процесса вместо создания временных файлов

    В большинстве случаев, если команда принимает файл в качестве ввода, файл может быть заменен выводом другой команды, используя подстановку процесса: <(command) . Это избавляет вас от необходимости записывать временный файл, передавать этот временный файл команде и, наконец, удалять временный файл. Это показано ниже:

    1
    2
    3
    4
    5
    6
    7
    8
    # using temp files
    command1 > file1
    command2 > file2
    diff file1 file2
    rm file1 file2
     
    # using process substitution
    diff <(command1) <(command2)
  16. Используйте mktemp если вам нужно создавать временные файлы

    Старайтесь избегать создания временных файлов. Если необходимо, используйте mktemp для создания временного каталога, а затем запишите в него свои файлы. Убедитесь, что вы удалите каталог после того, как вы закончите.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # set up a trap to delete the temp dir when the script exits
    unset temp_dir
    trap '[[ -d "$temp_dir" ]] && rm -rf "$temp_dir"' EXIT
     
    # create the temp dir
    declare -r temp_dir=$(mktemp -dt myapp.XXXXXX)
     
    # write to the temp dir
    command > "$temp_dir"/foo
  17. Используйте [[ и (( для условий тестирования

    Предпочитайте [[ ... ]] над [ ... ] потому что это безопаснее и обеспечивает более богатый набор функций. Используйте (( ... )) для арифметических условий, потому что это позволяет вам выполнять сравнения, используя знакомые математические операторы, такие как < и > вместо -lt и -gt . Обратите внимание, что если вы хотите мобильности, вы должны придерживаться старомодного [ ... ] . Вот несколько примеров:

    1
    2
    3
    4
    5
    [[ $foo == "foo" ]] && echo "match"  # don't need to quote variable inside [[
    [[ $foo == "a" && $bar == "a" ]] && echo "match"
     
    declare -i num=5
    (( num < 10 )) && echo "match"       # don't need the $ on $num in ((
  18. Используйте команды в тестовых условиях вместо состояния выхода

    Если вы хотите проверить, была ли команда выполнена успешно, прежде чем что-то делать, используйте команду непосредственно в условии вашего оператора if вместо проверки состояния завершения команды.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    # don't use exit status
    grep -q pattern file
    if (( $? == 0 ))
    then
        echo "pattern was found"
    fi
     
    # use the command as the condition
    if grep -q pattern file
    then
        echo "pattern was found"
    fi
  19. Используйте set -e

    Поместите это в верхней части вашего сценария. Это говорит оболочке выйти из скрипта, как только любой оператор вернет ненулевой код завершения.

  20. Пишите сообщения об ошибках в stderr
    Сообщения об ошибках относятся к stderr, а не к stdout.

    1
    echo "An error message" >&2

Ссылка: Shell Scripting — Best Practices от нашего партнера JCG Фахда Шарифа в блоге fahd.blog .