Инструменты пользователя

Инструменты сайта


igor:practicum_po_napisaniju_shell_scripts

Это старая версия документа!


Все мои статьи: Статьи Игоря Романова

Практикум по написанию сценариев оболочки

Понятие оболочки и понятие сценария

Обычный для юниксоидных систем режим работы - терминальный (консольный, диалоговый): пользователь вводит команду, ждет от системы ее выполнения и просматривает результаты, а затем дает следующую. Однако в жизни часты ситуации, когда некоторая последовательность команд заранее известна, так что нет смысла вводить команды каждый раз заново - их записывают в файл, и получается сценарий, или скрипт (англ. script).
По большому счету - это программа. Но программа особая, которая, в отличие от обычной программы, не может выполняться сама по себе, а требует наличия другой программы (интерпретатора). Интерпретатор читает сценарий строка за строкой и выполняет записанные в них команды. То есть режим работы интерпретатора со сценарием отличается от диалогового режима тем же, чем отличается пулемет от винтовки: физический процесс один и тот же, но в винтовке одно нажатие на курок производит один выстрел, а в пулемете - целую очередь, при условии, конечно, что патроны заряжены как следует.
Оболочка (англ. shell) - программа в составе операционной системы, принимающая команды пользователя и передающая их другим программам для выполнения требуемых действий. Название «оболочка» отражает тот факт, что пользователь имеет дело только с ней, а все другие части ОС скрыты под оболочкой. В современном компьютерном мире известно множество типов программ, которые играют или могут играть роль оболочки. То, что мы только что описали, - диалоговая оболочка. Многие используют графические и псевдографические оболочки (пример псевдографической - Midnight Commander). Однако такие оболочки, как правило, неспособны работать со сценариями. С другой стороны, существуют программы, не являющиеся оболочками, но способные «переваривать» сценарии. Пример - sed. Это достаточно интересная, хотя теперь почти забытая программа. Надеюсь, когда-нибудь я напишу о ней отдельную статью. Но сегодня предлагаю ограничиться диалоговыми оболочками.

История с предысторией

В юниксах оболочка была одна-единственная, и называлась просто sh, потому что другого ничего как будто и не было нужно. Впоследствии разные люди написали множество аналогичных программ, отличающихся в основном двумя чертами: во-первых, функциями редактирования ранее поданных команд с целью использовать их вновь (это нужно при работе в диалоговом режиме), и во-вторых, синтаксисом языка скриптов. Что имеем в результате? Если в вашей системе имеется синаптик или другой аналогичный пакетный менеджер, группирующий программы по сферам применения, то вы увидите целый отдельный каталог с диалоговыми оболочками - их там порядка 20! Наиболее известны оболочки Борна - Bourne shell (bsh) и Bourne again shell (bash), Альмквиста (ash), Корна (Korn shell - ksh), zsh и csh. Про последнюю говорят, что ее командный язык максимально приближен к языку приграммирования Си, что на самом деле далеко от действительности. Если уж говорить о языке Си, то наиболее приближен к нему язык оболочки sh, той самой, что была и остается стандартом для наших систем. По традиции, названия типа shell или *sh даются именно диалоговым оболочкам.
В современном линуксе наиболее употребительна оболочка bash, но употребительна для чего? - для диалогового режима. Посмотрите ваш каталог /etc и его подкаталоги - там сотни служебных скриптов, обеспечивающих правильную работу системы, и они-то как раз написаны под sh.
Итак, с учетом сказанного, что такое сценарий оболочки? - Это программа, выполняемая с помощью интерпретатора, в роли которого выступает sh или нечто другое sh-подобное.

Для чего нужно овладевать технологией написания сценариев?

Преимущества, которые получает человек, владеющий технологией написания сценариев, по сути дела, те же, которые дает «настоящее» программирование: возможность выполнять нужные конкретно нам действия, которые разработчик ОС не догадался для нас запрограммировать.
Преимущества скриптов перед «настоящими» программами. Настоящая программа пишется на некотором исходном языке (в наших системах наиболее употребительны языки семейства Си), после чего она требует компиляции и сборки («линковки»), а это может быть довольно-таки хлопотно и долго, и для этого требуется соответствующее программное обеспечение: компилятор, построитель задач (линкер, линковщик), куча библиотек и утилит… И все это надо знать.
Готовую программу, если нет исходного кода, невозможно изменить - только писать заново с чистого листа. Скрипт же представляет собой текстовый файл (по виду и по сути близкий к исходному тексту программы), который легко может быть прочитан и понят человеком. Ну а если можно прочитать, то отчего бы не попробовать написать? То есть если скрипт, написанный когда-то давно (и возможно не нами), перестает нас удовлетворять, нетрудно будет разобраться, что там к чему, и что-то переписать, и после этого он сразу готов к работе - от хлопот с компиляцией и линкованием мы свободны, и никакого специального ПО не нужно (а оболочка у нас по-любому имеется).
Короче, освоить технологию написания скриптов несколько проще, чем настоящее программирование.
А недостатки? - А наши недостатки, как известно, - продолжение наших достоинств. В наше время весь компьютерный мир наводнен тысячами полуграмотных сисадминов-эникейщиков, которые научились нажимать мышкой на кнопку «Продолжить» и теперь считают себя крутыми IT-специалистами. Доверь им написать скрипт - они вам такого понапишут - мало не покажется. А чтобы в такие ситуации не попадать, лучше освоить скриптизацию самостоятльно.
Если вы ничего не знаете о сценариях оболочки, то для начала рекомендую заглянуть вот сюда:
https://losst.ru/napisanie-skriptov-na-bash
Статья полезная, но минус в том, что не каждый читатель в состоянии понять, для чего по жизни нужны эти «Hello world». Поэтому у меня возникла идея дополнить ее, приблизить читателя «к земле». В результате получился небольшой практикум, который я и предлагаю вашему вниманию сегодня.

Эксперимент N1: цикл for, или цикл с параметром

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

Постановка задачи.

У вас, конечно, есть в домашнем каталоге каталог с музыкой, или вообще с аудиозаписями. В нем, вероятно, имеется множество файлов с «говорящими» названиями. Вы придерживаетесь какого-то порядка при присвоения имен аудиофайлам? У меня есть целая система, которая постороннему человеку покажется совершенно невменяемой. Однако человек не посторонний, а хоть немного меня знающий скажет, что у Романыча ничего с бухты-барахты не возникает: если система есть, значит она чем-то мотивирована. Если говорить о моих аудиозаписях, то для их воспроизведения я использую (или использовал раньше) различные устройства, которые далеко не единообразно воспринимают (не воспринимают) как имена файлов, так и id3-теги, находящиеся внутри файлов. Значит, нужно давать файлам имена не какие попало, а такие, которые позволят избежать путаницы. И вот теперь у меня появилось несколько каталогов, в каждом из которых несколько файлов с именами типа TrackNo01, TrackNo02, TrackNo03 и т. д. В нынешних ОС ситуация вполне легальная, но в своем доме я такого не допускаю: во-первых, неизбежна путаница, во-вторых, одно случайное нажатие F5 в полуночном командере - и файлы из некоторого каталога «затрут» одноименные файлы в другом каталоге (без малейшего шанса на восстановление!). Стало быть, надо все файлы переименовать в соответствии с моей системой: в начало имени каждого файла вставить две буквы, кодирующие исполнителя (или инструмент, если исполнитель неизвестен или разный в пределах каталога). Для всех файлов в том или ином каталоге эти две буквы будут одинаковы, например оркестр Г. фон Караяна - KA, испанская гитара - IG и т. д. Отсюда идея: не переименовывать файлы вручную, а написать скрипт. Используем цикл с параметром - цикл for:

#!/bin/bash
for fname in `ls *.mp3`
do
echo cp $fname IG-$fname
done


Вот такой простенький скрипт, который даже можно не записывать в файл.

Рассмотрим подробно, что здесь написано.

