Linux shell, pipes (конвейеры) и код завершения.

Преамбула

При работе с конвейерами (пайпами, «трубами», pipe), часто возникает вопрос, как отловить ошибку, произошедшую внутри конвейера.

Суть проблемы

Для примера возьмем код, где ход загрузки файла отображается dialog’овым прогрессбаром (копия):

#!/bin/bash

FADDR="http://tolik-punkoff.com/static/test.mp3"

wget --progress=dot -O "./test.mp3" "$FADDR" 2>&1 |\
stdbuf -o0 awk '/[.] +[0-9][0-9]?[0-9]?%/ { print substr($0,63,3) }' | \
dialog --gauge "Download file from $FADDR" 10 100

Как видите, конвейер (т.е. передача вывода от одной команды другой через |), тут налицо.

Если после данной команды просто дописать echo $?, то будет видно, что результат будет 0.

И когда все в порядке:

И когда все не в порядке (в данном случае, сделан обрыв связи):

На самом деле, это происходит потому, что в переменной $? оказывается код завершения последней команды в pipe.

Решение

В современных версиях bash перед использованием контейнера, необходимо добавить команду:

set -o pipefail

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

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

Проверка

После добавления вышеуказанной команды в скрипт делаем тест с обрывом связи:

Или указываем в переменной $FADDR некорректный адрес:

Теперь echo $? отображает корректный код завершения.

Модификация скрипта

Скрипт на GitHub

Источник

Linux pipes tips & tricks Копия в PDF

Скачивание файла и обработка ошибок в wget. Обратите внимание!

Преамбула

Хотел, конечно, назвать как-нибудь типа «гадское поведение wget» или «уничтожение файлов wget‘ом», но остановился на этом. Добавлю ссылку в пост о кодах ошибок (копия) на этот пост.

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

Суть проблемы

Оказывается, wget создает новый файл перед закачкой. Несмотря на то, появятся там какие-то ошибки или нет, и несмотря на то, какие ошибки это будут, хоть это ошибки сервера (например 404, файл не найден), или ошибки сети (файл не удалось докачать), файл все равно будет создан. Хотя по логике, например, ошибку сервера (пусть 404) можно было бы сначала и проверить. Но wget этого не делает, если не случилась ошибка с кодом 2 (ошибка параметров командной строки или конфигурационных файлов).

На самом деле, такой подход довольно правильный, ну и правда, какая разница, почему оно не скачалось (или не сохранилось на диск в случае I/O error). И для анализа ошибок проблем меньше.

Наибольшую проблему это представляет собой тогда, когда пользователь указывает в параметрах wget ключ -O <путь к файлу>, тогда wget придется его полюбому перезаписать.

Так-то у wget есть «защита от дурака», если файл, например, file уже существует, то при следующей закачке файл file (под тем же именем) будет сохранен, как file.1, и если что, старый файл останется в целости и сохранности. Но в скриптах ключ -O удобно использовать. Не надо следить за этими номерами файлов, и вообще в скрипте, wget без ключей, один сплошной геморрой. Так что ключи используют. Но как же правильно это сделать? Да выбирать место сохранения скачанного и проверять коды ошибок wget!

Решение

Если нужно скачать файл, а уж если скачался, заменить, не скачался, оставить старый

1. Скачиваем файл во временный:
wget -O "/tmp/test.tmp" "http://example.org/test.dat"
2. Проверяем коды ошибок (здесь самый простой вариант)

if [ "$?" -eq 0 ]; then
	#Пункт 3
fi


3. Копируем скачанный файл в целевой: cp /tmp/test.tmp /home/pi/test.dat

Естественно, все имена файлов и пути меняем на свои.

Надо скачать файл. Не скачался — вернуть сообщение об ошибке

1. Скачиваем файл:
wget -O "~.data/test.dat" "http://example.org/test.dat"

2. Проверяем коды ошибок (здесь самый простой вариант). Если ошибка — завершаем работу:

if [ "$?" -ne 0 ]; then
	rm "~.data/test.dat"
	echo "Data download error!"
	exit
fi

Посмотреть размер каталога в Linux. В консоли, но интерактивно и наглядно (ncdu).

И наглядно увидеть, где засрано 🙂

