diff --git a/README.md b/README.md index 36d0c0fc..1858640b 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,11 @@ Bahsly is responsible for: - Generating a **single, standalone bash script**. - Generating **usage texts** and help screens, showing your tool's arguments, - flags and subcommands (works for subcommands also). + flags and commands (works for subcommands also). - Parsing the user's command line and extracting: - Optional or required **positional arguments**. - Optional or required **option flags** (with or without flag arguments). - - **Subcommands** (and sub-subcommands). + - **Commands** (and subcommands). - Standard flags (like **--help** and **--version**). - 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. @@ -67,7 +67,7 @@ $ bashly init --minimal ``` This will create a sample `src/bashly.yml` file. -You can edit this file to specify which arguments, flags and subcommands you +You can edit this file to specify which arguments, flags and commands you need in your bash script. Then, generate an initial bash script and function placeholder scripts by @@ -92,24 +92,24 @@ Examples The `bashly.yml` file can be set up to generate two types of scripts: -1. Script with subcommands (for example, like `docker` or `git`). -2. Script without subcommands (for example, like `ls`) +1. Script with commands (for example, like `docker` or `git`). +2. Script without commands (for example, like `ls`) This is detected automatically by the contents of the configuration: If it -contains a `commands` definition, it will generate a script with subcommands. +contains a `commands` definition, it will generate a script with commands. -### Sample configuraiton for a script without subcommands +### Sample configuraiton for a script without commands - Generate this script by running `bashly generate --minimal` - [See the initial sample bashly.yml file](examples/minimal/src/bashly.yml) - [See the generated bash script](examples/minimal/download) -### Sample configuraiton for a script with subcommands +### Sample configuraiton for a script with commands - Generate this script by running `bashly generate` -- [See the initial sample bashly.yml file](examples/subcommands/src/bashly.yml) -- [See the generated bash script](examples/subcommands/cli) +- [See the initial sample bashly.yml file](examples/commands/src/bashly.yml) +- [See the generated bash script](examples/commands/cli) See the [examples](examples) folder for more examples. @@ -140,13 +140,14 @@ command and subcommands (under the `commands` definition). `short` | An additional, optional pattern - usually used to denote a one letter variation of the command name. You can add `*` as a suffix, to denote a "starts with" pattern - for example `short: m*`. *Applicable only in subcommands*. `help` | The header text to display when using `--help`. This option can have multiple lines. In this case, the first line will be used as summary wherever appropriate. `version` | The string to display when using `--version`. *Applicable only in the main command*. -`default` | Setting this to `yes` on any subcommand, will make unrecognized command line arguments to be passed to this command. *Applicable only in subcommands*. +`default` | Setting this to `yes` on any command, will make unrecognized command line arguments to be passed to this command. *Applicable only in subcommands*. `examples` | Specify an array of examples to show when using `--help`. Each example can have multiple lines. `environment_variables` | Specify an array of environment variables needed by your script. -`commands` | Specify the array of subcommands. Each subcommand will have its own args and flags. Note: if `commands` is provided, you cannot specify flags or args at the same level. +`commands` | Specify the array of commands. 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 this script needs. `flags` | Specify the array of option flags this script needs. `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. ### Argument options diff --git a/Runfile b/Runfile index c7269e3a..4e4777f5 100644 --- a/Runfile +++ b/Runfile @@ -27,17 +27,18 @@ end def examples [ "examples/colors/colorly", + "examples/command-default/ftp", + "examples/command-groups/ftp", + "examples/commands/cli", "examples/config-ini/configly", "examples/custom-includes/download", "examples/custom-strings/download", - "examples/default-command/ftp", "examples/dependencies/cli", "examples/docker-like/docker", "examples/environment-variables/cli", "examples/git-like/git", "examples/minimal/download", "examples/multiline/multi", - "examples/subcommands/cli", "examples/yaml/yaml", "spec/fixtures/workspaces/short-flag/rush", ] diff --git a/examples/default-command/README.md b/examples/command-default/README.md similarity index 100% rename from examples/default-command/README.md rename to examples/command-default/README.md diff --git a/examples/default-command/ftp b/examples/command-default/ftp similarity index 100% rename from examples/default-command/ftp rename to examples/command-default/ftp diff --git a/examples/default-command/src/bashly.yml b/examples/command-default/src/bashly.yml similarity index 100% rename from examples/default-command/src/bashly.yml rename to examples/command-default/src/bashly.yml diff --git a/examples/default-command/src/download_command.sh b/examples/command-default/src/download_command.sh similarity index 100% rename from examples/default-command/src/download_command.sh rename to examples/command-default/src/download_command.sh diff --git a/examples/default-command/src/initialize.sh b/examples/command-default/src/initialize.sh similarity index 100% rename from examples/default-command/src/initialize.sh rename to examples/command-default/src/initialize.sh diff --git a/examples/default-command/src/upload_command.sh b/examples/command-default/src/upload_command.sh similarity index 100% rename from examples/default-command/src/upload_command.sh rename to examples/command-default/src/upload_command.sh diff --git a/examples/default-command/test.sh b/examples/command-default/test.sh similarity index 100% rename from examples/default-command/test.sh rename to examples/command-default/test.sh diff --git a/examples/command-groups/README.md b/examples/command-groups/README.md new file mode 100644 index 00000000..838f677e --- /dev/null +++ b/examples/command-groups/README.md @@ -0,0 +1,7 @@ +Command Groups Example +================================================== + +This example was generated with: + + $ bashly init + $ bashly generate diff --git a/examples/command-groups/ftp b/examples/command-groups/ftp new file mode 100644 index 00000000..a1e285d9 --- /dev/null +++ b/examples/command-groups/ftp @@ -0,0 +1,542 @@ +#!/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 +ftp_usage() { + if [[ -n $long_usage ]]; then + printf "ftp - Sample application with command grouping\n" + echo + else + printf "ftp - Sample application with command grouping\n" + echo + fi + + printf "Usage:\n" + printf " ftp [command] [options]\n" + printf " ftp [command] --help | -h\n" + printf " ftp --version\n" + echo + # :command.usage_commands + printf "File Commands:\n" + echo " download Download a file" + echo " upload Upload a file" + printf "\nLogin Commands:\n" + echo " login Write login credentials to the config file" + echo " logout Delete login credentials to the config 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" + printf " Show version number\n" + echo + + fi +} + +# :command.usage +ftp_download_usage() { + if [[ -n $long_usage ]]; then + printf "ftp download - Download a file\n" + echo + else + printf "ftp download - Download a file\n" + echo + fi + + printf "Usage:\n" + printf " ftp download FILE [options]\n" + printf " ftp 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_args + printf "Arguments:\n" + + # :argument.usage + echo " FILE" + printf " File to download\n" + echo + + fi +} + +# :command.usage +ftp_upload_usage() { + if [[ -n $long_usage ]]; then + printf "ftp upload - Upload a file\n" + echo + else + printf "ftp upload - Upload a file\n" + echo + fi + + printf "Usage:\n" + printf " ftp upload FILE [options]\n" + printf " ftp 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_args + printf "Arguments:\n" + + # :argument.usage + echo " FILE" + printf " File to upload\n" + echo + + fi +} + +# :command.usage +ftp_login_usage() { + if [[ -n $long_usage ]]; then + printf "ftp login - Write login credentials to the config file\n" + echo + else + printf "ftp login - Write login credentials to the config file\n" + echo + fi + + printf "Usage:\n" + printf " ftp login [options]\n" + printf " ftp login --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 +ftp_logout_usage() { + if [[ -n $long_usage ]]; then + printf "ftp logout - Delete login credentials to the config file\n" + echo + else + printf "ftp logout - Delete login credentials to the config file\n" + echo + fi + + printf "Usage:\n" + printf " ftp logout [options]\n" + printf " ftp logout --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.inspect_args +inspect_args() { + echo args: + for k in "${!args[@]}"; do echo "- \${args[$k]} = ${args[$k]}"; done +} + +# :command.command_functions +# :command.function +ftp_download_command() { + # :src/download_command.sh + echo "# this file is located in 'src/download_command.sh'" + echo "# code for 'ftp download' goes here" + echo "# you can edit it freely and regenerate (it will not be overwritten)" + inspect_args +} + +# :command.function +ftp_upload_command() { + # :src/upload_command.sh + echo "# this file is located in 'src/upload_command.sh'" + echo "# code for 'ftp upload' goes here" + echo "# you can edit it freely and regenerate (it will not be overwritten)" + inspect_args +} + +# :command.function +ftp_login_command() { + # :src/login_command.sh + echo "# this file is located in 'src/login_command.sh'" + echo "# code for 'ftp login' goes here" + echo "# you can edit it freely and regenerate (it will not be overwritten)" + inspect_args +} + +# :command.function +ftp_logout_command() { + # :src/logout_command.sh + echo "# this file is located in 'src/logout_command.sh'" + echo "# code for 'ftp logout' 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 ) + version_command + exit 1 + ;; + + --help | -h ) + long_usage=yes + ftp_usage + exit 1 + ;; + + esac + # :command.environment_variables_filter + # :command.dependencies_filter + # :command.command_filter + action=$1 + + case $action in + -* ) + ;; + + download ) + action="download" + shift + ftp_download_parse_requirements "$@" + shift $# + ;; + + upload ) + action="upload" + shift + ftp_upload_parse_requirements "$@" + shift $# + ;; + + login ) + action="login" + shift + ftp_login_parse_requirements "$@" + shift $# + ;; + + logout ) + action="logout" + shift + ftp_logout_parse_requirements "$@" + shift $# + ;; + + * ) + ftp_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.parse_requirements +ftp_download_parse_requirements() { + # :command.fixed_flag_filter + case "$1" in + --version ) + version_command + exit 1 + ;; + + --help | -h ) + long_usage=yes + ftp_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[file]=$1 + shift + else + printf "missing required argument: FILE\nusage: ftp download FILE [options]\n" + exit 1 + fi + # :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 + if [[ ! ${args[file]} ]]; then + args[file]=$1 + shift + else + printf "invalid argument: %s\n" "$key" + exit 1 + fi + ;; + + esac + done +} + +# :command.parse_requirements +ftp_upload_parse_requirements() { + # :command.fixed_flag_filter + case "$1" in + --version ) + version_command + exit 1 + ;; + + --help | -h ) + long_usage=yes + ftp_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[file]=$1 + shift + else + printf "missing required argument: FILE\nusage: ftp upload FILE [options]\n" + exit 1 + fi + # :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 + if [[ ! ${args[file]} ]]; then + args[file]=$1 + shift + else + printf "invalid argument: %s\n" "$key" + exit 1 + fi + ;; + + esac + done +} + +# :command.parse_requirements +ftp_login_parse_requirements() { + # :command.fixed_flag_filter + case "$1" in + --version ) + version_command + exit 1 + ;; + + --help | -h ) + long_usage=yes + ftp_login_usage + exit 1 + ;; + + esac + # :command.environment_variables_filter + # :command.dependencies_filter + # :command.command_filter + action="login" + # :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.parse_requirements +ftp_logout_parse_requirements() { + # :command.fixed_flag_filter + case "$1" in + --version ) + version_command + exit 1 + ;; + + --help | -h ) + long_usage=yes + ftp_logout_usage + exit 1 + ;; + + esac + # :command.environment_variables_filter + # :command.dependencies_filter + # :command.command_filter + action="logout" + # :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.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 + parse_requirements "$@" + + if [[ $action == "download" ]]; then + if [[ ${args[--help]} ]]; then + long_usage=yes + ftp_download_usage + else + ftp_download_command + fi + + elif [[ $action == "upload" ]]; then + if [[ ${args[--help]} ]]; then + long_usage=yes + ftp_upload_usage + else + ftp_upload_command + fi + + elif [[ $action == "login" ]]; then + if [[ ${args[--help]} ]]; then + long_usage=yes + ftp_login_usage + else + ftp_login_command + fi + + elif [[ $action == "logout" ]]; then + if [[ ${args[--help]} ]]; then + long_usage=yes + ftp_logout_usage + else + ftp_logout_command + fi + + elif [[ ${args[--version]} ]]; then + version_command + elif [[ ${args[--help]} ]]; then + long_usage=yes + ftp_usage + elif [[ $action == "root" ]]; then + root_command + fi +} + +initialize +run "$@" diff --git a/examples/command-groups/src/bashly.yml b/examples/command-groups/src/bashly.yml new file mode 100644 index 00000000..8709cdd8 --- /dev/null +++ b/examples/command-groups/src/bashly.yml @@ -0,0 +1,29 @@ +name: ftp +help: Sample application with command grouping +version: 0.1.0 + +commands: +- name: download + help: Download a file + group: File + + args: + - name: file + required: true + help: File to download + +- name: upload + help: Upload a file + + args: + - name: file + required: true + help: File to upload + +- name: login + help: Write login credentials to the config file + group: Login + +- name: logout + help: Delete login credentials to the config file + diff --git a/examples/command-groups/src/download_command.sh b/examples/command-groups/src/download_command.sh new file mode 100644 index 00000000..d6f4bfd9 --- /dev/null +++ b/examples/command-groups/src/download_command.sh @@ -0,0 +1,4 @@ +echo "# this file is located in 'src/download_command.sh'" +echo "# code for 'ftp download' goes here" +echo "# you can edit it freely and regenerate (it will not be overwritten)" +inspect_args diff --git a/examples/subcommands/src/initialize.sh b/examples/command-groups/src/initialize.sh similarity index 100% rename from examples/subcommands/src/initialize.sh rename to examples/command-groups/src/initialize.sh diff --git a/examples/command-groups/src/login_command.sh b/examples/command-groups/src/login_command.sh new file mode 100644 index 00000000..618339a1 --- /dev/null +++ b/examples/command-groups/src/login_command.sh @@ -0,0 +1,4 @@ +echo "# this file is located in 'src/login_command.sh'" +echo "# code for 'ftp login' goes here" +echo "# you can edit it freely and regenerate (it will not be overwritten)" +inspect_args diff --git a/examples/command-groups/src/logout_command.sh b/examples/command-groups/src/logout_command.sh new file mode 100644 index 00000000..835c7b4f --- /dev/null +++ b/examples/command-groups/src/logout_command.sh @@ -0,0 +1,4 @@ +echo "# this file is located in 'src/logout_command.sh'" +echo "# code for 'ftp logout' goes here" +echo "# you can edit it freely and regenerate (it will not be overwritten)" +inspect_args diff --git a/examples/command-groups/src/upload_command.sh b/examples/command-groups/src/upload_command.sh new file mode 100644 index 00000000..f6058c2f --- /dev/null +++ b/examples/command-groups/src/upload_command.sh @@ -0,0 +1,4 @@ +echo "# this file is located in 'src/upload_command.sh'" +echo "# code for 'ftp upload' goes here" +echo "# you can edit it freely and regenerate (it will not be overwritten)" +inspect_args diff --git a/examples/command-groups/test.sh b/examples/command-groups/test.sh new file mode 100644 index 00000000..74012833 --- /dev/null +++ b/examples/command-groups/test.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +rm -f ./src/*.sh + +set -x + +bashly generate + +./ftp +./ftp -h +./ftp login \ No newline at end of file diff --git a/examples/commands-nested/README.md b/examples/commands-nested/README.md new file mode 100644 index 00000000..742a36bd --- /dev/null +++ b/examples/commands-nested/README.md @@ -0,0 +1,7 @@ +Sub-Commands Example +================================================== + +This example was generated with: + + $ bashly init + $ bashly generate diff --git a/examples/commands-nested/cli b/examples/commands-nested/cli new file mode 100644 index 00000000..bf6e57b0 --- /dev/null +++ b/examples/commands-nested/cli @@ -0,0 +1,792 @@ +#!/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 nested commands\n" + echo + else + printf "cli - Sample application with nested commands\n" + echo + fi + + printf "Usage:\n" + printf " cli [command] [options]\n" + printf " cli [command] --help | -h\n" + printf " cli --version\n" + echo + # :command.usage_commands + printf "Commands:\n" + echo " dir Directory commands" + echo " file File commands" + echo + + if [[ -n $long_usage ]]; then + printf "Options:\n" + # :command.usage_fixed_flags + echo " --help, -h" + printf " Show this help\n" + echo + echo " --version" + printf " Show version number\n" + echo + + fi +} + +# :command.usage +cli_dir_usage() { + if [[ -n $long_usage ]]; then + printf "cli dir - Directory commands\n" + echo + else + printf "cli dir - Directory commands\n" + echo + fi + + printf "Shortcut: d\n" + echo + + printf "Usage:\n" + printf " cli dir [command] [options]\n" + printf " cli dir [command] --help | -h\n" + echo + # :command.usage_commands + printf "Commands:\n" + echo " list Show files in the directory" + echo " remove Remove directory" + 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_dir_list_usage() { + if [[ -n $long_usage ]]; then + printf "cli dir list - Show files in the directory\n" + echo + else + printf "cli dir list - Show files in the directory\n" + echo + fi + + printf "Usage:\n" + printf " cli dir list PATH [options]\n" + printf " cli dir list --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_args + printf "Arguments:\n" + + # :argument.usage + echo " PATH" + printf " Directory path\n" + echo + + fi +} + +# :command.usage +cli_dir_remove_usage() { + if [[ -n $long_usage ]]; then + printf "cli dir remove - Remove directory\n" + echo + else + printf "cli dir remove - Remove directory\n" + echo + fi + + printf "Usage:\n" + printf " cli dir remove PATH [options]\n" + printf " cli dir remove --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 " Remove even if when not empty\n" + echo + # :command.usage_args + printf "Arguments:\n" + + # :argument.usage + echo " PATH" + printf " Directory path\n" + echo + + fi +} + +# :command.usage +cli_file_usage() { + if [[ -n $long_usage ]]; then + printf "cli file - File commands\n" + echo + else + printf "cli file - File commands\n" + echo + fi + + printf "Shortcut: f\n" + echo + + printf "Usage:\n" + printf " cli file [command] [options]\n" + printf " cli file [command] --help | -h\n" + echo + # :command.usage_commands + printf "Commands:\n" + echo " show Show file contents" + echo " edit Edit the file" + 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_file_show_usage() { + if [[ -n $long_usage ]]; then + printf "cli file show - Show file contents\n" + echo + else + printf "cli file show - Show file contents\n" + echo + fi + + printf "Usage:\n" + printf " cli file show PATH [options]\n" + printf " cli file show --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_args + printf "Arguments:\n" + + # :argument.usage + echo " PATH" + printf " Path to file\n" + echo + + fi +} + +# :command.usage +cli_file_edit_usage() { + if [[ -n $long_usage ]]; then + printf "cli file edit - Edit the file\n" + echo + else + printf "cli file edit - Edit the file\n" + echo + fi + + printf "Usage:\n" + printf " cli file edit PATH [options]\n" + printf " cli file edit --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_args + printf "Arguments:\n" + + # :argument.usage + echo " PATH" + printf " Path to file\n" + echo + + fi +} + +# :command.inspect_args +inspect_args() { + echo args: + for k in "${!args[@]}"; do echo "- \${args[$k]} = ${args[$k]}"; done +} + +# :command.command_functions + +# :command.function +cli_dir_list_command() { + # :src/dir_list_command.sh + echo "# this file is located in 'src/dir_list_command.sh'" + echo "# code for 'cli dir list' goes here" + echo "# you can edit it freely and regenerate (it will not be overwritten)" + inspect_args +} + +# :command.function +cli_dir_remove_command() { + # :src/dir_remove_command.sh + echo "# this file is located in 'src/dir_remove_command.sh'" + echo "# code for 'cli dir remove' goes here" + echo "# you can edit it freely and regenerate (it will not be overwritten)" + inspect_args +} + +# :command.function +cli_file_show_command() { + # :src/file_show_command.sh + echo "# this file is located in 'src/file_show_command.sh'" + echo "# code for 'cli file show' goes here" + echo "# you can edit it freely and regenerate (it will not be overwritten)" + inspect_args +} + +# :command.function +cli_file_edit_command() { + # :src/file_edit_command.sh + echo "# this file is located in 'src/file_edit_command.sh'" + echo "# code for 'cli file edit' 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 ) + version_command + exit 1 + ;; + + --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 + -* ) + ;; + + dir | d ) + action="dir" + shift + cli_dir_parse_requirements "$@" + shift $# + ;; + + file | f ) + action="file" + shift + cli_file_parse_requirements "$@" + shift $# + ;; + + * ) + 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.parse_requirements +cli_dir_parse_requirements() { + # :command.fixed_flag_filter + case "$1" in + --version ) + version_command + exit 1 + ;; + + --help | -h ) + long_usage=yes + cli_dir_usage + exit 1 + ;; + + esac + # :command.environment_variables_filter + # :command.dependencies_filter + # :command.command_filter + action=$1 + + case $action in + -* ) + ;; + + list ) + action="list" + shift + cli_dir_list_parse_requirements "$@" + shift $# + ;; + + remove ) + action="remove" + shift + cli_dir_remove_parse_requirements "$@" + shift $# + ;; + + * ) + cli_dir_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.parse_requirements +cli_dir_list_parse_requirements() { + # :command.fixed_flag_filter + case "$1" in + --version ) + version_command + exit 1 + ;; + + --help | -h ) + long_usage=yes + cli_dir_list_usage + exit 1 + ;; + + esac + # :command.environment_variables_filter + # :command.dependencies_filter + # :command.command_filter + action="dir list" + # :command.required_args_filter + if [[ $1 && $1 != -* ]]; then + args[path]=$1 + shift + else + printf "missing required argument: PATH\nusage: cli dir list PATH [options]\n" + exit 1 + fi + # :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 + if [[ ! ${args[path]} ]]; then + args[path]=$1 + shift + else + printf "invalid argument: %s\n" "$key" + exit 1 + fi + ;; + + esac + done +} + +# :command.parse_requirements +cli_dir_remove_parse_requirements() { + # :command.fixed_flag_filter + case "$1" in + --version ) + version_command + exit 1 + ;; + + --help | -h ) + long_usage=yes + cli_dir_remove_usage + exit 1 + ;; + + esac + # :command.environment_variables_filter + # :command.dependencies_filter + # :command.command_filter + action="dir remove" + # :command.required_args_filter + if [[ $1 && $1 != -* ]]; then + args[path]=$1 + shift + else + printf "missing required argument: PATH\nusage: cli dir remove PATH [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[path]} ]]; then + args[path]=$1 + shift + else + printf "invalid argument: %s\n" "$key" + exit 1 + fi + ;; + + esac + done +} + +# :command.parse_requirements +cli_file_parse_requirements() { + # :command.fixed_flag_filter + case "$1" in + --version ) + version_command + exit 1 + ;; + + --help | -h ) + long_usage=yes + cli_file_usage + exit 1 + ;; + + esac + # :command.environment_variables_filter + # :command.dependencies_filter + # :command.command_filter + action=$1 + + case $action in + -* ) + ;; + + show ) + action="show" + shift + cli_file_show_parse_requirements "$@" + shift $# + ;; + + edit ) + action="edit" + shift + cli_file_edit_parse_requirements "$@" + shift $# + ;; + + * ) + cli_file_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.parse_requirements +cli_file_show_parse_requirements() { + # :command.fixed_flag_filter + case "$1" in + --version ) + version_command + exit 1 + ;; + + --help | -h ) + long_usage=yes + cli_file_show_usage + exit 1 + ;; + + esac + # :command.environment_variables_filter + # :command.dependencies_filter + # :command.command_filter + action="file show" + # :command.required_args_filter + if [[ $1 && $1 != -* ]]; then + args[path]=$1 + shift + else + printf "missing required argument: PATH\nusage: cli file show PATH [options]\n" + exit 1 + fi + # :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 + if [[ ! ${args[path]} ]]; then + args[path]=$1 + shift + else + printf "invalid argument: %s\n" "$key" + exit 1 + fi + ;; + + esac + done +} + +# :command.parse_requirements +cli_file_edit_parse_requirements() { + # :command.fixed_flag_filter + case "$1" in + --version ) + version_command + exit 1 + ;; + + --help | -h ) + long_usage=yes + cli_file_edit_usage + exit 1 + ;; + + esac + # :command.environment_variables_filter + # :command.dependencies_filter + # :command.command_filter + action="file edit" + # :command.required_args_filter + if [[ $1 && $1 != -* ]]; then + args[path]=$1 + shift + else + printf "missing required argument: PATH\nusage: cli file edit PATH [options]\n" + exit 1 + fi + # :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 + if [[ ! ${args[path]} ]]; then + args[path]=$1 + shift + else + printf "invalid argument: %s\n" "$key" + exit 1 + fi + ;; + + esac + done +} + +# :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 + parse_requirements "$@" + + if [[ $action == "dir" ]]; then + if [[ ${args[--help]} ]]; then + long_usage=yes + cli_dir_usage + else + cli_dir_command + fi + + elif [[ $action == "dir list" ]]; then + if [[ ${args[--help]} ]]; then + long_usage=yes + cli_dir_list_usage + else + cli_dir_list_command + fi + + elif [[ $action == "dir remove" ]]; then + if [[ ${args[--help]} ]]; then + long_usage=yes + cli_dir_remove_usage + else + cli_dir_remove_command + fi + + elif [[ $action == "file" ]]; then + if [[ ${args[--help]} ]]; then + long_usage=yes + cli_file_usage + else + cli_file_command + fi + + elif [[ $action == "file show" ]]; then + if [[ ${args[--help]} ]]; then + long_usage=yes + cli_file_show_usage + else + cli_file_show_command + fi + + elif [[ $action == "file edit" ]]; then + if [[ ${args[--help]} ]]; then + long_usage=yes + cli_file_edit_usage + else + cli_file_edit_command + fi + + elif [[ ${args[--version]} ]]; then + version_command + elif [[ ${args[--help]} ]]; then + long_usage=yes + cli_usage + elif [[ $action == "root" ]]; then + root_command + fi +} + +initialize +run "$@" diff --git a/examples/commands-nested/src/bashly.yml b/examples/commands-nested/src/bashly.yml new file mode 100644 index 00000000..74588c3a --- /dev/null +++ b/examples/commands-nested/src/bashly.yml @@ -0,0 +1,41 @@ +name: cli +help: Sample application with nested commands +version: 0.1.0 + +commands: +- name: dir + short: d + help: Directory commands + commands: + - name: list + help: Show files in the directory + + args: &dir_args + - name: path + help: Directory path + required: true + + - name: remove + help: Remove directory + args: *dir_args # reuse args from the list command + + flags: + - long: --force + short: -f + help: Remove even if when not empty + +- name: file + short: f + help: File commands + + commands: + - name: show + help: Show file contents + args: &file_args + - name: path + help: Path to file + required: true + + - name: edit + help: Edit the file + args: *file_args # reuse args from the show command \ No newline at end of file diff --git a/examples/commands-nested/src/dir_list_command.sh b/examples/commands-nested/src/dir_list_command.sh new file mode 100644 index 00000000..9a1c5dce --- /dev/null +++ b/examples/commands-nested/src/dir_list_command.sh @@ -0,0 +1,4 @@ +echo "# this file is located in 'src/dir_list_command.sh'" +echo "# code for 'cli dir list' goes here" +echo "# you can edit it freely and regenerate (it will not be overwritten)" +inspect_args diff --git a/examples/commands-nested/src/dir_remove_command.sh b/examples/commands-nested/src/dir_remove_command.sh new file mode 100644 index 00000000..b0e4c275 --- /dev/null +++ b/examples/commands-nested/src/dir_remove_command.sh @@ -0,0 +1,4 @@ +echo "# this file is located in 'src/dir_remove_command.sh'" +echo "# code for 'cli dir remove' goes here" +echo "# you can edit it freely and regenerate (it will not be overwritten)" +inspect_args diff --git a/examples/commands-nested/src/file_edit_command.sh b/examples/commands-nested/src/file_edit_command.sh new file mode 100644 index 00000000..d55be0aa --- /dev/null +++ b/examples/commands-nested/src/file_edit_command.sh @@ -0,0 +1,4 @@ +echo "# this file is located in 'src/file_edit_command.sh'" +echo "# code for 'cli file edit' goes here" +echo "# you can edit it freely and regenerate (it will not be overwritten)" +inspect_args diff --git a/examples/commands-nested/src/file_show_command.sh b/examples/commands-nested/src/file_show_command.sh new file mode 100644 index 00000000..e792edcb --- /dev/null +++ b/examples/commands-nested/src/file_show_command.sh @@ -0,0 +1,4 @@ +echo "# this file is located in 'src/file_show_command.sh'" +echo "# code for 'cli file show' goes here" +echo "# you can edit it freely and regenerate (it will not be overwritten)" +inspect_args diff --git a/examples/commands-nested/src/initialize.sh b/examples/commands-nested/src/initialize.sh new file mode 100644 index 00000000..f2dbc52c --- /dev/null +++ b/examples/commands-nested/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/commands-nested/test.sh b/examples/commands-nested/test.sh new file mode 100644 index 00000000..e6e48ac7 --- /dev/null +++ b/examples/commands-nested/test.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +rm -f ./src/*.sh + +set -x + +bashly generate + +./cli +./cli -h +./cli dir +./cli file +./cli dir -h +./cli file -h +./cli dir list +./cli dir list -h +./cli file edit +./cli file edit -h +./cli file edit filename diff --git a/examples/subcommands/README.md b/examples/commands/README.md similarity index 86% rename from examples/subcommands/README.md rename to examples/commands/README.md index 8564b732..128f3574 100644 --- a/examples/subcommands/README.md +++ b/examples/commands/README.md @@ -1,4 +1,4 @@ -Subcommands Example +Commands Example ================================================== This example was generated with: diff --git a/examples/subcommands/cli b/examples/commands/cli similarity index 100% rename from examples/subcommands/cli rename to examples/commands/cli diff --git a/examples/subcommands/src/bashly.yml b/examples/commands/src/bashly.yml similarity index 100% rename from examples/subcommands/src/bashly.yml rename to examples/commands/src/bashly.yml diff --git a/examples/subcommands/src/download_command.sh b/examples/commands/src/download_command.sh similarity index 100% rename from examples/subcommands/src/download_command.sh rename to examples/commands/src/download_command.sh diff --git a/examples/commands/src/initialize.sh b/examples/commands/src/initialize.sh new file mode 100644 index 00000000..f2dbc52c --- /dev/null +++ b/examples/commands/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/subcommands/src/upload_command.sh b/examples/commands/src/upload_command.sh similarity index 100% rename from examples/subcommands/src/upload_command.sh rename to examples/commands/src/upload_command.sh diff --git a/examples/subcommands/test.sh b/examples/commands/test.sh similarity index 100% rename from examples/subcommands/test.sh rename to examples/commands/test.sh diff --git a/examples/minimal/download b/examples/minimal/download index f0696d2d..0df38088 100644 --- a/examples/minimal/download +++ b/examples/minimal/download @@ -18,10 +18,10 @@ version_command() { # :command.usage download_usage() { if [[ -n $long_usage ]]; then - printf "download - Sample minimal application without subcommands\n" + printf "download - Sample minimal application without commands\n" echo else - printf "download - Sample minimal application without subcommands\n" + printf "download - Sample minimal application without commands\n" echo fi diff --git a/examples/minimal/src/bashly.yml b/examples/minimal/src/bashly.yml index fe45b09e..58119ca0 100644 --- a/examples/minimal/src/bashly.yml +++ b/examples/minimal/src/bashly.yml @@ -1,5 +1,5 @@ name: download -help: Sample minimal application without subcommands +help: Sample minimal application without commands version: 0.1.0 args: diff --git a/lib/bashly/commands/init.rb b/lib/bashly/commands/init.rb index f93ce808..92fc1cc5 100644 --- a/lib/bashly/commands/init.rb +++ b/lib/bashly/commands/init.rb @@ -7,7 +7,7 @@ class Init < Base usage "bashly init [--minimal]" usage "bashly init (-h|--help)" - option "-m --minimal", "Use a minimal configuration file (without subcommands)" + option "-m --minimal", "Use a minimal configuration file (without commands)" environment "BASHLY_SOURCE_DIR", "The path to use for creating the configuration file [default: src]" diff --git a/lib/bashly/models/base.rb b/lib/bashly/models/base.rb index 8e7390e6..ac538bc3 100644 --- a/lib/bashly/models/base.rb +++ b/lib/bashly/models/base.rb @@ -13,6 +13,7 @@ class Base environment_variables examples flags + group help long name diff --git a/lib/bashly/models/command.rb b/lib/bashly/models/command.rb index 9ef4fa81..ca9d750c 100644 --- a/lib/bashly/models/command.rb +++ b/lib/bashly/models/command.rb @@ -28,12 +28,12 @@ def caption_string help ? "#{full_name} - #{summary}" : full_name end - # Returns only the names of the subcommands (Commands) + # Returns only the names of the Commands def command_names commands.map &:name end - # Returns an array of the subcommands (Commands) + # Returns an array of the Commands def commands return [] unless options["commands"] options["commands"].map do |options| diff --git a/lib/bashly/templates/minimal.yml b/lib/bashly/templates/minimal.yml index fe45b09e..58119ca0 100644 --- a/lib/bashly/templates/minimal.yml +++ b/lib/bashly/templates/minimal.yml @@ -1,5 +1,5 @@ name: download -help: Sample minimal application without subcommands +help: Sample minimal application without commands version: 0.1.0 args: diff --git a/lib/bashly/templates/strings.yml b/lib/bashly/templates/strings.yml index 4c3259fa..bce57b80 100644 --- a/lib/bashly/templates/strings.yml +++ b/lib/bashly/templates/strings.yml @@ -8,6 +8,7 @@ arguments: "Arguments:" commands: "Commands:" examples: "Examples:" environment_variables: "Environment Variables:" +group: "%{group} Commands:" # Usage helpers command_shortcut: "Shortcut: %{short}" diff --git a/lib/bashly/views/command/usage_commands.erb b/lib/bashly/views/command/usage_commands.erb index 5938473f..4ea7c66f 100644 --- a/lib/bashly/views/command/usage_commands.erb +++ b/lib/bashly/views/command/usage_commands.erb @@ -1,9 +1,18 @@ # :command.usage_commands +<%- unless commands.first.group -%> printf "<%= strings[:commands] %>\n" +<%- end -%> <%- maxlen = command_names.map(&:size).max -%> <%- commands.each do |command| -%> <%- summary = command.summary -%> <%- summary = strings[:default_command_summary] % { summary: summary } if command.default -%> +<%- if command.group -%> +<%- if command.name == commands.first.name -%> +printf "<%= strings[:group] % { group: command.group } %>\n" +<%- else -%> +printf "\n<%= strings[:group] % { group: command.group } %>\n" +<%- end -%> +<%- end -%> echo " <%= command.name.ljust maxlen %> <%= summary %>" <%- end -%> echo diff --git a/spec/approvals/cli/init/help b/spec/approvals/cli/init/help index 698adfa9..1b10515d 100644 --- a/spec/approvals/cli/init/help +++ b/spec/approvals/cli/init/help @@ -9,7 +9,7 @@ Usage: Options: -m --minimal - Use a minimal configuration file (without subcommands) + Use a minimal configuration file (without commands) -h --help Show this help diff --git a/spec/approvals/examples/default-command b/spec/approvals/examples/command-default similarity index 100% rename from spec/approvals/examples/default-command rename to spec/approvals/examples/command-default diff --git a/spec/approvals/examples/command-groups b/spec/approvals/examples/command-groups new file mode 100644 index 00000000..31c1ccf5 --- /dev/null +++ b/spec/approvals/examples/command-groups @@ -0,0 +1,53 @@ ++ bashly generate +creating user files in src +created src/initialize.sh +created src/download_command.sh +created src/upload_command.sh +created src/login_command.sh +created src/logout_command.sh +created ./ftp +run ./ftp --help to test your bash script ++ ./ftp +ftp - Sample application with command grouping + +Usage: + ftp [command] [options] + ftp [command] --help | -h + ftp --version + +File Commands: + download Download a file + upload Upload a file + +Login Commands: + login Write login credentials to the config file + logout Delete login credentials to the config file + ++ ./ftp -h +ftp - Sample application with command grouping + +Usage: + ftp [command] [options] + ftp [command] --help | -h + ftp --version + +File Commands: + download Download a file + upload Upload a file + +Login Commands: + login Write login credentials to the config file + logout Delete login credentials to the config file + +Options: + --help, -h + Show this help + + --version + Show version number + ++ ./ftp login +# this file is located in 'src/login_command.sh' +# code for 'ftp login' goes here +# you can edit it freely and regenerate (it will not be overwritten) +args: diff --git a/spec/approvals/examples/subcommands b/spec/approvals/examples/commands similarity index 100% rename from spec/approvals/examples/subcommands rename to spec/approvals/examples/commands diff --git a/spec/approvals/examples/commands-nested b/spec/approvals/examples/commands-nested new file mode 100644 index 00000000..02a216b3 --- /dev/null +++ b/spec/approvals/examples/commands-nested @@ -0,0 +1,142 @@ ++ bashly generate +creating user files in src +created src/initialize.sh +created src/dir_list_command.sh +created src/dir_remove_command.sh +created src/file_show_command.sh +created src/file_edit_command.sh +created ./cli +run ./cli --help to test your bash script ++ ./cli +cli - Sample application with nested commands + +Usage: + cli [command] [options] + cli [command] --help | -h + cli --version + +Commands: + dir Directory commands + file File commands + ++ ./cli -h +cli - Sample application with nested commands + +Usage: + cli [command] [options] + cli [command] --help | -h + cli --version + +Commands: + dir Directory commands + file File commands + +Options: + --help, -h + Show this help + + --version + Show version number + ++ ./cli dir +cli dir - Directory commands + +Shortcut: d + +Usage: + cli dir [command] [options] + cli dir [command] --help | -h + +Commands: + list Show files in the directory + remove Remove directory + ++ ./cli file +cli file - File commands + +Shortcut: f + +Usage: + cli file [command] [options] + cli file [command] --help | -h + +Commands: + show Show file contents + edit Edit the file + ++ ./cli dir -h +cli dir - Directory commands + +Shortcut: d + +Usage: + cli dir [command] [options] + cli dir [command] --help | -h + +Commands: + list Show files in the directory + remove Remove directory + +Options: + --help, -h + Show this help + ++ ./cli file -h +cli file - File commands + +Shortcut: f + +Usage: + cli file [command] [options] + cli file [command] --help | -h + +Commands: + show Show file contents + edit Edit the file + +Options: + --help, -h + Show this help + ++ ./cli dir list +missing required argument: PATH +usage: cli dir list PATH [options] ++ ./cli dir list -h +cli dir list - Show files in the directory + +Usage: + cli dir list PATH [options] + cli dir list --help | -h + +Options: + --help, -h + Show this help + +Arguments: + PATH + Directory path + ++ ./cli file edit +missing required argument: PATH +usage: cli file edit PATH [options] ++ ./cli file edit -h +cli file edit - Edit the file + +Usage: + cli file edit PATH [options] + cli file edit --help | -h + +Options: + --help, -h + Show this help + +Arguments: + PATH + Path to file + ++ ./cli file edit filename +# this file is located in 'src/file_edit_command.sh' +# code for 'cli file edit' goes here +# you can edit it freely and regenerate (it will not be overwritten) +args: +- ${args[path]} = filename diff --git a/spec/approvals/examples/minimal b/spec/approvals/examples/minimal index be703316..3f31d752 100644 --- a/spec/approvals/examples/minimal +++ b/spec/approvals/examples/minimal @@ -8,7 +8,7 @@ creating user files in src missing required argument: SOURCE usage: download SOURCE [TARGET] [options] + ./download -h -download - Sample minimal application without subcommands +download - Sample minimal application without commands Usage: download SOURCE [TARGET] [options] diff --git a/spec/bashly/models/command_spec.rb b/spec/bashly/models/command_spec.rb index c89a37f8..8d832126 100644 --- a/spec/bashly/models/command_spec.rb +++ b/spec/bashly/models/command_spec.rb @@ -77,7 +77,7 @@ expect(subject.commands.first).to be_a Models::Command end - it "sets the parents property of its subcommands" do + it "sets the parents property of its commands" do expect(subject.commands.first.parents).to eq ["docker"] end end