Error with getopts in shell - bash

I have a homework, write a program schedsim.sh with:
schedsim.sh [-h] [-c x] -i filename
In this:
-h: print username
-c: print x+1 (x is entered from keyboard), if don't enter x, print 1
-i: print size of filename, filename is a name of file that entered.
My code:
#i/bin/bash/
while getopts ":hc:i:" Option
do
case $Option in
h)
whoami
;;
c) a=$OPTARG
if [ -z "$a" ]; then
a=1
else
a=`expr $a + 1`
fi
echo $a
;;
i) echo 'Size of file: Kylobytes'
ls -s $OPTARG
;;
*) echo 'sonething wrong'
;;
esac
done
However, when i call:
./schedsim.sh -c -i abc.txt
Error.
Sorry, my English is poor!

Seems like you had the basic script quite close to working. I made a few changes such as adding a test for the existence of the user specified file before attempting to run ls on it and adding quotes around variables. I suggest asking your teacher how they would like you to calculate kilobytes for the area where you are using ls. du or stat may be better for this use case.
#!/bin/bash/
while getopts ":hc:i:" Option
do
case "$Option" in
h) whoami
;;
c) a=$(( $OPTARG + 1 ))
printf "$a\n"
;;
i) if ! [ -f "$OPTARG" ]
then printf "File does not exist\n"
else printf "Size of file: Kylobytes: "
ls -s "$OPTARG"
printf "\n"
fi
;;
*) printf "something wrong\n"
;;
esac
done
Another change I made was to use $(()) shell arithmetic instead of expr. Generally if one needs more powerful mathematics than $(()) can support they call out to bc (which supports floating point calculations) instead of expr.

Related

Parsing obligatory parameter with colon using getopts

Is there possibility to parse obligatory argument containing colon using getopt in bash?
Let's say I've prepared code like below:
while getopts "qt:i:" arg; do
case "$arg" in
:)
HOST=$(printf "%s\n" "$1"| cut -d : -f 1)
PORT=$(printf "%s\n" "$1"| cut -d : -f 2)
shift 1
;;
-q)
QUIET=1
shift 1
;;
-t)
TIMEOUT="$2"
if [ "$TIMEOUT" = "" ]; then break; fi
shift 2
;;
-i)
INTERVAL="$2"
if [ "$INTERVAL" = "" ]; then break; fi
shift 2
;;
-h)
usage 0
;;
*)
echoerr "Unknown argument: $1"
usage 1
;;
esac
done
Full code can be found here: https://pastebin.com/1eFsG8Qn
How i call the script:
wait-for database:3306 -t 60 -i 10
Problem is that this logic can't parse HOST_URL:PORT.
Any tips how to parse it?
Is there possibility to parse obligatory argument containing colon using getopt in bash?
Sure - they have to be after options.
wait-for -t 60 -i 10 database:3306
then:
while getopts .... ;do
case ...
blabla) .... ;;
esac
done
shift "$(($OPTIND-1))"
printf "Connect to %s\n" "$1" # here
The colon in argument is not anyhow relevant.
Any tips how to parse it?
Use getopt - it reorders the arguments.

Is there a way to continue in a Flag when there is no $OPTARG set in Bash, GETOPTS?

I would like to build a script with getopts, that continues in the flag, when an $OPTARG isn't set.
My script looks like this:
OPTIONS=':dBhmtb:P:'
while getopts $OPTIONS OPTION
do
case "$OPTION" in
m ) echo "m"
t ) echo "t"
d ) echo "d";;
h ) echo "h";;
B ) echo "b";;
r ) echo "r";;
b ) echo "b"
P ) echo hi;;
#continue here
\? ) echo "?";;
:) echo "test -$OPTARG requieres an argument" >&2
esac
done
My aim is to continue at my comment, when there is no $OPTARG set for -P.
All I get after running ./test -P is :
test -P requieres an argument
and then it continues after the loop but I want to continue in the -P flag.
All clear?
Any Ideas?
First, fix the missing ;; in some of the case branches.
I don't think you can: you told getopts that -P requires an argument: two error cases
-P without an argument is the last option. In this case getops sees that nothing follows -P and sets the OPTION variable to :, which you handle in the case statement.
-P is followed by another option: getopts will simply take the next word, even if the next word is another option, as OPTARG.
Change the case branch to
P ) echo "P: '$OPTARG'";;
Then:
invoking the script like bash script.sh -P -m -t, the output is
P: '-m'
t
invoking the script like bash script.sh -Pmt, the output is
P: 'mt'
This is clearly difficult to work around. How do you know if the user intended the option argument to be literally "mt" and not the options -m and -t?
You might be able to work around this using getopt (see the canonical example) using an optional argument for a long option (those require an equal sign like --long=value) so it's maybe easier to check if the option argument is missing or not.
Translating getopts parsing to getopt -- it's more verbose, but you have finer-grained control
die() { echo "$*" >&2; exit 1; }
tmpArgs=$(getopt -o 'dBhmt' \
--long 'b::,P::' \
-n "$(basename "$0")" \
-- "$#"
)
(( $? == 0 )) || die 'Problem parsing options'
eval set -- "$tmpArgs"
while true; do
case "$1" in
-d) echo d; shift ;;
-B) echo B; shift ;;
-h) echo h; shift ;;
-m) echo m; shift ;;
-t) echo t; shift ;;
--P) case "$2" in
'') echo "P with no argument" ;;
*) echo "P: $2" ;;
esac
shift 2
;;
--b) case "$2" in
'') echo "b with no argument" ;;
*) echo "b: $2" ;;
esac
shift 2
;;
--) shift; break ;;
*) printf "> %q\n" "$#"
die 'getopt internal error: $*' ;;
esac
done
echo "Remaining arguments:"
for ((i=1; i<=$#; i++)); do
echo "$i: ${!i}"
done
Successfully invoking the program with --P:
$ ./myscript.sh --P -mt foo bar
P with no argument
m
t
Remaining arguments:
1: foo
2: bar
$ ./myscript.sh --P=arg -mt foo bar
P: arg
m
t
Remaining arguments:
1: foo
2: bar
This does impose higher overhead on your users, because -P (with one dash) is invalid, and the argument must be given with =
$ ./myscript.sh --P arg -mt foo bar
P with no argument
m
t
Remaining arguments:
1: arg
2: foo
3: bar
$ ./myscript.sh --Parg mt foo bar
myscript.sh: unrecognized option `--Parg'
Problem parsing options
$ ./myscript.sh -P -mt foo bar
myscript.sh: invalid option -- P
Problem parsing options
$ ./myscript.sh -P=arg -mt foo bar
myscript.sh: invalid option -- P
myscript.sh: invalid option -- =
myscript.sh: invalid option -- a
myscript.sh: invalid option -- r
myscript.sh: invalid option -- g
Problem parsing options
Do not mix logic with arguments parsing.
Prefer lower case variables.
My aim is to continue at my comment, when there is no $OPTARG set for -P
I advise not to. The less you do at one scope, the less you have to think about. Split parsing options and executing actions in separate stages. I advise to:
# set default values for options
do_something_related_to_P=false
recursive=false
tree_output=false
# parse arguments
while getopts ':dBhmtb:P:' option; do
case "$option" in
t) tree_output=true; ;;
r) recursive="$OPTARG"; ;;
P) do_something_related_to_P="$OPTARG"; ;;
\?) echo "?";;
:) echo "test -$OPTARG requieres an argument" >&2
esac
done
# application logic
if "$do_something_related_to_P"; then
do something related to P
if "$recursive"; then
do it in recursive style
fi
fi |
if "$tree_output"; then
output_as_tree
else
cat
fi
Example of "don't put programming application logic in the case branches" -- the touch command can take a -t timespec option or a -r referenceFile option but not both:
$ touch -t 202010100000 -r file1 file2
touch: cannot specify times from more than one source
Try 'touch --help' for more information.
I would implement that like (ignoring other options):
while getopts t:r: opt; do
case $opt in
t) timeSpec=$OPTARG ;;
r) refFile=$OPTARG ;;
esac
done
shift $((OPTIND-1))
if [[ -n $timeSpec && -n $refFile ]]; then
echo "touch: cannot specify times from more than one source" >&2
exit 1
fi
I would not do this:
while getopts t:r: opt; do
case $opt in
t) if [[ -n $refFile ]]; then
echo "touch: cannot specify times from more than one source" >&2
exit 1
fi
timeSpec=$OPTARG ;;
r) if [[ -n $timeSpec ]]; then
echo "touch: cannot specify times from more than one source" >&2
exit 1
fi
refFile=$OPTARG ;;
esac
done
You can see if the logic gets more complicated (as I mentioned, exactly one of -a or -b or -c), that the case statement size can easily balloon unmaintainably.

run shell script with or without options

I would like to run a shell script that will print either 'yesterday' or 'tomorrow' based on the option that is provided at the command line. If the option is -y, then the output should be 'yesterday', otherwise is 'tomorrow'. In addition I would like to add the option help -h which will print the syntax of the script.
I made the script as:
#! /bin/bash
h= y=
while getopt :f:vql opt
do
case $opt in
y) setday=true
;;
h) tohelp=true
;;
esac
done
shift $((OPTIND - 1))
if [setday=true]
NAME=$yesterday
else
NAME=$tomorrow
fi
if [tohelp=true]
MSG=$'runner [-y]'
echo $NAME
echo $MSG
but when I run it, I simply get an infinite loop that prints
-- opt
-- opt
-- opt
etc
What I am getting wrong?
This should work.
#!/bin/bash
setday=false
tohelp=false
yesterday="Yesterday"
tomorrow="Tomorrow"
for i in "$#"
do
case $i in
-y)
setday=true
shift
;;
-h)
tohelp=true
shift
;;
esac
shift
done
if [ $setday = true ]; then
NAME=$yesterday
else
NAME=$tomorrow
fi
if [ $tohelp = true ]; then
MSG='runner [-y]'
fi
echo $NAME
echo $MSG
#itachi's answer looks good. Although, if you want to continue using getopts, this should also work:
#! /bin/bash
yesterday="yesterday"
tomorrow="tomorrow"
setday=false
tohelp=false
while getopts "yh" opt
do
case $opt in
y)
setday=true
;;
h)
tohelp=true
;;
esac
done
shift $((OPTIND - 1))
if $tohelp; then
echo "runner [-y]"
exit
elif $setday; then
NAME=$yesterday
else
NAME=$tomorrow
fi
echo $NAME
Output:
$ ./test.sh
tomorrow
$ ./test.sh -y
yesterday
$ ./test.sh -h
runner [-y]

Bash Script to function as wc command

I am trying to create a script using getopts that would work as wc. the problem is that I get stuck when I use two switches together. The script:
while getopts l:w:c: choice
do
case $choice in
l) wc -l $OPTARG;;
w) wc -w $OPTARG;;
c) wc -c $OPTARG;;
?) echo wrong option.
esac
done
When I run this script with ./script.sh -l file it works, but when I use ./script -wl file it just goes into an infinite loop. Can anyone please explain what's going on and how to fix it?
You're using it incorrectly. As per the getopts manual:
If a letter is followed by a colon, the option is expected to have an
argument.
And in your example you're not passing argument for -w and -l options;
Correct usage is:
./script -w file1 -l file2
Which will process both options correctly.
Otherwise to support an option without argument just use it without colon like this:
while getopts "hl:w:c:" choice
Here option h will not need an argument but l, w, c will support one.
You need to build the options in the case statement and then execute wc:
# Set WC_OPTS to empty string
WC_OPTS=();
while getopts lwc choice
do
case $choice in
l) WC_OPTS+='-l';;
w) WC_OPTS+='-w';;
c) WC_OPTS+='-c';;
?) echo wrong option.
esac
done
# Call wc with the options
shift $((OPTIND-1))
wc "${WC_OPTS[#]}" "$#"
To add to the other comments . . . the version of wc that I have handy seems to handle its options like this:
#!/bin/bash
options=()
files=()
while (( $# > 0 )) ; do
if [[ "$1" = --help || "$1" = --version ]] ; then
wc "$1" # print help-message or version-message
exit
elif [[ "${1:0:1}" = - ]] ; then
while getopts cmlLw opt ; do
if [[ "$opt" = '?' ]] ; then
wc "$1" # print error-message
exit
fi
options+="$opt"
done
shift $((OPTIND-1))
OPTIND=1
else
files+="$1"
shift
fi
done
wc "${options[#]}" "${files[#]}"
(The above could be refined further, by using a separate variable for each of the five possible options, to highlight the fact that wc doesn't care about the order its options appear in, and doesn't care if a given option appears multiple times.)
Got a workaround.
#!/bin/bash
if [ $# -lt 2 ]
then
echo not a proper usage
exit
fi
file=$2
while getopts wlc choice
do
case $choice in
l) wc -l $file
;;
w) wc -w $file
;;
c) wc -c $file
;;
?) echo thats not a correct choice
esac
done
I think I got obsessed with OPTARG, thanks everyone for your kind help

Exclude string from wildcard in bash

I'm trying to adapt a bash script from "Sams' Teach Yourself Linux in 24 Hours" which is a safe delete command called rmv. The files are removed by calling rmv -d file1 file2 etc. In the original script a max of 4 files can by removed using the variables $1 $2 $3 $4.
I want to extend this to an unlimited number of files by using a wildcard.
So I do:
for i in $*
do
mv $i $HOME/.trash
done
The files are deleted okay but the option -d of the command rmv -d is also treated as an argument and bash objects that it cannot be found. Is there a better way to do this?
Thanks,
Peter
#!/bin/bash
# rmv - a safe delete program
# uses a trash directory under your home directory
mkdir $HOME/.trash 2>/dev/null
# four internal script variables are defined
cmdlnopts=false
delete=false
empty=false
list=false
# uses getopts command to look at command line for any options
while getopts "dehl" cmdlnopts; do
case "$cmdlnopts" in
d ) /bin/echo "deleting: \c" $2 $3 $4 $5 ; delete=true ;;
e ) /bin/echo "emptying the trash..." ; empty=true ;;
h ) /bin/echo "safe file delete v1.0"
/bin/echo "rmv -d[elete] -e[mpty] -h[elp] -l[ist] file1-4" ;;
l ) /bin/echo "your .trash directory contains:" ; list=true ;;
esac
done
if [ $delete = true ]
then
for i in $*
do
mv $i $HOME/.trash
done
/bin/echo "rmv finished."
fi
if [ $empty = true ]
then
/bin/echo "empty the trash? \c"
read answer
case "$answer" in
y) rm -i $HOME/.trash/* ;;
n) /bin/echo "trashcan delete aborted." ;;
esac
fi
if [ $list = true ]
then
ls -l $HOME/.trash
fi
You can make use of shift here.
Once you find -d is one of the options in the switch, you can shift and get rid of -d from the positional parameters. Next you can
until [ -z $1 ] ; do
mv $1 $HOME/.trash
shift
done
getopts sets OPTIND to the index of the first argument after the options. (#)
So after parsing the options you can do:
shift $OPTIND-1
to remove the options from the argument list.
Then use "$#" rather than $*, and you can handle files with spaces in them.
Thanks a lot!
I changed the code to read:
#!/bin/bash
# rmv - a safe delete program
# todo: add ability to handle wildcards
# uses a trash directory under your home directory
mkdir $HOME/.trash 2>/dev/null
# four internal script variables are defined
cmdlnopts=false
delete=false
empty=false
list=false
# uses getopts command to look at command line for any options
while getopts "dehl" cmdlnopts; do
case "$cmdlnopts" in
d ) echo -e "deleting: \n" "${#:2}" ; delete=true ;;
e ) echo -e "emptying the trash..." ; empty=true ;;
h ) echo -e "safe file delete v1.0"
echo -e "rmv -d[elete] -e[mpty] -h[elp] -l[ist] file [...]" ;;
l ) echo -e "your .trash directory contains:" ; list=true ;;
esac
done
shift $OPTIND-1
if [ $delete = true ]
then
for i in $#
do
mv $i $HOME/.trash
done
echo "rmv finished."
fi
then
/bin/echo "empty the trash? \c"
read answer
case "$answer" in
y) rm -i $HOME/.trash/* ;;
n) /bin/echo "trashcan delete aborted." ;;
esac
fi
if [ $list = true ]
then
ls -l $HOME/.trash
fi
This deletes the files as desired but I get this error:
/home/peter/rmv: line 21: shift: 2-1: numeric argument required
mv: invalid option -- 'd'
Try `mv --help' for more information.
You need to use
shift $(($OPTIND - 1))
to get red of the processed command line args. Try this version:
#!/bin/bash
# rmv - a safe delete program
# uses a trash directory under your home directory
mkdir -p $HOME/.trash
# uses getopts command to look at command line for any options
while getopts "dehl" cmdlnopts; do
case "$cmdlnopts" in
d ) delete=true;;
e ) echo "emptying the trash..." ; empty=true ;;
h ) echo "safe file delete v1.0"
echo "rmv -d[elete] -e[mpty] -h[elp] -l[ist] files" ;;
l ) echo "your .trash directory contains:" ; list=true ;;
esac
done
shift $(($OPTIND - 1))
if [ -n "${delete}" ]; then
echo "deleting: " "${#}"
mv ${#} $HOME/.trash
echo "rmv finished."
fi
if [ -n "${empty}" ]; then
read -p "empty the trash? " answer
case "$answer" in
y) rm -i $HOME/.trash/* ;;
n) echo "trashcan delete aborted." ;;
esac
fi
if [ -n "${list}" ]; then
ls -l $HOME/.trash
fi
Late to the party, but for Googlers, this will generate the error Peter describes:
shift $OPTIND-1
while the syntax in Jurgen's reply will not:
shift $(($OPTIND-1))
The problem is that $OPTIND-1 is interpreted as a string, and shift can't use a string as an argument. $(( )) is Bash's arithmetic expansion operator. You put a string inside it, the string is evaluated as an arithmetic expression, and the value returned.

Resources