Приемы написания скриптов в 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
Big-IP Edge Client openconnect client cisco secure client download netextender download for mac wisenet viewer download admanager plus download globalprotect download citrix secure access watchguard ssl vpn download Qfinder Pro fortinet vpn forticlient mac