Using getopts for handling multiple options

Hi Guys,

I have created a script for our automated DB creation, it works fine with default option(-d).

[oracle@server1 scripts]$ ./test_db.ksh -d abc 11 dev
-d is Default option
ORACLE_SID=abc
ORACLE_VERSION=11
ENV_TYPE=dev

For creating a customized DB, i thought of giving the user different options.
like
-b for different block size
-c for different character set

When i use these customizing options along with default one(-d), it fails

[oracle@server1 scripts]$ ./test_db.ksh -d abc 11 dev -b 8192 -c AL32UTF8
arguments allowed: 3

When i use just -b or -c and execute, it works

[oracle@server1 scripts]$ ./test_db.ksh -b 4096
-b is for Non-default Block Size
BLOCK_SIZE=4096

[oracle@server1 scripts]$ ./test_db.ksh -c AL32UTF8
-c is for non-default character set
CHARACTER_SET=AL32UTF8

Here is my Complete Script:

#!/bin/ksh

bflg=0
cflg=0
dflg=0
while getopts "b(blk):c(char):d(default):h(help)" opt
do      case $opt in
        (b)     bflg=1;;

        (c)     cflg=1;;

        (d)     dflg=1;;

        (h)     echo "Sample Script"
                exit 0;;

        (?)     echo "Not valid option"
                exit 1;;
        esac
done
shift $((OPTIND - 1))

# Verify that at least one option was given and that two operands are present...
if [ $((bflg + cflg + dflg)) -eq 0 ]
then
        echo "Atleast One option required"
        exit 3
fi

# Perform the requested actions...


if [ $bflg -eq 1 ]
then
        if [ $# -ne 1 ]
        then
                echo "arguments allowed: 1"
                exit 2
        else
                BLK_SIZE=$1
                echo "-b is for Non-default Block Size"
                echo "BLOCK_SIZE=$BLK_SIZE"
        fi
fi

if [ $cflg -eq 1 ]
then
        if [ $# -ne 1 ]
        then
                echo "arguments allowed: 1"
                exit 2
        else

                CHAR_SET=$1
                echo "-c is for non-default character set"
                echo "CHARACTER_SET=$CHAR_SET"
        fi
fi

if [ $dflg -eq 1 ]
then
        if [ $# -ne 3 ]
        then
                echo "arguments allowed: 3"
                exit 2
        else
                ORACLE_SID=$1
                ORACLE_VER=$2
                ENV_TYPE=$3
                echo "-d is Default option"
                echo "ORACLE_SID=$ORACLE_SID"
                echo "ORACLE_VERSION=$ORACLE_VER"
                echo "ENV_TYPE=$ENV_TYPE"
        fi
fi

I want this script to work when i use -b or -c or both(-b &-c) along with -d option.

Can you guys please guide me in achieving this.

Please let me know if you need more information.

You could check for arguments before setting flags...

[[ $# -lt 2 ]] && echo "Requires at least 2 arguments..." && exit 1

Additionly, it gets difficult if your options require 2 arguments as shown in:

$ ./test_db.ksh -d abc 11 dev -b 8192 -c AL32UTF8

AFAIK: getopts can only handle options before the actual arguments.
NOTE: There is a getopt and a getopts, not sure if either (or which) one of them is for ksh...
Specialy, NONE of your options catch arguments you want to pass...

In some occasions, it even makes sense to force the user to enter variables in a certain order, with the last one beeing optional.
Or that options just trigger the handler (but have a preset value).

Furthermore, using getopts provides you with $OPTARG , which represents (somewhat) $1 .
Once you 'passed' the getopts part, and the according shift $(($OPTIND-1)) , the remaining ARGUMENTS, since all options and their arguments, have been removed by the previous shift command.

Saying, in your current flag check blocks, you are refering to empty (since removed) variables, thus the script fails.
You need to set all required argument of your options inside the getopts block.

In the like of (cut-out snippet from one of my scripts):

...
while getopts "b:f:F" opt
	case $opt in
	b)	char="${OPTARG:0:1}"
		case "$char" in
		a)	log_msg="Override audio bitrate ($BIT_AUDIO) with ${OPTARG:1}"
			BIT_AUDIO="${OPTARG:1}"
			;;
		v)	log_msg="Override video bitrate ($BIT_VIDEO) with ${OPTARG:1}"
			BIT_VIDEO="${OPTARG:1}"
			;;
		*)	log_msg="You did not define whether its audio or video: -$opt a|v$OPTARG"
			tui-status 1 "$log_msg"
			exit 1
			;;
		esac
		;;
	f)	useFPS=true
		FPS_ov="$OPTARG"
		log_msg="Force using $FPS_ov FPS"
		;;
	F)	useFPS=true
		doLog "Force using FPS from config file ($FPS)"
		;;	
	esac
