Subject: misc/10259: example in getopt(1) man page is broken
To: None <gnats-bugs@gnats.netbsd.org>
From: None <jbernard@mines.edu>
List: netbsd-bugs
Date: 06/01/2000 19:55:18
>Number: 10259
>Category: misc
>Synopsis: example in getopt(1) man page is broken
>Confidential: no
>Severity: non-critical
>Priority: low
>Responsible: misc-bug-people
>State: open
>Class: doc-bug
>Submitter-Id: net
>Arrival-Date: Thu Jun 01 19:56:00 PDT 2000
>Closed-Date:
>Last-Modified:
>Originator: Jim Bernard
>Release: May 30, 2000
>Organization:
Speaking for myself
>Environment:
System: NetBSD zoo 1.4Y NetBSD 1.4Y (ZOO-$Revision: 1.52 $) #0: Fri May 26 14:58:14 MDT 2000 jim@zoo:/home/tmp/compile/sys/arch/i386/compile/ZOO i386
>Description:
The usage example (under the heading EXAMPLE) in the getopt(1) man
page is incorrect. While it is true that when executed as shown
in the example, a seemingly correct result is obtained, that is just
an accident specific to the assumed input. For example, if one
executes the example with the command line "cmd -a -c arg" (no
filename arguments), the shell complains: "shift: can't shift that
many". A slightly extended version of the script (which supports
two options that take arguments), presented below, exhibits more
catastrophic failure, yielding incorrect values for variables it
sets. A working version of the code is also given in the script
below.
The source of this problem is that the example performs an extra
shift inside the case statement when an option that takes an
argument is processed. However, the shift before the case
statement is processed on every traversal of the loop, which includes
the arguments of the options that take arguments. In those cases,
the extra shift done in the case statement is superfluous, causing
the processing to get out of sync. If there are enough extra
arguments at the end of the command line and only one extra shift
from an option argument, there will be no complaints, and the example
will appear to work, even though the command line is not processed
correctly and one of the trailing "filename" arguments is lost. In
other circumstances (see below), the incorrect processing can cause
more obvious failures. (I imagine the example was originally
constructed under the assumption that the shifts caused the list
being processed by the for statement to be modified on each
iteration, but in fact it is static.)
In addition, giving an argument of "-a" to the option "-b" will cause
the code in the example to believe erroneously that the "-a" flag was
present (see discussion below in the fix section).
>How-To-Repeat:
Try this script, with, e.g., "cmd -a -b bb -c cc" with both versions
of the code; note the incorrect processing of the command line
with the first form (which corresponds to the approach used in the
example in the man page) and the correct processing with the second
form.
#! /bin/sh
args=`getopt ab:c: $*`
if [ $? != 0 ]
then
echo 'Usage: ...'
exit 2
fi
set -- $args
# This doesn't work [it's like the example in getopt(1)]:
for i; do
echo "arg: $i"
echo " arg 1 before shift: $1"
shift
echo " arg 1 after shift: $1"
case "$i" in
-a) flag=$i ;;
-b) barg=$1; shift;;
-c) carg=$1; shift;;
--) break;;
esac
done
# This does work:
#while [ $# > 0 ]; do
# echo "arg 1 before case: $1"
# case "$1" in
# -a) flag=$1 ;;
# -b) barg=$2; shift;;
# -c) carg=$2; shift;;
# --) shift; break;;
# esac
# echo " arg 1 after case, before shift: $1"
# shift
# echo " arg 1 after shift: $1"
#done
echo "flag: $flag"
echo "barg: $barg"
echo "carg: $carg"
echo "remaining args: $*"
>Fix:
The simplest fix, and the simplest resulting code would be to
remove the shifts inside the case statement, so that all of the
shifting is done by the single shift preceding the case statement.
However, that would still be subject to a somewhat more subtle
failure: the command line "cmd -b -a -c cc" would erroneously
report that the -a flag had been set (because the arguments of
options are processed by the case statement), instead of taking
the -a as the argument of -b. One way of coding the script that
works correctly is shown in the script above, using while instead
of for. I've used an approach like that in this patch for the man
page:
--- getopt.1.orig Wed Dec 1 05:41:34 1999
+++ getopt.1 Thu Jun 1 20:01:38 2000
@@ -48,23 +48,23 @@
if [ $? != 0 ]; then
echo 'Usage: ...'
exit 2
fi
set \-\- $args
-for i; do
- shift
- case "$i"; in
+while [ $# > 0 ]; do
+ case "$1"; in
\-a|\-b)
- flag=$i
+ flag=$1
;;
\-c)
- carg=$1; shift
+ carg=$2; shift
;;
\-\-)
- break
+ shift; break
;;
esac
+ shift
done
.Ed
.Pp
This code will accept any of the following as equivalent:
.Pp
>Release-Note:
>Audit-Trail:
>Unformatted: