8 подсказок, как сделать ваши скрипты лучше

/ Просмотров: 3643
Метки: ,

Перевод статьи "8 Tips for creating better bash scripts", написанной Benjamin Cane и опубликованной на сайте http://bencane.com 6 июня 2014

"8 Tips for creating better bash scripts"

Benjamin Cane, 2014⁄06⁄06

Когда я только начал администрировать Linux и Unix серверы, я работал в среде, где были тонны скриптов, написанных другими администраторами. Время от времени мне приходилось тестировать их, выясняя, почему тот или иной скрипт перестал работать. Иногда они были написаны правильно и понятно, иногда неуклюже и запутанно.
Хотя поиск и устранение ошибок в плохо написанных скриптах были всегда напрягом, они преподали мне важный урок. Это показало мне, что, даже если вы не думаете, что скрипт будет использоваться в будущем, лучше писать его так, как будто кто-нибудь будет диагностировать его через два года. Причем этот "кто-нибудь" может оказаться кем угодно, даже вами же самим.

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

ВСЕГДА НАЧИНАЙТЕ СВОИ СКРИПТЫ С ШЕБАНГ

Первое правило сценариев оболочки состоит в том, чтобы всегда начинать их шебангом. Хотя имя может звучать забавно, строка шебанга очень важна, она сообщает системе, что именно использовать в качестве интерпретатора для сценария. Без шебанга система не знает, какой язык использовать для обработки скрипта.
Типичный bash-шебанг выглядит так:

#!/bin/bash

Я видел много скриптов, где эта строка отсутствовала. Можно было подумать, что это и было причиной того, что они не работали, но это не обязательно верно. Если в скрипте явно не определен интерпретатор, то некоторые системы принимают значение по умолчанию как ⁄bin⁄sh. При дефолтном значении всё будет в порядке, если сценарий написан для оболочки Bourne, однако если скрипт подразумевал KSH или что-то иное, результаты могут быть неожиданными.
В отличие от других подсказок в этой статье, эта – скорее не подсказка, а правило. Вы должны всегда начинать ваши скрипты с определения интерпретатора. Без этого они в конечном счете перестанут работать.

ПОМЕСТИТЕ ОПИСАНИЕ СЦЕНАРИЯ В ЕГО НАЧАЛЕ

Каждый раз, когда я создаю сценарий или любую подобную программу, я стараюсь поместить в его начало описание того, что он делает.
Также я включаю в комментарии свое имя и, если пишу скрипт для работы, свой почтовый адрес и дату создания.
Вот пример заголовка скрипта:

#!/bin/bash
#### Description: Adds users based on provided CSV file 
#### CSV file must use : as separator
#### uid:username:comment:group:addgroups:/home/dir:/usr/shell:passwdage:password
#### Written by: Benjamin Cane - ben@example.com on 03-2012

Почему я так поступаю? Это очень просто. Описание должно обьяснить любому, что делает скрипт и сообщить любую информацию, которая может потребоваться. Я пишу свое имя и адрес затем, чтобы у читающих скрипт была возможность связаться со мной и обсудить возникшие вопросы. Я ставлю дату, чтобы было представление о том, когда скрипт был написан. Дата также добавляет каплю ностальгии, когда вы находите скрипт, написанный вами давно, и спрашиваете себя: "О чем я думал, когда писал это?"
Описательный заголовок может быть каким угодно по вашему желанию, нет никаких правил на его счет. В общем, просто сделайте его информативным и позаботьтесь поместить его в самое начало.

ДЕЛАЙТЕ ОТСТУПЫ В КОДЕ

Оформление кода таким образом, чтобы он имел читаемый вид, очень важно, о чем многие люди забывают. Прежде чем мы заберемся вглубь вопроса, почему отступы важны, посмотрим на пример:

NEW_UID=$(echo $x | cut -d: -f1)
NEW_USER=$(echo $x | cut -d: -f2)
NEW_COMMENT=$(echo $x | cut -d: -f3)
NEW_GROUP=$(echo $x | cut -d: -f4)
NEW_ADDGROUP=$(echo $x | cut -d: -f5)
NEW_HOMEDIR=$(echo $x | cut -d: -f6)
NEW_SHELL=$(echo $x | cut -d: -f7)
NEW_CHAGE=$(echo $x | cut -d: -f8)
NEW_PASS=$(echo $x | cut -d: -f9)    
PASSCHK=$(grep -c ":$NEW_UID:" /etc/passwd)
if [ $PASSCHK -ge 1 ]
then
echo "UID: $NEW_UID seems to exist check /etc/passwd"
else
useradd -u $NEW_UID -c "$NEW_COMMENT" -md $NEW_HOMEDIR -s $NEW_SHELL -g $NEW_GROUP -G $NEW_ADDGROUP $NEW_USER
if [ ! -z $NEW_PASS ]
then
echo $NEW_PASS | passwd --stdin $NEW_USER
chage -M $NEW_CHAGE $NEW_USER
chage -d 0 $NEW_USER 
fi
fi

Этот код делает свою работу, да. Но выглядит при этом несимпатично, и если бы он содержал 500 подобных строк без отступов, то было бы довольно трудно понять, что где в нем происходит. Теперь посмотрим на тот же код с добавлением отступов:

NEW_UID=$(echo $x | cut -d: -f1)
NEW_USER=$(echo $x | cut -d: -f2)
NEW_COMMENT=$(echo $x | cut -d: -f3)
NEW_GROUP=$(echo $x | cut -d: -f4)
NEW_ADDGROUP=$(echo $x | cut -d: -f5)
NEW_HOMEDIR=$(echo $x | cut -d: -f6)
NEW_SHELL=$(echo $x | cut -d: -f7)
NEW_CHAGE=$(echo $x | cut -d: -f8)
NEW_PASS=$(echo $x | cut -d: -f9)    
PASSCHK=$(grep -c ":$NEW_UID:" /etc/passwd)
if [ $PASSCHK -ge 1 ]
then
  echo "UID: $NEW_UID seems to exist check /etc/passwd"
else
  useradd -u $NEW_UID -c "$NEW_COMMENT" -md $NEW_HOMEDIR -s $NEW_SHELL -g $NEW_GROUP -G $NEW_ADDGROUP $NEW_USER
  if [ ! -z $NEW_PASS ]
  then
      echo $NEW_PASS | passwd --stdin $NEW_USER
      chage -M $NEW_CHAGE $NEW_USER
      chage -d 0 $NEW_USER 
  fi
fi

В версии с отступами код гораздо понятнее. Видна вложенность операторов, которую вы не сможете уловить при беглом взгляде на первый вариант записи, без отступов.
Стиль отступов – ваше личное дело. Хотите ли вы использовать 2 пробела, или 4, или табуляцию, не имеет никакого значения. Что имеет значение – так это то, что в коде должны соблюдаться избранные вами правила для отступов.

ДОБАВЬТЕ ПРОСТРАНСТВА

Где отступ делает код понятнее, пустые строки делают код более удобочитаемым. В целом мне нравится растягивать код, основываясь на том, что он делает. Это также личное предпочтение и пункт, делающий код проще для чтения и понимания.
Пример кода, приведенного выше, с добавлением интервалов:

NEW_UID=$(echo $x | cut -d: -f1)
NEW_USER=$(echo $x | cut -d: -f2)
NEW_COMMENT=$(echo $x | cut -d: -f3)
NEW_GROUP=$(echo $x | cut -d: -f4)
NEW_ADDGROUP=$(echo $x | cut -d: -f5)
NEW_HOMEDIR=$(echo $x | cut -d: -f6)
NEW_SHELL=$(echo $x | cut -d: -f7)
NEW_CHAGE=$(echo $x | cut -d: -f8)
NEW_PASS=$(echo $x | cut -d: -f9)

PASSCHK=$(grep -c ":$NEW_UID:" /etc/passwd)
if [ $PASSCHK -ge 1 ]
then
  echo "UID: $NEW_UID seems to exist check /etc/passwd"
else
  useradd -u $NEW_UID -c "$NEW_COMMENT" -md $NEW_HOMEDIR -s $NEW_SHELL -g $NEW_GROUP -G $NEW_ADDGROUP $NEW_USER

  if [ ! -z $NEW_PASS ]
  then
      echo $NEW_PASS | passwd --stdin $NEW_USER
      chage -M $NEW_CHAGE $NEW_USER
      chage -d 0 $NEW_USER 
  fi
fi

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

КОММЕНТИРУЙТЕ СВОЙ КОД

Начало скрипта хорошо подходит для описания его назначения, а для описания того, что происходит в коде, нужно добавлять комментарии в код. Ниже я покажу всё тот же фрагмент кода, но на сей раз добавлю комментарии, обьясняющие, что происходит на том или ином участке кода:

## Parse $x (the csv data) and put the individual fields into variables
NEW_UID=$(echo $x | cut -d: -f1)
NEW_USER=$(echo $x | cut -d: -f2)
NEW_COMMENT=$(echo $x | cut -d: -f3)
NEW_GROUP=$(echo $x | cut -d: -f4)
NEW_ADDGROUP=$(echo $x | cut -d: -f5)
NEW_HOMEDIR=$(echo $x | cut -d: -f6)
NEW_SHELL=$(echo $x | cut -d: -f7)
NEW_CHAGE=$(echo $x | cut -d: -f8)
NEW_PASS=$(echo $x | cut -d: -f9)

## Check if the new userid already exists in /etc/passwd
PASSCHK=$(grep -c ":$NEW_UID:" /etc/passwd)
if [ $PASSCHK -ge 1 ]
then
  ## If it does, skip
  echo "UID: $NEW_UID seems to exist check /etc/passwd"
else
  ## If not add the user
  useradd -u $NEW_UID -c "$NEW_COMMENT" -md $NEW_HOMEDIR -s $NEW_SHELL -g $NEW_GROUP -G $NEW_ADDGROUP $NEW_USER

  ## Check if new_pass is empty or not
  if [ ! -z $NEW_PASS ]
  then
      ## If not empty set the password and pass expiry
      echo $NEW_PASS | passwd --stdin $NEW_USER
      chage -M $NEW_CHAGE $NEW_USER
      chage -d 0 $NEW_USER 
  fi
fi

Если вы случайно встретили бы этот отрывок bash-кода и не знали, что он делает, вы могли бы понять это с помощью комментариев. Добавление комментариев к вашему коду будет чрезвычайно полезно следующему парню, и это может выручить даже вас самих. Я смотрю на скрипт, написанный мной месяц назад, и задаюсь вопросом: "Интересно, что я тут делал?" Если вы неукоснительно добавляете к коду комментарии, это может выручить и вас, и других даже спустя много времени.

ДАВАЙТЕ ПЕРЕМЕННЫМ ПОНЯТНЫЕ ИМЕНА

Называть переменные понятными именами может показаться очевидной вещью, но я всегда использую универсальные имена переменных. В большинстве это временные (локальные) переменные, которые никогда не используются за пределами данного блока кода, но даже с временными переменными всегда хорошо обьяснять, какие значения они содержат.
Ниже пример имен переменных, которые являются главным образом описательными.

for x in `cat $1`
do
    NEW_UID=$(echo $x | cut -d: -f1)
    NEW_USER=$(echo $x | cut -d: -f2)

В то время как довольно очевидно, что входит в $NEW_UID и $NEW_USER, не обязательно очевидно, что означает 1$, или что устанавливается как $x. Более понятный способ записать тот же код ниже.

INPUT_FILE=$1
for CSV_LINE in `cat $INPUT_FILE`
do
   NEW_UID=$(echo $CSV_LINE | cut -d: -f1)
   NEW_USER=$(echo $CSV_LINE | cut -d: -f2)

В переписанном блоке становится очевидно, что мы читаем входной файл, и этот файл формата CSV. Также наглядно, где мы заставляем новый UID и новую информацию о ПОЛЬЗОВАТЕЛЕ хранить в переменных $NEW_UID и $NEW_USER.
Пример выше может показаться некоторым излишеством, но кто-нибудь позже может поблагодарить вас за небольшое время, потраченное на то, чтобы сделать ваши переменные более понятными.

ИСПОЛЬЗУЙТЕ КОНСТРУКЦИЮ $(COMMAND) ДЛЯ ПОДСТАНОВКИ

Если вы хотите создать переменную, значение которой получено из другой команды, в bash есть два способа сделать это. Первый – обернуть команду в верхние обратные одинарные кавычки (гравис), как в примере ниже:

 DATE=`date +%F`

Второй метод использует другой синтаксис:

 DATE=$(date +%F)

Оба технически корректны, но лично я предпочитаю второй. Это чисто персональное предпочтение, но в целом я думаю, что синтаксис $(command) более очевиден. Скажем, вы роетесь в сотнях строк bash-кода, читаете и читаете, и наконец, перестаете различать знаки ` и '. Вдобавок ко всему, иногда одинарная кавычка имеет тенденцию быть похожей на обратную галочку. В конце дня всё это сводится к выбору. Так что используйте то, что лучше для вас; просто убедитесь, что действуете в соответствии с выбранным методом.

ПРЕЖДЕ ЧЕМ ВЫЙДЕТЕ НА ОШИБКУ, ОПИШИТЕ ПРОБЛЕМУ

Мы рассмотрели несколько приемов, которые делают код проще для чтения и понимания, но этот последний из них полезен прежде чем процесс устранения неполадок даже доберется до этой точки. (before the troubleshooting process even gets to that point.) Добавив описание ошибок скрипта, можно сберечь кому-то много времени на раннем этапе поиска и устранения неисправностей. Посмотрим на следующий код и подумаем, как мы можем сделать его более информативным.

 if [ -d $FILE_PATH ]
then
  for FILE in $(ls $FILE_PATH/*)
  do
    echo "This is a file: $FILE"
  done
else
  exit 1
fi  

Первым делом этот сценарий проверяет, является ли значение переменной $FILE_PATH каталогом, и если нет, то завершается с кодом 1 (ошибка). Замечательно, что мы использовали код выхода, который сообщит, что данная часть кода завершилась с ошибкой, следующим командам, но не людям, выполняющим скрипт.
Давайте сделаем этот код немного человекопонятнее и дружественнее:

if [ -d $FILE_PATH ]
then
  for FILE in $(ls $FILE_PATH/*)
  do
    echo "This is a file: $FILE"
  done
else
  echo "exiting... provided file path does not exist or is not a directory"
  exit 1
fi

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

Всё вышеописанное – всего лишь несколько правил, которые я стараюсь использовать всякий раз, когда пишу сценарий. Я уверен, что есть другие серьезные подсказки (other great tips) для создания чистых и читабельных bash-скриптов. Если вы их знаете, не стесняйтесь писать о них в комментариях. Всегда приятно видеть приемы, придуманные другими.

Оставьте комментарий

Комментарий будет опубликован после проверки

(обязательно)