BASH Tips&Tricks #000C: Lets try it now!

BASH
Давайте попробуем это сейчас! Да-да, именно вот этот чудесный кусочек кода выполним, а он нам взрыхлит почву, посеет нужные семена. А потом следующий за ним код, ещё более великолепный в своём совершенстве, даст сочные зелёные побеги. И уже в завершение всего этого благолепия наш скрипт пожнёт плоды, свяжет снопы и сложит стога…
Жаль, что вся этя идиллия оказывается досадной фикцией, когда наш трудолюбивый созидательный код вдруг помещают в условия неблагоприятного марсианского климата: в результате мало того, что ничегошеньки не всходит, так ещё и скрипт с оглушительным треском рушится. А может статься и того хуже: как ни в чём ни бывало начнёт выполняться следующий код, мерно перепахивающий красные марсианские пески в задумчивом цикле без конца и края…
Всё ещё не слишком ясно, о чём таком внеземном у нас сегодня пойдёт речь?
ОК, я немного увлёкся научной фантастикой, так что постараюсь теперь пояснить ближе к реалиям повседневного скриптотворчества.
Вот смотрите: вы, я, он, она и они — то есть «все мы», — часто пишем скрипты с мимимальными проверками на корректность завершения команд. И это, в принципе, не особо-то и плохо: ведь в BASH, к сожалению, нет такой волшебной палочки-выручалочки, как возможность устанавливать собственные обработчики исключительных ситуаций. Проверять же код возврата каждого grep'а или sed'а было бы откровенно глупо. С другой стороны, аварийное завершение скрипта или его продолжение после того, как произошла непредвиденная ошибка — часто бывает в равной мере нежелательно, а иногда просто катастрофично.
Посему выполнение наиболее критичных участков кода обязательно необходимо контролировать на предмет возможного возникновения ошибок, при этом исключая самопроизвольное «падение» скрипта. Как это сделать?
Для подобных вещей в BASH существует замечательная, хоть и весьма неспешная, функция eval: она безопасным образом выполняет переданный ей в качестве аргумента код, компилируя его прямо во время исполнения скрипта. При этом eval может выполнить как одну команду, так и целый блок кода произвольного размера.
Недостатком eval является то, что в случае возникновения ошибки не составляет труда выяснить, что something went wrong, но проблематично в удобном пользователю виде представить тот вывод, который отправили в STDERR «запнувшиеся» команды (а «кто», кроме самих этих команд, может лучше рассказать о возникших неполадках?). Также весьма неудобно то, что аргументом eval должна являться строка, обрамлённая либо двойными, либо одинарными кавычками.
Это приводит к использованию довольно странных конструкций вида:

eval "echo \"$Name $Surname\""
eval 'echo "I'"'"'ve recently read '"'"'The story of Mabeyka'"'"' in esperanto"'

Последнее выражение выглядит особенно уродливо, не так ли?
Но BASH не был бы собой, если бы не позволял нам делать лёгкие и элегантные трюки полуджазовом стиле KISS.
В качестве примера предлагаю потестировать простенькую функцию-обёртку для eval, которую написал я (буквально только что, «на коленке», в 3 часа ночи :)):

# Спасибо читателям блога за оптимизацию функции
try () {
 { STDERR=$( eval "$(cat -)" 2>&1 1>&3 ); } 3>&1
 return $?
}

Заметьте, что функция try использует STDIN, а не параметры командной строки, так что мы запросто можем использовать конструкцию «Документ здесь», что не только сделает код более читабельным, органично встраивая его в основной поток исполнения скрипта, но и решает проблему со всеми видами кавычек.
А вот как с помощью подобной функции можно выполнить код, непредвиденное завершение которого привело бы к неприятностям или даже к бессонной ночи вместо концерта «Animal ДжаZ»:

if ! try <<EOF
psql <<<"SELECT pg_start_backup('$TIMESTAMP',true);"
EOF
then
 error_ "Could not initiate backup procedure, reason given by server:\n$STDERR"
 exit 1
fi

Да, теперь мы можем справиться даже с тем кодом, который раньше казался совершенно неуправляемым: запряжём его eval'ом, чтобы не брыкался и в спокойной обстановке проанализируем причины возникновения ошибки/ошибок, изучив соотв. журнальный файл.

Итак, давайте всё же попробуем это. Но на сей раз — осторожно, дабы не давать ошибкам ни малейшего шанса испортить вам настроение ;)

17 комментариев

avatar
Здравствуйте!

А можно Вас попросить пояснить логику этой конструкции:
3<&2 2>&1 1>&3


Насколько я понял, потоки ошибок и вывода направляются в первый дескриптор, но почему не понимаю (:
avatar
Я рад, что мой код заинтересовал Вас! :)
Я сделал exchange потоков STDOUT(#1) и STDERR(#2), чтобы STDERR записать в переменную. Потом я «вернул на место» STDOUT конструкцией 2>&1. Это всё было сделано для того, чтобы и овцы были целы, и волки сыты: теперь STDERR сохранён в глобальной переменной, а STDOUT можно получить, например, при помощи обратных кавычек.
Полным аналогом обмена потоков является обычный обмен значениями переменных.
Например, так это делается в assembler'е (забудем про то, что есть инструкция xchg и красивый хак с использованием xor):
mox cx,ax
mov ax,bx
mov bx,cx
У меня в качестве такой вспомогательной «переменной» (наподобие регистра cx) выступает поток под номером 3 :)
Ещё раз спасибо за вопрос!
avatar
Кстати, я немного упростил функцию, так что она теперь стала совсем уж элементарной обёрткой для eval :)
В прошлом варианте крайне некрасиво выглядело использование глобальной переменной ret_ (коль скоро она не была объявлена ключевым словом local), при полном отсутствии на то объективной необходимости.
avatar
мне кажется так оптимальней
{ STDERR=$(eval "$(cat -)" 2>&1 1>&3 ); } 3>&1


и еще… Если последняя команда в try <<EOF… EOF даст 0, то вся конструкция не сработает.
Например:
if ! try <<EOF
   ls /rrr
   ls /ddd
   ls /ggg
   ls /tmp
EOF
then
   echo "bla-bla-bla: $STDERR"
fi
avatar
Согласен, так оптимальнее.
Код, которые мы «пробуем на зуб», должен исполняться как функция, так или иначе всегда возвращающая реальный статус своего выполнения. Т.е. в данном случае всё-таки

ls /rrr && ls /ddd && ls /ggg && ls /tmp

либо:

declare -i errc
for d in /rrr /ddd /ggg /tmp; do
 ls $d; errc+=$?
done
! (( errc )) 
avatar
что-то && из головы вылетело))))))

тогда можно сделать так:
try(){
 while read line; do
  { ERR=$( $line 2>&1 1>&3 ); } 3>&1 ||  return  1
 done
}

if ! try <<EOF
   ls /home
   ls /tmp
   ls /tmpz
   ls /tmpd
EOF
then
   echo "bla-bla-bla: $ERR"
fi
avatar
Тогда уж ERR+= :)
Главный смысл этой конструкции именно в использовании eval. И если уж на то пошло, ничто не мешает Вам проверить на выходе STDERR: если в переменной есть что-то, значит, всё не так уж гладко. Можно в т.ч. это «не гладко» сравнить с регулярным выражением, если наперёд известно, какие примерно ошибки могут возникать.
avatar
Вообще я не заметил в статье особой привязки к eval.
Смысл не в этом, а в том, что Вы попытались создать конструкцию из высокоуровнивых языов (более, так скажем, приспособленных) в bash… Помню когда-то тема была на wasm по объектно-ориентированному программированию на fasm. Это осуществлялось с помощью очень гибкой системы макросов в fasm.
Тобишь смысл создания подобный конструкций в том, чтобы абстрагироваться от какой-то проблемы.
Я предлагаю не привязываться к eval и продолжать улучшать эту функцию вместе…
Ещё одной проблемой оказалось, то что между EOF'ами нелзя запихнуть конвееры… видимо из-за того что команды наследуют дескрипторы от родителя и немогут нормально перенаправлять потоки
avatar
У функции try есть другая большая проблема: все переменные, которые будут объявлены внутри её блока окажутся локальными переменными функции и станут недоступны на выходе из неё!
avatar
В смысле, только те переменные, которые будут объявлены явным образом с помощью declare. Но я как раз активно использую хэши, и уже испытал определённые трудности в связи с таким поведением try
avatar
а у меня что-то ничего не выходит с пайпами
не получается обойти проблему с символом перенаправления
+ eval ls /tmp '|' grep co
avatar
Кажись заработало… если в eval по одной строчке запихивать…
try(){
while read -r line; do
  { ERR=$( eval $line 2>&1 1>&3 ); } 3>&1 ||  return 1
done
}

if ! try <<EOF
   ls /tmp | grep orb
   ls /ggg
EOF
then
   echo -e "Command: $line\n$ERR"
fi

Результат:
taha@Lenovo:/home/taha# bash ./tst.sh
orbit-taha
Command: ls /ggg
ls: невозможно получить доступ к /ggg: Нет такого файла или каталога
avatar
taha, теперь представьте, что будет, если в данном блоке будут использоваться многострочные циклы, условные операторы, если вы разобьёте одну строку на две для лучшей читаемости. И сейчас у вас $ERR будет содержать только выведенное в STDERR последней из выполнявшихся команд :)
avatar
я знаю что косяк в многострочности и это накладывает ограничения…
а по поводу последней команды, то я так и задумывал)) ведь как в прикладной программе, когда происходит исключение, его перехватывает системный обработчик и передает управление в функцию, которая указана в SEH :) поэтому я и использовал "|| return 1" другой косяк в том что если в конвеере будет, допустим, grep и он ничего не отыщет, то вся конструкция посчитается ошибочной, что нас не устроит))
кстати в bash есть хорошая переменная ${PIPESTATUS[@]}, но у меня пока нет времени совсем замарочится
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.