Для отображения размеров каталогов (с подкаталогами), есть стнадартная утилита du, но, если честно, она не очень удобная. Гораздо удобнее ее аналог, написанный с помощью ncursesncdu

Пользоваться очень просто. Заходим в каталог, в котором хотим узнать размеры подкаталогов, и просто вызываем утилиту:

ncdu

Наверное, ее единственный минус, что в каталог уровнем выше, чем тот, из которого ее вызвали, она не переходит. Так что если хотите посмотреть размеры каталогов сразу во всей файловой системе, надо вызывать ее из корня (/). Но причина такому поведению легко может быть найдена — после запуска утилита сканирует все подкаталоги, чтоб потом нам красиво показать. Чем с более верхнего уровня ее вызываешь, тем дольше процесс сканирования.


Сканирование

В подкаталоги входить можно по ENTER, возврат назад по стрелке влево, клавише h или < (обычно запятая с шифтом) или по нажатию ENTER на .., как в mc. Краткая справка по ? (не забывайте нажать SHIFT), выход по q.


Отображение каталогов и их размеров

В Slackware устанавливается штатным образом через sbopkg, как в других линуксах не знаю, в Убунте и Дебиане есть в репозиториях.

Linux wget, коды возврата/завершения. Коды ошибок wget.

Раз уж пошел разговор про wget (копия), решил я заодно дополнить добрым людям скрипт какими-нибудь осмысленными сообщениями об ошибках, а для того нужны коды завершения.

Кто-то, неизвестно почему, утверждал, что в man их нет. Есть. Но пусть уж будут и тут, и мне от склероза, и мало ли кому другому, кто не любит на буржуйском читать.

Коды завершения wget

0 — OK (ошибок нет)
1 — Иная / общая ошибка (generic error code)
2 — Ошибка в параметрах командной строки или файлах конфигурации (.wgetrc или .netrc)
3 — Ошибка файлового ввода/вывода (I/O error)
4 — Ошибка сети (например, при обрыве связи)
5 — Ошибка SSL
6 — Ошибка идентификации (неправильное имя пользователя или пароль)
7 — Ошибка протокола
8 — Ошибка сервера (например, нужный файл на сервере не найден, ошибка 404)

За исключением 0 и 1, коды выхода с меньшими номерами имеют приоритет над кодами с большими номерами, когда встречаются многочисленные типы ошибок.

Linux wget, тайм-аут и повторы. «Виснет» wget при разрыве связи.

Преамбула

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

Проблема

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

wget -O "~/buh01/data/kassa01.dat" "https://example.org/mag01/kassa.dat"

Решение

Оказывается, у wget не проставлен тайм-аут ожидания, а по умолчанию, если его не проставить, wget будет ждать аж 900 секунд (15 минут). Надо, соответственно, его напрямую указать в параметре ключа -T <секунды> (--timeout=<секунды>),
Например, можно поставить вменяемые полминуты (30 с.):

wget --timeout=30 -O "~/buh01/data/kassa01.dat" "https://example.org/mag01/kassa.dat"

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

Заодно можно указать wget‘у количество попыток для скачивания файла: -t <число> (--tries=<число>), тем более, что указание параметра -O (имя выходного файла) сбрасывает этот параметр в 1.

wget --timeout=30 --tries=3 -O "~/buh01/data/kassa01.dat" "https://example.org/mag01/kassa.dat"

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

Шпаргалка по Wget. Копия в PDF

Linux dialog —infobox с «мельницей», скрашивающей процесс ожидания.

Сделал гибрид старого скрипта waiter (копия) с dialog --infobox. В infobox‘е отображается псевдографическая «мельница»

https://www.youtube.com/watch?v=o9KempHy5To

Не стал уж совсем дублировать waiter, так что никаких параметров демо-скрипт не принимает, просто отсчитывает 10 секунд, показывая псевдографическую «крутилку».

Исходник на GitHub

Linux. Удалить комментарии из файла.

Заметка от склероза. Способ ниже удаляет закомментированные, а заодно и пустые строки из файла, но не трогает комментарии в строках. Хорошо помогает почистить какой-нибудь конфиг, в котором комментариев больше, чем самих параметров, и хрен че найдешь:

grep -o '^[^#]*' file.txt > cleaned.txt

Вместо file.txt подставляем исходный файл, вместо cleaned.txt — выходной.

Нашел здесь Копия в PDF

Там есть и другие варианты решения задачи.

Экзаменационные байки от PunkArr[]

Квасим сегодня редакцией по поводу очередной годовщины окончания «СООУ» (Самого Охуенного Образовательного Учреждения) товарищем PunkArr[]. Это же СООУ пытался закончить и L.S., но не взлетело, пришлось Гоптех aka Лестех заканчивать. Но сегодня истории не про Гоптех, а про СООУ.

Исполнитель анальных наказаний. Любимые приколы П.И.

Это наш препод по спецпредметам (электротехника и сопутствующие) П.И. Инженер, программист паскалем и ассемблером, тролль (хотя тогда такого слова не было) и электрик 80 уровня. Как он сам говорил — электротехнику я знаю на 4, на 5 ее знает Бог. Т.е. больше, чем на 3, рассчитывать на первом курсе не приходилось. Не, в группе был уникум, который получил 5. И вообще, редко получал даже 3. Ну он сейчас вроде как целую электростанцию возглавляет, так что не зря.

Из любимых приколов П.И.

Рисует П.И. на доске схему, чтоб мы ее рассчитали на уроке. И тут какая-то сука с задних парт возьми да ляпни: «Простая же схема».
П.И.: Действительно, простая. Ща поправлю.
Стирает и рисует хтонический ужас, достойный Лавкрафтовского Некрономикона. Сонмища резисторов, конденсаторов, индуктушек, расположенных в самом причудливом порядке. Элементов на 150 мог захреначить, причем из головы.
Естественно, вся группа ловит по «лебедю».
Его рекорд — 180 двоек на группу за одну пару. Точнее наш рекорд. За несданные практические работы.

Раздача медалей после контрольной. Робкий голос из зала «А почему мне 3?»
П.И.: Ну-ка, ходи сюда, еретик и крамольничек!
Внимательно рассматривает работу. Находит две ошибки.
Да какое тебе три! Тебе две!

Но препод был действительно от Ктулху, если видел в студенте заинтересованность, а студент задавал правильные вопросы хотя бы на семантическом уровне. Типа, не «почему мне два», а «какие ошибки я допустил». Кстати, когда мы более-менее подружились — надарил мне две огромных сумки раритетных изданий фантастики. Книги живы до сих пор.

Экзамен по электротехнике

Берем билеты, готовимся, первым отвечать никто не идет.

Повисает мхатовская пауза. Сильно мхатовская. Пауза превращается в Дамоклов меч.

Наконец, П.И. надоедает, он тасует зачетки как колоду карт, тыкает в меня пальцем и спрашивает, какая и откуда (сверху или снизу).

Я говорю, что первая снизу, и это оказывается МОЯ, блядь, МОЯ трижды ебаная зачетка. Так я даже в повсеместно стоящих игровых автоматах не проигрывал. 1/25, сука. Жестокая сука теория вероятности.

Он дает еще некоторое время дописать билет, а потом спрашивает «А ты кто по гороскопу?» Я отвечаю, что Дева. Он глубокомысленно листает зачетку, типа смотрит в гороскоп, а потом выдает:
— У тебя сегодня ужасное в гороскопе написано…
— А что?
— Сегодня Дева станет раком!
Пришлось идти отвечать, получил в результате 4, хотя с перепугу забыл закон Ома для полной цепи, который прямо на плакате в кабинете на стене висел. Над чем П.И. вместе со всей группой откровенно поржали.

Кому грядеше, экзамене?

Второй случай, у того же препода, но на этот раз он ведет два похожих предмета: «Ремонт электрооборудования» и «Эксплуатация электрооборудования». Мало того, что предметы похожие, вопросы в билетах похожие, так еще и экзамены подряд, один сегодня, другой завтра.

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

Ко мне подходит телемастер и спрашивает, че я не в выходной приперся, чего меня трясет, и «отчего я зеленый такой».

Я объясняю, дядька дает мне полтос и говорит, сходить за маленькой. Поскольку водку я тогда пил редко, то он в меня ее влил, размешивая с полстаканом кофе растворимого.

Я так стаканов 8 выпил. И меня поперло! Эффект, скажу вам, был не хуже, чем от спидов (которые я попробовал гораздо позже). Аж прибежал на экзамен, вытащил билет, и попросился первым без подготовки. Препод от такой наглости офигел, но согласился.

И вот я рассказал билет на одном дыхании.

Препод (П): Отлично! Но какой предмет мы сегодня сдаем?
Я: Ремонт!
П: Нет, это ты почему-то сдал без подготовки ремонт. А все сдают эксплуатацию. Поэтому по ремонту тебе 5, а по эксплуатации 2. Завтра пересдаешь на ремонте эксплуатацию.
Пришлось пересдавать…
Эксплуатацию сдал на 4 без допинга.

Замена концов строк MAC (CR, 015 OCT, 0xD HEX) на концы строк Linux (LF, 012 OCT, 0xA HEX)

Преамбула

Понадобилось для работы. Современная MacOS такие концы строк не использует, а некоторое ПО, для совместимости со старыми маками (LC II/LC III) их еще использует. И сами маки в качестве терминалов для наблюдения за датчиками погоды еще юзаются. И живые. Я на таких учился в школе.

Это же аптайам > 20 лет, хуясе. Сии компьютеры младше меня на 5 лет всего. А по характеристикам Intel 486DX 286, ну 386 в лучшем случае, а на них есть оконный интерфейс. Правда, консоли нет и коды ошибок уже не найти (а там скудные сообщения об ошибках — messagebox с кодом).

Решение

cat "file.txt" |tr '\015' '\012' >"file2.txt"

Техническое

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

L.S., директор проекта

Linux. Кодирование/декодирование файлов в/из BASE64

Преамбула

Как однажды сказал мой учитель, «линукс может все, а многое еще и из коробки, надо только знать как». Вот в данном случае именно так и оказалось. Понадобилось мне массово перекодировать файлы в/из BASE64.

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

Вот и придумали умные люди, а давайте все, что не влазит в английский алфавит и цифры, будем по хитрому алгоритму заменять, таки да. На английский алфавит и цифры. Так и получился BASE64. Подобная хрень в криптографии называется «транспортная броня».

Так вот, в Linux из коробки (входит в пакет Coreutils, который есть почти везде) есть утилита, способная кодировать/декодировать BASE64. Называется она, внезапно, base64.

Кодирование

Для опытов возьмем восьмибитного Сиро Исии. Он, когда живой был, много кого брал на опыты.

base64 isia.png >isia.b64

Закодированный Сиро Исии на Pastebin

Декодирование

base64 -d isia.b64 >isia.decoded.png

Тот случай, когда суть получилась короче преамбулы.

Аналог trim() в bash/linux shell

Преамбула

Пишу небольшой скрипт, для которого надо сохранять две выбранные пользователем при предыдущих запусках скрипта опции, а при следующих запусках их восстанавливать.
Решил воспользоваться самым простым методом. Создаем рядом со скриптом 2 файла, и туда сохраняем значения опций:

echo "$OPT1" > ./option1
echo "$OPT2" > ./option2

Некрасиво, но просто.

При запуске скрипта, читаем опции обратно:

OPT1=`cat ./option1`
OPT2=`cat ./option2`

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

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

Тестовый файл и вывод его в консоль

Возьмем, например, такой тестовый файл: На PasteBin

Если мы просто выведем его на консоль с помощью команды cat "trimtext.txt", то получится следующее:

smallwolfie@wolfschanze:~/myfiles/test/trim# ./testtrim


                   Trim()

smallwolfie@wolfschanze:~/myfiles/test/trim#

т.е. со всеми пробелами и пустыми строками.

Решение

Самая простая реализация trim() в shell/bash пропустить вывод команды через xargs без параметров. xargs, которая требует после себя команды, без указания команды все равно что echo без параметров. А echo без параметров все пустое обрезает.

#!/bin/bash

cat "trimtext.txt"|xargs

Вывод:

./testtrim
Trim()

Linux dialog, прикручиваем симпатичный progressbar к wget.

Преамбула

Какой бы ваш скрипт не был диалоговым, «само дерево жужжать не может», а в скрипте обычно жужжат утилиты, которые о диалоговых окнах представления не имеют. Так вот прикрутим контролируемый прогрессбар (--gauge) из виджетов dialog к всем известному wget.

Готовый код

#!/bin/bash

FADDR="http://tolik-punkoff.com/static/test.mp3"

wget --progress=dot -O "./test.mp3" "$FADDR" 2>&1 |\
stdbuf -o0 awk '/[.] +[0-9][0-9]?[0-9]?%/ { print substr($0,63,3) }' | \
dialog --gauge "Download file from $FADDR" 10 100

Разбираем, что он делает

1. Если просто запустить wget без параметра --progress=dot (параметр -O "./test.mp3" здесь показывает путь и имя для сохраняемого файла, для теста оно у нас жестко задано), то wget попытается нарисовать свой плохенький progressbar:

Использование параметра --progress=dot приводит вывод на экран в такой вид:

Теперь у нас видны проценты, которые выводятся по штуке на строчку через определенный интервал времени по внутреннему алгоритму wget.

2. wget почему-то выводит сообщения о своей работе не на stdout, а на stderr. Аж морово поветрие какое-то, ибо и некоторые другие утилиты тоже выводят на stderr. Оправданно на stderr выводит сообщения только dialog, тому ще он всю псевдографику выводит на stdout. Отучаем wget писать на stderr вместо stdout:

2>&1

2 — зарезервированный канал для stderr 1 — для stdout. Тут мы просто перенаправляем один канал в другой.

3. Меняем буферизацию. На самом деле, буфер консоли (терминала) выводит на экран сообщение, если в буфере консоли накопилось 1024 байта. Или программа завершилась. Иначе нет. С помощью утилиты stdbuf устанавливаем буфер stdout (-o) в ноль (-o0), тем самым мы добиваемся, чтоб символы сразу поступали на вход awk. stdbuf в качестве второго параметра требует программу, которой она будет посылать измененный буфер. В данном случае awk.

buffering in standard streams
man stdbuf на русском

За разъяснение спасибо другу из Телеграм.

4. Далее вывод wget‘а передается awk, которая хитрой регуляркой и оператором substr вырезает из вывода все, кроме значений процентов.

5. dialog --gauge может принимать из stdout числа, и отображать их в виде progressbar’а

Пример на GitHub

Linux. Определить в скрипте, что программа не найдена. И другие зарезервированные коды завершения.

Преамбула

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

Решение

Оказывается в Linux есть стандартные (зарезервированные) коды завершения, а если программа не найдена, то bash автоматически выдает код завершения 127. Таким образом, задача сводится к тому, чтобы на код 127 правильно отреагировать.

Можете проверить на этом скрипте:

#!/bin/bash

$1

EXITCODE=$?

if [ $EXITCODE -eq 127 ]; then
    echo "Command not found!"
else
    echo "Exit code: $EXITCODE"
fi

Проверка

/excode-test ls
excode-test pb01 pb02 test.mp3
Exit code: 0

Команда существует и выполнена успешно.

./excode-test sagfhjsdgfhjgf
./excode-test: line 3: sagfhjsdgfhjgf: command not found
Command not found!

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

Другие зарезервированные коды завершения

1 — разнообразные ошибки, используется как стандартный код ошибки в скриптах или программах
2 — согласно документации к Bash — неверное использование встроенных команд, иногда программисты его перелпределяют.
126 — вызываемая команда не может быть выполнена, возникает из-за проблем с правами доступа или когда вызван на исполнение неисполняемый файл
127 — «команда не найдена»
128 — неверный аргумент команды exit
128+n — фатальная ошибка по сигналу «n»
130 — завершение по Control-C
255 — код завершения вне допустимого диапазона, но утилита dialog использует этот код, как завершение по ESC.

Источник

Коды завершения, имеющие предопределенный смысл

Linux dialog —menu. Динамическое меню и прочие штуки

Динамическое меню из массива

dialog был бы совсем негибким инструментом, если бы не было возможности формировать элементы меню более-менее «налету», а не только непосредственно в коде скрипта. И такая возможность есть. Виджет --menu (а также --checklist и --radiolist, которые я здесь упущу), принимают на вход массив. Формат массива такой:

MNUARR[0]="Элемент 1"
MNUARR[1]="Описание 1"
MNUARR[2]="Элемент 2"
MNUARR[3]="Описание 2"
...

т.е. линейный массив, где в первый элемент записывается, извиняюсь за тавтологию, элемент, а в следующий — описание, далее все повторяется.

Массив будем брать из файла.

1. Определяем переменную с путем к файлу и массив:

MNUFILE="./dynmenu01.txt"
declare -a MNUARR

2. Заполняем массив:

IDX=0
while read LINE; do
    PT_1=`echo "$LINE"|awk '{print $1}'`
    PT_2=`echo "$LINE"|awk '{print $2}'`
    MNUARR[$IDX]=$PT_1
    let "IDX=IDX+1"
    MNUARR[$IDX]=$PT_2
    let "IDX=IDX+1"
done <"$MNUFILE"

3. Вызываем dialog:

dialog --clear \
    --title "Dynamic menu" \
    --menu "Select option:" \
    20 76 10 \
    "${MNUARR[@]}"

Пример на GitHub
Файл к нему

Меню из файла

Массивом, на самом деле, пользоваться неудобно, и код получается более громоздкий, и несовместимо это может оказаться с другими shell’ами, и возможностей у массива меньше. Есть другой выход — воспользоваться файлом специального формата. Для виджета --menu формат таков:

"Элемент 1"[пробел]"Описание 1"[пробел]\
"Элемент 2"[пробел]"Описание 2"[пробел]\

Собственно, от описания меню в коде ничем не отличающийся. Можно даже в одну строчку через пробелы все написать, но с пробелом и \ в конце строки симпатичнее.

Остается в параметре --file виджета --menu указать путь к файлу.

MNUFILE="./dynmenu-file.txt"

dialog --clear \
    --title "Dynamic menu" \
    --menu "Select option:" \
    20 76 10 \
    --file "$MNUFILE" \

Пример на GitHub
Файл к нему

Дополнительное описание (помощь) к элементу меню

С помошью общего для dialog ключа --item-help можно добавить к элементам подсказку, появляющуюся в выделенной строке внизу экрана. Подсказка ограничена количеством символов в строке, что не влезло — уползает за экран.

Формат файла для такого меню будет:

"Элемент 1"[пробел]"Описание 1"[пробел]"Помощь 1"[пробел]\
"Элемент 2"[пробел]"Описание 2"[пробел]"Помощь 1"[пробел]\

Точно также меню с помощью описывается и непосредственно в коде скрипта.

Вызов dialog:

MNUFILE="./dynmenu-file-help.txt"

dialog --clear \
    --item-help \
    --title "Dynamic menu" \
    --menu "Select option:" \
    20 76 10 \
    --file "$MNUFILE" \

Для динамического меню из массива это не работает (или я не понял как).

Пример на GitHub
Файл к нему

Разделители, или отображение небольших табличных данных в меню

Конечно, виджет --menu, это вам не DataGridView из .NET Framework, и вообще, отдельного виджета для отображения табличных данных у dialog нет, но, если данных немного, то можно воспользоваться общим параметром для виджетов dialog‘а
--column-separator. Раз параметр общий, то кроме --menu, должен срабатывать и для --checklist и --radiolist.
После параметра --column-separator задаем, собственно, сам разделитель (в примере |).

Примечание: параметр работает только для описаний пунктов меню.

MNUFILE="./dynmenu-file-sep.txt"

dialog --clear \
    --item-help \
    --column-separator "|" \
    --title "Dynamic menu" \
    --menu "Select option:" \
    20 76 10 \
    --file "$MNUFILE" \

Пример на GitHub
Файл к нему

Эта опция работает и для динамического меню, созданного с помощью массива:

Пример на GitHub
Файл к нему

Linux dialog —menu, пункт по умолчанию и сохранение выбранного элемента

Преамбула

dialog — консольная утилита, позволяющая встраивать в ваши скрипты псевдографический интерфейс. Есть и ее графические варианты Kdialog, Xdialog, позволяющие обеспечить ваши скрипты даже полноценным оконным интерфейсом в средах X-сервера.

Как пример можно привести скрипт пакетного переименования файлов (см. на GitHub) работающий в графической среде, или пакетный менеджер Slackware pkgtool.


pkgtool вид спереди

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

Примеры на GitHub

Общий синтаксис

Общий синтаксис утилиты dialog таков:

dialog <общие_параметры> <виджет> <параметры_виджета>

Пункт меню по умолчанию

Указание пункта меню по умолчанию возможен не только в виджете --menu, но и в других виджетах, потому прописать его надо в параметрах, указанных до вызова конкретного виджета.

Если будет указан несуществующий пункт меню или пустая строка, к ошибке это не приведет. Будет выбран первый пункт меню.

dialog --clear --title "Default Item Test" \
    --default-item "Item #2" \
    --menu "Select item:" \
    20 51 7 \
    "Item #1"  "Test Item #1" \
    "Item #2" "Test Item #2" \
    "Item #3" "Test Item #3" 2>"/tmp/dlgres.tmp"

Код на GitHub

Сохранение пункта меню и последующий его выбор

Минус утилиты dialog в том, что это не связанная система меню, а все-таки отдельная утилита, и, соответственно, один вызов с другим никак не связан. Так что если в скрипте несколько меню и предусматривается возврат в основное, или блуджание по системе меню, то у пользователя может возникнуть неудобство, поскольку при возвратах автоматически будет выбран первый пункт.

Но с помощью опции --default-item можно сделать так, чтоб при возврате в одно из меню выбирался последний выбранный пункт.

Сохранение последнего выбранного пункта меню в переменную

1. Заводим переменную для временного файла:

TEMPFILE="/tmp/dlgres.tmp"

2. И переменную для хранения пути к самой утилите dialog:

DIALOG="dialog"

Это не обязательно, но удобно, если понадобится переделать скрипт под Xdialog или Kdialog

3. Заводим переменную для последнего выбранного пункта:

LAST_MENU1=""

4. Само меню будем вызывать из функции menu_in_variable()

Далее внутри функции:

5. Заводим константы для кодов возврата программы dialog:

OK_C=0 #Нажата кнопка OK в диалоговом окне
ESC_C=255 #Нажата клавиша ESC
CANCEL_C=1 #Нажата кнопка Cancel в диалоговом окне

Примечание: На самом деле с отслеживанием клавиши ESC у утилиты menu какой-то конфликт с PuTTY, почему-то утилита завершает работу с кодом 255 не только по нажатию ESC, но и по нажатию клавиш F1..F4 Не в PuTTY это не проявляется.

5. Вызываем dialog:

$DIALOG --clear --title "Save last selected item into variable" \
    --default-item "$LAST_MENU1" \
    --menu "Select:" 20 60 7 \
    "Item #1" "Item 1" \
    "Item #2" "Item 2" \
    "Item #3" "Item 3" 2>"$TEMPFILE"

Примечание: dialog записывает в stderr выбранный элемент меню (в случае виджета --menu), потому перенаправляем вывод на stderr во временный файл, путь к которому указан в переменной $TEMPFILE (2>"$TEMPFILE")

6. Сохраняем код возврата:

RETVAL=$?

7. И далее его анализируем. Если нажата кнопка OK, то читаем файл с выбранным пунктом меню в переменную. Если нажата Cancel или клавиша ESC, то на stderr утилита ничего не выведет и временный файл будет пуст.

case "$RETVAL" in
	$OK_C)
		LAST_MENU1=`cat "$TEMPFILE" `
		#...
		;;
	$ESC_C)
		return;;
	$CANCEL_C)
		return;;
esac

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

Сохранение пункта меню в файл

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

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

STATEFILE="./savestate"

И перед вызовом dialog читаем файл в переменную:

if [ -e "$STATEFILE" ]; then
	LAST_MENU2=`cat "$STATEFILE"`
fi

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

$DIALOG --clear --title "Save last selected item into file" \
	--default-item "$LAST_MENU2" \
    --menu "Select:" 20 60 7 \
    "Item #1" "Item 1" \
    "Item #2" "Item 2" \
    "Item #3" "Item 3" 2>"$TEMPFILE"

RETVAL=$?

case "$RETVAL" in
	$OK_C)
		cp "$TEMPFILE" "$STATEFILE"
		LAST_MENU2=`cat "$STATEFILE"`    
		;;
	$ESC_C)
		return;;
	$CANCEL_C)
		return;;
esac

Пример полностью на GitHub