3

I have been researching on using bash scripts to process command-line arguments. I have multiple optional arguments, each of which have one or more operands. An example is:

./script.sh -f file1 file2 -s server1 server2

-f itself is optional, but must be followed by filename; -s is optional, can be used without any operands or operands.

I know I can force putting "" on operands so I only deal with arguments with one operand and can use case $1 $2 shift to process it.

But I am interested in doing so without quotes, just to save some typing for the users.

A rough idea would be read in "$@" as one string, and separate them by space, then locate arguments with -/-- and assign operands following them. Maybe I can use an array to do that?

Any suggestions would be welcome.

12
  • How would I run your script on a file called -s if ./script.sh -f -s thinks -s is the start of a new option? Commented Sep 24, 2017 at 21:08
  • @melpomene well, typically we won't have files called -s. Following your argument we won't have any command line option. Commented Sep 24, 2017 at 21:34
  • Have you checked this answer? Retrieving multiple arguments for a single option using getopts in Bash Commented Sep 24, 2017 at 22:46
  • 3
    I wouldn't even allow multiple arguments per option. Repeat the option (-f file1 -f file2) for each argument. Also, what does -s without an option mean? Consider making a separate zero-argument option for the no-argument case, then make -s a repeatable one-argument-required option. Commented Sep 25, 2017 at 1:29
  • 1
    @CrazyFrog, ...re: melpomene's prior comment -- the conventional UNIX command line does allow -s to be passed as a positional argument without any prefix, ./ or otherwise. If one passes --, a parser complying with POSIX guidelines will treat that as "end of options", meaning that all future arguments are positional and should be parsed as such. See POSIX utility syntax guidelines, guideline #10. Commented Sep 25, 2017 at 10:22

1 Answer 1

2

Thanks folks for your wonderful suggestions. After spending some more time I resolved to the solution below:

Simply put, I use case and few checks to determine if the argument is an option or not. I use only alter flag variables during argument processing and then use the flags to determine what functions I will perform. In a way that I can have options in different order.

main(){
# flags, 1 is false, 0 is true. it's the damn bash LOCAL_DEPLOY=1 SERVER_DEPLOY=1 DRY_RUN=0

FILES=("${ALLOWEDFILES[@]}"); 
DEPLOYTARGET=("${ALLOWEDSERVERS[@]}"); 

if [ $# -eq 0 ]
then
    printf -- "Missing optins, perform DRY RUN\nFor help, run with -h/--help\n"
    for target in "${FILES[@]}"; do generate "$target"; done
    echo "....dry run: markdown files generated in rendered/"
    exit 0
fi  

while true ; do
    case "$1" in 
        -f |--file) # required operands
            case "$2" in
                "") die $1 ;;
                *) 
                    FILES=($2)
                    for i in "${FILES[@]}"; do
                        if  is_option $i; then die $1; fi # check for option 
                                                    if ! check_allowed $i ${ALLOWEDFILES[@]}; then exit 1; fi
                    done; 
                    shift 2;; # input FILES are good
            esac ;;
        -l|--local) # no operands expected
            DRY_RUN=1 # turn off dryrun
            LOCAL_DEPLOY=0 # turn on local deploy
            shift ;;
        -s|--server) # optional operands
            case "$2" in
                "") shift ;; 
                *) 
                    DEPLOYTARGET=($2)  # use input
                    for i in "${DEPLOYTARGET[@]}"; do
                        if  is_option $i; then die $1; fi # check for option 
                                                    if ! check_allowed $i ${ALLOWEDSERVERS[@]}; then exit 1; fi
                    done ; shift 2;; # use input value
            esac
            DRY_RUN=1
            SERVER_DEPLOY=0
            ;;
        -n|--dryrun) # dry-run:generate markdown files only
            DRY_RUN=0
            shift ;;
        -h|--help) # docs
            print_help
            exit 0
            ;;
        --) shift; break ;;
        -?*)
            printf 'ERROR: Unkown option: %s\nExisting\n\n' "$1" >&2
            print_help
            exit 1
            shift
            ;; 
        *) 
            break ;; 
    esac 
done

echo  "choose files: ${FILES[@]}"
echo ""

# dry-run
if [ $DRY_RUN == 0 ]; then
    echo "..perform dry run.."
    for target in "${FILES[@]}"; do generate "$target"; done
    echo "....dry run: markdown files generated in rendered/"
    exit 0
fi

# local-deploy
if [ $LOCAL_DEPLOY == 0 ] && [ $SERVER_DEPLOY != 0 ]; then
    echo "..deploy locally"
    for target in "${FILES[@]}"; do 
        generate "$target" > /dev/null
        deploylocal "$target"
    done; 

    # sync hexo-gcs hexo-yby
    cd "$(dirname $HEXOLOCATION)"
    ./syncRepo.sh
    printf -- "....hexo-gcs hexo-yby synced\n"
    cd $CURRENTLOCATION
fi

# server-deploy
if [ $SERVER_DEPLOY == 0 ]; then
    echo "..deploy on servers: ${DEPLOYTARGET[@]}"
    echo ""

    for target in "${FILES[@]}"; do # deploy locally 
        generate "$target" > /dev/null
        deploylocal "$target"
    done 

    # sync hexo-gcs hexo-yby
    cd "$(dirname $HEXOLOCATION)"
    ./syncRepo.sh
    printf -- "....hexo-gcs hexo-yby synced\n"
    cd $CURRENTLOCATION

    # deploy to selected server: git or gcp
    for dt in "${DEPLOYTARGET[@]}"; do 
        deployserver $dt 
    done
fi

}

Sign up to request clarification or add additional context in comments.

5 Comments

Lots of bugs here -- filenames with spaces in them won't behave, nor will those with names that expand as globs. Consider running your code through shellcheck.net
@CharlesDuffy thank you! I will future test it. It works so far for my selection of filenames but I will try to make it more robust.
@CharlesDuffy by the way, may I ask a clarification questions? At the end of case command, what is the difference betweem --) -?* and *)? I would assume -?* skips unknown options starting with -, * skips any other word, but not understanding --) at all.
--) checks for the case of -- (the POSIX-specified end-of-options sigil) specifically. -?* checks for other unknown options, *) handles things that aren't options at all. The -- is how you can pass a file named -s as a positional argument (as opposed to an option) by putting -- as a separate argument earlier.
again thank you very much. i really appreciate your comments.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.