feat: accept stack folders as stack arguments

Folder references like qbittorrent/ or ./qbittorrent/ now select the
same stack as the bare name, which matches shell tab completion.
Paths are resolved against the stacks root and must point at a stack
folder inside STACKS_DIR; folders elsewhere are rejected before any
Docker command runs.
This commit is contained in:
lingkai5wu committed 2026-10-15 13:04:54 +08:00
1 parent d0d254cd1d
commit c002ec3244
1 file changed
+49 -1
+49 -1
View File
@@ -71,6 +71,10 @@ usage() {
cat <<EOF
Usage: $(basename "$0") COMMAND [STACK]
STACK may be a plain stack name or a stack folder such as qbittorrent/
or ./qbittorrent/. Folder references resolve against the stacks root;
paths outside it are rejected.
Lifecycle:
up [STACK] Create and start containers
start [STACK] Start existing containers
@@ -155,6 +159,42 @@ validate_name() {
die "invalid $kind name: $value"
}
# Rewrite a stack argument to its bare stack name in place. Plain names
# pass through unchanged. A folder reference such as ./stack/ or an
# absolute path must resolve, after following symlinks, to the stack
# folder inside STACKS_DIR; paths to folders elsewhere are rejected.
normalize_target() {
local target_name="$1"
local -n target="$target_name"
local original="$target"
local name path resolved canonical root
while [[ "$target" == */ ]]; do
target="${target%/}"
done
# Names without a path component are validated against the stacks.
if [[ "$target" != */* ]]; then
[[ -n "$target" ]] || die "invalid stack path: $original"
return 0
fi
name="${target##*/}"
[[ -n "$name" ]] || die "invalid stack path: $original"
root="$(readlink -f -- "$STACKS_DIR" 2>/dev/null)" ||
die "invalid STACKS_DIR: $STACKS_DIR"
path="$target"
[[ "$path" == /* ]] || path="$STACKS_DIR/$path"
resolved="$(readlink -f -- "$path" 2>/dev/null)" ||
die "stack folder not found: $original"
[[ -d "$resolved" ]] || die "stack folder not found: $original"
canonical="$(readlink -f -- "$root/$name" 2>/dev/null)"
[[ "$resolved" == "$canonical" ]] ||
die "not a stack folder in $root: $original"
target="$name"
}
# Set the maximum concurrency within each stage.
max_parallel() {
[[ $# -eq 1 ]] || die "max_parallel expects exactly one value"
@@ -170,6 +210,7 @@ add_stage() {
local stack
local -n stages="$stages_name"
local -n limits="$limits_name"
local -a normalized=()
shift 3
if [[ "${1:-}" == "--parallel" ]]; then
@@ -182,15 +223,17 @@ add_stage() {
[[ $# -ge 1 ]] || die "$stage expects at least one stack"
for stack in "$@"; do
normalize_target stack
validate_name "stack" "$stack"
contains "$stack" "${CONFIGURED_STACKS[@]}" &&
die "stack is assigned more than once: $stack"
contains "$stack" "${EXCLUDED_STACKS[@]}" &&
die "stack is both assigned and excluded: $stack"
CONFIGURED_STACKS+=("$stack")
normalized+=("$stack")
done
stages+=("$*")
stages+=("${normalized[*]}")
limits+=("$limit")
}
@@ -203,6 +246,7 @@ exclude() {
local stack
for stack in "$@"; do
normalize_target stack
validate_name "stack" "$stack"
contains "$stack" "${EXCLUDED_STACKS[@]}" &&
die "stack is excluded more than once: $stack"
@@ -707,6 +751,10 @@ main() {
;;
esac
if [[ -n "$TARGET" ]]; then
normalize_target TARGET
fi
load_config
discover_stacks
validate_config_stacks