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/.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/README.md b/examples/argfile/README.md new file mode 100644 index 00000000..cf2a4f54 --- /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 the 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/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/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..6c00e97b --- /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 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/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 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: