Pull to refresh

Comments 27

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

# Named args
declare -A args=()

#Positional args
args_r=()

parse_args() {
  local name
  local value
  local length

  while [ $# -gt 0 ]; do
    local arg="$1"
    shift

    if [[ "${arg}" != --* ]]; then
      args_r+=("${arg}")
      continue
    fi

    name="${arg%=*}"
    name="${name:2}"

    length=${#name}
    length=$((length+3))

    value="${arg:${length}}"
    if [ -z "${value}" ]; then
      value=true
    fi

    args["${name}"]="${value}"
  done

  readonly args
  readonly args_r
}

parse_args $@

script_dir=$(dirname "$(realpath $0)")

Я как-то таким пользовалься для определения директории скрипта, но не уверен, насколько оно универсально и/или надежно

Как плюс - работает не только в bash. Стоит заключить в кавычки как минимум $0, иначе будут проблемы с пробелами с пути.

Как писать bash-скрипты надежно и безопасно

Верный способ — писать на высокоуровневой обертке, которая производит корректный shell-скрипт. У нас на работе шелл генерируется кодом на Clojure, очень удобно.

Еще одно правило: любой скрипт длиннее пяти строк переписывать на Python, который сейчас на каждом утюге. И тогда не будет проблем с шеллом.

На второй или на третий?

Не будет проблем с шеллом - будут проблемы с Python'ом.

Как совершенно верно заметил автор, тащить интерпретатор в каждый docker-образ - так себе идея.

А даже и притащишь, потом начнется, то модуля нет, то версия не та. Выше уже задали правильный вопрос: на какой версии python переписывать, на 2-й или 3-й?

Если вам нужно иметь возможность передавать два флага как -ab вместо -a -b, придется дописать немного кода

Например, прогнав параметры через getopt(1)

UFO just landed and posted this here

Можно ещё выносить повторяющиеся части скрипта и подключать их при необходимости. Например, если вынести setup_colors() и msg() в ~/.my_bash_lib/bash_setup_colors.sh, код будет выглядеть намного лаконичнее.

Добавляем необходимый функционал из библиотеки при необходимости:

. ~/.my_bash_lib/bash_setup_colors.sh

или

source ~/.my_bash_lib/bash_setup_colors.sh

и использовать

msg "This is a ${RED}very important${NOFORMAT} message, but not a script output value!"

Преимущества и недостатки очевидны:

  • + лаконичные скрипты, DRY

  • + внёс изменения в библиотеку - применилось везде

  • + очень удобно обслуживать (коллекционировать наработки, распространять и синхронизировать на платформах тем же git)

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

Не недостаток легко компенсируется... скриптом :) Легко реализовать механизм, позволяющий сделать скрипт автономным:

~/.my_bash_lib/bash_make_script_standalone script_using_my_lib.sh new_standalone_script.sh

Мне иногда приходится писать на bash, и для себя я написал библиотеку-скрипт (https://github.com/dodo325/ohmylinux). Скрипт состоит из множества файлов так что я использую немного более продвинутый способ импорта:

SCRIPTPATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
. $SCRIPTPATH/lib/logs.sh --source-only
. $SCRIPTPATH/lib/sysinfo.sh --source-only

--source-only - нужен потому, что сами эти файлы можно запускать для отладки отдельно. В самих же файлах расположено нечто подобное:

main() {
    print_logo
    log_info     "info"
    log_success  "success"
    log_warning  "warning"
    log_debug    "debug"
    log_error    "error"
    log_critical "critical"
}
if [ "${1}" != "--source-only" ]; then
    main "${@}"
fi

П.с.

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

Поэтому я и написал программу-builder для скриптов из нескольких файлов.

Пытался в подобное, портабельный "вездеходный" скрипт для CI/CD, который должен работать на винде и маке разработчика, в любом дистрибутиве билд-агента, и в почти любом докер-образе. В итоге уперся в то, что помимо баша необходимо гарантировать наличие дополнительных пакетов для каждой более-менее серьезной задачи. Curl, jq, unzip... в итоге, вместо того чтобы везде таскать их, и сходить с ума от сложностей баш-скриптинга - плюнул и переписал все на python. При чем написать скрипт, который работает на py2 и py3 одновременно - вообще не проблема, буквально 3 if понадобилось. Зато теперь сплю спокойно.

На глазок, если "BASH_SOURCE[0]" заменить на "$0" и параметры не складывать в массив, то на ash тоже должно работать. ash — это часть busybox, который есть ещё в более каждом утюге, чем bash.

Почти никто не скажет "да, я люблю писать bash-скрипты",

Странно. Мне вот почему-то наоборот нравится BASH. Даже больше чем Python.

Мне пора проверять состояние психического здоровья, или ещё нет?

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

Например, ./script.sh -v -f 1 работает как ожидается, а ./script.sh -f 1 -v нет - логика verbose уже не применяется

Fix is welcome

-f там не принимает значения. Поэтому «1 -v» у вас, по видимому, уехали в args. Это, разумеется, если мы об оригинальном скрипте говорим

Думаю, что для определения "домашнего" каталога скрипта цепочку dirname/realpath можно опустить и оставить только dirname.

Во многих своих скриптах я часто использу такое подобие шаблона. Некоторые определения могут отсутствовать. Кое-что добавляется в зависимости от задачи. Функции die/warn/catecho - для удобства вывода диагностических сообщений и аварийного прекращения.

#!/usr/bin/env bash

readonly ME="$( basename "$0" )"
readonly MYDIR="$( dirname "$0" )"

print_help() {
	cat - <<HELP
USAGE
    $ME [OPTIONS]

OPTIONS    
  bla-bla
HELP
}

main() {
	init
	parse_options "$@"

	# do payload	
}

init() {
	# preparations and initialization
	# set trap if needs
}

parse_options() {
	while [ $# -gt 0 ]
	do
		# parse CLI options
	done
}

# own functions' definitions doing the script payload

die() {
	warn "$@"
	exit 1
}

warn() {
	catecho "$@" >&2
}

catecho() {
	[ -t 0 ] && echo "$@" || cat -
}

main "$@"
Использование realpath нужно если скрипт вызывается через симлинку, а надо подгружать что-то лежащее непосредственно рядом со скриптом.

Можно вас попросить рассказать про catecho и как именно вы ее применяете?

Промахнулся кнопкой "Ответить". Мой ответ Вам - ниже.

realpath нужно если скрипт вызывается через симлинку, а надо подгружать что-то лежащее непосредственно рядом со скриптом.

Не вижу проблем. Ну, если какие-то особые редкие случаи в экзотических дистрибутивах или в макоси, с которыми я не сталкивался.

Вот пример

$ cat /opt/scripts/test/z
#!/usr/bin/env bash

set -x

. "$( dirname "$0" )/z.sh"

blabla

$ cat /opt/scripts/test/z.sh
blabla() {
        echo "Hello! I am $FUNCNAME"
}

$ sudo ln -s /opt/scripts/test /test

$ /test/z
++ dirname /test/z
+ . /test/z.sh
+ blabla
+ echo 'Hello! I am blabla'
Hello! I am blabla

про catecho

Ваш вопрос заставил меня немного задуматься. И я вспомнил, откуда ноги растут у этой функции. Это кусок кода для логирования всего с разными уровнями (DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL). Не скажу, что в catecho сейчас есть большая нужда. Исторически так сложилось, что эта функция перетекает из одного скрипта в другой.

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

function logMsg()
{
	local level="$1"
	shift

	catecho "$@" \
	| sed "s|^|$(date '+%Y/%m/%d %H:%M:%S') ($$) ($level): |"
}

function warn()
{
	logMsg "WARNING" "$@"
}

echo "какая-то-команда неожиданно что-то напечатала" | warn
# DATE TIME (PID) (WARNING): какая-то-команда неожиданно что-то напечатала

warn "$( echo "какая-то-команда снова что-то напечатала" )"
# DATE TIME (PID) (WARNING): какая-то-команда снова что-то напечатала

warn "какие-то проблемы - надо что-то напечатать"
# DATE TIME (PID) (WARNING): какие-то проблемы - надо что-то напечатать
Если симлинка будет не на директорию, а на сам скрипт, то с ваш пример не сработает.

Про catecho все равно не очень понятно. В вашей реализации эта функция проверяет является ли stdin терминалом и если да, то печатает аргументы функции. Если нет, печатает stdin функции. Зачем нужно такое разное поведение?

Если симлинка будет не на директорию, а на сам скрипт, то с ваш пример не сработает.

Согласен.

Про catecho все равно не очень понятно.

Вы все верно поняли. Это не то чтобы разное поведение, это одинаковый, единообразный вывод.

В примере выше я привел функцию logMsg, которая используют особенности catecho. С их помощью и с помощью других функций (как, например, warn из второго примера выше) можно единообразно логгировать. Здесь, 1-ый и 2-ой примеры взаимозаменяемы, но 1-ый, по мнению автора, элегантнее.

some-command | warn
warn "$( some-command )"
warn "some-message"

А как насчет обернуть весь скрипт в
( ... ) || exit $? && exit 0;
чтобы редактирование скрипта во время его выполнения не приводило к ошибкам?

Я баш знаю плохо, и читая сам скрипт, заметил что меня корёжит от закрытия скобки без её открытия в функции парсинга аргументов. Несколько раз пробежался по коду и всякий раз взгляд пытается найти ‘(‘ и не найдя ее вызывает тошноту. Блин кто это придумал? Неужели что-то типа такого нельзя было ->

Sign up to leave a comment.

Articles