*nix scripting. Пишем user-friendly скрипты. Все началось довольно обычно. Как-то спонтанно возникло желание установить Slackware на домашнюю машинку. Ее установка немного отличается от того к чему привыкли юзеры Ubuntu, Suse или Fedora, а чем-то больше похоже на установку FreeBSD. Все действия по предварительной настройке системы выполняются из консоли, но при этом используются специальные диалоговые окна на псевдографике. При выборе пакетов для свежеустановленной слаки, была обнаружена утилита dialog, в описании которой было сказано, что все менюшки и дилоги, которые рисуются при установке системы, написаны именно на ней и естественно возникло желание попробовать прикрутить их к лабам по юниксу, благо интеграция dialog в шелл скрипты — проще некуда. 1.1 Установка При использовании пакетных менеджеров установка довольно проста, поэтому здесь будет описана наиболее «хардкорная» версия — установка из сорцов =) Для начала нам понадобится dev-версия библиотеки ncurses с хедерами функций, которые используются в dialog. Грубо говоря, ncurses — это набор функций, предназначенных для консольного ввода-вывода. За подробностями — в гугл. В RPM-дистрибутивах пакет называется ncurses-devel, в deb-дистрибутивах — ncurses-dev, их так же легко установить пакетными менегерами, но мы не ищем легких путей =), поэтому: 1.1.1 Ставим ncurses 1) создаем в своем хомяке временную папку cd ~ mkdir tmp cd ~/tmp 2) качаем в нее ncurses wget ftp://invisible-island.net/ncurses/ncurses-5.7.tar.gz 3) распаковываем tar -xvzf ./ncurses-5.7.tar.gz cd ./ncurses-5.7 Дальше стандартно ./configure --prefix=[куда хотим поставить] ./make ./make install Если чето не получилось — курим консольный вывод компилятора до просветления. 1.1.2 Ставим собственно dialog 4) качаем в ~/tmp cd ~/tmp wget ftp://ftp.us.debian.org/debian/pool/main/d/dialog/dialog_1.120080819.orig.tar.gz 5) распаковываем tar -xvzf ./dialog_1.1-20080819.orig.tar.gz 6) удаляем уже не нужные архивы (хотя можно и оставить) rm -rf ./dialog_1.1-20080819.orig.tar.gz ./ncurses-5.7.tar.gz cd ./dialog-1.1-20080819/ и снова configure && make && make install , не забывая в configure указать путь установки. Итак, все установлено, начнем издевательства над dialog-ом =) 1.2 HelloWorld Изучение любой вещи в программировании (хоть тут и не совсем программирование) начинается с хеловорлда. Напишем диалоговое окошко, которое выводит эту заветную фразу и пару кнопок, yes и no. #!/bin/bash dialog --title "hello world messagebox" --clear \ --yesno "Hello world!" 15 60 case $? in 0) 1) 255) esac echo "Yes pressed";; echo "No pressed";; echo "ESC pressed";; Вроде всё просто. Вызываем dialog и по коду возврата определяем, что произошло (нажаты кнопки yes/no или esc), в соответствии с этим выводим на консоль сообщение. Опция --title определяет заголовок диалога, --yesno его тип (в данном случае это чтото типа традиционного messagebox) и --clear указывающая, что перед отрисовкой диалогового окошка неплохо было бы почистить экран, 15 60 — размер диалогового окошка в символах. Единственное что не очень приятно и бросается в глаза — то что скрипт завершается сразу после нажатия на одну из трех кнопок, что бы этого избежать, можно немного его переделать: #!/bin/bash ret_val=0 ${DIALOG=dialog} main(){ $DIALOG --title "hello world messagebox" --clear \ # заменяем стандартный текст кнопок на свой --yes-label "Ага" --no-label "Не" \ --yesno "Даров, мир" 15 60 \ ret_val=$? } # Крутимся в цикле пока кто-то не нажмет кнопку "no". # если нажата любая другая кнопка кроме "no" — выводим # окно и опять ждем кода возврата while [ $ret_val -ne 1 ] do main done; Тут стоит упомянуть, что утилита, подобная dialog, есть и под Иксы — Xdialog, причем полностью совместимая с dialog по своим опциям, поэтому неплохо было бы добавить в начало нашего скрипта такую строку: ${DIALOG=dialog} и в функции main заменить вызов dialog на $DIALOG. Тогда если мы захотим погонять этот скрипт под иксами, то надо будет поменять всего-лишь одну букву: ${DIALOG=Xdialog} Можно еще немного усложнить этот мега-скрипт. Добавим строку ввода aka editbox на основной диалог и, в зависимости от него будем выдавать соответствующее сообщение с приветствием. #!/bin/bash ${DIALOG=dialog} tempfile=`tempfile 2>/dev/null` || tempfile=/tmp/test$$ trap "rm -f $tempfile" 0 1 2 5 15 $DIALOG --title "INPUT BOX" --clear \ --inputbox "Hi, pls enter your name" 10 40 2> $tempfile retval=$? case $retval in 0) $DIALOG --clear \ --title "Greetings from planet Mars" \ --backtitle "We came with peace!" \ --msgbox "Hello, human `cat $tempfile`" 7 40 ;; 1) echo "Cancel pressed.";; 255) echo "ESC pressed." ;; esac Тут вывод dialog перенаправляется во временный файл, созданный утилитой tempfile. (вместо нее можно создавать этот файл вручную или использовать утилитку mktemp), а далее строка из этого файла используется в msgbox. После всех махинаций получится что-то типа такого: Кроме того, можно модифицировать скрипт так, чтобы он сам определял, в иксах он или в консоли, и в зависимости от этого запускал соответственно Xdialog или dialog. Пример простого тестового скрипта с кривым скриншотом для иксов чуть ниже (как его "выпрямить" пока не знаю): #если в переменной DISPLAY пустая строка, то мы в консоли if [ -z $DISPLAY ] then DIALOG=dialog else #иначе мы в иксах DIALOG=Xdialog fi $DIALOG --yesno "test" 0 0 Вроде прикольно, но не сильно ясно, где это можно применить на практике (впрочем, это проблема всех hello-world приложений =) ) Поэтому следующий пример будет более практичным, однако по сути таким же бесполезным. 1.3 Фронт-энд для утилиты useradd #!/bin/bash # # # # # # # -------------------------------------------------------------------------"THE BEER-WARE LICENSE": <[email protected]> wrote this file. As long as you retain this notice you can do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer. -------------------------------------------------------------------------- # # # # # # # # # # # # склерозник по useradd -c name & surname -G группы -s shell -p password -d home directory -m create home dir -U создает группу с тем же именем что и у пользователя -e expire date YYYY-MM-DD -f число дней после устаревания пароля, до полной блокировки аккаунта 0 учётная запись блокируется сразу после устаревания пароля -1 данная возможность не используется declare -a A_SHELLS; declare -a A_USERS; declare -a A_GROUPS; ${DIALOG=dialog} # radiolist example generateShellsDialog() { SHELLS_DLG=./getShell.sh exec 3>&1 exec > $SHELLS_DLG echo "#!/bin/bash" echo "$DIALOG \\" echo "--title \"SHELLS LIST\" \\" echo "--radiolist \"Check login shell you need \" 20 60 15 \\" for shell in ${A_SHELLS[@]} do echo "\"$shell\" \"\" off \\" done echo "2>./~shell.tmp" echo "retval=\$?" echo echo echo echo echo echo echo echo } "USER_SHELL=\`cat ./~shell.tmp\`" "case \$retval in" "1)" "echo \"Cancel pressed.\"" "exit;;" "255)" "echo \"ESC pressed.\";;" "esac" exec 1>&3 3>&chmod +x $SHELLS_DLG # checklist example, almost the same as radiolist generateGroupsDialog() { GROUPS_DLG=./getGroups.sh exec 3>&1 exec > $GROUPS_DLG echo "#!/bin/bash" echo "$DIALOG \\" echo "--title \"GROUPS LIST\" \\" echo "--checklist \"Check groups you need \" 20 60 15 \\" for group in ${A_GROUPS[@]} do echo "$group \"\" off \\" done echo "2>./~groups.tmp" echo "retval=\$?" echo echo echo echo "USER_GROUPS=\`cat ./~groups.tmp\`" "case \$retval in" "1)" "echo \"Cancel pressed.\"" echo echo echo echo "exit;;" "255)" "echo \"ESC pressed.\";;" "esac" exec 1>&3 3>&chmod +x $GROUPS_DLG } # I do not know where to get list of installed shells # so i took it from /etc/shells. If you read this, and you know where to # find such list (on ALL Linux systems) pls mail me - I'll buy you a beer =) getShellsList() { if [ -z $1 ]; then echo "getShellsList() requers one parameter - filename\n"; return -1; else if [ -f $1 ]; then trap "rm -rf ./tmp" 0 1 2 5 15 sed -e '/^#.*/d' $1 > "./tmp"; #Remove all strings, starting with '#' A_SHELLS=( `cat "./tmp"`); generateShellsDialog; else echo "file $1 does'n exist, or you don't have rights to open it\n"; return -1; fi fi } # usually the shells list in POSIX OS is in /etc/passwd & in /etc/shadow # but to make application more flexible we'll pass the filename to function getUsersList() { if [ -z $1 ]; then echo "getUsersList() requers one parameter - filename\n"; return -1; else if [ -f $1 ]; then A_USERS=( `cat $1 | sort | cut -d":" -f1`); #get only users names else echo "file $1 does'n exist, or you don't have rights to open it\n"; return -1; fi fi } # usually... oh I think You know it already =); see /etc/group getGroupsList() { if [ -z $1 ]; then echo "getGroupsList() requers one parameter - filename\n"; return -1; else if [ -f $1 ]; then A_GROUPS=( `cat $1 | sort | cut -d":" -f1`); #get only groups names generateGroupsDialog else echo "file $1 does'n exist, or you don't have rights to open it\n"; return -1; fi fi } cleanup() { rm -rf rm -rf rm -rf rm -rf clear } ./~groups.tmp ./getGroups.sh ./getShell.sh ./tmp addUser() { EXPIRE_DAYS=0 sed -e 's/\"//g' ./~groups.tmp > ./groups.tmp USER_GROUPS=`sed -e 's/\s/,/g' ./groups.tmp` rm -rf ./groups.tmp useradd -c "$USER_NAME" -p "$USER_PASSWORD" \ -d /home/$USER_LOGIN -m -s $USER_SHELL \ -e $EXPIRE_DATE -f $EXPIRE_DAYS \ -G $USER_GROUPS $USER_LOGIN chown $USER_LOGIN /home/$USER_LOGIN cleanup } # calendar example getExpireDate() { cur_date=`date +%D | sed -e 'y/\// /'` exec 3>&1 EXPIRE_DATE=`$DIALOG --title "CALENDAR" --calendar\ "Please choose a password expire date..." 0 0 $cur_date 2>&1 1>&3` code=$? exec 3>&#convert dd/mm/yyyy (output from dialog) to #YYYY-MM-DD (input to useradd) OLD_IFS="$IFS" IFS="/" EXPIRE_DATE=($EXPIRE_DATE) IFS="$OLD_IFS" local year=${EXPIRE_DATE[2]} local month=${EXPIRE_DATE[1]} local day=${EXPIRE_DATE[0]} EXPIRE_DATE="$year-$month-$day" case $code in 0) addUser ;; 1) echo "Cancel pressed." exit ;; esac } # checklist dialog example getGroups() { ./getGroups.sh USER_GROUPS=`cat ./~groups.tmp` getExpireDate } # radiolist dialog example getLoginShell() { ./getShell.sh USER_SHELL=`cat ./~shell.tmp` rm -rf ./~shell.tmp getGroups } # inputform dialog example getUserName() { exec 3>&1 value=`$DIALOG --ok-label "Submit" \ --form "You can skip entering this data" \ 15 50 0 \ "Title:" 1 1 "$Title" 1 10 20 0 \ "Name:" 2 1 "$Name" 2 10 20 0 \ "Surname:" 3 1 "$Surname" 3 10 20 0 \ "Phone:" 4 1 "$Phone" 4 10 20 0 \ 2>&1 1>&3` returncode=$? exec 3>&USER_NAME=`echo "$value" |sed -e 's/^/ /'` } case $returncode in 0) getLoginShell ;; 1) exit ;; esac # Passwordform dialog example # this func' prints 2 edits and asks user to input password 2 times # if passwords don't match - it will print error message getPassword() { # link descriptor #3 with stdout exec 3>&1 value=`$DIALOG --cancel-label "Exit" \ --ok-label "Submit" \ --insecure \ --passwordform "Enter password and confirm it." \ 10 50 0 \ "Password:" 1 1 "$pass" 1 20 20 0 \ "Confirm password:" 2 1 "$c_pass" 2 20 20 0 \ 2>&1 1>&3` retval=$? # close descriptor exec 3>&- case $retval in 0) passwords=(`echo "$value" |sed -e 's/^/ /'`); } if [ ${passwords[0]} != ${passwords[1]} ];then $DIALOG --title "Error" \ --yesno \ " Passwords do not match \ Would you like to try again?" 0 0 case $? in 0) getPassword ;; 1) exit ;; esac else USER_PASSWORD=`./md5.crypt ${passwords[0]}`; getUserName; fi ;; 1) exit ;; esac # inputbox dialog example # this function prints input box and prompts user to enter login # if login exists - warning message 'll be shown getLogin() { tempfile=`tempfile 2>/dev/null` || tempfile=/tmp/test$$ trap "rm -rf $tempfile" 0 1 2 5 15 $DIALOG --cr-wrap \ --title "INPUT BOX" --clear \ --inputbox \ " It is usually recommended to only use usernames that begin with a lower case letter or an underscore, and are only followed by lower case letters, digits, underscores, dashes, and optionally terminated by a dollar sign. In regular expression terms: [a-z_][a-z0-9_-]*[$]? On Debian, the only constraints are that usernames must neither start with a dash ('-') nor contain a colon (':') or a whitespace (space, end of line, tabulation, etc.). Usernames may only be up to 32 characters long." 0 0 2> $tempfile retval=$?; case $retval in 0) USER_LOGIN=`cat $tempfile`; flag=0; # if user entered login exists in /etc/passwd file # print error message and try to enter it again for u_log in ${A_USERS[@]} do if [ "$USER_LOGIN" == "$u_log" ]; then flag=1; fi done if [ "$flag" == "1" ]; then $DIALOG --title "Error" \ --yesno \ " Login you've entered already exists. \ Would you like to try again?" 0 0 case $? in 0) getLogin ;; 1) exit ;; esac else getPassword fi ;; 1) exit; ;; esac } # ------------------------------------MAIN-----------------------------------main() { if [ $USER != "root" ]; then echo "You must be root, to execute this file!"; exit; else getShellsList "/etc/shells"; getUsersList "/etc/passwd"; getGroupsList "/etc/group"; $DIALOG --title "useradd frontend" \ --yesno "This is frontend to standart *NIX utility \ useradd. Press Yes, if you want to proceed and add \ user or no if you want to quit." 0 0 case $? in 0) getLogin ;; 1) $DIALOG --title "Exit" --clear \ --msgbox "Bye" 6 10 ;; esac fi } # ------------------------------------MAIN-----------------------------------main Вкратце, как это работает. Первым делом, при запуске скрипта, из файлов /etc/shells /etc/passwd и /etc/group забираются данные в глобальные массивы A_SHELLS, A_USERS и A_GROUPS соответственно. При вызовах функций для набора данных в массивы A_SHELLS и A_GROUPS генерируются временные скрипты ./getShell.sh и ./getGroups.sh, которые содержат в себе вызов диалога для выбора шелла и групп. Такой изврат с генерацией скриптов необходим, поскольку изначально неизвестно — сколько и какие группы есть в системе (аналогично с шеллами). Эти скрипты будут вызваны позднее. Дальше выводится предложение ввести логин нового пользователя, и если такой логин еще не существует в системе, то выводится предложение ввести пароль. 2 раза. Если оба введенных пароля совпадают, то выводится предложение ввести информацию о пользователе (ее можно и не вводить). После всего этого вызываются сгенерированные скрипты ./getShell.sh, за ним ./getGroups.sh для выбора шелла и групп, после чего диалог с выбором даты истечения "срока годности" пароля и наконец вызов функции addUser() которая вызывает непосредственно useradd с выбранными параметрами и сменяет владельца домашней директории на нужного. Остальное в коментах. Чуть не забыл, пароль в useradd надо переделать в зашифрованном виде. В мане написано что для этого используется функция crypt() (http://www.opennet.ru/man.shtml? topic=crypt&category=3&russian=0) Для возможности вызова ее из скрипта была написана небольшая обертка md5.crypt.c которая компилируется командой gcc -lcrypt md5.crypt.c -o md5.crypt Ее задача — выводить в stdout результат работы функции crypt() #include <stdio.h> #include <unistd.h> int main (int argc, char* argv[]) { char *result; printf ("%s\n", crypt (argv[1], "$1$")); return (0); } Внизу унылые скриншоты того, что получилось (не всё) 1.4 Вместо послесловия Просьба не спрашивать, что употреблял аффтар при написании данного быдлокода, т.к. аффтар не помнит =). Так же просьба не тыкать мордой в баги, бо они там есть (при вводе паролей, например, можно оставить одно поле пустым). Целью было не написать 100% работающий скрипт, а показать возможности утилиты dialog ну и так вышло, что некоторые фишки интерпретатора bash. Более простые примеры писать было неинтересно, т.к. их можно нагуглить в огромнейшем количестве (правда они все какие-то одинаковые), ну и в самом пакете dialog есть замечательная папка samples, так что если лень разбирать получившийся быдлокод, то можно посмотреть простые примеры там. По поводу коментов в коде на английском (если это можно назвать английским) просто было лень переключать раскладку, все остальное, что вы видите на русском — копипаст откуда-то. И последняя просьба для адептов командной строки — не стоит говорить, что "просто написать useradd [options] LOGIN быстрее в 1000 раз. Зачем заморачиваться с подобной дурнёй". Да быстрее =) Но цель данной "статьи" — показать как использовать утилиту dialog, а не как писать в консоли безумные однострочные скрипты, наподобие perl -e '$??s:;s:s;;$?::s;;=]=>%{<-|}<&|`{;;y; -/:-@[-`{-};`-{/" -;;s;;$_;see' При написании скрипта никто не пострадал был использован ОЧЕНЬ полезный плагин для Eclipse - shelled. Качать тут http://sourceforge.net/projects/shelled. Он занимается подсветкой синтаксиса в шелл файлах, и поддерживает тучу различных форматов скриптов, включая bash, csh, zsh, ksh и так далее.