Администраторам 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
Какой скрипт без параметров? Об их обработке написано тонны литературы. Поделюсь своим видением.
- Крайне желательно, чтобы параметры обрабатывались независимо от их последовательности.
- Я не люблю использовать однобуквенные параметры (а следовательно и getopts) по той простой причине, что скриптов очень много, а букв мало. И запомнить, что для одного скрипта -r означает replace, для другого replicate, а для третьего вообще remove практически невозможно. Поэтому я использую 2 нотации, причем одновременно: а) --show-files-only, б) -sfo (как сокращение от предыдущего). Практика показывает, что такие ключи запоминаются мгновенно и очень надолго.
- Скрипт должен выдавать ошибку на неизвестный ему ключ. Это частично поможет выявить ошибки при написании параметров.
- Из совета 2 возьмем правило: никогда не запускать скрипт без подтверждения. Но добавим к этому важное исключение — если не указан ключ --yes (ключ, конечно, может быть любым).
- Ключи могут сопровождаться значением. В этом случае для длинных ключей действует такое правило: --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