├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── shellcheck.yml │ └── test.yml ├── LICENSE ├── README.md ├── app ├── .author ├── .bash_cli ├── .help ├── .name ├── .version ├── command │ ├── .help │ ├── create │ ├── create.help │ ├── create.sh │ ├── create.usage │ ├── rm │ ├── rm.help │ ├── rm.sh │ └── rm.usage ├── example │ ├── completion │ ├── completion.complete │ ├── completion.help │ └── completion.usage ├── install ├── install.help ├── install.sh ├── install.usage ├── uninstall ├── uninstall.help ├── uninstall.sh └── uninstall.usage ├── bash-cli.inc.sh ├── cli ├── complete ├── help └── tests └── cli.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - directory: / 4 | package-ecosystem: github-actions 5 | schedule: 6 | interval: daily 7 | -------------------------------------------------------------------------------- /.github/workflows/shellcheck.yml: -------------------------------------------------------------------------------- 1 | name: Shellcheck 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | shellcheck: 11 | name: Shellcheck 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@master 16 | 17 | - name: Register ShellCheck problem matchers 18 | uses: lumaxis/shellcheck-problem-matchers@v2.1.0 19 | 20 | - name: Run ShellCheck 21 | uses: ludeeus/action-shellcheck@master 22 | env: 23 | SHELLCHECK_OPTS: -e SC2148 24 | with: 25 | check_together: 'yes' 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test-linux: 7 | name: Test (Linux) 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@master 12 | 13 | - shell: bash 14 | name: Run tests 15 | run: | 16 | set -e 17 | for test in tests/*; do $test; done 18 | 19 | 20 | test-macos: 21 | name: Test (Mac OS) 22 | runs-on: macos-latest 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@master 26 | 27 | - shell: bash 28 | name: Run tests 29 | run: | 30 | set -e 31 | for test in tests/*; do $test; done 32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Sierra Softworks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bash CLI 2 | **A command line framework built using nothing but Bash and compatible with anything** 3 | 4 | Bash CLI was borne of the need to provide a common entrypoint into a range of scripts 5 | and tools for a project. Rather than port the scripts to something like Go or Python, 6 | or merge them into a single bash script, we opted to build a framework which allows 7 | and executable to be presented as a sub-command. 8 | 9 | ## Example 10 | 11 | ```sh 12 | bash-cli install my-app 13 | bash-cli command create start 14 | my-app start 15 | ``` 16 | 17 | ## Customizing Bash CLI 18 | Bash CLI is designed to make it as simple as possible for you to create **your** application. 19 | To that end, everything that makes it "Bash CLI" can be tweaked and changed by simply modifying 20 | the following files in your `app` directory. 21 | 22 | - **.name** should contain the name of your command line, something like "My Awesome App" 23 | - **.author** is meant to contain your name (or the name of your company) 24 | - **.version** should contain the version of your app, you can automatically include this using `git describe --tags > app/.version` 25 | - **.help** should be a short-ish description of what your app does and how people should use it. 26 | Don't worry about including help for every command here, or even a command list, Bash CLI will 27 | handle that for you automatically. 28 | 29 | ## Adding Commands 30 | Bash CLI commands are just a stock-standard script with a filename that matches the command name. 31 | These scripts are contained within your `app` folder, or within nested folders there if you want 32 | to create a tree-based command structure. 33 | 34 | For example, the script `app/test/hello` would be available through `cli test hello`. Any arguments 35 | passed after the command will be curried through to the script, making it trivial to pass values and 36 | options around as needed. 37 | 38 | The simplest way to add a command however, is to just run `bash-cli command create [command name]` 39 | and have it plop down the files for you to customize. 40 | 41 | ### Contextual Help 42 | Bash CLI provides tools which enable your users to easily discover how to use your command line without 43 | needing to read your docs (a travesty, we know). To make this possible, you'll want to add two extra 44 | files for each command. 45 | 46 | The first, `[command].usage` should define the arguments list that your command expects to receive, 47 | something like `NAME [MIDDLE_NAMES...] SURNAME`. This file is entirely optional, leaving it out will 48 | have Bash CLI present the command as if it didn't accept arguments. 49 | 50 | The second, `[command].help` is used to describe the arguments that your command accepts, as well as 51 | provide a bit of additional context around how it works, when you should use it etc. 52 | 53 | In addition to providing help for commands, you may also provide it for directories to explain what 54 | their sub-commands are intended to achieve. To do this, simply add a `.help` file to the directory. 55 | 56 | ## Autocomplete 57 | Autocomplete functionality has been added to make navigating the command line even easier than it 58 | was before. To install it, simply add the following to `/etc/bash_completion.d/my-app`. 59 | 60 | ```sh 61 | source "/opt/my-app/complete" 62 | complete -F _bash_cli my-app 63 | ``` 64 | 65 | If you want to add completion to your commands just create `[command].complete` file which returns array. 66 | ```sh 67 | OPTIONS=("one" "two" "three") 68 | echo ${OPTIONS[@]} 69 | ``` 70 | 71 | See `example/completion` command as example. 72 | 73 | You also might need to have access to arguments it could be done via: 74 | `local_args_array=(${COMP_WORDS[@]:${cmd_arg_start}})` 75 | for: `cli example completion 1 2 3 4` 76 | `local_args_array` will be `'1 2 3 4'` 77 | 78 | You also could use fzf in here to make interactive selects: 79 | 80 | ```sh 81 | echo -e "one\ntwo\nthree" | fzf 82 | ``` 83 | 84 | ## Frequently Asked Questions 85 | 86 | 1. **Can I use Bash CLI to run things which aren't bash scripts?** 87 | Absolutely, Bash CLI simply executes files - it doesn't care whether they're written in Bash, Ruby, 88 | Python or Go - if you can execute the file then you can use it with Bash CLI. 89 | 90 | 1. **Will Bash CLI work on my Mac?** 91 | It should, we've built everything to keep it as portable as possible, so if you do have a problem 92 | don't hesitate to open a bug report. 93 | 94 | 1. **Does it allow me to use tab-autocomplete?** 95 | As of the latest version, yes it does. The install command included in this repo will automatically 96 | set up your `/etc/bash_completion.d/` directory to provide support for your project. 97 | -------------------------------------------------------------------------------- /app/.author: -------------------------------------------------------------------------------- 1 | Benjamin Pannell -------------------------------------------------------------------------------- /app/.bash_cli: -------------------------------------------------------------------------------- 1 | v1 -------------------------------------------------------------------------------- /app/.help: -------------------------------------------------------------------------------- 1 | This is a demonstration application which makes use of Bash CLI 2 | to present its interface. 3 | 4 | You can place any scripts here and Bash CLI will automatically make 5 | them available, you can also add .help files to provide contextual 6 | help information for individual commands. 7 | 8 | Directories allow you to create tree based command hierarchies and you 9 | can add a .help file to each directory to describe what that tree does. -------------------------------------------------------------------------------- /app/.name: -------------------------------------------------------------------------------- 1 | Bash CLI -------------------------------------------------------------------------------- /app/.version: -------------------------------------------------------------------------------- 1 | 1.0 -------------------------------------------------------------------------------- /app/command/.help: -------------------------------------------------------------------------------- 1 | Tools to manage commands in a Bash CLI project. 2 | 3 | This includes things like creating new commands, removing existing 4 | commands and opening their various files for editing. -------------------------------------------------------------------------------- /app/command/create: -------------------------------------------------------------------------------- 1 | create.sh -------------------------------------------------------------------------------- /app/command/create.help: -------------------------------------------------------------------------------- 1 | COMMAND - The full name of the command you wish to create. 2 | Example: new command 3 | 4 | This will create the files required to fully describe a command 5 | to Bash CLI, specifically the , .usage and 6 | .help files. The correct directory structure to host 7 | the command will be created, and .help files will be created 8 | at each directory level. -------------------------------------------------------------------------------- /app/command/create.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# == 0 ]; then 4 | exit 3 5 | fi 6 | 7 | APP_DIR=$(pwd) 8 | if [[ -d "$APP_DIR/app" && -f "$APP_DIR/app/.bash_cli" ]]; then 9 | APP_DIR="$APP_DIR/app" 10 | fi 11 | 12 | if [[ ! -f "$APP_DIR/.bash_cli" ]]; then 13 | >&2 echo -e "\033[31mYou are not within a Bash CLI project\033[39m" 14 | >&2 echo "Please change your directory to a valid project or run the init command to set one up." 15 | exit 1 16 | fi 17 | 18 | CMD_DIR="$APP_DIR" 19 | 20 | if [[ $# -gt 1 ]]; then 21 | for dir in "${@:1:$(($#-1))}"; do 22 | CMD_DIR="$CMD_DIR/$dir" 23 | if [[ ! -d "$CMD_DIR" ]]; then 24 | mkdir "$CMD_DIR" 25 | echo "TODO: Add help for this directory" > "$CMD_DIR/.help" 26 | fi 27 | done 28 | fi 29 | 30 | CMD_NAME="${!#}" 31 | if [[ -f "$CMD_DIR/$CMD_NAME" ]]; then 32 | >&2 echo -e "\033[31mThat command already exists\033[39m" 33 | >&2 echo "We'd rather not overwrite commands you've already created." 34 | exit 1 35 | fi 36 | 37 | cat > "$CMD_DIR/$CMD_NAME" < "$CMD_DIR/$CMD_NAME.usage" 45 | cat > "$CMD_DIR/$CMD_NAME.help" <&2 echo -e "\033[31mYou are not within a Bash CLI project\033[39m" 14 | >&2 echo "Please change your directory to a valid project or run the init command to set one up." 15 | exit 1 16 | fi 17 | 18 | CMD_DIR="$APP_DIR" 19 | 20 | if [[ $# -gt 1 ]]; then 21 | for dir in "${@:1:$(($#-1))}"; do 22 | CMD_DIR="${CMD_DIR:?}/$dir" 23 | if [[ ! -d "$CMD_DIR" ]]; then 24 | exit 0 25 | fi 26 | done 27 | fi 28 | 29 | CMD_NAME="${!#}" 30 | if [[ -f "${CMD_DIR:?}/$CMD_NAME" ]]; then 31 | rm -f "${CMD_DIR:?}/$CMD_NAME" 32 | rm -f "${CMD_DIR:?}/$CMD_NAME.help" 33 | rm -f "${CMD_DIR:?}/$CMD_NAME.usage" 34 | elif [[ -d "${CMD_DIR:?}/$CMD_NAME" ]]; then 35 | rm -Rf "${CMD_DIR:?}/$CMD_NAME" 36 | else 37 | echo -e "\033[31mCommand \033[36m$*\033[31m did not exist\033[39m" 38 | exit 1 39 | fi 40 | 41 | echo -e "\033[32mCommand \033[36m$*\033[32m successfully removed\033[39m" 42 | -------------------------------------------------------------------------------- /app/command/rm.usage: -------------------------------------------------------------------------------- 1 | COMMAND -------------------------------------------------------------------------------- /app/example/completion: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo -e "\033[36mTODO\033[39m: Implement this command" 3 | -------------------------------------------------------------------------------- /app/example/completion.complete: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This is a simple example how to add autocompletion to you command. 3 | OPTIONS=("one" "two" "three") 4 | echo ${OPTIONS[@]} 5 | 6 | # You also might need to have access to arguments 7 | # it could be done via `local_args_array=(${COMP_WORDS[@]:${cmd_arg_start}})` 8 | # local_args_array will contain list of all argument after your command: 9 | # for: cli example completion 1 2 3 4 10 | # local_args_array will be '1 2 3 4' 11 | # You also could use fzf in here to make interactive selects. 12 | -------------------------------------------------------------------------------- /app/example/completion.help: -------------------------------------------------------------------------------- 1 | ARGS - The arguments you wish to provide to this command 2 | 3 | TODO: Fill out the help information for this command. 4 | -------------------------------------------------------------------------------- /app/example/completion.usage: -------------------------------------------------------------------------------- 1 | ARGS 2 | -------------------------------------------------------------------------------- /app/install: -------------------------------------------------------------------------------- 1 | install.sh -------------------------------------------------------------------------------- /app/install.help: -------------------------------------------------------------------------------- 1 | NAME - The name you would like to use for your commands 2 | Example: my-app 3 | FOLDER - (Optional) The directory you would like the command line installed to 4 | Default: /usr/bin 5 | Example: /usr/sbin 6 | 7 | Installs your command line app under the given name by creating a symlink 8 | to the Bash CLI proxy. -------------------------------------------------------------------------------- /app/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# == 0 ]; then 4 | exit 3 5 | fi 6 | 7 | APP_DIR=$(pwd) 8 | 9 | if [[ -f "$APP_DIR/.bash_cli" ]]; then 10 | APP_DIR=$(dirname "$APP_DIR") 11 | fi 12 | 13 | if [[ ! -f "$APP_DIR/app/.bash_cli" ]]; then 14 | >&2 echo -e "\033[31mYou are not within a Bash CLI project\033[39m" 15 | >&2 echo "Please change your directory to a valid project or run the init command to set one up." 16 | exit 1 17 | fi 18 | 19 | NAME="$1" 20 | FOLDER="${2-"/usr/bin"}" 21 | 22 | ln -s "$APP_DIR/cli" "$FOLDER/$NAME" 23 | cat > "/etc/bash_completion.d/$NAME" < use fallback to perl 15 | perl -e 'use Cwd "abs_path"; print abs_path(shift)' "$1" 16 | fi 17 | } 18 | 19 | APP_DIR=$(pwd) 20 | 21 | if [[ -f "$APP_DIR/.bash_cli" ]]; then 22 | APP_DIR=$(dirname "$APP_DIR") 23 | fi 24 | 25 | if [[ ! -f "$APP_DIR/app/.bash_cli" ]]; then 26 | >&2 echo -e "\033[31mYou are not within a Bash CLI project\033[39m" 27 | >&2 echo "Please change your directory to a valid project or run the init command to set one up." 28 | exit 1 29 | fi 30 | 31 | NAME="$1" 32 | FOLDER="${2-"/usr/bin"}" 33 | 34 | if [[ ! -f "$FOLDER/$NAME" ]]; then 35 | >&2 echo -e "\033[31mCommand \033[36m$1\033[31m did not exist in \033[36m$2\033[39m" 36 | exit 1 37 | fi 38 | 39 | LN_PATH=$(realpath "$FOLDER/$NAME") 40 | 41 | if [[ "$LN_PATH" != "$APP_DIR/cli" ]]; then 42 | >&2 echo -e "\033[31mCommand \033[36m$1\033[31m doesn't resolve to this project\033[39m" 43 | >&2 echo "Expected: $APP_DIR/cli" 44 | >&2 echo "Got: $LN_PATH" 45 | exit 1 46 | fi 47 | 48 | rm "$FOLDER/$NAME" 49 | -------------------------------------------------------------------------------- /app/uninstall.usage: -------------------------------------------------------------------------------- 1 | NAME [FOLDER] 2 | -------------------------------------------------------------------------------- /bash-cli.inc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # shellcheck disable=SC2034 # These are defined, even if not used, for simplicity's sake 4 | COLOR_BLACK="\033[30m" 5 | COLOR_RED="\033[31m" 6 | COLOR_GREEN="\033[32m" 7 | COLOR_YELLOW="\033[33m" 8 | COLOR_BLUE="\033[34m" 9 | COLOR_MAGENTA="\033[35m" 10 | COLOR_CYAN="\033[36m" 11 | COLOR_LIGHT_GRAY="\033[37m" 12 | COLOR_DARK_GRAY="\033[38m" 13 | COLOR_NORMAL="\033[39m" 14 | 15 | 16 | function bcli_resolve_path() { 17 | if [ -x "$( which realpath )" ] 18 | then 19 | # call the realpath utility if installed 20 | "$( which realpath )" "$1" 21 | else 22 | # on MacOS there is no realpath utility on a default installation 23 | # -> use fallback to perl 24 | perl -e 'use Cwd "abs_path"; print abs_path(shift)' "$1" 25 | fi 26 | } 27 | 28 | function bcli_trim_whitespace() { 29 | # Function courtesy of http://stackoverflow.com/a/3352015 30 | local var="$*" 31 | var="${var#"${var%%[![:space:]]*}"}" # remove leading whitespace characters 32 | var="${var%"${var##*[![:space:]]}"}" # remove trailing whitespace characters 33 | echo -n "$var" 34 | } 35 | 36 | function bcli_show_header() { 37 | echo -e "$(bcli_trim_whitespace "$(cat "$1/.name")")" 38 | echo -e "${COLOR_CYAN}Version ${COLOR_NORMAL}$(bcli_trim_whitespace "$(cat "$1/.version")")" 39 | echo -e "${COLOR_CYAN}Author ${COLOR_NORMAL}$(bcli_trim_whitespace "$(cat "$1/.author")")" 40 | } 41 | 42 | function bcli_entrypoint() { 43 | local root_dir; 44 | root_dir=$(dirname "$(bcli_resolve_path "$0")") 45 | 46 | local cli_entrypoint; 47 | cli_entrypoint=$(basename "$0") 48 | 49 | # Locate the correct command to execute by looking through the app directory 50 | # for folders and files which match the arguments provided on the command line. 51 | local cmd_file; 52 | cmd_file="$root_dir/app/" 53 | local cmd_arg_start; 54 | cmd_arg_start=1 55 | while [[ -d "$cmd_file" && $cmd_arg_start -le $# ]]; do 56 | 57 | # If the user provides help as the last argument on a directory, then 58 | # show them the help for that directory rather than continuing 59 | if [[ "${!cmd_arg_start}" == "help" ]]; then 60 | # Strip off the "help" portion of the command 61 | local args; 62 | args=("$@") 63 | unset "args[$((cmd_arg_start-1))]" 64 | args=("${args[@]}") 65 | 66 | "$root_dir/help" "$0" "${args[@]}" 67 | exit 3 68 | fi 69 | 70 | cmd_file="$cmd_file/${!cmd_arg_start}" 71 | cmd_arg_start=$((cmd_arg_start+1)) 72 | done 73 | 74 | # Place the arguments for the command in their own list 75 | # to make future work with them easier. 76 | local cmd_args; 77 | cmd_args=("${@:cmd_arg_start}") 78 | 79 | # If we hit a directory by the time we run out of arguments, then our user 80 | # hasn't completed their command, so we'll show them the help for that directory 81 | # to help them along. 82 | if [ -d "$cmd_file" ]; then 83 | "$root_dir/help" "$0" "$@" 84 | exit 3 85 | fi 86 | 87 | # If we didn't couldn't find the exact command the user entered then warn them 88 | # about it, and try to be helpful by displaying help for that directory. 89 | if [[ ! -f "$cmd_file" ]]; then 90 | "$root_dir/help" "$0" "${@:1:$((cmd_arg_start-1))}" 91 | >&2 echo -e "${COLOR_RED}We could not find the command ${COLOR_CYAN}$cli_entrypoint ${*:1:$cmd_arg_start}${COLOR_NORMAL}" 92 | >&2 echo -e "To help out, we've shown you the help docs for ${COLOR_CYAN}$cli_entrypoint ${*:1:$((cmd_arg_start-1))}${COLOR_NORMAL}" 93 | exit 3 94 | fi 95 | 96 | # If --help is passed as one of the arguments to the command then show 97 | # the command's help information. 98 | arg_i=0 # We need the index to be able to strip list indices 99 | for arg in "${cmd_args[@]}"; do 100 | if [[ "${arg}" == "--help" ]]; then 101 | # Strip off the `--help` portion of the command 102 | unset "cmd_args[$arg_i]" 103 | cmd_args=("${cmd_args[@]}") 104 | 105 | # Pass the result to the help script for interrogation 106 | "$root_dir/help" "$0" "${@:1:$((cmd_arg_start - 1))}" "${cmd_args[@]}" 107 | exit 3 108 | fi 109 | arg_i=$((arg_i+1)) 110 | done 111 | 112 | # Run the command and capture its exit code for introspection 113 | "$cmd_file" "${cmd_args[@]}" 114 | EXIT_CODE=$? 115 | 116 | # If the command exited with an exit code of 3 (our "show help" code) 117 | # then show the help documentation for the command. 118 | if [[ $EXIT_CODE == 3 ]]; then 119 | "$root_dir/help" "$0" "$@" 120 | fi 121 | 122 | # Exit with the same code as the command 123 | exit $EXIT_CODE 124 | } 125 | 126 | function bcli_help() { 127 | local root_dir; 128 | root_dir=$(dirname "$(bcli_resolve_path "$0")") 129 | 130 | local cli_entrypoint; 131 | cli_entrypoint=$(basename "$1") 132 | 133 | # If we don't have any additional help arguments, then show the app's 134 | # header as well. 135 | if [ $# == 0 ]; then 136 | bcli_show_header "$root_dir/app" 137 | fi 138 | 139 | # Locate the correct level to display the helpfile for, either a directory 140 | # with no further arguments, or a command file. 141 | local help_file; 142 | help_file="$root_dir/app/" 143 | local help_arg_start; 144 | help_arg_start=2 145 | while [[ -d "$help_file" && $help_arg_start -le $# ]]; do 146 | help_file="$help_file/${!help_arg_start}" 147 | help_arg_start=$((help_arg_start+1)) 148 | done 149 | 150 | # If we've got a directory's helpfile to show, then print out the list of 151 | # commands in that directory along with its help content. 152 | if [[ -d "$help_file" ]]; then 153 | echo -e "${COLOR_GREEN}$cli_entrypoint ${COLOR_CYAN}${*:2:$((help_arg_start-1))} ${COLOR_NORMAL}" 154 | 155 | # If there's a help file available for this directory, then show it. 156 | if [[ -f "$help_file/.help" ]]; then 157 | cat "$help_file/.help" 158 | echo "" 159 | fi 160 | 161 | echo "" 162 | echo -e "${COLOR_MAGENTA}Commands${COLOR_NORMAL}" 163 | echo "" 164 | 165 | for file in "$help_file"/*; do 166 | cmd=$(basename "$file") 167 | 168 | # Don't show hidden files as available commands 169 | if [[ "$cmd" != .* && "$cmd" != *.* ]]; then 170 | echo -en "${COLOR_GREEN}$cli_entrypoint ${COLOR_CYAN}${*:2:$((help_arg_start-1))} $cmd ${COLOR_NORMAL}" 171 | 172 | if [[ -f "$file.usage" ]]; then 173 | bcli_trim_whitespace "$(cat "$file.usage")" 174 | echo "" 175 | elif [[ -d "$file" ]]; then 176 | echo -e "${COLOR_MAGENTA}...${COLOR_NORMAL}" 177 | else 178 | echo "" 179 | fi 180 | fi 181 | done 182 | 183 | exit 0 184 | fi 185 | 186 | echo -en "${COLOR_GREEN}$cli_entrypoint ${COLOR_CYAN}${*:2:$((help_arg_start-1))} ${COLOR_NORMAL}" 187 | if [[ -f "$help_file.usage" ]]; then 188 | bcli_trim_whitespace "$(cat "$help_file.usage")" 189 | echo "" 190 | else 191 | echo "" 192 | fi 193 | 194 | 195 | if [[ -f "$help_file.help" ]]; then 196 | cat "$help_file.help" 197 | echo "" 198 | fi 199 | } 200 | 201 | function bcli_bash_completions() { 202 | local root_dir; 203 | root_dir= 204 | root_dir=$(dirname "$(bcli_resolve_path "$(which "${COMP_WORDS[0]}")")") 205 | 206 | local curr_arg; 207 | curr_arg="${COMP_WORDS[COMP_CWORD]}" 208 | 209 | # Locate the correct command to execute by looking through the app directory 210 | # for folders and files which match the arguments provided on the command line. 211 | local cmd_file="$root_dir/app/" 212 | local cmd_arg_start=1 213 | while [[ -d "$cmd_file" && $cmd_arg_start -le $COMP_CWORD ]]; do 214 | 215 | # Handle the help virtual command by "ignoring" it 216 | if [[ "${COMP_WORDS[cmd_arg_start]}" == "help" ]]; then 217 | cmd_arg_start=$((cmd_arg_start+1)) 218 | continue 219 | fi 220 | 221 | cmd_file="$cmd_file/${COMP_WORDS[cmd_arg_start]}" 222 | cmd_arg_start=$((cmd_arg_start+1)) 223 | done 224 | 225 | # If we've found something which doesn't exist, then let's 226 | # look at its containing directory for info. 227 | if [[ ! -e "$cmd_file" ]]; then 228 | cmd_file=$(dirname "$cmd_file") 229 | fi 230 | 231 | # If cursor is on the end of command we want to get a name of current file/directory 232 | # and don't look inside folder. 233 | if [[ $curr_arg = $(basename "$cmd_file") ]]; then 234 | # shellcheck disable=SC2207 # Using this as alternatives are not cross-platform or introduce dependencies 235 | COMPREPLY=($(basename "$cmd_file")) 236 | return 237 | fi 238 | 239 | # If we found a command, then suggest the `--help` argument 240 | # TODO: Add parsing of .usage files for this 241 | if [[ -f "$cmd_file" ]]; then 242 | # Check if we've already got a `--help`, don't output anything 243 | # if we do. 244 | for i in $(seq $cmd_arg_start "$COMP_CWORD"); do 245 | if [[ "${COMP_WORDS[$i]}" == "--help" ]]; then 246 | COMPREPLY=() 247 | return 248 | fi 249 | done 250 | # Use bash completion file if any. 251 | if [ -f "${cmd_file}.complete" ]; then 252 | # shellcheck disable=SC2207 # Using this as alternatives are not cross-platform or introduce dependencies 253 | # shellcheck disable=SC1090 # Disabling as nature of this file is a really dynamic 254 | COMPREPLY=($(compgen -W "--help $(source "${cmd_file}.complete")" -- "$curr_arg" ) ) 255 | return 256 | else 257 | # shellcheck disable=SC2207 # Using this as alternatives are not cross-platform or introduce dependencies 258 | COMPREPLY=($(compgen -W '--help' -- "$curr_arg")) 259 | return 260 | fi 261 | # If we found a directory, then show all the commands which are 262 | # available within it, as well as the `help` virtual command. 263 | elif [ -d "$cmd_file" ]; then 264 | local opts=("help") 265 | while IFS= read -d $'\0' -r file ; do 266 | # shellcheck disable=SC2207 # Using this as alternatives are not cross-platform or introduce dependencies 267 | opts=("${opts[@]}" $(basename "$file")) 268 | done < <(find "$cmd_file"/ -maxdepth 1 ! -path "$cmd_file"/ ! -iname '*.*' -print0) 269 | 270 | IFS=" 271 | " 272 | # shellcheck disable=SC2207 # Using this as alternatives are not cross-platform or introduce dependencies 273 | COMPREPLY=($(compgen -W "$(printf '%s\n' "${opts[@]}")" -- "$curr_arg")) 274 | fi 275 | } 276 | -------------------------------------------------------------------------------- /cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | realpath() { 4 | if [ -x "$( which realpath )" ] 5 | then 6 | # call the realpath utility if installed 7 | "$( which realpath )" "$1" 8 | else 9 | # on MacOS there is no realpath utility on a default installation 10 | # -> use fallback to perl 11 | perl -e 'use Cwd "abs_path"; print abs_path(shift)' "$1" 12 | fi 13 | } 14 | 15 | ROOT_DIR=$(dirname "$(realpath "$0")") 16 | 17 | # shellcheck source=./bash-cli.inc.sh 18 | . "$ROOT_DIR/bash-cli.inc.sh" 19 | 20 | # Run the Bash CLI entrypoint 21 | bcli_entrypoint "$@" 22 | -------------------------------------------------------------------------------- /complete: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # this file is sourced by /etc/bash_completion.d/* and therefore should not shadow 4 | # any existing commands in the shell -> add bcli_ prefix to realpath-helper function here 5 | bcli_realpath() { 6 | if [ -x "$( which realpath )" ] 7 | then 8 | # call the realpath utility if installed 9 | "$( which realpath )" "$1" 10 | else 11 | # on MacOS there is no realpath utility on a default installation 12 | # -> use fallback to perl 13 | perl -e 'use Cwd "abs_path"; print abs_path(shift)' "$1" 14 | fi 15 | } 16 | 17 | function _bash_cli() { 18 | local root_dir; 19 | root_dir=$(dirname "$(bcli_realpath "$(which "${COMP_WORDS[0]}")")") 20 | 21 | # shellcheck source=./bash-cli.inc.sh 22 | . "$root_dir/bash-cli.inc.sh" 23 | 24 | bcli_bash_completions 25 | } 26 | -------------------------------------------------------------------------------- /help: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | realpath() { 4 | if [ -x "$( which realpath )" ] 5 | then 6 | # call the realpath utility if installed 7 | "$( which realpath )" "$1" 8 | else 9 | # on MacOS there is no realpath utility on a default installation 10 | # -> use fallback to perl 11 | perl -e 'use Cwd "abs_path"; print abs_path(shift)' "$1" 12 | fi 13 | } 14 | 15 | ROOT_DIR=$(dirname "$(realpath "$0")") 16 | 17 | # shellcheck source=./bash-cli.inc.sh 18 | . "$ROOT_DIR/bash-cli.inc.sh" 19 | 20 | bcli_help "$@" 21 | -------------------------------------------------------------------------------- /tests/cli.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | CMD="$(dirname "$(dirname "$0")")/cli" 4 | 5 | function fail() { 6 | echo "!!! FAIL !!!" 7 | exit 1 8 | } 9 | 10 | function test_entrypoint() { 11 | echo "Command listing should include the install command..." 12 | $CMD | perl -pe 's/\x1b\[[0-9;]*[mG]//g' | grep " install NAME \[FOLDER\]" >/dev/null || fail 13 | 14 | echo "Command listing should include the uninstall command..." 15 | $CMD | perl -pe 's/\x1b\[[0-9;]*[mG]//g' | grep " uninstall NAME \[FOLDER\]" >/dev/null || fail 16 | 17 | echo "Command listing should include the commad subcommand..." 18 | $CMD | perl -pe 's/\x1b\[[0-9;]*[mG]//g' | grep "command ..." >/dev/null || fail 19 | 20 | } 21 | test_entrypoint 22 | 23 | 24 | function test_help() { 25 | echo "Command should print help for the install command..." 26 | $CMD install --help | perl -pe 's/\x1b\[[0-9;]*[mG]//g' | grep "Installs your command line app" >/dev/null || fail 27 | 28 | } 29 | test_help 30 | --------------------------------------------------------------------------------