From 736ced8e68a88ecb3cc5665491ca2957a28e3a1a Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Mon, 23 Mar 2026 15:05:48 +0200 Subject: [PATCH 1/4] - Change `command.argfile` to apply defaults instead of prepending the command line --- examples/README.md | 2 +- examples/argfile/README.md | 21 ++++++++--- lib/bashly/script/introspection/commands.rb | 5 --- lib/bashly/views/command/argfile_filter.gtx | 17 +++++++-- lib/bashly/views/command/argfile_helpers.gtx | 36 ------------------- lib/bashly/views/command/master_script.gtx | 1 - .../views/command/parse_requirements.gtx | 2 +- lib/bashly/views/flag/argfile_case.gtx | 6 ++++ lib/bashly/views/flag/argfile_case_arg.gtx | 23 ++++++++++++ lib/bashly/views/flag/argfile_case_no_arg.gtx | 7 ++++ schemas/bashly.json | 2 +- support/schema/bashly.yml | 4 ++- 12 files changed, 74 insertions(+), 52 deletions(-) delete mode 100644 lib/bashly/views/command/argfile_helpers.gtx create mode 100644 lib/bashly/views/flag/argfile_case.gtx create mode 100644 lib/bashly/views/flag/argfile_case_arg.gtx create mode 100644 lib/bashly/views/flag/argfile_case_no_arg.gtx diff --git a/examples/README.md b/examples/README.md index a3a3a154..6fe22c37 100644 --- a/examples/README.md +++ b/examples/README.md @@ -39,7 +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 +- [argfile](argfile#readme) - auto-load flag defaults 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/README.md b/examples/argfile/README.md index cf2a4f54..fbbf72b9 100644 --- a/examples/argfile/README.md +++ b/examples/argfile/README.md @@ -1,7 +1,7 @@ # Argfile Example -Demonstrates how to autoload additional arguments from a file using the -`argfile` command option. +Demonstrates how to autoload flag defaults from a file using the `argfile` +command option. This example was generated with: @@ -23,7 +23,7 @@ name: download help: Sample application with autoloaded arguments version: 0.1.0 -# Allow users to configure args and flags in a file named '.download' +# Allow users to configure flag defaults in a file named '.download' argfile: .download args: @@ -49,6 +49,9 @@ flags: ```` +Only flag lines are loaded from the argfile. Each flag value must appear on the +same line as the flag. Non-flag lines are ignored. + ## Output @@ -67,6 +70,17 @@ args: ```` +### `$ ./download --help` + +````shell +download - Sample application with autoloaded arguments + +Usage: + download SOURCE [OPTIONS] + download --help | -h + download --version | -v +```` + ### `$ ./download somesource --log cli.log` ````shell @@ -83,4 +97,3 @@ args: ```` - diff --git a/lib/bashly/script/introspection/commands.rb b/lib/bashly/script/introspection/commands.rb index 8e4bc7c7..087ac203 100644 --- a/lib/bashly/script/introspection/commands.rb +++ b/lib/bashly/script/introspection/commands.rb @@ -7,11 +7,6 @@ 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 index 321a9c77..0d3f400e 100644 --- a/lib/bashly/views/command/argfile_filter.gtx +++ b/lib/bashly/views/command/argfile_filter.gtx @@ -1,5 +1,18 @@ = view_marker -> load_command_argfile "{{ argfile }}" "$@" -> set -- "${argfile_input[@]}" +> local argfile_line argfile_key argfile_value escaped +> [[ -f "{{ argfile }}" ]] || return +> +> while IFS= read -r argfile_line || [[ -n "$argfile_line" ]]; do +> [[ "$argfile_line" =~ ^[[:space:]]*(-{1,2}[^[:space:]]+)([[:space:]]+(.+))?[[:space:]]*$ ]] || continue +> argfile_key="${BASH_REMATCH[1]}" +> argfile_value="${BASH_REMATCH[3]:-}" +> argfile_value="${argfile_value#"${argfile_value%%[![:space:]]*}"}" +> argfile_value="${argfile_value%"${argfile_value##*[![:space:]]}"}" +> [[ "$argfile_value" =~ ^\"(.*)\"$ || "$argfile_value" =~ ^\'(.*)\'$ ]] && argfile_value="${BASH_REMATCH[1]}" +> +> case "$argfile_key" in += flags.map { |flag| flag.render(:argfile_case) }.join.indent 4 +> esac +> done < "{{ argfile }}" > diff --git a/lib/bashly/views/command/argfile_helpers.gtx b/lib/bashly/views/command/argfile_helpers.gtx deleted file mode 100644 index 6c00e97b..00000000 --- a/lib/bashly/views/command/argfile_helpers.gtx +++ /dev/null @@ -1,36 +0,0 @@ -= 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 3770660a..0b5eccd4 100644 --- a/lib/bashly/views/command/master_script.gtx +++ b/lib/bashly/views/command/master_script.gtx @@ -4,7 +4,6 @@ = 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 f19eee72..bcd16644 100644 --- a/lib/bashly/views/command/parse_requirements.gtx +++ b/lib/bashly/views/command/parse_requirements.gtx @@ -8,8 +8,8 @@ end > local key > -= render(:argfile_filter).indent 2 if argfile = render(:fixed_flags_filter).indent 2 += render(:argfile_filter).indent 2 if argfile = render(:environment_variables_filter).indent 2 = render(:dependencies_filter).indent 2 = render(:command_filter).indent 2 diff --git a/lib/bashly/views/flag/argfile_case.gtx b/lib/bashly/views/flag/argfile_case.gtx new file mode 100644 index 00000000..27ef6e0c --- /dev/null +++ b/lib/bashly/views/flag/argfile_case.gtx @@ -0,0 +1,6 @@ += view_marker + +> {{ aliases.join " | " }}) += render(arg ? :argfile_case_arg : :argfile_case_no_arg).indent 2 +> ;; +> diff --git a/lib/bashly/views/flag/argfile_case_arg.gtx b/lib/bashly/views/flag/argfile_case_arg.gtx new file mode 100644 index 00000000..5318eaa3 --- /dev/null +++ b/lib/bashly/views/flag/argfile_case_arg.gtx @@ -0,0 +1,23 @@ += view_marker + +> if [[ -n "$argfile_value" ]]; then + +if repeatable + > escaped="$(printf '%q' "$argfile_value")" + > if [[ -z ${args['{{ name }}']+x} ]]; then + > args['{{ name }}']="$escaped" + if unique + > unique_lookup["{{ name }}:$escaped"]=1 + > elif [[ -z "${unique_lookup["{{ name }}:$escaped"]:-}" ]]; then + > args['{{ name }}']="${args['{{ name }}']} $escaped" + > unique_lookup["{{ name }}:$escaped"]=1 + else + > else + > args['{{ name }}']="${args['{{ name }}']} $escaped" + end + > fi +else + > [[ -n ${args['{{ name }}']+x} ]] || args['{{ name }}']="$argfile_value" +end + +> fi diff --git a/lib/bashly/views/flag/argfile_case_no_arg.gtx b/lib/bashly/views/flag/argfile_case_no_arg.gtx new file mode 100644 index 00000000..0a7e2e0e --- /dev/null +++ b/lib/bashly/views/flag/argfile_case_no_arg.gtx @@ -0,0 +1,7 @@ += view_marker + +if repeatable + > ((args['{{ name }}'] += 1)) +else + > [[ -n ${args['{{ name }}']+x} ]] || args['{{ name }}']=1 +end diff --git a/schemas/bashly.json b/schemas/bashly.json index ca7a360e..69dc00f6 100644 --- a/schemas/bashly.json +++ b/schemas/bashly.json @@ -856,7 +856,7 @@ }, "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", + "description": "A file containing flag defaults to autoload for the current script or sub-command.\nEach supported flag must appear on a single line, optionally followed by its value.\nNon-flag lines are ignored.\nhttps://bashly.dev/configuration/command/#argfile", "type": "string", "minLength": 1, "examples": [ diff --git a/support/schema/bashly.yml b/support/schema/bashly.yml index b41a511e..7281ab06 100644 --- a/support/schema/bashly.yml +++ b/support/schema/bashly.yml @@ -724,7 +724,9 @@ definitions: argfile-property: title: argfile description: |- - A file containing additional arguments to autoload for the current script or sub-command + A file containing flag defaults to autoload for the current script or sub-command. + Each supported flag must appear on a single line, optionally followed by its value. + Non-flag lines are ignored. https://bashly.dev/configuration/command/#argfile type: string minLength: 1 From d4f42c5b97fc6f76206729f95b1a2cb40af24aca Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Mon, 23 Mar 2026 15:21:22 +0200 Subject: [PATCH 2/4] update argfile example --- examples/argfile/.download | 12 ++++++++ examples/argfile/README.md | 54 ++++++++++++++++++++++++++------- examples/argfile/src/bashly.yml | 9 +++++- examples/argfile/test.sh | 2 ++ spec/approvals/examples/argfile | 14 +++++++++ 5 files changed, 79 insertions(+), 12 deletions(-) diff --git a/examples/argfile/.download b/examples/argfile/.download index b9b953a1..0505d193 100644 --- a/examples/argfile/.download +++ b/examples/argfile/.download @@ -1,2 +1,14 @@ +# Boolean flags in the argfile are loaded as defaults --force + +# Flag values must appear on the same line --log "some path with spaces.log" + +# Arguments in argfile also work for repeatable flags +--header "x-from-file: 1" + +# Unknown flags in the argfile are ignored +--no-such-flag + +# Non-flag lines in the argfile are ignored +this line is ignored diff --git a/examples/argfile/README.md b/examples/argfile/README.md index fbbf72b9..58944518 100644 --- a/examples/argfile/README.md +++ b/examples/argfile/README.md @@ -39,22 +39,46 @@ flags: short: -l arg: path help: Path to log file + +# Arguments in argfile also work for repeatable flags +- long: --header + short: -H + arg: value + repeatable: true + help: Add an HTTP header ```` ## `.download` ````bash +# Boolean flags in the argfile are loaded as defaults --force + +# Flag values must appear on the same line --log "some path with spaces.log" -```` +# Arguments in argfile also work for repeatable flags +--header "x-from-file: 1" -Only flag lines are loaded from the argfile. Each flag value must appear on the -same line as the flag. Non-flag lines are ignored. +# Unknown flags in the argfile are ignored +--no-such-flag + +# Non-flag lines in the argfile are ignored +this line is ignored + +```` ## Output +### `$ ./download --version` + +````shell +0.1.0 + + +```` + ### `$ ./download somesource` ````shell @@ -64,24 +88,30 @@ same line as the flag. Non-flag lines are ignored. # Feel free to edit this file; your changes will persist when regenerating. args: - ${args[--force]} = 1 +- ${args[--header]} = x-from-file:\ 1 - ${args[--log]} = some path with spaces.log - ${args[source]} = somesource ```` -### `$ ./download --help` +### `$ ./download somesource --log cli.log` ````shell -download - Sample application with autoloaded arguments +# 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[--header]} = x-from-file:\ 1 +- ${args[--log]} = cli.log +- ${args[source]} = somesource + -Usage: - download SOURCE [OPTIONS] - download --help | -h - download --version | -v ```` -### `$ ./download somesource --log cli.log` +### `$ ./download somesource --header "x-from-cli: 2"` ````shell # This file is located at 'src/root_command.sh'. @@ -90,10 +120,12 @@ Usage: # Feel free to edit this file; your changes will persist when regenerating. args: - ${args[--force]} = 1 -- ${args[--log]} = cli.log +- ${args[--header]} = x-from-file:\ 1 x-from-cli:\ 2 +- ${args[--log]} = some path with spaces.log - ${args[source]} = somesource ```` + diff --git a/examples/argfile/src/bashly.yml b/examples/argfile/src/bashly.yml index 7055321a..3fb575ba 100644 --- a/examples/argfile/src/bashly.yml +++ b/examples/argfile/src/bashly.yml @@ -2,7 +2,7 @@ name: download help: Sample application with autoloaded arguments version: 0.1.0 -# Allow users to configure args and flags in a file named '.download' +# Allow users to configure flag defaults in a file named '.download' argfile: .download args: @@ -18,3 +18,10 @@ flags: short: -l arg: path help: Path to log file + +# Arguments in argfile also work for repeatable flags +- long: --header + short: -H + arg: value + repeatable: true + help: Add an HTTP header diff --git a/examples/argfile/test.sh b/examples/argfile/test.sh index 5a58e0a7..bb31b0a5 100644 --- a/examples/argfile/test.sh +++ b/examples/argfile/test.sh @@ -8,5 +8,7 @@ bashly generate ### Try Me ### +./download --version ./download somesource ./download somesource --log cli.log +./download somesource --header "x-from-cli: 2" diff --git a/spec/approvals/examples/argfile b/spec/approvals/examples/argfile index 950ab885..b34e156d 100644 --- a/spec/approvals/examples/argfile +++ b/spec/approvals/examples/argfile @@ -3,6 +3,8 @@ creating user files in src skipped src/root_command.sh (exists) created ./download run ./download --help to test your bash script ++ ./download --version +0.1.0 + ./download somesource # This file is located at 'src/root_command.sh'. # It contains the implementation for the 'download' command. @@ -10,6 +12,7 @@ run ./download --help to test your bash script # Feel free to edit this file; your changes will persist when regenerating. args: - ${args[--force]} = 1 +- ${args[--header]} = x-from-file:\ 1 - ${args[--log]} = some path with spaces.log - ${args[source]} = somesource + ./download somesource --log cli.log @@ -19,5 +22,16 @@ args: # Feel free to edit this file; your changes will persist when regenerating. args: - ${args[--force]} = 1 +- ${args[--header]} = x-from-file:\ 1 - ${args[--log]} = cli.log - ${args[source]} = somesource ++ ./download somesource --header 'x-from-cli: 2' +# 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[--header]} = x-from-file:\ 1 x-from-cli:\ 2 +- ${args[--log]} = some path with spaces.log +- ${args[source]} = somesource From a412ba3035a2507e3ea2d4c07319509080831048 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Mon, 23 Mar 2026 16:41:27 +0200 Subject: [PATCH 3/4] test argfile with unique repeatable flags --- examples/argfile/README.md | 19 ++++++++++++++++++- examples/argfile/src/bashly.yml | 3 ++- examples/argfile/test.sh | 3 +++ spec/approvals/examples/argfile | 10 ++++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/examples/argfile/README.md b/examples/argfile/README.md index 58944518..7cb542df 100644 --- a/examples/argfile/README.md +++ b/examples/argfile/README.md @@ -40,11 +40,12 @@ flags: arg: path help: Path to log file -# Arguments in argfile also work for repeatable flags +# Arguments in argfile also work for repeatable and unique flags - long: --header short: -H arg: value repeatable: true + unique: true help: Add an HTTP header ```` @@ -127,5 +128,21 @@ args: ```` +### `$ ./download somesource --header "x-from-file: 1"` + +````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[--header]} = x-from-file:\ 1 +- ${args[--log]} = some path with spaces.log +- ${args[source]} = somesource + + +```` + diff --git a/examples/argfile/src/bashly.yml b/examples/argfile/src/bashly.yml index 3fb575ba..c8097463 100644 --- a/examples/argfile/src/bashly.yml +++ b/examples/argfile/src/bashly.yml @@ -19,9 +19,10 @@ flags: arg: path help: Path to log file -# Arguments in argfile also work for repeatable flags +# Arguments in argfile also work for repeatable and unique flags - long: --header short: -H arg: value repeatable: true + unique: true help: Add an HTTP header diff --git a/examples/argfile/test.sh b/examples/argfile/test.sh index bb31b0a5..d4751a55 100644 --- a/examples/argfile/test.sh +++ b/examples/argfile/test.sh @@ -12,3 +12,6 @@ bashly generate ./download somesource ./download somesource --log cli.log ./download somesource --header "x-from-cli: 2" + +# demonstrating uniqueness across command line and argfile +./download somesource --header "x-from-file: 1" diff --git a/spec/approvals/examples/argfile b/spec/approvals/examples/argfile index b34e156d..87567281 100644 --- a/spec/approvals/examples/argfile +++ b/spec/approvals/examples/argfile @@ -35,3 +35,13 @@ args: - ${args[--header]} = x-from-file:\ 1 x-from-cli:\ 2 - ${args[--log]} = some path with spaces.log - ${args[source]} = somesource ++ ./download somesource --header 'x-from-file: 1' +# 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[--header]} = x-from-file:\ 1 +- ${args[--log]} = some path with spaces.log +- ${args[source]} = somesource From 47aa242664348d683b5b0a26aad8b8d9ee80c577 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Mon, 23 Mar 2026 16:46:35 +0200 Subject: [PATCH 4/4] fix shellcheck --- lib/bashly/views/command/argfile_filter.gtx | 2 +- lib/bashly/views/flag/argfile_case.gtx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/bashly/views/command/argfile_filter.gtx b/lib/bashly/views/command/argfile_filter.gtx index 0d3f400e..ade1d719 100644 --- a/lib/bashly/views/command/argfile_filter.gtx +++ b/lib/bashly/views/command/argfile_filter.gtx @@ -14,5 +14,5 @@ > case "$argfile_key" in = flags.map { |flag| flag.render(:argfile_case) }.join.indent 4 > esac -> done < "{{ argfile }}" +> done <"{{ argfile }}" > diff --git a/lib/bashly/views/flag/argfile_case.gtx b/lib/bashly/views/flag/argfile_case.gtx index 27ef6e0c..89cf0f2a 100644 --- a/lib/bashly/views/flag/argfile_case.gtx +++ b/lib/bashly/views/flag/argfile_case.gtx @@ -2,5 +2,5 @@ > {{ aliases.join " | " }}) = render(arg ? :argfile_case_arg : :argfile_case_no_arg).indent 2 -> ;; +> ;; >