Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Implement command line parser builtin #613

Open
hdwalters opened this issue Nov 23, 2024 · 3 comments
Open

[Feature] Implement command line parser builtin #613

hdwalters opened this issue Nov 23, 2024 · 3 comments
Assignees
Labels
enhancement New feature or request

Comments

@hdwalters
Copy link
Contributor

hdwalters commented Nov 23, 2024

Is your feature request related to a problem? Please describe.
When writing Bash scripts taking command line options, I use getopt to preprocess the command line, and a while loop and case statement to handle individual options:

#!/usr/bin/env bash

# Parse command line or print help text.
script=""
files=()
title=""
items=()
depth=0
ultra=0
verbose=0
getopt=$(getopt --options=t:i:d:uv --longoptions=title:,items:,depth:,ultra,verbose,help -- "$@") || exit
eval set -- $getopt
while true; do
    case "$1" in
        -t|--title)
            title="$2"
            shift
            shift
            ;;
        -i|--items)
            items+=("$2")
            shift
            shift
            ;;
        -d|--depth)
            depth="$2"
            shift
            shift
            ;;
        -u|--ultra)
            ultra=1
            shift
            ;;
        -v|--verbose)
            verbose=1
            shift
            ;;
        --help)
            cat <<EOF
Test Amber command line parser
Syntax: $(basename $0) [options]
  SCRIPT: Text ......... Script path
  FILES: [Text] ........ File paths
  -t|--title: Text ..... Title text
  -i|--items: [Text] ... Item text
  -d|--depth: Num ...... File depth
  -u|--ultra: Bool ..... Ultra flag
  -v|--verbose: Bool ... Verbose flag
  --help ............... Show help text
EOF
            exit 1
            ;;
        --)
            shift
            break
            ;;
        *)
            exit 1
            ;;
    esac
done

# Copy positional parameters.
script="$1"
shift
files=("$@")

# Print positional parameters and options.
echo "Script: \"${script}\""
for file in "${files[@]}"; do
    echo "File: \"${file}\""
done
echo "Title: \"${title}\""
for item in "${items[@]}"; do
    echo "Item: \"${item}\""
done
echo "Depth: ${depth}"
echo "Ultra: ${ultra}"
echo "Verbose: ${verbose}"

This can handle:

  1. Long options with params --label=zzz or --label "zzz"
  2. Short options with params -lzzz
  3. Long options for flags --flag
  4. Short options for flags -f
  5. Short options for multiple flags -xyz expanded to -x -y -z
  6. Positional arguments left over at the end of the while loop
$ ./cli.sh run.sh one.txt two.txt three.txt --title 'My Fruit' -iapple -ibanana -icherry -d4 -uv
Script: "run.sh"
File: "one.txt"
File: "two.txt"
File: "three.txt"
Title: "My Fruit"
Item: "apple"
Item: "banana"
Item: "cherry"
Depth: 4
Ultra: 1
Verbose: 1

Describe the solution you'd like
However, this is such a painful process, I have to copy and adapt a previous script every time. I would like to avoid this pain in Amber, by writing a set of builtin functions. This is my proposal:

#!/usr/bin/env amber

main(args) {
    // Parse command line or print help text.
    let parser = parser("Test Amber command line parser")
    let script = param(parser, "script", "", "Script path")
    let files = param(parser, "files", [Text], "File paths")
    let title = param(parser, "-t|--title", "", "Title text")
    let items = param(parser, "-i|--items", [Text], "Item text")
    let depth = param(parser, "-d|--depth", 0, "File depth")
    let ultra = param(parser, "-u|--ultra", false, "Ultra flag")
    let verbose = param(parser, "-v|--verbose", false, "Verbose flag")
    getopt(parser, args)

    // Print positional parameters and options.
    echo "Script: \"{script}\""
    for index, file in files {
        echo "File #{index}: \"{file}\""
    }
    echo "Title: \"{title}\""
    for index, item in items {
        echo "Item #{index}: \"{item}\""
    }
    echo "Depth: {depth}"
    echo "Ultra: {ultra}"
    echo "Verbose: {verbose}"
}

The parser builtin creates a parser struct to hold the array of (reference counted) argument structs for Bash generation. The param builtin parses the option match string, a default value (which also defines whether it takes a parameter at all, and the type of that parameter) and a help string, and creates the argument structs for the parser. The getopt builtin causes the command line to be parsed. Subsequently, the argument structs are responsible for returning the parsed values for use in the main script.

Describe alternatives you've considered
The only alternative would be to attempt to write this as a standard library function, but (i) it requires interaction between the various components, which is just not possible in pure Amber, and (ii) it would be unable to support typed arguments, which are necessary to (for example) iterate over array arguments.

Additional context
N/A

@hdwalters hdwalters added the enhancement New feature or request label Nov 23, 2024
@github-project-automation github-project-automation bot moved this to 🆕 New in Amber Project Nov 23, 2024
@hdwalters hdwalters self-assigned this Nov 23, 2024
@Ph0enixKM
Copy link
Member

Great idea! In the future we could extend these library functions with structs:

const args = {
    script: {
        argument: "script",
        default: null,
        title: "Script path",
    },
    verbose: {
        argument: ["--verbose", "-v"],
        default: false,
        title: "Verbose flag"
    }
}

main (args) {
    // Parse the command line or print help text.
    let parser = parser(args)

    // Print the positional parameters and options.
    echo "Script: \"{parser.script}\""
    for index, file in parser.script {
        echo "File #{index}: \"{file}\""
    }
    echo "Title: \"{parser.title}\""
    for index, item in parser.items {
        echo "Item #{index}: \"{item}\""
    }
    echo "Depth: {parser.depth}"
    echo "Ultra: {parser.ultra}"
    echo "Verbose: {parser.verbose}"
}

@hdwalters
Copy link
Contributor Author

My proposed usage code may look familiar; I modelled it after the Python argparse module.

@Ph0enixKM
Copy link
Member

Oh I see @hdwalters then we can stick to it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants