Скрипты на Bash. Как много в этом слове. Любому разработчику рано или поздно приходится их писать. Почти никто не скажет "да, я люблю писать bash-скрипты", и поэтому этой теме уделяют мало внимания.

Я не буду пытаться сделать из вас эксперта в Bash, а просто покажу минимальный шаблон, который поможет сделать ваши скрипты более надежными и безопасными.

Лучше всего суть Bash-скриптинга была выражена недавно в одном твите:

Но у Bash есть кое-что общее с другим многими любимым языком. Как и JavaScript, он просто так не исчезнет. Хоть Bash (к счастью!) и не станет основным языком буквально для всего, он все равно всегда будет где-то рядом.

Bash можно найти почти в каждой Linux-системе, включая образы Docker. Так что если вам нужно создать скрипт для запуска серверного приложения, этапа CI/CD или запуска интеграционных тестов, Bash подойдет в самый раз.

Bash - самое простое и наиболее естественное решение, позволяющее склеить несколько команд вместе, передать вывод от одной команды к другой и запустить какой-нибудь исполняемый файл. Хотя есть смысл писать более длинные и сложные скрипты на других языках, вы не можете быть уверенными, что Python, Ruby, fish или любой другой интерпретатор, который, по вашему мнению, подойдет лучше, будет доступен везде. И вам, вероятно, следует подумать дважды, а затем еще раз, прежде чем добавлять его на какой-нибудь продакшн-сервер, в образ Docker или в CI.

Да, Bash очень далек от совершенства. Синтаксис просто кошмарный. Обработка ошибок сложна. Повсюду подводные камни. И нам придется с этим что-то делать.

Шаблон bash-скрипта

Давайте сразу к делу

Вот он!
#!/usr/bin/env bash

set -Eeuo pipefail
trap cleanup SIGINT SIGTERM ERR EXIT

script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)

usage() {
  cat <<EOF
Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-v] [-f] -p param_value arg1 [arg2...]

Script description here.

Available options:

-h, --help      Print this help and exit
-v, --verbose   Print script debug info
-f, --flag      Some flag description
-p, --param     Some param description
EOF
  exit
}

cleanup() {
  trap - SIGINT SIGTERM ERR EXIT
  # script cleanup here
}

setup_colors() {
  if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; then
    NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' YELLOW='\033[1;33m'
  else
    NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW=''
  fi
}

msg() {
  echo >&2 -e "${1-}"
}

die() {
  local msg=$1
  local code=${2-1} # default exit status 1
  msg "$msg"
  exit "$code"
}

parse_params() {
  # default values of variables set from params
  flag=0
  param=''

  while :; do
    case "${1-}" in
    -h | --help) usage ;;
    -v | --verbose) set -x ;;
    --no-color) NO_COLOR=1 ;;
    -f | --flag) flag=1 ;; # example flag
    -p | --param) # example named parameter
      param="${2-}"
      shift
      ;;
    -?*) die "Unknown option: $1" ;;
    *) break ;;
    esac
    shift
  done

  args=("$@")

  # check required params and arguments
  [[ -z "${param-}" ]] && die "Missing required parameter: param"
  [[ ${#args[@]} -eq 0 ]] && die "Missing script arguments"

  return 0
}

parse_params "$@"
setup_colors

# script logic here

msg "${RED}Read parameters:${NOFORMAT}"
msg "- flag: ${flag}"
msg "- param: ${param}"
msg "- arguments: ${args[*]-}"

Идея была в том, чтобы не делать его слишком длинным - мало кому хочется скроллить 500 строк чтобы увидеть основную логику скрипта. С другой стороны, хотелось иметь надежный фундамент для любых применений.

Bash, к сожалению, никак не упрощает эту задачу из-за отсутствия какого-либо способа управления зависимостями. Одним из решений было бы иметь отдельный скрипт со всеми стандартными и служебными функциями и запускать его в самом начале. Недостатком этого варианта будет необходимость всегда таскать этот второй файл повсюду, что уже все-таки далеко от идеи «простого Bash-скрипта». Поэтому я решил добавить в шаблон только то, что считаю минимально необходимым и сделать его максимально коротким.

А теперь давайте углубимся в детали.

Выбираем интерпретатор

#!/usr/bin/env bash

Скрипты обычно начинаются с шебанга. Для лучшей совместимости я использую /usr/bin/env вместо непосредствено /bin/bash. Хотя, учитывая комментарии на StackOverflow по ссылке выше, даже это может иногда не сработать.

Fail fast

set -Eeuo pipefail

Команда 'set' изменяет параметры выполнения скрипта. Например, обычно Bash'у вообще наплевать на ошибку выполнения какой-либо команды, которая завершилась с ненулевым кодом возврата - он просто беззаботно переходит к выполнению следущей строки. А теперь посмотрим вот на такой небольшой скрипт:

#!/usr/bin/env bash
cp important_file ./backups/
rm important_file

Что произойдет, если './backups/' не существует? Именно, вы получите сообщение об ошибке в консоли, но прежде чем вы успеете отреагировать, файл уже будет безвозвратно удален второй командой.

О том, как работает '-Eeuo pipefail' и от чего он защищает, можно прочитать вот в этой статье.

Ну и еще нужно отметить, что существуют и аргументы против подобной практики.

Узнаем путь

script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)

Эта строка пытается определить директорию расположения скрипта.

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

Но если наша CI-система запустит скрипт вот так:

/opt/ci/project/script.sh

то наш скрипт сработает не в директории проекта, а в каком-то совершенно другом месте. Мы можем исправить это, перейдя по нужному пути перед выполнением скрипта:

cd /opt/ci/project && ./script.sh

Но гораздо лучше решить эту проблему на стороне скрипта. Итак, если скрипт читает какой-то файл или запускает другую программу из той же директории, где лежит он сам, он будет делать это так:

cat "$script_dir/my_file"

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

Прибираемся

trap cleanup SIGINT SIGTERM ERR EXIT

cleanup() {
  trap - SIGINT SIGTERM ERR EXIT
  # script cleanup here
}

Относитесь к 'trap' как к блоку finally для скрипта. В конце работы скрипта - при нормальном завершении, либо из-за ошибки или внешнего сигнала - будет выполнена функция cleanup(). Это то место, где вы можете, например, попытаться удалить все временные файлы, созданные скриптом в процессе работы.

При этом помните, что cleanup() может быть вызван не только в конце, но и при выполнении скриптом любой части его логики. Не обязательно, что все ресурсы, которые вы пытаетесь очистить, будут существовать.

Отображаем справочную информацию

usage() {
  cat <<EOF
Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-v] [-f] -p param_value arg1 [arg2...]

Script description here.

...
EOF
  exit
}

Располагая usage() где-то в начале скрипта, мы одним выстрелом убиваем двух зайцев:

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

  2. Имеем минимальную документацию на тот случай, когда кто-то будет вносить изменения в скрипт (например, даже вы сами две недели спустя, уже не помня, что и как было сделано).

    Я не утверждаю, что здесь нужно документировать каждую функцию. Но короткое красивое сообщение об использовании скрипта является обязательным минимумом.

Красивый вывод сообщений

setup_colors() {
  if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; then
    NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' YELLOW='\033[1;33m'
  else
    NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW=''
  fi
}

msg() {
  echo >&2 -e "${1-}"
}

Тут нужно отметить несколько вещей:

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

Во-вторых, эти объявления цветов предназначены для использования только с моей функцией msg(), а не с командой echo.

Функция msg() предназначена для печати всего, что не является непосредственно выхлопом скрипта. Сюда входят все логи и сообщения, а не только ошибки.

Пример использования:

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

Чтобы проверить, как он себя ведет, когда stderr не является интерактивным терминалом, добавьте в скрипт строку, подобную приведенной выше. Затем запустите его, перенаправив stderr на stdout и подключив его к cat. В результате операции конвейера вывод больше не отправляется непосредственно на терминал, а отправляется следующей команде, поэтому теперь вы не увидите цветов, но и не увидите никакого мусора:

Парсинг параметров

parse_params() {
  # default values of variables set from params
  flag=0
  param=''

  while :; do
    case "${1-}" in
    -h | --help) usage ;;
    -v | --verbose) set -x ;;
    --no-color) NO_COLOR=1 ;;
    -f | --flag) flag=1 ;; # example flag
    -p | --param) # example named parameter
      param="${2-}"
      shift
      ;;
    -?*) die "Unknown option: $1" ;;
    *) break ;;
    esac
    shift
  done

  args=("$@")

  # check required params and arguments
  [[ -z "${param-}" ]] && die "Missing required parameter: param"
  [[ ${#args[@]} -eq 0 ]] && die "Missing script arguments"

  return 0
}

Если имеет смысл вынести что-то в параметры скрипта, то я обычно делаю это. Даже если этот скрипт используется только в одном месте. Это упрощает его копирование и повторное использование, что рано или поздно случается. Кроме того, даже если что-то нужно захардкодить, то обычно для этого на уровне повыше есть место более подходящее, чем сам bash-скрипт.

Есть три основых типа параметров CLI: флаги, именованные параметры и позиционные аргументы. Все они поддерживаются функцией parse_params().

Единственный из распространенных видов параметров, который здесь не обрабатывается - это объединение нескольких однобуквенных флагов. Если вам нужно иметь возможность передавать два флага как -ab вместо -a -b, придется дописать немного кода.

Цикл while - это разбор параметров вручную. На любом другом языке вы бы использовали какой-нибудь встроенный парсер или библиотеку, но это Bash, поэтому как есть, так есть.

В шаблоне уже для примера объявлен флаг (-f) и именованный параметр (-p). Просто измените или скопируйте их, чтобы добавить другие параметры. И не забудьте после этого обновить usage() :)

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

В Bash есть две альтернативы ручному парсингу параметров. Это 'getopt' и 'getopts'. Есть аргументы как за, так и против их использования. Я считаю эти функции не самым лучшим вариантом, поскольку по умолчанию 'getopt' в MacOS ведет себя не так на других системах, а 'getopts' не поддерживает длинные параметры (например, --help).

Использование шаблона

Просто скопипастьте его, как вы это обычно делаете с кодом, который вы находите в интернете.

После того, как вы его скопируете, вам нужно изменить только 4 вещи:

  1. текст usage() с описанием скрипта;

  2. содержимое cleanup();

  3. параметры в parse_params() - оставьте --help и --no-color, но замените -f и -p (это примеры)

  4. основную логику скрипта :)

Переносимость

Я тестировал шаблон на MacOS (где по дефолту древний Bash 3.2) и нескольких образах Docker: Debian, Ubuntu, CentOS, Amazon Linux, Fedora. Всё работает.

Очевидно, что он не будет работать в средах без Bash, таких как Alpine Linux. Alpine, как минималистичная система, использует легковесный ash.

Вы можете спросить: не лучше ли использовать Bourne Shell-совместимый скрипт, который будет работать почти везде? Ответ, по крайней мере в моем случае: нет. Bash безопаснее и мощнее (пусть и не прост в использовании), поэтому я могу смириться с отсутствием поддержки нескольких дистрибутивов Linux, с которыми мне редко приходится иметь дело.

Внеклассное чтение

При создании скриптов CLI на Bash или другом лучшем языке существуют некоторые универсальные правила. Эти материалы расскажут, как сделать ваши небольшие скрипты и большие приложения с консольным интерфейсом надежными:

И в заключение

Я не первый и не последний, кто создал шаблон скрипта Bash. Хорошей альтернативой является этот проект, хоть он и слишком большой для моих повседневных нужд. В конце концов, я стараюсь делать bash-скрипты как можно компактнее.

При написании скриптов Bash используйте среду IDE, которая поддерживает линтер ShellCheck, например IDE JetBrains. Это не даст вам наворотить кучу вещей, которые могут привести к неприятным последствиям.

Мой шаблон Bash-скрипта также доступен как GitHub Gist (под лицензией MIT)

Комментарии (27)


  1. megahertz
    18.11.2021 19:37
    +9

    Пришел к очень похожему скрипту, разве что вместо парсинга флагов в 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 $@


  1. kira-dev
    19.11.2021 01:19
    +5

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

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


    1. megahertz
      19.11.2021 07:53
      +1

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


    1. TheDenis
      19.11.2021 13:44
      +2

      В макоси нет realpath


  1. igrishaev
    19.11.2021 11:02
    -1

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

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


    1. igrishaev
      19.11.2021 11:04
      +6

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


      1. ivegner
        22.11.2021 23:00
        +4

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


      1. hrenov_drummer
        23.11.2021 16:39
        +6

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

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

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


  1. Vesh
    19.11.2021 11:56

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

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


    1. F0iL Автор
      19.11.2021 11:56
      +2

      Про getopt в статье отдельно написано.


  1. ITficator
    19.11.2021 12:43
    +2

    Можно ещё выносить повторяющиеся части скрипта и подключать их при необходимости. Например, если вынести 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


    1. dodo101000101
      20.11.2021 00:07
      +2

      Мне иногда приходится писать на 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 для скриптов из нескольких файлов.


  1. Rast1234
    19.11.2021 15:44
    +1

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


  1. event1
    19.11.2021 16:05
    +4

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


  1. MuuNu
    19.11.2021 16:20
    +6

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

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

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


    1. vvzvlad
      20.11.2021 17:53
      +4

      Пора.


  1. phikus
    19.11.2021 23:37
    +1

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

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

    Fix is welcome


    1. wStranger
      23.11.2021 00:14
      +2

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


  1. RumataEstora
    20.11.2021 14:41
    +1

    Думаю, что для определения "домашнего" каталога скрипта цепочку 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 "$@"
    


    1. kt97679
      21.11.2021 01:30

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

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


      1. RumataEstora
        21.11.2021 04:29

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


  1. RumataEstora
    21.11.2021 04:24

    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): какие-то проблемы - надо что-то напечатать
    


    1. kt97679
      21.11.2021 05:46

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

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


      1. RumataEstora
        21.11.2021 12:16
        +1

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

        Согласен.

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

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

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

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


        1. kt97679
          21.11.2021 20:55

          Спасибо за объяснение, это я знатно протормозил. Сам использовал похожий подход некоторое время назад: github.com/kt97679/chroot-build/blob/master/chroot-build.sh#L139-L147


  1. pansa
    23.11.2021 14:51

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


  1. Sigest
    26.11.2021 08:05

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