Журнал LinuxFormat - перейти на главную

LXF147:tut5

Материал из Linuxformat
Версия от 18:20, 17 июля 2014; 2sash-kan (обсуждение | вклад)

(разн.) ← Предыдущая | Текущая версия (разн.) | Следующая → (разн.)
Перейти к: навигация, поиск

Содержание

Команды: GNU/Linux и смекалка

Тихон Тарнавский рассказывает, как средствами командной строки

облегчить работу с Интернет-ресурсами.


В предыдущих статьях мы работали только с локальными данными. А на дворе давно век Интернета! Давайте теперь решим пару задачек, связанных с сетью. Вполне возможно, у вас есть несколько (или даже много) «любимых» сайтов, которые вы регулярно проверяете на обновления. Многие сайты сейчас предоставляют подписки по RSS или электронной почте, но, к сожалению, не все. Более того, нет таких подписок, как правило, на сайтах или разделах сайтов, которые обновляются не слишком часто – а открывать каждый день одну и ту же неизменную страницу со временем становится скучновато, и мы все легче и легче об этом забываем. Так вспомним же известный принцип: «если ты не хочешь читать вывод программы, заставь это делать другую программу». Ведь для человека разницы между программой и сайтом в этом смысле нет, даже если на деле содержимое сайта не генерируется программой, а пишется вручную другим человеком (что, впрочем, последнее время бывает не так уж часто).

Первый пошел

Следуя еще одному принципу – «делай большое дело малыми частями» – для начала проследим за обновлением одного сайта. Уже когда все будет готово, повторим полученные действия для всех страниц из желаемого списка. Первое, что нам нужно сделать – загрузить нужную страницу. Поскольку эту страницу нужно будет с чем-то сравнивать, лучше всего сохранить ее в файл. Для загрузки страниц из сети воспользуемся программой wget. Нам понадобится всего одна ее опция, которая позволяет задавать имя файла сохраняемой страницы, '-O' [от слова output – вывод]:

wget http://linuxformat.ru/event -O ~/tmp/linuxformat.html

Движемся дальше. Нам нужно сначала сохранить текущую версию страницы, а затем точно таким же способом загружать новую и сравнивать с предыдущей. К тому же запуск такого сравнения лучше всего будет автоматизировать, а не вызывать его каждый раз вручную – ведь о ручном запуске ничуть не сложнее забыть, чем об открытии страницы в браузере. Поэтому лучше будет сразу начать записывать нужные команды в файл. Назовем этот файл site-update и сохраним в уже знакомом нам каталоге ~/bin/.

Нам понадобится каждый раз сравнивать две версии одной и той же страницы. Исходя из этого, предлагаю версию, лежащую с прошлого раза, назвать “имя-сайта.html”, а новую (которая останется лишь до окончания работы скрипта) – “имя-сайта.tmp”. Сокращение “tmp”, напомню на всякий случай, происходит от слова “temporary”, то есть «временный». Поскольку мы хотим одну и ту же последовательность команд запускать и в первый раз, и при повседневной обработке, и, скажем, при добавлении новых страниц в список слежения, лучше всего будет исходить из того, что мы не знаем, есть ли у нас уже ее предыдущая сохраненная версия. Значит, изначально мы в любом случае сохраняем страницу во «временный» файл:

#!/bin/bash
wget http://linuxformat.ru/event -O ~/tmp/linuxformat.tmp

Теперь логично будет первым обработать тот случай, когда старой версии еще нет. Проверить ее наличие можно уже известной нам командой test или [. У нее есть множество различных ключей, позволяющих проверять, чем является (или не является) заданный параметр. Конечно, в понятие «не является (чем-то конкретным)» включается и вариант «не существует в принципе». Большинство опций команды [ (как и многих других команд) – «говорящие». Так, опции f соответствует проверка на обычный файл [file]:

[ -f ~/tmp/linuxformat.html ]

Если такого файла не существует, нам нужно только создать его (точнее, переименовать уже сохраненную в предыдущей строке версию) и завершить работу – поскольку все дальнейшие проверки в этом случае бессмысленны. Можно было бы реализовать это оператором “if... else... fi”. Но тогда ту часть, что мы пишем сейчас, пришлось бы написать в конце (после else), а позже вставлять команды в середину. Это нарушает последовательность решения задачи, и мы так делать не будем. Как из любой безвыходной ситуации, тут существует минимум два выхода. Первый – записать «обратное» условие через оператор отрицания, обозначаемого восклицательным знаком (такое обозначение тоже будет привычно знакомым с азами программирования на некоторых языках):

[ ! -f ~/tmp/linuxformat.html ]

Но так как после переименования файла работа должна завершиться, то совершенно незачем запихивать все последующие команды внутрь “else... fi”. Вместо этого мы можем воспользоваться антиподом уже известного нам оператора '&&' – '||'. Он предписывает выполнять следующую команду только в том случае, если предыдущая завершилась с ошибкой (то есть, в данном случае, заданное условие не выполнилось).

[ -f ~/tmp/linuxformat.html ] || нужные-команды

Однако мы умеем записывать после && или || только одну команду, а сейчас нам нужно несколько – переименование файла и завершение работы. Для этого в командной оболочке применяются также довольно широко распространенные в этих целях фигурные скобки:

{ команда1
 команда2
}

Или, в одну строку:

{ команда1; команда2; }

Обратите внимание: закрывающая скобка пишется обязательно с новой строки – то есть после разделителя (которым вместо перевода строки может выступать точка с запятой).

Перейдем к «внутренностям» скобок. В первую очередь нам нужно переименовать файл. Мы такой команды пока не знаем, но легко можем ее найти с помощью man -k rename [rename – переименовать]:

$ man -k rename
[...]
mv (1) - move (rename) files

Вызывается она в нашем случае более чем просто: “mv старое-имя новое-имя”. А команда завершения работы называется столь очевидно, что ее даже искать не придется – exit [выход]. Итак:

#!/bin/bash
wget linuxformat.ru/event -O ~/tmp/linuxformat.tmp
[ -f ~/tmp/linuxformat.html ] || {
 mv ~/tmp/linuxformat.tmp ~/tmp/linuxformat.html
 exit
}

Теперь у нас наверняка есть две версии страницы, можем переходить к сравнению. Вывод команды man -k compare [compare — сравнивать] может оказаться довольно большим. Чтобы легче было искать, вспоминаем, что сравнивать нам требуется файлы, и добавляем фильтр: man -k compare | grep files. Теперь нетрудно найти нужное:

diff (1) - compare files line by line

т. е. «сравнить файлы построчно». По умолчанию эта команда выводит различия между текстовыми файлами. Нам различия не нужны, а нужно лишь проверить сам факт полного совпадения файлов. Для этого предназначена опция q.

(thumbnail)
Рис. 1. Сравнение файлов командой diff.

Давайте проверим ее на одинаковых и на разных файлах. Но чтобы не создавать файлы специально для этой проверки, воспользуемся еще одной конструкцией подстановки: '<(...)'. Она создает временный псевдофайл в файловой системе устройств /dev/ и подставляет его имя в заданном месте. С опцией q команда diff, как и grep, завершается корректно либо с ошибкой в зависимости от результата, поэтому код ее завершения мы тоже проверим (см. рис. 1). Как видно из этой проверки, «безошибочным» считается случай одинаковых файлов. А вот в случае разных файлов кроме «ошибочного» результата выводится текстовое сообщение, которое нам при автоматической обработке совершенно не нужно. Для таких случаев в Unix-системах существует специальное псевдоустройство в той же файловой системе /dev/: /dev/null. Достаточно перенаправить вывод в него, чтобы тот, образно говоря, исчез. Значит, целиком нужная нам команда будет выглядеть так:

diff -q ~/tmp/linuxformat.html ~/tmp/linuxformat.tmp >/dev/null || что-то делаем

А что будем делать, если содержимое файла изменилось? Видимо, стоит предложить открыть этот сайт в браузере; но и дать возможность отказаться от этого. Значит, здесь подходит известная нам с прошлого раза программа zenity. Например, так:

diff -q ~/tmp/linuxformat.html ~/tmp/linuxformat.tmp >/dev/null || \
 zenity --question --title=”Сайт обновился” \

--text=”Сайт linuxformat.ru/event обновился. Хотите открыть его в браузере?”

При положительном ответе нужно запустить браузер с заданным именем сайта (если браузер уже открыт, сайт, скорее всего, откроется в его новой вкладке или новом окне). Операторы || и && можно составлять в цепочки любой длины. Но в цепочке “команда1 || команда2 && команда3” третья команда выполнится при удачном завершении хотя бы одной из первых двух. Как быть? Вариантов два. Можно вместо “команда2 && команда3” воспользоваться оператором if: “if команда2; then команда3; fi”. А можно заключить последние две команды в фигурные скобки – на этот раз не для их объединения между собой (они и так объединены оператором &&), а для отделения от первой. Последний штрих: если браузер еще не открыт, он запустится как отдельный процесс, и хорошо бы, чтобы скрипт не ждал его закрытия, а завершился сразу. Для этого можно запустить нужную команду в фоновом режиме, добавив после нее одиночный символ &. После чего заменим старую версию файла новой, чтобы в следующий раз сравнивать уже с ней. Итак:

#!/bin/bash
wget http://linuxformat.ru/event -O ~/tmp/linuxformat.tmp
[ -f ~/tmp/linuxformat.html ] || {
 mv ~/tmp/linuxformat.tmp ~/tmp/linuxformat.html
 exit
}
diff -q ~/tmp/linuxformat.html ~/tmp/linuxformat.tmp >/dev/null || \
 zenity --question --title=”Сайт обновился” \
 --text=”Сайт linuxformat.ru/event обновился. Хотите открыть его в браузере?” && \
 firefox http://linuxformat.ru/event &
mv ~/tmp/linuxformat.tmp ~/tmp/linuxformat.html

Огласите, пожалуйста, весь список

Теперь переходим к отслеживанию нескольких сайтов. Во-первых, раз сохраненных страниц будет несколько, лучше не захламлять ими каталог ~/tmp/, а создать свой. Традиционно таким «личным каталогам программ» дают имена, начинающиеся с точки, и размещают их в домашнем каталоге. Так что назовем его ~/.site-update:

mkdir -p ~/.site-update

Команда mkdir – сокращение от “make directory” [создать каталог], а ключ p делает две вещи: во-первых, все родительские каталоги, если их еще нет, тоже будут созданы; а во-вторых, не выводится сообщение об ошибке, если заданный каталог уже есть. Это избавляет нас от дополнительной проверки: мы можем не беспокоиться, что нужный каталог может уже существовать. Но поскольку этот каталог будет и дальше использоваться в скрипте, лучше его сохранить в переменной, а затем эту переменную подставлять. Переменные shell (командной оболочки) назначаются конструкцией имя=значение (обязательно без пробелов вокруг знака равенства), а подстановка значений выглядит как $имя.

dir=~/.site-update
mkdir -p $dir

Список адресов лучше хранить в отдельном файле, а не в самом скрипте – так его удобнее будет редактировать впоследствии, да и обрабатывать тоже. Файл этот резонно держать в том же каталоге, а назвать его можно site-list [список сайтов]. Для повторения всех уже записанных нами действий для каждого сайта из списка воспользуемся уже известным нам циклом for. Но в нем элементы списка нужно указывать прямо в самой команде, а не получать из файла. Для этого совместим еще две знакомые конструкции: подстановку результатов выполнения – $(...) или `...` – и ввод из файла – <.

for site in $(<$dir/site-list); do

Теперь нужно подставить сюда все написанное раньше. Тут возникает вопрос, как быть с именами файлов. Адреса страниц в неизменном виде использовать в качестве имен нельзя, так как в них могут быть слэши, а в самих именах слэши недопустимы, ведь они используются как разделители каталогов. Самое простое решение – заменить слэш другим символом, который вряд ли будет встречаться в адресах страниц на том же месте, что и слэш. Например, знаком плюс. Для столь простой замены даже не нужно прибегать к специальным утилитам – достаточно встроенного функционала bash. Имя файла сейчас хранится в переменной site, а для замены внутри переменных существуют такие конструкции: ${имя/было/будет} и ${имя//было/будет}. Вариант с одиночным первым слэшем заменяет только первое вхождение заданного текста, а со сдвоенным – все. Значит, нам нужен второй вариант. Раз заменять нам нужно тот же слэш, который используется здесь как «управляющий» символ, его придется «экранировать» обратным слэшем. Конструкция получится не слишком удобочитаемая, но главное, что она будет работать. Поскольку сам неизменный адрес нам тоже еще пригодится, сохраним результат в другой переменной, оставив первую нетронутой. А раз сохраняем мы имя файла, то добавим к нему сразу и имя каталога, чтобы потом не писать его каждый раз:

file=$dir/${site//\//+}
(thumbnail)
Рис. 2. Cкрипт, проверяющий, не обновились ли сайты из списка.

Наконец, редактируем все, что у нас уже было, подставляя нужное имя файла, и завершаем цикл. И делаем пару последних штрихов. Теперь при отсутствии одного из файлов нам нужно прервать не весь скрипт, а только один проход цикла, переходя к следующему. Это делается командой continue [продолжить], на которую мы и заменим exit. Второе: мы «заземлили» вывод команды diff, но команда wget тоже выводит сообщения о загрузке файлов. Причем здесь такое же перенаправление не поможет, так как выводит она их не на стандартный вывод (stdout), а в поток стандартных ошибок (stderr). По умолчанию оба потока направлены на управляющий терминал, но управлять ими можно (и нужно) по отдельности – для этого они и разделены. Перенаправление стандартных ошибок делается парой символов 2> (без пробела) вместо >. Но чем добавлять второе перенаправление в /dev/null, мы можем поступить гораздо хитрее. Нам не нужен вообще никакой вывод от этого скрипта, ведь все общение с человеком идет через диалоговое окно zenity.

И тут очень кстати оказывается еще одно полезное свойство bash: вся конструкция for... do... done (да и не только она, а и if... else... fi, и другие аналогичные) в смысле ввода-вывода воспринимается как одна команда. В частности, можно поставить значок перенаправления после done, «заземлив» весь цикл сразу. А кроме того, можно перенаправить в одно и то же место сразу оба выходных потока – стандартного вывода и ошибок – парой символов &> (тоже без пробела). Теперь можем любоваться результатом (рис. 2).

(thumbnail)
Рис. 3. Исходный вид пользовательского файла crontab.

Четко по расписанию

Осталась завершающая часть: запускать полученный скрипт автоматически с заданной периодичностью. Запуском таких периодичных заданий занимается системный демон cron. Да, он умеет запускать не только системные задания, но и пользовательские. Для этого существуют специальные пользовательские файлы crontab [cron table], управлять которыми можно с помощью одноименной программы. Программа это весьма проста в использовании и имеет всего три основных опции: e [edit] – редактировать файл crontab; l [list] – вывести текущее содержимое этого файла; и r [remove] – удалить файл. Справку по синтаксису файла crontab можно получить командой man 5 crontab. Если вы в свое время ознакомились с man-страницей самой команды man, то из секции «смотри также» в man crontab вам должно быть это понятно. Кроме того, при первом запуске crontab -e созданный файл скорее всего будет щедро откомментирован (см. рис. 3).

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

  1. минута
  2. час
  3. день месяца
  4. месяц
  5. день недели

В каждом из полей может стоять либо звездочка, либо число или перечень чисел. Звездочка означает «любой». То есть если поставить звездочки во всех пяти полях, задача будет выполняться ежеминутно без перерывов. Перечень чисел можно задать в явном виде, перечислив через запятую отдельные числа или их диапазоны: например, если в пятом поле написать “1,2,4-7”, то задание будет выполняться во все дни, кроме среды. А можно задать и неявно, в виде “*/число”. Если * означает «каждый» (день, месяц и т. д.), то, скажем, */2 – каждый второй, а */5 – каждый пятый.

Как видите, настраивать периодичность можно очень гибко. Например, так можно запланировать задание на полдень первой субботы каждого месяца: “0 $52 $5-7 * 6”. Или, в качестве шутки: если вы суеверны и боитесь пятницы тринадцатого, можете поставить какое-нибудь зловещее уведомление на полночь таких дней, в каком бы месяце они ни выпали – “0 $5 13 * 5”.

Кстати о полночи. Задания выполняются cron’ом только четко в заданное время. Если компьютер в это время был выключен, задание не выполнится совсем. Домашние компьютеры редко держат включенными круглосуточно и круглый год. Поэтому, если вы найдете удобным назначать компьютеру автоматические задания, которые не захотите пропускать из-за его выключения, можете использовать для этого пакет anacron [anachronistic cron], который запускает и пропущенные задания – при первом включении компьютера после запланированного по расписанию времени. У файлов anacrontab синтаксис еще проще (хотя, казалось бы, куда уж проще?), так что оставляю его вам для самостоятельного изучения.

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

*/10 * * * * ~/bin/site-update

Или – по пятницам после обеда:

15 14 * * 5 ~/bin/site-update
(thumbnail)
Рис. 4. Окончательный вариант скрипта, который можно запускать с помощью cron.

Но тут возникает еще одна задачка. Мы-то при проверке запускали скрипт из «иксов». И в этих же «иксах» у нас отображалось окно zenity и вызывался браузер. А задания cron об «иксах» ничего не знают. Так что и скрипт, запускаемый с помощью этих заданий, знать о них ничего не будет. Значит, нужно ему об этом сказать. Для этого есть стандартный для всех Unix-подобных систем механизм общения между процессами (работающими программами), называемый переменными окружения. Среди этих переменных есть «системные», со специальными зарезервированными именами; такие имена, как правило, пишутся заглавными буквами. Каждая переменная создается неким одним процессом и доступна только его потомкам, то есть процессам, вызванным из него или других его потомков. Нужная нам переменная «принадлежит» X-серверу и хранит его «имя», а называется она DISPLAY. Соответственно, для того, чтобы запустить программу на уже работающем X-сервере, нужно передать ей правильное значение переменной DISPLAY. Посмотреть нужное значение можно из «иксового» терминала привычной командой echo, поскольку переменные окружения и переменные командной оболочки – это по своей сути одно и то же (с одной оговоркой, о которой чуть ниже):

$ echo $DISPLAY
:0.0

Значит, нам нужно задать этой переменной подходящее значение прямо в скрипте. Для этого обычно сначала задаются переменные оболочки, которые затем экспортируются в то самое «окружение» с помощью команды export:

переменная1=значение1
переменная2=значение2
export переменная1 переменная2

Но если переменная одна, экспорт и назначение можно совместить и написать вот так:

export DISPLAY=:0.0

Добавляйте эту строчку в начальную часть скрипта (рис. 4), составляйте список сайтов, создавайте задание cron – и ваш очередной личный помощник готов.

Персональные инструменты
купить
подписаться
Яндекс.Метрика