bash: choose default from case when enter is pressed in a “select” prompt

后端 未结 4 1399
说谎
说谎 2021-02-05 16:55

I\'m prompting questions in a bash script like this:

optionsAudits=(\"Yep\" \"Nope\")
    echo \"Include audits?\"
    select opt in \"${optionsAudits[@]}\"; do
         


        
相关标签:
4条回答
  • 2021-02-05 17:24

    To complement Aserre's helpful answer, which explains the problem with your code and offers an effective workaround, with background information and a generic, reusable custom select implementation that allows empty input:


    Background information

    To spell it out explicitly: select itself ignores empty input (just pressing Enter) and simply re-prompts - user code doesn't even get to run in response.

    In fact, select uses the empty string to signal to user code that an invalid choice was typed.
    That is, if the output variable - $opt, int this case - is empty inside the select statement, the implication is that an invalid choice index was typed by the user.

    The output variable receives the chosen option's text - either 'Yep' or 'Nope' in this case - not the index typed by the user.

    (By contrast, your code examines $REPLY instead of the output variable, which contains exactly what the user typed, which is the index in case of a valid choice, but may contain extra leading and trailing whitespace).

    Note that in the event that you didn't want to allow empty input, you could simply indicate to the user in the prompt text that ^C (Ctrl+C) can be used to abort the prompt.


    Generic custom select function that also accepts empty input

    The following function closely emulates what select does while also allowing empty input (just pressing Enter). Note that the function intercepts invalid input, prints a warning, and re-prompts:

    # Custom `select` implementation that allows *empty* input.
    # Pass the choices as individual arguments.
    # Output is the chosen item, or "", if the user just pressed ENTER.
    # Example:
    #    choice=$(selectWithDefault 'one' 'two' 'three')
    selectWithDefault() {
    
      local item i=0 numItems=$# 
    
      # Print numbered menu items, based on the arguments passed.
      for item; do         # Short for: for item in "$@"; do
        printf '%s\n' "$((++i))) $item"
      done >&2 # Print to stderr, as `select` does.
    
      # Prompt the user for the index of the desired item.
      while :; do
        printf %s "${PS3-#? }" >&2 # Print the prompt string to stderr, as `select` does.
        read -r index
        # Make sure that the input is either empty or that a valid index was entered.
        [[ -z $index ]] && break  # empty input
        (( index >= 1 && index <= numItems )) 2>/dev/null || { echo "Invalid selection. Please try again." >&2; continue; }
        break
      done
    
      # Output the selected item, if any.
      [[ -n $index ]] && printf %s "${@: index:1}"
    
    }
    

    You could call it as follows:

    # Print the prompt message and call the custom select function.
    echo "Include audits (default is 'Nope')?"
    optionsAudits=('Yep' 'Nope')
    opt=$(selectWithDefault "${optionsAudits[@]}")
    
    # Process the selected item.
    case $opt in
      'Yep') includeAudits=true; ;;
      ''|'Nope') includeAudits=false; ;; # $opt is '' if the user just pressed ENTER
    esac
    

    Optional reading: A more idiomatic version of your original code

    Note: This code doesn't solve the problem, but shows more idiomatic use of the select statement; unlike the original code, this code re-displays the prompt if an invalid choice was made:

    optionsAudits=("Yep" "Nope")
    echo "Include audits (^C to abort)?"
    select opt in "${optionsAudits[@]}"; do
        # $opt being empty signals invalid input.
        [[ -n $opt ]] || { echo "What's that? Please try again." >&2; continue; }
        break # a valid choice was made, exit the prompt.
    done
    
    case $opt in  # $opt now contains the *text* of the chosen option
      'Yep')
         includeAudits=true
         ;;
      'Nope') # could be just `*` in this case.
         includeAudits=false
         ;;
    esac
    

    Note:

    • The case statement was moved out of the select statement, because the latter now guarantees that only valid inputs can be made.

    • The case statement tests the output variable ($opt) rather than the raw user input ($REPLY), and that variable contains the choice text, not its index.

    0 讨论(0)
  • 2021-02-05 17:27

    Your problem is due to the fact that select will ignore empty input. For your case, read will be more suitable, but you will lose the utility select provides for automated menu creation.

    To emulate the behaviour of select, you could do something like that :

    #!/bin/bash
    optionsAudits=("Yep" "Nope")
    while : #infinite loop. be sure to break out of it when a valid choice is made
    do
        i=1
        echo "Include Audits?"
        #we recreate manually the menu here
        for o in  "${optionsAudits[@]}"; do
            echo "$i) $o"
            let i++
        done
    
        read reply
        #the user can either type the option number or copy the option text
        case $reply in
            "1"|"${optionsAudits[0]}") includeAudits=true; break;;
            "2"|"${optionsAudits[1]}") includeAudits=false; break;;
            "") echo "empty"; break;;
            *) echo "Invalid choice. Please choose an existing option number.";;
        esac
    done
    echo "choice : \"$reply\""
    
    0 讨论(0)
  • 2021-02-05 17:37

    Updated answer:

    echo "Include audits? 1) Yep, 2) Nope"
    read ans
    case $ans in
        Yep|1  )  echo "yes"; includeAudits=true; v=1 ;;
        Nope|2 )  echo "no"; includeAudits=false; v=2 ;;
        ""     )  echo "default - yes"; includeAudits=true; v=1 ;;
        *      )  echo "Whats that?"; exit ;;
    esac
    

    This accepts either "Yep" or "1" or "enter" to select yes, and accepts "Nope" or "2" for no, and throws away anything else. It also sets v to 1 or 2 depending on whether the user wanted yes or no.

    0 讨论(0)
  • 2021-02-05 17:37

    This will do what you are asking for.

    options=("option 1" "option 2");
    while :
    do
        echo "Select your option:"
        i=1;
        for opt in  "${options[@]}"; do
            echo "$i) $opt";
            let i++;
        done
    
        read reply
        case $reply in
            "1"|"${options[0]}"|"")
              doSomething1();
              break;;
            "2"|"${options[1]}")
              doSomething2();
              break;;
            *)
              echo "Invalid choice. Please choose 1 or 2";;
        esac
    done
    
    0 讨论(0)
提交回复
热议问题