sigoden/argc

Support separate files for subcommand implementations

lexun opened this issue · 8 comments

lexun commented

Hello, love the project! What a clean API and implementation!

Just curious what you'd think about supporting subcommands being implemented in other files. I'm not currently seeing a way to break large CLI projects down into smaller files for organization purposes.

I was thinking something like this could be useful:

# main.sh

# @describe A demo cli

# @cmd Upload a file
# @alias    u
# @arg target!                      File to upload
upload() {
    echo "cmd                       upload"
    echo "arg:  target              $argc_target"
}

# @cmd Download a file
# @cmd-file ./subcommands/download.sh

eval "$(argc --argc-eval "$0" "$@")"
# subcommands/download.sh

# @alias    d
# @flag     -f --force              Override existing file
# @option   -t --tries <NUM>        Set number of retries to NUM
# @arg      source!                 Url to download from
# @arg      target                  Save file to
download() {
    echo "cmd:                      download"
    echo "flag:   --force           $argc_force"
    echo "option: --tries           $argc_tries"
    echo "arg:    source            $argc_source"
    echo "arg:    target            $argc_target"
}

This might just be opening a big can of worms, but I'd love to hear your thoughts!

Including files are prone to errors.

The recommended approach is to use the external subcommand.

main.sh

# @cmd  Download a file
# @arg args~
download() {
   ./subcommands/download.sh "$@"
}

subcommands/download.sh

# @flag     -f --force              Override existing file
# @option   -t --tries <NUM>        Set number of retries to NUM
# @arg      source!                 Url to download from
# @arg      target                  Save file to
lexun commented

Interesting, I like that better in theory. I can't quite get the documentation delegated properly though.

For instance, if I run ./main.sh download -h, I'll get the following:

Download a file

USAGE: main download [ARGS]...

ARGS:
  [ARGS]...

If I pass another arg before -h then it works, for example with ./main.sh download my-source -h

USAGE: download [OPTIONS] <SOURCE> [TARGET]

ARGS:
  <SOURCE>  Url to download from
  [TARGET]  Save file to

OPTIONS:
  -f, --force        Override existing file
  -t, --tries <NUM>  Set number of retries to NUM
  -h, --help         Print help

These are the full implementations I'm testing with:

# main.sh

# @describe A demo cli

# @cmd Download a file
# @arg args~
download() {
    ./subcommands/download.sh "$@"
}

eval "$(argc --argc-eval "$0" "$@")"
# subcommands/download.sh

# @alias    d
# @flag     -f --force              Override existing file
# @option   -t --tries <NUM>        Set number of retries to NUM
# @arg      source!                 Url to download from
# @arg      target                  Save file to

main() {
    echo "cmd:                      download"
    echo "flag:   --force           $argc_force"
    echo "option: --tries           $argc_tries"
    echo "arg:    source            $argc_source"
    echo "arg:    target            $argc_target"
}

eval "$(argc --argc-eval "$0" "$@")"

It seems like skipping the special behavior around known args like -h and -v when encountering @arg args~ would solve this. Is there currently a way to override that behavior?

cf2cb38 solved the problem.

Build argc from main branch or wait for the next realease. Then try agin.

lexun commented

That works! 🎉

And wow, only 12 minutes from the time I posted you already had a fix on main.

You're an open source legend, thank you 🙏

Hello and thank you very much for this wonderful project!

Can you help me understand how to generate completion from all subcommands, recursively, if they are defined in separate files? Thx!

The core for generate completion from seperate file is:

argc --argc-compgen generic <argc-file> <arg>s...

Example

#!/usr/bin/env bash
set -e

SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"

# @cmd  Download a file
# @arg args~[?`_choice_args`]
download() {
   "$SCRIPT_DIR/subcommands/download.sh" "$@"
}

# @cmd  Upload a file
# @arg args~[?`_choice_args`]
upload() {
   "$SCRIPT_DIR/subcommands/upload.sh" "$@"
}

_choice_args() {
    args=( "${argc__positionals[@]}" )
    args[-1]="$ARGC_LAST_ARG"
    argc --argc-compgen generic "$SCRIPT_DIR/subcommands/$argc__cmd_fn.sh"  "$argc__cmd_fn" "${args[@]}" 
}

# See more details at https://github.com/sigoden/argc
eval "$(argc --argc-eval "$0" "$@")"

Explain

Why need args[-1]="$ARGC_LAST_ARG" ?

Argc has specially processed the last argument, such as removing quote or something, which needs to be restored here.

That's awesome! Thanks, also for the explanation!

If I understand well, we need to be sure that the primary command name (the bash function name) is equal to the subcommand filename in order to use $argc__cmd_fn as lookup key. This method than works perfectly with command alias!

I noticed that also help is always listed as possible completion.
It's not a big deal, I solved with grep, but I think could be useful to let you know.

This is my solution:

argc --argc-compgen generic "$commands/${argc__cmd_fn:?}.sh"  "$argc__cmd_fn" "${args[@]}" | grep -v "help"