Хтось знає про будь-які ресурси, які говорять про кращі практики чи схеми дизайну для скриптів оболонки (sh, bash тощо)?
Хтось знає про будь-які ресурси, які говорять про кращі практики чи схеми дизайну для скриптів оболонки (sh, bash тощо)?
Відповіді:
Я написав досить складні сценарії оболонки, і моя перша пропозиція - "не робити". Причина в тому, що досить легко зробити невелику помилку, яка перешкоджає вашому сценарію або навіть робить його небезпечним.
При цьому, у мене немає інших ресурсів, щоб передати вам, крім мого особистого досвіду. Ось те , що я зазвичай роблю, що це перебір, але , як правило, бути твердим, хоча дуже багатослівний.
Заклик
змушуйте ваш сценарій приймати довгі та короткі варіанти. будьте обережні, оскільки є дві команди для розбору варіантів, getopt та getopts. Використовуйте getopt, оскільки у вас виникнуть менше проблем.
CommandLineOptions__config_file=""
CommandLineOptions__debug_level=""
getopt_results=`getopt -s bash -o c:d:: --long config_file:,debug_level:: -- "$@"`
if test $? != 0
then
echo "unrecognized option"
exit 1
fi
eval set -- "$getopt_results"
while true
do
case "$1" in
--config_file)
CommandLineOptions__config_file="$2";
shift 2;
;;
--debug_level)
CommandLineOptions__debug_level="$2";
shift 2;
;;
--)
shift
break
;;
*)
echo "$0: unparseable option $1"
EXCEPTION=$Main__ParameterException
EXCEPTION_MSG="unparseable option $1"
exit 1
;;
esac
done
if test "x$CommandLineOptions__config_file" == "x"
then
echo "$0: missing config_file parameter"
EXCEPTION=$Main__ParameterException
EXCEPTION_MSG="missing config_file parameter"
exit 1
fi
Ще одним важливим моментом є те, що програма завжди повинна повернути нуль, якщо вона успішно завершена, не нульова, якщо щось пішло не так.
Функціональні дзвінки
Ви можете викликати функції в bash, просто не забудьте визначити їх перед викликом. Функції схожі на сценарії, вони можуть повертати лише числові значення. Це означає, що вам потрібно винайти іншу стратегію повернення рядкових значень. Моя стратегія полягає у використанні змінної під назвою RESULT для зберігання результату та повернення 0, якщо функція виконана чисто. Крім того, ви можете збільшити винятки, якщо ви повертаєте значення, відмінне від нуля, а потім встановите дві "змінні винятку" (моє: EXCEPTION і EXCEPTION_MSG), перша містить тип винятку, а друга - повідомлення, прочитане людиною.
Коли ви викликаєте функцію, параметри функції призначаються спеціальним параметрам $ 0, $ 1 і т.д. оголосити змінні всередині функції локальними:
function foo {
local bar="$0"
}
Ситуації, схильні до помилок
У bash, якщо ви не заявите інше, незмінна змінна використовується як порожній рядок. У випадку помилки друку це дуже небезпечно, оскільки про неправильно набрану змінну не буде повідомлено, і вона буде оцінена як порожня. використання
set -o nounset
щоб не допустити цього. Однак будьте обережні, тому що якщо ви це зробите, програма буде скасовувати кожен раз, коли ви оцінюєте невизначену змінну. З цієї причини єдиним способом перевірити, чи не визначена змінна, є наступний:
if test "x${foo:-notset}" == "xnotset"
then
echo "foo not set"
fi
Ви можете оголосити змінні як прочитані тільки:
readonly readonly_var="foo"
Модуляризація
Ви можете домогтися модуляризації типу "python like", якщо використовувати наступний код:
set -o nounset
function getScriptAbsoluteDir {
# @description used to get the script path
# @param $1 the script $0 parameter
local script_invoke_path="$1"
local cwd=`pwd`
# absolute path ? if so, the first character is a /
if test "x${script_invoke_path:0:1}" = 'x/'
then
RESULT=`dirname "$script_invoke_path"`
else
RESULT=`dirname "$cwd/$script_invoke_path"`
fi
}
script_invoke_path="$0"
script_name=`basename "$0"`
getScriptAbsoluteDir "$script_invoke_path"
script_absolute_dir=$RESULT
function import() {
# @description importer routine to get external functionality.
# @description the first location searched is the script directory.
# @description if not found, search the module in the paths contained in $SHELL_LIBRARY_PATH environment variable
# @param $1 the .shinc file to import, without .shinc extension
module=$1
if test "x$module" == "x"
then
echo "$script_name : Unable to import unspecified module. Dying."
exit 1
fi
if test "x${script_absolute_dir:-notset}" == "xnotset"
then
echo "$script_name : Undefined script absolute dir. Did you remove getScriptAbsoluteDir? Dying."
exit 1
fi
if test "x$script_absolute_dir" == "x"
then
echo "$script_name : empty script path. Dying."
exit 1
fi
if test -e "$script_absolute_dir/$module.shinc"
then
# import from script directory
. "$script_absolute_dir/$module.shinc"
elif test "x${SHELL_LIBRARY_PATH:-notset}" != "xnotset"
then
# import from the shell script library path
# save the separator and use the ':' instead
local saved_IFS="$IFS"
IFS=':'
for path in $SHELL_LIBRARY_PATH
do
if test -e "$path/$module.shinc"
then
. "$path/$module.shinc"
return
fi
done
# restore the standard separator
IFS="$saved_IFS"
fi
echo "$script_name : Unable to find module $module."
exit 1
}
Ви можете імпортувати файли з розширенням .shinc із наступним синтаксисом
імпортувати "AModule / ModuleFile"
Який буде шуканий у SHELL_LIBRARY_PATH. Коли ви завжди імпортуєте у глобальну область імен, не забудьте встановити всі свої функції та змінні з відповідним префіксом, інакше ви ризикуєте зіткненням імен. Я використовую подвійне підкреслення як крапку пітона.
Крім того, поставте це як перше, що у вашому модулі
# avoid double inclusion
if test "${BashInclude__imported+defined}" == "defined"
then
return 0
fi
BashInclude__imported=1
Об'єктно-орієнтоване програмування
В bash, ви не можете робити об'єктно-орієнтоване програмування, якщо тільки ви не побудуєте досить складну систему розподілу об'єктів (я думав про це. Це можливо, але божевільно). На практиці, однак, ви можете робити "програмування, орієнтоване на одиночку": у вас є один екземпляр кожного об'єкта і лише один.
Що я роблю: я визначаю об'єкт у модулі (див. Запис модуляризації). Тоді я визначаю порожні vars (аналогічні змінним членам) функцію init (конструктор) та функції члена, як у цьому прикладі коду
# avoid double inclusion
if test "${Table__imported+defined}" == "defined"
then
return 0
fi
Table__imported=1
readonly Table__NoException=""
readonly Table__ParameterException="Table__ParameterException"
readonly Table__MySqlException="Table__MySqlException"
readonly Table__NotInitializedException="Table__NotInitializedException"
readonly Table__AlreadyInitializedException="Table__AlreadyInitializedException"
# an example for module enum constants, used in the mysql table, in this case
readonly Table__GENDER_MALE="GENDER_MALE"
readonly Table__GENDER_FEMALE="GENDER_FEMALE"
# private: prefixed with p_ (a bash variable cannot start with _)
p_Table__mysql_exec="" # will contain the executed mysql command
p_Table__initialized=0
function Table__init {
# @description init the module with the database parameters
# @param $1 the mysql config file
# @exception Table__NoException, Table__ParameterException
EXCEPTION=""
EXCEPTION_MSG=""
EXCEPTION_FUNC=""
RESULT=""
if test $p_Table__initialized -ne 0
then
EXCEPTION=$Table__AlreadyInitializedException
EXCEPTION_MSG="module already initialized"
EXCEPTION_FUNC="$FUNCNAME"
return 1
fi
local config_file="$1"
# yes, I am aware that I could put default parameters and other niceties, but I am lazy today
if test "x$config_file" = "x"; then
EXCEPTION=$Table__ParameterException
EXCEPTION_MSG="missing parameter config file"
EXCEPTION_FUNC="$FUNCNAME"
return 1
fi
p_Table__mysql_exec="mysql --defaults-file=$config_file --silent --skip-column-names -e "
# mark the module as initialized
p_Table__initialized=1
EXCEPTION=$Table__NoException
EXCEPTION_MSG=""
EXCEPTION_FUNC=""
return 0
}
function Table__getName() {
# @description gets the name of the person
# @param $1 the row identifier
# @result the name
EXCEPTION=""
EXCEPTION_MSG=""
EXCEPTION_FUNC=""
RESULT=""
if test $p_Table__initialized -eq 0
then
EXCEPTION=$Table__NotInitializedException
EXCEPTION_MSG="module not initialized"
EXCEPTION_FUNC="$FUNCNAME"
return 1
fi
id=$1
if test "x$id" = "x"; then
EXCEPTION=$Table__ParameterException
EXCEPTION_MSG="missing parameter identifier"
EXCEPTION_FUNC="$FUNCNAME"
return 1
fi
local name=`$p_Table__mysql_exec "SELECT name FROM table WHERE id = '$id'"`
if test $? != 0 ; then
EXCEPTION=$Table__MySqlException
EXCEPTION_MSG="unable to perform select"
EXCEPTION_FUNC="$FUNCNAME"
return 1
fi
RESULT=$name
EXCEPTION=$Table__NoException
EXCEPTION_MSG=""
EXCEPTION_FUNC=""
return 0
}
Захоплення та обробка сигналів
Я вважаю це корисним для лову та виправлення винятків.
function Main__interruptHandler() {
# @description signal handler for SIGINT
echo "SIGINT caught"
exit
}
function Main__terminationHandler() {
# @description signal handler for SIGTERM
echo "SIGTERM caught"
exit
}
function Main__exitHandler() {
# @description signal handler for end of the program (clean or unclean).
# probably redundant call, we already call the cleanup in main.
exit
}
trap Main__interruptHandler INT
trap Main__terminationHandler TERM
trap Main__exitHandler EXIT
function Main__main() {
# body
}
# catch signals and exit
trap exit INT TERM EXIT
Main__main "$@"
Підказки та поради
Якщо з якихось причин щось не працює, спробуйте змінити порядок коду. Порядок важливий і не завжди інтуїтивний.
навіть не розглядайте роботу з tcsh. він не підтримує функції, і це взагалі жахливо.
Сподіваюся, це допоможе, хоча зверніть увагу. Якщо вам доведеться використовувати такі речі, про які я писав тут, це означає, що ваша проблема занадто складна, щоб її вирішити з оболонкою. використовувати іншу мову. Мені довелося використовувати його через людські чинники та спадщину.
getopt
vs getopts
? getopts
є більш портативним і працює в будь-якій оболонці POSIX. Тим більше, що питання полягає в найкращій роботі з оболонками, а не в конкретних обставинах, я б підтримав відповідність POSIX для підтримки кількох оболонок, коли це можливо.
Погляньте на Розширений посібник з сценаріїв Bash, щоб отримати багато мудрості в сценаріях оболонок - і не тільки Bash.
Не слухайте людей, які говорять вам про перегляд інших, можливо більш складних мов. Якщо сценарій оболонки відповідає вашим потребам, використовуйте це. Ви хочете функціональність, а не вигадливість. Нові мови надають цінні нові навички для вашого резюме, але це не допоможе, якщо у вас є робота, яку потрібно виконати, і ви вже знаєте оболонку.
Як зазначалося, існує не так багато "найкращих практик" чи "моделей дизайну" для сценаріїв оболонок. Різне використання має різні рекомендації та упередження - як і будь-яка інша мова програмування.
скрипт оболонки - це мова, призначена для управління файлами та процесами. Хоча це чудово для цього, це не мова загального призначення, тому завжди намагайтеся склеїти логіку з існуючих утиліт, а не відтворювати нову логіку в скрипті оболонки.
Окрім цього загального принципу, я зібрав деякі поширені помилки сценарію оболонки .
Цього року в OSCON відбувся чудовий сеанс на цю тему: http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf
Знайте, коли ним користуватися. Для швидкого та брудного склеювання команд це добре. Якщо вам потрібно прийняти більше ніж кілька нетривіальних рішень, циклів і нічого, перейдіть на Python, Perl та модулюйте .
Найбільша проблема з оболонкою часто полягає в тому, що кінцевий результат просто схожий на велику кульку грязі, 4000 ліній і тріщину ... і ви не можете її позбутися, тому що тепер від цього залежить весь ваш проект. Звичайно, він стартував з 40 ліній красивого баша.
Легко: використовуйте python замість оболонок скриптів. Ви отримуєте читабельність майже в 100 разів, не ускладнюючи нічого, що вам не потрібно, і зберігаючи здатність еволюціонувати частини вашого сценарію у функції, об'єкти, стійкі об’єкти (zodb), розподілені об'єкти (pyro) майже без жодного додатковий код.
Щоб знайти деякі "найкращі практики", подивіться, як дистрибутив Linux (наприклад, Debian) пише свої init-скрипти (зазвичай їх можна знайти в /etc/init.d)
Більшість з них не мають "bash-isms" і мають гарне розділення налаштувань конфігурації, бібліотеки-файлів та форматування джерела.
Мій особистий стиль - написати головний оболонку, яка визначає деякі змінні за замовчуванням, а потім намагається завантажити ("джерело") файл конфігурації, який може містити нові значення.
Я намагаюся уникати функцій, оскільки вони, як правило, ускладнюють сценарій. (Perl був створений для цієї мети.)
Щоб переконатися, що сценарій є портативним, протестуйте не тільки #! / Bin / sh, але також використовуйте #! / Bin / ash, #! / Bin / dash тощо. Ви досить швидко помітите специфічний код Bash.