Простой интерактив в bash-скриптах, часть 2. Списки (меню).
В bash-скрипте можно организовать простое меню, в виде списка, и предоставить пользователю выбор из нескольких вариантов.
Список вариантов можно сформировать вручную, просто перечислив нужные в скрипте. Другой способ - сформировать список динамически, так, как это, например, описано здесь копия. Покажу второй способ.
Сначала возьмем из скрипта по ссылке выше функцию
create_list()
и необходимые переменные:FOUNDLST=""
DIR1="/home/smallwolfie/test/archives"
TARGZ=""
create_list() #$1 - dir, $2 - file mask
{
FOUNDLST=""
for FLE in $(find $1 -maxdepth 1 -iname $2); do
if [ -n "$FLE" ]; then
FOUNDLST="$FOUNDLST"`basename $FLE`"\n"
fi
done
}
#тут будет функция вывода списка
#...
#создаем список
create_list $DIR1 "*.tar.gz"
if [ -z "$FOUNDLST" ]; then
echo "$DIR1 *.tar.gz not found"
exit
else
TARGZ="$FOUNDLST"
fi
Организуем это дело в виде отдельной функции
ask_list()
, куда первым параметром передается список, а вторым - запрос для пользователя.1. Скопируем содержимое первого параметра в переменную
$LIST_BUF
, со списком придется сделать несколько преобразований: LIST_BUF=$1
2. Список у меня в виде строк, разделенных стандартным переносом
\n
, так что заодно добавлю пункт Cancel, чтобы пользователь мог отказаться от выбора.LIST_BUF="$LIST_BUF""Cancel"
3. Оператор
select
работает со списком значений, разделенных пробелами, так что надо символы \n
заменить на пробелы:LIST_BUF=`echo -e "$LIST_BUF"|sed 's/\n/ /'`
4. Устанавливаем подсказку ввода для пользователя, присвоив второй параметр функции специальной переменной
PS3
.PS3=$2
5. Теперь используем оператор
select
:echo
select LIST_RET in $LIST_BUF; do
if [ -n "$LIST_RET" ];then
break
fi
done
На экран будет выведен список, а пользователю будет предложено ввести номер из списка:
smallwolfie@wolfschanze: ~/test/$ ./asklist2
1) slinstall.tar.gz 3) teSt.Tar.gZ 5) Cancel
2) slmini.tar.gz 4) TEST.TAR.GZ
Select file:
Если внутри конструкции
select..done
не поставить break
, то цикл выбора окажется бесконечным. Значение (строка после номера) будет записано в переменную, указанную после ключевого слова
select
.Если пользователь введет номер не из списка, или вообще что-то левое, то переменная
$LIST_RET
окажется пустой. Здесь внутри конструкции
select..done
добавлена конструкция для проверки этого. Если переменная окажется пустой - пользователю будет предложено повторить ввод:smallwolfie@wolfschanze: ~/test/$ ./asklist-select
1) slinstall.tar.gz 3) teSt.Tar.gZ 5) Cancel
2) slmini.tar.gz 4) TEST.TAR.GZ
Select file: blablabla
Select file:
Функция целиком:
ask_list() #$1 - list #$2 - header
{
LIST_BUF=$1
LIST_BUF="$LIST_BUF""Cancel"
LIST_BUF=`echo -e "$LIST_BUF"|sed 's/\n/ /'`
PS3=$2
echo
select LIST_RET in $LIST_BUF; do
if [ -n "$LIST_RET" ];then
break
fi
done
}
Пример вызова функции:
ask_list "$TARGZ" "Select file: "
if [[ "$LIST_RET" == "Cancel" ]]; then
echo "Cancelled by user!"
exit
fi
echo "User select: $LIST_RET"
Результат:
smallwolfie@wolfschanze: ~/test/$ ./asklist-select
1) slinstall.tar.gz 3) teSt.Tar.gZ 5) Cancel
2) slmini.tar.gz 4) TEST.TAR.GZ
Select file: 5
Cancelled by user!
smallwolfie@wolfschanze: ~/test/$ ./asklist-select
1) slinstall.tar.gz 3) teSt.Tar.gZ 5) Cancel
2) slmini.tar.gz 4) TEST.TAR.GZ
Select file: 3
User select: teSt.Tar.gZ
Скрипт на GitHub
Скрипт на PasteBin
Организуем это дело также в виде функции
ask_list()
с аналогичным набором параметров, только оператором select
пользоваться не будем, а сделаем, как в примере с вопросом Да/Нет из предыдущей части копия. Начало и конец скрипта останутся теми же, рассмотрим только видоизмененную функцию ask_list()
.Вместо оператора
select
будем выводить список сами, а для ввода данных воспользуемся оператором read
.Минус способа: данный вариант подходит для списков с небольшим количеством вариантов, от 1 до 9. Ну и да, в таком виде это будет корректно работать только в оболочке bash, ибо есть башизм.
Плюс способа: на экране ничего не меняется, если пользователь ввел неверное значение (т.е. такое меню выглядит симпатичнее, и с экрана никуда не уползет). И заголовок меню будет сверху.
Еще один плюс - можно использовать элементы списка с пробелами.
1. Присваиваем первый параметр функции переменной
LIST_BUF
, затем выводим второй параметр - заголовок меню, и выводим пустую строку, отделяющую заголовок от меню:LIST_BUF=$1
echo "$2"
echo
2. Как я уже говорил, функция
create_list()
формирует список в виде строк с символом переноса строки (\n
) на конце, поэтому последний символ \n
надо отрезать, если он есть, чтобы в дальнейшем пункт Cancel
оказался на нужном месте на экране, а не был отделен пустой строкой, и чтобы правильно было посчитано количество строк (элементов) в списке. Для этого и применяется такой вот жутковатый башизм:START_CHR=$((${#LIST_BUF}-2))
CHR_BUF=${LIST_BUF:$START_CHR:2}
if [[ "$CHR_BUF" == "\n" ]];then
LIST_BUF=${LIST_BUF::-2}
fi
Сначала получаем номер начального символа, учитывая то, что перенос строки хранится в переменной в виде эскейп-последовательности (т.е. не как символ с кодом
0x0A
, а в виде последовательности символов \
и n
), т.е надо взять 2 символа с конца строки. Потом вырезаем эти 2 символа, сравниваем с шаблоном, и, наконец, отрезаем его, если он таки есть.3. Получаем количество строк, хранящихся в переменной
$LIST_BUF
, выводим ее с помощью echo
с параметром -e
(т.е. преобразовать эскейп-последовательности в реальные символы) и передать их команде wc -l
, подсчитывающей количество строк:LIST_CTR=`echo -e "$LIST_BUF"|wc -l`
4. Выведем список строк на экран. Опять же, передадим содержимое списка в
$LIST_BUF
команде echo -e
, а потом этот вывод поступит команде nl
, которая пронумерует каждую строку (последовательность символов, заканчивающуюся переносом строки).echo -e "$LIST_BUF"|nl
5. Добавим, как и в предыдущем примере, пункт Cancel (Отмена) и пустую строку.
echo -e " C Cancel"
echo
6. Далее организуем бесконечный цикл, как и в примере Да/нет из первой части:
while [ 1 -eq 1 ];do
#тут будет код
done
7. В цикле первым делом используем оператор
read
, с параметрами -n1
(читать 1 символ и завершать свою работу, передавая управление следующей команде) и -s
(не отображать введенные символы):read -n1 -s
8. Далее проверяем, не нажал ли пользователь клавишу
C
(или c
). Да, дополнительный символ x
перед переменными и строками нужен, чтоб правильно сработало логическое условие ИЛИ
(||
), ну вот такой кривой bash (и shell вообще), что я-то сделаю:if [[ "x$REPLY" == "xC" || "x$REPLY" == "xc" ]]; then
LIST_RET="C"
return
fi
Если нажал, записываем в переменную
LIST_RET
значение C
. Значит, пользователь отменил ввод.9. Далее, следует такая вот конструкция:
if (echo "$REPLY" | grep -E -q "^?[0-9]+$"); then
if [ "$REPLY" -gt 0 -a "$REPLY" -le "$LIST_CTR" ]; then
#тут код, который рассмотрим ниже
fi
fi
В первом
if
'е определяем, оказалось ли в специальной переменной $REPLY
число копия заметки про определение числа в bash, если нет, то опять возвращаемся в цикл ввода.Во втором условии проверяем, чтобы введенное значение было больше (
-gt
0)
, и (-a
) меньше или равно последнему элементу списка (-le "$LIST_CTR"
)Если так, то выполнится код внутри условия, если нет - экран пользователя не изменится, и ему придется ввести корректное значение из списка.
10. Код в условии:
10.1 В переменную
LIST_RET
запишем то, что нажал пользователь:LIST_RET=$REPLY
10.2. Далее получим значение (текстовую строку) элемента:
Передадим содержимое переменной
$LIST_BUF
, команде awk
, чья задача найти нужный номер строки:LIST_ITEM=`echo -e "$LIST_BUF"|awk -v lnum="${LIST_RET}" '(NR == lnum)'`
С помощью конструкции
awk -v lnum="${LIST_RET}
, программе awk
передается номер строки, который нужно найти, далее, значение переменной LIST_RET
предается в переменную скрипта awk
, и awk
вытаскивает строку с нужным номером из переменной, содержащей весь список.Вся функция:
ask_list() #$1 - list #$2 - header
{
LIST_BUF=$1
echo "$2"
echo
START_CHR=$((${#LIST_BUF}-2))
CHR_BUF=${LIST_BUF:$START_CHR:2}
if [[ "$CHR_BUF" == "\n" ]];then
LIST_BUF=${LIST_BUF::-2}
fi
LIST_CTR=`echo -e "$LIST_BUF"|wc -l`
echo -e "$LIST_BUF"|nl
echo -e " C Cancel"
echo
while [ 1 -eq 1 ];do
read -n1 -s
if [[ "x$REPLY" == "xC" || "x$REPLY" == "xc" ]]; then
LIST_RET="C"
return
fi
if (echo "$REPLY" | grep -E -q "^?[0-9]+$"); then
if [ "$REPLY" -gt 0 -a "$REPLY" -le "$LIST_CTR" ]; then
LIST_RET=$REPLY
LIST_ITEM=`echo -e "$LIST_BUF"|awk -v lnum="${LIST_RET}" '(NR == lnum)'`
return
fi
fi
done
}
Пример вызова функции:
ask_list $TARGZ "Select file"
if [[ "$LIST_RET" == "C" ]]; then
echo "Cancelled by user"
else
echo "User select item: $LIST_RET"
echo "Item value: $LIST_ITEM"
fi
Вывод в консоль:
smallwolfie@wolfschanze: ~/test/$ ./asklist
Select file
1 slinstall.tar.gz
2 slmini.tar.gz
3 teSt.Tar.gZ
4 TEST.TAR.GZ
C Cancel
Отмена:
smallwolfie@wolfschanze: ~/test/$ ./asklist
Select file
1 slinstall.tar.gz
2 slmini.tar.gz
3 teSt.Tar.gZ
4 TEST.TAR.GZ
C Cancel
Cancelled by user
Выбор из списка:
smallwolfie@wolfschanze: ~/test/$ ./asklist
Select file
1 slinstall.tar.gz
2 slmini.tar.gz
3 teSt.Tar.gZ
4 TEST.TAR.GZ
C Cancel
User select item: 2
Item value: slmini.tar.gz
Скрипт на GitHub
Скрипт на PasteBin
Взаимодействие bash-скриптов с пользователем. Часть 2
Это репост с сайта http://tolik-punkoff.com
Оригинал: http://tolik-punkoff.com/2019/05/13/pros