Не бойтесь сценариев 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 Mode ∘ Other 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 |
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 |
$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 |
Вышеупомянутый скрипт проверяет, равно ли количество параметров двум. Если это не так, отображается 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 | |
} |
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 |
В руководстве предлагается, чтобы все сообщения об ошибках отправлялись на 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" |
Обратите внимание, что при присвоении значения переменной нет пробелов перед знаком равенства и после него. Первый использует знак +=
для присоединения второго значения к первому. Во втором используются двойные кавычки с пробелом между переменными. В последнем случае используется фигурная скобка, поскольку за переменной 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 |
Результатом является подробная трассировка выполнения скрипта.
+ 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 |
Первый разрезает строку от первой буквы до третьей. Второй - от второго до четвертого. Последний нарезает пятую букву.
Расширение параметра выполняет извлечение подстроки ${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 |
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" |
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. |
${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" |
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 |
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" |
Выход:
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 |
&>/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 |
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##*/}" |
На этой странице 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" |
Не все программисты используют один и тот же языковой стандарт. Программисты из Франции могут использовать 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 |
Вы можете использовать +
вместо —
, чтобы отключить атрибут.
#!/usr/bin/env bash | |
declare -i var1=5 | |
# error | |
# var1="can't" | |
# works | |
var1=7 | |
echo "$var1" |
Мы объявили 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" |
Обратите внимание, что когда вы используете 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 |
Следующее сохраняет статус выхода для проверки того, существует ли файл и является ли он каталогом.
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 $? |
Первый возвращается:
/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, став участником.
использованная литература
- Https://tldp.org/LDP/abs/html/special-chars.html
- Https://tldp.org/LDP/abs/html/parameter-substitution.html#PARAMSUBREF