?

Log in

No account? Create an account
nyaload

Журнал Пушыстого

Журнал Пушыстого

Previous Entry Share Next Entry
потому что в кузнице не было гвоздя
nyaload
_winnie
Не было гвоздя — подкова пропала.
Не было подковы — лошадь захромала.
Лошадь захромала — командир убит.
Конница разбита — армия бежит.
Враг ступает в город, пленных не щадя,
От того, что в кузнице не было гвоздя


В свои bash-скрипты я вставляю
#!/usr/bin/env bash
set -euo pipefail



Опция -e останавливает скрипт если процесс вернул не 0 (и пишет в stderr на какой строке ошибка).

Это предотвращает беду, если в списке команд одна из фейлится:
svn up
build
copy some files
delete secret files
deploy build to external server

Опция -u останавливает скрипт, если используется неопределённая переменная. Это предотвращает беду например в таких случаях:

tar -czf download.tarball.tar.gz "$PROJECT_DIR/bin"

Если почему-то PROJECT_DIR не определена, то пакуется и отправляется пользователям системная /bin, вместо скомпилированых файлов проекта. И есть менеее забавные фейлы, превращение rm -rf "$1/$2" в rm -rf "/" со стиранием всего.

В комбинации с предыдущей опцией - опечатки в переменных окружения перестают быть непредсказуемым каскадно-гвоздевым фейерверком.
опция -o pipefail фейлит выполнение пайпа, если один из подкомпонентов выполняется с ошибкой. Например,
cat файл_который_не_существует | iconv -f cp1251 -t UTF-8 > результирующий файл.


Ожидаемые ошибки я игнорирую явно.
Если мне похрен на результат команды, вставляю || true после неё
cmd || true #'||' запускает вторую команду, если первая вернула не ноль. '||' можно читать "а иначе".

Если я удаляю папку, которая может не существовать, я явно проверяю что она есть перед удалением:
test -d dir_to_delete && rm -r dir_to_delete.

grep с пустым выводом возвращает код 1, и код ошибки 2 если есть реальная ошибка. Игнорирую коды меньше 2 явно:
cmd1 | (grep c || test $? -lt 2) | cmd2. # $? - код возврата,  test A -lt B - сравнение


Я не знаю, как удобным образом проверить ошибки в cmd2 в таком коде:
cmd1 $(cmd2)
. Подскажите?

Я не эксперт по "портабельному sh", поэтому если используете #!/bin/sh который ссылка на ksh/dash/bash/некий лже-POSIX, то надо смотреть в манах/гуглах какие есть опции.

В bat-файлах программировать надёжно сложно, и я не хочу внимательно вникать в cmd.exe, боюсь за свою психику.
Тем не менее, если я загоняю в bat-файл простой список команд, я в конце каждой команды ставлю || goto error или || exit /b 1 (или || pause если скрипт интерактивный, запускается всегда мышкой).

build || pause
copy some files || pause
delete secret files || pause
deploy build to external server || pause


Данная техника позволила в скрипте апдейта арта для дизайнеров найти тупые и хитрые ошибки в первые два месяца проекта (иначе бы мы с ними жили два года).

Ничего сложного на bat-файлах я стараюсь не писать, они в пять коварней чем C++, bash, assembler и perl вместе взятые.

Если не убеждаться в правильности работы каждой команды из цепочки,










  • 1

Re: set -euo pipefail

Кстати, интересно, что исторически по-умолчанию сложилась другая ситуация.
Или, "...ведь в конце концов кто знает? Может быть, так надо. Может быть, именно в этом великая сермяжная правда."

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

Спасибо, пригодится.

(я думал, что это дефолтное поведение)

> удаляю папку, которая может не существовать я явно проверяю что она есть перед удалением:
> test -d dir_to_delete && rm -r dir_to_delete.

rm -rf завершается удачей при отсутствии файла.

> Я не знаю, как удобным образом проверить ошибки в cmd2 в таком коде:
> cmd1 $(cmd2)

Не знаю, считается ли это за "удобный образ":

VAR=$(cmd2) || { echo ERROR ; exit 1 ; }
cmd2 $VAR

>rm -rf завершается удачей при отсутствии файла.
Ага. Ну, там ещё тонкости, что rm -rf завершается удачей если файл есть, но был например заблокирован, или пользователь опечатался в его имени, или нет прав для удаления, или, или, или, ...
И при этом ситуакция с оставшимся файлом в сборке почему-то является недопустимой.
Вместо rm так же может стоять что-нибудь ещё.

>Не знаю, считается ли это за "удобный образ":
>VAR=$(cmd2) || { echo ERROR ; exit 1 ; }
>cmd2 $VAR

Не, не считается :) Хочется использовать доступные инструкции. А не страдать что они есть, но их нельзя использовать из-за обработки ошибок. Которая при этом раздувает скрипт в несколько раз. Настолько, что за обработкой ошибки не видна настоящая опечатка - использование cmd2 вмето cmd1 при копипасте :)

VAR=$(cmd2) || { echo ERROR ; exit 1 ; }
cmd2 $VAR

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

Edited at 2010-12-19 12:20 pm (UTC)

> rm -rf завершается удачей если файл есть, но был например заблокирован
Это - portability issue, "заблокированы" файлы бывают только в одной
малопопулярной ОС. Думаю, исправят - если зафайлить соответствующий баг в
msys или откуда там rm на винде взялся.

> или пользователь опечатался в его имени,
Что, собственно, и есть "отсутствие файла".
Не может же rm знать какой файл пользователь имел в виду?
(если б знала, то тогда зачем ей вообще аргументы :-)

> или нет прав для удаления
Неправда ваша.
При отсутвии прав rm даже с -f жалуется как положено и выставляет $? в единичку.
$ rm -f / ; echo $?
rm: cannot remove `/': Is a directory
1

> Хочется использовать доступные инструкции.
Собственно, command expansion используется.
Только что таким образом, который не перетирает только что $? второй командой.
Это цена компактного кода.
Ровно как в каком-нибудь ruby/python: если пишем в одну строчечку
someObject.reverse.filter.reverse.map.map.invert.discuss.collapse.delete() - то
разобраться кто из них на самом деле выкинул exception будет нелегко.

> А не страдать что они есть, но их нельзя использовать из-за обработки ошибок.
> Которая при этом раздувает скрипт в несколько раз.
Вам шашечки или ехать? В смысле, ошибки обрабатывать или скрипт маленьким держать?
Обработка ошибок всегда увеличивает размер программы.
"В несколько раз" - это только на синтетических примерах, когда
программа занимает одну строку, а с обработкой - две.
Или у вас в кажой строке command expansion?

> Настолько, что за обработкой ошибки не видна настоящая опечатка -
> использование cmd2 вмето cmd1 при копипасте
Вот если бы там были не придуманные cmd1 и cmd2, а реальные ls и rm -
опечатку было бы видно. А так - какая разница? "Если замысел художника ясен, то
зачем доводить картину до конца?".

> но не в обычных ежедневных скриптах.
Ой, в обычных ежедневных обработки ошибок нету вообще.
Потом обычный ежедневный становится кронжобом, потом mission-critical,
потом всё бумкается - и вот потом там появляется обработка вот именно
этой ошибки.

Правильный пример про недостаточные права для удаления:
$ rm -f /bin/sh ; echo $?
rm: cannot remove `/bin/sh': Permission denied
1

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

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

Мне иногда проще записать
rm -rf $dst; chflags -R 0 $dst ; rm -rf $dst

первая команда может пройти с ошибкой.

> cmd1 $(cmd2)
Ещё вариант:
cmd2 | xargs cmd1

А как дефолтное это можно в башрц прописать например?

Как это сделать правильно, что бы не мешало работать старым скриптам и интерактивному интерпретатору - не в курсе

Попробовал тупо добавить в .bashrc - bash стал закрываться от любой неправильной команды, может быть можно сделать if-команду типа "если запускаюсь неинтерактивно".

Ещё надо учесть, что значительная часть скриптов использует #!/bin/sh а не #!/bin/bash

Если это сделать глобально для интерпретатора, могут сломаться какие-то системные или third-party скрипты.

Поскольку мои скрипты запускаются на разных хостах разными пользователями (и роботами), то я про .bashrc и общесистемные хаки не интересовался


Edited at 2010-12-19 05:15 pm (UTC)

За это и люблю питон, за философию "лучше развалиться, чем сделать что-то не то".

Баш-скрипты, соответственно, не люблю.

В питоне есть некоторые другие редкие ловушки. Сходу припоминаются такие:

1) zip(списки разной длины).
Я стараюсь всегда вставлять assert, когда из ближайших строк не следует равенство длин.
def f(a, b):
   assert len(a) == len(b)
   zip(a, b)


2)
os.walk(несуществующая папка)
os.listdir(несуществующая папка)

Всегда ставлю перед ними
assert(os.path.isdir(dir_path))
os.walk(dir_path)

3) subprocess не очень удобен, если надо написать "cmd1 | cmd2 | cmd3" с обработкой кодов.


Почитавши http://bappoy.pp.ru/tag/bash-pitfalls думаю, что если хочется надёжно программировать то надо использовать более нормальный язык программирования (Питон например)

А можно как-нибудь аналог finally организовать? В случае фейла не просто вываливаться, а ещё и чистить за собой временные файлы, например.

можно обернуть скрипт во второй скрипт или функцию, единственная задача которого - посмотреть на exit code первого большого скрипта, и сделать cleanup перед передачей кода ошибки дальше.
Может есть прямой аналог finally, но я не эксперт шелл-программирования, я только изучил как обложить грабли соломой.

Псевдо-код:
try:
  A()
  B()
  C()
finally:
  Clean()

Псевдо-шелл код :)
function f {
  A
  B
  C
}
f && Clean || ( Clean ; false ) 
if f ; 
  then Clean ; 
  else Clean; false; 
fi

Как trap на сигналы ставить, например — это ясно, а вызовы всё равно надо в short circuit заворачивать, получается :(


Ты почему-то не дописал стихотоворение.
"Враг ступает в город, пленных не щадя
От того, что в кузнице не было гвоздя"

Спасибо, поправил :)

К сожалению, set -e не всегда печатает строку, он у меня молча падает на вполне безобидном abc="$(whatever)". В таких случаях, обычно, нормально, что оно падает, тогда строка получится пустой, что код дальше проверит и так. А вот чтоб определить место падения тогда приходится делать ещё и set -v.

s/что оно падает/что команда внутри возвращает ошибку/

В этой ссылке кажется про другое - про то, как отличить код не-ошибки от кода ошибки.
То что я у себя обозначил как

cmd1 | (grep pattern || test $? -lt 2) | cmd2

А, не, про то как грамотно присвоить в переменную и проверить возвращаемый код со своей обработкой, а не дефолтной.

Вообщем да, в баше надо таскать с собой свитки заклинаний, а не просто стыковать кубики.

вот ещё нашлось интересное, о том, как надо писать скрипты: LDP/Advanced Bash-Scripting Guide

  • 1