6

I'm writing a script to build a software from sources, and there's a --platforms option. I would like to allow the user to select multiple items, but I don't know how to prevent them from making a mistake.

Example:

read -p "For what platforms do you wish to build [mac/win/linux32/linux64/all] ? "

if [[ -n "`echo $REPLY | grep 'win\|mac\|linux32\|linux64\|all`" ]] ; then
    echo "ok"
else 
    echo "not ok"
fi

If the user answers linux32, it should be OK (and it is)

If the user answers linux32,mac, it should be OK (and it is)

If the user answers lulz, it should NOT be OK (and it is not)

If the user answers linux32,lulz, it should NOT be OK (and it is, that's my issue)

I was wondering if you knew a way to allow the user to input whatever they want separated by commas, but only if it's one of the options the script is offering, so in this case linux32 linux64 mac win all.

Maybe with case there is a way to allow multiple inputs, or maybe add an elif $REPLY contains anything else than what we want. Another idea, could awk be used? I can't figure out myself how to do that.

2
  • Mac is not a platform, OS X is. (Not Mac OS X!)
    – user3730
    Commented Jun 2, 2014 at 13:03
  • Mac is the option that --platforms= reads, and not OSX. That is how the build works, and I can't do anything about it.
    – MrVaykadji
    Commented Jun 2, 2014 at 13:16

6 Answers 6

5

read can break the input into words and store the result in an array. Set the IFS variable to the word separator character (it needs to be a single character, not a string — if the value of IFS contains multiple characters, then each character is a word separator).

IFS=, read -a platforms

Then check each element of the array against the set of supported platforms.

for p in "${platforms[@]}"; do
  case "$p" in
    win|mac|linux32|linux64) :;;
    all) platforms=(win mac linux32 linux64);;
    *) printf 1>&2 "Unsupported platform: %s\n" "$p"; return 2;;
  esac
done

You can also compare the set of platforms in one go. This is more convenient if you don't want to hardcode the set of supported platforms in the checking code¹.

supported_platforms=(win mac linux32 linux64)
IFS=, read -a platforms
bad_platform_names=($(comm -23 <(printf '%s\n' all "${platforms[@]}" | sort -u) \
                               <(printf '%s\n' "${supported_platforms[@]}" | sort -u)))
if [[ ${#bad_platform_names[@]} -ne 0 ]]; then
  printf "Unsupported platform: %s\n" "${bad_platform_names[@]}"
  exit 1
fi
if printf '%s\n' "${platforms[@]}" | grep -qx all; then
  platforms=("${supported_platforms[@]}")
fi

A different approach would be to prompt for platforms one at a time using the select builtin.

¹ Of course you can do this in pure bash if you prefer.

7
  • This doesn't seem to work. I always have the "unsupported platform:" message and it also only records the first input.
    – MrVaykadji
    Commented Jun 1, 2014 at 23:54
  • @MrVaykadji Typo fixed Commented Jun 2, 2014 at 0:01
  • It still only records the first input, and the second is always stated as unsupported, even if it should be ok. I don't understand
    – MrVaykadji
    Commented Jun 2, 2014 at 0:06
  • @MrVaykadji I've tested both snippets this time. The first worked, the second had several more typos. Commented Jun 2, 2014 at 0:22
  • 1
    @MrVaykadji The bug is in your last line: $platforms is the first element of the array (this is a quirk of ksh and bash syntax). You need "${platforms[@]}" to get the list of elements in the array. Commented Jun 2, 2014 at 0:31
5

Try this!

buildvar=0
read -p "For what platforms do you wish to build [mac/win/linux32/linux64/all] ? " input
IFS=',' read -a options <<< "$input"

for option in "${options[@]}"
 do
  case "$option" in
    linux32)
        buildcommand="linux32" && buildvar=1
        ;;
    linux64)
        [ $buildvar == 1 ] && buildcommand="$buildcommand",linux64 || buildcommand="linux64" && buildvar=1
        ;;  
    mac)
        [ $buildvar == 1 ] && buildcommand="$buildcommand",mac || buildcommand="mac" && buildvar=1
        ;;  
    win)
        [ $buildvar == 1 ] && buildcommand="$buildcommand",win || buildcommand="win" && buildvar=1
        ;;
    all)
        buildcommand="all" && buildvar=1
        ;;
    *) 
        echo "'$option' was ignored." 
        ;;  
        esac
    done

[ $buildvar == "0" ] && echo "Incorrect input. Default build selected." && buildcommand="default"

echo "=> $buildcommand"

This will prompt for a comma separated list of options. It will then split this list into an array and iterate through the elements checking each element separately, then combine all "good" elements into a single variable.

2
  • Thanks, I had to rewrite some of it, but it's working like a charm :)
    – MrVaykadji
    Commented Jun 2, 2014 at 0:35
  • You have set IFS generally for the script which can cause unexpected consequences later. You can set it only for the environment of the read command by prefixing the IFS=, there as in Gilles answer.
    – Matt
    Commented Jun 2, 2014 at 9:14
2

You can read a single line of input with sed and translate the delimiter to newlines like:

% sed 'y/,/\n/;q' /dev/tty
> this,is,a,single,line
##OUTPUT
this
is
a
single
line

Because sed writes the result to stdout as a text file, following that up with an explicit grep is easy and occurs in a single stream. And, in fact, you can use sed like a smart tee if you make use of its write to file function; and, if you write to file descriptor devices, you can split its output based on rules you define.

A function that prompts for input and outputs only a newline delimited list of acceptable arguments to stdout and erroneous output to stderr might look like:

_accept_prompt() (
   . /dev/fd/0
   IFS=${delim-,}
   _prompt "$@" >&2
   { _read_split "$@" |
        err=all _grep_ok "$@" |
        sed '1{$d}' >&2
   } 3>&1 | _grep_ok "$@"
) <<\HELPERS

_prompt() {
    cat ; printf ' : '
} <<-PROMPT
    Choose from : $(printf "'%s' " "$@")
    Enter a '$IFS'-delimited selection below...
PROMPT

_read_split() {
    y="y/${IFS}/\n/"
    sed -ne "H;x;s/^/Invalid input IGNORED:/;${y};p;x" \
        -ne "/all/s/.*/$*/;${y};w /dev/fd/3" -ne q
} </dev/tty

_grep_ok() {
    grep -${err+v}xF "$(printf '%s\n' "$@" $err)"
}
HELPERS

I split this into hopefully more descriptively named helper functions in lieu of the comments and attached them to the main function. So the flow all happens in the first few lines. I hoped to make this clearer.

_read_split outputs two streams - >&1 and >&3. _grep_ok picks up the first with $err defined and writes to >&2 all lines contained in its input that are not among _accept_prompt's positional parameters.

_grep_ok also concurrently picks up the second stream - >&3 and writes to its >&1 stdout all lines in its input that are among _accept_prompt's positional parameters.

Running it:

% _accept_prompt this is the list of acceptable parameters
###PROMPT
    Choose from : 'this' 'is' 'the' 'list' 'of' 'acceptable' 'parameters'
    Enter a ','-delimited selection below...
###INPUT
 : all,invalid
###STDOUT
this
is
the
list
of
acceptable
parameters
###STDERR
Invalid input IGNORED:
invalid

You can alter the default ,comma delimiter on invocation like:

delim=? _accept_prompt $args
5
  • I find that code difficult to read.
    – user3730
    Commented Jun 2, 2014 at 15:48
  • It might be just me, but I find it too complicated.
    – user3730
    Commented Jun 2, 2014 at 16:08
  • I also am kind of lost reading that^^ But it's interresting
    – MrVaykadji
    Commented Jun 2, 2014 at 16:39
  • 1
    @MrVaykadji - is it any clearer now? I tried...
    – mikeserv
    Commented Jun 2, 2014 at 19:30
  • Yes, very much !
    – MrVaykadji
    Commented Jun 2, 2014 at 20:01
2

I am presuming you already have some way of getting the variables in from input, since you mentioned a --platforms option. You can use regular expressions to match on the valid conditions.

regex='^(mac|win|linux(32|64)|all)(,(mac|win|linux(32|64)|all))*$'
if [[ $1 =~ $regex ]]; then 
  echo "ok"
else 
  echo "not ok"
fi

That should satisfy all the requirements:

$ ./stack.sh linux32
ok
$ ./stack.sh linux32,mac
ok
$ ./stack.sh lulz
not ok
$ ./stack.sh linux32,lulz
not ok

Granted, this doesn't stop people from using "all,all,all,all", but that at least can be trimmed down when actually running stuff later.

Benefits of this method include being able to loop until the user gets it right, if you're using the read command you have in the first post. Drawbacks include being a bit confusing to just glance at.

regex='^(mac|win|linux(32|64)|all)(,(mac|win|linux(32|64)|all))*$'
while ! [[ $REPLY =~ $regex ]]; do
  read -p "For what platforms do you wish to build [mac/win/linux32/linux64/all] ? "
done
1

A simplified/improved version of arnefm's answer:

read -p 'Enter a comma-separated list of platforms to build for [win/mac/linux32/linux64/all]: ' input
IFS=',' read -a options <<< "$input"

shopt -s extglob

for option in "${options[@]}"; do
    case "$option" in
        win|mac|linux@(32|64)|all)
            buildcommand="${buildcommand:+$buildcommand,}$option"
            buildvar=1;;
        *)
            printf 'Invalid option "%s" ignored.\n' "$option" >&2;;
    esac
done

IFS=',' read -a options <<< "$buildcommand"

for option in "${options[@]}"; do
    if [[ $option == 'all' ]]; then
        buildcommand='all'
        break
    fi
done

if (( !buildvar )); then
    echo 'Incorrect input. Default build selected.' >&2
    buildcommand='default'
fi
14
  • osx is not a valid choice for --platforms= option.
    – MrVaykadji
    Commented Jun 2, 2014 at 13:16
  • I see. I updated my answer to reflect that.
    – user3730
    Commented Jun 2, 2014 at 13:18
  • As far as I'm concern, you're right, it should be "osx" but not according to the sources I'm building : github.com/popcorn-official/popcorn-app/blob/dev-0.3/…
    – MrVaykadji
    Commented Jun 2, 2014 at 13:19
  • 1
    Also, you might want to take a look at mywiki.wooledge.org. It's a really good resource for shell/bash scripting. Specifically this and this is relevant.
    – user3730
    Commented Jun 2, 2014 at 15:46
  • 1
    I meant better than MrVaykadji's proposal, which might work in this particular case, but isn't really a clean solution. Passing a variable to echo, especially as first argument, and it's not even quoted (see my previous links). That grep might match more than expected, though not in this particular case.
    – user3730
    Commented Jun 2, 2014 at 16:34
0

The alternate method as suggested by Gilles, get users to input as little as possible by prompting them for the options.

available_platforms="mac win linux32 linux64"

positive() {
  printf "%s (Answer y for yes or anything else for no) " "$1"
  read answer
  if [ "$answer" != "y" ] && [ "$answer" != "Y" ]; then
    return 1
  fi
}

if positive "Do you want to build for all platforms [$available_platforms]?"; then
  build_platforms="$available_platforms"
else
  for platform in $available_platforms; do
    if positive "Do you want to build for [$platform]?"; then
      build_platforms="${build_platforms}${platform} "
    fi
  done
fi

printf "build_platforms: %s\n" "$build_platforms"

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .