{% youtube src="https://www.youtube.com/watch?v=CKAah_DsCJ8" %}{% endyoutube %}
В прошлый раз мы остановились на том, что создали файл и в скрипте добавили возможность брать данные о пользователе и группе из этого файла. Теперь же попробуем в файле указать несколько пользователей:
sudo nano /var/users
и добавить их разом. Выполнив ту же команду cut:
cut -d' ' -f1 /var/users
мы увидим весь список пользователей. Если передать команде useradd такой список пользователей, она этого не поймёт – useradd может добавлять только по одному пользователю за раз. А значит нам нужно будет для каждой строчки запускать отдельный useradd.
И так, задача у нас такая – для каждой строчки в файле /var/users создавать группу, проверять sudoers и создавать пользователя. То есть всё что ниже 30 строчки:
tail -n +30 myscript
Если речь про повторное запускание одной и той же команды – то речь обычно о циклах. Есть две стандартные команды для работы с циклами – for и while. for обычно связан со списком, а while – с условием, хотя нередко можно использовать и ту, и другую. Давайте начнём с for. Синтаксис такой:
for переменная in список значений
do команды
done
При запуске команды переменная получит первое значение из списка значений, потом выполнятся все команды, а после команд переменная получит второе значение из списка значений и опять выполнятся все команды. И так будет повторяться до тех пор, пока не закончатся все значения в списке, после чего цикл завершится. Каждое повторение называется итерацией.
Давайте посмотрим пример:
nano for
for number in 1 two "line № 3"
do echo This is $number
done
chmod +x for
./for
Как видите, сначала переменная number получила первое значение – 1, выполнилась команда echo. Потом переменная number взяла второе значение – two. Ну и так далее. Вроде ничего сложного.
Список значений можно задать по разному, например, взять его из вывода команды:
nano for
for line in $(cat /var/users)
do echo In this line: $line
done
sudo ./for
Но вместо того, чтобы увидеть в виде значения каждую строчку, мы видим пользователя и группу на разных строчках. То есть цикл сначала присвоил переменной line в виде значения имя первого юзера, а после итерации значение переменной стало имя группы. И так с каждой строчкой. То есть, вместо того, чтобы разделять значения построчно, значения разделялись по пробелам.
Помните, мы в команде cut использовали опцию -d – разделитель:
sudo cut -d' ' -f1 /var/users
И мы этой опцией указали, что разделителем является пробел. bash, чтобы взять список значений, тоже использует разделитель – сначала он попытается разделить значения по пробелу, потом по табуляции и только потом по переводу строки. А чтобы bash в качестве разделителя использовал сразу перевод строки, нам нужно об этом ему сказать. Для этого есть переменная IFS – внутренний разделитель полей.
Чтобы указать, что мы хотим в качестве разделителя использовать перевод строки, даём переменной такое значение:
IFS=$'\n'
\n – это newline. Если захотим знак табуляции меняем n на t:
IFS=$'\t'
Если брать, например, /etc/passwd, то там разделителем выступает двоеточие, тогда можно указать так:
IFS=:
Но с этой переменной нужно быть осторожным – другие команды в скрипте также могут использовать эту переменную, а значит то что у вас работало с пробелами, может начать работать с новыми строками. И чтобы не пришлось переделывать пол скрипта, мы можем поменять эту переменную временно, а потом вернуть старое значение. Но для этого нужно старое значение предварительно сохранить:
oldIFS=$IFS
выполнения нужных команд можем восстановить старое значение:
IFS=$oldIFS
Но нам сейчас нужен newline:
IFS=$'\n'
Попробуем запустить скрипт:
sudo ./for
Теперь всё окей – при каждой итерации переменная будет получать в качестве значения целую строчку.
Дальше нужно просто из этой переменной достать имя пользователя и группы, допустим, с помощью того же cut. А чтобы передать команде cut значение переменной, можно использовать пайп:
echo $line | cut -d' ' -f1
В итоге мы достанем из строчки имя пользователя. И чтобы это имя стало переменной, напишем так:
user=$(echo $line | cut -d' ' -f1)
Тоже самое с группой:
group=$(echo $line | cut -d' ' -f2)
Последний штрих:
echo Username: $user Group: $group
sudo ./for
Как видите, всё сработало как надо. Теперь попытаемся сделать тоже самое с нашим скриптом.
Предварительно сохраним значение переменной IFS:
oldIFS=$IFS
Тут у нас уже есть строчки назначения переменных из файла, но это нам не подходит, потому что нам нужно брать переменные в цикле, поэтому эти строчки убираем. Попробуем вписать сюда наш цикл:
IFS=$'\n'
for line in $(cat $file)
do
user=$(echo $line | cut -d' ' -f1)
group=$(echo line | cut -d' ' -f2)
echo Username: $user Group: $group
done
IFS=$oldIFS
Запустим и проверим:
sudo ./myscript
tail -5 /etc/passwd
У нас там было несколько пользователей, а создался только последний. Подумайте, почему так случилось?
Обратите внимание на наш цикл – переменные получают свои значения, выполняется команда echo, а после итерации всё происходит заново, пока не дойдёт до последнего значения. И только после этого начинают выполняться все остальные команды – группы, sudoers и т.д. Нам же нужно, чтобы с каждой итерацией выполнялись все эти команды.
Что мне мешает поставить done в конце скрипта? Мы сейчас находимся в условии – я не могу просто посреди for закончить условие if и продолжить выполнять команды for. Команда началась внутри условия – там же она должна закончится. Есть вариант скопировать все оставшиеся команды сюда. Но это плохой вариант – это увеличит размер скрипта, в дальнейшем придётся редактировать команды и внутри цикла, и отдельно.
Вот у нас есть куча команд и я не хочу, чтобы они повторялись в скрипте в нескольких местах. Чтобы решить эту проблему, я могу объединить все эти команды под одним названием. Для этого я пишу название, допустим: create_user() - ставлю после названия скобки, а потом внутри фигурных скобок указываю все нужные команды:
create_user() {
groupadd …
}
Это называется функцией. И в дальнейшем, когда я захочу запустить все эти команды, я просто запущу команду:
create_user
Но функция должна быть задана до того, как к ней обращаются, поэтому переместим нашу функцию наверх, скажем, после переменных. Но тут ещё один нюанс – IFS возвращает старое значение:
IFS=$oldIFS
после выполнения цикла, а значит после выполнения всех команд в функции. А так как все наши команды там, то лучше перенести эту команду:
IFS=$oldIFS
в начало функции.
А теперь пропишем её в наших условиях – просто написав create_user в командах каждого из условий.
Хорошо, давайте пройдёмся по скрипту. Вначале мы проверяем на root права. Задаём переменные. Создаём функцию – create_user – тут у нас все нужные команды для создания группы и пользователя. А в конце у нас проверка, как мы запускали программу – с параметрами, с файлом или интерактивно – в зависимости от этого назначаются переменные и запускается функция.
Окей, давайте протестируем:
sudo ./myscript
tail -5 /etc/passwd
Как видите, все пользователи создались, всё работает.
Теперь немного проработаем наше интерактивное меню, то есть опцию else. Сейчас, при запуске скрипта, в этом режиме оно запросит юзернейм, пароль, создаст пользователя и закроется. Я бы хотел, чтобы после создания пользователя наш скрипт не завершался, а предлагал заново создать пользователя и всякие другие менюшки. Для этого мне понадобятся две команды. Первая будет показывать меню – это команда select. Синтаксис чем-то похож на for:
select переменная in список значений
do команды
done
Например:
nano select
select number in 1 2 3
do echo This is number: $number
done
chmod+x select
./select
select показал нам меню, где с помощью цифр мы можем выбрать какое-то из значений и переменная получит это значение. Дальше выполнится команда и после неё опять появится меню.
Теперь мне нужна команда, которая будет запускать какие-то команды в зависимости от значения переменной. Речь про команду case. Синтаксис такой:
case $переменная in
значение 1) команды;;
значение 2) команды ;;
*) команды, если значения нет в списке ;;
esac
Например:
nano case
number=one
case $number in
one) echo 1;;
two) echo 2;;
*) echo something wrong ;;
esac
chmod +x case
./case
Как видите, значение переменной было one. Оно подошло под первую опцию, в следствии чего сработала первая команда.
Теперь объединим select и case. Например:
select number in 1 2 3
do
case $number in
1) echo One;;
2) echo Two;;
3) echo Three;;
*) echo something wrong ;;
esac ;
done
./case
Теперь, select предлагает нам выбрать одно из значений, это значение назначается переменной number, затем case, в зависимости от значения переменной, запускает соответствующую команду.
А чтобы не застрять в бесконечной петле, в списке опций пропишем что-нибудь типа stop, а в case используем команду:
break
чтобы прекратить цикл. После break цикл прерывается и начинают выполняться другие команды после цикла:
./case
Теперь добавим это в нашем скрипте. Допустим, сделаем так, чтобы можно было добавить пользователя, посмотреть текущих пользователей, либо выйти:
select option in "Add user" "Show users" "Exit"
do
case $option in
"Add user") read -p … ;;
"Show users") cut -d: -f1 /etc/passwd ;;
"Exit") break ;;
*) echo Wrong option ;;
esac ;;
done
Сохраним, удалим файл:
sudo rm /var/users
чтобы наш скрипт предложил интерактивное меню и попробуем запустить скрипт:
sudo ./myscript
Выбираем опцию 1 – у нас выходит приглашение ввести имя пользователя и группу. Окей, нажимаем enter – меню появилось ещё раз. Теперь выбираем 2 – и видим список всех пользователей. Выбираем 3 и выходим.
Подводя итоги, сегодня мы с вами разобрали команду for, с помощью которой мы можем создавать циклы; переменную IFS; функции, с помощью которых можем вызывать одну или несколько заранее прописанных команд; команду select, с помощью которой мы можем создать интерактивное меню; и команду case, с помощью которой мы можем запускать команды в зависимости от значения переменной.