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/.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 cf2a4f54..7cb542df 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: @@ -39,19 +39,47 @@ flags: short: -l arg: path help: Path to log file + +# 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 ```` ## `.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" + +# 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 @@ -61,6 +89,7 @@ flags: # 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 @@ -76,11 +105,44 @@ 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"` + +````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 x-from-cli:\ 2 +- ${args[--log]} = some path with spaces.log +- ${args[source]} = somesource + + +```` + +### `$ ./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 7055321a..c8097463 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,11 @@ flags: short: -l arg: path help: Path to log file + +# 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 5a58e0a7..d4751a55 100644 --- a/examples/argfile/test.sh +++ b/examples/argfile/test.sh @@ -8,5 +8,10 @@ bashly generate ### Try Me ### +./download --version ./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/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..ade1d719 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..89cf0f2a --- /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/spec/approvals/examples/argfile b/spec/approvals/examples/argfile index 950ab885..87567281 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,26 @@ 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 ++ ./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 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