Не бойтесь сценариев Bash. После прочтения этих советов будет легче

[Обновлено 2021–02-18. Коды изменены на Gist и добавлены ссылки]

Table of Contents
Introduction
1. Clean Structure
2. Install ShellCheck on Your Editor
3. Usage Function
4. Error Messages
5. Function Comments
6. How To Concatenate String Variables
7. How To Set a Debug ModeOther useful set options
8. How To Slice Strings
9. How To Make Sure Users Use a Correct Bash Version
10. How To Transform a String Case
11. Ternary Operator-Like Statement
12. How To Find the Length of a String and an Array
13. How To Unset Variables
14. How To Set a Default Value
15. How To Determine Your Bash Script Name
16. How To Make a Variable Constant
17. How To Find OSs
18. How To Determine Which HTTP Get Tool the System Has Installed
19. How To Determine Which Python the System Has Installed
20. Parameter expansion: How to Find the Script Name and Directory
21. How To Make Status Messages
22. How To Set Locale
23. Type Hinting in Bash?
24. Store Exit Status in a Variable
25. Using Trap for Unexpected Termination
26. Don’t Reinvent the Wheel
27. Subshell and Exit Status
Conclusion
Newsletter
References

Вступление

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

1. Чистая структура

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

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

#!/usr/bin/env bash
#####################################
# Author: Your name
# Version: v1.0.0
# Date: 2021-02-20
# Description: This script does this and that.
# Usage: myscript <directory_name> <file_name>
###########################
# Global variables ##############
# Functions #####################
# Main body #####################
exit 0
view raw declare.txt hosted with ❤ by GitHub

2. Установите ShellCheck в свой редактор.

ShellCheck - это инструмент статического анализа скриптов оболочки. Установив ShellCheck в свой редактор, вы сможете избежать многих подводных камней для начинающих. После установки вы можете запустить его на своем терминале.

$ shellcheck my_awesome_script

Или вы можете установить его на VS Code.

Если вас интересует проверка списка кодов ошибок ShellCheck, проверьте эту суть.

3. Функция использования

Если ваш скрипт использует позиционные параметры, добавьте функцию, объясняющую, как и какие пользователи могут использовать эти параметры.

usage() {
echo "Usage: $0 [ -d DAYS ] [ -f FROM_DIR ] [ -t TO_DIR ]"
exit 2
}
while getopts "f:d:t:?h" opt; do
case $opt in
f) FROM_DIR=$OPTARG ;;
d) DAYS=$OPTARG ;;
t) TO_DIR=$OPTARG ;;
h | *) usage ;;
esac
done
view raw parameters.sh hosted with ❤ by GitHub

$0 выводит имя сценария. В приведенном выше случае функция usage будет вызываться, когда пользователь использует h или любые буквы, кроме f, d или t.

