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
Related
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
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[#]}"
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 -.
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
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")
I have written a script that gets a variable number of arguments:
test.sh -i <input1> <input2> ... -o <output1> <output2> ...
I'm parsing the arguments as follows:
while [ $# -gt 1 ]; do
TMP=$(echo "$#" | cut -d '-' -f 2) #i <input1> <input2>
TMP1=$(echo "$TMP" | cut -d ' ' -f 1) #i
CNT=$(echo "$TMP" | wc -w) #3
set -x
case "$TMP1" in
i)
INPUTS=$(echo "$TMP" | cut -c 3-)
shift "$CNT"
;;
o)
OUTPUTS=$(echo "$TMP" | cut -c 3-)
shift "$CNT"
;;
esac
done
This works everytime, except for files that happen to have a '-' in their name.
Example:
./test.sh -i file1.txt file-2.txt -o out1.txt out-2.txt
Is there anyway I can force cut to ignore delimiters that occur within the file names?
You don't need all this string manipulation; each argument is already a separate word.
while (( $# > 0 )); do
case $1 in
-i) shift
while [[ $# -gt 0 && $1 != -* ]]; do
inputs+=( "$1" )
shift
done
;;
-o) shift
while [[ $# -gt 0 && $1 != -* ]]; do
outputs+=( "$1" )
shift
done
;;
*) echo "Unrecognized option $1"
exit 1
;;
esac
done
This can be refactored a little to avoid the repeated checks for running out of arguments.
for arg in "$#"; do
case $1 in
-i) mode=input; continue ;;
-o) mode=output; continue ;;
esac
case $mode in
input) input+=("$arg") ;;
output) output+=("$arg") ;;
*) echo "Unknown mode: $mode"
exit 1
;;
esac
done
Here's an alternative approach that may benefit someone.
The fact is, argument parsing is always a tradeoff, hence there's benefit in tailoring it to the application. Here's a pretty generic solution that allows a little bit of error checking and disorder in the arguments.
It's very simple, but I have added some example output and comments, and for the sake of readability and compatibility, stayed away from complex ways to save a line or two (especially on the if statements).
Sample Usage:
bash #> touch file-1 file3 file4 file-8 file7
bash #> argparse -i file-1 file3 file4 -c -k --q --j -r -t -o file-8 file7
Output:
Input files: file-1 file3 file4
Output files: file-8 file7
Args are: c k q j r t
Doing action for argument "c"
Doing action for argument "k"
Doing action for argument "j"
Script:
#!/bin/bash
#argparse
#Assign arrays
until [[ $# < 1 ]]; do
#ignore args "-i" and "-o", and tell the script to check for files following
if [ "$1" == "-i" ] ; then unset output ; input=1 ; shift
elif [ "$1" == "-o" ] ; then unset input ; output=1 ; shift
fi
#Add input and output files to respective arrays
if [ -f "$1" ] ; then
if [[ $input == 1 ]]; then
infiles+=($1)
elif [[ $output == 1 ]]; then
outfiles+=($1)
fi
else
#Add args to array
arg="$(echo "$1" | sed 's/-//g')"
args+=($arg)
fi
shift
done
#Some debug feedback
echo -e "Input files: ${infiles[#]}\nOutput files: ${outfiles[#]}\nArgs are: ${args[#]}\n"
#Simulate actually "doing" something with the args
for arg in "${args[#]}" ; do
case $arg in
"c") echo "Doing action for argument \"c\"" ;;
"k") echo "Doing action for argument \"k\"" ;;
"j") echo "Doing action for argument \"j\"" ;;
*) ;;
esac
done
Update/Edit: I've just realised, that the OP didn't have any requirement for parsing actual arguments other than -i and -o. Well regardless, this may still come in handy for someone at some point.
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.
I have to write a bash script which will count all the commands in a text file. Arguments to a script are -p, -n num, and a file. This means that commands like:
script.sh -n 3 -p file.txt
script -p -n 3 file.txt
and similar are all legit.
However, I have to echo an error for any commands that are not similar to this: script.sh -n -k file.txt for example.
Here is a link to my code.
I managed to make it work, but it is way too long and redundant. Is there a way I can do this in a short way?
You may want to have a look at one of the following standard commands:
getopts is a Bash builtin. It is newer and simple to use, but does not support long options (--option).
getopt is an external program which may involve a little more glue code. There are different implementations. getopt usually supports long options.
This is a small getopts example (modified one of the examples from this external site):
#!/bin/bash
flag=off
dir=
# iterate over each option with getopts:
while getopts fd: opt
do
case "$opt" in
f) flag=on;;
d) dir="$OPTARG";;
*) echo >&2 "usage: $0 [-f] [-d directory] [file ...]"
exit 1;;
esac
done
# remove all positional pararmeters we already
# handled from the command line:
shift $(( expr $OPTIND - 1 ))
# main part of your program, remaining arguments are now in
# $# resp. $0, $1, ...
I'd like to suggest another snippet that is a lot simpler to read than yours, because it exactly depicts the only two valid cases you specified in your comment:
If I want to "call" my script it has to look like this: script.sh -n +number -p file.txt. file.txt must be the last argument, however, -n and -p can be switched.
So the cases are ($0 to $4):
script.sh -n +number -p file.txt
script.sh -p -n +number file.txt
It uses only if and Bash's logical operators:
#!/bin/bash
if ! { [[ "$1" = "-n" ]] && [[ "$2" =~ ^-[0-9]+$ ]] && [[ "$3" = "-p" ]] && [[ "$4" =~ ".txt"$ ]] ; } &&
! { [[ "$2" = "-n" ]] && [[ "$3" =~ ^-[0-9]+$ ]] && [[ "$1" = "-p" ]] && [[ "$4" =~ ".txt"$ ]] ; }
then
echo "Error" && exit 1
fi
Notes:
The group ({, }) syntax expects a ; at the end of its list.
You have to use a regex to check for *.txt
The number regex you gave will require the number to start with a -, while in your specification you say +.
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.