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.

CrazyFrog
  • 323
  • 5
  • 19
  • 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? – melpomene Sep 24 '17 at 21:08
  • @melpomene well, typically we won't have files called -s. Following your argument we won't have any command line option. – CrazyFrog Sep 24 '17 at 21:34
  • Have you checked this answer? [Retrieving multiple arguments for a single option using getopts in Bash](https://stackoverflow.com/questions/7529856/bash-getopts-retrieving-multiple-variables-from-one-flag) – codeforester Sep 24 '17 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. – chepner Sep 25 '17 at 01:29
  • @melpomene Typically, if a file name could be mistaken for an option, you need to use a more explicit path to avoid starting with a `-`, such as `./script.sh -f ./-s`. – chepner Sep 25 '17 at 01:31
  • @codeforester yes I have thank you for suggestion. – CrazyFrog Sep 25 '17 at 07:50
  • Leaving out quotes breaks your script; it's **absolutely not** something you should consider a benefit for users. Consider if you want to pass `first file.txt` and `second file.txt` -- as it is, there's no possible way for your script to be used. – Charles Duffy Sep 25 '17 at 10:17
  • Similarly, if someone were to create a file with `touch '* READ ME FIRST *'`, that name couldn't be passed to your script: The `*` would be replaced with a list of names by glob expansion. – Charles Duffy Sep 25 '17 at 10:18
  • Moreover, if you train your users that they can and should perform unquoted expansions (despite that practice being behind most of the [Bash Pitfalls](http://mywiki.wooledge.org/BashPitfalls)), then they're going to be in a bad place when they're trying to run programs written by people other than you. Much better to help folks "bite the bullet" and learn to use the command line (or write scripts) properly. – Charles Duffy Sep 25 '17 at 10:20
  • 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](http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_02), guideline #10. – Charles Duffy Sep 25 '17 at 10:22
  • @CrazyFrog, ...that's useful for people writing scripts that need to work with unknown inputs: `cp -- "$source" "$dest"` means there's no question that the values, whatever they are, will be treated as filenames. – Charles Duffy Sep 25 '17 at 10:28
  • Hi @CharlesDuffy thank you very much for pointing out the bugs and unconventional uses in my script. There are a lot of inputs and I have to look them over one by one. melpomene sorry about my earlier stubbornness. I guess it's safer to follow the conventional UNIX programming, then do you have any suggested function (like getopt) that I can look up, which can both take unquoted (multiple) operands and treat special characters? Sorry for being naive but I just used shell for very basic tasks. – CrazyFrog Sep 25 '17 at 17:36

1 Answers1

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

}

CrazyFrog
  • 323
  • 5
  • 19
  • 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 http://shellcheck.net/ – Charles Duffy Sep 25 '17 at 10:13
  • @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. – CrazyFrog Sep 25 '17 at 17:28
  • @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. – CrazyFrog Sep 25 '17 at 17:39
  • 2
    `--)` 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. – Charles Duffy Sep 25 '17 at 17:41
  • again thank you very much. i really appreciate your comments. – CrazyFrog Sep 25 '17 at 17:41