Простой интерактив в 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

Простой вариант с помощью оператора select.

Организуем это дело в виде отдельной функции 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

One Response to Простой интерактив в bash-скриптах, часть 2. Списки (меню).

  1. Pingback: Интерактивный скрипт для переключения VPN’ок | Персональный блог Толика Панкова

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *