Об опасности использования eval в bash-скриптах.
За ценное дополнение спасибо wasserstrahl@ljr
Об опасности использования eval в bash-скриптах.
За ценное дополнение спасибо wasserstrahl@ljr
Какой бы ваш скрипт не был диалоговым, «само дерево жужжать не может», а в скрипте обычно жужжат утилиты, которые о диалоговых окнах представления не имеют. Так вот прикрутим контролируемый прогрессбар (--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’а
Понадобилось мне перед запуском скрипта определить, имеется ли в пользовательской системе утилита, которая вызывается в скрипте, и если ее таки нет, сказать юзеру, мол, поставьте, вот ссылка на скачивание.
Оказывается в 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.
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[@]}"
Массивом, на самом деле, пользоваться неудобно, и код получается более громоздкий, и несовместимо это может оказаться с другими 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" \
С помошью общего для 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" \
Для динамического меню из массива это не работает (или я не понял как).
Конечно, виджет --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" \
Эта опция работает и для динамического меню, созданного с помощью массива:
КУРВОСОС — вампир, чьи гастрономические предпочтения склоняются к дамам с низкой социальной ответственностью.
dialog
— консольная утилита, позволяющая встраивать в ваши скрипты псевдографический интерфейс. Есть и ее графические варианты Kdialog, Xdialog, позволяющие обеспечить ваши скрипты даже полноценным оконным интерфейсом в средах X-сервера.
Как пример можно привести скрипт пакетного переименования файлов (см. на GitHub) работающий в графической среде, или пакетный менеджер Slackware pkgtool
.
Утилита древняя, как копролиты динозавра, но почему-то при этом на русском довольно мало примеров в сети и нет хорошей документации, и даже официальные примеры кода не полные, да еще и удалены в некоторых пакетах и их приходится искать отдельно. Слава Ктулху, примеры свободные, потому не грех и перезалить.
Общий синтаксис утилиты 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"
Минус утилиты 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
Вот что рекламируют? Неужто именно то Хуйло-Хуйло?
Долго искал, как выйти из X через консоль, и в мануалах ничего не мог найти. Почему-то это нигде толком не описано. Хотя, я думаю, завершить X на другой машине удаленно, имея под рукой только консоль/удаленный терминал, возникала не у меня одного.
И оказалось, что какого-то единственного универсального способа нет.
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
Неверное, самый универсальный способ завершить работу X-server на лету и через консоль, это переключить runlevel. Команда должна быть выполнена от root
.
— Для Slackware:
init 3
— Для дистрибутивов с systemd:
systemctl isolate runlevel3.target
Вернуться в иксы.
Для Slackware:
init 4
— Для дистрибутивов с systemd:
systemctl isolate graphical.target
— Для 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
Опять же, повторюсь, стопроцентно это сработает только если 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+F2…F6 и вам откроется чистый терминал. На 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
В недавнем примере я допустил, что называется, «детский мат», грубую и далеко неочевидную новичку ошибку, о которой, впрочем, все знают. Но прописные истины неплохо повторять, так что не стоит кидаться на эту заметку с криками «боян!!!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
? Да в том, что эта команда преобразует любую переданную ей строку в команду или команды 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
А заодно и другой лишней ерунды вроде доступа к микрофону или запроса геолокации.
1. Заходим в Инструменты —> Настройки
2. Переходим на вкладку «Приватность и защита»
Или сразу пишем в строке адреса: about:preferences#privacy
3. Мотаем вниз до раздела Разрешения:
4. Жмем кнопку «Параметры» напротив уведомлений, очищаем список, если в нем что-то есть, и ставим галочку Блокировать новые запросы на отправку вам уведомлений
5. Жмем Сохранить изменения
6. ФАНФАРЫ!
Таким же образом можно поступить с местоположением, камерой и микрофоном.
Или как раскидать результат работы 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.
Предположим, что заголовок удален, в файле остались только данные.
В 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
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"`
Герман Grep
(из классификатора «Эсхатологические мутанты»)
Исходники: openal-soft-1.17.1.tar.bz2
SlackBuild: OpenAL.tar.gz
Он, кстати, почему-то отрабатывает не до конца, библиотеку собирает, а готовый пакет нет.
Готовый пакет: openal-soft-1.17.1.txz
базар-квазар
Приходится кое-где сидеть на старой машине с 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:
Без использования клиента 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
и задать там любые желательные имена.
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
Статья на Киберфоруме Копия в PDF
В BAT/CMD файле некоторые символы (перенаправления >
и <
, конвейера |
, символ &
, указание переменной %
) считаются специальными. Если символ должен быть включен в команду как символ, могут быть глюки. Перед символом нужно указать символ экранирования: ^
(крышку).
Пример:
echo Use batfile.bat ^<address^>
(выведет на экран Use batfile.bat <address>
)
Источник Копия в PDF Там есть и другие полезные штуковины.
Например, надо удалить из переменной %VAR%
все кавычки:
set VAR=%VAR:"=%
Источник Копия в PDF В источнике есть и другие примеры работы со строками.
Если попытаться соединиться с 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; }