Bash в примерах, часть 1

Основы программирования в Bourne again shell (bash)

Введение

Вы можете удивиться, почему вы должны изучать программирование в bash. Хорошо, вот пара веских причин:

Вы уже работаете в нем

Если вы проверите, вы вероятно обнаружите, что прямо сейчас работаете в bash. Даже если вы изменили используемую вами оболочку, bash, вероятно, где-то на вашей системе все еще выполняется, поскольку это стандартная оболочка Linux и она используется в самых разнообразных целях. Поскольку bash уже выполняется, любым дополнительным скрипты bash, которые вы запускаете, присуща эффективность по памяти, потому что они разделяют память с любым уже выполняемым процессом bash. Зачем загружать интерпретатор в 500K, если вы уже выполняете что-то, что может справиться и справиться хорошо с вашей задачей?

Вы уже используете его

Вы не только уже запустили bash, но вы на самом деле каждый день взаимодействуете с bash. Он всегда здесь, то есть имеет смысл изучить, как использовать его потенциал в полной мере. Такое изучение сделает ваш опыт работы с bash более плодотворным и веселым. Но почему вы должны изучать программирование на bash? Это очевидно, потому что вы уже думаете в терминах выполняющихся команд, копирования файлов, конвейеров и перенаправления вывода. Нужно ли изучать язык, который позволит вам использовать, строить на основе его экономящих время конструкций, которые вы уже знаете, как использовать? Командные оболочки раскрывают потенциал систем UNIX, а bash это оболочка Linux. Это клей высокого уровня между вами и машиной. Обогатите ваше знание bash, и вы автоматически повысите вашу производительность под Linux и UNIX, это же очевидно.

Путаница Bash

Неправильное изучение bash может быть очень беспорядочным процессом. Многие новички набирают 'man bash', чтобы посмотреть сраницы руководства man, только чтобы оказаться наедине с очень кратким и техническим описанием функциональности оболочки. Другие набирая 'info bash' (чтобы увидеть документацию GNU info), вызывают либо вывод той же страницы руководства man, либо (если повезет) лишь чуть более дружественной документации info.

Хотя это и может как-то разочаровать новичков, но стандартная документация bash не может быть всем для всех, а годится тем, кто уже в общих чертах знаком с программированием на bash. Определенно есть много превосходной технической информации в руководстве man, но для новичков ее полезность ограничена.

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

Переменные среды

В bash и почти во всех остальных оболочках пользователь может определить переменные среды, которые хранятся как строки ASCII. Одно из самых удобных свойств переменных среды заключается в том, что они являются стандартной частью модели процессов UNIX. Это значит, что переменные среды доступны не только скриптам оболочки, но их также можно использовать и в стандартно скомпилированных программах. Когда в bash мы "экспортируем" переменную среды, любая последующая запускаемая нами программа может прочесть нашу установку, является ли эта программа скриптом оболочки или нет. Хороший пример - команда 'vipw', которая обычно позволяет пользователю root редактировать системный файл паролей. Определив в переменной среды 'EDITOR' имя вашего любимого редактора, вы можете сконфигурировать использование vipw вместо vi, удобно, если вы пользуетесь xemacs и в самом деле не любите vi.

Стандартный способ определить переменную среды в bash:

$ myvar='Это моя переменная среды!'

Эта команда определила переменную среды с именем "myvar" и содержит строку "Это моя переменная среды!". Стоит сделать несколько замечаний: во-первых, с обеих сторон от знака "=" нет никаких пробелов; любой пробел приведет к ошибке (попробуйте и увидите). Второе, о чем стоит сказать, это то, что мы могли бы опустить кавычки, если бы определяли единственное слов, но кавычки необходимы, если значение переменной среды состоит из более чем единственного слова (содержит пробелы или табуляцию).

Note:

Очень подробную информацию о том, как следует использовать кавычки в bash, вы можете посмотреть в разделе "ЭКРАНИРОВАНИЕ" (QUOTING) страницы руководства man. Наличие специальных последовательностей символов, которые дают "раскрытые" (замененные) другими значениями, усложняет обработку строк в bash. Мы дадим в этой серии наиболее часто используемую функциональность квотирования.