#!/usr/bin/env bash
usage() {
echo "Usage: $0 <match_text> <filename>"
exit 2
}
if [ $# -ne 2 ]; then
usage
exit 1
fi
view raw usage.sh hosted with ❤ by GitHub

Вышеупомянутый скрипт проверяет, равно ли количество параметров двум. Если это не так, отображается usage.

Вы можете использовать Bash Heredoc:

usage(){
cat <<EOF
my_script_name
Description: Your description.
Usage: movies [flag] or movies [movieToSearch]
-u Update
-h Show the help
-v Get the tool version
-d Show detailed information
Examples:
my_script_name something
my_script_name anything
EOF
}
view raw Heredoc.sh hosted with ❤ by GitHub

4. Сообщения об ошибках

Руководство по стилю оболочки Google рекомендует функцию для распечатки сообщений вместе с другой информацией о статусе.

err() {
echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $*" >&2
}
if ! do_something; then
err "Unable to do_something"
exit 1
fi
view raw messages.sh hosted with ❤ by GitHub

В руководстве предлагается, чтобы все сообщения об ошибках отправлялись на STDERR, потому что это упрощает отделение нормального состояния от реальных проблем.

5. Комментарии к функциям

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

#######################################
# Description: This and that.
# Globals:
# BACKUP_DIR
# Arguments:
# None
# Outputs:
# My outputs
# Returns:
# 0 if thing was deleted, non-zero on error.
#######################################
myfunction() {
}

6. Как объединить строковые переменные

В сценарии Bash вы можете объединять строки по-разному.

#!/usr/bin/env bash
# method 1
foo="I like"
foo+=" Bash Scripting."
echo "$foo"
# method 2
foo="I like"
bar="Bash Scripting."
foobar="$foo $bar"
echo "$foobar"
# method 3
foo="Writ"
foo="${foo}ing Bash Scripting is fun."
echo "$foo"
view raw concatenate.sh hosted with ❤ by GitHub

Обратите внимание, что при присвоении значения переменной нет пробелов перед знаком равенства и после него. Первый использует знак += для присоединения второго значения к первому. Во втором используются двойные кавычки с пробелом между переменными. В последнем случае используется фигурная скобка, поскольку за переменной foo сразу следует символ.

7. Как установить режим отладки

Используйте set -x. Устанавливается опция -x или xtrace. Это отображает развернутые команды и переменные данной строки кода перед запуском кода.

#!/usr/bin/env bash
set -x
foo="Bash"
if [ $foo == "Bash" ]; then
echo "$foo"
else
echo "Not Bash"
fi
view raw setdebugmode.sh hosted with ❤ by GitHub

Результатом является подробная трассировка выполнения скрипта.

+ foo=Bash
+ '[' Bash == Bash ']'
+ echo Bash
Bash

Легко увидеть, как скрипт запускается и присваивает значения.

Другие полезные параметры набора

set -e немедленно завершает работу, если команда завершается с ненулевым статусом.

set -u при подстановке рассматривает неустановленные переменные как ошибку.

set -C запрещает перезапись существующих обычных файлов.

Таким образом, вы можете использовать set -Ceu в начале вашего скрипта.

Узнайте больше о set использовании help set.

8. Как нарезать струны

Вы можете использовать cut -cstart-end, чтобы разрезать строку.

#!/usr/bin/env bash
echo "abcdefg" | cut -c1-3
echo "abcdefg" | cut -c2-4
echo "abcdefg" | cut -c5-5
view raw slicestrings.sh hosted with ❤ by GitHub

Первый разрезает строку от первой буквы до третьей. Второй - от второго до четвертого. Последний нарезает пятую букву.

Расширение параметра выполняет извлечение подстроки ${s:off-set-index:length}:

#!/usr/bin/env bash
s="abcdefg"
echo ${s:0:3}
echo ${s:1:3}
echo ${s:4:1}
# output
# abc
# bcd
# e

Смещение отсчитывается от нуля.

9. Как убедиться, что пользователи используют правильную версию Bash

Следующий сценарий гарантирует, что пользователи будут использовать Bash версии 4.0 или выше.

if ((BASH_VERSINFO[0] < 4)); then
    printf '%s\n' "Error: This requires Bash v4.0 or higher. You have version $BASH_VERSION." 1>&2
    exit 2
fi

10. Как преобразовать регистр строки

Когда вы читаете вводимые пользователем данные, вводимые данные могут быть в нижнем или верхнем регистре. Для Bash ниже версии 4 вы можете изменить его на верхний регистр, используя команду tr с [:lower:] и [:upper:].

#!/usr/bin/env bash
set -x
echo -n "Can you say Hello in Japanese?"
read -r answer
answer=$(echo "$answer" | cut -c 1-1 | tr "[:lower:]" "[:upper:]")
if [ "$answer" = Y ]
then
echo "Wow you are awesome."
else
echo "Neither can I."
fi

Обратите внимание, что мы установили параметр -x для отладки.

Строка 4: чтение пользовательского ввода.

Строка 5: каналы, |, позволяют использовать выходные данные одной команды в качестве входных данных другой команды. Мы cut первую букву, затем преобразовываем ее из нижнего регистра в верхний. Вы можете поменять местами строчные и прописные буквы, чтобы преобразовать их с верхнего регистра в нижний.

В Bash 4+ вы можете использовать операторы модификации case.

#!/usr/bin/env bash
fox=theQuickBrownFOX
# theQuickBrownFOX
echo ${fox}
# TheQuickBrownFOX, First char uppercase.
echo ${fox^}
# THEQUICKBROWNFOX, All chars uppercase.
echo ${fox^^}
# theQuickBrownFOX, First char lowercase.
echo ${fox,}
# thequickbrownfox, All chars lowercase.
echo ${fox,,}

Используя эти операторы модификации регистра, вы можете создавать функции.

#!/usr/bin/env bash
fox=theQuickBrownFOX
first_upper() {
echo "${1^}"
}
to_upper() {
echo "${1^^}"
}
first_lower() {
echo "${1,}"
}
to_lower() {
echo "${1,,}"
}
echo $fox
first_upper $fox
to_upper $fox
first_lower $fox
to_lower $fox

11. Утверждение, подобное тернарному оператору.

В Bash нет тернарного оператора, но вы можете сделать то же самое для простых назначений переменных.

Мы можем написать if оператор:

if [ $foo -ge $bar ]; then
  baz="Smile!"
else
  baz="Sleep!"
fi

Используя вместе && и ||, мы можем создать оператор, аналогичный тернарному оператору:

[ $foo -ge $bar ] && baz="Smile!" || baz="Sleep!"
#!/usr/bin/env bash
foo=3
bar=5
[ $foo -ge $bar ] && baz="$foo is greater than $bar" || baz="$foo is smaller than $bar"
echo $baz
foo=5
bar=3
[ $foo -ge $bar ] && baz="$foo is greater than $bar" || baz="$foo is smaller than $bar"
echo $baz
view raw ternary-like.sh hosted with ❤ by GitHub

12. Как найти длину строки и массива

${#string} возвращает длину строки и аналогично ${#array[@]} возвращает длину массива.

#!/usr/bin/env bash
string="this length is 18."
str_len=${#string}
echo "$str_len"
array=(one two three)
arr_len=${#array[@]}
echo "$arr_len"
view raw stringlength.sh hosted with ❤ by GitHub

13. Как сбросить переменные

Отмена установки переменных гарантирует, что вы не используете предопределенные переменные.

В начале сценария:

#!/usr/bin/env bash
unset var1 var2 var3
var1=some_value
...

14. Как установить значение по умолчанию

${foo-$DEFAULT} и ${foo=$DEFAULT} оцениваются как $DEFAULT, если foo не установлен.

${foo:-$DEFAULT} и ${foo:=$DEFAULT} оценивается как $DEFAULT, если foo не установлен или пуст.

#!/usr/bin/env bash
# If baz is not set, evaluate expression as $DEFAULT.
DEFAULT=1
# foo=${baz-$DEFAULT}
# same
foo1=${baz=$DEFAULT}
echo "$foo1"
# If baz is not set or is empty, evaluate expression as $DEFAULT.
DEFAULT=2
baz=""
# bar=${baz:-$DEFAULT}
# same
foo2=${baz:=$DEFAULT}
echo "$foo2"
# bar=${baz-$DEFAULT} # this will return null string since baz is set.
view raw setdefault.sh hosted with ❤ by GitHub

${foo+$OTHER} и ${foo:+OTHER} оцениваются как $OTHER, если foo установлен, в противном случае - как пустая строка.

#!/usr/bin/env bash
baz2=3
foo3=${baz2+$OTHER}
echo "$foo3" # returns null string
# same
foo4=${baz2:+$OTHER}
echo "$foo4" # returns null string
OTHER=3
foo5=${baz2+$OTHER}
echo "$foo5"
# same
foo6=${baz2:+$OTHER}
echo "$foo6"
view raw setdefault2.sh hosted with ❤ by GitHub

15. Как определить имя сценария Bash

Добавив все свои сценарии Bash в каталог ~/bin и добавив его путь к файлу конфигурации вашего терминала, ~/.bashrc или ~/.zshrc, вы можете запускать их из любого каталога. Но перед тем, как вы зададите имя сценария, лучше не использовать имя сценария где-либо еще. Для проверки используйте команду type или which.

$ type my_script
my_script not found
$ which which
which: shell built-in command

Имя сценария с my_script отсутствует, но which уже занято.

16. Как сделать переменную постоянной

readonly делает переменные и функции доступными только для чтения.

#!/bin/bash
readonly MAX=10
echo $MAX
MAX=12 # this returns an error.
echo "$MAX"

Строка 3: установить переменную, доступную только для чтения.
Строка 5: Попытка перезаписать переменную, доступную только для чтения. Но это возвращает ошибку: «строка 5: MAX: переменная только для чтения».

Выход:

10
/Users/shinokada/bin/ex6: line 5: MAX: readonly variable
10

Вы можете вывести readonly переменные:

#!/usr/bin/env bash
readonly
view raw readonly.sh hosted with ❤ by GitHub

17. Как найти ОС

В разных ОС есть разные команды. Например, Linux использует readlink, а macOS использует greadlink.

Следующий пример довольно тривиален, но он находит ОС пользователя и применяет правильную команду.

#!/usr/bin/env bash
# Using $OSTYPE
# this script is in /Users/shinokada/bin
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
# Linux
script_dir_path=$(dirname "$(readlink -f "$0")")
elif [[ "$OSTYPE" == "darwin"* ]]; then
# Mac OSX
script_dir_path=$(dirname "$(greadlink -f "$0")")
fi
echo "Your script path is $script_dir_path"
# Using uname
if [[ $(uname) == "Linux" ]]; then
os="linux"
elif [[ $(uname) == "Darwin" ]]; then
os="mac"
fi
echo "Your OS is $os"
view raw find-os.sh hosted with ❤ by GitHub

Выход:

Your script path is /Users/shinokada/bin
Your OS is mac

18. Как определить, какой инструмент HTTP Get установлен в системе

Ваш сценарий Bash может использовать инструмент HTTP GET. В разных системах используются разные инструменты. Это может быть curl, wget, http, fetch или что-то еще.

#!/usr/bin/env bash
HttpClient=""
getHttpClient() {
if command -v curl &>/dev/null; then
HttpClient="curl"
elif command -v wget &>/dev/null; then
HttpClient="wget"
elif command -v http &>/dev/null; then
HttpClient="httpie"
elif command -v fetch &>/dev/null; then
HttpClient="fetch"
else
echo "Error: This tool requires either curl, wget, httpie or fetch to be installed." >&2
return 1
fi
}
httpGet()
{
case "$configuredClient" in
curl) curl -A curl -s "$@" ;;
wget) wget -qO- "$@" ;;
httpie) http -b GET "$@" ;;
fetch) fetch -q "$@" ;;
esac
}
checkInternet()
{
httpGet github.com > /dev/null 2>&1 || { echo "Error: no active internet connection" >&2; return 1; } # query github with a get request
}
getConfiguredClient || exit 1
checkInternet || exit 1
view raw HTTP-tool.sh hosted with ❤ by GitHub

&>/dev/null перенаправляет стандартный поток вывода и стандартный поток ошибок на /dev/null. Это то же самое, что и >/dev/null 2>&1.

command -v отображает путь к исполняемому файлу или определение псевдонима конкретной команды.

19. Как определить, какой Python установлен в системе

Точно так же, если ваш скрипт использует Python, вы можете найти системный Python:

SystemPython=""
getSystemPython(){
if command -v python3 &>/dev/null; then
SystemPython="python3"
elif command -v python2 &>/dev/null; then
SystemPython="python2"
elif command -v python &>/dev/null; then
SystemPython="python"
else
echo "Error: This tool requires python to be installed."
return 1
fi
}
getSystemPython || exit 1
view raw Find-Python.sh hosted with ❤ by GitHub

20. Расширение параметров: как найти имя сценария и каталог

Используйте ${parameter##*/}, чтобы получить имя файла, и используйте ${parameter%/*}, чтобы получить путь к каталогу.

#!/usr/bin/env bash
x=/one/two/three/four/five.txt
# ${parameter%word} removes smallest suffix pattern.
# remove the last, /five.txt. Find the directory path.
echo "The path is ${x%/*}"
# ${parameter##word} removes largest prefix pattern.
# find the file name
echo "The file name is ${x##*/}"
view raw scriptname.sh hosted with ❤ by GitHub

На этой странице Tech.io объясняется, как использовать ${parameter%word}, ${parameter%%word}, ${parameter#word}, ${parameter##word}.

Вы также можете использовать basename "$0", чтобы получить имя файла.

Вы можете найти свой каталог сценариев Bash:

#!/usr/bin/env bash
c=$0
echo "${c%/*}"

Выходы:

/Users/shinokada/bin

21. Как сделать статусные сообщения

Вы можете сообщить пользователям, что делает ваш скрипт.

Если вы хотите выводить сообщения о состоянии, вы должны запустить его с имени программы. Используйте $(basename "$0"), чтобы получить имя программы. Сообщение должно быть написано о стандартной ошибке с использованием echo ... 1>&2.

#!/usr/bin/env bash
progname=$(basename "$0")
...
echo "$progname: Running this and that" 1>&2

22. Как установить языковой стандарт

Вы можете найти свою локальную среду:

$ locale
LANG="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_CTYPE="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_ALL="en_US.UTF-8"
view raw locale.sh hosted with ❤ by GitHub

Не все программисты используют один и тот же языковой стандарт. Программисты из Франции могут использовать fr_CH.UTF-8; компьютерные фанаты из Англии могут использовать en_GB.UTF-8. Чтобы убедиться, что пользователи используют правильный языковой стандарт, вы можете использовать LC_ALL для установки языкового стандарта:

LC_ALL="en_US.UTF-8"

23. Введите подсказку в Bash?

В сценарии Bash нет подсказок типа, но вы можете использовать declare для создания индексированных массивов переменных, ассоциативных массивов или целых чисел. Вы можете найти declare параметры, используя help declare.

Options which set attributes:
-a to make NAMEs indexed arrays (if supported)
-A to make NAMEs associative arrays (if supported)
-i to make NAMEs have the `integer' attribute
-l to convert the value of each NAME to lower case on assignment
-n make NAME a reference to the variable named by its value
-r to make NAMEs readonly
-t to make NAMEs have the `trace' attribute
-u to convert the value of each NAME to upper case on assignment
-x to make NAMEs export
view raw declare.txt hosted with ❤ by GitHub

Вы можете использовать + вместо , чтобы отключить атрибут.

#!/usr/bin/env bash
declare -i var1=5
# error
# var1="can't"
# works
var1=7
echo "$var1"
view raw typehint.sh hosted with ❤ by GitHub

Мы объявили var1 как целое число в строке 3. Если вы попытаетесь преобразовать его в строку, вы получите ошибку (строка 5). Но вы можете изменить его на другое целое число (строка 7).

#!/usr/bin/env bash
echome() {
var1="var 1 from echome"
# this make var2 local
declare var2="var 2 from echome"
}
echome
echo "$var1"
# no output
echo "$var2"
view raw typehint2.sh hosted with ❤ by GitHub

Обратите внимание, что когда вы используете declare в функции, переменная становится локальной переменной, к которой вы не можете получить доступ вне функции.

24. Сохранить статус выхода в переменной

Вместо использования $? в if операторах:

mkdir /tmp/tmp_dir
if [ $? = 0 ]; then
  # do something
fi

Сохраните статус выхода в переменной:

mkdir /tmp/tmp_dir
# es (exit status)
mkdir_es=$?
case $mkdir_es in
0)
echo "Created a dir" >&2
;;
1)
echo 'Must supply a parameter, exiting.' >&2
exit 1
;;
*)
echo "Unknown error $status, exiting." >&2
exit "$exit_status"
esac
view raw exit-status.sh hosted with ❤ by GitHub

Следующее сохраняет статус выхода для проверки того, существует ли файл и является ли он каталогом.

test -d /tmp/tmp_dir
test_es=$?
if [[ ${test_es} -ne 0 ]]; then  
    echo "No dir found. Exiting script!" >&2
    exit 1
fi

25. Использование ловушки для неожиданного завершения

Пользователи могут завершить ваш скрипт, используя Ctrl-c во время его работы. Если ваш сценарий изменяет каталог или файл, вам необходимо вернуть его в исходное состояние. Команда trap предназначена для этой ситуации.

Когда пользователь использует Ctrl-c, он генерирует сигнал SIGINT. Когда пользователь завершает процесс, он генерирует сигнал SIGTERM. Вы можете перечислить все сигналы, используя:

# Linux, from terminal
$ trap --list
# macOS, from a script
trap --list

Стандартные сигналы можно найти на man7.org.

Команда trap имеет форму trap "do something" signal_to_trap. Если ваш код очистки длинный, вы можете создать функцию.

Давайте перехватим сигнал Ctrl-c:

tempfile=/tmp/myfile
cleanup(){
    rm -f $tempfile
}
trap cleanup SIGINT

Подробнее о команде ловушки.

26. Не изобретайте колесо заново

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

27. Дополнительная оболочка и статус выхода

Можете ли вы найти разницу между следующими кодами?

no_func1 || (
echo "there is nothing"
exit 1
)
echo $?
no_func2 || {
echo "there is nothing"
exit 1
}
echo $?
view raw subshell.sh hosted with ❤ by GitHub

Первый возвращается:

/Users/myname/bin/ex5: line 34: no_func1: command not found
there is nothing
1

И второй возвращается:

/Users/myname/bin/ex5: line 34: no_func2: command not found
there is nothing

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

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

Заключение

Это 27 советов по созданию сценариев Bash, которые вы можете использовать в своем следующем проекте сценариев Bash. Многие из них легко использовать, и они делают ваш сценарий Bash более профессиональным.

Удачного кодирования!

Новостная рассылка

Получите полный доступ ко всем статьям на Medium, став участником.

использованная литература