Пользователь
0,0
рейтинг
16 ноября 2012 в 21:17

Разработка → Приемы написания скриптов в Bash из песочницы

Администраторам Linux писать скрипты на Bash приходится регулярно. Ниже я привожу советы, как можно ускорить эту работу, а также повысить надежность скриптов.

Совет 1

Не пишите скриптов, которые выполняют действия ничего не спрашивая. Такие скрипты нужны довольно редко. А вот всевозможного «добра» для копирования, синхронизации, запуска чего-либо, хоть отбавляй. И если в любимом Midnight Commander Вы вдруг нажали не на тот скрипт, то с системой может произойти все что угодно. Это как правила дорожного движения — «написано кровью».

Совет 2

Отталкиваясь от предыдущего, в начало каждого скрипта неплохо помещать что-то вроде:
read -n 1 -p "Ты уверен, что хочешь запустить это (y/[a]): " AMSURE 
[ "$AMSURE" = "y" ] || exit
echo "" 1>&2
Команда echo, кстати, здесь нужна потому, что после нажатия кнопки <y> у вас не будет перевода строки, следовательно, следующий любой вывод пойдет в эту же строку.

Совет 3

Это ключевой совет из всех. Для того, чтобы не писать каждый раз одно и то же — пользуйтесь библиотеками функций. Прочитав много статей по Bash, я вынужден констатировать, что этой теме уделяется мало внимания. Возможно в силу очевидности. Однако я считаю необходимым напомнить об этом. Итак.
Заведите свою библиотеку функций, например myfunc.sh и положите ее, например в /usr/bin. При написании скриптов она не только поможет сократить ваш труд, но и позволит одним махом доработать множество скриптов, если Вы улучшите какую-либо функцию.
Например, в свете совета 2 можно написать такую функцию:
myAskYN() 
{
local AMSURE
if [ -n "$1" ] ; then
   read -n 1 -p "$1 (y/[a]): " AMSURE
else
   read -n 1 AMSURE
fi
echo "" 1>&2
if [ "$AMSURE" = "y" ] ; then
   return 0
else
   return 1
fi
}
Единственным необязательным параметром эта функция принимает строку вопроса. Если строка не задана — молчаливое ожидание нажатия (в случаях, когда скрипт уже успел вывести все что нужно еще до вызова этой функции). Таким образом, применение возможно такое:
myAskYN "Ты уверен, что хочешь запустить это?" || exit
Можно написать и еще одну аналогичную функцию myAskYNE, с буквой E на конце, в которой return заменить на exit. Тогда запись будет еще проще:
myAskYNE "Ты уверен, что хочешь запустить это?"
Плюсы очевидны: а) пишете меньше кода, б) код легче читать, в) не отвлекаетесь на мелочи, вроде приставки " (y/[a]): " к тесту (замечу, что [a] означает any, а забранная в квадратные кавычки указывает, что это по умолчанию).
И последнее здесь. Для того, чтобы использовать функции из нашей библиотеки, ее надо не забыть включить в сам скрипт:
#!/bin/bash 
a1=myfunc.sh ; source "$a1" ; if [ $? -ne 0 ] ; then echo "Ошибка —
нет библиотеки функций $a1" 1>&2 ; exit 1 ; fi 

myAskYN "Ты уверен, что хочешь запустить это?" 
echo Run!
Я намеренно уложил весь вызов и обработку ошибки в одну строку, поскольку это вещь стандартная и не относится напрямую к логике скрипта. Зачем же ее растягивать на пол-экрана? Обратите также внимание, что имя скрипта присваивается переменной. Это позволяет задавать имя скрипта один раз, а стало быть, можно дублировать строку и заменить имя библиотеки, чтобы подключить другую библиотеку функций, если надо.
Теперь любой скрипт, начинающийся с этих трех строчек никогда не выполнит что-то без подтверждения. Предоставляю вам самим написать аналогичную myAskYN функцию, называемую myAskYESNO.

Совет 4

Разовьем успех и продемонстрируем несколько очевидных функций с минимальными комментариями.
sayWait() 
{ 
   local AMSURE 
   [ -n "$1" ] && echo "$@" 1>&2 
   read -n 1 -p "(нажмите любую клавишу для продолжения)" AMSURE 
   echo "" 1>&2 
} 
 
 cdAndCheck() 
{ 
   cd "$1" 
   if ! [ "$(pwd)" = "$1" ] ; then 
      echo "!!Не могу встать в директорию $1 - продолжение невозможно. Выходим." 1>&2 
      exit 1 
   fi 
} 
 
 checkDir() 
{ 
   if ! [ -d "$1" ] ; then 
      if [ -z "$2" ] ; then 
         echo "!!Нет директории $1 - продолжение невозможно. Выходим." 1>&2 
      else 
         echo "$2" 1>&2 
      fi 
      exit 1 
   fi 
} 
checkFile() 
{ 
   if ! [ -f "$1" ] ; then 
      if [ -z "$2" ] ; then 
         echo "!!Нет файла $1 - продолжение невозможно. Выходим." 1>&2 
      else 
         echo "$2" 1>&2 
      fi 
      exit 1 
   fi 
} 
checkParm() 
{ 
   if [ -z "$1" ] ; then 
      echo "!!$2. Продолжение невозможно.  Выходим." 1>&2 
      exit 1 
   fi 
}
Здесь обращу ваше внимание на постоянно встречающееся сочетание 1>&2 после echo. Дело в том, что ваши скрипты, возможно, будут выводить некую ценную информацию. И не всегда эта информация влезет в экран, а потому ее неплохо бывает сохранить в файл или отправить на less. Комбинация 1>&2 означает перенаправление вывода на стандартное устройство ошибок. И когда вы вызываете скрипт таким образом:
my-script.sh > out.txt
my-script.sh | less
в нем не окажется лишних ошибочных и служебных сообщений, а только то, что вы действительно хотите вывести.

Совет 5

В Bash не очень хорошо обстоят дела с возвратом значения из функции. Однако при помощи собственной библиотеки этот вопрос легко решается. Просто заведите переменную, в которую функция будет заносить значение, а по выходу из функции анализируйте эту переменную. Кстати, объявление переменной неплохо поместить в начало самой библиотеки ваших функций. Также, вы можете завести и другие переменные, которые будете использовать повсеместно. Вот начало вашей библиотеки функций:
curPath=  # переменная с текущим абсолютным путем, где находится скрипт
cRes=     # переменная для возврата текстовых значений из функций
pYes=     # параметр --yes, который обсудим позднее
Теперь можем добавить к коллекции еще такую полезную функцию:
input1() 
{ 
   local a1 
 
   if [ -n "$1" ] ; then 
      read -p "$1" -sn 1 cRes 
   else 
      read -sn 1 cRes 
   fi 
 
   # Проверка допустимых выборов 
   while [ "$2" = "${2#*$cRes}" ] ; do 
      read -sn 1 cRes 
   done 
   echo $cRes 1>&2 
}
Вот пример ее использования:
cat <<'EOF' 
Выбери желаемое действие: 
------------------------ 
   a) Действие 1 
   b) Действие 2 
   .) Выход 
EOF 
input1 "Твой выбор: " "ab." 
echo "Выбор был: $cRes"
Эта функция ограничивает нажатие клавиш до списка указанных (в пример это a, b, и точка). Никакие иные клавиши восприниматься не будут и при их нажатии ничего выводиться тоже не будет. Пример также показывает использование переменной возврата ($cRes). В ней возвращается буква, нажатая пользователем.

Совет 6

Какой скрипт без параметров? Об их обработке написано тонны литературы. Поделюсь своим видением.
  1. Крайне желательно, чтобы параметры обрабатывались независимо от их последовательности.
  2. Я не люблю использовать однобуквенные параметры (а следовательно и getopts) по той простой причине, что скриптов очень много, а букв мало. И запомнить, что для одного скрипта -r означает replace, для другого replicate, а для третьего вообще remove практически невозможно. Поэтому я использую 2 нотации, причем одновременно: а) --show-files-only, б) -sfo (как сокращение от предыдущего). Практика показывает, что такие ключи запоминаются мгновенно и очень надолго.
  3. Скрипт должен выдавать ошибку на неизвестный ему ключ. Это частично поможет выявить ошибки при написании параметров.
  4. Из совета 2 возьмем правило: никогда не запускать скрипт без подтверждения. Но добавим к этому важное исключение — если не указан ключ --yes (ключ, конечно, может быть любым).
  5. Ключи могут сопровождаться значением. В этом случае для длинных ключей действует такое правило: --source-file=my.txt (написание через равно), а для коротких такое: -sf my.txt (через пробел).
В этом свете обработка параметров может выглядеть так:
while [ 1 ] ; do 
   if [ "$1" = "--yes" ] ; then 
      pYes=1 
   elif [ "${1#--source-file=}" != "$1" ] ; then 
      pSourceFile="${1#--source-file=}" 
   elif [ "$1" = "-sf" ] ; then 
      shift ; pSourceFile="$1" 
   elif [ "${1#--dest-file=}" != "$1" ] ; then 
      pDestFile="${1#--dest-file=}" 
   elif [ "$1" = "-df" ] ; then 
      shift ; pDestFile="$1" 
   elif [ -z "$1" ] ; then 
      break # Ключи кончились 
   else 
      echo "Ошибка: неизвестный ключ" 1>&2 
      exit 1 
   fi 
   shift 
done 
 
checkParm "$pSourceFile" "Не задан исходный файл" 
checkParm "$pDestFile" "Не задан выходной файл" 
 
if [ "$pYes" != "1" ] ; then 
   myAskYNE "Ты уверен, что хочешь запустить это?" 
fi 
echo "source=$pSourceFile, destination=$pDestFile"
Этот код дает следующие возможности:
  • ./test.sh -sf mysource -df mydest
    ./test.sh --source-file=mysource --dest-file=mydest
    ./test.sh --source-file=mysource --dest-file=mydest --yes
  • Параметры могут задаваться в любом порядке и комбинации полной и сокращенной формы ключей.
  • Поскольку параметры обязательны, то присутствует проверка их наличия (но не корректности), благодаря checkParm.
  • Если отсутствует ключ --yes, обязательно возникнет запрос подтверждения.
Это базовая часть, которую можно развивать и дальше. Например, добавим пару функций обработки параметров в нашу библиотеку:
procParmS() 
{ 
   [ -z "$2" ] && return 1 
   if [ "$1" = "$2" ] ; then 
      cRes="$3" 
      return 0 
   fi 
   return 1 
} 
procParmL() 
{ 
   [ -z "$1" ] && return 1 
   if [ "${2#$1=}" != "$2" ] ; then 
      cRes="${2#$1=}" 
      return 0 
   fi 
   return 1 
} 
При этом цикл обработки параметров будет выглядеть гораздо более удобоваримым:
while [ 1 ] ; do 
   if [ "$1" = "--yes" ] ; then 
      pYes=1 
   elif procParmS "-sf" "$1" "$2" ; then 
      pSourceFile="$cRes" ; shift 
   elif procParmL "--source-file" "$1" ; then 
      pSourceFile="$cRes" 
   elif procParmS "-df" "$1" "$2" ; then 
      pDestFile="$cRes" ; shift 
   elif procParmL "--dest-file" "$1" ; then 
      pDestFile="$cRes" 
   elif [ -z "$1" ] ; then 
      break # Ключи кончились 
   else 
      echo "Ошибка: неизвестный ключ" 1>&2 
      exit 1 
   fi 
   shift 
done
Фактически, этот цикл можно копировать из скрипта в скрипт не задумываясь ни о чем, кроме названий ключей и имени переменной для этого ключа. Причем они в данном случае не повторяются и возможность ошибки исключена.
Нет предела совершенству, и можно еще долго «улучшать» функции, например в procParmS проверить на непустое значение третий параметр и вывалиться по ошибке в таком случае. И так далее.
Файл библиотеки функций из этого примера можно скачать здесь.
Тестовый файл здесь.
@justAdmin
карма
6,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

  • +44
    Не знаю, откуда вы взяли совет 1. Я бы написал наоборот:

    1. Никогда ничего не спрашивайте, если действие, производимое скриптом, не деструктивно по сути и притом работает с резервной копией. Если деструктивно, но не трогает резервную копию тоже не спрашивайте, при исполнении следующих правил риска никакого, а вопросы раздражают.
    2. Давайте нормальные имена скриптам и всегда добавляйте документацию, если сам скрипт не умещается на экране.
    3. Для того, чтобы запустить именно нужный скрипт, есть консоль и автодополнение в ней. Никогда не запускайте что‐либо по нажатию в MC.
    • –5
      3.1 chmod a-x ./*
      • +10
        Таким образом вы запретите использование всех подкаталогов.
        • +3
          find. -type f -exec chmod a-x {} \;
          • –1
            Большинство современных chmod умеет -R:

            chmod -R a-x .
            

            Кстати, хорошая задачка, которую я иногда на собеседованиях админов спрашиваю: как восстановить систему после такого — хороша тем, что потенциальных решений сотни :)
            • +4
              Ещё бы большинство людей научились проверять свои предположения, прежде чем публиковать их.
               $ chmod -R a-x .
              chmod: cannot read directory ‘.’: Permission denied

              • –1
                Эм, речь о том, чтобы выполнять это из под рута, разумеется.
                • +1
                  Речь о том, что ваш вариант запретит использование большего числа каталогов вместо того, чтобы решить проблему, только и всего.
                  • 0
                    Мы вообще-то изначально говорили о способе _повредить_ систему, а не решить какую-то проблему (какую, кстати?).
                    • +3
                      Нет. vlivyur привёл решение, которое, по его мнению должно было запретить запуск чего‐либо по нажатию в MC (для текущего каталога) (во всяком случае, я так понял).
                      Я указал, что данное решение также запретит использование всех подкаталогов, затем Frosty показал, как можно этого избежать.
                      Ваш комментарий к ответу Frosty был воспринят как попытка указать на лучший способ решения проблемы (никаких явных указаний, что вы хотите этим способом повредить систему в нём нет, а других причин иметь именно такой комментарий именно к тому ответу я не вижу).
                      Поэтому sledopit указал, что это просто усугубляет проблему.
                      Вы, очевидно, не поняли, что sledopit хотел этим сказать, так как указанное им поведение наблюдается при запуске от пользователя.
                      Поэтому я уточнил, о чём там идёт речь.

                      Теперь вы говорите, что кто‐то говорил о том, как повредить систему, хотя ни в каком из предшествовавших вашему комментариев это не предполагалось.

                      Вот этот
                      Да, упустил это, но смысл решения понятен.
                      комментарий vlivyur ясно указывает на то, что речь не шла о повреждении системы.
                      • +1
                        Забавно. Я воспринял первое сообщение от vlivyur не как попытку что-то «исправить» (мне даже в голову такое не пришло) и «запретить запуск чего-либо», а как попытку проиллюстрировать, какие бывают «вредные» команды, запуск которых может испортить систему. Обычно народ в качестве такого пытается показать различные варианты rm -rf /, а тут — видимо, человек попытался изобразить chmod, снимающий x со всех-всех файлов.

                        Frosty, видимо, понял это так же и предложил «исправленный» вариант команды, которая гарантированно портит execution-биты во всех подкаталогах ниже текущего, а я, соответственно, добавил, что куда проще, если уж говорить о случайных командах, которые могут все испортить, попасться на рекурсивном chmod.

                        Как можно пытаться «решать» проблему того, что человек пользуется mc, работает под рутом и случайно может что-то запустить — я не знаю. Видимо, для начала отбирать у человека права и, возможно, mc.
                        • 0
                          Мне бы тоже не пришло в голову исправлять эту проблему этим способом. Но строки «3.1» в начале сообщения было достаточно, чтобы понять, что это уточнение к моему третьему пункту. В третьем пункте говорилось о том, что не надо использовать MC для запуска скриптов, соответственно напрашивается единственное толкование. Предположение о том, что та команда являлась примером скрипта с деструктивным поведением, противоречит контексту беседы: «3.1» в начале и вообще весь мой комментарий, на который, собственно, отвечали, не предполагают возможности данного толкования.
        • +1
          Да, упустил это, но смысл решения понятен.
    • –4
      1. Возможно первое правило действительно нуждается в лучшей формулировке. Да, у меня тоже не все скрипты просят подтверждения. Ключевые слова «выполняют действия» — это именно то, что Вы называете деструктивным поведением.
      2. Я пока не нашел универсального способа именования скриптов… Поделитесь.
      3. Я пользуюсь mc 12 часов в сутки. И он у меня по Enter не только запускает скрипты, но и открывает почти все файлы. Не говоря обо всех его остальных возможностях. Это такой удобный и естественный инструмент, что мне как-то даже нечего сказать по поводу «автодополнения в консоли».
      • +1
        Лично я именую скрипты по принципу «дополнения» какого-то функционала, это более-менее общепринятая практика, например для алиасов команд: lsa = ls -a или самописный скрипт обновления dns-записей локальной сети: bindlocalupdate
        Функции, подгружаемые оболочкой, обычно делаю «обобщающими», придумывая собственные запоминающиеся названия, например есть такая, давно откуда-то скопированная: extract
        смотреть код
        #
        # extract archives
        #
        
        extract ()
        {
            if [ -f "$1" ]
                then
                    case "$1" in
                         *.tar.bz2) tar xvjf "$1" ;;
                         *.tar.gz) tar xzvf "$1" ;;
                         *.bz2) bunzip2 -v "$1" ;;
                         *.deb) ar xv "$1" ;;
                         *.gz) gunzip -v "$1" ;;
                         *.rar) unrar xv "$1" ;;
                         *.rpm) rpm2cpio -v "$1" | cpio --quiet -i --make-directories ;;
                         *.tar) tar xfv "$1" ;;
                         *.tbz2) tar xjfv "$1" ;;
                         *.tgz) tar xzfv "$1" ;;
                         *.zip) unzip "$1" ;;
                         *.z) uncompress -v "$1" ;;
                         *.7z) 7z xv "$1" ;;
                         *) echo "'$1' cannot be extracted via extract" ;;
                esac
            else
                echo "'$1' is not a valid file"
            fi
        }
        

        • 0
          Подумал. Надо дописать, что именование «дополнением» удобно для вывода всех связанных с какой-то утилитой функций при дополнении в консоли (особенно удобно это в zsh, когда список динамически меняется под строкой ввода, а не пролистывается).
        • 0
          Что характерно — в современном мире эта функция практически бесполезна, все то же самое делает просто tar -xf. Использование «bunzip2» и «gunzip» — вообще сурово, т.к. в отличие от всех других альтернатив, еще и убивает исходный файл, никого не спрашивая.
          • 0
            Таскаю в .zshrc уже три года, не переписывая. Привёл в качестве примера, т.к. это самая используемая функция у меня.
            Привычка таскать из системы в систему свои функции и писать вспомогательные скрипты — это тема отдельного разговора. Многие программисты старшего поколения воспринимают такие привычки, как само-собой разумеющееся, для меня же польза этого открылась сравнительно недавно, а для многих моих сверстников, разбалованных современными DE, остаётся тайной.
            • 0
              Таскать, по идее, надо все настройки целиком — у меня они просто банально скоммичены в некий репозитарий, откуда деплоятся вот скриптом в виде симлинков, т.е. что-то типа:

              ~/.signature => ~/personal-home/home/.signature
              ~/.ssh/id_dsa => ~/personal-home/home/.ssh/id_dsa
              ~/bin/sum_all => ~/personal-home/home/bin/sum_all

              и т.д.
      • +2
        С первым правилом не согласен.
        В крайнем случае можно так:

        а) скрипт без параметров должен ничего не делать, либо делать что-то с теми путями, которые в нём (или в файле конфига) hardcoded (а раз так, значит он настроен пользователем системы на эту систему и ничего плохого не случится)

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

        в) можно добавить --dry-run

        И никаких проблем при случайном нажатии на скрипт в mc не будет.

        Если Вам нужен скрипт который что-то спрашивает, может Вам стоит в сторону GUI приложений посмотреть?
      • +1
        1. Возможно первое правило действительно нуждается в лучшей формулировке. Да, у меня тоже не все скрипты просят подтверждения. Ключевые слова «выполняют действия» — это именно то, что Вы называете деструктивным поведением.
          Зачем нужны скрипты, которые не выполняют действия?

          То, что я называю «деструктивным поведением» — это поведение, в результате которого безвозвратно теряются данные. Это не bunzip file.bz2/bzip2 file — оно обратимо. Это не загрузка скриншотов на Яндекс.Фотки, отмонтирование, убийство operapluginwrapper, печать файла как книги на принтере или даже удаление сокетов dtach (даже если всех, включая соответствующие рабочим процессам) (dtach — аналог screen, из которого выпилен весь функционал за исключением возможности работы после убийства эмулятора терминала/ssh сессии и возможности переприсоединения).
        2. Я пока не нашел универсального способа именования скриптов… Поделитесь.
          Именуйте так, как вам будет понятно. Более универсального принципа я не знаю.
        3. Я пользуюсь mc 12 часов в сутки. И он у меня по Enter не только запускает скрипты, но и открывает почти все файлы. Не говоря обо всех его остальных возможностях. Это такой удобный и естественный инструмент, что мне как-то даже нечего сказать по поводу «автодополнения в консоли».
          Естественным он быть не может, равно как и консоль и вообще все интерфейсы.

          Относительно удобства — опишите любой use‐case и я скажу, как это сделать быстрее в консоли с автодополнением. Единственный случай, когда mc является удобным — это когда я не помню, что находится в конкретном каталоге. Так как единственные каталоги, про которые я этого не помню — это то куда скачиваются torrent’ы, а все операции сводятся к изредка используемым aplayer /mnt/files/torrent/Series/Common\ series\ prefix\ <->\ *.mkv (можно и просто …/*.mkv, но в этом случае при случайном выходе из mplayer сложнее сказать, что хочешь смотреть все серии, начиная со, скажем, 15‐й. А так <-> заменяется на <15->), то иметь mc ради такого случая мне кажется странным. Кроме того, мне не понятно, как в случае запуска с N‐й серии мне поможет mc.
    • 0
      По поводу первого пункта — поддерживаю. Считаю гораздо правильнее обвешать критичные действия проверками, чем заставлять пользователя каждый раз делать проверки самому. Все мы знаем, чем заканчиваются бесконечные y/n?
    • +15
      Я бы сказал, что тут практически все советы — вредные:

      • Для того, чтобы писать скрипты на *bash*, по-моему, нужны серьезные основания. От использования /bin/sh вместо bash жизнь часто становится сильно проще.
      • Написание велосипедных «пользовательских интерфейсов» с помощью read, а не dialog — ужасно и приводит к совсем диким последствиям, если вместо пользователя внезапно возникает пайп.
      • Написание выводимых сообщений безальтернативно на русском языке — однозначное зло и грабли, которые больно бьют, как только человеку внезапно придется воспользоваться какой-нибудь консолью со странной кодировкой или передать скрипт коллеге, не говорящему по-русски.
      • Написание собственных getopt — зло. За нарушение всеми принятых и всеми ожидаемых принципов обработки однострочных опций и предложение сокращать "--show-files-only" до "-sfo" нужно убивать :)
      • Написание собственных библиотек функций — может, и не столь очевидное зло (хотя вот зачем писать, когда можно взять готовое, явно более оттестированное и документированное, чем напишется на коленке?), но совет класть это в /usr/bin, а не в ~/bin, например — это явно bad practice.
      • «Постоянно встречающееся сочетание» 1>&2, субъективно, во-первых, лучше записывать, как >&2, во-вторых, раз уж выводить error messages хочется так часто — сделать в библиотеке функций типа функции «log_info», «log_warning», «log_error» — их и раскрашивать можно, и поотключать, и позаворачивать при желании в разные места — и все такое.
      • echo "" не стоит писать почти никогда — достаточно echo без параметров.
    • 0
      1) имхо тоже не лучший вариант, я пользуюсь в таком случае правилом:
      скрипт всегда должен требовать ключи для свой работы, скрипт запущенный без ключей выдает справку по пользованию скриптом
  • +4
    Названия функций вроде «myAskYNE» никуда не годятся. По такому названию не определишь, что функция делает.
  • +4
    Читал и ждал совета всегда использовать «set -e». Этим часто пренебрегают, но это действительно спасает от глупых ошибок.
    что-то вроде

    PATH="$(error_function)"
    rm -rf /path/to/$PATH
    


    Если some_function выдаст ошибку, PATH будет пустой строкой и будет вызван rm -rf /path/to/ и удалит всю папку

    Если же использовать set -e, то скрипт прекратит работу сразу после первой строки
    • 0
      set -e это очень хорошо, но неиспользование стандартных переменных должно быть условным рефлексом. В данном конкретном примере ничего не будет удалено, поскольку оболочка не будет знать, где искать rm.
      • 0
        это же не рабочий код, ясное дело что PATH не используют. вообще да, принято. жаль что уже не могу отредактировать.
    • +2
      Использовать -e лучше всего с -x — а то иногда не понятно что и где отвалилось.
      А еще внимательно читать документацию на программы которые использутся, например банальный grep в конце пайпа(ов) (например: "cat file | head -n 2 | grep smthn") если ничего не нашел — вернет 1, и скрипт завершится.
      Или использование expr, казалось бы, что плохого в res=$(expr 1 - 1), но expr вернет 1 если результат операции будет 0.

      В общем с "-e" нужно очень внимательно подходить к тому чего хочется достичь. Иногда лучше регулярно проверять код выхода чтобы выводить человеческие сообщения об ошибках, чем просто завершать скрипт по любому поводу.
  • +6
    Насчет правила написания «скриптов». Я бы сказал так, что «правила написания скриптов» НЕ должны особенно отличаться от «правил написания программ»:

    1) Ваш скрипт/программа принимает какие-то параметры? Предусмотрите штатный способ получения информации о том, какие это параметры и что они означают, без необходимости лазить в исходник скрипта/программы. Т.е. например выдавать «usage()» по -р/--help. Вы же не лезете каждый раз в исходник tar если не помните навскидку каким ключом можно задать список «exclude files»?

    2) Ваш скрипт/программма может произвести «деструктивное» действие? Обьект над которым проводиться это действие — должен задаваться исключительно в качестве параметра, и ни в коем случае, не «по умолчанию». Обратите внимание на то, как это работает: Например «недеструктивная» комманда ls имеет «обьект по умолчанию» — текущую директорию. А представьте какой «адский песец» был бы, если бы программа mkfs «по умолчанию» использовала бы, например /dev/sda1. Или rm безпараметров бы вычищал текущую директорию?

    3) Для скрипта/программы exit-код обязателен! Можно даже без «детализации причин». Обязательное правило: Минимальная возвращаемая информация — это 0/1, сигнализирующая о том, что «да, запрашиваемое действие успешно выполнено» или «нет, запрашиваниемое действие не выполнено». Это даст возможность использовать ваш скрипт/программу из других скриптов/программ.

    4) Для скриптов/программ имеющих возможность работать в пакетном режиме, должна быть возможность работы в полностью пакетном режиме. Т.е. НЕ надо настаивать на лишней интерактивности там, где она не является необходимой. Это к вопросу, по поводу «Вы уверенны [y/n]?». Самый лучший пример: программа rm, у которой есть ключ "-i" заставляющий спрашивать подтверждения длействия, а также ключ "-f" заставляющий ее продолжить пакетную работу даже если возникли проблемы с удалением конкретного файла. Если бы интерактивность rm была бы всегда, ее бы просто выкинули на помойку сразу после первой же необходимости прочистить директорию с несколькими тысячами файлов.
  • +15
    Нет, я понимаю, что тут не принято критиковать подобные посты, но всё же.

    1. Unix никогда не скажет «пожалуйста». У этого есть причина. Покажите в багтрекере баша баг вроде «cd должен спрашивать подтверждения перед переходом в каталог». Написанные пользователями windows программы, ведущие диалог с пользователем, бесят хотя бы из-за сложности скриптования (пример — ntpasswd). Да, скрипты используются наравне со всеми UNIX-командами и точно так же могут быть в любом месте конвеера. Не заставляйте пользователей выкусывать бесполезный выхлоп. Мусорить \n в stderr тоже нехорошо.

    3. Не прикасайтесь к /usr/bin руками без серьёзной причины. Там работает пакетный менеджер. Да и бэкапить отдельные файлы из него неудобно. Библиотеке функций место где-нибудь в (сюрприз) ~, куда пользователь может писать. В произвольном дотфайле, включенном в ~/.$SHELLrc, поскольку в повседневной работе (не все же пользуются mc) функции тоже нужны, не зря же их писали.

    4. Если сообщение кажется лишним и ему не место в выводе — оно лишнее (вообще можно сделать $DEBUG, если сильно хочется). Если оно критично — оно должно быть в stdout, чтобы его мог протать следующий скрипт в пайпе. В чём смысл озвучивания test(1)? ЕМНИП, на хабре уже был топик о плохих сообщениях об ошибках, тут — каноничный пример.

    5. Не организуйте диалог с пользователем велосипедными средствами. Во-первых, пользователь может оказаться пайпом. Во-вторых, есть dialog(1). И да, curPath=$(basename $BASH_SOURCE).

    Скрипт — это программа, пусть и написанная для себя и для конкретной машины/группы машин. Не надо ненавидеть себя, ухудшая юзабилити. Хотя пользователям mc сойдёт.
    • +7
      Насчет Пункта 3. не все хабраюзеры знакомы «со стандартами», поэтому позволю себе «развернуть данный пункт»:

      * /usr/bin — для тех программ, которые установленны из вашего дистрибутива. туда они ставятся, оттуда удаляются, там обновляются пакетным менеджером. И таки да, что-то самому туда класть — надо иметь серьезную причину.

      * ~/bin — для тех программ, которые пользователем предполагаются для использования для самого себя. Т.е. это те скрипты, которыми пользователь автоматизирует свою работу над своими файлами.

      * /usr/local/bin — программы, существующие/установленные/написанные для локально конкретной машины/группы машин. Все то, что не является частью дистрибутива, а установленно/написанно админом руками. Сами пакетные менеджеры дистрибутивов никогда туда ничего не кладут. Все что там есть — установил локально админ.

      * /opt/[product-name]/bin — бинарники «больших» опциональных «программных продуктов». Если у вас такой массивный, тесно связанный программный продукт, то велкам туда.

      P.S.#1: таки-да, некоторых товарищей, активно проталкивающих самые худшие практики из мира windows в мир unix, хочется пристрелить.
      P.S.#2: насчет «mc юзеров» — мы же пониманием, что те mc юзеры о которых вы говорите, они же его просто «не умеют правильно готовить» (одни макроподстановки многого стоят, не говоря уже о прочей кастомизации)

      • +4
        Подводя все вышесказанное. Все ваши скрипты должны лежать в ~/scripts. Всегда. Пофиг на хороший тон, главное — удобство.
        Туда может писать пользователь
        Туда не нужны права доступа
        Каталог с пользовательскими скриптами бекапится наравне с пользовательскими данными, и при этом не надо помнить — тот скрипт в /bin мой, а этот нет.
        Если в PATH вы установите каталог выше системного, вы можете заменить системные программы, не влияя на остальную систему — rm -> rm -i(да, я знаю что это лучше сделать алиасом, но это пример, большие скрипты в алиасы оборачивать трудно и ненаглядно. и вызвать их не из сеанса нельзя)

        Можно добавить папку в ~/.$SHELLrc, можно просто в скрипте логина PATH=/home/vvzvlad/scripts;$PATH
  • +1
    Поддержу вышеотписавшихся, правило номер 1 из области вредных советов. Обычно консольные программы наоборот делают своё дело тихо, без лишних вопросов. Правда большинству консольных утилит необходимо передать некие параметры, например имя файла, или ключи. Без параметров программа, которой они обязательно нужны должна выводить некий мануал по использованию себя.

    Но правило номер 1 разве что будет уместно, если скрипт пишется только для себя, когда обычно лениво(или нет времени) делать ключи, вместо этого данные хардкодятся в самом скрипте, тогда действительно полезно напомнить пользователю что делает скрипт.
    • 0
      Выводить запросы полезно при распространении скриптов с чем-либо, а не как самостоятельных утилит. Например, я пользуюсь запросы и case-ы zenity для создания сценариев работы с git-репозиториями, там же и размещая.
      Для себя, как раз, быстрее и проще сохранить одну команду с кучей параметров, а для скрипта, решающего единственную задачу лучше действия пользователя ограничить сценарием, где можно прописать кучу правил.
      С последней ситуацией в консоле поможет справиться zsh, но не все им пользуются.
  • 0
    Я бы рекомендовал сделать функцию log/message для сообщений консоли и сообщений в лог и по соответствующим параметрам командной строки вести вывод в консоль/лог(если пайп или планировщик) — все что вывелось на консоль при вызове из планировщика уйдет письмом на root@.
    библиотеки хранить в ~/bin — в системные каталоги лучше не лезть — как пить дать забудете, а скопировав ~ — все переехало.
    а вообще это все дело здравой логики — я, например когда написал свои скрипты по обработке видео от вебкамер, про сервер вообще забыл, только потому что предусмотрел максимальное количество проблем сразу — уход в даун самба шары на сервере бекапов, очистку места на локальном разделе, куда складывается архив от камер. Я последний раз зашел на сервер перед увольнением закрыть сессии и обновить — скрипты работали как часы. у меня одна из публикаций на хабре как раз про них.
    PS — да, у меня даже проверялось наличие зависимостей инсталляционным скриптом.
  • –1
    Не стоит ничего закидывать системные директории руками без веской на то причины, ибо такое действие может породить трудноуловимые баги (типа внезапно сменившейся локали, $PATH и так далее). Предлагаю так:

    Выкидываем bash, берем любой скриптовый язык типа перла, руби или питона. Можете считать это личным мнением, но не вижу смысла интенсивно использовать sh скрипты, когда есть более мощные (и, субъективно, с менее крышесносящим синтаксисом) языки типа перла, где в большинстве случаев написание скрипта сведется к написанию cli-интерфейса к какому-нибудь модулю с cpan.

    Пишем установочный скрипт, кидаем все в гит-репозиторий. Установщик должен определять, запущен ли он с повышенными привилегиями (и тогда ставить основной скрипт в /usr/local/bin) или нет (скрипт отправляется в $HOME/bin, эта же директория добавляется в $PATH). Тогда на целевых машинах делаем curl path/to/raw/installer.script | [sudo] sh. Можно хранить в отдельном репозитории бандлы — наборы адресов к инсталлерам, и тогда можно будет одной командой развертывать десятки скриптов.
    • +2
      Интересная схема, как-то сам не додумывался. Практикуете такое или это был теоретический подход?
      • 0
        Частично. Пока есть разнесенные по машинам организованные таким образом скрипты. Все, что мне нужно — залить их на гитхаб, заодно можно навести порядок в .dot файлах. Пришла в голову собрать все именно таким способом только сейчас.

        На гитхабе некоторые проекты практикуют такую схему установки. Например:

        github.com/kraih/mojo
        github.com/nvie/gitflow/wiki/Linux
  • +3
    Совет 1 — не пользуйтесь mc, лучше настраивайте окружение что бы не требовалось бегать по путям лишний раз. А привычка работать просто в командной строке вам поможет если сервер будет в перегрузке.
    Совет 2 — прежде чем что-то вещать, читайте и гуглите. Есть много вещей, которые в общем называются unix way, жаль я не видел их более-менее оформленными как например в питоне. Одна из таких вещей — утилиты и скрипты должны делаться атомарными, на одно действие, но достаточно полными, уметь работать с потоками io и так далее. Для стандартных действий всегда можно найти уже рабочий и отлаженный скрипт или утилиту.
    Совет 3 — для того что бы пользоваться ем-нить надо неделя-месяц, что бы стать более-менее опытным в чем либо надо полгода-год. Уровень эксперта — 2-3 года и уровень мастера 5-7 лет ( пресловутые 10к часов ). Каждый из пройденных этапов можно явно почувствовать улучшением эффективности в критичной на этот момент работе, что вызывает обычно небольшую эйфорию и желание поделиться своей мудростью. Воздерживайтесь по мере сил от этого, пока не станете мастером.

  • +6
    И это в плюсе?! Ужс.

    Приемы написания скриптов в Bash

    Собственно, конкретно bash'а я тут и не увидел(впрочем, код смотрел по диагонали).
    Вы вдруг нажали не на тот скрипт, то с системой может произойти все что угодно.

    Вы работаете под рутом?
    Ты уверен, что хочешь запустить это (y/[a])

    Собственно к каждому скрипту надо делать usage(), а не эту хрень спрашивающую почём зря неадекватные вопросы. Как вы этот скрипт будете запускать на 100 серверов? Писать второй на expect?
    В Bash не очень хорошо обстоят дела с возвратом значения из функции

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

    Автору: сходи-ка ты почитай хороший sh, например, svn.freebsd.org/base/head/etc/rc.subr
    • +7
      > И это в плюсе?! Ужс.

      Ну почему же. Хороший пример того, когда комментарии ценнее самой статьи и плюсовать ее стоит как минимум за это :)
  • +2
    Совет 0. Ваш sh-скрипт должен занимать максимум 2-4 экрана в редакторе. Если скрипт слишком большой — перепишите его на нормальном языке программирования. Perl и Python сейчас есть практически везде (embedded-гусары, молчать!).

    Если Вы не можете написать простой и лаконичный скрипт, то это не скрипт. Либо напишите его и никому не показывайте (.bashrc — отличное место, где можно делать всё, что Вам угодно).

    Противники этого совета могут попробовать взять на поддержку пачку унаследованных связанных друг с другом sh-скриптов хотя бы в 2 тысячи строк размером и 5 лет возрастом и поделиться ощущениями.
    • +3
      Для пущего эффекта снабдить скрипт кучей объёмных неформатированных sed и awk вставок без единого комментария. Например таких:
      echo 0|sed 's909=bO%3g)o19;s0%0aob)]vO0;s()(0eh}=(;s%}%r1="?0^2{%;
      s)")@l2h3%"@$);sw%wh]r()$o%!w;sz(z^+.z;sa+a !z" a;sxzxi?v{a)ax;:b;
      s/\(\(.\).\)\(\(..\)*\)\(\(.\).\)\(\(..\)*%.*\6.*\2.*\)/\5\1\3\7/;
      tb;s/%.*//;s/.\(.\)/\1/g' 
      • +4
        К сожалению ваш код не портабельный :) POSIX sed сломался на цикле, вот этот должен работать и не на gnu sed:
        echo 0|sed 's909=bO%3g)o19;s0%0aob)]vO0;s()(0eh}=(;s%}%r1="?0^2{%;
        s)")@l2h3%"@$);sw%wh]r()$o%!w;sz(z^+.z;sa+a !z" a;sxzxi?v{a)ax;:b;
        s/\(\(.\).\)\(\(..\)*\)\(\(.\).\)\(\(..\)*%.*\6.*\2.*\)/\5\1\3\7/;
        tb;
        s/%.*//;s/.\(.\)/\1/g'
        

        (метка в данном случае будет b; так как отсекается по концу строки а не разделителю)
  • +6
    Совет -1. Если вы пользуйтесь скриптами для автоматизации каких-либо процессов, которые должны проходить без участия пользователя, то не пользуйтесь Советом 1.
    Ваш кэп.

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