Третье, хотя мы обычно можем использовать двойные кавычки вместо одинарных, если мы сделаем это в предыдущем примере, это приведет к ошибке. Почему? Потому что использование одинарных кавычек лишает bash свойства, которое называется раскрытие, когда специальные символы и последовательности символов заменяются их значениями. Например, символ "!" это символ раскрытия истории, bash обычно заменяет его предыдущей набранной командой. (В этой серии статей мы не даем раскрытие истории, поскольку оно не часто используется в программировании на bash. Более подробную информацию смотри в разделе "РАСКРЫТИЕ ИСТОРИИ" (HISTORY EXPANSION) руководства man по bash.) Хотя такая макро функциональность может оказаться удобной, сейчас в конце нашей переменной среды нам нужен знак восклицания, а не макро.

Теперь посмотрим, как на самом деле используют переменные среды. Вот пример:

$ echo $myvar
Это моя переменная среды!

Помещая перед именем нашей переменной среды $, мы можем заставить bash заменить ее значением myvar. В терминологии bash это называется "раскрытием переменной". Но, что если мы попробуем следующее:

$ echo foo$myvarbar
foo

Мы хотели, чтобы вывелось "fooЭто моя переменная среды!bar", но это не работает. Что неправильно? В двух словах, возможности bash раскрытия переменных сбиты с толку. Нельзя сказать, хотим ли мы раскрыть переменную $m, $my, $myvar и т. д. Как мы можем более явно и ясно сказать bash, какую переменную мы имеем в виду? Попробуйте так:

$ echo foo${myvar}bar
fooЭто моя переменная среды!bar

Как вы видите, мы можем заключить имя переменной среды в фигурные скобки, если оно отделяется от окружающего текста не очевидным образом. Хотя $myvar быстрее набрать и это будет работать в большинстве случаев, ${myvar} будет правильно обработано почти во всех ситуациях. В остальном, оба варианта делают одно и то же, и в оставшейся части серии вы встретитесь и с тем, и с другим. Вам необходимо вспомнить об использовании более явной формы с фигурными скобками, если ваша переменная среды не отделена от окружающего текста белыми пробелами (пробелами или табуляцией).

Вспомним, что мы упоминали, что мы можем "экспортировать" переменные. Когда мы экспортируем переменную среды, она автоматически становится доступной в среде любого запущенного позже скрипта или программы. Скрипты оболочки могут "добраться" до переменной среды с помощью встроенной в оболочку поддержки переменных среды, в то время как программы C могут использовать вызов функции getenv(). Вот некоторый пример кода на C, который вам следует набрать и скомпилировать, это поможет нам понять переменные среды с точки зрения C:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
  char *myenvvar=getenv("EDITOR");
  printf("Переменная среды EDITOR установлена в %s\n",myenvvar);
}

Сохраните это исходный текст в файл с именем myenv.c, а затем скомпилируйте его, выдав команду:

$ gcc myenv.c -o myenv

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

$ ./myenv
Переменная среды EDITOR установлена в (null)

Xmm... потому что переменная среды 'EDITOR' не была установлена, программа C получила нулевую строку. Давайте попробуем задать ей какое-нибудь специальное значение:

$ EDITOR=xemacs
$ ./myenv
Переменная среды EDITOR установлена в (null)

Хотя мы могли бы ожидать, что myenv напечатает значение "xemacs", она совсем не работает, потому что мы не экспортировали переменную среды 'EDITOR'. На этот раз мы заставим ее работать:

$ export EDITOR
$ ./myenv
Переменная среды EDITOR установлена в xemacs

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

$ export EDITOR=xemacs

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

$ unset EDITOR
$ ./myenv
Переменная среды EDITOR установлена в (null)

Обзор обрезания строк

Обрезание строк, то есть, разделение исходной строки на малые, отдельные куски - из тех задач, которые ежедневно выполняются вашим средним скриптом оболочки. Часто скриптам оболочки требуется взять полностью указанный путь и найти завершающий файл или директорию. Именно это возможно (и забавно!) закодировать в bash, стандартный исполнимый файл UNIX 'basename' выполнит это очень хорошо:

$ basename /usr/local/share/doc/foo/foo.txt
foo.txt
$ basename /usr/home/drobbins
drobbins

'basename' очень удобное средство для обрезания строк. Его спутник с именем 'dirname' возвращает часть пути, которую отбрасывает 'basename':

$ dirname /usr/local/share/doc/foo/foo.txt
/usr/local/share/doc/foo
$ dirname /usr/home/drobbins/
/usr/home

Замечание:

И 'dirname', и 'basename' не занимаются поиском каких-либо файлов или директорий на диске, это просто команды работы со строками.

Подстановка команд

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

$ MYDIR=$(dirname /usr/local/share/doc/foo/foo.txt)
$ echo $MYDIR
/usr/local/share/doc/foo

То, что мы сделали, называется подстановкой команды. В этом примере стоит сделать несколько замечаний. В первой строке мы просто заключили команду, которую мы хотим выполнить в $( ).

Заметим, что то же самое можно сделать с помощью обратных кавычек, на клавиатуре соответствующая клавиша обычно находится над клавишей табуляции:

$ MYDIR=`dirname /usr/local/share/doc/foo/foo.txt`
$ echo $MYDIR
/usr/local/share/doc/foo

Как вы можете видеть, bash дает несколько способов выполнить в точности одно и то же. Используя подстановку команды, мы можем поместить любую команду или конвейер команд между ` ` или $( ) и присвоить результат выполнения переменной среды. Удобное средство! Вот пример использования конвейера с подстановкой команды:

$ MYFILES=$(ls /etc | grep pa)
$ echo $MYFILES
pam.d passwd

Стоит также отметить, что в скриптах оболочки $( ) обычно предпочтительнее ` `, поскольку более универсально поддерживается различными оболочками, легче набирать и читать и менее сложно в использовании во вложенных формах, например:

$ MYFILES=$(ls $(dirname foo/bar/oni))

Обрезание строк профессионально

Хотя 'basename' и 'dirname' - замечательные инструменты, временами нам нужно выполнить более продвинутые операции "обрезания" строк, чем просто стандартные действия с путевым именем. Когда нам нужна большая эффективность, мы можем воспользоваться продвинутой функциональностью раскрытия переменных, встроенной в bash, которая выглядит следующим образом: ${MYVAR}. Но bash сам может также выполнить некоторые удобные обрезания строк. Взгляните на эти примеры:

$ MYVAR=foodforthought.jpg
$ echo ${MYVAR##*fo}
rthought.jpg
$ echo ${MYVAR#*fo}
odforthought.jpg

В первом примере мы набрали ${MYVAR##*fo}. Что это в точности означает? По существу внутри ${ }, мы набирали имя переменной среды, два ##, и образец ("*fo"). Затем bash взял 'MYVAR', нашел самую длинную подстроку, начинающуюся со строки "foodforthought.jpg", которая соответствует образцу "*fo", и обрезал начало строки. Это немного сложно осознать сразу, и чтобы почувствовать, как работает эта специальная опция "##", давайте пройдем по шагам и посмотрим, как bash совершает это раскрытие. Во-первых, он начал искать подстроки в начале "foodforthought.jpg", что соответствует образцу "*fo". Вот проверяемые им подстроки:

f       
fo              MATCHES *fo
foo     
food
foodf           
foodfo          MATCHES *fo
foodfor
foodfort        
foodforth
foodfortho      
foodforthou
foodforthoug
foodforthought
foodforthought.j
foodforthought.jp
foodforthought.jpg

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

Вторая форма раскрытия переменной, показанная выше, кажется идентичной первой за исключением того, что использует только один "#", и bash выполняет почти идентичные действия. Он проверяет то же множество подстрок, как это делалось и в первом примере, за исключением того, что bash удаляет из исходной строки самое короткое совпадение и возвращает результат. Итак, как только он обнаруживает подстроку "fo", он удаляет "fo" из нашей строки и возвращает "odforthought.jpg".

Это может выглядеть очень загадочно, поэтому я покажу вам легкий способ запомнить такую функциональность. При поиске длинного совпадения используйте ## (поскольку ## длиннее, чем #). При поиске короткого совпадения используйте #. Подумайте, совсем не трудно запомнить! Постойте, как вы запомните, что подразумеваем использовать символ '#' для удаления с *начала* строки? Просто! Заметьте, что на клавиатуре shift-4 это "$", это символ bash для раскрытия переменной. На клавиатуре непосредственно слева от "$" - "#". Итак, вы моете видеть, что "#" находится "в начале" от "$", и, таким образом (согласно нашей мнемонике), "#" удаляет символы с начала строки. Вы можете поинтересоваться, как удалить символы с конца строки. Если вы предположите, что мы используем символ клавиатуры непосредственно справа от "$" ("%"), вы правы! Вот несколько живых примеров, как обрезать хвостовые части строк:

$ MYFOO="chickensoup.tar.gz"
$ echo ${MYFOO%%.*}
chickensoup
$ echo ${MYFOO%.*}
chickensoup.tar

Как вы можете видеть, опции раскрытия переменных % и %% работают идентично # и ##, только они удаляют совпадающий образец с конца строки. Заметим, что вы не должны использовать символ "*", если вы хотите удалить указанную строку с конца:

MYFOOD="chickensoup"
$ echo ${MYFOOD%%soup}
chicken

В этом примере не имеет значения, используем ли мы "%%" или "%", так как возможно только одно совпадение. И помните, если вы забудете, что нужно использовать, "#" или "%", посмотрите на клавиши 3, 4 и 5 вашей клавиатуры и вычислите их.

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

$ EXCLAIM=cowabunga
$ echo ${EXCLAIM:0:3}
cow
$ echo ${EXCLAIM:3:7}
abunga

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

Применение обрезания строк

Теперь, когда мы узнали все об обрезании строк, давайте напишем маленький простой скрипт. Наш скрипт будет принимать как аргумент один файл и печатать, является ли это файл архивом tar. Чтобы определить, tar-архив это или нет, скрипт ищет образец ".tar" в конце файла. Вот он:

#!/bin/bash

if [ "${1##*.}" = "tar" ]
then
       echo По-видимому, это архив tar.
else
       echo На первый взгляд это не кажется архивом tar.
fi

Чтобы запустить это скрипт, введите его в файл с именем mytar.sh, и наберите 'chmod 755 mytar.sh', чтобы сделать его исполнимым. Затем попробуйте запустить его на архив tar, например:

$ ./mytar.sh thisfile.tar
По-видимому, это архив tar.
$ ./mytar.sh thatfile.gz
На первый взгляд это не кажется архивом tar.

Хорошо, он работает, но он не очень функционален. Прежде, чем мы сделаем его более полезным, давайте посмотрим на используемое в нем предложение "if". В нем у нас есть булевское выражение. В bash оператор сравнения "=" проверяет равенство строк. В bash все булевские выражения заключаются в квадратные скобки. Но что на самом деле проверяет булевское выражение? Посмотрим на левую часть. В соответствии с тем, что мы узнали об обрезании строк, "${1##*.}" удалит большее совпадение "*." с начала строки, содержащейся в переменной среды "1", и вернет результат. Это будет причиной того, что будет возвращено все после последнего "." в файле. Очевидно, если файл заканчивается ".tar", мы в результате получим "tar", и условие будет истинным.

Вы можете поинтересоваться, что за переменная среды "1" на первом месте. Очень просто, $1 в скрипте -- это первый аргумент командной строки, $2 -- второй и т.д. Хорошо, теперь когда мы рассмотрели функцию, мы может бросить наш первый взгляд на предложение "if".

Предложение if

Как и большинство языков, bash имеет свою собственную форму условного предложения. При использовании его придерживайтесь использованного выше формата; то есть, набирайте "if" и "then" на отдельных строках, выравнивайте с ними по горизонтали "else" и завершающий "fi". Это делает код легче читаемым и отлаживаемым. В добавок к форме "if,else" есть несколько других форм предложения "if":

if      [ условие ]
then
        действие
fi

Это форма выполняет действие только, если условие истинно, в противном случае никакого действия не выполняется и продолжается выполнение со строки после "fi".

if [ условие ]
then 
        действие
elif [ условие2 ]
then
        действие2
.
.
.
elif [ условие3 ]
then

else
        действиеX
fi

Форма "elif" последовательно проверяет каждое условие и выполняет действие, соответствующее первому истинному условию. Если истинных условий нет, он выполнит действие, соответствующее "else", если оно есть, а затем продолжит со строки, следующей за всем предложением "if,elif,else".

Что дальше

Теперь, когда мы охватили большую часть функциональности bash, время ускорить шаги и подготовиться к написанию настоящих скриптов. А следующей статье я дам конструкции цикла, функции, пространства имен и другие необходимые темы. После этого мы будет готовы написать некоторые более сложные скрипты. В третьей статье мы сфокусируемся почти исключительно н очень сложных скриптах и функциях, а также на нескольких опциях разработки скриптов. До встречи!

Ресурсы

Полезные ссылки


Daniel Robbins

silly photo
This is not really me

Daniel Robbins is the founder of the Gentoo community and creator of the Gentoo Linux operating system. Daniel resides in New Mexico with his wife Mary and two energetic daughters, and is founder and lead of Funtoo. Daniel has also written many technical articles for IBM developerWorks, Intel Developer Services and C/C++ Users Journal.