Skip to content

Commit 7cbb239

Browse files
committed
core: Extract @go.search_plugins
Part of #120. This implements semantics similar to npm's `node_modules` by providing a parent directory-recursive plugin search. After implementing #130 and coming back to my original `@go.find_plugin_item_path` that I'd had in another branch, I realized the parent-recursive search could be abstracted into `@go.search_plugins`. Ironically, it was implementing the algorithm first in `_@go.set_search_paths` than enabled me to more thoroughly test the extracted function, since plugin scope was already established and tested. Talk about circular dependencies! As with the earlier commits building up #120, the implication is that rather than having all plugins comprising an application always checkout their own plugins in their own subtree, eventually they can be installed in their parent's plugin directory. This nesting of plugins can be arbitrarily deep, ending at the top-level `_GO_PLUGINS_DIR`. This will help break cyclical dependencies (though they should still be avoided) and save disk space, and paves the way for semantic versioning and `npm`-like functionality (but in pure Bash, modulo `git` or `curl`, et. al, via `./go get`).
1 parent 60f2ec6 commit 7cbb239

File tree

3 files changed

+212
-17
lines changed

3 files changed

+212
-17
lines changed

go-core.bash

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,10 @@ declare -x _GO_CMD_NAME=
114114
# string with the arguments delimited by the ASCII Unit Separator ($'\x1f').
115115
declare -x _GO_CMD_ARGV=
116116

117-
# The directory in which plugins are installed.
117+
# The top-level directory in which plugins are installed.
118+
#
119+
# If a command script is running as a plugin, this value will be the plugins
120+
# directory of the top-level `./go` script.
118121
declare _GO_PLUGINS_DIR=
119122

120123
# Directories containing executable plugin scripts.
@@ -204,6 +207,52 @@ declare _GO_INJECT_MODULE_PATH="$_GO_INJECT_MODULE_PATH"
204207
return "$result"
205208
}
206209

210+
# Searches through plugin directories using a helper function
211+
#
212+
# The search will begin in `_GO_SCRIPTS_DIR/plugins`. As long as `search_func`
213+
# returns nonzero, every parent `/plugins/` directory will be searched, up to
214+
# and including the top-level `_GO_PLUGINS_DIR`. The search will end either when
215+
# `search_func` returns zero, or when all of the plugin paths are exhausted.
216+
#
217+
# The helper function, `search_func`, will receive the current plugin directory
218+
# being searched as its sole argument. The `@go.search_plugins` caller's
219+
# variables will be available in its scope. It should return zero when the
220+
# search criteria are satisfied, nonzero if the search should continue.
221+
#
222+
# For example, to search for a particular item in a particular plugin:
223+
#
224+
# find_plugin_item() {
225+
# [[ -e "$1/item_path" ]] && item_path="$1/item_path"
226+
# }
227+
#
228+
# my_func() {
229+
# local item_path='foo/bar'
230+
# if @go.search_plugins find_plugin_item; then
231+
# # Do something with $item_path
232+
# fi
233+
# }
234+
#
235+
# Arguments:
236+
# search_func: Helper function implementing the search operation
237+
#
238+
# Returns:
239+
# Zero if `search_func` ever returns zero, nonzero otherwise
240+
@go.search_plugins() {
241+
local __gsp_plugins_dir="$_GO_SCRIPTS_DIR/plugins"
242+
243+
# Set `_GO_PLUGINS_DIR` if called from a top-level `./go` script before `@go`.
244+
_GO_PLUGINS_DIR="${_GO_PLUGINS_DIR:-$_GO_SCRIPTS_DIR/plugins}"
245+
246+
while true; do
247+
if "$1" "$__gsp_plugins_dir"; then
248+
return
249+
elif [[ "$__gsp_plugins_dir" == "$_GO_PLUGINS_DIR" ]]; then
250+
return 1
251+
fi
252+
__gsp_plugins_dir="${__gsp_plugins_dir%/plugins/*}/plugins"
253+
done
254+
}
255+
207256
# Main driver of ./go script functionality.
208257
#
209258
# Arguments:

lib/internal/path

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
#! /bin/bash
22

3+
_@go.set_search_paths_add_plugin_paths() {
4+
local plugin_paths=("$1"/*/bin)
5+
if [[ "${plugin_paths[0]}" != "$1/*/bin" ]]; then
6+
_GO_PLUGINS_PATHS+=("${plugin_paths[@]}")
7+
fi
8+
return 1
9+
}
10+
311
_@go.set_search_paths() {
4-
local plugins_dir="$_GO_SCRIPTS_DIR/plugins"
5-
local plugins_paths=()
612
local plugin_path
713

814
if [[ -n "$_GO_INJECT_SEARCH_PATH" ]]; then
@@ -13,21 +19,9 @@ _@go.set_search_paths() {
1319
# A plugin's own local plugin paths will appear before inherited ones. If
1420
# there is a version incompatibility issue with other installed plugins, this
1521
# allows a plugin's preferred version to take precedence.
16-
while [[ "$plugins_dir" =~ ^$_GO_PLUGINS_DIR ]]; do
17-
plugin_paths=("$plugins_dir"/*/bin)
18-
if [[ "${plugin_paths[0]}" != "$plugins_dir/*/bin" ]]; then
19-
_GO_PLUGINS_PATHS+=("${plugin_paths[@]}")
20-
fi
21-
if [[ "$plugins_dir" == "$_GO_PLUGINS_DIR" ]]; then
22-
break
23-
fi
24-
plugins_dir="${plugins_dir%/plugins/*}/plugins"
25-
done
22+
@go.search_plugins '_@go.set_search_paths_add_plugin_paths'
2623

27-
# A plugin's _GO_SCRIPTS_DIR may continue to appear in _GO_PLUGINS_PATHS so
28-
# that it's available to other plugins that depend upon it as a circular
29-
# dependency (though such dependencies are strongly discouraged). However, we
30-
# will ensure its _GO_SCRIPTS_DIR isn't duplicated in _GO_SEARCH_PATHS.
24+
# Ensure a plugin's _GO_SCRIPTS_DIR isn't duplicated in _GO_SEARCH_PATHS.
3125
for plugin_path in "${_GO_PLUGINS_PATHS[@]}"; do
3226
if [[ "$plugin_path" != "$_GO_SCRIPTS_DIR" ]]; then
3327
_GO_SEARCH_PATHS+=("$plugin_path")

tests/core/search-plugins.bats

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#! /usr/bin/env bats
2+
3+
load ../environment
4+
5+
setup() {
6+
test_filter
7+
@go.create_test_go_script \
8+
'collect_dirs_impl() {' \
9+
' [[ -d "$1" ]] && dirs_searched+=("$1")' \
10+
' [[ "$((--COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS))" -eq "0" ]]' \
11+
'}' \
12+
'collect_dirs() {' \
13+
' local dirs_searched=()' \
14+
' @go.search_plugins collect_dirs_impl' \
15+
' local result="$?"' \
16+
' printf "%s\n" "${dirs_searched[@]}"' \
17+
' return "$result"' \
18+
'}' \
19+
'if [[ -z "$COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS" ]]; then' \
20+
' COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS=1' \
21+
'fi' \
22+
'@go "$@"'
23+
}
24+
25+
teardown() {
26+
@go.remove_test_go_rootdir
27+
}
28+
29+
@test "$SUITE: _GO_PLUGINS_DIR doesn't exist" {
30+
@go.create_test_command_script 'top-level' 'collect_dirs'
31+
run "$TEST_GO_SCRIPT" 'top-level'
32+
assert_success ''
33+
}
34+
35+
@test "$SUITE: _GO_PLUGINS_DIR exists" {
36+
mkdir -p "$TEST_GO_PLUGINS_DIR"
37+
@go.create_test_command_script 'top-level' 'collect_dirs'
38+
run "$TEST_GO_SCRIPT" 'top-level'
39+
assert_success "$TEST_GO_PLUGINS_DIR"
40+
}
41+
42+
@test "$SUITE: _GO_PLUGINS_DIR exists, search returns failure" {
43+
mkdir -p "$TEST_GO_PLUGINS_DIR"
44+
@go.create_test_command_script 'top' 'collect_dirs'
45+
COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS='0' run "$TEST_GO_SCRIPT" 'top'
46+
assert_failure "$TEST_GO_PLUGINS_DIR"
47+
}
48+
49+
@test "$SUITE: plugin without plugins dir finds _GO_PLUGINS_DIR" {
50+
@go.create_test_command_script 'plugins/foo/bin/foo' 'collect_dirs'
51+
COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS='2' run "$TEST_GO_SCRIPT" 'foo'
52+
assert_success "$TEST_GO_PLUGINS_DIR"
53+
}
54+
55+
@test "$SUITE: plugin with plugins dir finds both plugins dirs" {
56+
mkdir -p "$TEST_GO_PLUGINS_DIR/foo/bin/plugins"
57+
@go.create_test_command_script 'plugins/foo/bin/foo' 'collect_dirs'
58+
COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS='2' run "$TEST_GO_SCRIPT" 'foo'
59+
assert_success "$TEST_GO_PLUGINS_DIR/foo/bin/plugins" \
60+
"$TEST_GO_PLUGINS_DIR"
61+
}
62+
63+
@test "$SUITE: plugin with plugins dir, return success after first dir" {
64+
mkdir -p "$TEST_GO_PLUGINS_DIR/foo/bin/plugins"
65+
@go.create_test_command_script 'plugins/foo/bin/foo' 'collect_dirs'
66+
COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS='1' run "$TEST_GO_SCRIPT" 'foo'
67+
assert_success "$TEST_GO_PLUGINS_DIR/foo/bin/plugins"
68+
}
69+
70+
@test "$SUITE: plugin finds both plugins dirs, returns failure" {
71+
mkdir -p "$TEST_GO_PLUGINS_DIR/foo/bin/plugins"
72+
@go.create_test_command_script 'plugins/foo/bin/foo' 'collect_dirs'
73+
COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS='3' run "$TEST_GO_SCRIPT" 'foo'
74+
assert_failure "$TEST_GO_PLUGINS_DIR/foo/bin/plugins" \
75+
"$TEST_GO_PLUGINS_DIR"
76+
}
77+
78+
@test "$SUITE: nested_plugin without plugins dir finds parent dirs" {
79+
mkdir -p "$TEST_GO_PLUGINS_DIR/foo/bin/plugins/bar/bin"
80+
@go.create_test_command_script 'plugins/foo/bin/foo' '@go bar'
81+
@go.create_test_command_script 'plugins/foo/bin/plugins/bar/bin/bar' \
82+
'collect_dirs'
83+
COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS='3' run "$TEST_GO_SCRIPT" 'foo'
84+
assert_success "$TEST_GO_PLUGINS_DIR/foo/bin/plugins" \
85+
"$TEST_GO_PLUGINS_DIR"
86+
}
87+
88+
@test "$SUITE: nested_plugin with plugins dir finds all plugin dirs" {
89+
mkdir -p "$TEST_GO_PLUGINS_DIR/foo/bin/plugins/bar/bin/plugins"
90+
@go.create_test_command_script 'plugins/foo/bin/foo' '@go bar'
91+
@go.create_test_command_script 'plugins/foo/bin/plugins/bar/bin/bar' \
92+
'collect_dirs'
93+
COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS='3' run "$TEST_GO_SCRIPT" 'foo'
94+
assert_success "$TEST_GO_PLUGINS_DIR/foo/bin/plugins/bar/bin/plugins" \
95+
"$TEST_GO_PLUGINS_DIR/foo/bin/plugins" \
96+
"$TEST_GO_PLUGINS_DIR"
97+
}
98+
99+
@test "$SUITE: nested_plugin stops after parent plugin dir" {
100+
@go.create_test_command_script 'plugins/foo/bin/foo' '@go bar'
101+
@go.create_test_command_script 'plugins/foo/bin/plugins/bar/bin/bar' \
102+
'collect_dirs'
103+
COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS='2' run "$TEST_GO_SCRIPT" 'foo'
104+
# Note it doesn't have its own plugin dir this time.
105+
assert_success "$TEST_GO_PLUGINS_DIR/foo/bin/plugins"
106+
}
107+
108+
@test "$SUITE: nested_plugin with plugins dir finds all dirs, returns failure" {
109+
mkdir -p "$TEST_GO_PLUGINS_DIR/foo/bin/plugins/bar/bin/plugins"
110+
@go.create_test_command_script 'plugins/foo/bin/foo' '@go bar'
111+
@go.create_test_command_script 'plugins/foo/bin/plugins/bar/bin/bar' \
112+
'collect_dirs'
113+
COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS='4' run "$TEST_GO_SCRIPT" 'foo'
114+
assert_failure "$TEST_GO_PLUGINS_DIR/foo/bin/plugins/bar/bin/plugins" \
115+
"$TEST_GO_PLUGINS_DIR/foo/bin/plugins" \
116+
"$TEST_GO_PLUGINS_DIR"
117+
}
118+
119+
@test "$SUITE: /plugins/ in _GO_ROOTDIR, _GO_SCRIPTS_DIR (pathological)" {
120+
local test_rootdir="$TEST_GO_ROOTDIR/plugins/plugins"
121+
local test_go_script="$test_rootdir/go"
122+
local test_scripts_dir="$test_rootdir/plugins"
123+
local test_plugins_dir="$test_scripts_dir/plugins"
124+
mkdir -p "$test_plugins_dir/foo/bin/plugins/bar/bin/plugins"
125+
126+
local line
127+
while IFS= read -r line; do
128+
line="${line%$'\r'}"
129+
if [[ "$line" =~ go-core\.bash ]]; then
130+
line=". '$_GO_CORE_DIR/go-core.bash' 'plugins'"
131+
fi
132+
printf '%s\n' "$line" >> "$test_go_script"
133+
done < "$TEST_GO_SCRIPT"
134+
chmod 700 "$test_go_script"
135+
136+
local foo_path="${test_plugins_dir#$BATS_TEST_ROOTDIR/}/foo/bin/foo"
137+
local bar_path="${foo_path%/*}/plugins/bar/bin/bar"
138+
139+
# We can't use `@go.create_test_command_script` since we can't change the
140+
# readonly `TEST_GO_*` variables.
141+
create_bats_test_script "$foo_path" '@go bar'
142+
create_bats_test_script "$bar_path" 'collect_dirs'
143+
144+
test_printf 'test_go_script: %s\n' "$test_go_script" >&2
145+
test_printf 'test_plugins_dir: %s\n' "$test_plugins_dir" >&2
146+
test_printf 'foo_path: %s\n' "${foo_path}" >&2
147+
test_printf 'bar_path: %s\n' "${bar_path}" >&2
148+
COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS='10' run "$test_go_script" 'foo'
149+
assert_failure "$test_plugins_dir/foo/bin/plugins/bar/bin/plugins" \
150+
"$test_plugins_dir/foo/bin/plugins" \
151+
"$test_plugins_dir"
152+
}

0 commit comments

Comments
 (0)