2

I am working on a Bash script that needs to take zero to multiple strings as an input but I am unsure how to do this because of the lack of a flag before the list.

The script usage:

script [ list ] [ -t <secs> ] [ -n <count> ]

The list takes zero, one, or multiple strings as input. When a space is encountered, that acts as the break between the strings in a case of two or more. These strings will eventually be input for a grep command, so my idea is to save them in an array of some kind. I currently have the -t and -n working correctly. I have tried looking up examples but have been unable to find anything that is similar to what I want to do. My other concern is how to ignore string input after a flag is set so no other strings are accepted.

My current script:

while getopts :t:n: arg; do
  case ${arg} in
    t)
      seconds=${OPTARG}
      if ! [[ $seconds =~ ^[1-9][0-9]*$ ]] ; then
        exit
      fi
      ;;
    n)
      count=${OPTARG}
      if ! [[ $count =~ ^[1-9][0-9]*$ ]] ; then
        exit
      fi
      ;;
    :)
      echo "$0: Must supply an argument to -$OPTARG" >&2
      exit
      ;;
    ?)
      echo "Invalid option: -${OPTARG}"
      exit
      ;;
  esac
done

Edit: This is for a homework assignment and am unsure if the order of arguments can change

Edit 2: Options can be in any order

  • Can we switch the order of arguments suc as: `script [ -t ] [ -n ] [ list ]`? – tshiono Mar 16 '22 at 01:50
  • @tshiono I should of stated this in the post (I will update it). This is for a homework assignment so no, the order cannot be changed. – JohnnAustin12108 Mar 16 '22 at 01:55
  • how do you distinguish between `-t 5` being the optional `[ -t ]` versus `[ list ]` ? – jhnc Mar 16 '22 at 02:00
  • What is `grep` supposed to do when `[ list ]` is zero strings? – jhnc Mar 16 '22 at 02:02
  • @jhnc I have a default value set for seconds and count if the `-t` or `-n` flags are not met. I have been unable to figure out how to take a string in the first place. So I do not believe I can fully answer your question. `grep` is combined with a `ps` command to list certain processes. – JohnnAustin12108 Mar 16 '22 at 02:09
  • what I mean is that `script -t 5` is ambiguous. `-t 5` could be either a 2-string `list`, or a 0-string `list` followed by a `-t ` – jhnc Mar 16 '22 at 04:34
  • and `grep` requires a pattern to search for, so your program needs to handle the case that zero patterns are supplied (technically, `grep ''` should work - it matches everything - but you need to decide if that is the desired behaviour) – jhnc Mar 16 '22 at 04:41
  • The standard when mixing options and raw arguments is to set the options first with getopt or getopts, then optionally stop at -- for end of option switch, and process any variable arguments afterward. – Léa Gris Mar 16 '22 at 13:09

4 Answers4

2

Would you please try the following:

#!/bin/bash

# parse the arguments before getopts
for i in "$@"; do
    if [[ $i = "-"* ]]; then
        break
    else                # append the arguments to "list" as long as it does not start with "-"
        list+=("$1")
        shift
    fi
done

while getopts :t:n: arg; do
    : your "case" code here
done

# see if the variables are properly assigned
echo "seconds=$seconds" "count=$count"
echo "list=${list[@]}"
tshiono
  • 21,248
  • 2
  • 14
  • 22
  • From the quick tests I have done, it appears to work for my use case! I really apriciate the help. To make sure I understand what it is doing: it cheks for strings, once a `-` is met, it goes to my case code and operates – JohnnAustin12108 Mar 16 '22 at 02:38
  • Thank you for the prompt feedback. Your understanding commented above is absolutely correct. If you have any questions in your further testing, please let me know. Cheers. – tshiono Mar 16 '22 at 02:52
  • 1
    As `$@` is *defaults argumentts*, `for varname in "$@" ;do` could be written `for varname ; do`. (`in "$@"` in implicit). – F. Hauri - Give Up GitHub Mar 16 '22 at 06:44
  • Why the break? You need continue. – Juergen Schulze Mar 06 '23 at 14:52
1

Try:

#! /bin/bash -p

# Set defaults
count=10
seconds=20

args=( "$@" )
end_idx=$(($#-1))

# Check for '-n' option at the end
if [[ end_idx -gt 0 && ${args[end_idx-1]} == -n ]]; then
    count=${args[end_idx]}
    end_idx=$((end_idx-2))
fi

# Check for '-t' option at the (possibly new) end
if [[ end_idx -gt 0 && ${args[end_idx-1]} == -t ]]; then
    seconds=${args[end_idx]}
    end_idx=$((end_idx-2))
fi

# Take remaining arguments up to the (possibly new) end as the list of strings
strings=( "${args[@]:0:end_idx+1}" )

declare -p strings seconds count
  • The basic idea is to process the arguments right-to-left instead of left-to-right.
  • The code assumes that the only acceptable order of arguments is the one given in the question. In particular, it assumes that the -t and -n options must be at the end if they are present, and they must be in that order if both are present.
  • It makes no attempt to handle option arguments combined with options (e.g. -t5 instead of -t 5). That could be done fairly easily if required.
  • It's OK for strings in the list to begin with -.
pjh
  • 6,388
  • 2
  • 16
  • 17
  • I have tested this one as well and it works with my test cases. This one is more strict on the order, so it may be better depending on what answer I recieve for my professor on if order matters. The homework does not state explicitly. Thank you! – JohnnAustin12108 Mar 16 '22 at 03:02
1

My shorter version

Some remarks:

  • Instead of loop over all argument**, then break if argument begin by -, I simply use a while loop.
  • From How do I test if a variable is a number in Bash?, added efficient is_int test function
  • As any output (echo) done in while getopts ... loop would be an error, redirection do STDERR (>&2) could be addressed to the whole loop instead of repeated on each echo line.
  • ** Note doing a loop over all argument could be written for varname ;do. as $@ stand for default arguments, in "$@" are implicit in for loop.
#!/bin/bash

is_int() { case ${1#[-+]} in
               '' | *[!0-9]* ) echo "Argument '$1' is not a number"; exit 3;;
           esac ;}

while [[ ${1%%-*} ]];do
    args+=("$1")
    shift
done

while getopts :t:n: arg; do
    case ${arg} in
        t ) is_int "${OPTARG}" ; seconds=${OPTARG} ;;
        n ) is_int "${OPTARG}" ; count=${OPTARG} ;;
        : ) echo "$0: Must supply an argument to -$OPTARG" ; exit 2;;
        ? ) echo "Invalid option: -${OPTARG}" ; exit 1;;
    esac
done >&2

declare -p seconds count args
F. Hauri - Give Up GitHub
  • 64,122
  • 17
  • 116
  • 137
1

Standard practice is to place option arguments before any non-option arguments or variable arguments.

getopts natively recognizes -- as the end of option switches delimiter. If you need to pass arguments that starts with a dash -, you use the -- delimiter, so getopts stops trying to intercept option arguments.

Here is an implementation:

#!/usr/bin/env bash

# SYNOPSIS
#   script [-t<secs>] [-n<count>] [string]...

# Counter of option arguments
declare -i opt_arg_count=0

while getopts :t:n: arg; do
  case ${arg} in
    t)
      seconds=${OPTARG}
      if ! [[ $seconds =~ ^[1-9][0-9]*$ ]] ; then
        exit
      fi
      opt_arg_count+=1
      ;;
    n)
      count=${OPTARG}
      if ! [[ $count =~ ^[1-9][0-9]*$ ]] ; then
        exit 1
      fi
      opt_arg_count+=1
      ;;
    ?)
      printf 'Invalid option: -%s\n' "${OPTARG}" >&2
      exit 1
      ;;
  esac
done

shift "$opt_arg_count" # Skip all option arguments

[[ "$1" == -- ]] && shift # Skip option argument delimiter if any

# Variable arguments strings are all remaining arguments
strings=("$@")

declare -p count seconds strings

Example usages

With strings not starting with a dash:

$ ./script -t45 -n10  foo bar baz qux
declare -- count="10"
declare -- seconds="45"
declare -a strings=([0]="foo" [1]="bar" [2]="baz" [3]="qux")

With string starting with a dash, need -- delimiter:

$ ./script -t45 -n10 -- '-dashed string'  foo bar baz qux
declare -- count="10"
declare -- seconds="45"
declare -a strings=([0]="-dashed string" [1]="foo" [2]="bar" [3]="baz" [4]="qux")
Léa Gris
  • 17,497
  • 4
  • 32
  • 41