done
....
	if $useFPS
	then	[[ -z $FPS_ov ]] || FPS=$FPS_ov
		cmd_video_all+=" -r $FPS"
		doLog "Added '$FPS' to commandlist"
	fi
	

Hope this helps

EDIT: Please note that my example is bash.

Hi.

I have not tried to use multiple, whitespace-sparated arguments to options, so I would try first setting up your code to assume either a quoted string or a comma-separated string:

-d "s1 s2 s2"

or

-d s1,s2,s3

Of course, you will need to do the work of splitting the string into the correct variables.

The version of getotps in at least ksh 93u+ allows complex forms, so perhaps it can accept arguments such as you desire. See getopts --man

I think there are other codes which may process multiple strings such as you have intended to use, but I am fairly sure that the ksh getopts is not one of them.

Appendix B of Learning the Korn Shell, 2nd, has many more details on getopts.

I seem to recall seeing simple shell code to collect strings until they hit an argument that began with with a [+-] -- so you could roll your own.

Apologies for the brevity; keep us posted.

Best wishes ... cheers, drl

How about using ":" between your -d args.

$ ./test_db.ksh -d abc:11:dev -b 12
-b is for Non-default Block Size
BLOCK_SIZE=12
-d is Default option
ORACLE_SID=abc
ORACLE_VERSION=11
ENV_TYPE=dev

Here is the script:

#!/bin/ksh

while getopts "b:c:d:h" opt
do      case $opt in
        (b)     BLK_SIZE=${OPTARG};;

        (c)     CHAR_SET=${OPTARG};;

        (d)     DARGS=${OPTARG};;

        (h)     echo "Sample Script"
                exit 0;;

        (?)     echo "Not valid option"
                exit 1;;
        esac
done
shift $((OPTIND-1))

# Verify that at least one option was given and that two operands are present...
if [ -z "${BLK_SIZE}${CHAR_SET}${DARGS}" ]
then
        echo "Atleast One option required"
        exit 3
fi

# Perform the requested actions...


if [ -n "$BLK_SIZE" ]
then
    echo "-b is for Non-default Block Size"
    echo "BLOCK_SIZE=$BLK_SIZE"
fi

if [ -n "$CHAR_SET" ]
then
    echo "-c is for non-default character set"
    echo "CHARACTER_SET=$CHAR_SET"
fi

if [ -n "$DARGS" ]
then
then
     IFS=":" read ORACLE_SID ORACLE_VER ENV_TYPE <<EOF
$DARGS
EOF

     if [ -z "$ENV_TYPE" ]
     then
         echo "Invalid -d option requires 2 colons ($DARGS is invalid)"
         exit 1
     fi
     echo "-d is Default option"
     echo "ORACLE_SID=$ORACLE_SID"
     echo "ORACLE_VERSION=$ORACLE_VER"
     echo "ENV_TYPE=$ENV_TYPE"
fi
1 Like

Hi.

Apparently this looked familiar to me because I had written one.

This script, s5, will show 3 methods for accumulating multiple arguments:

#!/usr/bin/env ksh

# @(#) s5	Demonstrate collecting arguments, splitting arguments.
# -a string can be repeated
# -b delimited string
# -c strings until end or next option
# -d delimiter character

# Utility functions: print-as-echo, print-line-with-visual-space, debug.
# export PATH="/usr/local/bin:/usr/bin:/bin"
LC_ALL=C ; LANG=C ; export LC_ALL LANG
pe() { for _i;do printf "%s" "$_i";done; printf "\n"; }
pl() { pe;pe "-----" ;pe "$*"; }
db() { ( printf " db, ";for _i;do printf "%s" "$_i";done;printf "\n" ) >&2 ; }
db() { : ; }
# C=$HOME/bin/context && [ -f $C ] && . $C

echo
echo " Args before \"$*\""
aindex=0
cindex=0
# declare -a a	# for bash
# declare -a c	# for bash
err=0
del=","
while getopts a:b:c:d: opt
do
  case $opt in
    a)	a[$aindex]=$OPTARG ; (( aindex++ )) ;;
    b)	b=$OPTARG ;;
    # could use bash/ksh patterns in [[ ]] in place of grep
    
    c)
      db " before c loop, args #, 1, 2 = :$#:, :$1:, :$2:"
      c[$cindex]=$2
      shift
      while [ -n "$1" ] && echo "$2" | grep -v -q '^[-+]'
      do
        c[$cindex]=$1 ; (( cindex++ ))
        shift
        db " at end loop, $# args are [$*]"
      done
	  if [ -n "$1" ]
	  then
        c[$cindex]=$1
	  fi
      KEEPER=$2
      set -- $2 $*
    ;;
    d)	del=$OPTARG ;;
    *)	(( err++ )) ;;
  esac
done
if (( OPTIND-1 <= $# ))
then
shift $(( OPTIND-1 ))
fi
echo " Args after  \"$*\""

[[ $err != 0 ]] && ( echo " Errors: $err, aborting." ; exit 1 )

# Continue processing.
# Display the content of the collected array: a.
if [[ ${#a} > 0 ]]
then
  echo
  echo " Multiply-specified arguments of option a, collected in array:"
  for (( i=0 ; i<${#a[*]} ; i++ ))
  do
    echo " $i ${a[$i]}"
  done
fi

# Continue processing.
# Display the content of the collected array: c.
if [[ ${#c} > 0 ]]
then
  echo
  echo " Collected sequence arguments of option c :"
  for (( i=0 ; i<${#c[*]} ; i++ ))
  do
    echo " $i ${c[$i]}"
  done
fi

# Split the delimiter-separated items.
if [ -n "$b" ]
then
  echo
  echo " Elements that were separated by \"$del\":"
  v=( $( echo "$b" | sed "s/$del/ /g" ) )
  i=0
  for j in ${v[*]}
  do
    echo " $i $j"
    (( i++ ))
  done
fi

exit 0

This is a driver script, run5, that will exercise the methods:

#!/usr/bin/env ksh

# @(#) run5	Exercise script s5 to demonstrate methods.

# Utility functions: print-as-echo, print-line-with-visual-space, debug.
# export PATH="/usr/local/bin:/usr/bin:/bin"
LC_ALL=C ; LANG=C ; export LC_ALL LANG
pe() { for _i;do printf "%s" "$_i";done; printf "\n"; }
pl() { pe;pe "-----" ;pe "$*"; }
db() { : ; }
db() { ( printf " db, ";for _i;do printf "%s" "$_i";done;printf
"\n" ) >&2 ; }
C=$HOME/bin/context && [ -f $C ] && . $C

# Test multiply-specified options.
pl " Multiply-specified options:"
./s5 -a alpha -a beta file1 ... filen

# Test comma-separated methods.
pl " Comma-separated methods:"
./s5 -b 3,4,5 file1 ... filen

# Showing unexpected results for -b=..."
pl " Expected anomaly for -b=...:"
./s5 -b=6,7

# Choose a different delimiter:"
pl " Select delimiter \":\" instead of \",\":"
./s5 -d ":" -b 8:9 

# Command not found error when using bare \"|\":"
pl " Expect error for unprotected \"|\":"
./s5 -d | -b 10|11

# Protect \"|\" with quotes:"
pl " Better results for protected pipe symbol:"
./s5 -d "|" -b "10|11"

pl " Collect until new -option letter:"
./s5 -c able baker charlie -a first -a second

pl " Collect again until new -option letter:"
./s5 -c delta echo foxtrot -- file1 file2

pl " Collect once more until new -option letter or end:"
./s5 -c golf hotel india

exit 0

Executing run5 produces:

$ ./run5

Environment: LC_ALL = C, LANG = C
(Versions displayed with local utility "version")
OS, ker|rel, machine: Linux, 2.6.26-2-amd64, x86_64
Distribution        : Debian 5.0.8 (lenny, workstation) 
ksh 93s+

-----
 Multiply-specified options:

 Args before "-a alpha -a beta file1 ... filen"
 Args after  "file1 ... filen"

 Multiply-specified arguments of option a, collected in array:
 0 alpha
 1 beta

-----
 Comma-separated methods:

 Args before "-b 3,4,5 file1 ... filen"
 Args after  "file1 ... filen"

 Elements that were separated by ",":
 0 3
 1 4
 2 5

-----
 Expected anomaly for -b=...:

 Args before "-b=6,7"
 Args after  ""

 Elements that were separated by ",":
 0 =6
 1 7

-----
 Select delimiter ":" instead of ",":

 Args before "-d : -b 8:9"
 Args after  ""

 Elements that were separated by ":":
 0 8
 1 9

-----
 Expect error for unprotected "|":
./s5: -d: argument expected
./run5: line 33: -b: not found
./run5[33]: 11: not found [No such file or directory]

-----
 Better results for protected pipe symbol:

 Args before "-d | -b 10|11"
 Args after  ""

 Elements that were separated by "|":
 0 10
 1 11

-----
 Collect until new -option letter:

 Args before "-c able baker charlie -a first -a second"
 Args after  ""

 Multiply-specified arguments of option a, collected in array:
 0 first
 1 second

 Collected sequence arguments of option c :
 0 able
 1 baker
 2 charlie

-----
 Collect again until new -option letter:

 Args before "-c delta echo foxtrot -- file1 file2"
 Args after  "file1 file2"

 Collected sequence arguments of option c :
 0 delta
 1 echo
 2 foxtrot

-----
 Collect once more until new -option letter or end:

 Args before "-c golf hotel india"
 Args after  ""

 Collected sequence arguments of option c :
 0 golf
 1 hotel
 2 india

Best wishes ... cheers, drl

( Edit 1: correct minor typo )

Sometimes, manual interaction is simpler than using builtins...

In this example, we parse the arguments as they are occouring, and remove them as soon they are read.
(untested but should work)

# Sometimes, manual interaction is simpler than using builtins...


# Removing args as they are parsed
for A in $@ ; do
	case $A in
	-b)	shift
		rate=$1
		shift
		;;
	-c)	# Set the vars
		shift
		charA=$1
		numB=$2
		charC=$3
		
		# Remove them from arg list
		shift 3
		;;
	-d)	shift
		name=$1
		shift
		;;
	esac
done

[[ -z $rate ]] && echo "B was passed"
[[ -z $charC ]] && echo "C was passed"
[[ -z $name ]] && echo "D was passed"

What you would need to do, is to check (specialy for -c ) if the 'following' arguments, do start with a "-" or otherwise not match.
Remind you, since the -b or -c was already removed, you can simply check (for example) if there are enough (3) follow up arguments by [[ 3 -eq $# ]] inside the case block.

Hope this helps

Hi veeresh_15,
The "complete" script you provided shows us why you are having problems using getopts , but it doesn't explain what you're really trying to do. You have shown us four sample command lines used to test your option handling capabilities, but to understand how to best help you, what we really need is the Synopsis lines from the man page for your utility (or better yet, the complete man page) so we can understand what options (with and without option arguments) and what operands your script will need to process for your utility.

It is unusual to need an option for "defaults". If you're setting up a utility to create databases, what you're describing as defaults (the database ID, version, and type) don't seem like defaults; they seem like something that would be required to set up any database and they would be different for each database you create. If that is the case here, these three items should be mandatory operands; not options.

I can easily see having default values for database block size and character sets that would apply to most databases you create. And, having options (with option arguments) to override those defaults is perfectly reasonable.

So, if I have guessed correctly, your Synopsis would be something like:

createdb [-b block_size] [-c character_set] sid version type

and your code should be something more like:

#!/bin/ksh
# createdb [-b block_size] [-c character_set] sid version type

# Initialize variables
BLK_SIZE=4096
CHAR_SET="AL32UTF8"
IAm=${0##*/}
USAGE='Usage: %s [-b block_size] [-c character_set] sid version type\n'

# Process options:
while getopts "b:c:h" opt
do	case $opt in
	(b)	BLK_SIZE="$OPTARG";;

	(c)	CHAR_SET="$OPTARG";;

	(h)	printf "$USAGE" "$IAm"
		exit 0;;

	(?)	printf "$USAGE" "$IAm" >&2
		exit 1;;
	esac
done
shift $((OPTIND - 1))

# Verify operands:
if [ $# -ne 3 ]
then	printf '%s: 3 operands are required, %d found.\n' "$IAm" >&2
	printf "$USAGE" "$IAm" >&2
	exit 2
fi
ORACLE_SID="$1"
ORACLE_VER="$2"
ENV_TYPE="$3"

# Create the database:
printf 'Create database with these attributes:
	block size:	%d
	character set:	%s
	SID:		%s
	version:	%s
	type:		%s
' "$BLK_SIZE" "$CHAR_SET" "$ORACLE_SID" "$ORACLE_VER" "$ENV_TYPE"
# Add code to acutally create the database here...

And, then you could successfully invoke it with command lines like:

createdb -b2048 MyDatabase 1 MyType
createdb -c USASCII MyDatabase 1 MyType
createdb -b 20480 -c"UTF-8" MyDatabase 1 MyType

PS
If you're using a 1993 or later version of ksh , you'll get bad option diagnostics consistent with the rest of the diagnostics produced by the above script if you change:

while getopts "b:c:h" opt

to:

while getopts -a "$IAm" "b:c:h" opt
1 Like

Hi Don Cragun,

Apologies for the ambiguity in my question, you have correctly understood my requirement. My idea of having -d (default) was to have those three arguments mandatory.

Your sample code really serves my purpose, thanks a lot for your time and guidance!

Regards,
Veeresham

---------- Post updated at 03:48 PM ---------- Previous update was at 01:56 PM ----------

Hi Don,

Script works fine as long as i provide -b & -c and their arguments before the mandatory arguments.

createdb -b 20480 -c"UTF-8" MyDatabase 1 MyType

But if i execute the script this way:

createdb MyDatabase 1 MyType -b 20480 -c"UTF-8" 

Its not taking the values specified with -b or -c. Instead its taking the default values defined in the script.

Regards,
Veeresham

Yes. The standard Utility Syntax Guidelines say that options should come before operands on the command line. The getopts utility assumes that your command lines follow this guideline.

Instead of taking the default values, the script I gave you should have generated a diagnostic something like:

createdb: 3 operands are required, 6 found.
Usage: created [-b block_size] [-c character_set] sid version type

I know that some systems "reorder" arguments passed to some utilities to put what it believes are options before what it believes are operands. The standards don't allow this; options are required to be presented before operands except in a very few cases (like compilers) where options may need to vary for different operands. I don't see the need for the extra complexity for the script you have described in this thread.

You can code around this restriction, but it makes option and operand parsing MUCH more difficult. I won't offer to modify the script I suggested earlier to allow options to be mixed in with operands.

Sorry,
Don