Приемы написания скриптов в 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 проверить на непустое значение третий параметр и вывалиться по ошибке в таком случае. И так далее.


См. примеры кода для статьи.

  • Soft Space SIA
    Latvia
  • Этот адрес электронной почты защищен от спам-ботов. У вас должен быть включен JavaScript для просмотра.
  • +371 25 46 42 49