6
\$\begingroup\$

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 every num2 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
200_success
146k22 gold badges190 silver badges479 bronze badges
asked Feb 6, 2012 at 2:15
\$\endgroup\$

2 Answers 2

4
\$\begingroup\$

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}"
answered Feb 12, 2012 at 18:41
\$\endgroup\$
0
2
\$\begingroup\$

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!

answered Feb 6, 2012 at 11:24
\$\endgroup\$
2
  • \$\begingroup\$ Thanks. However this script is for an embedded device. It does not have getopts. I have to use getopt only. \$\endgroup\$ Commented Feb 6, 2012 at 18:37
  • \$\begingroup\$ @abc: I have tried to add a version with getopt. Hopefully it can provide some pointers \$\endgroup\$ Commented Feb 7, 2012 at 6:52

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.