Menú de selección múltiple en script bash

28

Soy un novato bash, pero me gustaría crear un script en el que me gustaría permitir que el usuario seleccione múltiples opciones de una lista de opciones.

Esencialmente, lo que me gustaría es algo similar al siguiente ejemplo:

       #!/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

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

Sin embargo, mi script tendría más opciones, y me gustaría permitir que se seleccionen múltiples. Entonces algo como esto:

1) Opción 1
2) Opción 2
3) Opción 3
4) Opción 4
5) Listo

Tener comentarios sobre los que han seleccionado también sería genial, por ejemplo, signos más junto a los que ya han seleccionado. Por ejemplo, si selecciona "1", me gustaría paginar para borrar y volver a imprimir:

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

Entonces, si selecciona "3":

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

Además, si nuevamente seleccionaron (1) me gustaría que "deseleccione" la opción:

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

Y finalmente, cuando se presiona Listo, me gustaría tener una lista de las que se seleccionaron para que se muestren antes de que el programa salga, por ejemplo, si el estado actual es:

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

Presionando 5 debería imprimir:

Option 2, Option 3, Option 4

... y el script termina.

Entonces mi pregunta: ¿es esto posible en bash, y si es así, alguien puede proporcionar una muestra de código?

Cualquier consejo sería muy apreciado.

usuario38939
fuente

Respuestas:

35

Creo que deberías echar un vistazo al diálogo o la cola de látigo .

caja de diálogo

Editar:

Aquí hay un script de ejemplo que usa las opciones de su pregunta:

#!/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
Pausado hasta nuevo aviso.
fuente
Gracias por eso. Parece más complejo de lo que esperaba, pero lo comprobaré :-)
user38939
@ am2605: Vea mi edición. Agregué un script de ejemplo.
Pausado hasta nuevo aviso.
3
Solo parece complejo hasta que lo haya usado una o dos veces, luego nunca usará nada más ...
Chris S
27

Si crees que whiptailes complejo, aquí va un código solo de bash que hace exactamente lo que quieres. Es corto (~ 20 líneas), pero un poco críptico para un principiante. Además de mostrar "+" para las opciones marcadas, también proporciona comentarios para cada acción del usuario ("opción no válida", "la opción X fue marcada" / sin marcar, etc.).

Dicho eso, ahí tienes!

Espero que lo disfruten ... fue un desafío bastante divertido hacerlo :)

#!/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"
MestreLion
fuente
¡Buen trabajo! ¡Buen trabajo!
Daniel
44
Este es un poco críptico, pero me encanta su uso de complejas expansiones de llaves y matrices dinámicas. Me tomó un poco de tiempo poder leer todo mientras sucede, pero me encanta. También me encanta el hecho de que haya utilizado la función printf () incorporada. No encuentro muchos que sepan que existen en bash. Muy útil si uno está acostumbrado a codificar en C.
Yokai
1
Si alguien quisiera poder seleccionar varias opciones (separadas por espacios) a la vez:while menu && read -rp "$prompt" nums && [[ "$nums" ]]; do while read num; do ... done < <(echo $nums |sed "s/ /\n/g") done
TAAPSogeking
1
¡Esto fue realmente útil en el desarrollo de un script que es utilizado por muchas otras personas que no tienen acceso a Whiptail u otros paquetes porque lo están usando git bashen Windows!
Dr Ivol
5

Aquí hay una manera de hacer exactamente lo que quiere usando solo las funciones de Bash sin dependencias externas. Marca las selecciones actuales y le permite alternarlas.

#!/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, cambie las dos primeras líneas de la función:

function choice {
    typeset choice=$1

y el shebang a #!/bin/ksh.

Pausado hasta nuevo aviso.
fuente
Buen ejemplo! ¿Cómo lograr ejecutarlo en KSH?
FuSsA
1
@FuSsA: edité mi respuesta para mostrar los cambios necesarios para que funcione en ksh.
Pausado hasta nuevo aviso.
1
El manejo de la matriz en bash es muy duro. No eres solo el primero, eres el único por encima de 40k en toda la trinidad.
Peter dice que reinstalará a Monica el
1
@FuSsA: options=(*)(u otros patrones globales) obtendrá una lista de archivos en la matriz. El desafío sería obtener las marcas de selección array ( ${opts[@]}) comprimidas junto con él. Se puede hacer con un forbucle, pero tendría que ejecutarse para cada pasada a través del whilebucle externo . Es posible que desee considerar usar dialogo whiptailcomo mencioné en mi otra respuesta, aunque estas son dependencias externas.
Pausado hasta nuevo aviso.
1
@FuSsA: Entonces podría guardar la cadena en otra matriz (o usar ${opts[@]}y guardar la cadena, pasada como un argumento adicional a la función, en lugar de +).
Pausado hasta nuevo aviso.
2

Escribí una biblioteca llamada cuestionario , que es un mini-DSL para crear cuestionarios de línea de comandos. Le pide al usuario que responda una serie de preguntas e imprime las respuestas en stdout.

Hace tu tarea realmente fácil. Instálelo pip install questionnairey cree un script, por ejemplo questions.py, así:

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()

Entonces corre python questions.py. Cuando haya terminado de responder las preguntas, se imprimirán en stdout. Funciona con Python 2 y 3, uno de los cuales seguramente está instalado en su sistema.

También puede manejar cuestionarios mucho más complicados, en caso de que alguien quiera hacer esto. Aquí hay algunas características:

  • Imprime las respuestas como JSON (o como texto sin formato) en stdout
  • Permite a los usuarios regresar y responder preguntas
  • Admite preguntas condicionales (las preguntas pueden depender de respuestas anteriores)
  • Admite los siguientes tipos de preguntas: entrada sin formato, elija una, elija muchas
  • Sin acoplamiento obligatorio entre la presentación de preguntas y los valores de respuesta
kylebebak
fuente
1

Usé el ejemplo de MestreLion y redacté el código a continuación. Todo lo que necesita hacer es actualizar las opciones y acciones en las dos primeras secciones.

#!/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
Nathan Davieau
fuente
Excelente respuesta Agregue también una nota para aumentar el número, por ejemplo, Opción 15; ¿Dónde n1 SELECTIONestá la parte crucial para aumentar el número de dígitos?
Dbf
Olvidé agregar; donde -n2 SELECTIONaceptará dos dígitos (por ejemplo, 15), -n3acepta tres (por ejemplo 153), etc.
DBF
1

Aquí hay una función bash que permite al usuario seleccionar múltiples opciones con las teclas de flecha y el Espacio, y confirmar con Enter. Tiene una agradable sensación de menú. Lo escribí con la ayuda de https://unix.stackexchange.com/a/415155 . Se puede llamar así:

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

El resultado se almacena como una matriz en una variable con el nombre proporcionado como primer argumento. El último argumento es opcional y se usa para seleccionar algunas opciones por defecto. Se parece a esto.

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[@]}")'
}
Denis Semenenko
fuente
¿como lo llamas? ¿Cómo se vería el archivo?
Eli
-1
export supermode=none

source easybashgui

list "Option 1" "Option 2" "Option 3" "Option 4"
usuario173209
fuente
2
¿Quizás podrías agregar una pequeña descripción de lo que está haciendo? Para futuros visitantes, no tanto para el OP.
slm
Además, un enlace al origen de easybashgui.
Pausado hasta nuevo aviso.