Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Handling of arguments is one of the reasons I reach for Python or Powershell instead of a bash script when writing my own stuff.

https://docs.python.org/3/library/argparse.html is great.

Powershell has the Param keyword that functions like argparse in Python

https://docs.microsoft.com/en-us/powershell/module/microsoft...



But handling args isn't that bad in bash.

    while [[ $# -gt 0 ]]; do
      case "$1" in
         -h|--help)
           do_help
           exit
           ;;
         -v|--version)
           do_version
           exit
           ;;
         -d|--debug)
           debug=true
           shift
           ;;
         -a|--arg)
           arg_value=$2
           shift 2
           ;;
      esac
    done


    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument('-v','--version',action='version', version='demo',help='Print version information')
    parser.add_argument('-d','--debug', help='Enable Debug Mode')
    parser.add_argument('a','arg', help="Argument Documentation")
    args = parser.parse_args()
Personally I feel like this is more readable code, gets me better validation, and help docs for "free". That's the attraction.


Elegant, but then it's no longer a basic shell-script as it requires python installed.

If you can live with additional dependencies, then I like the node [1] commander package, which is very readable and nice to work with in my opinion.

        #!/usr/bin/env node
        const { program } = require('commander');

        program
          .command('clone <source> [destination]')
          .description('clone a repository')
          .action((source, destination) => {
            console.log('clone command called');
          });
It also automatically generates the --help output for ./script -h

[1] https://github.com/tj/commander.js/


To expand on that pattern:

    while (( $# )); do
      case "$1" in
        -h|--help)
          usage
          exit
          ;;

        -v|--version)
          do_version
          exit
          ;;

        -d|--debug)
          debug=true
          ;;

        -a|--arg)
          arg_value="$2"
          shift
          ;;

        *)
          if [[ ! -v pos1 ]]; then
            pos1="$1"
          elif [[ ! -v pos2 ]]; then
            pos2="$1"
          else
            >&2 printf "%s: unrecognized argument\n" "$1"
            >&2 usage
            exit 1
          fi
      esac

      shift
    done


The one downside of this is that it doesn't handle squeezing flags as in

    foo -da bar
whereas getopts does. On the other hand, with (the Bash built-in) getopts you're limited to single character flags.


You can do that it will just make things a little less pretty.

    while (( $# )); do
        case "$1" in
            -*h*|--help)
                do_help
                exit
                ;;
            -*v*|--version)
                do_version
                exit
                ;;
            -*d*|--debug)
                debug=true
                ;;&
            -*a*|--arg)
                value="$2"
                shift
                ;;&
        esac
        shift
    done
It doesn't support args of the form -avalue but those a pretty uncommon anyway.


That wouldn't work in the general case. Those patterns would also match long options. If I add a case pattern `--all)`, and I call the script with `--all`, it's also going to match

  -*a*|--arg)
You could fix that with:

  -a*|-[!-]*a*|--arg)
> It doesn't support args of the form -avalue but those a pretty uncommon anyway.

You could

  -a*|-[!-]*a*|--arg)
    if [[ "$1" != --arg ]]; then
      value="${1#*a}"
    fi
    if [[ ! "$value" ]]; then
      value="$2"
      shift
    fi
  ;;&
Putting the option stuck together to its value has the advantage of working nicely with brace expansion. For example, you can call `strace -p{1111,2222,3333}` to trace those 3 pids and avoid having to type `-p` 3 times.


As a final addendum, case clauses of options that take arguments like -a/--arg should not be terminated with `;;&`, but rather with `;;`.


This is awesome! Thank you for being a total bash nerd.


There's still one problem. To exemplify it, if you call with `-av`, it'll process the `v` as the option `-v` instead of the option value to `-a`. If you only have one possible option that takes a value, this can be fixed by putting its case clause before all others. If you have more, then that'll require things to get a little more complicated:

      -d*|-[!-]*d*|--debug)
        if [[ ! "$finished_case" && ("$1" = --debug || "$1" =~ '^[^ad]*d') ]]; then
          debug=true
        fi
      ;;&

      -a*|-[!-]*a*|--arg)
        if [[ ! "$finished_case" && ("$1" = --arg || "$1" =~ '^[^a]*a') ]]; then
          if [[ "$1" != --arg ]]; then
            value="${1#*a}"
          fi
          if [[ ! "$value" ]]; then
            value="$2"
            shift
          fi
          finished_case=true
        fi
      ;;&
      ...
    esac

    shift
    finished_case=
  done
All case-clauses would need to use `;;&` by the way, including `-v` and `-h`. The regex is generally:

  "^[^${all_options_with_values}${current_option}]*${current_option}"
Another problem is that option and argument non-recognition would not work as previously layed out. You can include short options that aren't recognized, and they'll be ignored instead of raising errors. For positional arguments, one would need a condition to check for options, since using `;;&` for everything means that everything would land to

  *)
Maybe those are the last issues, but this is already out of hand for otherwise small and simple shell scripts. All these complications arise from trying to support the sticking together of short options and their possible values. Processing arguments in a case loop is much, much simpler if we avoid supporting those 2 features.


> https://docs.python.org/3/library/argparse.html is great

Argparse is okay (and being in stdlib makes it always-available), but it's no click. https://click.palletsprojects.com/en/7.x/


This is why I love the HN community, learning something new every day. (Happy 10,000) I use argparse because it is stdlib, but will checkout click!


Thumbs up for 'Click'. I used it for a project once, and I was really happy with it. Easy to use, good docs. Would use it again.


Googling the library appears to be about ~8,000 lines of code (core.py is ~2,000 alone).

Is that really reasonable sounding to most people for parsing CLI input/output and display manpages or helptext?


I suppose it depends on the use case. Personally I've always thought argparse is good enough, and have never hit a roadblock "because I'm using argparse" so to say. Having said that, I do like the pattern click is going for. If it argparse allowed the same pattern, in my opinion that would be cool, and it would probably be my first choice.


argparse should not be the first thing to reach for, imo, when good old sys.argv can do the job.


At that point I wouldn't leave bash. I feel like argparse allows for better documentation, error handling and input validation.


I didn’t mean to suggest we should reach for Click for simple help/manpage display.

The case I used it for was much more complex. What I liked about it was the easy to use API, clear documentation & examples, and readable patterns.

For simple text display, I like the solution from the article, and I learned something new about bash scripts. Also, I learned from comments you can use heredoc in bash!


I'm not trying to be antagonistic here... but who cares how many lines it has unless you plan to maintain it?


It’s fantastic and should be used by most CLI programs. Argparse is much faster and avoids having a dependency, so it does serve a purpose.


The original argparse is available in every Unix shell: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/g...


See also: https://github.com/nhoffman/argparse-bash

A great option when you're stuck with an old crusty script that you don't want to completely rewrite in python, but do want to clean up enough so that you can call it with `-h` and remember how to use it a few months in the future.

Unfortunately, this won't help you if you're on embedded where python isn't in the base system.


PowerShell also has comment-based help, which is like a manpage embedded as a comment within the script. It's like OP's suggested help format, but better.

https://docs.microsoft.com/en-us/powershell/module/microsoft...


I also really like the "flag" package for Go, it generates the help text for you and easily lets you set defaults as well as helps you type-check inputs.


I like https://github.com/jpillora/opts for Go arg parsing. It has better, sane defaults with long/short opts.


Cool, I'll check it out. There's something attractive about using stuff from the standard library though.


My rule for a long time has been that any time you have more than one screen's worth of code and/or are using any of arrays, functions, or more than simple positional arguments you'll save time by rewriting it in Python. shellcheck has helped soften that a bit but the usability difference is pretty noticeable even with multiple decades writing shell scripts.


Try pyhon click: from flask& jinja2 author, it is very nice




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: