From 1086e984cacc5b35f637e8c5ead6ceae23d0541c Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Sun, 22 Mar 2026 11:26:09 +0200 Subject: [PATCH 1/6] - Add support for command argfile --- lib/bashly/config_validator.rb | 1 + lib/bashly/script/command.rb | 2 +- lib/bashly/script/introspection/commands.rb | 5 +++ lib/bashly/views/command/argfile_filter.gtx | 5 +++ lib/bashly/views/command/argfile_helpers.gtx | 36 +++++++++++++++++++ lib/bashly/views/command/master_script.gtx | 1 + .../views/command/parse_requirements.gtx | 1 + 7 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 lib/bashly/views/command/argfile_filter.gtx create mode 100644 lib/bashly/views/command/argfile_helpers.gtx diff --git a/lib/bashly/config_validator.rb b/lib/bashly/config_validator.rb index 2fcac278..4b3060e3 100644 --- a/lib/bashly/config_validator.rb +++ b/lib/bashly/config_validator.rb @@ -193,6 +193,7 @@ def assert_command(key, value) assert_optional_string "#{key}.group", value['group'] assert_optional_string "#{key}.filename", value['filename'] assert_optional_string "#{key}.function", value['function'] + assert_optional_string "#{key}.argfile", value['argfile'] assert_boolean "#{key}.private", value['private'] assert_default_command "#{key}.default", value['default'] diff --git a/lib/bashly/script/command.rb b/lib/bashly/script/command.rb index 33e06e59..a356d9f6 100644 --- a/lib/bashly/script/command.rb +++ b/lib/bashly/script/command.rb @@ -14,7 +14,7 @@ class Command < Base class << self def option_keys @option_keys ||= %i[ - alias args catch_all commands completions + alias argfile args catch_all commands completions default dependencies environment_variables examples extensible expose filename filters flags footer function group help help_header_override name diff --git a/lib/bashly/script/introspection/commands.rb b/lib/bashly/script/introspection/commands.rb index 087ac203..8e4bc7c7 100644 --- a/lib/bashly/script/introspection/commands.rb +++ b/lib/bashly/script/introspection/commands.rb @@ -7,6 +7,11 @@ def catch_all_used_anywhere? deep_commands(include_self: true).any? { |x| x.catch_all.enabled? } end + # Returns true if the command or any of its descendants has `argfile` + def argfile_used_anywhere? + deep_commands(include_self: true).any?(&:argfile) + end + # Returns a full list of the Command names and aliases combined def command_aliases commands.map(&:aliases).flatten diff --git a/lib/bashly/views/command/argfile_filter.gtx b/lib/bashly/views/command/argfile_filter.gtx new file mode 100644 index 00000000..321a9c77 --- /dev/null +++ b/lib/bashly/views/command/argfile_filter.gtx @@ -0,0 +1,5 @@ += view_marker + +> load_command_argfile "{{ argfile }}" "$@" +> set -- "${argfile_input[@]}" +> diff --git a/lib/bashly/views/command/argfile_helpers.gtx b/lib/bashly/views/command/argfile_helpers.gtx new file mode 100644 index 00000000..339f0711 --- /dev/null +++ b/lib/bashly/views/command/argfile_helpers.gtx @@ -0,0 +1,36 @@ += view_marker + +> load_command_argfile() { +> local argfile_path line arg +> argfile_path="$1" +> shift +> argfile_input=() +> +> if [[ ! -f "$argfile_path" ]]; then +> argfile_input=("$@") +> return +> fi +> +> while IFS= read -r line || [[ -n "$line" ]]; do +> line="${line#"${line%%[![:space:]]*}"}" +> line="${line%"${line##*[![:space:]]}"}" +> +> [[ -z "$line" || "${line:0:1}" == "#" ]] && continue +> +> if [[ "$line" =~ ^(-{1,2}[^[:space:]]+)[[:space:]]+(.+)$ ]]; then +> for arg in "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}"; do +> arg="${arg#"${arg%%[![:space:]]*}"}" +> arg="${arg%"${arg##*[![:space:]]}"}" +> [[ "$arg" =~ ^\"(.*)\"$ || "$arg" =~ ^\'(.*)\'$ ]] && arg="${BASH_REMATCH[1]}" +> argfile_input+=("$arg") +> done +> else +> arg="$line" +> [[ "$arg" =~ ^\"(.*)\"$ || "$arg" =~ ^\'(.*)\'$ ]] && arg="${BASH_REMATCH[1]}" +> argfile_input+=("$arg") +> fi +> done < "$argfile_path" +> +> argfile_input+=("$@") +> } +> diff --git a/lib/bashly/views/command/master_script.gtx b/lib/bashly/views/command/master_script.gtx index 0b5eccd4..3770660a 100644 --- a/lib/bashly/views/command/master_script.gtx +++ b/lib/bashly/views/command/master_script.gtx @@ -4,6 +4,7 @@ = render :version_command = render :usage = render :normalize_input += render :argfile_helpers if argfile_used_anywhere? = render :inspect_args if Settings.enabled? :inspect_args = render :user_lib if user_lib.any? = render :command_functions diff --git a/lib/bashly/views/command/parse_requirements.gtx b/lib/bashly/views/command/parse_requirements.gtx index 88d5e4fb..f19eee72 100644 --- a/lib/bashly/views/command/parse_requirements.gtx +++ b/lib/bashly/views/command/parse_requirements.gtx @@ -8,6 +8,7 @@ end > local key > += render(:argfile_filter).indent 2 if argfile = render(:fixed_flags_filter).indent 2 = render(:environment_variables_filter).indent 2 = render(:dependencies_filter).indent 2 From 9878f5b67837fd4966b4daeb9ca9b14498ed9056 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Sun, 22 Mar 2026 11:29:49 +0200 Subject: [PATCH 2/6] add argfile to bashly schema --- schemas/bashly.json | 15 +++++++++++++++ support/schema/bashly.yml | 13 +++++++++++++ 2 files changed, 28 insertions(+) diff --git a/schemas/bashly.json b/schemas/bashly.json index dfdcb352..ca7a360e 100644 --- a/schemas/bashly.json +++ b/schemas/bashly.json @@ -854,6 +854,15 @@ "dir_commands/list.sh" ] }, + "argfile-property": { + "title": "argfile", + "description": "A file containing additional arguments to autoload for the current script or sub-command\nhttps://bashly.dev/configuration/command/#argfile", + "type": "string", + "minLength": 1, + "examples": [ + ".mycli" + ] + }, "filters-property": { "title": "filters", "description": "Filters of the current script or sub-command\nhttps://bashly.dev/configuration/command/#filters", @@ -951,6 +960,9 @@ "filename": { "$ref": "#/definitions/filename-property" }, + "argfile": { + "$ref": "#/definitions/argfile-property" + }, "filters": { "$ref": "#/definitions/filters-property" }, @@ -1028,6 +1040,9 @@ "filename": { "$ref": "#/definitions/filename-property" }, + "argfile": { + "$ref": "#/definitions/argfile-property" + }, "filters": { "$ref": "#/definitions/filters-property" }, diff --git a/support/schema/bashly.yml b/support/schema/bashly.yml index 1cc26157..b41a511e 100644 --- a/support/schema/bashly.yml +++ b/support/schema/bashly.yml @@ -721,6 +721,15 @@ definitions: minLength: 1 examples: - dir_commands/list.sh + argfile-property: + title: argfile + description: |- + A file containing additional arguments to autoload for the current script or sub-command + https://bashly.dev/configuration/command/#argfile + type: string + minLength: 1 + examples: + - .mycli filters-property: title: filters description: |- @@ -802,6 +811,8 @@ definitions: $ref: '#/definitions/sub-command-expose-property' filename: $ref: '#/definitions/filename-property' + argfile: + $ref: '#/definitions/argfile-property' filters: $ref: '#/definitions/filters-property' function: @@ -850,6 +861,8 @@ properties: $ref: '#/definitions/root-extensible-property' filename: $ref: '#/definitions/filename-property' + argfile: + $ref: '#/definitions/argfile-property' filters: $ref: '#/definitions/filters-property' function: From 5c36a85f73b78f4352c14465d0a77418ac8294e0 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Sun, 22 Mar 2026 11:47:05 +0200 Subject: [PATCH 3/6] - Add argfile example --- examples/README.md | 1 + examples/argfile/.download | 2 + examples/argfile/README.md | 86 ++++++++ examples/argfile/download | 294 +++++++++++++++++++++++++++ examples/argfile/src/bashly.yml | 20 ++ examples/argfile/src/root_command.sh | 5 + examples/argfile/test.sh | 12 ++ spec/approvals/examples/argfile | 23 +++ 8 files changed, 443 insertions(+) create mode 100644 examples/argfile/.download create mode 100644 examples/argfile/README.md create mode 100644 examples/argfile/download create mode 100644 examples/argfile/src/bashly.yml create mode 100644 examples/argfile/src/root_command.sh create mode 100644 examples/argfile/test.sh create mode 100644 spec/approvals/examples/argfile diff --git a/examples/README.md b/examples/README.md index f82d413b..a3a3a154 100644 --- a/examples/README.md +++ b/examples/README.md @@ -39,6 +39,7 @@ Each of these examples demonstrates one aspect or feature of bashly. - [private-reveal](private-reveal#readme) - allowing users to reveal private commands, flags or environment variables - [stdin](stdin#readme) - reading input from stdin - [filters](filters#readme) - preventing commands from running unless custom conditions are met +- [argfile](argfile#readme) - auto-load arguments from a file - [commands-expose](commands-expose#readme) - showing subcommands in the parent's help - [key-value-pairs](key-value-pairs#readme) - parsing key=value arguments and flags - [command-examples-on-error](command-examples-on-error#readme) - showing examples on error diff --git a/examples/argfile/.download b/examples/argfile/.download new file mode 100644 index 00000000..b9b953a1 --- /dev/null +++ b/examples/argfile/.download @@ -0,0 +1,2 @@ +--force +--log "some path with spaces.log" diff --git a/examples/argfile/README.md b/examples/argfile/README.md new file mode 100644 index 00000000..911358ab --- /dev/null +++ b/examples/argfile/README.md @@ -0,0 +1,86 @@ +# Argfile Example + +Demonstrates how to autoload additional arguments from a file using the +`argfile` command option. + +This example was generated with: + +```bash +$ bashly init --minimal +# ... now edit src/bashly.yml to match the example +# ... now create .download to match te example +$ bashly generate +``` + + + +----- + +## `bashly.yml` + +````yaml +name: download +help: Sample application with autoloaded arguments +version: 0.1.0 + +# Allow users to configure args and flags in a file named '.download' +argfile: .download + +args: +- name: source + required: true + help: URL to download from + +flags: +- long: --force + short: -f + help: Overwrite existing files +- long: --log + short: -l + arg: path + help: Path to log file +```` + +## `.download` + +````bash +--force +--log "some path with spaces.log" + +```` + + +## Output + +### `$ ./download somesource` + +````shell +# This file is located at 'src/root_command.sh'. +# It contains the implementation for the 'download' command. +# The code you write here will be wrapped by a function named 'root_command()'. +# Feel free to edit this file; your changes will persist when regenerating. +args: +- ${args[--force]} = 1 +- ${args[--log]} = some path with spaces.log +- ${args[source]} = somesource + + +```` + +### `$ ./download somesource --log cli.log` + +````shell +# This file is located at 'src/root_command.sh'. +# It contains the implementation for the 'download' command. +# The code you write here will be wrapped by a function named 'root_command()'. +# Feel free to edit this file; your changes will persist when regenerating. +args: +- ${args[--force]} = 1 +- ${args[--log]} = cli.log +- ${args[source]} = somesource + + +```` + + + diff --git a/examples/argfile/download b/examples/argfile/download new file mode 100644 index 00000000..5d524ca5 --- /dev/null +++ b/examples/argfile/download @@ -0,0 +1,294 @@ +#!/usr/bin/env bash +# This script was generated by bashly 1.3.6 (https://bashly.dev) +# Modifying it manually is not recommended + +# :wrapper.bash3_bouncer +if ((BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 2))); then + printf "bash version 4.2 or higher is required\n" >&2 + exit 1 +fi + +# :command.master_script +# :command.root_command +root_command() { + # src/root_command.sh + echo "# This file is located at 'src/root_command.sh'." + echo "# It contains the implementation for the 'download' command." + echo "# The code you write here will be wrapped by a function named 'root_command()'." + echo "# Feel free to edit this file; your changes will persist when regenerating." + inspect_args + +} + +# :command.version_command +version_command() { + echo "$version" +} + +# :command.usage +download_usage() { + printf "download - Sample application with autoloaded arguments\n\n" + + printf "%s\n" "Usage:" + printf " download SOURCE [OPTIONS]\n" + printf " download --help | -h\n" + printf " download --version | -v\n" + echo + + # :command.long_usage + if [[ -n "$long_usage" ]]; then + # :command.usage_options + printf "%s\n" "Options:" + + # :command.usage_flags + # :flag.usage + printf " %s\n" "--force, -f" + printf " Overwrite existing files\n" + echo + + # :flag.usage + printf " %s\n" "--log, -l PATH" + printf " Path to log file\n" + echo + + # :command.usage_fixed_flags + printf " %s\n" "--help, -h" + printf " Show this help\n" + echo + printf " %s\n" "--version, -v" + printf " Show version number\n" + echo + + # :command.usage_args + printf "%s\n" "Arguments:" + + # :argument.usage + printf " %s\n" "SOURCE" + printf " URL to download from\n" + echo + + fi +} + +# :command.normalize_input +# :command.normalize_input_function +normalize_input() { + local arg passthru flags + passthru=false + + while [[ $# -gt 0 ]]; do + arg="$1" + if [[ $passthru == true ]]; then + input+=("$arg") + elif [[ $arg =~ ^(--[a-zA-Z0-9_\-]+)=(.+)$ ]]; then + input+=("${BASH_REMATCH[1]}") + input+=("${BASH_REMATCH[2]}") + elif [[ $arg =~ ^(-[a-zA-Z0-9])=(.+)$ ]]; then + input+=("${BASH_REMATCH[1]}") + input+=("${BASH_REMATCH[2]}") + elif [[ $arg =~ ^-([a-zA-Z0-9][a-zA-Z0-9]+)$ ]]; then + flags="${BASH_REMATCH[1]}" + for ((i = 0; i < ${#flags}; i++)); do + input+=("-${flags:i:1}") + done + elif [[ "$arg" == "--" ]]; then + passthru=true + input+=("$arg") + else + input+=("$arg") + fi + + shift + done +} + +# :command.argfile_helpers +load_command_argfile() { + local argfile_path line arg + argfile_path="$1" + shift + argfile_input=() + + if [[ ! -f "$argfile_path" ]]; then + argfile_input=("$@") + return + fi + + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" + + [[ -z "$line" || "${line:0:1}" == "#" ]] && continue + + if [[ "$line" =~ ^(-{1,2}[^[:space:]]+)[[:space:]]+(.+)$ ]]; then + for arg in "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}"; do + arg="${arg#"${arg%%[![:space:]]*}"}" + arg="${arg%"${arg##*[![:space:]]}"}" + [[ "$arg" =~ ^\"(.*)\"$ || "$arg" =~ ^\'(.*)\'$ ]] && arg="${BASH_REMATCH[1]}" + argfile_input+=("$arg") + done + else + arg="$line" + [[ "$arg" =~ ^\"(.*)\"$ || "$arg" =~ ^\'(.*)\'$ ]] && arg="${BASH_REMATCH[1]}" + argfile_input+=("$arg") + fi + done < "$argfile_path" + + argfile_input+=("$@") +} + +# :command.inspect_args +inspect_args() { + local k + + if ((${#args[@]})); then + readarray -t sorted_keys < <(printf '%s\n' "${!args[@]}" | sort) + echo args: + for k in "${sorted_keys[@]}"; do + echo "- \${args[$k]} = ${args[$k]}" + done + else + echo args: none + fi + + if ((${#deps[@]})); then + readarray -t sorted_keys < <(printf '%s\n' "${!deps[@]}" | sort) + echo + echo deps: + for k in "${sorted_keys[@]}"; do + echo "- \${deps[$k]} = ${deps[$k]}" + done + fi + + if ((${#env_var_names[@]})); then + readarray -t sorted_names < <(printf '%s\n' "${env_var_names[@]}" | sort) + echo + echo "environment variables:" + for k in "${sorted_names[@]}"; do + echo "- \$$k = ${!k:-}" + done + fi +} + +# :command.command_functions + +# :command.parse_requirements +parse_requirements() { + local key + + # :command.argfile_filter + load_command_argfile ".download" "$@" + set -- "${argfile_input[@]}" + + # :command.fixed_flags_filter + while [[ $# -gt 0 ]]; do + key="$1" + case "$key" in + --version | -v) + version_command + exit + ;; + + --help | -h) + long_usage=yes + download_usage + exit + ;; + + *) + break + ;; + + esac + done + + # :command.command_filter + action="root" + + # :command.parse_requirements_while + while [[ $# -gt 0 ]]; do + key="$1" + case "$key" in + # :flag.case + --force | -f) + + # :flag.case_no_arg + args['--force']=1 + shift + ;; + + # :flag.case + --log | -l) + + # :flag.case_arg + if [[ -n ${2+x} ]]; then + args['--log']="$2" + shift + shift + else + printf "%s\n" "--log requires an argument: --log, -l PATH" >&2 + exit 1 + fi + ;; + + -?*) + printf "invalid option: %s\n" "$key" >&2 + exit 1 + ;; + + *) + # :command.parse_requirements_case + # :command.parse_requirements_case_simple + # :argument.case + if [[ -z ${args['source']+x} ]]; then + args['source']=$1 + shift + else + printf "invalid argument: %s\n" "$key" >&2 + exit 1 + fi + + ;; + + esac + done + + # :command.required_args_filter + if [[ -z ${args['source']+x} ]]; then + printf "missing required argument: SOURCE\nusage: download SOURCE [OPTIONS]\n" >&2 + + exit 1 + fi + +} + +# :command.initialize +initialize() { + declare -g version="0.1.0" + set -euo pipefail + +} + +# :command.run +run() { + # :command.globals + declare -g long_usage='' + declare -g -A args=() + declare -g -A deps=() + declare -g -a env_var_names=() + declare -g -a input=() + + normalize_input "$@" + parse_requirements "${input[@]}" + + case "$action" in + "root") root_command ;; + esac +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + # :command.start + command_line_args=("$@") + initialize + run "${command_line_args[@]}" +fi diff --git a/examples/argfile/src/bashly.yml b/examples/argfile/src/bashly.yml new file mode 100644 index 00000000..7055321a --- /dev/null +++ b/examples/argfile/src/bashly.yml @@ -0,0 +1,20 @@ +name: download +help: Sample application with autoloaded arguments +version: 0.1.0 + +# Allow users to configure args and flags in a file named '.download' +argfile: .download + +args: +- name: source + required: true + help: URL to download from + +flags: +- long: --force + short: -f + help: Overwrite existing files +- long: --log + short: -l + arg: path + help: Path to log file diff --git a/examples/argfile/src/root_command.sh b/examples/argfile/src/root_command.sh new file mode 100644 index 00000000..47e7f868 --- /dev/null +++ b/examples/argfile/src/root_command.sh @@ -0,0 +1,5 @@ +echo "# This file is located at 'src/root_command.sh'." +echo "# It contains the implementation for the 'download' command." +echo "# The code you write here will be wrapped by a function named 'root_command()'." +echo "# Feel free to edit this file; your changes will persist when regenerating." +inspect_args diff --git a/examples/argfile/test.sh b/examples/argfile/test.sh new file mode 100644 index 00000000..5a58e0a7 --- /dev/null +++ b/examples/argfile/test.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +rm -f ./download + +set -x + +bashly generate + +### Try Me ### + +./download somesource +./download somesource --log cli.log diff --git a/spec/approvals/examples/argfile b/spec/approvals/examples/argfile new file mode 100644 index 00000000..950ab885 --- /dev/null +++ b/spec/approvals/examples/argfile @@ -0,0 +1,23 @@ ++ bashly generate +creating user files in src +skipped src/root_command.sh (exists) +created ./download +run ./download --help to test your bash script ++ ./download somesource +# This file is located at 'src/root_command.sh'. +# It contains the implementation for the 'download' command. +# The code you write here will be wrapped by a function named 'root_command()'. +# Feel free to edit this file; your changes will persist when regenerating. +args: +- ${args[--force]} = 1 +- ${args[--log]} = some path with spaces.log +- ${args[source]} = somesource ++ ./download somesource --log cli.log +# This file is located at 'src/root_command.sh'. +# It contains the implementation for the 'download' command. +# The code you write here will be wrapped by a function named 'root_command()'. +# Feel free to edit this file; your changes will persist when regenerating. +args: +- ${args[--force]} = 1 +- ${args[--log]} = cli.log +- ${args[source]} = somesource From 6e323957f15c81a03b086b6e6d843ea7352994d2 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Sun, 22 Mar 2026 12:01:52 +0200 Subject: [PATCH 4/6] fix shellcheck --- examples/argfile/.gitignore | 1 + examples/argfile/download | 294 ------------------- lib/bashly/views/command/argfile_helpers.gtx | 2 +- 3 files changed, 2 insertions(+), 295 deletions(-) create mode 100644 examples/argfile/.gitignore delete mode 100644 examples/argfile/download diff --git a/examples/argfile/.gitignore b/examples/argfile/.gitignore new file mode 100644 index 00000000..8a58e1ae --- /dev/null +++ b/examples/argfile/.gitignore @@ -0,0 +1 @@ +download \ No newline at end of file diff --git a/examples/argfile/download b/examples/argfile/download deleted file mode 100644 index 5d524ca5..00000000 --- a/examples/argfile/download +++ /dev/null @@ -1,294 +0,0 @@ -#!/usr/bin/env bash -# This script was generated by bashly 1.3.6 (https://bashly.dev) -# Modifying it manually is not recommended - -# :wrapper.bash3_bouncer -if ((BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 2))); then - printf "bash version 4.2 or higher is required\n" >&2 - exit 1 -fi - -# :command.master_script -# :command.root_command -root_command() { - # src/root_command.sh - echo "# This file is located at 'src/root_command.sh'." - echo "# It contains the implementation for the 'download' command." - echo "# The code you write here will be wrapped by a function named 'root_command()'." - echo "# Feel free to edit this file; your changes will persist when regenerating." - inspect_args - -} - -# :command.version_command -version_command() { - echo "$version" -} - -# :command.usage -download_usage() { - printf "download - Sample application with autoloaded arguments\n\n" - - printf "%s\n" "Usage:" - printf " download SOURCE [OPTIONS]\n" - printf " download --help | -h\n" - printf " download --version | -v\n" - echo - - # :command.long_usage - if [[ -n "$long_usage" ]]; then - # :command.usage_options - printf "%s\n" "Options:" - - # :command.usage_flags - # :flag.usage - printf " %s\n" "--force, -f" - printf " Overwrite existing files\n" - echo - - # :flag.usage - printf " %s\n" "--log, -l PATH" - printf " Path to log file\n" - echo - - # :command.usage_fixed_flags - printf " %s\n" "--help, -h" - printf " Show this help\n" - echo - printf " %s\n" "--version, -v" - printf " Show version number\n" - echo - - # :command.usage_args - printf "%s\n" "Arguments:" - - # :argument.usage - printf " %s\n" "SOURCE" - printf " URL to download from\n" - echo - - fi -} - -# :command.normalize_input -# :command.normalize_input_function -normalize_input() { - local arg passthru flags - passthru=false - - while [[ $# -gt 0 ]]; do - arg="$1" - if [[ $passthru == true ]]; then - input+=("$arg") - elif [[ $arg =~ ^(--[a-zA-Z0-9_\-]+)=(.+)$ ]]; then - input+=("${BASH_REMATCH[1]}") - input+=("${BASH_REMATCH[2]}") - elif [[ $arg =~ ^(-[a-zA-Z0-9])=(.+)$ ]]; then - input+=("${BASH_REMATCH[1]}") - input+=("${BASH_REMATCH[2]}") - elif [[ $arg =~ ^-([a-zA-Z0-9][a-zA-Z0-9]+)$ ]]; then - flags="${BASH_REMATCH[1]}" - for ((i = 0; i < ${#flags}; i++)); do - input+=("-${flags:i:1}") - done - elif [[ "$arg" == "--" ]]; then - passthru=true - input+=("$arg") - else - input+=("$arg") - fi - - shift - done -} - -# :command.argfile_helpers -load_command_argfile() { - local argfile_path line arg - argfile_path="$1" - shift - argfile_input=() - - if [[ ! -f "$argfile_path" ]]; then - argfile_input=("$@") - return - fi - - while IFS= read -r line || [[ -n "$line" ]]; do - line="${line#"${line%%[![:space:]]*}"}" - line="${line%"${line##*[![:space:]]}"}" - - [[ -z "$line" || "${line:0:1}" == "#" ]] && continue - - if [[ "$line" =~ ^(-{1,2}[^[:space:]]+)[[:space:]]+(.+)$ ]]; then - for arg in "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}"; do - arg="${arg#"${arg%%[![:space:]]*}"}" - arg="${arg%"${arg##*[![:space:]]}"}" - [[ "$arg" =~ ^\"(.*)\"$ || "$arg" =~ ^\'(.*)\'$ ]] && arg="${BASH_REMATCH[1]}" - argfile_input+=("$arg") - done - else - arg="$line" - [[ "$arg" =~ ^\"(.*)\"$ || "$arg" =~ ^\'(.*)\'$ ]] && arg="${BASH_REMATCH[1]}" - argfile_input+=("$arg") - fi - done < "$argfile_path" - - argfile_input+=("$@") -} - -# :command.inspect_args -inspect_args() { - local k - - if ((${#args[@]})); then - readarray -t sorted_keys < <(printf '%s\n' "${!args[@]}" | sort) - echo args: - for k in "${sorted_keys[@]}"; do - echo "- \${args[$k]} = ${args[$k]}" - done - else - echo args: none - fi - - if ((${#deps[@]})); then - readarray -t sorted_keys < <(printf '%s\n' "${!deps[@]}" | sort) - echo - echo deps: - for k in "${sorted_keys[@]}"; do - echo "- \${deps[$k]} = ${deps[$k]}" - done - fi - - if ((${#env_var_names[@]})); then - readarray -t sorted_names < <(printf '%s\n' "${env_var_names[@]}" | sort) - echo - echo "environment variables:" - for k in "${sorted_names[@]}"; do - echo "- \$$k = ${!k:-}" - done - fi -} - -# :command.command_functions - -# :command.parse_requirements -parse_requirements() { - local key - - # :command.argfile_filter - load_command_argfile ".download" "$@" - set -- "${argfile_input[@]}" - - # :command.fixed_flags_filter - while [[ $# -gt 0 ]]; do - key="$1" - case "$key" in - --version | -v) - version_command - exit - ;; - - --help | -h) - long_usage=yes - download_usage - exit - ;; - - *) - break - ;; - - esac - done - - # :command.command_filter - action="root" - - # :command.parse_requirements_while - while [[ $# -gt 0 ]]; do - key="$1" - case "$key" in - # :flag.case - --force | -f) - - # :flag.case_no_arg - args['--force']=1 - shift - ;; - - # :flag.case - --log | -l) - - # :flag.case_arg - if [[ -n ${2+x} ]]; then - args['--log']="$2" - shift - shift - else - printf "%s\n" "--log requires an argument: --log, -l PATH" >&2 - exit 1 - fi - ;; - - -?*) - printf "invalid option: %s\n" "$key" >&2 - exit 1 - ;; - - *) - # :command.parse_requirements_case - # :command.parse_requirements_case_simple - # :argument.case - if [[ -z ${args['source']+x} ]]; then - args['source']=$1 - shift - else - printf "invalid argument: %s\n" "$key" >&2 - exit 1 - fi - - ;; - - esac - done - - # :command.required_args_filter - if [[ -z ${args['source']+x} ]]; then - printf "missing required argument: SOURCE\nusage: download SOURCE [OPTIONS]\n" >&2 - - exit 1 - fi - -} - -# :command.initialize -initialize() { - declare -g version="0.1.0" - set -euo pipefail - -} - -# :command.run -run() { - # :command.globals - declare -g long_usage='' - declare -g -A args=() - declare -g -A deps=() - declare -g -a env_var_names=() - declare -g -a input=() - - normalize_input "$@" - parse_requirements "${input[@]}" - - case "$action" in - "root") root_command ;; - esac -} - -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - # :command.start - command_line_args=("$@") - initialize - run "${command_line_args[@]}" -fi diff --git a/lib/bashly/views/command/argfile_helpers.gtx b/lib/bashly/views/command/argfile_helpers.gtx index 339f0711..6c00e97b 100644 --- a/lib/bashly/views/command/argfile_helpers.gtx +++ b/lib/bashly/views/command/argfile_helpers.gtx @@ -29,7 +29,7 @@ > [[ "$arg" =~ ^\"(.*)\"$ || "$arg" =~ ^\'(.*)\'$ ]] && arg="${BASH_REMATCH[1]}" > argfile_input+=("$arg") > fi -> done < "$argfile_path" +> done <"$argfile_path" > > argfile_input+=("$@") > } From 5b78b39f6054fb90a0d821614ded95d727e3c77f Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Sun, 22 Mar 2026 12:12:30 +0200 Subject: [PATCH 5/6] fix typo --- examples/argfile/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/argfile/README.md b/examples/argfile/README.md index 911358ab..35aba054 100644 --- a/examples/argfile/README.md +++ b/examples/argfile/README.md @@ -8,7 +8,7 @@ This example was generated with: ```bash $ bashly init --minimal # ... now edit src/bashly.yml to match the example -# ... now create .download to match te example +# ... now create .download to match the example $ bashly generate ``` From 0689d0870cb3065a6b3605139cbb2c1b07ab324a Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Sun, 22 Mar 2026 12:13:13 +0200 Subject: [PATCH 6/6] fix example readme --- examples/argfile/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/argfile/README.md b/examples/argfile/README.md index 35aba054..cf2a4f54 100644 --- a/examples/argfile/README.md +++ b/examples/argfile/README.md @@ -7,8 +7,8 @@ This example was generated with: ```bash $ bashly init --minimal -# ... now edit src/bashly.yml to match the example -# ... now create .download to match the example +# ... now edit src/bashly.yml to match the example ... +# ... now create .download to match the example ... $ bashly generate ```