Сочетание символов #! в первой строке скрипта (оно имеет даже собственное название - шебанг) говорит о том, что непосредственно за ним следует полное имя программы-интерпретатора. Шебанг должен располагаться в самом начале файла.
Следующая строка - цикл for, или цикл с параметром. Параметром является переменная fname, которой мы присваиваем последовательные значения из списка, а в качестве списка используем «выхлоп» команды ls *.mp3. С таким же успехом можно написать:
for fname in $(ls *.mp3)
или просто
for fname in `*.mp3`
Объясняю «механику» этих команд. Выше я сравнил баш с пулеметом, выстреливающим патроны очередями. На первых порах такая степень приблизительности нас устраивала. Пришла пора увидеть, что на самом деле все несколько сложнее.
Прочитав очередную команду из скрипта, баш сначала смотрит, нет ли в ней чего-то такого, что следовало бы как-то преобразовать. Если не находит, то использует строку как она есть, а если находит, то выполняет нужные преобразования и только потом производит «выстрел». Конкретно в нашей команде есть нечто, заключенное в обратные кавычки. Баш рассматривает это нечто как команду, которую нужно выполнить, ловит ее стандартный вывод и подставляет его вместо обратных кавычек. Увидев знак $ и скобку за ним, он рассматривает все, что в скобках, аналогичным образом. (зачем такие два варианта одного и того же действия? - Дань традиции, вероятно). Ну и третий вариант: снова обратные кавычки, но команды внутри их нет, а есть нечто, содержащее звездочку - он воспринимает все это как шаблон поиска файлов и «достраивает» этот шаблон до получения конкретных имен файлов. Короче, во всех трех рассмотренных ситуациях для производства «выстрела» будет использован «патрон» вот с таким «зарядом»:
for fname in TrackNo01 TrackNo02 TrackNo03
После строки for стоит служебное слово do, открывающее цепочку команд, которые будут выполняться на каждом шаге цикла. Замыкает цепочку служебное слово done.
Теперь обратите внимание на echo в предпоследней строке. Написанный так скрипт только печатает на экране команды, которые он якобы выполняет, но на самом деле никаких действий не производит, а значит и испортить ничего не может. Это отладочный режим работы скрипта. Выражаясь языком заводских инженеров, программирующих станки с ЧПУ, - сухой прогон (англ. dry run). На станке режим сухого прогона включается специальной кнопкой. У нас такой кнопки нет, так что если команды, которые мы видим на экране, нас устраивают, нам нужно подредактировать скрипт - убрать echo, и тогда уже запускаем скрипт в боевом режиме.
Обратите внимание на то, как мы задаем и используем переменную. Во-первых, имя переменной в языках командных оболочек может содержать буквы и цифры (начинаться с буквы) и, возможно, некоторые другие символы… какие именно? - вероятно, в разных оболочках требования разные, я не особенно вникал - лично мне вполне достаточно букв. Это нюансы, я же сейчас говорю о принципе, а он одинаков как в языках всех командных оболочек, так и в языках «настоящего» программирования. Во-вторых, мы сначала присваиваем переменной значение (с помощью знака равенства), а затем используем ее в каких-либо операциях, предваряя имя символом $. Все это относится к обычным переменным - внутренним (локальным) для нашего скрипта. В дальнейшем мы познакомимся со специфическими переменными, которые и выглядят иначе, и обращаться с ними следует по-другому, ну а пока вот так… В-третьих, в языках оболочек нет разделения переменных на типы: целые, вещественные и т. п. Здесь все переменные - символьные (строковые).

Теперь усложним задачу.

Все как бы то же самое, но мы хотим избавиться полностью от символов TrackNo в именах файлов, т. е. чтобы имя каждого файла включало только буквы исполнителя и цифры номера дорожки. Оговоримся еще раз, что номера дорожек идут последовательно от 01 до, допустим, 13. Для решения этой задачи снова используем цикл for, но в несколько ином виде. Раньше у нас был цикл с параметром по списку, теперь напишем цикл с параметром в интервале, т. е. переменная num будет получать последовательные числовые значения от начального до конечного, а уже из этого параметра мы будем строить для каждого файла «старое» имя - oldfname и «новое» - newfname:

#!/bin/bash
for num in {01..13}
do
oldfname=TrackNo$num.mp3
newfname=IG-$num.mp3
echo cp $oldfname $newfname
done


Обратите внимание на то, как задается интервал значений параметра: {01..13}. Это «башизм», т. е. написание, характерное для bash и не поддерживаемое в других оболочках (по крайней мере, во многих других). Это свойство баша делает его привлекательным для меня и до недавнего времени не позволяло мне заменить баш чем-то другим. Но теперь я узнал, что в других оболочках (да и в баше тоже) для этой цели можно использовать утилиту seq.
Еще о циклах for читайте вот здесь: https://losst.ru/tsikly-bash

Эксперимент N2: условный оператор

