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

LXF150:tut4

Материал из Linuxformat
Перейти к: навигация, поиск

Содержание

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

Тихон Тарнавский составляет скрипт, а скрипт составляет словарную тетрадку – вот так линуксоиды изучают языки.
Наш эксперт

Тихон Тарнавский работает в Linux-консоли больше восьми лет и точно знает, как сделать эту работу удобной.

Продолжаем начатое в предыдущей статье. Напомню: начали мы «работать со словарем» и успели выделить из словарной статьи интересующую нас информацию.

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

На чем мы остановились?

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

#!/bin/bash
word=”$(xclip -o)”

А в другой переменной сохраним полученный в предыдущей статье текст переводов

text=”$(dict “$word” | tail -n +6 | sed -r 's/^ *[0-9]+[.)]/`/; s/^ *_[IVX]+/`/' |
 tr '\n' ' ' | tr '`' '\n' | sed -r 's/; *[a-Z]+.*$//' | tr ';' '\n' |
 tr -s ' ' | sed -r 's/^ *\[.+]//; s/ *_[a-zа-я]+\. */ /; s/^ *//; s/ *$//' |
 sed 's/^([^)]*) *//; s/ *[-:][a-Z :-]*$//; s/([^)]*[a-Z][^)]*) *//' |
 egrep -v ‘^$|^[а-я]\) ‘)”

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

Для начала обработаем пару нестандартных ситуаций. Во-первых, если ничего не выделено, просто молча прекратим работу. Проверить мы это можем уже известной нам командой test или '['. У нее есть опция -z, которая проверяет, что переданная ей строка пуста:

[ -z “$word” ] && exit 0

А можно сделать и наоборот – проверить, что в строке что-то есть:

[ -n “$word” ] || exit 0

Или даже так:

[ “$word” ] || exit 0

Во-вторых, проверим, что произойдет, если слово не обнаружится в словаре:

$ dict qwe
No definitions found for “qwe”, perhaps you mean:
mueller7: awe ewe owe we

Логично предположить, что это сообщение выводится в стандартный поток ошибок. Проверим:

$ dict qwe 2>/dev/null
$

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

(thumbnail)
Рис. 1. Ошибка: слово не найдено.
[ “$text” ] || {
 zenity --error --text=”Слово ‘$word’ не найдено.”
 exit 1
}

Все вместе будет выглядеть так:

#!/bin/bash
word=”$(xclip -o)”
[ “$word” ] || exit 0
text=”$(dict “$word” 2>/dev/null | tail -n +6 |
 sed -r 's/^ *[0-9]+[.)]/`/; s/^ *_[IVX]+/`/' |
 tr '\n' ' ' | tr '`' '\n' | sed -r 's/; *[a-Z]+.*$//' | tr ';' '\n' |
 tr -s ' ' | sed -r 's/^ *\[.+]//; s/ *_[a-zа-я]+\. */ /; s/^ *//; s/ *$//' |
 sed 's/^([^)]*) *//; s/ *[-:][a-Z :-]*$//; s/([^)]*[a-Z][^)]*) *//' |
 egrep -v ‘^$|^[а-я]\) ‘)”
[ “$text” ] || {
 zenity --error --text=”Слово ‘$word’ не найдено.”
 exit 1
}

«Выбирайте, господа!»

Теперь нужно полученный текст показать в каком-нибудь окошке с возможностью выбора из имеющихся вариантов. Здесь нам снова поможет утилита zenity: у нее есть опция checklist, выводящая список с галочками для выбора. У аргумента этой опции достаточно своеобразный синтаксис: “TRUE|FASLE текст1 TRUE|FALSE текст2 ...”

Поэтому, прежде чем передавать ей текст, его нужно сначала обработать – привести к нужному виду. Запись TRUE|FALSE в примере означает, что в этом месте может стоять либо TRUE, либо FALSE. Первое обозначение подразумевает отмеченную «птичку», второе – не отмеченную. Нам логично выбор предоставить человеку (то есть самому себе), то есть по умолчанию не выбирать ничего. Другими словами, в каждом пункте у нас будет FALSE. Теперь вспомним, как выглядит наша переменная text на данный момент – добавим в конец скрипта строчку echo “$text”, выделим нужное слово и запустим скрипт:

$ tetradka
свободный, вольный
находящийся на свободе
независимый
добровольный, без принуждения
незанятый, свободный
легкий, грациозный
неограниченный, не стесненный правилами, обычаями и т. п.
щедрый
обильный
бесплатный, даровой
освобожденный от оплаты
открытый, доступный
неприкрепленный, незакрепленный
лишенный
свободный
free currency необратимая валюта, валюта, не имеющая обеспечения
свободно
бесплатно
освобождать
выпускать на свободу

Этот текст нам нужно сделать вот каким:

“FALSE 'первая строка текста' FALSE 'вторая строка текста' ...”

Апострофы, окружающие каждое значение слова, нужны для того, чтобы zenity не восприняла пробелы внутри этих значений как разделители полей. Достаточно просто понять, как такое преобразование с текстом сделать: нужно в начале каждой строки дописать FALSE ', а в конце – один апостроф. И затем заменить все символы перевода строки пробелами. Все это мы уже умеем делать: добавление строк в начало и в конец с помощью sed, а замену символов перевода строки – с помощью tr:

echo “$text” | sed “s/^/FALSE '/; s/$/'/” | tr '\n' ' '

Именно такой конвейер мы подставим в командную строку zenity – конечно же, окружив его конструкцией подстановки вывода – “$(...)”.

Теперь разберемся с другими нужными нам опциями zenity. Во-первых, нужно задать тип окна и его заголовок. Нужный нам перечень с галочками – это частный случай списка, так что тип окна будет list (список). А в качестве заголовка логично будет выбрать слово, которое мы переводим:

zenity --list --text=”$word”

Далее, в тех случаях, когда текста в переводе будет достаточно много, умолчательный размер этого окна окажется не слишком удобен, поэтому добавим еще изменение ширины (width) и высоты (height) окна:

zenity --list --text=”$word” --width=600 --height=450

А теперь перейдем собственно к типу и содержимому списка. Сначала нужно задать тип списка – у нас это будет checklist. Затем с помощью повторения опции column с аргументом задаются имена колонок. Колонок у нас будет всего две: в первой – галочки, во второй – варианты перевода:

zenity --list --text=”$word” --width=600 --height=450 \
--checklist --column=# --column=перевод

Напомню, что обратный слэш в конце строки «отменяет» символ перевода строки, который идет сразу после него. Другими словами, это достаточно распространенный способ разбивки длинных строк в скриптах для читабельности. Не забудьте только, что перевод строки не заменяется пробелом, а именно «исчезает». Именно поэтому перед слэшем стоит пробел – если его не вставить, две опции склеятся вместе и zenity выдаст ошибку:

$ zenity --list --text=”$word” --width=600 --height=450\
> --checklist --column=# --column=перевод

Этот параметр недоступен. Используйте --help для просмотра всех возможных параметров.

Если после выполнения этой команды вы вызовете ее из истории стрелкой вверх, то наглядно увидите отсутствие пробела:

$ zenity --list --text=”$word” --width=600 --height=450--checklist --column=# --column=перевод

Теперь добавляем список, который мы сформировали чуть раньше:

zenity --list --text=”$word” --width=600 --height=450 \
 --checklist --column=# --column=перевод \
 “$(echo “$text” | sed “s/^/FALSE '/; s/$/'/” | tr '\n' ' ')”

Но если вы запустите полученную команду в таком виде, получите пустое окно списка с одной галочкой (рис. 2). В чем же дело? А в обработке кавычек и апострофов командной оболочкой. Ведь такая обработка проводится при запуске команды или конвейера всего один раз, причем перед запуском всех программ, входящих в конвейер. То есть апострофы, поставленные командой sed, обработаны не были: на момент обработки их еще там нет. Нетрудно понять, как выйти из этой ситуации: нужно приказать командной оболочке обработать команду второй раз. Для этого в оболочке есть специальная встроенная команда eval:

(thumbnail)
Рис. 2. Без дополнительной обработки видим лишь пустой список.
eval zenity --list --text=”$word” --width=600 --height=450 \
 --checklist --column=# --column=перевод \
 “$(echo “$text” | sed “s/^/FALSE '/; s/$/'/” | tr '\n' ' ')”

Теперь результат будет таким, как и ожидалось (рис. 3). Запускаем скрипт заново, выбираем несколько вариантов и смотрим, что выдаст zenity на стандартный вывод:

(thumbnail)
Рис. 3. А теперь все правильно.
$ tetradka
свободный, вольный|находящийся на свободе

Как видите, выбранные варианты разделены вертикальной чертой. Заменим ее переводом строки, после чего допишем в начало каждой строки переводимое слово:

eval zenity --list --text=”$word” --width=600 --height=450 \
 --checklist --column=# --column=перевод \
 “$(echo “$text” | sed “s/^/FALSE '/; s/$/'/” | tr '\n' ' ')” |
 tr '|' '\n' | sed “s/^/$word /”

Очевидно, есть смысл записывать все это в некий файл. Только известное нам перенаправление вывода символом ‘>' здесь не подойдет. Ведь при нем файл каждый раз перезаписывается заново, а нам нужно добавлять новые слова с переводами в уже существующий. Для этого в командной оболочке есть другая, родственная конструкция перенаправления – '>>'. Отличается такой сдвоенный знак от одиночного именно поведением в том случае, когда заданный файл уже существует: в этом варианте вывод команды не перезапишет файл, а добавится в конец. Файл, в который мы будем складывать слова с переводами, логично назвать .tetradka (исходя из имени скрипта) и разместить в домашнем каталоге:

eval zenity --list --text=”$word” --width=600 --height=450 \
 --checklist --column=# --column=перевод \
 “$(echo “$text” | sed “s/^/FALSE '/; s/$/'/” | tr '\n' ' ')” |
 tr '|' '\n' | sed “s/^/$word /” >>~/.tetradka

«Но только по одной в одни руки!»

А что если мы по ошибке или по забывчивости добавим в нашу тетрадку значение, которое там уже есть? Хорошо было бы автоматически избавляться от таких дубликатов. Чтобы эти дубликаты найти, текст нужно отсортировать. А удалить повторяющиеся строки в отсортированном файле можно либо специальной командой uniq (от unique – уникальный), либо соответствующей опцией самой команды sort. Называется она в длинном варианте точно так же, как и команда (uniq), а в коротком, соответственно, u. У этой опции нет гибких возможностей управлять унификацией, как у отдельной команды. Но в нашем случае ее вполне хватит.

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

#!/bin/bash
file=~/.tetradka
word=”$(xclip -o)”
[…]
eval zenity --list --text=”$word” --width=600 --height=450 \
 --checklist --column=# --column=перевод \
 “$(echo “$text” | sed “s/^/FALSE '/; s/$/'/” | tr '\n' ' ')” |
 tr '|' '\n' | sed “s/^/$word “ >>$file
mv $file $file.tmp; sort -u <$file.tmp >$file; rm $file.tmp

Раз мы удаляем дубликаты слова с переводом, стоит предусмотреть еще один нюанс: это слово может встречаться в текстах, а значит, и в нашей «тетрадке» – в разном регистре. Например, в начале предложения оно будет написано с заглавной буквы, а в середине – со строчной. Вероятно, возможны и такие случаи, когда все слово будет записано заглавными буквами. Программа dict понимает слова вне зависимости от регистра, а вот при сверке дубликатов этот момент будет мешать. Самый простой вариант – автоматически привести все буквы слова к нижнему регистру. В этом нам поможет sed: во «второй части» команды s/// допустим шаблон '\L', который приводит все следующие за ним символы к нижнему регистру. А следовать за ним должно «все то, что было в первой части команды s///». Для этого понятия тоже есть свой шаблон – '&'. Итак, немного модифицируем сохранение слова в начале скрипта:

word=”$(xclip -o | sed 's/.*/\L&/g')”

Теперь снова соединяем все вместе (рис. 4).

(thumbnail)
Рис. 4. Промежуточный результат в текстовом редакторе.

«Где-то я это уже видел»

Отлично, записывать слова в тетрадку мы уже умеем. Но смысл ее ведь не только в этом. Нужно эти слова оттуда и читать, причем желательно тоже не только вручную. Например, если нам встретится в тексте слово, один или несколько переводов которого уже записаны, то хорошо бы, чтобы наш скрипт сам сказал нам об этом. То есть при получении слова из буфера нужно сначала проверить, есть ли уже такое слово в нашей тетрадке. Если есть, то показать записанные варианты перевода и спросить, записывать ли новый. И только при положительном ответе на вопрос продолжать дальше: отображать полный список переводов с галочками и сохранять выбранные из этого списка.

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

eval zenity --list --text=”$word” --width=600 --height=450 \
 --checklist --column=# --column=перевод \
 “$(echo “$text” | sed “s/^/FALSE '/; s/$/'/” | tr '\n' ' ')” |
tr '|' '\n' | sed “s/^/$word\t/” >>$file

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

grep “^$word” $file

После $word нужно добавить разделитель – не зря ведь мы его вставляли. Тут есть небольшая сложность: команда grep не понимает эскейп-последовательности (такие как '\t', с помощью которой мы обозначали табуляцию). Как из любой безвыходной ситуации, из этой тоже есть как минимум два выхода. Первый – «в лоб»: вписать нужный символ в явном виде. Он не слишком удобен, так как теряется наглядность кода; да и не для любых символов подойдет. Второй способ не настолько прост и очевиден, зато более универсален и читабелен: подставить нужный символ в строку с помощью команды echo, которая как раз умеет обрабатывать такие последовательности, если ей задать ключ -e (от “escape sequence” – эскейп-последовательность):

grep “^$word$(echo -e '\t')” $file

Конечно, само переводимое слово отображать в каждой строке незачем, поэтому мы его отрежем командой cut:

grep “^$word$(echo -e '\t')” $file | cut -f 2-

Вас, возможно, удивило, что мы, вопреки предыдущему опыту использования этой команды, на сей раз не задали ей разделитель полей. Ошибки тут нет: именно табуляция выступает разделителем по умолчанию. Сохраним полученное в какой-нибудь переменной; можно в той же самой text – она сейчас еще не используется:

#!/bin/bash
file=~/.tetradka
word=”$(xclip -o | sed 's/.*/\L&/g')”
[ “$word” ] || exit 0
text=”$(grep “^$word$(echo -e '\t')” $file | cut -f 2-)”

Далее проверим содержимое этой переменной. И если она не пуста – то есть если строки с таким словом были найдены в тетрадке – то выведем окошко с вопросом, где перечислим найденные варианты. В случае отрицательного ответа на вопрос завершим работу скрипта. А в случае положительного – продолжим, то есть дадим возможность записать еще один вариант. В окошке отобразим: текстовое сообщение; потом отделенный пустой строкой список найденных переводов; а затем, после еще одной пустой строки, вопрос о том, выбирать ли и сохранять ли новый перевод. Поскольку пустые строки мы будем записывать эскейп-последовательностями, которые zenity, как и grep, тоже не понимает, то воспользуемся еще раз тем же самым приемом:

[ “$text” ] && {
 zenity --question --text “$(echo -e “Уже записанные переводы слова ‘$word’:\n\n$text\n\nЗаписать новый перевод?”)” ||
 exit 0
}

А вот так (рис. 5) это будет выглядеть на экране.

(thumbnail)
Рис. 5. Добавлять ли новый перевод к уже существующим?

«А нету ли такого же, но без крыльев?»

Все уже отлично, когда мы можем выделить в тексте словарную форму слова. Но далеко не всегда именно в словарной форме оно там встречается. Как мы видели выше, если в словаре заданное слово не найдено, но найдено несколько похожих, то список этих похожих, предваренный названием словаря, выводится в стандартный поток ошибок. Давайте в этом случае предложим на выбор слова из этого списка и выдадим перевод для выбранного слова. А уж если и похожих не найдется – тогда все-таки выдадим сообщение об ошибке. Делать все это, конечно же, нужно внутри скобок [“$text”] || {...}. В первую очередь «повернем реки вспять»: поток вывода отправим в никуда, а поток ошибок – на вывод (иначе обработать его будет нельзя):

dict “$word” 2>&1 >/dev/null

Запись '&1' здесь и означает первый поток, то есть стандартный вывод. Важно: перенаправлять потоки нужно именно в такой последовательности. Если сделать наоборот, то к моменту перенаправления ошибок стандартный вывод уже будет «заземлен в пустоту» – и ошибки уйдут туда же.

Далее выбираем нужную строчку по имени словаря и удаляем это имя, оставляя только варианты слова:

dict “$word” 2>&1 >/dev/null | grep '^mueller7:' | sed 's/^mueller7: *//'

И, наконец, приводим список к построчной форме, «сжав» пробелы, а затем заменив их переводами строки. Результат сохраним все в той переменной text:

text=”$(dict “$word” 2>&1 >/dev/null | grep ‘^mueller7:’ |
 sed 's/^mueller7: *//' | tr -s ' ' | tr ' ' '\n')”

Теперь снова проверяем, попало ли что-то в переменную text, и на этот раз в случае пустоты уже выдаем ошибку:

[ “$text” ] || {
 zenity --error --text=”Слово ‘$word’ не найдено.”
 exit 1
}

Снова почти так же вызываем zenity для формирования списка. Только теперь список у нас должен быть не с множественным выбором, а с единичным: не checklist, а radiolist:

eval zenity --list --text=”$word” --width=600 --height=450 \
 --radiolist --column=# --column=вариант \
 “$(echo “$text” | sed “s/^/FALSE '/; s/$/'/” | tr '\n' ' ')”

Теперь с полученным словом нужно заново сделать все то же самое, что было описано во всех предыдущих частях статьи. Кто сказал «заново»? Нет и нет! Сейчас мы, подобно барону Мюнхаузену, вытащим себя за волосы из болота. А именно: скопируем имеющееся слово в тот же иксовый буфер, из которого мы его брали – и вызовем сами себя. «Принцип чайника» в действии. Скопировать можно той же утилитой xclip с помощью ее опции -i (от input – ввод):

eval zenity --list --text=”$word” --width=600 --height=450 \
 --radiolist --column=# --column=вариант \
 “$(echo “$text” | sed “s/^/FALSE '/; s/$/'/” | tr '\n' ' ')” |
xclip -i

А дальше объединим две небольшие хитрости. Помните из первой статьи? Список параметров, переданных функции или скрипту в командной строке, хранится в переменных с цифровыми именами, начиная с ‘$1’. Продолжая эту аналогию, в переменной ‘$0’ содержится имя самого скрипта или функции. Таким образом мы можем запустить себя, даже не зная как нас зовут. А запустить себя нам нужно заново – то есть не параллельно с выполняющимся сейчас скриптом, а вместо него. Для этого тоже есть специальная хитрость – встроенная команда оболочки под названием exec (от execute – выполнять). Если обычно любая команда, заданная внутри скрипта, запускается «параллельно скрипту» (который ждет ее завершения, а затем продолжает работу), то с exec команда будет выполнена вместо скрипта. То есть скрипт работу не продолжит, а завершится в этой точке. Этим мы и воспользуемся для финального аккорда:

[ “$text” ] || {
 text=”$(dict “$word” 2>&1 >/dev/null | grep '^mueller7:' | sed 's/^mueller7: *//' |
  tr -s ' ' | tr ' ' '\n')”
   [ “$text” ] || {
 zenity --error --text=”Слово '$word' не найдено.”
 exit 1
}
 eval zenity --list --text=”$word” --width=600 --height=450 \
  --radiolist --column=# --column=вариант \
  “$(echo “$text” | sed “s/^/FALSE '/; s/$/'/” | tr '\n' ' ')” |
  xclip -i
 exec $0
}
Персональные инструменты
купить
подписаться
Яндекс.Метрика