Skip to content

Supports multiple command script sources #249

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -295,7 +295,8 @@ where:
file within your project's copy of the framework (adjusted to reflect where
your copy of `go-script-bash` actually resides)
- `scripts` is the path to the directory holding your project's command scripts
relative to the project root (it can be any name you like)
relative to the project root (it can be any name you like). You can specify
multiple paths, separated by a space, according to your project's structure.

#### Directory structure

@@ -338,9 +339,10 @@ The following variables are set by the framework based on the above example
* `_GO_SCRIPTS_DIR`: `$_GO_ROOTDIR/scripts`
* `_GO_PLUGINS_DIR`: `/absolute/path/to/project-root/plugins`

For plugins, `_GO_ROOTDIR` and `_GO_SCRIPTS_DIR` will be scoped to the root
directory of the plugin installation; the other variables will remain the same.
See `./go help plugins` for more details.
`_GO_SCRIPTS_DIR` and `_GO_PLUGINS_DIR` are arrays of file paths to support
flexible project structures. For plugins, `_GO_ROOTDIR` and `_GO_SCRIPTS_DIR`
will be scoped to the root directory of the plugin installation; the other
variables will remain the same. See `./go help plugins` for more details.

#### Command scripts

97 changes: 58 additions & 39 deletions go-core.bash
Original file line number Diff line number Diff line change
@@ -110,8 +110,8 @@ declare _GO_IMPORTED_MODULE_FILES=()
# Used in the plugin module namespace collision warning message.
declare _GO_IMPORTED_MODULE_CALLERS=()

# Path to the project's script directory
declare _GO_SCRIPTS_DIR=
# Paths to the project's script directories
declare _GO_SCRIPTS_DIRS=()

# Directory containing Bats tests, relative to `_GO_ROOTDIR`
declare -r -x _GO_TEST_DIR="${_GO_TEST_DIR:-tests}"
@@ -135,11 +135,11 @@ declare -x _GO_CMD_NAME=
# string with the arguments delimited by the ASCII Unit Separator ($'\x1f').
declare -x _GO_CMD_ARGV=

# The top-level directory in which plugins are installed.
# The top-level directories in which plugins are installed.
#
# If a command script is running as a plugin, this value will be the plugins
# directory of the top-level `./go` script.
declare _GO_PLUGINS_DIR=
declare _GO_PLUGINS_DIRS=()

# Directories containing executable plugin scripts.
declare _GO_PLUGINS_PATHS=()
@@ -231,9 +231,9 @@ declare _GO_INJECT_MODULE_PATH="$_GO_INJECT_MODULE_PATH"

# Searches through plugin directories using a helper function
#
# The search will begin in `_GO_SCRIPTS_DIR/plugins`. As long as `search_func`
# The search will begin in `_GO_SCRIPTS_DIRS/plugins`. As long as `search_func`
# returns nonzero, every parent `/plugins/` directory will be searched, up to
# and including the top-level `_GO_PLUGINS_DIR`. The search will end either when
# and including the top-level `_GO_PLUGINS_DIRS`. The search will end either when
# `search_func` returns zero, or when all of the plugin paths are exhausted.
#
# The helper function, `search_func`, will receive the current plugin directory
@@ -265,16 +265,26 @@ declare _GO_INJECT_MODULE_PATH="$_GO_INJECT_MODULE_PATH"
# Returns:
# Zero if `search_func` ever returns zero, nonzero otherwise
@go.search_plugins() {
local __gsp_plugins_dir="$_GO_SCRIPTS_DIR/plugins"

while true; do
if "$1" "$__gsp_plugins_dir"; then
return
elif [[ "$__gsp_plugins_dir" == "$_GO_PLUGINS_DIR" ]]; then
return 1
fi
__gsp_plugins_dir="${__gsp_plugins_dir%/plugins/*}/plugins"
local __gsp_plugins_dir
local scripts_dir
local plugins_dir

for scripts_dir in "${_GO_SCRIPTS_DIRS[@]/%//plugins}"; do
__gsp_plugins_dir="$scripts_dir"
while true; do
if "$1" "$__gsp_plugins_dir"; then
return
else
for plugins_dir in "${_GO_PLUGINS_DIRS[@]}"; do
if [[ "$__gsp_plugins_dir" == "$plugins_dir" ]]; then
break 2
fi
done
fi
__gsp_plugins_dir="${__gsp_plugins_dir%/plugins/*}/plugins"
done
done
return 1
}

# Main driver of ./go script functionality.
@@ -332,11 +342,15 @@ declare _GO_INJECT_MODULE_PATH="$_GO_INJECT_MODULE_PATH"
return 1
fi

if [[ "${__go_cmd_path#$_GO_SCRIPTS_DIR}" =~ /plugins/[^/]+/bin/ ]]; then
[email protected]_plugin_command_script "$__go_cmd_path" "${__go_argv[@]}"
else
[email protected]_command_script "$__go_cmd_path" "${__go_argv[@]}"
fi
local script_dir
for script_dir in "${_GO_SCRIPTS_DIRS[@]}"; do
if [[ "${__go_cmd_path[0]#$script_dir}" =~ /plugins/[^/]+/bin/ ]]; then
[email protected]_plugin_command_script "${__go_cmd_path[0]}" "${__go_argv[@]}"
return
fi
done

[email protected]_command_script "${__go_cmd_path[0]}" "${__go_argv[@]}"
}

[email protected]_builtin() {
@@ -346,13 +360,13 @@ [email protected]_builtin() {
}

[email protected]_plugin_command_script() {
local _GO_SCRIPTS_DIR="${__go_cmd_path%/bin/*}/bin"
local _GO_ROOTDIR="${_GO_SCRIPTS_DIR%/*}"
local _GO_SCRIPTS_DIRS="${__go_cmd_path[0]%/bin/*}/bin"
local _GO_ROOTDIR="${_GO_SCRIPTS_DIRS%/*}"
local _GO_PLUGINS_PATHS=()
local _GO_SEARCH_PATHS=()

[email protected]_search_paths
[email protected]_command_script "$__go_cmd_path" "${__go_argv[@]}"
[email protected]_command_script "${__go_cmd_path[0]}" "${__go_argv[@]}"
}

[email protected]_command_script() {
@@ -397,23 +411,28 @@ [email protected]_command_script() {
}

[email protected]_scripts_dir() {
local scripts_dir="$_GO_ROOTDIR/$1"

if [[ "$#" -ne '1' ]]; then
echo "ERROR: there should be exactly one command script dir specified" >&2
return 1
elif [[ ! -e "$scripts_dir" ]]; then
echo "ERROR: command script directory $scripts_dir does not exist" >&2
return 1
elif [[ ! -d "$scripts_dir" ]]; then
echo "ERROR: $scripts_dir is not a directory" >&2
return 1
elif [[ ! -r "$scripts_dir" || ! -x "$scripts_dir" ]]; then
echo "ERROR: you do not have permission to access the $scripts_dir" \
"directory" >&2
if [[ "$#" -eq '0' ]]; then
echo "ERROR: no command script dir specified" >&2
return 1
fi
_GO_SCRIPTS_DIR="$scripts_dir"

local scripts_dir
while [[ "$#" -gt 0 ]]; do
scripts_dir="$_GO_ROOTDIR/$1"
if [[ ! -e "$scripts_dir" ]]; then
echo "ERROR: command script directory $scripts_dir does not exist" >&2
return 1
elif [[ ! -d "$scripts_dir" ]]; then
echo "ERROR: $scripts_dir is not a directory" >&2
return 1
elif [[ ! -r "$scripts_dir" || ! -x "$scripts_dir" ]]; then
echo "ERROR: you do not have permission to access the $scripts_dir" \
"directory" >&2
return 1
fi
shift
_GO_SCRIPTS_DIRS+=("$scripts_dir")
done
}

if ! [email protected]_scripts_dir "$@"; then
@@ -428,4 +447,4 @@ elif [[ -z "$COLUMNS" ]]; then
fi
export COLUMNS="${COLUMNS:-80}"
fi
_GO_PLUGINS_DIR="$_GO_SCRIPTS_DIR/plugins"
_GO_PLUGINS_DIRS=("${_GO_SCRIPTS_DIRS[@]/%//plugins}")
12 changes: 6 additions & 6 deletions go-template
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@
#
# This template automatically checks for the presence of the go-script-bash
# sources and downloads the go-script-bash repository contents if necessary
# before dispatching commands. (If you prefer, you can change the logic to
# before dispatching commands. (If you prefer, you can change the logic to
# create a shallow or regular clone instead.) This allows users to set up the
# framework without taking any extra steps when running the command for the
# first time, without the need to commit the framework to your repository.
@@ -19,7 +19,7 @@
# Make sure the variables within this script are configured as necessary for
# your program. You can add any other initialization or configuration between:
#
# . "$GO_SCRIPT_BASH_CORE_DIR/go-core.bash" "$GO_SCRIPTS_DIR"`
# . "$GO_SCRIPT_BASH_CORE_DIR/go-core.bash" "${GO_SCRIPTS_DIR[@]}"`
# `@go "$@"`

# Set to 'true' if your script is a standalone program, i.e. not bound to
@@ -30,15 +30,15 @@ export _GO_STANDALONE=
# The path where your command scripts reside
#
# For `_GO_STANDALONE` programs and plugins containing command scripts, you may
# wish to set GO_SCRIPTS_DIR to `bin` and have a separate `./go` script to
# wish to set GO_SCRIPTS_DIRS to `bin` and have a separate `./go` script to
# manage project tasks that finds its command scripts in `scripts`.
declare GO_SCRIPTS_DIR="${GO_SCRIPTS_DIR:-scripts}"
declare GO_SCRIPTS_DIR=("${GO_SCRIPTS_DIRS[@]-scripts}")

# The `GO_SCRIPT_BASH_REPO_URL` tag or branch you wish to use
declare GO_SCRIPT_BASH_VERSION="${GO_SCRIPT_BASH_VERSION:-v1.7.0}"

# The go-script-bash installation directory within your project
declare GO_SCRIPT_BASH_CORE_DIR="${GO_SCRIPT_BASH_CORE_DIR:-${0%/*}/$GO_SCRIPTS_DIR/go-script-bash}"
declare GO_SCRIPT_BASH_CORE_DIR="${GO_SCRIPT_BASH_CORE_DIR:-${0%/*}/${GO_SCRIPTS_DIR[0]}/go-script-bash}"

# The URL of the go-script-bash framework sources
declare GO_SCRIPT_BASH_REPO_URL="${GO_SCRIPT_BASH_REPO_URL:-https://github.com/mbland/go-script-bash.git}"
@@ -120,6 +120,6 @@ if [[ ! -e "$GO_SCRIPT_BASH_CORE_DIR/go-core.bash" ]]; then
fi
fi

. "$GO_SCRIPT_BASH_CORE_DIR/go-core.bash" "$GO_SCRIPTS_DIR"
. "$GO_SCRIPT_BASH_CORE_DIR/go-core.bash" "${GO_SCRIPTS_DIR[@]}"
# Add any other configuration or initialization steps here.
@go "$@"
20 changes: 13 additions & 7 deletions lib/internal/complete
Original file line number Diff line number Diff line change
@@ -6,7 +6,11 @@ [email protected]_top_level_commands() {
[email protected]_builtin 'commands'
else
printf 'help\n'
[email protected]_builtin 'commands' "$_GO_SCRIPTS_DIR"

local scripts_paths
printf -v scripts_paths '%s:' "${_GO_SCRIPTS_DIRS[@]}"
scripts_paths="${scripts_paths%:}"
[email protected]_builtin 'commands' "$scripts_paths"
fi
}

@@ -35,21 +39,23 @@ [email protected]_command_path() {
elif ! [email protected]_command_path_and_argv "$@"; then
return 1
fi
(( __go_complete_word_index -= ($# - ${#__go_argv[@]}) ))
((__go_complete_word_index -= ($# - ${#__go_argv[@]})))

if [[ "$__go_complete_word_index" -lt '0' ]]; then
# This (sub)command itself is the completion target.
echo "${__go_cmd_path##*/}"
echo "${__go_cmd_path[*]##*/}"
return 1

elif [[ "$__go_complete_word_index" -eq '0' ]]; then
# Complete subcommand scripts.
local c
local subcommands=()
for c in "${__go_cmd_path}.d"/*; do
if [[ -f "$c" && -x "$c" ]]; then
subcommands+=("${c##*/}")
fi
for path in "${__go_cmd_path[@]}"; do
for c in "$path.d"/*; do
if [[ -f "$c" && -x "$c" ]]; then
subcommands+=("${c##*/}")
fi
done
done
echo "${subcommands[@]}"
fi
68 changes: 27 additions & 41 deletions lib/internal/path
Original file line number Diff line number Diff line change
@@ -23,54 +23,40 @@ [email protected]_command_path_and_argv() {
return 1
fi

local cmd_args=("$@")
local cmd_name="${cmd_args[0]}"
local cmd_path
local path_suffix
local try_path

unset 'cmd_args[0]'

for try_path in "${_GO_SEARCH_PATHS[@]}"; do
try_path="$try_path/$cmd_name"

if [[ -f "$try_path" && -x "$try_path" ]]; then
cmd_path="$try_path"
__go_cmd_name=()
__go_argv=()
__go_cmd_path=()

for ((i = "$#"; i > 0; i--)); do
for try_path in "${_GO_SEARCH_PATHS[@]}"; do
path_suffix="$(printf '%s.d/' "${@:1:i}")"
path_suffix="${path_suffix%.d/}"
try_path="$try_path/$path_suffix"

if [[ -f "$try_path" && -x "$try_path" ]]; then
__go_cmd_path+=("$try_path")
if [[ "${#__go_argv[@]}" -eq 0 ]]; then
__go_cmd_name=("${@:1:i}")
__go_argv=("${@:i+1}")
fi
fi
done

if [[ "${#__go_cmd_path[@]}" -ne 0 ]]; then
break
elif [[ -e "$try_path" ]]; then
@go.printf "$try_path is not an executable script\n" >&2
return 1
fi
done

if [[ -z "$cmd_path" ]]; then
printf "Unknown command: ${cmd_name}\n\n" >&2
# The command that is the most nested one takes precedence. Eg
# `scripts/foobar/aaa/bbb/ccc arg1 arg2` takes precedence over
# `scripts/foobar/aaa bbb ccc arg1 arg2`.

if [[ "${#__go_cmd_name[*]}" -eq 0 ]]; then
printf "Unknown command: $1\n\n" >&2
[email protected]_available_commands "${_GO_SEARCH_PATHS[@]}" >&2
return 1
fi

local cmd_arg_index=1
__go_cmd_name=("$cmd_name")

for arg in "${cmd_args[@]}"; do
# This is most likely to happen during argument completion.
if [[ -z "$arg" ]]; then
break
fi

try_path="${cmd_path}.d/$arg"

if [[ ! -e "$try_path" ]]; then
break
elif [[ ! (-f "$try_path" && -x "$try_path") ]]; then
@go.printf "$try_path is not an executable script\n" >&2
return 1
fi

__go_cmd_name+=("$arg")
cmd_path="$try_path"
unset "cmd_args[$((cmd_arg_index++))]"
done

__go_cmd_path="$cmd_path"
__go_argv=("${cmd_args[@]}")
}
18 changes: 15 additions & 3 deletions lib/internal/set-search-paths
Original file line number Diff line number Diff line change
@@ -5,9 +5,21 @@ [email protected]_search_paths_add_plugin_paths() {
local plugin_path

if [[ "${plugin_paths[0]}" != "$1/*/bin" ]]; then
# Ensure a plugin's _GO_SCRIPTS_DIR isn't duplicated in _GO_PLUGINS_PATHS.
# If more plugins are located under "$1" (ie there are folder under "$1"
# that have a 'bin' folder inside), then the star expansion will fail and
# `*/bin` will be treated as a string. If that does not happen then there
# are plugins under "$1".

# Ensure a plugin's _GO_SCRIPTS_DIRS isn't duplicated in _GO_PLUGINS_PATHS.
for plugin_path in "${plugin_paths[@]}"; do
if [[ "$plugin_path" != "$_GO_SCRIPTS_DIR" ]]; then
local included_path
local found='false'
for included_path in "${_GO_SCRIPTS_DIRS[@]}"; do
if [[ "$plugin_path" == "$included_path" ]]; then
found='true'
fi
done
if [[ "$found" == 'false' ]]; then
_GO_PLUGINS_PATHS+=("$plugin_path")
fi
done
@@ -21,7 +33,7 @@ [email protected]_search_paths() {
if [[ -n "$_GO_INJECT_SEARCH_PATH" ]]; then
_GO_SEARCH_PATHS+=("$_GO_INJECT_SEARCH_PATH")
fi
_GO_SEARCH_PATHS+=("$_GO_CORE_DIR/libexec" "$_GO_SCRIPTS_DIR")
_GO_SEARCH_PATHS+=("$_GO_CORE_DIR/libexec" "${_GO_SCRIPTS_DIRS[@]}")

# A plugin's own local plugin paths will appear before inherited ones. If
# there is a version incompatibility issue with other installed plugins, this
Loading