diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index fc4f1d40..be468396 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -13,7 +13,7 @@ jobs:
LC_ALL: en_US.UTF-8 # consistent sort order
strategy:
- matrix: { ruby: ['2.4', '2.5', '2.6', '2.7', '3.0'] }
+ matrix: { ruby: ['2.7', '3.0'] }
steps:
- name: Checkout code
diff --git a/README.md b/README.md
index ba6cd3cf..862822ef 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,7 @@ Create beautiful bash scripts from simple YAML configuration
- [Flag options](#flag-options)
- [Environment Variable options](#environment-variable-options)
- [Extensible Scripts](#extensible-scripts)
+- [Bash Completions](#bash-completions)
- [Real World Examples](#real-world-examples)
- [Contributing / Support](#contributing--support)
@@ -75,6 +76,7 @@ Bahsly is responsible for:
- Optional or required **option flags** (with or without flag arguments).
- **Commands** (and subcommands).
- Standard flags (like **--help** and **--version**).
+- Preventing your script from running unless the command line is valid.
- Providing you with a place to input your code for each of the functions
your tool performs, and merging it back to the final script.
- Providing you with additional (optional) framework-style, standard
@@ -82,6 +84,7 @@ Bahsly is responsible for:
- **Color output**.
- **Config file management** (INI format).
- **YAML parsing**.
+ - **Bash completions**.
- and more.
@@ -198,6 +201,7 @@ command and subcommands (under the `commands` definition).
`commands` | Specify the array of [commands](#command-options). Each command will have its own args and flags. Note: if `commands` is provided, you cannot specify flags or args at the same level.
`args` | Specify the array of [positional arguments](#argument-options) this script needs.
`flags` | Specify the array of option [flags](#flag-options) this script needs.
+`completions` | Specify an array of additional completion suggestions when used in conjunction with `bashly add comp`. See [Bash Completions](#bash-completions).
`catch_all` | Specify that this command should allow for additional arbitrary arguments or flags. It can be set in one of three ways:
- Set to `true` to just enable it.
- Set to a string, to use this string in the usage help text.
- Set to a hash containing `label` and `help` keys, to show a detailed help for it when running with `--help`.
`dependencies` | Specify an array of any required external dependencies (commands). The script execution will be halted with a friendly error unless all dependency commands exist.
`group` | In case you have many commands, use this option to specify a caption to display before this command. This option is purely for display purposes, and needs to be specified only for the first command in each group.
@@ -326,6 +330,71 @@ The generated script will execute `git status`.
See the [extensible-delegate example](examples/extensible-delegate).
+## Bash Completions
+
+Bashly comes with built-in bash completions generator, provided by the
+[completely][completely] gem.
+
+By running any of the `bashly add comp` commands, you can add this
+functionality to your script in one of three ways:
+
+- `bashly add comp function` - creates a function in your `./src/lib` directory
+ that echoes a completion script. You can then call this function from any
+ command (for example `yourcli completions`) and your users will be able to
+ install the completions by running `eval "$(yourcli completions)"`.
+- `bashly add comp script` - creates a standalone completion script that can be
+ sourced or copies to the system's bash completions directory.
+- `bashly add comp yaml` - creates the "raw data" YAML file. This is intended
+ mainly for development purposes.
+
+The bash completions generation is completely automatic, and you will have to
+rerun the `bashly add comp *` command whenever you change your `bashly.yml`
+script.
+
+In addition to suggesting subcommands and flags, you can instruct bashly to
+also suggest files, directories, users and more. To do this, add another option
+in your `bashly.yml` on the command you wish to alter:
+
+```yaml
+# bashly.yml
+commands:
+- name: upload
+ help: Upload a file
+ completions: [directory, user]
+
+```
+
+Valid completion additions are:
+
+| Keyword | Meaning
+|-------------|---------------------
+| `alias` | Alias names
+| `arrayvar` | Array variable names
+| `binding` | Readline key binding names
+| `builtin` | Names of shell builtin commands
+| `command` | Command names
+| `directory` | Directory names
+| `disabled` | Names of disabled shell builtins
+| `enabled` | Names of enabled shell builtins
+| `export` | Names of exported shell variables
+| `file` | File names
+| `function` | Names of shell functions
+| `group` | Group names
+| `helptopic` | Help topics as accepted by the help builtin
+| `hostname` | Hostnames, as taken from the file specified by the HOSTFILE shell variable
+| `job` | Job names
+| `keyword` | Shell reserved words
+| `running` | Names of running jobs
+| `service` | Service names
+| `signal` | Signal names
+| `stopped` | Names of stopped jobs
+| `user` | User names
+| `variable` | Names of all shell variables
+
+Note that these are taken from the [Programmable Completion Builtin][compgen],
+and will simply be added using the `compgen -A action` command.
+
+
## Real World Examples
- [Rush][rush] - a Personal Package Manager
@@ -344,3 +413,5 @@ to contribute, feel free to [open an issue][issues].
[rush]: https://github.com/DannyBen/rush-cli
[alf]: https://github.com/DannyBen/alf
[git-changelog]: https://github.com/DannyBen/git-changelog
+[completely]: https://github.com/DannyBen/completely
+[compgen]: https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion-Builtins.html
diff --git a/bashly.gemspec b/bashly.gemspec
index f372bf93..418c092a 100644
--- a/bashly.gemspec
+++ b/bashly.gemspec
@@ -15,9 +15,10 @@ Gem::Specification.new do |s|
s.executables = ['bashly']
s.homepage = 'https://github.com/dannyben/bashly'
s.license = 'MIT'
- s.required_ruby_version = ">= 2.3.0"
+ s.required_ruby_version = ">= 2.7.0"
s.add_runtime_dependency 'colsole', '~> 0.6'
+ s.add_runtime_dependency 'completely', '~> 0.1', '>= 0.1.2'
s.add_runtime_dependency 'mister_bin', '~> 0.7'
s.add_runtime_dependency 'requires', '~> 0.1'
end
diff --git a/examples/README.md b/examples/README.md
index 5bff5cbc..ad50688b 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -42,3 +42,4 @@ Each of these examples demonstrates one aspect or feature of bashly.
- [config-ini](config-ini#readme) - using the config (INI) functions
- [colors](colors#readme) - using the color print feature
- [yaml](yaml#readme) - using the YAML reading functions
+- [completions](completions#readme) - adding bash completion functionality
diff --git a/examples/completions/README.md b/examples/completions/README.md
new file mode 100644
index 00000000..c5df165d
--- /dev/null
+++ b/examples/completions/README.md
@@ -0,0 +1,164 @@
+# Bash Completions Example
+
+Demonstrates how to build a script that supports bash completions.
+
+This example was generated with:
+
+ $ bashly init
+ $ bashly add comp function
+ $ bashly generate
+
+-----
+
+## `bashly.yml`
+
+```yaml
+name: cli
+help: Sample application with bash completions
+version: 0.1.0
+
+commands:
+- name: completions
+ help: |-
+ Generate bash completions
+ Usage: eval "\$(cli completions)"
+
+- name: download
+ short: d
+ help: Download a file
+ completions: [file]
+
+ args:
+ - name: source
+ required: true
+ help: URL to download from
+ - name: target
+ help: "Target filename (default: same as source)"
+
+ flags:
+ - long: --force
+ short: -f
+ help: Overwrite existing files
+
+ examples:
+ - cli download example.com
+ - cli download example.com ./output -f
+
+ environment_variables:
+ - name: default_target_location
+ help: Set the default location to download to
+
+- name: upload
+ short: u
+ help: Upload a file
+ completions: [directory, user]
+ args:
+ - name: source
+ required: true
+ help: File to upload
+
+ flags:
+ - long: --user
+ short: -u
+ arg: user
+ help: Username to use for logging in
+ required: true
+ - long: --password
+ short: -p
+ arg: password
+ help: Password to use for logging in
+```
+
+## Generated script output
+
+### `$ ./cli`
+
+```shell
+cli - Sample application with bash completions
+
+Usage:
+ cli [command]
+ cli [command] --help | -h
+ cli --version | -v
+
+Commands:
+ completions Generate bash completions
+ download Download a file
+ upload Upload a file
+
+
+
+```
+
+### `$ ./cli -h`
+
+```shell
+cli - Sample application with bash completions
+
+Usage:
+ cli [command]
+ cli [command] --help | -h
+ cli --version | -v
+
+Commands:
+ completions Generate bash completions
+ download Download a file
+ upload Upload a file
+
+Options:
+ --help, -h
+ Show this help
+
+ --version, -v
+ Show version number
+
+
+
+```
+
+### `$ ./cli completions -h`
+
+```shell
+cli completions
+
+ Generate bash completions
+ Usage: eval "$(cli completions)"
+
+Usage:
+ cli completions
+ cli completions --help | -h
+
+Options:
+ --help, -h
+ Show this help
+
+
+
+```
+
+### `$ ./cli completions`
+
+```shell
+#!/usr/bin/env bash
+
+# This bash completions script was generated by
+# completely (https://github.com/dannyben/completely)
+# Modifying it manually is not recommended
+_cli_completions() {
+ local cur=${COMP_WORDS[COMP_CWORD]}
+
+ case "$COMP_LINE" in
+ 'cli completions'*) COMPREPLY=($(compgen -W "--help -h" -- "$cur")) ;;
+ 'cli download'*) COMPREPLY=($(compgen -A file -W "--force --help -f -h" -- "$cur")) ;;
+ 'cli upload'*) COMPREPLY=($(compgen -A directory -A user -W "--help --password --user -h -p -u" -- "$cur")) ;;
+ 'cli'*) COMPREPLY=($(compgen -W "--help --version -h -v completions download upload" -- "$cur")) ;;
+ esac
+}
+
+complete -F _cli_completions cli
+
+
+```
+
+
+
diff --git a/examples/completions/cli b/examples/completions/cli
new file mode 100644
index 00000000..78d7838a
--- /dev/null
+++ b/examples/completions/cli
@@ -0,0 +1,572 @@
+#!/usr/bin/env bash
+# This script was generated by bashly (https://github.com/DannyBen/bashly)
+# Modifying it manually is not recommended
+
+# :command.version_command
+version_command() {
+ echo "$version"
+}
+
+# :command.usage
+cli_usage() {
+ if [[ -n $long_usage ]]; then
+ printf "cli - Sample application with bash completions\n"
+ echo
+ else
+ printf "cli - Sample application with bash completions\n"
+ echo
+ fi
+
+ printf "Usage:\n"
+ printf " cli [command]\n"
+ printf " cli [command] --help | -h\n"
+ printf " cli --version | -v\n"
+ echo
+ # :command.usage_commands
+ printf "Commands:\n"
+ echo " completions Generate bash completions"
+ echo " download Download a file"
+ echo " upload Upload a file"
+ echo
+
+ if [[ -n $long_usage ]]; then
+ printf "Options:\n"
+ # :command.usage_fixed_flags
+ echo " --help, -h"
+ printf " Show this help\n"
+ echo
+ echo " --version, -v"
+ printf " Show version number\n"
+ echo
+
+ fi
+}
+
+# :command.usage
+cli_completions_usage() {
+ if [[ -n $long_usage ]]; then
+ printf "cli completions\n"
+ echo
+ printf " Generate bash completions\n Usage: eval \"\$(cli completions)\"\n"
+ echo
+ else
+ printf "cli completions - Generate bash completions\n"
+ echo
+ fi
+
+ printf "Usage:\n"
+ printf " cli completions\n"
+ printf " cli completions --help | -h\n"
+ echo
+
+ if [[ -n $long_usage ]]; then
+ printf "Options:\n"
+ # :command.usage_fixed_flags
+ echo " --help, -h"
+ printf " Show this help\n"
+ echo
+
+ fi
+}
+
+# :command.usage
+cli_download_usage() {
+ if [[ -n $long_usage ]]; then
+ printf "cli download - Download a file\n"
+ echo
+ else
+ printf "cli download - Download a file\n"
+ echo
+ fi
+
+ printf "Shortcut: d\n"
+ echo
+
+ printf "Usage:\n"
+ printf " cli download SOURCE [TARGET] [options]\n"
+ printf " cli download --help | -h\n"
+ echo
+
+ if [[ -n $long_usage ]]; then
+ printf "Options:\n"
+ # :command.usage_fixed_flags
+ echo " --help, -h"
+ printf " Show this help\n"
+ echo
+ # :command.usage_flags
+ # :flag.usage
+ echo " --force, -f"
+ printf " Overwrite existing files\n"
+ echo
+ # :command.usage_args
+ printf "Arguments:\n"
+
+ # :argument.usage
+ echo " SOURCE"
+ printf " URL to download from\n"
+ echo
+
+ # :argument.usage
+ echo " TARGET"
+ printf " Target filename (default: same as source)\n"
+ echo
+ # :command.usage_environment_variables
+ printf "Environment Variables:\n"
+
+ # :environment_variable.usage
+ echo " DEFAULT_TARGET_LOCATION"
+ printf " Set the default location to download to\n"
+ echo
+ # :command.usage_examples
+ printf "Examples:\n"
+
+ printf " cli download example.com\n"
+ printf " cli download example.com ./output -f\n"
+ echo
+
+ fi
+}
+
+# :command.usage
+cli_upload_usage() {
+ if [[ -n $long_usage ]]; then
+ printf "cli upload - Upload a file\n"
+ echo
+ else
+ printf "cli upload - Upload a file\n"
+ echo
+ fi
+
+ printf "Shortcut: u\n"
+ echo
+
+ printf "Usage:\n"
+ printf " cli upload SOURCE [options]\n"
+ printf " cli upload --help | -h\n"
+ echo
+
+ if [[ -n $long_usage ]]; then
+ printf "Options:\n"
+ # :command.usage_fixed_flags
+ echo " --help, -h"
+ printf " Show this help\n"
+ echo
+ # :command.usage_flags
+ # :flag.usage
+ echo " --user, -u USER (required)"
+ printf " Username to use for logging in\n"
+ echo
+
+ # :flag.usage
+ echo " --password, -p PASSWORD"
+ printf " Password to use for logging in\n"
+ echo
+ # :command.usage_args
+ printf "Arguments:\n"
+
+ # :argument.usage
+ echo " SOURCE"
+ printf " File to upload\n"
+ echo
+
+ fi
+}
+
+# :command.inspect_args
+inspect_args() {
+ readarray -t sorted_keys < <(printf '%s\n' "${!args[@]}" | sort)
+ if (( ${#args[@]} )); then
+ echo args:
+ for k in "${sorted_keys[@]}"; do echo "- \${args[$k]} = ${args[$k]}"; done
+ else
+ echo args: none
+ fi
+
+ if (( ${#other_args[@]} )); then
+ echo
+ echo other_args:
+ echo "- \${other_args[*]} = ${other_args[*]}"
+ for i in "${!other_args[@]}"; do
+ echo "- \${other_args[$i]} = ${other_args[$i]}"
+ done
+ fi
+}
+
+# :command.user_lib
+# :src/lib/send_completions.sh
+send_completions() {
+ echo $'#!/usr/bin/env bash'
+ echo $''
+ echo $'# This bash completions script was generated by'
+ echo $'# completely (https://github.com/dannyben/completely)'
+ echo $'# Modifying it manually is not recommended'
+ echo $'_cli_completions() {'
+ echo $' local cur=${COMP_WORDS[COMP_CWORD]}'
+ echo $''
+ echo $' case "$COMP_LINE" in'
+ echo $' \'cli completions\'*) COMPREPLY=($(compgen -W "--help -h" -- "$cur")) ;;'
+ echo $' \'cli download\'*) COMPREPLY=($(compgen -A file -W "--force --help -f -h" -- "$cur")) ;;'
+ echo $' \'cli upload\'*) COMPREPLY=($(compgen -A directory -A user -W "--help --password --user -h -p -u" -- "$cur")) ;;'
+ echo $' \'cli\'*) COMPREPLY=($(compgen -W "--help --version -h -v completions download upload" -- "$cur")) ;;'
+ echo $' esac'
+ echo $'}'
+ echo $''
+ echo $'complete -F _cli_completions cli'
+}
+
+# :command.command_functions
+# :command.function
+cli_completions_command() {
+ # :src/completions_command.sh
+ # Call the `send_completions` function which was added by running:
+ #
+ # $ bashly add comp function
+ #
+ # Users can now enable bash completion for this script by running:
+ #
+ # $ eval "$(cli completions)"
+ #
+ send_completions
+}
+
+# :command.function
+cli_download_command() {
+ # :src/download_command.sh
+ echo "# this file is located in 'src/download_command.sh'"
+ echo "# code for 'cli download' goes here"
+ echo "# you can edit it freely and regenerate (it will not be overwritten)"
+ inspect_args
+}
+
+# :command.function
+cli_upload_command() {
+ # :src/upload_command.sh
+ echo "# this file is located in 'src/upload_command.sh'"
+ echo "# code for 'cli upload' goes here"
+ echo "# you can edit it freely and regenerate (it will not be overwritten)"
+ inspect_args
+}
+
+# :command.parse_requirements
+parse_requirements() {
+ # :command.fixed_flag_filter
+ case "$1" in
+ --version | -v )
+ version_command
+ exit
+ ;;
+
+ --help | -h )
+ long_usage=yes
+ cli_usage
+ exit 1
+ ;;
+
+ esac
+ # :command.environment_variables_filter
+ # :command.dependencies_filter
+ # :command.command_filter
+ action=$1
+
+ case $action in
+ -* )
+ ;;
+
+ completions )
+ action="completions"
+ shift
+ cli_completions_parse_requirements "$@"
+ shift $#
+ ;;
+
+ download | d )
+ action="download"
+ shift
+ cli_download_parse_requirements "$@"
+ shift $#
+ ;;
+
+ upload | u )
+ action="upload"
+ shift
+ cli_upload_parse_requirements "$@"
+ shift $#
+ ;;
+
+ # :command.command_fallback
+ * )
+ cli_usage
+ exit 1
+ ;;
+
+ esac
+ # :command.required_args_filter
+ # :command.required_flags_filter
+ # :command.parse_requirements_while
+ while [[ $# -gt 0 ]]; do
+ key="$1"
+ case "$key" in
+
+ -* )
+ printf "invalid option: %s\n" "$key"
+ exit 1
+ ;;
+
+ * )
+ # :command.parse_requirements_case
+ printf "invalid argument: %s\n" "$key"
+ exit 1
+ ;;
+
+ esac
+ done
+ # :command.default_assignments
+ # :command.whitelist_filter
+}
+
+# :command.parse_requirements
+cli_completions_parse_requirements() {
+ # :command.fixed_flag_filter
+ case "$1" in
+ --version | -v )
+ version_command
+ exit
+ ;;
+
+ --help | -h )
+ long_usage=yes
+ cli_completions_usage
+ exit 1
+ ;;
+
+ esac
+ # :command.environment_variables_filter
+ # :command.dependencies_filter
+ # :command.command_filter
+ action="completions"
+ # :command.required_args_filter
+ # :command.required_flags_filter
+ # :command.parse_requirements_while
+ while [[ $# -gt 0 ]]; do
+ key="$1"
+ case "$key" in
+
+ -* )
+ printf "invalid option: %s\n" "$key"
+ exit 1
+ ;;
+
+ * )
+ # :command.parse_requirements_case
+ printf "invalid argument: %s\n" "$key"
+ exit 1
+ ;;
+
+ esac
+ done
+ # :command.default_assignments
+ # :command.whitelist_filter
+}
+
+# :command.parse_requirements
+cli_download_parse_requirements() {
+ # :command.fixed_flag_filter
+ case "$1" in
+ --version | -v )
+ version_command
+ exit
+ ;;
+
+ --help | -h )
+ long_usage=yes
+ cli_download_usage
+ exit 1
+ ;;
+
+ esac
+ # :command.environment_variables_filter
+ # :command.dependencies_filter
+ # :command.command_filter
+ action="download"
+ # :command.required_args_filter
+ if [[ $1 && $1 != -* ]]; then
+ args[source]=$1
+ shift
+ else
+ printf "missing required argument: SOURCE\nusage: cli download SOURCE [TARGET] [options]\n"
+ exit 1
+ fi
+ # :command.required_flags_filter
+ # :command.parse_requirements_while
+ while [[ $# -gt 0 ]]; do
+ key="$1"
+ case "$key" in
+ # :flag.case
+ --force | -f )
+ args[--force]=1
+ shift
+ ;;
+
+
+ -* )
+ printf "invalid option: %s\n" "$key"
+ exit 1
+ ;;
+
+ * )
+ # :command.parse_requirements_case
+ if [[ ! ${args[source]} ]]; then
+ args[source]=$1
+ shift
+ elif [[ ! ${args[target]} ]]; then
+ args[target]=$1
+ shift
+ else
+ printf "invalid argument: %s\n" "$key"
+ exit 1
+ fi
+ ;;
+
+ esac
+ done
+ # :command.default_assignments
+ # :command.whitelist_filter
+}
+
+# :command.parse_requirements
+cli_upload_parse_requirements() {
+ # :command.fixed_flag_filter
+ case "$1" in
+ --version | -v )
+ version_command
+ exit
+ ;;
+
+ --help | -h )
+ long_usage=yes
+ cli_upload_usage
+ exit 1
+ ;;
+
+ esac
+ # :command.environment_variables_filter
+ # :command.dependencies_filter
+ # :command.command_filter
+ action="upload"
+ # :command.required_args_filter
+ if [[ $1 && $1 != -* ]]; then
+ args[source]=$1
+ shift
+ else
+ printf "missing required argument: SOURCE\nusage: cli upload SOURCE [options]\n"
+ exit 1
+ fi
+ # :command.required_flags_filter
+ argstring="$*"
+ if [[ "$argstring" != *--user* && "$argstring" != *-u* ]]; then
+ printf "missing required flag: --user, -u USER\n"
+ exit 1
+ fi
+ # :command.parse_requirements_while
+ while [[ $# -gt 0 ]]; do
+ key="$1"
+ case "$key" in
+ # :flag.case
+ --user | -u )
+ if [[ $2 ]]; then
+ args[--user]="$2"
+ shift
+ shift
+ else
+ printf "%s\n" "--user requires an argument: --user, -u USER"
+ exit 1
+ fi
+ ;;
+
+ # :flag.case
+ --password | -p )
+ if [[ $2 ]]; then
+ args[--password]="$2"
+ shift
+ shift
+ else
+ printf "%s\n" "--password requires an argument: --password, -p PASSWORD"
+ exit 1
+ fi
+ ;;
+
+
+ -* )
+ printf "invalid option: %s\n" "$key"
+ exit 1
+ ;;
+
+ * )
+ # :command.parse_requirements_case
+ if [[ ! ${args[source]} ]]; then
+ args[source]=$1
+ shift
+ else
+ printf "invalid argument: %s\n" "$key"
+ exit 1
+ fi
+ ;;
+
+ esac
+ done
+ # :command.default_assignments
+ # :command.whitelist_filter
+}
+
+# :command.initialize
+initialize() {
+ version="0.1.0"
+ long_usage=''
+ set -e
+
+ # :src/initialize.sh
+ # Code here runs inside the initialize() function
+ # Use it for anything that you need to run before any other function, like
+ # setting environment vairables:
+ # CONFIG_FILE=settings.ini
+ #
+ # Feel free to empty (but not delete) this file.
+}
+
+# :command.run
+run() {
+ declare -A args
+ declare -a other_args
+ parse_requirements "$@"
+
+ if [[ $action == "completions" ]]; then
+ if [[ ${args[--help]} ]]; then
+ long_usage=yes
+ cli_completions_usage
+ else
+ cli_completions_command
+ fi
+
+ elif [[ $action == "download" ]]; then
+ if [[ ${args[--help]} ]]; then
+ long_usage=yes
+ cli_download_usage
+ else
+ cli_download_command
+ fi
+
+ elif [[ $action == "upload" ]]; then
+ if [[ ${args[--help]} ]]; then
+ long_usage=yes
+ cli_upload_usage
+ else
+ cli_upload_command
+ fi
+
+ elif [[ $action == "root" ]]; then
+ root_command
+ fi
+}
+
+initialize
+run "$@"
diff --git a/examples/completions/src/bashly.yml b/examples/completions/src/bashly.yml
new file mode 100644
index 00000000..02c1f553
--- /dev/null
+++ b/examples/completions/src/bashly.yml
@@ -0,0 +1,54 @@
+name: cli
+help: Sample application with bash completions
+version: 0.1.0
+
+commands:
+- name: completions
+ help: |-
+ Generate bash completions
+ Usage: eval "\$(cli completions)"
+
+- name: download
+ short: d
+ help: Download a file
+ completions: [file]
+
+ args:
+ - name: source
+ required: true
+ help: URL to download from
+ - name: target
+ help: "Target filename (default: same as source)"
+
+ flags:
+ - long: --force
+ short: -f
+ help: Overwrite existing files
+
+ examples:
+ - cli download example.com
+ - cli download example.com ./output -f
+
+ environment_variables:
+ - name: default_target_location
+ help: Set the default location to download to
+
+- name: upload
+ short: u
+ help: Upload a file
+ completions: [directory, user]
+ args:
+ - name: source
+ required: true
+ help: File to upload
+
+ flags:
+ - long: --user
+ short: -u
+ arg: user
+ help: Username to use for logging in
+ required: true
+ - long: --password
+ short: -p
+ arg: password
+ help: Password to use for logging in
diff --git a/examples/completions/src/completions_command.sh b/examples/completions/src/completions_command.sh
new file mode 100644
index 00000000..79f951b4
--- /dev/null
+++ b/examples/completions/src/completions_command.sh
@@ -0,0 +1,9 @@
+# Call the `send_completions` function which was added by running:
+#
+# $ bashly add comp function
+#
+# Users can now enable bash completion for this script by running:
+#
+# $ eval "$(cli completions)"
+#
+send_completions
diff --git a/examples/completions/src/download_command.sh b/examples/completions/src/download_command.sh
new file mode 100644
index 00000000..950ed370
--- /dev/null
+++ b/examples/completions/src/download_command.sh
@@ -0,0 +1,4 @@
+echo "# this file is located in 'src/download_command.sh'"
+echo "# code for 'cli download' goes here"
+echo "# you can edit it freely and regenerate (it will not be overwritten)"
+inspect_args
diff --git a/examples/completions/src/initialize.sh b/examples/completions/src/initialize.sh
new file mode 100644
index 00000000..f2dbc52c
--- /dev/null
+++ b/examples/completions/src/initialize.sh
@@ -0,0 +1,6 @@
+# Code here runs inside the initialize() function
+# Use it for anything that you need to run before any other function, like
+# setting environment vairables:
+# CONFIG_FILE=settings.ini
+#
+# Feel free to empty (but not delete) this file.
diff --git a/examples/completions/src/lib/send_completions.sh b/examples/completions/src/lib/send_completions.sh
new file mode 100644
index 00000000..a1c60b2f
--- /dev/null
+++ b/examples/completions/src/lib/send_completions.sh
@@ -0,0 +1,19 @@
+send_completions() {
+ echo $'#!/usr/bin/env bash'
+ echo $''
+ echo $'# This bash completions script was generated by'
+ echo $'# completely (https://github.com/dannyben/completely)'
+ echo $'# Modifying it manually is not recommended'
+ echo $'_cli_completions() {'
+ echo $' local cur=${COMP_WORDS[COMP_CWORD]}'
+ echo $''
+ echo $' case "$COMP_LINE" in'
+ echo $' \'cli completions\'*) COMPREPLY=($(compgen -W "--help -h" -- "$cur")) ;;'
+ echo $' \'cli download\'*) COMPREPLY=($(compgen -A file -W "--force --help -f -h" -- "$cur")) ;;'
+ echo $' \'cli upload\'*) COMPREPLY=($(compgen -A directory -A user -W "--help --password --user -h -p -u" -- "$cur")) ;;'
+ echo $' \'cli\'*) COMPREPLY=($(compgen -W "--help --version -h -v completions download upload" -- "$cur")) ;;'
+ echo $' esac'
+ echo $'}'
+ echo $''
+ echo $'complete -F _cli_completions cli'
+}
\ No newline at end of file
diff --git a/examples/completions/src/upload_command.sh b/examples/completions/src/upload_command.sh
new file mode 100644
index 00000000..755e02ff
--- /dev/null
+++ b/examples/completions/src/upload_command.sh
@@ -0,0 +1,4 @@
+echo "# this file is located in 'src/upload_command.sh'"
+echo "# code for 'cli upload' goes here"
+echo "# you can edit it freely and regenerate (it will not be overwritten)"
+inspect_args
diff --git a/examples/completions/test.sh b/examples/completions/test.sh
new file mode 100644
index 00000000..917ec9ec
--- /dev/null
+++ b/examples/completions/test.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+set -x
+
+bashly add comp function
+bashly generate
+
+./cli
+./cli -h
+./cli completions -h
+./cli completions
diff --git a/lib/bashly/commands/add.rb b/lib/bashly/commands/add.rb
index 409e8e2f..4e824854 100644
--- a/lib/bashly/commands/add.rb
+++ b/lib/bashly/commands/add.rb
@@ -8,15 +8,24 @@ class Add < Base
usage "bashly add config [--force]"
usage "bashly add colors [--force]"
usage "bashly add yaml [--force]"
+ usage "bashly add comp FORMAT [OUTPUT]"
usage "bashly add (-h|--help)"
option "-f --force", "Overwrite existing files"
+ param "FORMAT", "Output format, can be one of:\n function : generate a function file to be included in your script.\n script : generate a standalone bash completions script\n yaml : generate a yaml compatible with 'completely'"
+ param "OUTPUT", "For the 'comp function' command: Name of the generated function.\nFor the 'comp script' or 'comp yaml' commands: path to output file.\nIn all cases, this is optional and will have sensible defaults."
+
command "strings", "Copy an additional configuration file to your project, allowing you to customize all the tips and error strings."
command "lib", "Create the additional lib directory for additional user scripts. All *.sh scripts in this folder will be included in the final bash script."
command "config", "Add standard functions for handling INI files to the lib directory."
command "colors", "Add standard functions for printing colorful and formatted text to the lib directory."
command "yaml", "Add standard functions for reading YAML files to the lib directory."
+ command "comp", "Generate a bash completions script or function."
+
+ example "bashly add strings --force"
+ example "bashly add comp function"
+ example "bashly add comp script completions.bash"
environment "BASHLY_SOURCE_DIR", "The path containing the bashly configuration and source files [default: src]"
@@ -40,7 +49,25 @@ def yaml_command
safe_copy_lib "yaml.sh"
end
+ def comp_command
+ format = args['FORMAT']
+ output = args['OUTPUT']
+
+ case format
+ when "function"
+ save_comp_function output
+ when "yaml"
+ save_comp_yaml output
+ when "script"
+ save_comp_script output
+ else
+ raise Error, "Unrecognized format: #{format}"
+ end
+
+ end
+
private
+
def safe_copy_lib(libfile)
safe_copy asset("templates/lib/#{libfile}"), "#{Settings.source_dir}/lib/#{libfile}"
end
@@ -63,6 +90,61 @@ def deep_copy(source, target)
FileUtils.mkdir_p target_dir unless Dir.exist? target_dir
FileUtils.cp source, target
end
+
+ def config
+ @config ||= Config.new "#{Settings.source_dir}/bashly.yml"
+ end
+
+ def command
+ @command ||= Models::Command.new config
+ end
+
+ def completions
+ @completions ||= command.completion_data
+ end
+
+ def completions_script
+ @completions_script ||= command.completion_script
+ end
+
+ def completions_function
+ @completions_function ||= command.completion_function
+ end
+
+ def save_comp_yaml(filename = nil)
+ filename ||= "#{Settings.target_dir}/completions.yaml"
+ File.write filename, completions.to_yaml
+ say "created !txtgrn!#{filename}"
+ say ""
+ say "This file can be converted to a completions script using the !txtgrn!completely!txtrst! gem."
+ end
+
+ def save_comp_script(filename = nil)
+ filename ||= "#{Settings.target_dir}/completions.bash"
+ File.write filename, completions_script
+ say "created !txtgrn!#{filename}"
+ say ""
+ say "In order to enable completions, run:"
+ say ""
+ say " !txtpur!$ source #{filename}"
+ end
+
+ def save_comp_function(name = nil)
+ name ||= "send_completions"
+ target_dir = "#{Settings.source_dir}/lib"
+ filename = "#{target_dir}/#{name}.sh"
+
+ FileUtils.mkdir_p target_dir unless Dir.exist? target_dir
+ File.write filename, completions_function
+
+ say "created !txtgrn!#{filename}"
+ say ""
+ say "In order to use it in your script, create a command or a flag (for example: !txtgrn!#{command.name} completions!txtrst! or !txtgrn!#{command.name} --completions!txtrst!) that calls the !txtgrn!#{name}!txtrst! function."
+ say "Your users can then run something like this to enable completions:"
+ say ""
+ say " !txtpur!$ eval \"$(#{command.name} completions)\""
+ end
+
end
end
end
diff --git a/lib/bashly/concerns/completions.rb b/lib/bashly/concerns/completions.rb
new file mode 100644
index 00000000..5a783449
--- /dev/null
+++ b/lib/bashly/concerns/completions.rb
@@ -0,0 +1,50 @@
+require 'completely'
+
+module Bashly
+ # This is a `Command` concern responsible for providing bash completion data
+ module Completions
+ def completion_data(with_version: true)
+ result = { full_name => completion_words(with_version: with_version) }
+
+ commands.each do |command|
+ result.merge! command.completion_data(with_version: false)
+ end
+
+ result
+ end
+
+ def completion_script
+ completion_generator.script
+ end
+
+ def completion_function(name = nil)
+ completion_generator.wrapper_function(name)
+ end
+
+ private
+
+ def completion_generator
+ Completely::Completions.new(completion_data)
+ end
+
+ def completion_flag_names
+ flags.map(&:name) + flags.map(&:short)
+ end
+
+ def completion_actions
+ completions ? completions.map { |c| "<#{c}>" } : []
+ end
+
+ def completion_words(with_version: false)
+ trivial_flags = %w[--help -h]
+ trivial_flags += %w[--version -v] if with_version
+ all = (
+ command_names + trivial_flags +
+ completion_flag_names + completion_actions
+ )
+
+ all.compact.uniq.sort
+ end
+
+ end
+end
diff --git a/lib/bashly/concerns/renderable.rb b/lib/bashly/concerns/renderable.rb
index 11cb67ce..d6ade503 100644
--- a/lib/bashly/concerns/renderable.rb
+++ b/lib/bashly/concerns/renderable.rb
@@ -4,10 +4,7 @@ module Bashly
module Renderable
def render(view)
template = File.read view_path(view)
- # TODO: This new format is only supported in Ruby >= 2.6
- # So for now, we keep the old deprecated syntax
- # ERB.new(template, trim_mode: '%-').result(binding)
- ERB.new(template, nil, '%-').result(binding)
+ ERB.new(template, trim_mode: '%-').result(binding)
end
def strings
diff --git a/lib/bashly/models/base.rb b/lib/bashly/models/base.rb
index 75714f3e..97b95222 100644
--- a/lib/bashly/models/base.rb
+++ b/lib/bashly/models/base.rb
@@ -9,6 +9,7 @@ class Base
allowed
arg
catch_all
+ completions
default
dependencies
description
diff --git a/lib/bashly/models/command.rb b/lib/bashly/models/command.rb
index e49dd427..f0795b7b 100644
--- a/lib/bashly/models/command.rb
+++ b/lib/bashly/models/command.rb
@@ -1,6 +1,8 @@
module Bashly
module Models
class Command < Base
+ include Completions
+
# Returns the name to be used as an action.
# - If it is the root command, the action is "root"
# - Else, it is all the parents, except the first tone (root) joined
diff --git a/spec/approvals/cli/add/comp-error b/spec/approvals/cli/add/comp-error
new file mode 100644
index 00000000..a9ab2be3
--- /dev/null
+++ b/spec/approvals/cli/add/comp-error
@@ -0,0 +1 @@
+#
\ No newline at end of file
diff --git a/spec/approvals/cli/add/comp-function b/spec/approvals/cli/add/comp-function
new file mode 100644
index 00000000..7e034b8d
--- /dev/null
+++ b/spec/approvals/cli/add/comp-function
@@ -0,0 +1,6 @@
+created spec/tmp/src/lib/send_completions.sh
+
+In order to use it in your script, create a command or a flag (for example: cli completions or cli --completions) that calls the send_completions function.
+Your users can then run something like this to enable completions:
+
+ $ eval "$(cli completions)"
diff --git a/spec/approvals/cli/add/comp-function-file b/spec/approvals/cli/add/comp-function-file
new file mode 100644
index 00000000..4323157e
--- /dev/null
+++ b/spec/approvals/cli/add/comp-function-file
@@ -0,0 +1,18 @@
+send_completions() {
+ echo $'#!/usr/bin/env bash'
+ echo $''
+ echo $'# This bash completions script was generated by'
+ echo $'# completely (https://github.com/dannyben/completely)'
+ echo $'# Modifying it manually is not recommended'
+ echo $'_cli_completions() {'
+ echo $' local cur=${COMP_WORDS[COMP_CWORD]}'
+ echo $''
+ echo $' case "$COMP_LINE" in'
+ echo $' \'cli download\'*) COMPREPLY=($(compgen -W "--force --help -f -h" -- "$cur")) ;;'
+ echo $' \'cli upload\'*) COMPREPLY=($(compgen -W "--help --password --user -h -p -u" -- "$cur")) ;;'
+ echo $' \'cli\'*) COMPREPLY=($(compgen -W "--help --version -h -v download upload" -- "$cur")) ;;'
+ echo $' esac'
+ echo $'}'
+ echo $''
+ echo $'complete -F _cli_completions cli'
+}
\ No newline at end of file
diff --git a/spec/approvals/cli/add/comp-script b/spec/approvals/cli/add/comp-script
new file mode 100644
index 00000000..f539fd6e
--- /dev/null
+++ b/spec/approvals/cli/add/comp-script
@@ -0,0 +1,5 @@
+created spec/tmp/completions.bash
+
+In order to enable completions, run:
+
+ $ source spec/tmp/completions.bash
diff --git a/spec/approvals/cli/add/comp-script-file b/spec/approvals/cli/add/comp-script-file
new file mode 100644
index 00000000..92f29aaa
--- /dev/null
+++ b/spec/approvals/cli/add/comp-script-file
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+# This bash completions script was generated by
+# completely (https://github.com/dannyben/completely)
+# Modifying it manually is not recommended
+_cli_completions() {
+ local cur=${COMP_WORDS[COMP_CWORD]}
+
+ case "$COMP_LINE" in
+ 'cli download'*) COMPREPLY=($(compgen -W "--force --help -f -h" -- "$cur")) ;;
+ 'cli upload'*) COMPREPLY=($(compgen -W "--help --password --user -h -p -u" -- "$cur")) ;;
+ 'cli'*) COMPREPLY=($(compgen -W "--help --version -h -v download upload" -- "$cur")) ;;
+ esac
+}
+
+complete -F _cli_completions cli
diff --git a/spec/approvals/cli/add/comp-yaml b/spec/approvals/cli/add/comp-yaml
new file mode 100644
index 00000000..7e8e1ef2
--- /dev/null
+++ b/spec/approvals/cli/add/comp-yaml
@@ -0,0 +1,3 @@
+created spec/tmp/completions.yaml
+
+This file can be converted to a completions script using the completely gem.
diff --git a/spec/approvals/cli/add/comp-yaml-file b/spec/approvals/cli/add/comp-yaml-file
new file mode 100644
index 00000000..bda8fbf8
--- /dev/null
+++ b/spec/approvals/cli/add/comp-yaml-file
@@ -0,0 +1,20 @@
+---
+cli:
+- "--help"
+- "--version"
+- "-h"
+- "-v"
+- download
+- upload
+cli download:
+- "--force"
+- "--help"
+- "-f"
+- "-h"
+cli upload:
+- "--help"
+- "--password"
+- "--user"
+- "-h"
+- "-p"
+- "-u"
diff --git a/spec/approvals/cli/add/help b/spec/approvals/cli/add/help
index 559bcf58..a4c9ab40 100644
--- a/spec/approvals/cli/add/help
+++ b/spec/approvals/cli/add/help
@@ -6,6 +6,7 @@ Usage:
bashly add config [--force]
bashly add colors [--force]
bashly add yaml [--force]
+ bashly add comp FORMAT [OUTPUT]
bashly add (-h|--help)
Commands:
@@ -27,6 +28,9 @@ Commands:
yaml
Add standard functions for reading YAML files to the lib directory.
+ comp
+ Generate a bash completions script or function.
+
Options:
-f --force
Overwrite existing files
@@ -34,6 +38,23 @@ Options:
-h --help
Show this help
+Parameters:
+ FORMAT
+ Output format, can be one of:
+ function : generate a function file to be included in your script.
+ script : generate a standalone bash completions script
+ yaml : generate a yaml compatible with 'completely'
+
+ OUTPUT
+ For the 'comp function' command: Name of the generated function.
+ For the 'comp script' or 'comp yaml' commands: path to output file.
+ In all cases, this is optional and will have sensible defaults.
+
Environment Variables:
BASHLY_SOURCE_DIR
The path containing the bashly configuration and source files [default: src]
+
+Examples:
+ bashly add strings --force
+ bashly add comp function
+ bashly add comp script completions.bash
diff --git a/spec/approvals/cli/add/init b/spec/approvals/cli/add/init
new file mode 100644
index 00000000..82a02c6a
--- /dev/null
+++ b/spec/approvals/cli/add/init
@@ -0,0 +1,2 @@
+created spec/tmp/src/bashly.yml
+run bashly generate to create the bash script
diff --git a/spec/approvals/cli/generate/usage b/spec/approvals/cli/generate/usage
index b80fc35d..f759837f 100644
--- a/spec/approvals/cli/generate/usage
+++ b/spec/approvals/cli/generate/usage
@@ -4,4 +4,5 @@ Usage:
bashly add config [--force]
bashly add colors [--force]
bashly add yaml [--force]
+ bashly add comp FORMAT [OUTPUT]
bashly add (-h|--help)
diff --git a/spec/approvals/completions/advanced b/spec/approvals/completions/advanced
new file mode 100644
index 00000000..8cfae073
--- /dev/null
+++ b/spec/approvals/completions/advanced
@@ -0,0 +1,30 @@
+---
+say:
+- "--help"
+- "--version"
+- "-h"
+- "-v"
+- goodbye
+- hello
+say hello:
+- "--help"
+- "-h"
+- world
+say hello world:
+- "--force"
+- "--help"
+- "--verbose"
+- "-h"
+- ""
+- ""
+say goodbye:
+- "--help"
+- "-h"
+- universe
+say goodbye universe:
+- "--color"
+- "--help"
+- "--verbose"
+- "-c"
+- "-h"
+- "-v"
diff --git a/spec/approvals/completions/function b/spec/approvals/completions/function
new file mode 100644
index 00000000..85ac0375
--- /dev/null
+++ b/spec/approvals/completions/function
@@ -0,0 +1,16 @@
+custom_name() {
+ echo $'#!/usr/bin/env bash'
+ echo $''
+ echo $'# This bash completions script was generated by'
+ echo $'# completely (https://github.com/dannyben/completely)'
+ echo $'# Modifying it manually is not recommended'
+ echo $'_get_completions() {'
+ echo $' local cur=${COMP_WORDS[COMP_CWORD]}'
+ echo $''
+ echo $' case "$COMP_LINE" in'
+ echo $' \'get\'*) COMPREPLY=($(compgen -A file -W "--force --help --verbose --version -h -v" -- "$cur")) ;;'
+ echo $' esac'
+ echo $'}'
+ echo $''
+ echo $'complete -F _get_completions get'
+}
\ No newline at end of file
diff --git a/spec/approvals/completions/script b/spec/approvals/completions/script
new file mode 100644
index 00000000..696fa91b
--- /dev/null
+++ b/spec/approvals/completions/script
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+
+# This bash completions script was generated by
+# completely (https://github.com/dannyben/completely)
+# Modifying it manually is not recommended
+_say_completions() {
+ local cur=${COMP_WORDS[COMP_CWORD]}
+
+ case "$COMP_LINE" in
+ 'say goodbye universe'*) COMPREPLY=($(compgen -W "--color --help --verbose -c -h -v" -- "$cur")) ;;
+ 'say hello world'*) COMPREPLY=($(compgen -A directory -A user -W "--force --help --verbose -h" -- "$cur")) ;;
+ 'say goodbye'*) COMPREPLY=($(compgen -W "--help -h universe" -- "$cur")) ;;
+ 'say hello'*) COMPREPLY=($(compgen -W "--help -h world" -- "$cur")) ;;
+ 'say'*) COMPREPLY=($(compgen -W "--help --version -h -v goodbye hello" -- "$cur")) ;;
+ esac
+}
+
+complete -F _say_completions say
diff --git a/spec/approvals/completions/simple b/spec/approvals/completions/simple
new file mode 100644
index 00000000..d677f376
--- /dev/null
+++ b/spec/approvals/completions/simple
@@ -0,0 +1,9 @@
+---
+get:
+- "--force"
+- "--help"
+- "--verbose"
+- "--version"
+- "-h"
+- "-v"
+- ""
diff --git a/spec/approvals/examples/completions b/spec/approvals/examples/completions
new file mode 100644
index 00000000..0b1dd43c
--- /dev/null
+++ b/spec/approvals/examples/completions
@@ -0,0 +1,80 @@
++ bashly add comp function
+created src/lib/send_completions.sh
+
+In order to use it in your script, create a command or a flag (for example: cli completions or cli --completions) that calls the send_completions function.
+Your users can then run something like this to enable completions:
+
+ $ eval "$(cli completions)"
++ bashly generate
+creating user files in src
+skipped src/initialize.sh (exists)
+skipped src/completions_command.sh (exists)
+skipped src/download_command.sh (exists)
+skipped src/upload_command.sh (exists)
+created ./cli
+run ./cli --help to test your bash script
++ ./cli
+cli - Sample application with bash completions
+
+Usage:
+ cli [command]
+ cli [command] --help | -h
+ cli --version | -v
+
+Commands:
+ completions Generate bash completions
+ download Download a file
+ upload Upload a file
+
++ ./cli -h
+cli - Sample application with bash completions
+
+Usage:
+ cli [command]
+ cli [command] --help | -h
+ cli --version | -v
+
+Commands:
+ completions Generate bash completions
+ download Download a file
+ upload Upload a file
+
+Options:
+ --help, -h
+ Show this help
+
+ --version, -v
+ Show version number
+
++ ./cli completions -h
+cli completions
+
+ Generate bash completions
+ Usage: eval "$(cli completions)"
+
+Usage:
+ cli completions
+ cli completions --help | -h
+
+Options:
+ --help, -h
+ Show this help
+
++ ./cli completions
+#!/usr/bin/env bash
+
+# This bash completions script was generated by
+# completely (https://github.com/dannyben/completely)
+# Modifying it manually is not recommended
+_cli_completions() {
+ local cur=${COMP_WORDS[COMP_CWORD]}
+
+ case "$COMP_LINE" in
+ 'cli completions'*) COMPREPLY=($(compgen -W "--help -h" -- "$cur")) ;;
+ 'cli download'*) COMPREPLY=($(compgen -A file -W "--force --help -f -h" -- "$cur")) ;;
+ 'cli upload'*) COMPREPLY=($(compgen -A directory -A user -W "--help --password --user -h -p -u" -- "$cur")) ;;
+ 'cli'*) COMPREPLY=($(compgen -W "--help --version -h -v completions download upload" -- "$cur")) ;;
+ esac
+}
+
+complete -F _cli_completions cli
diff --git a/spec/bashly/commands/add_spec.rb b/spec/bashly/commands/add_spec.rb
index 159ccc0f..10471fe8 100644
--- a/spec/bashly/commands/add_spec.rb
+++ b/spec/bashly/commands/add_spec.rb
@@ -99,4 +99,38 @@
end
end
+ context "with comp command" do
+ before do
+ reset_tmp_dir create_src: true
+ expect { subject.run %w[init] }.to output_approval('cli/add/init')
+ end
+
+ context "with yaml subcommand" do
+ it "creates completions.yaml" do
+ expect { subject.run %w[add comp yaml] }.to output_approval('cli/add/comp-yaml')
+ expect(File.read "#{target_dir}/completions.yaml").to match_approval('cli/add/comp-yaml-file')
+ end
+ end
+
+ context "with script subcommand" do
+ it "creates completions.bash" do
+ expect { subject.run %w[add comp script] }.to output_approval('cli/add/comp-script')
+ expect(File.read "#{target_dir}/completions.bash").to match_approval('cli/add/comp-script-file')
+ end
+ end
+
+ context "with function subcommand" do
+ it "creates lib/send_completions.sh" do
+ expect { subject.run %w[add comp function] }.to output_approval('cli/add/comp-function')
+ expect(File.read "#{source_dir}/lib/send_completions.sh").to match_approval('cli/add/comp-function-file')
+ end
+ end
+
+ context "with an unrecognized subcommand" do
+ it "raises an error" do
+ expect { subject.run %w[add comp no-such-format] }.to raise_approval('cli/add/comp-error')
+ end
+ end
+ end
+
end
diff --git a/spec/bashly/concerns/completions_spec.rb b/spec/bashly/concerns/completions_spec.rb
new file mode 100644
index 00000000..2f9b576d
--- /dev/null
+++ b/spec/bashly/concerns/completions_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe Models::Command do
+ let(:fixture) { :completions_simple }
+
+ subject do
+ options = load_fixture('models/commands')[fixture]
+ described_class.new options
+ end
+
+ describe '#completion_data' do
+ it "returns a data structure for completely" do
+ expect(subject.completion_data.to_yaml).to match_approval("completions/simple")
+ end
+ end
+
+ describe '#completion_function' do
+ it "returns a bash completion script wrapped in a function" do
+ expect(subject.completion_function "custom_name")
+ .to match_approval("completions/function")
+ end
+ end
+
+ context "with a more complex command" do
+ let(:fixture) { :completions_advanced }
+
+ describe '#completion_data' do
+ it "returns a data structure for completely" do
+ expect(subject.completion_data.to_yaml)
+ .to match_approval("completions/advanced")
+ end
+ end
+
+ describe '#completion_script' do
+ it "returns a bash completion script" do
+ expect(subject.completion_script)
+ .to match_approval("completions/script")
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/models/commands.yml b/spec/fixtures/models/commands.yml
index 49283544..31786e4e 100644
--- a/spec/fixtures/models/commands.yml
+++ b/spec/fixtures/models/commands.yml
@@ -127,3 +127,30 @@
catch_all:
label: additional params
help: Any additional argument or flag
+
+:completions_simple:
+ name: get
+ completions: [file]
+
+ flags:
+ - long: --force
+ - long: --verbose
+
+:completions_advanced:
+ name: say
+ commands:
+ - name: hello
+ commands:
+ - name: world
+ completions: [directory, user]
+ flags:
+ - long: --force
+ - long: --verbose
+ - name: goodbye
+ commands:
+ - name: universe
+ flags:
+ - long: --color
+ short: -c
+ - long: --verbose
+ short: -v