I wrote a bash script to print contents of files in a given directory in a loop.
How can I improve command line parameters handling to the script? I feel the parameter processing as of now uses too much code and it is quite clumsy. I think I had to write too much code to validate the the option arguments are numeric.
The script takes either -c numeric_choice_id
and/or -l time_in_seconds
and keep printing the contents of the files.
- only
-c
number: it cats the file and ends - only
-l
number: repeatedly cats all the files -c num1
and-l num2
: repeatedly cats that particular file everynum2
seconds
#!/bin/bash
SYSPATH="$PWD/"
FILES="$PWD/file1
$PWD/file2
$PWD/file3"
count=0
for i in $FILES
do
(( count++ ))
menu="${menu}\n${count}. `basename $i`"
list="${list} `basename $i`"
done
testNumeric()
{
if ! echo "1ドル" | grep -q '^[0-9]\+$'
then
echo "0ドル Non numeric argument supplied to choice or time"
exit
fi
}
usage()
{
echo -e "`basename 0ドル` -c numeric_choice [ -l time in seconds ]\n
-c numeric id for the choice
-l print o/p every time seconds
-h print this menu
choice could be: \n$menu"
exit
}
# Test if any arguments have been provided
if [ "x1ドル" = "x" ] || [ "1ドル" = "-h" ]
then
usage
fi
# Allow getopt to parse the options and report error if needed
args=`getopt "c:l:" "$@"`
if [ "$?" -ne "0" ]
then
usage
fi
# Set positional parameters
set -- $args
for i;
do
case "$i" in
-c )
choice=2ドル
testNumeric $choice
shift
shift ;;
-l ) repeatTime=2ドル
testNumeric "$repeatTime"
shift
shift ;;
-- )
break ;;
esac
done
[ "x$choice" = "x" ] && choice=`seq $count`
[ "x$repeatTime" = "x" ] && repeatTime=0
set -- $list
while [ true ]
do
if [ "$repeatTime" = "0" ]
then
for a in $choice
do
eval temp=\${${a}}
cat $SYSPATH${temp}
done
break;
else
for a in $choice
do
eval temp=\${${a}}
cat $SYSPATH${temp}
done
sleep $repeatTime
fi
done
2 Answers 2
Storing a list of file names and iterating over it
FILES="$PWD/file1 $PWD/file2 $PWD/file3"
So you're setting FILES
to a newline-separated list of files. That's a bad idea because your script will choke if a file name contains a newline. In some circumstances, where you control the file names, that can be ok, but don't do it unless you really have no better way. Since you're writing a bash script, you never need to do this. Store the file names in an array instead:
FILES=("$PWD/file1" "$PWD/file2" "$PWD/file3")
Then, to iterate over the array:
for i in "${FILES[@]}"; do ...
If you had a shell without arrays and decided to use a newline-separated list of file names, then you would need some extra effort to split the lines at newlines only. The IFS
variable specifies where to split words in unquoted variable substitutions, so you need to set it to contain only a newline (the default value also contains the ordinary space character and the tab character). Also, the shell performs globbing (filename generation from wildcards) on the results of unquoted variable substitutions, so you need to turn that feature off. Here's a POSIX-compliant way of iterating over the pieces of a newline-delimited string:
IFS='
'
set -g
for i in $FILES; do ...
Since you never change directories, you could store just the files' base names instead of full paths. If you control the file names, you then don't need to be so careful with special characters in the names.
Always use double quotes around variable substitutions
I see a lot of places where you write unquoted variable substitutions, e.g. basename $i
. Do you really want to split $i
into words, then perform globbing on the result? If you don't, then use double quotes: "$i"
. Only leave off the double quotes if you intend to perform word splitting and filename generation (as in the for loop example above), and then be mindful of the assumptions you're making on the value of the variable.
On the same note, if there was a risk that $i
began with a -
, you would need to call basename -- "$i"
to avoid $i
being interpreted by basename
as an option. This is not going to happen with your script since $i
is an absolute path, beginning with /
.
I also recommend not using the backquote command susbsitution, which behaves strangely with nested quotes sometimes. Use $(...)
, which has an intuitive syntax.
menu="${menu}\n${count}. $(basename -- "$i")"
Incidentally, note that this puts the characters \n
into menu
, not a newline. The \n
is expanded by echo -e
below.
Testing for a number
if ! echo "1ドル" | grep -q '^[0-9]\+$'
No need to call grep
for that! Here's a simple bash construct:
if [[ 1ドル = *[^0-9]* ]]; then ... # non-numeric
Here's a POSIX construct:
case 1ドル in
*[!0-9]*) ...;; # non-numeric
esac
Here documents
To insert a multiline string in a script, use a here document. It's more readable.
usage()
{
cat <<EOF
$(basename -- "0ドル") -c ID [-l TIME]
-c ID numeric id for the choice
-l TIME print output every TIME seconds
-h print this menu
ID could be:
$menu
EOF
}
It's conventional to use upper case for argument descriptions in the symbolic syntax. Clearly indicate the options that take an argument in the left-hand column. Don't use obscure abbreviations that barely save a few characters like o/p
(I think you meant "output", but I'm not sure).
Parsing the command line
Use the shell builtin getopts
rather than the external command getopt
. If you have bash, you have getopts
. If you don't have getopts
, then you're running an embedded shell which may lack other features, so check that the other features you want to use are supported.
To test if there are any arguments, check the number of arguments, in $#
.
if [ $# -eq 0 ] || [ "1ドル" = "-h"]; then ...
Actually, you don't need any special handling for -h
. Treat it like any other option.
I'm not going to go over getopts
usage, there are plenty of examples around.
Before the main loop
set -- $args ... set -- $list
Remember what I wrote about unquoted substitutions above? Each of these commands performs word splitting and filename generation.
[ "x$choice" = "x" ] && choice=`seq $count` [ "x$repeatTime" = "x" ] && repeatTime=0
You need to initialize choice
and repeatTime
before the argument parsing, unless you intended to allow them to be pulled from the environment.
The main loop
while [ true ]
This is a strange way of writing while true
. You could also write while [ false ]
or while ! [ ! true ]
. Keep it simple.
eval temp=\${${a}} cat $SYSPATH${temp}
In bash, you can avoid the use of eval
. Also, remember to double quote variable substitutions unless you have a good reason not to.
cat "$SYSPATH${!a}"
A few suggestions at your disposal: You can make use of getopts
instead of getopt
to avoid shift
operation. You can make use of a simple flag to make sure that the mandatory -c
option has been passed. Also from this post you can make use of any of the solutions for check for numeric value. Maybe something on these lines:
...
usage(){
...
}
...
#Process arguments passed
c_set=false # Flag to check if mandatory -c is passed or not
repeatTime=0 # Set repeatTime value to 0 which will be default
while getopts ":c:l:h" opt; do
case "$opt" in
c)
# Check is borrowed from the mentioned link
! [[ "$OPTARG" =~ ^[0-9]+$ ]] && echo "Non-numeric passed to option:\"-c\"" && usage
# if can be used instead of the above one-liner as posted in the link
choice="$OPTARG"
c_set=true
;;
l)
! [[ "$OPTARG" =~ ^[0-9]+$ ]] && echo "Non-numeric passed to option:\"-l\"" && usage
repeatTime="$OPTARG"
;;
h)
usage
;;
*)
echo "Invalid parameters passed"
usage
;;
esac
done
# If the mandatory option not provided exit
# This will be false even if no arguments passed
[[ $c_set != true ]] && usage
#Actual operation
set -- $list
...
Edit: Added a version with getopt
using the tips from tldp
...
c_set=false # Flag to check if mandatory -c is passed or not
repeatTime=0 # Set repeatTime value to 0 which will be default
args=$(getopt :c:l:h "$@") || usage
eval set -- "$args"
while [ ! -z "1ドル" ]
do
case "1ドル" in
-c)
# One liner in case of -l can be used instead of this "if"
if ! [[ "2ドル" =~ ^[0-9]+$ ]];then
echo "Non-numeric passed to option:\"-c\"" && usage
fi
choice="2ドル"
c_set=true
;;
-l)
# "if" in case of -c can be used instead of this one-liner
! [[ "2ドル" =~ ^[0-9]+$ ]] && echo "Non-numeric passed to option:\"-l\"" && usage
repeatTime="2ドル"
;;
-h)
usage
;;
esac
shift
done
[[ $c_set != true ]] && usage
...
Improvements/comments/remarks are welcome. Hope this helps!
-
\$\begingroup\$ Thanks. However this script is for an embedded device. It does not have getopts. I have to use getopt only. \$\endgroup\$Ankur Agarwal– Ankur Agarwal2012年02月06日 18:37:17 +00:00Commented Feb 6, 2012 at 18:37
-
\$\begingroup\$ @abc: I have tried to add a version with
getopt
. Hopefully it can provide some pointers \$\endgroup\$another.anon.coward– another.anon.coward2012年02月07日 06:52:50 +00:00Commented Feb 7, 2012 at 6:52