Подводные камни shell скриптинга

в 14:18, , рубрики: linux, shell, метки: ,

Подводные камни shell скриптингаНесмотря на повсеместное использование графики, shell не теряет своей актуальности и по сей день. А порой позволяет выполнять операции значительно быстрее и проще, нежели в графическом окружении. Однако есть множество вещей, о которых большинство даже не подозревает.
Я бы не хотел привязываться к какому-то определённому шеллу, тем не менее не каждая из рассмотренных ниже возможностей может быть POSIX совместима, однако гарантировано будет работать в ksh/bash/zsh.

1. Переменные и test

Ни для кого не секрет, что в shell можно сравнивать строки, числа и даже переменные. (:

[[ 2 -eq 3 ]]
[[ "test" == "test" ]]
[[ $VAR -eq 3 ]]

Но вот последний вариант, после случайной опечатки (забыл $ перед VAR) заставил поподробнее изучить поведение в данном случае, т.к. к моему удивлению конструкция отработала без ошибок и значение VAR подставилось как если бы я не забыл $. Более того, если в VAR сослаться на другую переменную, то всё прекрасно работает.
Разработчики даже от рекурсии не забыли защититься:

$ V=V ; [[ V -eq 12 ]]
-bash: [[: V: expression recursion level exceeded (error token is "V")

Как оказалось, это не пасхалка, всё так и задумано. Подробности описаны в man bash.

on the words between the [[ and ]]; tilde expansion, parameter and variable
expansion, arithmetic expansion, command substitution, process substitution, and quote removal are performed.

Arithmetic Expansion

The evaluation is performed according to the rules listed below under ARITHMETIC EVALUATION.

ARITHMETIC EVALUATION

Within an expression, shell variables may also be referenced by name without using the parameter expansion syntax.

2. Достать переменные из subshell'a без сторонних утилит

Допустим у нас есть простая конструкция:

do_something | while read LINE ; do export VAR_N=${LINE##%*} ; done

Новичкам частенько приходится ломать голову, почему же VAR_N не доступна после завершения конструкции. Дело в том, что для цикла while создаётся subshell из которого переменные до родителя уже не доходят. Чтобы достать нужные нам переменные из цикла приходится изрядно попотеть. Все предлагаемые в интернетах варианты, как правило, сводятся либо к запоминанию вывода в переменную и многократное распарсивание:

VAR=$(cycle)
VAR_N=$(echo "$VAR"|sed 'magic_sed')

работает, но как-то некрасиво. Да и всякие sed'ы, perl'ы и прочие радости жизни приходится многократно вызывать, что явно не в лучшую сторону сказывается на производительности. Да и зачем они все, если можно обойтись без них?
Просто нужно корректно софрмировать вывод с красивым разделителем. Например так:

OLD__IFS="$IFS"
IFS='~' #или любой другой разделитель
set -- $(cycle)
VAR_N="$1"
VAR_NN="$2"
IFS="$OLD__IFS"

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

3. Спрятать часть данных от пайпа

Порой возникает ситуация, когда часть данных нужно спрятать от одного из пайпов, а потом соединить со всем потоком обратно. В такой ситуации скрытые данные можно перенаправить в stderr, а потом из stderr вернуть обратно в stdin:

$ ( { echo DATA ; echo HIDDEN_DATA >&2 ; } | sed 's/^/MODIFIED_/' ) 2>&1 | sed s/$/_CATCHED/
HIDDEN_DATA_CATCHED
MODIFIED_DATA_CATCHED

4. Я хочу парсить историю команд

Наряду с любителями парсить вывод ls, находятся ещё и любители парсить историю команд (для дальнейшей автоматической обработки результата), но они даже не догадываются о чём-нибудь кроме history, более того, совершенно не принимают во внимание, что вывод history на ура кастомизируется. Тут можете задать вопрос на засыпку: «а чем же ещё можно посмотреть history?» (ответ специально под спойлер спрячу) (:

Если вы так хотите парсить историю команд, пожалуйста,

используйте fc (подробности в help fc, он builin в любом шелле).

5. Я помню, что if .. then .. else .. fi отличается от .. && .. || ..

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

if $(condition); then com_1; else com_2; fi
condition && com_1 || com_2

Неочевидная разница в том, что в первом случае com_2 будет выполнена лишь в одном случае: condition вернёт false.
Во втором случае com_2 будет запущена если condition вернёт false, а так же в том случае, если com_1 вернёт ошибку. Не верите? убедитесь сами:

 $ if true ; then false ; else echo 'hello' ; fi
 $ true && false || echo 'hello'
hello

Ну вот пожалуй и всё. Хотел было замолвить пару слов про любимый sed, но уже и так как-то длинно получилось. Может в другой раз (:

Автор: sledopit

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js