Skip to content

Commit 8751045

Browse files
committed
Multi-service commands: empty != default/all
1 parent d39633e commit 8751045

File tree

4 files changed

+88
-56
lines changed

4 files changed

+88
-56
lines changed

Compose.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,15 @@ doco.version() { docker-compose version "$@"; }
124124

125125
#### Multi-Service Subcommands
126126

127-
Subcommands that accept multiple services get any services in the current service set appended to the command line. (The service set is empty by default, causing docker-compose to apply commands to all services by default.)
127+
Subcommands that accept multiple services get any services in the current service set appended to the command line. (The service set is empty by default, causing docker-compose to apply commands to all services by default.) If any targets have been explicitly specified, there must be at least one service in the current set.
128128

129129
```shell
130130
# Commands that accept services
131131
compose-targeted() {
132-
any-target @current || true # ok if none are set
132+
if any-target @current; then
133+
# Non-default target; make sure it's not empty
134+
with-targets "${REPLY[@]}" -- require-services + "${DOCO_COMMAND:-$1}" || return
135+
fi
133136
compose "$@" "${REPLY[@]}"
134137
}
135138
```

bin/doco

Lines changed: 70 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -600,8 +600,8 @@ doco-target::__create() {
600600
event resolve "created $1 $TARGET_NAME" "$TARGET_NAME"
601601
}
602602
603-
doco-target::get() { REPLY=("${TARGET[@]}"); this "${1-exists}" "${@:2}"; }
604-
doco-target::has-count() { REPLY=("${TARGET[@]}"); eval "(( ${#REPLY[@]} ${1-} ))"; }
603+
doco-target::get() { REPLY=("${TARGET[@]}"); this "$@"; }
604+
doco-target::has-count() { this get; eval "(( ${#REPLY[@]} ${1-} ))"; }
605605
606606
doco-target::readonly() { readonly "${TARGET_VAR}"; this "$@"; }
607607
@@ -617,23 +617,38 @@ doco-target::set() {
617617
618618
doco-target::set-default() { this exists || this set "$@"; }
619619
620+
doco-target::foreach() {
621+
this get; for REPLY in "${REPLY[@]}"; do with-targets "$REPLY" -- "$@"; done
622+
}
620623
@current-target::set() { fail "@current group is read-only"; }
621624
@current-target::declare-service() { fail "@current is a group, but a service was expected"; }
622625
@current-target::declare-group() { :; }
623626
627+
@current-target::exists() { [[ ${HAVE_SERVICES-} ]]; }
628+
@current-target::get() {
629+
REPLY=(); [[ ! ${HAVE_SERVICES-} ]] || REPLY=("${TARGET[@]}"); this "$@"
630+
}
631+
624632
with-targets() {
625633
local s=(); while (($#)) && [[ $1 != -- ]]; do s+=("$1"); shift; done
626634
all-targets "${s[@]}" || return
627-
# oh bash, why do you hate us so...
628-
local DOCO_SERVICES; DOCO_SERVICES=("${REPLY[@]}"); readonly DOCO_SERVICES
629-
"${@:2}"
635+
__apply-targets = "${REPLY[@]}" -- "${@:2}"
630636
}
631637
638+
without-targets() { __apply-targets '' -- "$@"; }
639+
640+
__apply-targets() {
641+
local HAVE_SERVICES=$1 DOCO_SERVICES DOCO_COMMAND
642+
DOCO_SERVICES=(); shift
643+
while (($#)) && [[ $1 != -- ]]; do DOCO_SERVICES+=("$1"); shift; done
644+
readonly DOCO_SERVICES
645+
"${@:2}"
646+
}
632647
# set REPLY to merge of all given target names
633648
all-targets() {
634649
local services=()
635650
while (($#)); do
636-
target "$1" get || [[ $1 == @current ]] ||
651+
target "$1" get exists || [[ $1 == @current ]] ||
637652
fail "'$1' is not a known group or service" || return
638653
for REPLY in "${REPLY[@]}"; do
639654
[[ " ${services[*]-} " == *" $REPLY "* ]] || services+=("$REPLY")
@@ -646,16 +661,12 @@ all-targets() {
646661
# set REPLY to contents of the first existing target
647662
any-target() {
648663
for REPLY; do
649-
if target "$REPLY" get; then return ; fi
664+
if target "$REPLY" get exists; then return ; fi
650665
done
651666
REPLY=(); false
652667
}
653668
654-
foreach-service() {
655-
for REPLY in ${DOCO_SERVICES[@]+"${DOCO_SERVICES[@]}"}; do
656-
with-targets "$REPLY" -- "$@"
657-
done
658-
}
669+
foreach-service() { target @current foreach "$@"; }
659670
have-services() { target @current has-count "$@"; }
660671
project-name() {
661672
REPLY=${COMPOSE_PROJECT_NAME-}
@@ -664,12 +675,16 @@ project-name() {
664675
! (($#)) || REPLY=$REPLY"_${1}_${2-1}" # container name
665676
}
666677
require-services() {
667-
local REPLY=("${DOCO_SERVICES[@]}")
668-
case "$1${#REPLY[@]}" in
669-
?1|-0|.*) return ;; # 1 is always acceptable
670-
?0) fail "no services specified for $2" ;;
671-
[-1]*) fail "$2 cannot be used on multiple services" ;;
672-
esac
678+
[[ ${1-} == [-+1.] ]] ||
679+
fail "require-services first argument must be ., -, +, or 1" || return
680+
(($#>1)) || set -- "$1" "${DOCO_COMMAND:-the current command}"
681+
(($#>2)) || set -- "$1" "$2" @current
682+
any-target "${@:3}" || true
683+
case "$1${#REPLY[@]}" in
684+
?1|-0|.*) return ;; # 1 is always acceptable
685+
?0) fail "no services specified for $2" ;;
686+
[-1]*) fail "$2 cannot be used on multiple services" ;;
687+
esac
673688
}
674689
services-matching() {
675690
REPLY=()
@@ -703,7 +718,7 @@ generate-jq-func() {
703718
}
704719
ALIAS() { mdsh-splitwords "$1"; GROUP "${REPLY[@]}" += "${@:2}"; }
705720
alias-exists() { target "$1" exists; }
706-
get-alias() { target "$1" get || true; }
721+
get-alias() { target "$1" get; }
707722
set-alias() { target "$1" set "$@"; }
708723
with-alias() { target "$1" call "${@:2}"; }
709724
with-service() { mdsh-splitwords "$1"; with-targets @current "${REPLY[@]}" -- "${@:2}"; }
@@ -782,7 +797,10 @@ doco.version() { docker-compose version "$@"; }
782797
783798
# Commands that accept services
784799
compose-targeted() {
785-
any-target @current || true # ok if none are set
800+
if any-target @current; then
801+
# Non-default target; make sure it's not empty
802+
with-targets "${REPLY[@]}" -- require-services + "${DOCO_COMMAND:-$1}" || return
803+
fi
786804
compose "$@" "${REPLY[@]}"
787805
}
788806
# Commands that don't accept a list of services
@@ -800,36 +818,33 @@ compose-singular() {
800818
argv+=("$1"); if [[ $1 =~ $opts ]]; then shift; argv+=("$1"); fi
801819
done
802820
803-
if ! any-target @current || ! ((${#REPLY[@]})) ; then
804-
# no current or default target, check command line for one
805-
if ((! $#)) || ! target "$1" get; then
806-
fail "No service/group specified for $cmd" || return
807-
else
808-
shift # remove the explicit target from the tail
809-
fi
810-
fi
811-
if ((${#REPLY[@]} != 1)); then
812-
with-targets "${REPLY[@]}" -- require-services 1 "$cmd"
813-
else
814-
compose "${argv[@]}" "${REPLY[@]}" "$@"
821+
if ! any-target @current; then
822+
# no current or default target, check command line for one and remove it
823+
if is-target-name "${1-}" && target "$1" get exists; then shift; fi
815824
fi
825+
826+
with-targets "${REPLY[@]}" -- require-services 1 "${DOCO_COMMAND:-$cmd}" || return
827+
compose "${argv[@]}" "${REPLY[@]}" "$@"
816828
}
817829
830+
doco_had_args=0 # times doco has been called with arguments
831+
818832
loco_do() {
819833
project-is-finalized ||
820834
fail "doco CLI cannot be used before the project spec is finalized" || return
835+
(( ! $# )) || doco_had_args=1
821836
case ${1-} in
822837
--*=*) doco-optarg "$@" ;; # --[option]=value
823838
--*) doco-option "$@" ;; # --[option]
824839
-[^=]=*) doco-optarg "$@" ;; # -a=bcd
825840
-[^=]?*) doco-options "$@" ;; # -abcd
826841
-?) doco-option "$@" ;; # -x
827-
'') doco-null-command ;; # empty or missing command
842+
'') doco-null "$@" ;; # empty or missing command
828843
*) doco-other "$@" ;; # commands, services, and groups
829844
esac
830845
}
831-
doco-null-command() {
832-
if target "@current" exists; then
846+
doco-null() {
847+
if ((doco_had_args && ! $#)); then
833848
target "@current" get
834849
${REPLY[@]+printf '%s\n' "${REPLY[@]}"} # only output lines if there are some
835850
else
@@ -864,14 +879,17 @@ doco-optarg() {
864879
fi
865880
}
866881
doco-other() {
867-
if fn-exists "doco.$1"; then "doco.$@"
882+
if fn-exists "doco.$1"; then
883+
with-command "${DOCO_COMMAND:-$1}" "doco.$@"
868884
elif is-target-name "$1" && target "$1" exists; then
869885
with-targets @current "$1" -- doco "${@:2}"
870886
else fail "'$1' is not a recognized option, command, service, or group"
871887
fi
872888
}
873-
# Execute the rest of the command line with NO specified service(s)
874-
doco.--() { with-targets -- doco "$@"; }
889+
890+
with-command() { local DOCO_COMMAND=$1; "${@:2}"; }
891+
# Execute the rest of the command line without specified services
892+
doco.--() { without-targets doco "$@"; }
875893
doco.--dry-run() {
876894
docker() { printf -v REPLY ' %q' "docker" "$@"; echo "${REPLY# }"; } >&2
877895
docker-compose() { printf -v REPLY ' %q' "docker-compose" "$@"; echo "${REPLY# }"; } >&2
@@ -887,49 +905,51 @@ function doco.--with=() {
887905
}
888906
function doco.--with-default=() {
889907
if target @current has-count || ! target "$1" exists; then doco "${@:2}"
890-
else with-targets "$1" -- doco "${@:2}"; fi
908+
else with-targets "$1" -- with-command "${DOCO_COMMAND-}" doco "${@:2}"; fi
891909
}
892910
function doco.--require-services=() {
893911
[[ ${1:0:1} == [-+1.] ]] || loco_error "--require-services argument must begin with ., -, +, or 1"
894-
mdsh-splitwords "$1" && require-services "${REPLY[@]}" "${2-}" && doco "${@:2}";
912+
mdsh-splitwords "$1"; ((${#REPLY[@]}>1)) || REPLY+=("${DOCO_COMMAND:-${2-}}")
913+
require-services "${REPLY[@]:0:2}" && doco "${@:2}"
914+
}
915+
doco.cmd() {
916+
[[ ${DOCO_COMMAND-} != cmd ]] || local DOCO_COMMAND=$2
917+
doco --with-default cmd-default --require-services "$@"
895918
}
896-
doco.cmd() { doco --with-default cmd-default --require-services "$@"; }
897919
doco.cp() {
898920
local opts=() seen=''
899921
while (($#)); do
900922
case "$1" in
901923
-a|--archive|-L|--follow-link) opts+=("$1") ;;
902924
--help|-h) docker help cp || true; return ;;
903-
-*) loco_error "Unrecognized option $1; see 'docker help cp'" ;;
925+
-*) fail "Unrecognized option $1; see 'docker help cp'" || return ;;
904926
*) break ;;
905927
esac
906928
shift
907929
done
908-
(($# == 2)) || loco_error "cp requires two non-option arguments (src and dest)"
930+
(($# == 2)) || fail "cp requires two non-option arguments (src and dest)" || return
909931
while (($#)); do
910932
if [[ $1 == *:* ]]; then
911-
[[ ! "$seen" ]] || loco_error "cp: only one argument may contain a :"
933+
[[ ! "$seen" ]] || fail "cp: only one argument may contain a :" || return
912934
seen=yes
913935
if [[ "${1%%:*}" ]]; then
914936
project-name "${1%%:*}"; set -- "$REPLY:${1#*:}" "${@:2}"
915-
elif have-services '==1'; then
916-
project-name "${DOCO_SERVICES[0]}"; set -- "$REPLY$1" "${@:2}"
917937
else
918-
doco --with-default=shell-default --require-services=1 cp "${opts[@]}" "$@"
919-
return
938+
require-services 1 cp @current shell-default || return
939+
project-name "$REPLY"; set -- "$REPLY$1" "${@:2}"
920940
fi
921941
elif [[ $1 != /* && $1 != - ]]; then
922942
# make paths relative to original run directory
923943
set -- "$LOCO_PWD/$1" "${@:2}";
924944
fi
925945
opts+=("$1"); shift
926946
done
927-
[[ "$seen" ]] || loco_error "cp: either source or destination must contain a :"
947+
[[ "$seen" ]] || fail "cp: either source or destination must contain a :" || return
928948
docker cp ${opts[@]+"${opts[@]}"}
929949
}
930950
doco.foreach() { foreach-service doco "$@"; }
931951
doco.jq() { RUN_JQ "$@" <"$DOCO_CONFIG"; }
932-
doco.sh() { doco cmd 1 exec bash "$@"; }
952+
doco.sh() { doco --with-default cmd-default exec bash "$@"; }
933953
DEFINE "${mdsh_raw_jq_api[*]}"
934954
935955
# shellcheck disable=SC2059 # argument is a printf format string

specs/CLI-Options.cram.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ Add services matching *jq-filter* to the current service set for the remainder o
3737
example1
3838
$ doco --where false
3939
$ doco --where=false ps
40-
docker-compose ps
40+
no services specified for ps
41+
[64]
4142
$ doco --where=true ps
4243
docker-compose ps example1
4344
~~~

specs/Compose-Integration.cram.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,23 @@
1919

2020
#### Multi-Service Subcommands
2121

22-
Subcommands that accept multiple services get any services in the current service set appended to the command line. (The service set is empty by default, causing docker-compose to apply commands to all services by default.)
22+
Subcommands that accept multiple services get any services in the current service set appended to the command line. (The service set is empty by default, causing docker-compose to apply commands to all services by default.) If any targets have been explicitly specified, there must be at least one service in the current set.
2323

2424
```shell
2525
$ doco foo2
2626
'foo2' is not a recognized option, command, service, or group
2727
[64]
28-
$ (GROUP foo2 += bar; doco foo2 ps)
28+
29+
$ GROUP foo2 :=
30+
$ doco foo2 ps
31+
no services specified for ps
32+
[64]
33+
34+
$ GROUP foo2 += bar
35+
$ doco foo2 ps
2936
docker-compose ps bar
30-
$ (GROUP foo2 += bar; doco foo2 bar)
37+
38+
$ doco foo2 bar
3139
bar
3240
```
3341

0 commit comments

Comments
 (0)