У вас, конечно, есть в домашнем каталоге каталог с фотографиями?
Обычно фотоаппарат, записывая снимки на карту памяти, присваивает им имена типа IMGP0238.JPG, DSC00238.JPG или еще хуже: Фото0238.JPG. Найти нужный снимок среди файлов с такими именами - непростая задача. А прикиньте, если вы часть снимков сделали на настоящий фотоаппарат, часть - на телефон, что-то скачали с интернета… Часть снимков, на которых вы изображены, сделали ваши друзья на свои аппараты/телефоны/видеокамеры… В общем, жуткая путаница неизбежна. Стало быть, опять-таки нужна некоторая система. Поступим так: имя снимка будет включать дату в формате ГГГГ-ММ-ДД и далее четыре цифры номера снимка, присвоенные съемочным устройством.
– Снова будем заниматься переименованием файлов, да?
– Purquois pas? («Почемы бы и нет?») Наведение порядка в файловом хозяйстве - работа, с которой приходится сталкиваться. И чем больше хозяйство, тем чаще, и объем работы больше. Во времена моей досовской молодости эта работа была весьма хлопотной, и таковой остается сейчас для тех, кто живет в Windows. У нас ситуация другая: линукс - система, в которой автоматизировать и скриптизировать можно буквально все… А значит, давай, мой мальчик, дави на газ!
Скрипт, который мы здесь рассматриваем, - пример из/для школьного учебника. Если в будущем мы захотим довести его до профессионально-индустриального уровня, нам придется изучить утилиту exiftool и еще кое-что, вероятно не обойдется без вышеупомянутого sed… Но это чуть позже, а сейчас сосредоточимся на простом условном операторе.

Постановка задачи

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

#!/bin/bash
for num in {0037..0081}
do
oldfname=DSC0$num.JPG
newfname=2010-08-23-$num.jpg
if test -e $oldfname
then
echo cp $oldfname $newfname
else
echo Не копируем $oldfname - такого файла нет
fi
done


Условный оператор включает: проверку условия - if, непосредственно за которой идет подлежащее проверке условие, команды для выполнения при истинности условия (начинаются служебным словом then), команды для выполнения при ложности условия (начинаются словом else) и замыкающее служебное слово - fi.

Лирическое отступление

Если честно, то от этого fi разит такой средневековой замшелостью, которая даже у меня - программиста закалки 1980-х годов - поначалу вызывала неприятие. Сразу вспоминаю наших профессоров, которые нам плели сказочки про какой-то псевдокод, а что за псевдокод и для чего - они сами не знали… Потом, много лет спустя, мой компьютерный гуру высказал более-менее внятную (однако же спорную) гипотезу, для чего нужен псевдокод. Поздняк было рыпаться: много лет, даже на ассемблере, я писал текст программы сразу как надо, без всяких схем алгоритмов и уж конечно без всякого псевдокода…
Не знаю, может быть я зря впутываю вас в дебри, но все-таки скажу. Это fi знаменует собой безблоковый принцип построения языка, который если где и применялся, то только в псевдокоде, а в настоящих языках программирования - никогда. В классических языках типа паскаля и Си строго соблюдался блоковый принцип, известный с доисторических времен, с эпохи алгола и ПЛ-1.
Так что если вам кто-то будет говорить, что язык баша похож на Си…
Предвижу вопрос вдумчивого читателя: если блоковый принцип такой правильный, почему же в языках командных оболочек используется безблоковый?
Я вам рассказывал про винтовку и пулемет? Рассказывал применительно к командным оболочкам. Винтовки и пулеметы хороши в локальной контртеррористической операции, но серьезную войну ими не выиграть - для серьезной войны нужна тяжелая артиллерия. Наша артиллерия - компиляционные системы разработки программ. Их языки - это не винтовки и не пулеметы - это шрапнель: все боеприпасы собираются в единое изделие и выстреливаются одномоментно. Чтобы стрельба была эффективна, изделие приходится усложнять, а значит всерьез думать об обеспечении технологичности его производства. Блоковый принцип избавляет от массы гемора при разработке сложных программ. В середине 20-го века, когда появился алгол, люди такими тонкостями отнюдь не пренебрегали. Алгола уже давно нет, но весь наш нынешний юниксоидно-линуксоидный мир (да и виндоусоидный тоже) вырос из него.
Однако сегодня у нас другие приоритеты. Скрипты пишутся для сравнительно несложных задач. Грубо говоря, с объемом текста в пределах пары страниц и с трудоемкостью в пределах одного человеко-дня. Для решения таких задач нам нужен автомат Калашникова: легкий в освоении, надежный и неприхотливый в эксплуатации. Он как бы и винтовка, как бы и пулемет, и не совсем винтовка, и не совсем пулемет… К нему предъявляются противоречивые требования: он должен с одинаковой эффективностью стрелять как одиночными, так и очередями. И вот тут оказывается, что безблоковый язык подходит для этой цели лучше, чем блоковый.

Однако давайте ближе к делу

После if может идти, вообще говоря, любая команда. Фокус в том, что в юниксоидных системах каждая программа при завершении выдает т. н. exit code: при благополучном завершении - 0, при неблагополучном - какое-то другое число. На этом мы и можем сыграть. Однако по жизни в качестве программы - «проверочника» чаще других используется команда test, в данном примере с ключом -e (exist - существует ли файл). Она, если проверяемое условие истинно, выдает exit code = 0.
Рассмотренная форма записи условного оператора - каноническая, она издавна используется во всех юниксоидных системах. В наше время bash и многие другие оболочки предлагают более наглядную форму записи того же самого оператора:
if [ -e $oldfname ]
then
……

Здесь у нас проверяемое условие одноместное (унарное), поэтому ставим одну пару квадратных скобок. В баше, если бы условие было двухместное (бинарное, например сравнение двух переменных), потребовались бы двойные квадратные скобки, а в других оболочках может быть по-разному. В любом случае мы должны понять, что квадратные скобки просто представляют команду test, и ничего более.
Возможен и еще один вариант записи условного оператора. Правда, этот вариант будет пригоден только, если мы при истинности условия хотим выполнить единственную команду, а при ложности условия ничего вообще не делать (а в самом деле, так ли сильно нам нужно сообщение «Не копируем… файла нет»? На первых порах оно нам полезно, но по мере «взросления» мы сможем от него отказаться). В общем, запишем так:
[ -e $oldfname ] && cp $oldfname $newfname
Здесь условного оператора как такового нет, а используется имеющаяся в наших ОС фича: можно запускать некоторую команду (в данном примере cp) после благополучного завершения некоторой другой команды (в данном примере test, изображаемая квадратными скобками). Знаки && как раз и обеспечивают такую «запряжку цугом».
А как записать, если некоторую команду нужно выполнить при ложности условия?
Да почти так же, только вместо && написать ||.
Кстати, комбинируя эти знаки, можно писать условные операторы с объединением нескольких условий по И и по ИЛИ. Здесь мы сталкиваемся с хитрым свойством оболочек, которое отсутствует в «настоящем» программировании: любая команда может сочетать выполнение операций и проверку условий, тогда как в «настоящем» программировании эти функции разделены.

Эксперимент N3: параметры командной строки

Что мне не нравится в рассмотренном только что скрипте?
Этот скрипт обрабатывает группу снимков, сделанных в заданный день, и больше он ни на что не годится. Чтобы обработать снимки, сделанные на следующий день, нам придется открывать скрипт редактором, переписывать значения начала и конца цикла и дату, записывать скрипт на место… и все это ради того, чтобы произвести единственную «пулеметную очередь». А дальше лыко и мочало: начинай сказку с начала.
Хотелось бы написать скрипт так, чтобы его можно было использовать многократно без редактирования, а нужные параметры задавать при каждом запуске. Инструменты для этого в нашем распоряжении есть. Для начала рассмотрим один из них: командную строку с параметрами.
Если, допустим, наш скрипт называется renamefoto, то до нынешнего момента мы его запускали командой, содержащей только это название. Теперь мы хотим его запускать командной строкой со всеми нужными параметрами, например вот так:

igor@bigwhite $ renamefoto 0082 0095 2010-08-24

Для этого перепишем скрипт:

#!/bin/bash
for num in {$1..$2}
do
oldfname=DSC0$num.JPG
newfname=$3-$num.jpg
[ -e $oldfname ] && echo cp $oldfname $newfname
done


Вывше я вам рассказывал про переменные, имена которых начинаются с буквы, а предваряются знаком $. Здесь у нас знак $, за которым, вместо ожидаемого имени переменной, видим просто 1 - на имя переменной никак не похоже. И главное, мы сразу используем это имя в выражении, не присвоив переменной никакого значения - это как вообще??? Выше я говорил о существовании специфических переменных - и вот теперь мы одну из них встречаем вживую. Это, собственно, и есть параметр командной строки, а всего в этом скрипте их три.

Эксперимент N4: переменные окружения

На написание этого скрипта меня вдохновил пример MacOS. Там, если к компьютеру подключаешь фотоаппарат (или флэшку с фотографиями), фотографии с него автоматически перекачиваются в компьютер. Я не разобрался, является ли эта фича свойством MacOS или это чья-то самодельная хитрушка, но понял: я, Вань, такую же хочу, но с перламутровыми пуговицами…
Я, однако, не стал слепо копировать все, что видел в MacOS. В частности, мне совсем ни к чему перекачивать в компьютер все фотки, найденные на любом носителе: мне надо это делать только, если это флэшка именно с фотоаппарата. Фотоаппарат, как мы знаем, записывает снимки в определенный каталог на карте памяти (обычно подкаталог каталога DCIM). Я хочу те снимки, которые успешно перекачаны в компьютер (в каталог /home/igor/foto-today-camera), сохранить на карте памяти, но переместить в находящийся на ней же каталог-дублер (THISYEAR, который я по окончании года переименую, например, в 2016), чтобы в следующий раз знать, какие снимки новые, а какие уже есть в компьютере.
Поскольку данная операция должна выполняться автоматически при подсоединении внешнего накопителя («диска», флэшки) к компьютеру, используем udev, и скрипт для копирования фотографий будем запускать из-под udev. Начало скрипта у меня уже было написано (см. статью Udev и его применение для монтирования файловой системы ). Сегодня я хочу заострить внимание на том, какие переменные я здесь использую и как.

#!/bin/bash
# 1). Параметр командной строки - название типа карты/флэшки.
# Если не указан, используем card.
if [ $1 ]
then
secretword=$1
else
secretword=card
fi
# 2). Если есть метка тома ID_FS_LABEL, используем ее в имени каталога - точки монтирования,
# а если нет - используем ID_FS_UUID, который есть обязательно
if [ $ID_FS_LABEL ]
then
dirname=$secretword-$ID_FS_LABEL
else
dirname=$secretword-$ID_FS_UUID
fi
# 3). Готовим имя каталога - точки монтирования.
# Существует ли такой каталог? Если нет, создадим его.
mountpointname=/media/$dirname
if [ ! -d $mountpointname ]
then
mkdir $mountpointname
fi
# 4). Монтирование.
mount -o utf8,uid=1000,user $DEVNAME $mountpointname
# 5). Если это карта от фотоаппарата, перекачиваем фотографии и размонтируем носитель.
if [[ $ID_FS_LABEL = SAMSUNG ]]
then
cp $mountpointname/DCIM/162PHOTO/*.JPG /home/igor/foto-today-camera
mv $mountpointname/DCIM/162PHOTO/*.JPG $mountpointname/THISYEAR
umount $DEVNAME
fi


Udev, запуская наш скрипт, дает команду не просто shellmount (так скрипт называется), а что-нибудь типа shellmount camera или shellmount sdcard. Слово, стоящее после имени скрипта, как мы уже знаем, - параметр командной строки - $1. В этом скрипте он единственный.
Что означает запись if [ $1 ]? Она означает просто проверку переменной на существование (переменная существует, если ее значение содержит хотя бы один символ).
А что означает восклицательный знак в операторе if [ ! -d $mountpointname ] в пункте 3? Он означает инверсию, т. е. условие будет считаться выполненным, если каталог с именем mountpointname не существует.
В пункте 2 мы видим переменную ID_FS_LABEL, которой мы также не присваивали никакого значения. Это переменная окружения - ее нам поставляет udev. Точнее мы пишем скрипт с расчетом на то, что он нам ее поставит. Если мы захотим запустить этот скрипт без участия udev, то этой переменной не будет (и ID_FS_UUID тоже), и скрипт будет работать неправильно.
Окружение, или среда (англ. environment), так же как и командная строка с параметрамии, - нструмент влияния на поведение программ, имеющийся во многих ОС (в т. ч. в DOS и Windows). Каждый процесс в такой ОС «работает в своем окружении» (в своей среде). Если «родительский» процесс (в нашем случае udev) порождает «дочерний» процесс (в нашем случае shellmount), то он формирует для дочернего процесса определенное окружение. Окружение нашего скрипта содержит порядка 20 переменных, описывающих носитель данных, с которым наш скрипт должен работать. Из этого набора переменных мы используем ID_FS_LABEL («метку тома», «метку диска»), ID_FS_UUID (уникальный идентификатор файловой системы) и DEVNAME (имя файла-устройства, под которым наш носитель зарегистрирован в каталоге /dev и которое необходимо для осуществления монтирования).

Совет от седого компьютерного волка

Скрипт, который мы здесь рассмотрели, на самом деле представляет собой совсем несложную программу. Не сравнить с АСУ производством, которую я разрабатывал лет 15 назад. Однако на примере этого скрипта я хочу показать вам один прием программирования, который не только придает нашему творению некую простую «красоту», но и, возможно, избавит нас от определенного гемора потом, если мы захотим что-то в этом скрипте переделать. Я стараюсь все операции по присвоению значения некоторой переменной собрать «в кучку» в одном месте текста, а затем использовать эту переменную как можно скорее. Например, в первом пункте мы занимаемся только тем, что присваиваем значение переменной secretword, и это значение окончательно, а используем его во втором пункте и больше нигде. Во втором пункте мы присваиваем значение переменной dirname на основе secretword, и это значение опять-таки окончательно и используется в следующем пункте… Таким образом данные как бы передаются от одной переменной к другой по эстафете:
secretword=sdcard
dirname=sdcard-0873-f2c0
mountpointname=/media/sdcard-0873-f2c0
Разумеется, в реальном программировании такая эстафета возможна далеко не всегда. Но там, где она возможна, я настоятельно рекомендую вам ее соблюдать. Если потом, через много лет, вы захотите что-то изменить, то всегда существует риск что-то испортить, но такой принцип программирования позволяет свести этот риск к минимуму. Конкретно я недавно решил: если носитель имеет «метку тома», то необязательно в имя точки монтирования вставлять название типа носителя (sdcard и т. п.) - метки тома вполне достаточно. И тогда я пункт 2 переписал в следующем виде:

if [ $ID_FS_LABEL ]
then
dirname=$ID_FS_LABEL
else
dirname=$secretword-$ID_FS_UUID
fi


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

Эксперимент N5: условие с многими вариантами - оператор case

Собствнно, скрипт будет тот же самый, только мы перепишем пункт 5 с расчетом на то, что у нас не один фотоаппарат, а несколько.
По самому простому варианту можно использовать уже знакомый нам оператор if, но в баше имеется специальный оператор case, аналогичный тому, что имеется почти в любом языке «настоящего» программирования:

# 5). Если это карта от фотоаппарата, перекачиваем фотографии и размонтируем.
case $ID_FS_LABEL in
OLYMPUS)
cp $mountpointname/DCIM/101OLYMP/*.JPG /home/igor/foto-today-camera
mv $mountpointname/DCIM/101OLYMP/*.JPG $mountpointname/THISYEAR
umount $DEVNAME
;;
SAMSUNG)
cp $mountpointname/DCIM/162PHOTO/*.JPG /home/igor/foto-today-camera
mv $mountpointname/DCIM/162PHOTO/*.JPG $mountpointname/THISYEAR
umount $DEVNAME
;;
esac


Начинающие программисты часто боятся оператора case. С расчетом на них в языках оболочек имеется конструкция if - then - elif - fi, призванная заменить оператор case. Эта конструкция, как и вышеупомянутый fi, не вписывается в философию классических блочных языков. Я этой конструкцией никогда не пользовался и вам не советую. Почему? - По той причине, которой уже слегка касался: программа должна с первых минут писаться так, чтобы потом при локальных «улучшизациях» не только не нарушалась ее работа, но и не терялась ясность и логичность текста. Оператор case позволяет записать проверяемое условие один раз, а затем перечислить все возможные варианты (их ведь может быть не два, а гораздо больше) - стало быть, он соответствует сегодняшним требованиям больше, чем конструкция if - then - elif - fi. А если совсем просто, то оператор case позволяет сделать работу более красиво.
———————-
Игорь Романов

igor/practicum_po_napisaniju_shell_scripts.1518549853.txt.bz2 · Последнее изменение: 2018/02/13 22:24 — igor