9

This answer shows great and intuitive bash menu where you just press number and item is selected. But it's kinda inconvenient for file list, because it's all hardcoded. I'd rather fill my files in some sort of array, then let user pick a number which again maps to array offset.


Basically this is what I imagine:

Following `*.war` archives were found, select one:

  1) old.war
  2) debug.war
  3) release.war

Use number to select a file or 'stop' to cancel: blah
'blah' is not a number
Use number to select a file or 'stop' to cancel: 2
debug.war installed

But how do I turn list of files into this array thingy:

options=("Option 1" "Option 2" "Option 3" "Quit")

How do I get string at certain offset in options? How do I ensure user is asked to try again? Can I allow for string stop to stop selection mode?

1
  • How do you get your file names if they're not hardcoded? Should they be read from a directory? For example, all files in the war directory?
    – terdon
    Oct 6, 2015 at 11:20

3 Answers 3

15

To save the outputs of find in a bash array use this:

unset options i
while IFS= read -r -d $'\0' f; do
  options[i++]="$f"
done < <(find /dir/ -maxdepth 1 -type f -name "*.war" -print0 )
  • read reads the input from find null delimited (-d $'\0').
    • The array $options is filled with the filenames.
  • find searches only for files (-type f) within the given directory (-maxdepth 1) with ending .war (-name "*.war") and prints them delimted by the null character (-print0).

The select menu can be done like this:

select opt in "${options[@]}" "Stop the script"; do
  case $opt in
    *.war)
      echo "War file $opt selected"
      # processing
      ;;
    "Stop the script")
      echo "You chose to stop"
      break
      ;;
    *)
      echo "This is not a number"
      ;;
  esac
done

It works as follows:

1) /dir/old.war
2) /dir/debug.war
3) /dir/release.war
4) Stop the script
#? test
This is not a number
#? 2
War file /dir/debug.war selected
#? 4
You chose to stop
5
  • Can I forbid find from searching sub-directories? Oct 6, 2015 at 11:33
  • @TomášZato sure, -maxdepth 1, i edit the answer
    – chaos
    Oct 6, 2015 at 11:36
  • Thanks, the --help for find isn't very helprul... Oct 6, 2015 at 11:37
  • 1
    I prefer the more detailed manpage: man find
    – chaos
    Oct 6, 2015 at 11:38
  • You can add elements to an array without using an index variable: options+=("$f") Oct 6, 2015 at 22:27
8

You could also use a shell glob to get the list of files. This approach has the advantage of not using an external program (find) and of not needing any restriction on the type of file (*war, for example) given:

#!/usr/bin/env bash

## Collect the files in the array $files
files=( ~/foo/war/* )
## Enable extended globbing. This lets us use @(foo|bar) to
## match either 'foo' or 'bar'.
shopt -s extglob

## Start building the string to match against.
string="@(${files[0]}"
## Add the rest of the files to the string
for((i=1;i<${#files[@]};i++))
do
    string+="|${files[$i]}"
done
## Close the parenthesis. $string is now @(file1|file2|...|fileN)
string+=")"

## Show the menu. This will list all files and the string "quit"
select file in "${files[@]}" "quit"
do
    case $file in
    ## If the choice is one of the files (if it matches $string)
    $string)
        ## Do something here
        echo "$file"
        ## Uncomment this line if you don't want the menu to
        ## be shown again
        # break;
        ;;

    "quit")
        ## Exit
        exit;;
    *)
        file=""
        echo "Please choose a number from 1 to $((${#files[@]}+1))";;
    esac
done
1
  • +1 - This has the advantage that the filename handling is completely dynamic. Oct 6, 2015 at 22:28
6

select can do most of this for you, without much effort needed.

How do I turn list of files into this array thingy?

You don't actually need to. select takes a series of words to display as options. These can be given directly (select color in red green blue) or come from the expansion of a file glob (select file in *.war), as well as expanding an array into words as the example you found does (select option in "${options[@]}").

How do I get string at certain offset in options?

select does this automatically, and stores it into the variable you provide. (If the user's input is invalid, it stores the empty string.)

How do I ensure user is asked to try again?

Again select does this for you, because select makes a loop, like while. It will keep asking until you break out of the loop (or until it reads EOF, usually entered by Ctrl+D).

Can I allow for string stop to stop selection mode?

Yes, the user's input is put in the variable REPLY, regardless of whether it corresponds to one of the options, so you can check for specific values and handle them differently.

Putting it all together:

echo "The following `*.war` archives were found; select one:"

# set the prompt used by select, replacing "#?"
PS3="Use number to select a file or 'stop' to cancel: "

# allow the user to choose a file
select filename in *.war
do
    # leave the loop if the user says 'stop'
    if [[ "$REPLY" == stop ]]; then break; fi

    # complain if no file was selected, and loop to ask again
    if [[ "$filename" == "" ]]
    then
        echo "'$REPLY' is not a valid number"
        continue
    fi

    # now we can use the selected file
    echo "$filename installed"

    # it'll ask for another unless we leave the loop
    break
done
2
  • +1 - for not needing an array Oct 6, 2015 at 22:35
  • +1 for using PS3 prompt but especially for readable code that's the easiest to understand of all three answers, for "noobs like me". Oct 1, 2016 at 18:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.