Menu de seleção múltipla no script bash


28

Sou iniciante no bash, mas gostaria de criar um script no qual gostaria de permitir que o usuário selecione várias opções em uma lista de opções.

Basicamente, o que eu gostaria é algo semelhante ao exemplo abaixo:

       #!/bin/bash
       OPTIONS="Hello Quit"
       select opt in $OPTIONS; do
           if [ "$opt" = "Quit" ]; then
            echo done
            exit
           elif [ "$opt" = "Hello" ]; then
            echo Hello World
           else
            clear
            echo bad option
           fi
       done

(Originário de http://www.faqs.org/docs/Linux-HOWTO/Bash-Prog-Intro-HOWTO.html#ss9.1 )

No entanto, meu script teria mais opções e gostaria de permitir a seleção de múltiplos. Então, algo como isto:

1) Opção 1
2) Opção 2
3) Opção 3
4) Opção 4
5) Concluído

Ter feedback sobre os que eles selecionaram também seria ótimo, por exemplo, sinais de adição ao lado dos que eles já selecionaram. Por exemplo, se você selecionar "1", gostaria de paginar para limpar e reimprimir:

1) Option 1 +
2) Option 2
3) Option 3
4) Option 4
5) Done

Então, se você selecionar "3":

1) Option 1 +
2) Option 2
3) Option 3 +
4) Option 4
5) Done

Além disso, se eles selecionarem novamente (1), eu gostaria que "desselecione" a opção:

1) Option 1
2) Option 2
3) Option 3 +
4) Option 4
5) Done

E, finalmente, quando Concluído é pressionado, gostaria que uma lista daqueles que foram selecionados fossem exibidos antes da saída do programa, por exemplo, se o estado atual for:

1) Option 1
2) Option 2 +
3) Option 3 + 
4) Option 4 +
5) Done

Pressionar 5 deve imprimir:

Option 2, Option 3, Option 4

... e o script termina.

Então, minha pergunta - isso é possível no bash, e se alguém é capaz de fornecer um exemplo de código?

Qualquer conselho seria muito apreciado.

Respostas:


35

Eu acho que você deveria dar uma olhada no diálogo ou no chicote .

caixa de diálogo

Editar:

Aqui está um exemplo de script usando as opções da sua pergunta:

#!/bin/bash
cmd=(dialog --separate-output --checklist "Select options:" 22 76 16)
options=(1 "Option 1" off    # any option can be set to default to "on"
         2 "Option 2" off
         3 "Option 3" off
         4 "Option 4" off)
choices=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty)
clear
for choice in $choices
do
    case $choice in
        1)
            echo "First Option"
            ;;
        2)
            echo "Second Option"
            ;;
        3)
            echo "Third Option"
            ;;
        4)
            echo "Fourth Option"
            ;;
    esac
done

Obrigado por isso. Looks mais complexas do que eu esperava, mas vou check it out :-)
user38939

@ am2605: Veja minha edição. Eu adicionei um script de exemplo.
Pausado até novo aviso.

3
Ele só parece complexa até que você tenha usado uma ou duas vezes, então você nunca vai usar qualquer outra coisa ...
Chris S

27

Se você acha que whiptailé complexo, aqui está um código apenas do bash que faz exatamente o que você deseja. É curto (~ 20 linhas), mas um pouco enigmático para um iniciante. Além de mostrar "+" para as opções marcadas, também fornece feedback para cada ação do usuário ("opção inválida", "opção X foi marcada" / desmarcada, etc).

Dito isto, lá vai você!

Espero que gostem ... foi um desafio muito divertido fazê-lo :)

#!/bin/bash

# customize with your own.
options=("AAA" "BBB" "CCC" "DDD")

menu() {
    echo "Avaliable options:"
    for i in ${!options[@]}; do 
        printf "%3d%s) %s\n" $((i+1)) "${choices[i]:- }" "${options[i]}"
    done
    if [[ "$msg" ]]; then echo "$msg"; fi
}

prompt="Check an option (again to uncheck, ENTER when done): "
while menu && read -rp "$prompt" num && [[ "$num" ]]; do
    [[ "$num" != *[![:digit:]]* ]] &&
    (( num > 0 && num <= ${#options[@]} )) ||
    { msg="Invalid option: $num"; continue; }
    ((num--)); msg="${options[num]} was ${choices[num]:+un}checked"
    [[ "${choices[num]}" ]] && choices[num]="" || choices[num]="+"
done

printf "You selected"; msg=" nothing"
for i in ${!options[@]}; do 
    [[ "${choices[i]}" ]] && { printf " %s" "${options[i]}"; msg=""; }
done
echo "$msg"

Bom trabalho! Bom trabalho!
Daniel

4
Este é um pouco enigmático, mas eu adoro o uso de expansões de chaves complexas e matrizes dinâmicas. Demorei um pouco para ler tudo como acontece, mas eu adoro. Também adoro o fato de você ter usado a função printf () incorporada. Não encontro muitos que saibam disso no bash. Muito útil se um é usado para codificação em C.
Yokai

1
Se alguém quiser selecionar várias opções (separadas por espaço) de uma só vez:while menu && read -rp "$prompt" nums && [[ "$nums" ]]; do while read num; do ... done < <(echo $nums |sed "s/ /\n/g") done
TAAPSogeking

1
Isso foi realmente útil no desenvolvimento de um script que é usado por várias outras pessoas que não têm acesso ao whiptail ou a outros pacotes porque estão usando git bashno Windows!
Dr Ivol

5

Aqui está uma maneira de fazer exatamente o que você deseja, usando apenas os recursos do Bash sem dependências externas. Ele marca as seleções atuais e permite alterná-las.

#!/bin/bash
# Purpose: Demonstrate usage of select and case with toggleable flags to indicate choices
# 2013-05-10 - Dennis Williamson

choice () {
    local choice=$1
    if [[ ${opts[choice]} ]] # toggle
    then
        opts[choice]=
    else
        opts[choice]=+
    fi
}

PS3='Please enter your choice: '
while :
do
    clear
    options=("Option 1 ${opts[1]}" "Option 2 ${opts[2]}" "Option 3 ${opts[3]}" "Done")
    select opt in "${options[@]}"
    do
        case $opt in
            "Option 1 ${opts[1]}")
                choice 1
                break
                ;;
            "Option 2 ${opts[2]}")
                choice 2
                break
                ;;
            "Option 3 ${opts[3]}")
                choice 3
                break
                ;;
            "Option 4 ${opts[4]}")
                choice 4
                break
                ;;
            "Done")
                break 2
                ;;
            *) printf '%s\n' 'invalid option';;
        esac
    done
done

printf '%s\n' 'Options chosen:'
for opt in "${!opts[@]}"
do
    if [[ ${opts[opt]} ]]
    then
        printf '%s\n' "Option $opt"
    fi
done

Para ksh, altere as duas primeiras linhas da função:

function choice {
    typeset choice=$1

e o shebang para #!/bin/ksh.


Bom exemplo! Como conseguir executá-lo no KSH?
FuSsA 27/03

1
@ FuSsA: Editei minha resposta para mostrar as alterações necessárias para fazê-lo funcionar no ksh.
Pausado até novo aviso.

1
A manipulação de array no bash é muito grave. Você não é apenas o primeiro, é o único acima de 40k em toda a trindade.
peterh diz restabelecer Monica

1
@ FuSsA: options=(*)(ou outros padrões de globbing) fornecerão uma lista de arquivos na matriz. O desafio seria obter o conjunto de marcas de seleção ( ${opts[@]}) compactado junto com ele. Isso pode ser feito com um forloop, mas precisaria ser executado para cada passagem pelo whileloop externo . Você pode considerar o uso dialogou whiptailcomo mencionei na minha outra resposta - embora sejam dependências externas.
Pausado até novo aviso.

1
@ FuSsA: Em seguida, você pode salvar a string em outra matriz (ou usar ${opts[@]}e salvar a string, passada como um argumento adicional para a função, em vez de +).
Pausado até novo aviso.

2

Eu escrevi uma biblioteca chamada questionário , que é um mini-DSL para criar questionários de linha de comando. Ele solicita que o usuário responda a uma série de perguntas e imprime as respostas no stdout.

Isso torna sua tarefa muito fácil. Instale-o pip install questionnairee crie um script, por exemplo questions.py, assim:

from questionnaire import Questionnaire
q = Questionnaire(out_type='plain')

q.add_question('options', prompt='Choose some options', prompter='multiple',
               options=['Option 1', 'Option 2', 'Option 3', 'Option 4'], all=None)

q.run()

Então corra python questions.py. Quando terminar de responder às perguntas, elas serão impressas em stdout. Funciona com o Python 2 e 3, um dos quais quase certamente está instalado no seu sistema.

Ele também pode lidar com questionários muito mais complicados, caso alguém queira fazer isso. Aqui estão alguns recursos:

  • Imprime respostas como JSON (ou como texto sem formatação) em stdout
  • Permite que os usuários voltem e reanalisem as perguntas
  • Suporta perguntas condicionais (as perguntas podem depender de respostas anteriores)
  • Suporta os seguintes tipos de perguntas: entrada bruta, escolha uma, escolha várias
  • Nenhum acoplamento obrigatório entre a apresentação da pergunta e os valores da resposta

1

Usei o exemplo do MestreLion e elaborei o código abaixo. Tudo que você precisa fazer é atualizar as opções e ações nas duas primeiras seções.

#!/bin/bash
#title:         menu.sh
#description:   Menu which allows multiple items to be selected
#author:        Nathan Davieau
#               Based on script from MestreLion
#created:       May 19 2016
#updated:       N/A
#version:       1.0
#usage:         ./menu.sh
#==============================================================================

#Menu options
options[0]="AAA"
options[1]="BBB"
options[2]="CCC"
options[3]="DDD"
options[4]="EEE"

#Actions to take based on selection
function ACTIONS {
    if [[ ${choices[0]} ]]; then
        #Option 1 selected
        echo "Option 1 selected"
    fi
    if [[ ${choices[1]} ]]; then
        #Option 2 selected
        echo "Option 2 selected"
    fi
    if [[ ${choices[2]} ]]; then
        #Option 3 selected
        echo "Option 3 selected"
    fi
    if [[ ${choices[3]} ]]; then
        #Option 4 selected
        echo "Option 4 selected"
    fi
    if [[ ${choices[4]} ]]; then
        #Option 5 selected
        echo "Option 5 selected"
    fi
}

#Variables
ERROR=" "

#Clear screen for menu
clear

#Menu function
function MENU {
    echo "Menu Options"
    for NUM in ${!options[@]}; do
        echo "[""${choices[NUM]:- }""]" $(( NUM+1 ))") ${options[NUM]}"
    done
    echo "$ERROR"
}

#Menu loop
while MENU && read -e -p "Select the desired options using their number (again to uncheck, ENTER when done): " -n1 SELECTION && [[ -n "$SELECTION" ]]; do
    clear
    if [[ "$SELECTION" == *[[:digit:]]* && $SELECTION -ge 1 && $SELECTION -le ${#options[@]} ]]; then
        (( SELECTION-- ))
        if [[ "${choices[SELECTION]}" == "+" ]]; then
            choices[SELECTION]=""
        else
            choices[SELECTION]="+"
        fi
            ERROR=" "
    else
        ERROR="Invalid option: $SELECTION"
    fi
done

ACTIONS

Excelente resposta. Adicione também uma nota para aumentar o número, por exemplo, Opção 15; onde n1 SELECTIONé a parte fundamental para aumentar o número de dígitos ..
DBF

Esqueceu de adicionar; onde -n2 SELECTIONaceitará dois dígitos (por exemplo, 15), -n3aceita três (por exemplo, 153) etc.)
dbf

1

Aqui está uma função bash que permite ao usuário selecionar várias opções com as teclas de seta e espaço e confirmar com Enter. Tem uma sensação agradável de menu. Eu escrevi com a ajuda de /unix//a/415155 . Pode ser chamado assim:

multiselect result "Option 1;Option 2;Option 3" "true;;true"

O resultado é armazenado como uma matriz em uma variável com o nome fornecido como o primeiro argumento. O último argumento é opcional e é usado para fazer algumas opções selecionadas por padrão. Se parece com isso.

function prompt_for_multiselect {

    # little helpers for terminal print control and key input
    ESC=$( printf "\033")
    cursor_blink_on()   { printf "$ESC[?25h"; }
    cursor_blink_off()  { printf "$ESC[?25l"; }
    cursor_to()         { printf "$ESC[$1;${2:-1}H"; }
    print_inactive()    { printf "$2   $1 "; }
    print_active()      { printf "$2  $ESC[7m $1 $ESC[27m"; }
    get_cursor_row()    { IFS=';' read -sdR -p $'\E[6n' ROW COL; echo ${ROW#*[}; }
    key_input()         {
      local key
      IFS= read -rsn1 key 2>/dev/null >&2
      if [[ $key = ""      ]]; then echo enter; fi;
      if [[ $key = $'\x20' ]]; then echo space; fi;
      if [[ $key = $'\x1b' ]]; then
        read -rsn2 key
        if [[ $key = [A ]]; then echo up;    fi;
        if [[ $key = [B ]]; then echo down;  fi;
      fi 
    }
    toggle_option()    {
      local arr_name=$1
      eval "local arr=(\"\${${arr_name}[@]}\")"
      local option=$2
      if [[ ${arr[option]} == true ]]; then
        arr[option]=
      else
        arr[option]=true
      fi
      eval $arr_name='("${arr[@]}")'
    }

    local retval=$1
    local options
    local defaults

    IFS=';' read -r -a options <<< "$2"
    if [[ -z $3 ]]; then
      defaults=()
    else
      IFS=';' read -r -a defaults <<< "$3"
    fi
    local selected=()

    for ((i=0; i<${#options[@]}; i++)); do
      selected+=("${defaults[i]}")
      printf "\n"
    done

    # determine current screen position for overwriting the options
    local lastrow=`get_cursor_row`
    local startrow=$(($lastrow - ${#options[@]}))

    # ensure cursor and input echoing back on upon a ctrl+c during read -s
    trap "cursor_blink_on; stty echo; printf '\n'; exit" 2
    cursor_blink_off

    local active=0
    while true; do
        # print options by overwriting the last lines
        local idx=0
        for option in "${options[@]}"; do
            local prefix="[ ]"
            if [[ ${selected[idx]} == true ]]; then
              prefix="[x]"
            fi

            cursor_to $(($startrow + $idx))
            if [ $idx -eq $active ]; then
                print_active "$option" "$prefix"
            else
                print_inactive "$option" "$prefix"
            fi
            ((idx++))
        done

        # user key control
        case `key_input` in
            space)  toggle_option selected $active;;
            enter)  break;;
            up)     ((active--));
                    if [ $active -lt 0 ]; then active=$((${#options[@]} - 1)); fi;;
            down)   ((active++));
                    if [ $active -ge ${#options[@]} ]; then active=0; fi;;
        esac
    done

    # cursor position back to normal
    cursor_to $lastrow
    printf "\n"
    cursor_blink_on

    eval $retval='("${selected[@]}")'
}

como você chama isso? como seria o arquivo?
Eli


-1
export supermode=none

source easybashgui

list "Option 1" "Option 2" "Option 3" "Option 4"

2
Talvez você possa adicionar uma pequena descrição do que isso está fazendo? Para futuros visitantes, não muito para o OP.
Slm

Além disso, um link para a origem de easybashgui.
Pausado até novo aviso.
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.