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

Выход из X через консоль (и желательно удаленно)

Долго искал, как выйти из X через консоль, и в мануалах ничего не мог найти. Почему-то это нигде толком не описано. Хотя, я думаю, завершить X на другой машине удаленно, имея под рукой только консоль/удаленный терминал, возникала не у меня одного.

И оказалось, что какого-то единственного универсального способа нет.

Проверить runlevel

Runlevel или уровень запуска — это программная конфигурация системы, которая позволяет запускать только выбранную группу процессов на определенном этапе. Их до 10, но нас интересует уровень 3 — многопользовательский (консольный) режим, и уровень 5 (в Slackware — 4), многопользовательский графический режим, в котором X-server запускается по умолчанию.

Если система находится на уровне 4 (5), то способы как либо пришибить иксы могут не сработать, иксы перезапустятся. С уровня 3 можно запустить X-сервер вручную, для этого надо в консоли ввести (обычно) startx. Если система на уровне 3, то иксы сравнительно легко прибить (см. ниже).

Визуально уровень запуска обычно определить легко. 3 — после загрузки ОС будет консольное приглашение ввести логин и пароль, например:

Welcome to Linux 4.4.14-smp (tty1)

wolfsсhanze login:

На уровне 4 (5) на экране будет предложение ввести логин/пароль, но уже в иксовой форточке.
Правда, некоторые не очень популярные дистрибутивы хитрят. Например, Puppy Slacko запускается на уровне 3, а X-сервер вызывает уже из своих инициализационных скриптов.

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

runlevel

Вывод:

N 3

или

who -r

Вывод:

run-level 3 2020-01-28 07:12 last=S

Переключить runlevel

Неверное, самый универсальный способ завершить работу X-server на лету и через консоль, это переключить runlevel. Команда должна быть выполнена от root.

— Для Slackware:

init 3

— Для дистрибутивов с systemd:

systemctl isolate runlevel3.target

Вернуться в иксы.

Для Slackware:

init 4

— Для дистрибутивов с systemd:

systemctl isolate graphical.target

Переключить runlevel по умолчанию (при старте системы)

— Для Slackware:

1. Под root запускаем mc и идем в /etc
2. Ищем там файл inittab и открываем его в редакторе.
3. Ищем строчки:

# Default runlevel. (Do not set to 0 or 6)
id:4:initdefault:

Они обычно в начале файла.

4. Меняем 4 на 3 и сохраняем файл. Если надо X при старте — меняем 3 на 4. Если что, обычно в файле есть комментарий-подсказка (на буржуйском).

— Для дистрибутивов с systemd:

Чтоб X был выключен по умолчанию:

systemctl set-default runlevel3.target

Чтоб X по умолчанию был включен:

systemctl set-default multi-user.target

Подробнее почитать о runlevel

Runlevel в Unix/Linux Копия

Другие способы завершить X-сервер через консоль.

Опять же, повторюсь, стопроцентно это сработает только если X-server запущен вручную (или через скрипты), когда система находится в runlevel 3.

— Придушить X-сервер совсем:

killall Xorg

— Более аккуратно придушить иксы (для систем с systemd). Надо отправить команду завершения оконному менеджеру.

В общем виде:

systemctl stop display-manager.service

Вместо display-manager.service подставляем свой оконный менеджер (наверное, не все поддерживают, но у меня systemd нет, так что не тестировал):

systemctl stop gdm

— Способ для xfce:

xfce4-session-logout --logout --display :0.0

Корректно срабатывает только с локальной консоли. Удаленно может не работать.

Еще про способы выйти в «чистую» консоль из иксов

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

Для этого надо нажать Ctrl+Alt+F2F6 и вам откроется чистый терминал. На Ctrl+Alt+F7 обычно сидят сами иксы, и таким образом, можно к ним вернуться. А первый терминал (Ctrl+Alt+F1) иксы занимают под служебные нужды. В некоторых системах для выхода из иксов срабатывает такой способ:

1. Переключиться в первую консоль (Ctrl+Alt+F1)
2. Нажать Ctrl+C/Ctrl+Break

В некоторых системах для выхода из иксов может сработать комбинация Ctrl+Alt+Backspace

В системах, запускающихся в графическом runlevel по умолчанию, это можно использовать для перезагрузки графического окружения, если X зависли (как soft-restart в Windows 98). Так же можно использовать и команду killall Xorg

Об опасности использования eval в bash-скриптах.

Преамбула

В недавнем примере я допустил, что называется, «детский мат», грубую и далеко неочевидную новичку ошибку, о которой, впрочем, все знают. Но прописные истины неплохо повторять, так что не стоит кидаться на эту заметку с криками «боян!!!111пыщ».
Для кого-то может и боян, а кому-то это может быть неизвестно. Или вот я, зная в теории об ошибке, все равно ее допустил, и даже не сразу понял, что я так грубо наебался, и мой скрипт стал небезопасным от слова «совсем».

Исходная задача

Имелся следующий набор данных в виде форматированного текста (таблицы в текстовом файле):

verb666,Misha Verbitsky,+415314499922,42,11:00-16:00
ktvs421,Vasiliy Kotov,+415314499966,77a,00:00-06:00
dkldn89,Dmitry Kaledin,+415314499949,65b,22:00-00:00
vfurry1,Veniamin Furman,+415314499900,99,12:20-19:25
tpunk56,Tolik Punkoff,+415314499911,59,00:00-11:00

Исходная задача состояла в том, чтобы раскидать вывод awk в переменные bash, так, чтоб значение из соответствующей колонки таблицы попало в указанную переменную bash, и изначальный код был таким:

# ... цикл по строкам
for TMPSTRING in $(cat "testfile.txt")
do
    eval $(echo "$TMPSTRING"|awk -F "," '{print "LOGIN=" dq $1 dq; 
    print "FULLNAME=" dq $2 dq; print "PHONE=" dq $3 dq; print"ROOM=" dq $4 dq;
    print "WORKTIME=" dq $5 dq}' dq='"')

# ... делаем что-то с данными в переменных
done
# ...

Т.е. тут мы сначала получаем строчку текстового файла, передаем ее awk, awk, в свою очередь, возвращает строки:

...
LOGIN="vfurry1"
FULLNAME="Veniamin Furman"
PHONE="+415314499922"
ROOM="99"
WORKTIME="12:20-19:25"
...

Похоже на команду присваивания значений переменным. Да так оно и есть!
Далее, полученная строка передается команде eval.

Опасность eval

В чем опасность eval? Да в том, что эта команда преобразует любую переданную ей строку в команду или команды bash, и немедленно их выполняет. Любую, абсолютно любую строку.

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

verb666,Misha Verbitsky,+415314499922,42,11:00-16:00
ktvs421,Vasiliy Kotov,+415314499966,77a,00:00-06:00
dkldn89,Dmitry Kaledin,+415314499949,65b,22:00-00:00
vfurry1,Veniamin Furman `rm -rf ./testdir`,+415314499900,99,12:20-19:25
tpunk56,Tolik Punkoff,+415314499911,59,00:00-11:00

Тут, после Veniamin Furman вписана команда удаления директории ./testdir в текущем каталоге, а могло быть вписано и $HOME или даже /.

Строки, взятые в двойные кавычки, интерпретируются bash’ем не только, как строки с чисто тестовыми данными. В двойных кавычках можно и нужно сделать подстановку, если это возможно. А символы `` (обратные кавычки) значат, что нужно выполнить команду внктри них, а потом подставить результат в то место, где указано выражение в обратных кавычках.

Таким образом, команда

FULLNAME="Veniamin Furman `rm -rf ./testdir`"
выполнится так:
1. В переменную записывается строка Veniamin Furman
2. Выполняется команда rm -rf ./testdir
3. Вывод команды дописывается в переменную

Соответственно, каталог ./testdir в процессе будет удален.

Общие рекомендации

Всегда держите в голове, что данные внутри eval$() будут исполнены bash’ем, как код (команды оболочки).

Исправление

1. Заменить двойные кавычки (") на одинарные ('). Строки в одинарных кавычках будут интерпретированы просто как текстовые последовательности, и без всякого выполнения и изменения записаны в переменные, так как есть:

eval $(echo "$TMPSTRING"|awk -F "," '{print "LOGIN=" sq $1 sq;
print "FULLNAME=" sq $2 sq; print "PHONE=" sq $3 sq; print"ROOM=" sq $4 sq;
print "WORKTIME=" sq $5 sq}' sq="'")

Вывод:

vfurry1 Veniamin Furman `rm -rf ./testdir` +415314499900 99 12:20-19:25

2. Но и это еще не конец, злоумышленник может обмануть и одинарные кавычки, обернув свою команду в …одинарные кавычки:

vfurry1,Veniamin Furman '`rm -rf ./testdir`',+415314499900,99,12:20-19:25

Тогда выполнение будет происходить так. Когда очередь дойдет до FULLNAME awk добросовестно выведет:

FULLNAME='Veniamin Furman '`rm -rf ./testdir`'

eval же это также добросовестно интерпретирует. В $FULLNAME будет записано Veniamin Furman, далее закроется одинарная кавычка, т.е. больше ничего в переменную писаться не будет, а далее выполнится rm -rf ./testdir.

Решение таково — удалить или заменить на что-либо безобидное, одинарные кавычки из входной строки перед передачей ее awk. Можно удалить sed‘ом:

sed -e 's~'\''~~g'

Код целиком:

eval $(echo "$TMPSTRING"|sed -e 's~'\''~~g'|awk -F "," '{print "LOGIN=" sq $1 sq;
print "FULLNAME=" sq $2 sq; print "PHONE=" sq $3 sq; print"ROOM=" sq $4 sq;
print "WORKTIME=" sq $5 sq}' sq="'")

Вывод зловредной сткроки будет:

vfurry1 Veniamin Furman `rm -rf ./testdir` +415314499900 99 12:20-19:25

Тестовый пример

Файлы:

maketestdir — скрипт для создания тестовых каталогов.

testfile.txt — Тестовый файл. Ошибку безопасности можно обойти, заменив двойные кавычки одинарными в коде.
testfile2.txt — Замена двойных кавычек одинарными не поможет. Необходимо дополнительно удалять одинарные кавычки во входной строке.

wrongcode — файл без исправлений кода (в коде используются двойные кавычки)
wrongcode2 — только первое исправление (в коде используются одинарные кавычки)
goodcode — окончательно исправленный код.

Вызов скриптов: <скрипт> <тестовый файл>

Например:

wrongcode testfile2.txt

Пример на GitHub

Отключение всплывающих уведомлений в Firefox.

А заодно и другой лишней ерунды вроде доступа к микрофону или запроса геолокации.

1. Заходим в Инструменты —> Настройки
2. Переходим на вкладку «Приватность и защита»
Или сразу пишем в строке адреса: about:preferences#privacy
3. Мотаем вниз до раздела Разрешения:

4. Жмем кнопку «Параметры» напротив уведомлений, очищаем список, если в нем что-то есть, и ставим галочку Блокировать новые запросы на отправку вам уведомлений

5. Жмем Сохранить изменения
6. ФАНФАРЫ!

Таким же образом можно поступить с местоположением, камерой и микрофоном.

Записать вывод awk в несколько переменных bash

Или как раскидать результат работы awk по нескольким переменным.

Преамбула

Предположим, у нас есть некоторая таблица в виде файла CSV с набором полей, например таких Login,FullName,Phone,Room,WorkTime и разделителем полей , (запятая):

verb666,Misha Verbitsky,+415314499922,42,11:00-16:00
ktvs421,Vasiliy Kotov,+415314499966,77a,00:00-06:00
dkldn89,Dmitry Kaledin,+415314499949,65b,22:00-00:00
vfurry1,Veniamin Furman,+415314499900,99,12:20-19:25
tpunk56,Tolik Punkoff,+415314499911,59,00:00-11:00

Нужно вытащить из нее некоторые данные, и далее как-либо обработать. Вытащить данные можно с помощью awk, используя оператор print, но возникает вопрос, как передать данные обратно в bash.

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

awk и eval

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

1. Организуем цикл, в котором будем производить обработку данных:

IFS_=$IFS
IFS=$'\n'
for TMPSTRING in $(cat "demotable.txt")
do
#тут будет код
done
IFS=$IFS_

Перед циклом я подправил переменную $IFS содержащую глобальные разделители, в нее, в частности, «смотрят» операторы циклов, чтобы определить, где начинается следующий элемент. По умолчанию переменная $IFS содержит пробел, табуляцию и перевод строки, но поскольку у нас есть данные с пробелом, то это не подходит, цикл будет работать неверно. Потому сохраняем старое значение во временную переменную, устанавливаем новое значение в перевод строки (\n). После цикла возвращаем значение на место.
В цикле организуем разбор данных:

echo "$TMPSTRING"|awk -F "," '{print "LOGIN=" $1; print "FULLNAME=" $2
print "PHONE=" $3; print "ROOM=" $4; print "WORKTIME=" $5 }'

Если запустить скрипт сейчас, то он выведет следующее:

LOGIN=verb666
FULLNAME=Misha Verbitsky
PHONE=+415314499922
ROOM=42
WORKTIME=11:00-16:00

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

./awk2vars01: line 8: Verbitsky: command not found

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

awk print и вывод кавычки

Кавычки для оператора print awk являются служебными символами, в двойные кавычки берутся строковые литералы, т.е. те строки, которые нужно вывести без изменений, как например, "LOGIN=" в коде выше, а в одинарные — вся программа awk. Экранирование (\" или \') в операторе print приведет к ошибке.

Решение — завести внутреннюю переменную awk, содержащую кавычку, и печатать ее в нужном месте:

echo "$TMPSTRING"|sed -e 's~'\''~~g'|awk -F "," '{print "LOGIN=" sq $1 sq; 
    print "FULLNAME=" sq $2 sq; print "PHONE=" sq $3 sq; print"ROOM=" sq $4 sq;
    print "WORKTIME=" sq $5 sq}' sq="'"

Поскольку данные строки далее будут переданы в eval и обработаны как команды оболочки, то необходимо позаботиться о безопасности, и использовать только одинарные кавычки, а также удалять одинарные кавычки из входных строк, при передаче их awk:

Об опасности использования eval в bash-скриптах. Копия

Вывод:

LOGIN='verb666'
FULLNAME='Misha Verbitsky'
PHONE='+415314499922'
ROOM='42'

Теперь можно обернуть все это в eval, чтобы раскидать результат работы awk по переменным.

eval $(echo "$TMPSTRING"|sed -e 's~'\''~~g'|awk -F "," '{print "LOGIN=" sq $1 sq; 
    print "FULLNAME=" sq $2 sq; print "PHONE=" sq $3 sq;
    print"ROOM=" sq $4 sq;
    print "WORKTIME=" sq $5 sq}' sq="'")

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

echo "Login: $LOGIN"
echo "Full name: $FULLNAME"
echo "Phone: $PHONE"
echo "Room: $ROOM"
echo "Work time: $WORKTIME"

Вывод:

Login: verb666
Full name: Misha Verbitsky
Phone: +415314499922
Room: 42
Work time: 11:00-16:00
...

Скрипты полностью можно посмотреть на GitHub

Немного об оптимизации

Получение строки с определенным номером из файла в Linux

С помощью sed

sed -n 5p /path/to/file

Получить 5 строку из текстового файла с путем /path/to/file

С помощью awk

awk 'NR == 5' /path/to/file

На мой взгляд sed‘ом несколько проще, потому что не нужны лишние движения для подстановки переменных shell/bash-скрипта:

TEXTFILE="/etc/group"
STRNO=5

TMPSTRING=`sed -n "$STRNO"p "$TEXTFILE"`

Подключение Riseup VPN в Windows XP

Приходится кое-где сидеть на старой машине с XP (хорошо, не с 2000), а безопасно посидеть в интернете хочется. Так что от склероза, и мало ли кому надо будет. Аналогичным образом можно подключить на XP и VPN от calyx.net

Подготовка

1. Необходимо установить OpenVPN для Windows XP:

Скачать с официального сайта:

Версия x86 (32-разрядная)
Версия x64 (64-разрядная)

Копии, на случай, если с официального сайта пропадет:

Версия x86 (32-разрядная)
Версия x64 (64-разрядная)

2. Далее, качаем конфиги:

Для Riseup:
C Mega.nz
C Google.Drive

Для Calyx:
C Mega.nz
C Google.Drive

3. Конфиги необходимо распаковать в каталог для файлов конфигурации OpenVPN (C:\Program Files\OpenVPN\config)

Получение ключей и сертификатов

Получение ключей для VPN сделано обычным BAT-файлом, без проверок fingerprint’а сертификата провайдера, а в wget, которым все и выкачивается, указан ключ --no-check-certificate, так что все это дело не сильно секурно.

1. Скачиваем архив с GitHub
На всякий случай копия на Google.Drive и Mega.nz

2. Распаковываем архив в отдельный каталог, пусть будет C:\GetKeys
3. Запускаем командную строку (Пуск —> Выполнить и вводим cmd)
4. Переходим в каталог:

С:
cd \GetKeys

5. Запускаем получение ключей.
Для Riseup:

getkeys.bat riseup.net

Для Calyx, соответственно:

getkeys.bat calyx.net

После того, как батник отработает, в C:\GetKeys образуется подкаталог data с подкаталогами провайдеров. Из C:\GetKeys\data\riseup.net надо скопировать в C:\Program Files\OpenVPN\config файлы cacert.pem и openvpn.pem для Riseup VPN, а для Calyx из C:\GetKeys\data\calyx.net те же файлы, переименовав их, соответственно как cacert_calyx.pem и openvpn_calyx.pem. Или можете отредактировать конфиг calyx-net.ovpn и задать там любые желательные имена.

Теперь можно запускать OpenVPN GUI, выбирать, щелкнув по иконке в трее, нужный конфиг, и соединяться.

Проверка

До соединения:

Riseup:


Calyx

Получение ключей и сертификатов для Riseup VPN и calyx.net в Windows.

Без использования клиента Riseup или Bitmask для Calyx.

Преамбула

Наскриптил тут утилитку, которая выкачивает ключи для Riseup VPN копия, а заодно уж и для calyx копия под Windows. Изначально писалось для XP, т.к. оказалось, что официальный клиент Riseup в XP не работает, а Calyx вообще использует Bitmask, которого для винды не предвидится.

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

Так что получилась такая утилита, чтобы подключаться к Riseup и Calyx не качая официального клиента. Пусть уж будет.

Порядок действий

1. Ставим OpenVPN
2. Качаем виндоконфиги:
Для Riseup:
C Mega.nz
C Google.Drive
Для Calyx:
C Mega.nz
C Google.Drive
3. Распаковываем конфиги в каталог конфигов OpenVPN (Обычно C:\Users\<имя_пользователя>\OpenVPN\config\).
4. Качаем утилиту отсюда и распаковываем в отдельный каталог. Она портабельная, никакой установки не надо.
5. Запускаем.
6. Выбираем в выпадающем списке провайдера.
7. Жмем на кнопку «Получить ключ пользователя» Если данных и сертификата провайдера нет, то программа сначала получит данные провайдера, а потом пользователя. Если данные провайдера есть, то обновится только пользовательский ключ.
Если в дальнейшем надо будет принудительно обновить данные провайдера, жмем кнопку «Обновить данные провайдера«

Примечание: Если надо добавить нового провайдера, то дописываем адрес без http://, https:// и www в файл providers.txt в подкаталоге data, по адресу на строку.

Должно получиться как на скриншоте:

8. Жмем кнопку «Сохранить ключи…» и сохраняем их в каталог с конфигами OpenVPN. Имена ключей для Riseup оставляем без изменений, а для Calyx сохраняем как cacert_calyx.pem и openvpn_calyx.pem. Или можете отредактировать конфиг calyx-net.ovpn и задать там любые желательные имена.

Репозиторий на GitHub

Полезные фишки в BAT/CMD файлах.

Выход из BAT-файла

exit /B

Ключ /B обязателен! Если использовать просто exit — закроется командный интерпретатор.

Проверка, пустая ли переменная

Можно использовать для проверки параметров в командной строке.

if "%~1"=="" (
	echo Use %0 ^<address^>
	echo Address must be without http://, https:// or www
	echo e.g. riseup.net
	pause
	exit /B
)

Запись вывода программы в переменную

Аналог VAR=`program` в bash.

for /f %%i in ('program') do set "API_URI=%%i"& goto f1
:f1

Пример:

for /f %%i in ('bin\jq .api_uri %WORKDIR%/provider.json') do set "API_URI=%%i"& goto f1
:f1

Источник

SetLocal и расширенная обработка команд

Статья на Киберфоруме Копия в PDF

Экранирование спецсимволов

В BAT/CMD файле некоторые символы (перенаправления > и <, конвейера |, символ &, указание переменной %) считаются специальными. Если символ должен быть включен в команду как символ, могут быть глюки. Перед символом нужно указать символ экранирования: ^ (крышку).

Пример:

echo Use batfile.bat ^<address^>

(выведет на экран Use batfile.bat <address>)

Источник Копия в PDF Там есть и другие полезные штуковины.

Удаление определенного символа из строки.

Например, надо удалить из переменной %VAR% все кавычки:

set VAR=%VAR:"=%

Источник Копия в PDF В источнике есть и другие примеры работы со строками.

C#, HttpWebRequest: использование самоподписанного (self-signed) сертификата.

Преамбула

Если попытаться соединиться с HTTPS-сервером, имеющим самоподписанный сертификат, то HttpWebRequest сгенерирует исключение WebException Базовое соединение закрыто: Не удалось установить доверительные отношения для защищенного канала SSL/TLS. (The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.). Оно и понятно, HttpWebRequest не смог проверить сертификат узла и закономерно нас послал.

Решение заключается в переопределении глобального обработчика System.Net.ServicePointManager.ServerCertificateValidationCallback.

Внимание! Изменение ServerCertificateValidationCallback повлияет на все соединения в программе, пока обработчик не будет установлен по умолчанию, надо не забывать это, и возвращать обработчик к стандартному значению, после того, как работа с сервером, имеющим самоподписанный сертификат, завершена.

Предположим, есть класс-обертка над HttpWebRequest, надо его дополнить так, чтоб можно было работать с серверами с self-signed сертификатами.

Игнорирование неправильных сертификатов

Самый простой способ, но не самый безопасный. ServerCertificateValidationCallback передается делегат, который всегда возвращает true. Добавляем в класс функцию:

public void EnableIgnoreCertError()
{
    ServicePointManager.ServerCertificateValidationCallback =
        delegate { return true; };
}

И функцию, которая возвращает ServerCertificateValidationCallback к значению по умолчанию:

public void DisableIgnoreCertError()
{
    ServicePointManager.ServerCertificateValidationCallback = 
        null;
}

Начали работать с сервером — вызвали первую, закончили — вызвали вторую.

Самостоятельная проверка сертификата

Немного более сложный, но более безопасный способ. Необходимо заранее либо иметь сам сертификат, либо знать его хэш. В следующем примере будем проверять как раз хэш SHA1 сертификата.
1. Подключим в References’ах System.Security.Cryptography.
2. Подключим необходимые пространства имен:

using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

3. Заведем в классе поле, куда из вызывающей программы будем передавать заранее известный хэш сертификата:

public string CertHashString { get; set; }

4. Заведем две функции, для включения и выключения самостоятельной проверки сертификата:

public void EnableValidateCert()
{
    ServicePointManager.ServerCertificateValidationCallback =
        ValidateCert;
    
}

public void DisableValidateCert()
{
    ServicePointManager.ServerCertificateValidationCallback =
        null;
}

В первой функции вместо делегата, возвращающего true, указываем нашу функцию проверки.

Функция проверки сертификата

Функция проверки сертификата обязательно должна иметь следующий заголовок:
bool FunctionName (object, X509Certificate, X509Chain, SslPolicyErrors), т.е., например:

private bool ValidateCert(object sender, X509Certificate cert,
    X509Chain chain, SslPolicyErrors sslPolicyErrors)

В функции:
1. Проверяем наличие ошибок SSL, если их нет, значит сертификат уже был опознан:

if (sslPolicyErrors == SslPolicyErrors.None)
{
    return true;
}

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

if (string.IsNullOrEmpty(CertHashString))
{
    return false;
}

3. Получаем хэш сертификата сервера в виде строки:

string hashstring = cert.GetCertHashString();

4. Если хэши полученного и известного сертификата совпадают — все ок, возвращаем true, иначе false.

if (hashstring == CertHashString)
{
    return true;
}

return false;

Функция целиком:

private bool ValidateCert(object sender, X509Certificate cert,
    X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
    //все и так хорошо
    if (sslPolicyErrors == SslPolicyErrors.None)
    {
        return true;
    }

    //не передан хэш сертификата
    //значит и проверять нечего
    if (string.IsNullOrEmpty(CertHashString))
    {
        return false;
    }

    //получаем хэш сертификата сервера в виде строки
    string hashstring = cert.GetCertHashString();

    //если хэши полученного и известного 
    //сертификата совпадают - все ок.
    if (hashstring == CertHashString)
    {
        return true;
    }

    return false;
}

Класс целиком на PasteBin
На GitHub

Источник

C#, получение хэша (fingerprint) сертификата X509.

Преамбула

Понадобилось проверить сертификат X509 на целостность и подлинность. Поставщик сертификата отдельно передает его fingerprint и алгоритм хэширования по которому тот вычисляется.

Судя по документации от MS, получить fingerprint, он же хэш, можно функцией GetCertHashString() (или GetCertHash() в виде массива байт) класса X509Certificate2, однако, в .NET 2.0 отсутствует перегрузка функции GetCertHashString(HashAlgorithmName), которая позволяет выбрать алгоритм хеширования (GetCertHashString() возвращает хэш SHA1). Потому сделаем руками, благо ничего сложного в этом нет.

Получение fingerprint

1. Подключаем в References System.Security.Cryptography
2. Подключаем необходимые пространства имен в классе:

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

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

if (!File.Exists(CertFile))
{
    Console.WriteLine("File not found.");
    return null;
}

4. Загружаем сертификат (создаем экземпляр класса X509Certificate2). Если вдруг файл сертификата поврежден, то конструктор X509Certificate2 вызовет Exception Требуемый объект не найден. Поэтому конструктор надо обернуть в try...catch:

try
{
    X509 = new X509Certificate2(CertFile);
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
    return null;
}

5. Получаем загруженный сертификат в виде массива байт:

byte[] cert = X509.GetRawCertData();

6. Создаем объект HashAlgorithm, который и займется вычислением хэша/fingerprint’а. Я завел в функции входную переменную AlgName, куда записывается строка с названием алгоритма, что бы иметь возможность выбора алгоритма хэширования если что:

HashAlgorithm alg = null;
switch (AlgName.ToUpperInvariant())
{
    case "MD5": alg = MD5.Create(); break;
    case "SHA1": alg = SHA1.Create(); break;
    case "SHA256": alg = SHA256.Create(); break;
    case "SHA384": alg = SHA384.Create(); break;
    case "SHA512": alg = SHA512.Create(); break;
    default:
        {
            Console.WriteLine("Unknow algorithm.");
            return null;
        }
}

7. Получаем хэш в виде массива байт:

byte[] hash = alg.ComputeHash(cert);

8. Преобразуем массив байт в строку шестнадцатеричных чисел:

string hex = BitConverter.ToString(hash).ToLowerInvariant().
    Replace("-", "");

ToLowerInvariant() преобразует буквы в строке, полученной BitConverter в строчные, а Replace("-", "") удаляет разделители (-) между значениями байтов, которые BitConverter вставляет в строку.

Код функции целиком
Тестовый пример на GitHub

C#, парсинг и прочая работа с JSON. В том числе и в .NET 2.0

Преамбула

Понадобилось достать несколько значений из JSON-файла, искал, чем это сделать и нашел просто офигенную библиотеку Json.NET. Это не просто парсер, это целый набор инструментов: парсер, сериализаторы, десериализаторы, конвертеры. Кроме банального вытаскивания значений, можно формировать JSON, конвертировать его в XML и XML в JSON, обращаться к объектам JSON через запрос SelectToken или LINQ. А можно и просто вытащить нужное значение, обращаясь к полям JSON, используя встроенные функции объектов.

Несомненный плюс библиотеки в том, что даже в последней версии она поддерживает .NET Framework 2.0 Правда нельзя будет использовать синтаксис LINQ, но и без него инструментов хватает.

Простой парсинг и извлечение значений

С помощью JObject и JToken

1. Подключаем библиотеку для своего Framework’а через References и в исходнике:

using Newtonsoft.Json.Linq;

2. Загружаем JSON в переменную, например из файла.

3. Парсим JSON в JObject:

JObject JSONObj = JObject.Parse(JSONBuf);

JObject.Parse может вызвать exception, если есть синтаксические ошибки в JSON, так что лучше поместить его в try...catch:

public static JObject ParseJSON(string JSONBuf)
{
    JObject JSONObj = null;
    try
    {
        JSONObj = JObject.Parse(JSONBuf);
    }
    catch (Exception ex)
    {
        Console.WriteLine("ERROR: " + ex.Message);
        return null;
    }
    return JSONObj;
}

Пример реакции на ошибку синтаксиса:

Invalid character after parsing property name. Expected ':' but got: {. Path description.languages', line 15, position 9.

4. Далее, можно вытащить значение поля JSON в объект JToken помощью функции JSONObj.GetValue(FieldName);, если поле со значением находится сразу в корне JSON, как, например, api_uri в этом примере, то JToken можно сразу преобразовать в строку с помощью функции .ToString:

JToken tok = JSONObj.GetValue("api_uri");
tok.ToString();

Однако, если поле отсутствует, то это вызовет exception:

В экземпляре объекта не задана ссылка на объект.

Этого можно избежать, отловив исключение в try...catch, либо проверив наличие поля функцией JObject.ContainsKey("имя_поля") перед вызовом GetValue, либо воспользовавшись функцией TryGetValue вместо GetValue

Примеры кода на PasteBin
Тестовый проект

Если же нужное значение находится где-то глубоко в недрах JSON, то необходимо воспользоваться функцией JObject.SelectToken() Пример в официальной документации Копия

Что дальше?

Как я уже говорил, инструмент очень мощный, так что лучше начать с изучения официальной документации

Скачать

Json.NET с официального сайта
Копия библиотеки v 120r3