chain commands, and let a project refine a catalogue stack

myos build up logs host/fabio runs the three in order and stops at the first
failure, the way make build up logs STACK=host/fabio did. Leading words that
name commands are commands; the first word that is not one starts the stacks.

A stack found in several directories of the stack path is now merged rather
than shadowed, least specific first, so a project drops
stack/postgres/postgres.local.yml next to the catalogue's postgres.yml and
refines it. Settings hooks follow the same order, so a project can redefine a
default the catalogue ships. Neither engine did this before: the project
directory simply hid the catalogue one.

An unknown command now says so and suggests the command to type, instead of
printing the whole usage.
This commit is contained in:
Yann Autissier
2026-09-03 21:17:17 +02:00
parent 55fae625d6
commit 3e55cdcd14
15 changed files with 238 additions and 86 deletions
+109 -55
View File
@@ -24,11 +24,22 @@ for _m in core str var tags naming stack config compose hooks; do
. "$MYOS_ROOT/lib/$_m.sh"
done
# myos_is_command WORD true when WORD names a command rather than a stack
myos_is_command() {
case $1 in
up|down|start|stop|restart|ps|logs|config|build|pull|create|kill|top|images) return 0 ;;
version|help) return 0 ;;
print-*|stack-*-*) return 0 ;;
*@*) myos_is_command "${1%@*}"; return $? ;;
esac
[ -f "$MYOS_ROOT/lib/cmd/$1.sh" ]
}
# --- command line ---------------------------------------------------------
# These are read by the lib/cmd/* files sourced further down.
# shellcheck disable=SC2034
{
MYOS_CMD=
MYOS_CMDS=
MYOS_REFS=
MYOS_REFS_RAW=
MYOS_VARS=
@@ -83,39 +94,63 @@ while [ $# -gt 0 ]; do
-h|--help) usage; exit 0 ;;
--) shift; MYOS_ARGS="$*"; break ;;
-*)
if [ -n "$MYOS_CMD" ]; then
if [ -n "$MYOS_CMDS" ]; then
MYOS_ARGS="${MYOS_ARGS:+$MYOS_ARGS }$1"; shift
else
myos_error "unknown option: $1"; usage >&2; exit "$MYOS_E_USAGE"
fi ;;
*=*) eval "${1%%=*}=\${1#*=}"; export "${1%%=*}"; shift ;;
*)
if [ -z "$MYOS_CMD" ]; then MYOS_CMD=$1; else MYOS_REFS="${MYOS_REFS:+$MYOS_REFS }$1"; fi
# Leading words that name commands are commands, the rest are stacks:
# `myos build up logs host/fabio` runs three commands on one stack, the
# way `make build up logs STACK=host/fabio` did.
if [ -z "$MYOS_REFS" ] && myos_is_command "$1"; then
MYOS_CMDS="${MYOS_CMDS:+$MYOS_CMDS }$1"
else
MYOS_REFS="${MYOS_REFS:+$MYOS_REFS }$1"
fi
shift ;;
esac
done
[ -n "$MYOS_CMD" ] || { usage; exit "$MYOS_E_USAGE"; }
if [ -z "$MYOS_CMDS" ]; then
if [ -n "$MYOS_REFS" ]; then
# the first word was meant as a command; name it rather than dump the usage
myos_error "unknown command: ${MYOS_REFS%% *}"
myos_error "to act on a stack of that name, say what to do: myos up ${MYOS_REFS%% *}"
exit "$MYOS_E_USAGE"
fi
usage
exit "$MYOS_E_USAGE"
fi
MYOS_REFS_RAW=$MYOS_REFS
case $MYOS_CMD in
env|ls|doctor) MYOS_VARS=$MYOS_REFS; MYOS_REFS= ;;
esac
# Targets of the make engine keep working: print-VAR, stack-<stack>-<command>,
# <command>@<env> and a bare group name.
case $MYOS_CMD in
print-*)
MYOS_VARS=${MYOS_CMD#print-}
# shellcheck disable=SC2209 # the literal string "env", not the command
MYOS_CMD=env ;;
*@*) ENV=${MYOS_CMD#*@}; MYOS_CMD=${MYOS_CMD%@*} ;;
esac
case $MYOS_CMD in
stack-*-*)
_rest=${MYOS_CMD#stack-}
MYOS_CMD=${_rest##*-}
MYOS_REFS="${_rest%-*} $MYOS_REFS"
MYOS_REFS=${MYOS_REFS% } ;;
# Targets of the make engine keep working: print-VAR, stack-<stack>-<command>
# and <command>@<env> each translate to a command of the CLI.
_cmds=
for _c in $MYOS_CMDS; do
case $_c in
*@*) ENV=${_c#*@}; _c=${_c%@*} ;;
esac
case $_c in
print-*)
MYOS_VARS="${MYOS_VARS:+$MYOS_VARS }${_c#print-}"
# shellcheck disable=SC2209 # the literal string "env", not the command
_c=env ;;
stack-*-*)
_rest=${_c#stack-}
_c=${_rest##*-}
MYOS_REFS="${_rest%-*}${MYOS_REFS:+ $MYOS_REFS}" ;;
esac
_cmds="${_cmds:+$_cmds }$_c"
done
MYOS_CMDS=$_cmds
# env, ls and doctor take variable names where the others take stacks
case $MYOS_CMDS in
env|ls|doctor)
[ -n "$MYOS_VARS" ] || MYOS_VARS=$MYOS_REFS
MYOS_REFS= ;;
esac
# --- configuration --------------------------------------------------------
@@ -178,10 +213,13 @@ if [ -z "$MYOS_REFS" ]; then
fi
fi
if [ -z "$MYOS_REFS" ]; then
case $MYOS_CMD in
env|env-update|ls|doctor|version|help) ;;
*) myos_die "$MYOS_E_USAGE" "no stack given, and no compose file in $WORKDIR" ;;
esac
# these commands describe the installation rather than act on a stack
for _c in $MYOS_CMDS; do
case $_c in
env|env-update|ls|doctor|version|help) ;;
*) myos_die "$MYOS_E_USAGE" "no stack given, and no compose file in $WORKDIR" ;;
esac
done
fi
# shellcheck disable=SC2086 # a list of references
@@ -198,48 +236,64 @@ APP_SCHEME=${APP_SCHEME:-http}
# Per-stack hooks must run in this shell: everything downstream reads the
# variables they set, and a command substitution would throw them away.
for _ref in $MYOS_STACKS; do
_hdir=$(myos_stack_resolve "$_ref" 2>/dev/null) || continue
myos_stack_hooks "$_hdir" "$(myos_stack_name "$_ref")"
for _hdir in $(myos_stack_dirs "$_ref"); do
myos_stack_hooks "$_hdir" "$(myos_stack_name "$_ref")"
done
done
# The two functions below are called from the lib/cmd/* files sourced later,
# which shellcheck cannot see.
# shellcheck disable=SC2329
# myos_framework_compose_files the networks and volumes overlays myos itself
# provides; they always come last so a stack can rely on them being there.
myos_framework_compose_files() {
myos_compose_files "$MYOS_ROOT/share/compose" "networks volumes" "$(myos_compose_suffixes)" "$ENV"
}
# shellcheck disable=SC2329
# myos_stack_compose_files REF the ordered compose files of one reference
myos_stack_compose_files() {
_dir=$(myos_stack_resolve "$1") || return $?
_dirs=$(myos_stack_dirs "$1")
[ -n "$_dirs" ] || { myos_stack_resolve "$1" >/dev/null; return $?; }
_name=$(myos_stack_name "$1")
_suffixes="$(myos_compose_suffixes) $(myos_stack_version "$1")"
case $1 in
.|./*|/*|../*)
myos_compose_files "$_dir" "docker-compose compose" "$_suffixes" "$ENV"
myos_compose_files "$_dir/docker" "docker-compose compose" "$_suffixes" "$ENV" ;;
*)
myos_compose_files "$_dir" "docker-compose $_name" "$_suffixes" "$ENV" ;;
esac
for _dir in $_dirs; do
case $1 in
.|./*|/*|../*)
myos_compose_files "$_dir" "docker-compose compose" "$_suffixes" "$ENV"
myos_compose_files "$_dir/docker" "docker-compose compose" "$_suffixes" "$ENV" ;;
*)
myos_compose_files "$_dir" "docker-compose $_name" "$_suffixes" "$ENV" ;;
esac
done
}
# --- dispatch -------------------------------------------------------------
case $MYOS_CMD in
version) printf 'myos %s\n' "$MYOS_VERSION"; exit 0 ;;
help) usage; exit 0 ;;
esac
# myos_dispatch COMMAND run one command
myos_dispatch() {
case $1 in
version) printf 'myos %s\n' "$MYOS_VERSION"; return 0 ;;
help) usage; return 0 ;;
esac
if [ -f "$MYOS_ROOT/lib/cmd/$1.sh" ]; then
# shellcheck source=/dev/null
. "$MYOS_ROOT/lib/cmd/$1.sh"
"myos_cmd_$(printf '%s' "$1" | tr '-' '_')"
return $?
fi
# commands that map straight onto docker compose
case $1 in
up|down|start|stop|restart|ps|logs|config|build|pull|create|kill|top|images) ;;
*) myos_error "unknown command: $1"; usage >&2; return "$MYOS_E_USAGE" ;;
esac
# shellcheck source=/dev/null
. "$MYOS_ROOT/lib/cmd/_compose.sh"
myos_cmd_compose "$1"
}
# shellcheck source=/dev/null
if [ -f "$MYOS_ROOT/lib/cmd/$MYOS_CMD.sh" ]; then
. "$MYOS_ROOT/lib/cmd/$MYOS_CMD.sh"
"myos_cmd_$(printf '%s' "$MYOS_CMD" | tr '-' '_')"
exit $?
fi
# commands that map straight onto docker compose
case $MYOS_CMD in
up|down|start|stop|restart|ps|logs|config|build|pull|create|kill|top|images) ;;
*) myos_error "unknown command: $MYOS_CMD"; usage >&2; exit "$MYOS_E_USAGE" ;;
esac
. "$MYOS_ROOT/lib/cmd/_compose.sh"
myos_cmd_compose "$MYOS_CMD"
# Several commands run in order and stop at the first failure, as make did.
_rc=0
for MYOS_CMD in $MYOS_CMDS; do
myos_dispatch "$MYOS_CMD" || { _rc=$?; break; }
done
exit "$_rc"