Passing arguments from a bash shell script to a command

I'm pretty new to bash scripting and I've found myself writing things like this (and the same with even more nesting):

    if $CATEGORIES; then
        if $LABEL_SLOTS; then
          $pyth "$wd/texify_grammar.py" "$input" "$texfile" "--label-slots" "--categories" "$CATEGORY_LIST"
        else
          $pyth "$wd/texify_grammar.py" "$input" "$texfile" "--categories" "$CATEGORY_LIST"
        fi
    else
        if $LABEL_SLOTS; then
          $pyth "$wd/texify_grammar.py" "$input" "$texfile" "--label-slots"
        else
          $pyth "$wd/texify_grammar.py" "$input" "$texfile"
        fi
    fi

There must be an easier way to do this... I've tried searching for the answer but haven't had much luck (probably because I don't know the right keywords to search for). Any help would be much appreciated!

Best wishes,
M

In my Posix Shell (which is not bash) the above two conditions are always true regardless of whether the environment variables contain values or not.

Please post sample values of the variables and explain what decisions you need to make.

That if-statement is wrong, I presume you're using -z or something to tell if they're blank...

Build a string:

# Replace $1, $2, ... with your options
set -- "$input" "$texfile"

# Append more options
[ ! -z "$LABEL_SLOTS" ] && set -- "$@" "--label-slots"
# Append more options
[ ! -z "$CATEGORIES" ] && set -- "$@" "--categories" "$CATEGORY_LIST"
...

# Test if it's really what you want
echo $pyth "$wd/texify_grammar.py" "$*"
# $pyth "$wd/texify_grammar.py" "$*"

If done carefully, this ought to even preserve spaces in arguments right, since the exact string "$@" expands to all the previous aruments exactly as given, warts and all.

Thanks... that ought to be enough to let me figure it out. FWIW, the original code looked like this, and it works. (Which are certainly not to say that it couldn't be cleaner -- I'm only just starting with bash scripting.)

#!/usr/bin/bash                                                                                                        
 
wd=$(dirname "$0")
pyth=$(which python)
latex=$(which latex)
xelatex=$(which xelatex)
mkindex=$(which makeindex)

input="$wd/ALMSGrammar.txt"
texfile="$wd/build/ALMSGrammar.tex"

INDICES=false
LABEL_SLOTS=false
CATEGORIES=false

while getopts "isc:" opt; do
  case $opt in
    i)
      INDICES=true
      ;;
    s)
      LABEL_SLOTS=true
      ;;
    c)
      CATEGORIES=true
      CATEGORY_LIST=$OPTARG
      ;;
    \?)
      echo "Invalid option: -$OPTARG" >&2
      echo "Usage:" >&2
      echo -e "   -i \tgenerate indices" >&2
      echo -e "   -s \tlabel slots" >&2
      exit 1
      ;;
  esac
done

#there musst be an easier way of doing this...
if $INDICES; then
    if $CATEGORIES; then
        if $LABEL_SLOTS; then
          $pyth "$wd/texify_grammar.py" "$input" "$texfile" "--indices" "--label-slots" "--categories" "$CATEGORY_LIST"
        else
          $pyth "$wd/texify_grammar.py" "$input" "$texfile" "--indices" "--categories" "$CATEGORY_LIST"
        fi
    else
        if $LABEL_SLOTS; then
          $pyth "$wd/texify_grammar.py" "$input" "$texfile" "--indices" "--label-slots"
        else
          $pyth "$wd/texify_grammar.py" "$input" "$texfile" "--indices" 
        fi
    fi
else
    if $CATEGORIES; then
        if $LABEL_SLOTS; then
          $pyth "$wd/texify_grammar.py" "$input" "$texfile" "--label-slots" "--categories" "$CATEGORY_LIST"
        else
          $pyth "$wd/texify_grammar.py" "$input" "$texfile" "--categories" "$CATEGORY_LIST"
        fi
    else
        if $LABEL_SLOTS; then
          $pyth "$wd/texify_grammar.py" "$input" "$texfile" "--label-slots"
        else
          $pyth "$wd/texify_grammar.py" "$input" "$texfile"
        fi
    fi
fi

pushd "$wd/build"

if $INDICES; then
  "$latex" ALMSGrammar.tex
  "$mkindex" tokens
  "$mkindex" categories
  "$mkindex" todo
  "$latex" ALMSGrammar.tex
fi

"$xelatex" ALMSGrammar.tex
popd

mv "$wd/build/ALMSGrammar.pdf" "$wd"
open "$wd/ALMSGrammar.pdf"

You've made two fundamental errors in such a way that they cancel each other out and work correctly here. I'm impressed :smiley:

"true" and "false" are not special values to bash. They're just strings.

But...

As it happens, there are external utilities named true and false. true always returns success, and false always returns error.

$ whereis true
true: /bin/true /usr/share/man/man1/true.1.bz2 /usr/share/man/man1p/true.1p.bz2
$

Also, the code if program arguments ... is a way of checking the result of an external program. If $VARNAME was 'rm -Rf /home/myusername/', if $VARNAME would run the command rm -Rf /home/myusername/. If 'rm' succeeded in deleting your home directory, it would return a zero value, which the if-statement would consider a boolean 'true'. If it failed for some reason and returned a nonzero code, that would be a boolean 'false'.

So you've been converting the strings true and false into zero or nonzero program return codes, by running external programs that happen to be named true and false, then checking their result. Rather the long way around! :smiley:

Seeing the code, if your filenames and arguments will never contain spaces, you can make this much much simpler:

#!/usr/bin/bash
 
wd=$(dirname "$0")
pyth=$(which python)
latex=$(which latex)
xelatex=$(which xelatex)
mkindex=$(which makeindex)

input="$wd/ALMSGrammar.txt"
texfile="$wd/build/ALMSGrammar.tex"

ARGS="$input $texfile"

while getopts "isc:" opt; do
  case $opt in
    i)
        ARGS="$ARGS --indices"
        INDICES=1
      ;;
    s)
      ARGS="$ARGS --label-slots"
      ;;
    c)
      ARGS="$ARGS --categories $OPTARG"
      ;;
    \?)
      echo "Invalid option: -$OPTARG" >&2
      echo "Usage:" >&2
      echo -e "   -i \tgenerate indices" >&2
      echo -e "   -s \tlabel slots" >&2
      exit 1
      ;;
  esac
done

# Note $ARGS is not in quotes and MUST NOT be in quotes.
# The spaces cause the arguments to split apart where you want them to.
# "$ARGS" would make it one giant argument.
$pyth "$wd/texify_grammar.py" $ARGS 

pushd "$wd/build"

# This is how you do string comparison!  -z means "if the string is empty".
# See http://tldp.org/LDP/abs/html/refcards.html#AEN22167 for details
# on more kinds of comparison.
if [ ! -z "$INDICES" ]
then
  "$latex" ALMSGrammar.tex
  "$mkindex" tokens
  "$mkindex" categories
  "$mkindex" todo
  "$latex" ALMSGrammar.tex
fi

"$xelatex" ALMSGrammar.tex
popd

mv "$wd/build/ALMSGrammar.pdf" "$wd"
open "$wd/ALMSGrammar.pdf"
1 Like

Thanks! That's extremely helpful. I should be okay now.

FWIW, I didn't invent the true/false -- I got them out of an introduction to getopts that I found somewhere on the web. (And I hadn't had cause to use conditionals in a script before.)

Unfortunately I do have to deal with the possibility of spaces in filenames. (One of my collaborators works on a Mac, and this seems to be a perpetual issue there.) But I should still be able to adapt the

[ ! -z "$LABEL_SLOTS" ] && set -- "$@" "--label-slots"

to do the trick, right?

Edit: Actually, ack, it's just occurred to me that if I use set inside the getopts loop things are liable to go very wrong! I'll use three flags instead.

Yes, set -- would mess up your arguments if you were still using the arguments at the time. Good thinking realizing that.

In the case of spaces in filenames, I'd use flags like you had, but use the set -- trick as well:

#!/usr/bin/bash                                                                                                        
 
wd=$(dirname "$0")
pyth=$(which python)
latex=$(which latex)
xelatex=$(which xelatex)
mkindex=$(which makeindex)

input="$wd/ALMSGrammar.txt"
texfile="$wd/build/ALMSGrammar.tex"

INDICES=
LABEL_SLOTS=
CATEGORY_LIST=

while getopts "isc:" opt; do
  case $opt in
    i)
      INDICES=1
      ;;
    s)
      LABEL_SLOTS=1
      ;;
    c)
      CATEGORY_LIST=$OPTARG
      ;;
    \?)
      echo "Invalid option: -$OPTARG" >&2
      echo "Usage:" >&2
      echo -e "   -i \tgenerate indices" >&2
      echo -e "   -s \tlabel slots" >&2
      exit 1
      ;;
  esac
done

set -- "$input" "$texfile"

[ -z "$INDICES" ] || set -- "$@" "--indices"
[ -z "$LABEL_SLOTS" ] || set -- "$@" "--label-slots"

[ -z "$CATEGORY_LIST" ] || set -- "$@" "--categories" "$CATEGORY_LIST"

$pyth "$wd/texify_grammar.py" "$@"

pushd "$wd/build"

if [ ! -z $INDICES ]
then
  "$latex" ALMSGrammar.tex
  "$mkindex" tokens
  "$mkindex" categories
  "$mkindex" todo
  "$latex" ALMSGrammar.tex
fi

"$xelatex" ALMSGrammar.tex
popd

mv "$wd/build/ALMSGrammar.pdf" "$wd"
open "$wd/ALMSGrammar.pdf"

You can also use arrays instead of "set --" to keep an argument list, but "set --" works in any bourne shell -- bash, ksh, old-fashioned sh, zsh, and more. This script would be simple to use anywhere. BASH arrays on the other hand only work when BASH is available...

---------- Post updated at 11:30 AM ---------- Previous update was at 11:26 AM ----------

As for what || and && do:

true && echo "&& checks if the previous command succeeded.  This should print."
false && echo "This shouldn't print because 'false' returns error"
true || echo "|| checks if the previous command failed.  This won't print."
false || echo "...but this will."

true && true && true && echo "You can chain on more than one."

true &&
        echo "You can break && statements across lines to avoid really wide scripts"

echo "Pipes also can be broken across lines like that" |
        cat


1 Like

Thank you again! I had hammered out pretty much the same thing (except with && instead of ||), and was trying to figure out what to replace $* with, and whether to quote it -- so after reading that I'm sorted. You've been incredibly helpful.

The difference between "$" and "$@" is that "$" will split where the spaces are, not where the arguments are. "$@" will keep the original arguments intact.

Got it -- thanks!