• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dynamotn / dybatpho / 16619628884

30 Jul 2025 10:07AM UTC coverage: 84.588% (-0.9%) from 85.451%
16619628884

push

github

dynamotn
test: correct test case name

472 of 558 relevant lines covered (84.59%)

42.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

63.55
/src/cli.sh
1
#!/usr/bin/env bash
2
# @file cli.sh
3
# @brief Utilities for getting options when calling command from CLI or in script with CLI-like format
4
# @description
5
#   This module contains functions to define, get options (flags, parameters...) for command or subcommand
6
#   when calling it from CLI or in shell script.
7
#
8
# Theses are type of function arguments that defined in this file
9
#
10
# |Type|Description|
11
# |----|-----------|
12
# |`switch`|A type as a string with format `-?`, `--*`, `--no-*`, `--with-*`, `--without-*`, `--{no-}*` (expand to use both `--flag` and `--no-flag`), `--with{out}-*` (expand to `--with-flag` and `--without-flag`)|
13
# |`key:value`|`key1:value1` style arguments, if `:value` is omitted, it is the same as `key:key`|
14
#
15
# ### Key-value type
16
# |Format|Description|
17
# |------|-----------|
18
# |`action:<code>`|List of multiple statements, split by `;` as `key:value`, eg `"action:foo; bar"`|
19
# |`init:<method>`|Method to initial value of variable from spec by variable name with key `init:`, used for `dybatpho::opts::flag` and `dybatpho::opts::param`, see `Initial variable kind` below|
20
# |`on:<string>`|The positive value whether option is switch as `--flag`, `--with-flag`, default is `"true"`, used for `dybatpho::opts::flag` and `dybatpho::opts::param`|
21
# |`off:<string>`|The negative value whether option is not presence, or as `--no-flag`, `--without-flag`, default is empty `''`, used for `dybatpho::opts::flag` and `dybatpho::opts::param`|
22
# |`export:<bool>`|Export variable in spec command or not, default is true, used for `dybatpho::opts::flag` and `dybatpho::opts::param`|
23
# |`optional:<bool>`|Used for `dybatpho::opts::param` whether option is optional, default is false (restrict)|
24
# |`validate:<code>`|Validate statements for options, eg: `"_function1 \$OPTARG"` (must have `\$OPTARG` to pass param value of option), used for `dybatpho::opts::flag` and `dybatpho::opts::param`|
25
# |`error:<code>`|Custom error messages function for options, eg: `"_show_error1"`,  used for `dybatpho::opts::flag` and `dybatpho::opts::param`|
26
#
27
# ### Initial variable kind
28
# |Format|Description|
29
# |------|-----------|
30
# |`init:@empty`|Initial value as empty. It's default behavior|
31
# |`init:@on`|Initial value with same as `on` key|
32
# |`init:@off`|Initial value with same as `off` key|
33
# |`init:@unset`|Unset the variable|
34
# |`init:@keep`|Do not initialization (Use the current value as it is)|
35
# |`init:action:<code>`|Initialize by run statement(s) and not assigned to variable|
36
# |`init:=<code>`| Initialize by plain code and assigned to variable|
37
: "${DYBATPHO_DIR:?DYBATPHO_DIR must be set. Please source dybatpho/init.sh before other scripts from dybatpho.}"
208✔
38

39
DYBATPHO_CLI_DEBUG="${DYBATPHO_CLI_DEBUG:-false}"
208✔
40

41
# @section Internal functions
42
# @description Functions are triggered by `dybatpho::generate_from_spec`
43

44
#######################################
45
# @description Parse options with a spec from `dybatpho::opts::flag`,
46
#              `dybatpho::opts::param`
47
# @arg $1 bool Flag that defined option that take argument in spec
48
# @arg $@ string Passed arguments from `dybatpho::opts::(flag|param)`
49
# @exitcode 0
50
#######################################
51
function __parse_opt {
52
  if dybatpho::is false "${__done_initial}"; then
4✔
53
    local need_argument=$1 && shift
4✔
54
    __on="true" __off="" __init="@empty" __export="true"
8✔
55
    while dybatpho::still_has_args "$@" && shift; do
10✔
56
      case $1 in
4✔
57
        [!-]*) __parse_key_value "$1" "__" ;;
2✔
58
        -?)
59
          dybatpho::is true "${need_argument}" \
2✔
60
            && __params="${__params}${1#-}" \
61
            || __flags="${__flags}${1#-}"
62
          ;;
63
      esac
64
    done
65
  else
66
    __validate="" __on="true" __off="" __export="true" __optional="false" __switch=""
12✔
67
    shift # ignore flag $1
2✔
68
    while dybatpho::still_has_args "$@" && shift; do
10✔
69
      case $1 in
4✔
70
        --\{no-\}*)
71
          i=${1#--?no-?}
×
72
          __add_switch "'--${i}'|'--no-${i}'"
×
73
          ;;
74
        --with\{out\}-*)
75
          i=${1#--*-}
×
76
          __add_switch "'--with-${i}'|'--without-${i}'"
×
77
          ;;
78
        -? | --*) __add_switch "'$1'" ;;
2✔
79
        *) __parse_key_value "$1" "__" ;;
2✔
80
      esac
81
    done
82
    __assign_quoted __on "${__on}"
2✔
83
    __assign_quoted __off "${__off}"
2✔
84
  fi
85
}
86

87
#######################################
88
# @description Write script with indentation to stdout
89
# @arg $1 number Number of indentation level
90
# @arg $@ string Line of code to generate
91
# @stdout Generated code
92
# @exitcode 0
93
#######################################
94
function __print_indent {
95
  local indent=$1
252✔
96
  shift
252✔
97
  for ((i = indent; i > 0; i--)); do
1,866✔
98
    echo -n "  "
681✔
99
  done
100
  echo "$@"
252✔
101
}
102

103
#######################################
104
# @description Assign the quoted string to a variable
105
# @arg $1 string Variable name to be assigned
106
# @arg $2 string Input string to be quoted
107
# @exitcode 0
108
#######################################
109
function __assign_quoted {
110
  local quote="$2'" result=""
4✔
111
  while [ "${quote}" ]; do
8✔
112
    result="${result}${quote%%\'*}'\''" && quote=${quote#*\'}
8✔
113
  done
114
  quote="'${result%????}'" && quote=${quote#\'\'} && quote=${quote%\'\'}
12✔
115
  eval "$1=\${quote:-\"''\"}"
8✔
116
}
117

118
#######################################
119
# @description Prepend export of before string of command,
120
#              based on `export:<bool>` switch
121
# @arg $1 string String of command
122
#######################################
123
function __prepend_export {
124
  echo "$(dybatpho::is true "${__export}" && echo "export ")$1"
27✔
125
}
126

127
#######################################
128
# @description Define variable from spec from `dybatpho::opts::flag`,
129
#              `dybatpho::opts::param`
130
# @arg $1 string Name of variable to be defined
131
#######################################
132
function __define_var {
133
  case ${__init} in
14✔
134
    @keep) : ;;
×
135
    @empty) __print_indent 0 "$(__prepend_export "$1=''")" ;;
14✔
136
    @unset) __print_indent 0 "unset $1 ||:" ;;
×
137
    *)
138
      case ${__init} in @on) __init=${__on} ;; esac
×
139
      case ${__init} in @off) __init=${__off} ;; esac
×
140
      case ${__init} in =*)
141
        __print_indent 0 "$(__prepend_export "$1${__init}")"
×
142
        return 0
×
143
        ;;
144
      esac
145
      case ${__init} in action:*)
146
        local action=""
×
147
        __parse_key_value "${__init#init:}"
×
148
        __print_indent 0 "${action}"
×
149
        return 0
×
150
        ;;
151
      esac
152
      __assign_quoted __init "${__init#=}"
×
153
      __print_indent 0 "$(__prepend_export "$1=${__init}")"
×
154
      ;;
155
  esac
156
}
157

158
#######################################
159
# @description Extract key value from spec with format `x:y`,
160
#              to get settings of option
161
# @arg $1 key:value Key-value string to extract
162
# @arg $2 string Prefix of key to assign as variable
163
#######################################
164
function __parse_key_value() {
165
  eval "${2-}${1%%:*}=\${1#*:}"
14✔
166
}
167

168
# shellcheck disable=2016
169
#######################################
170
# @description Generate logic from spec of script/function to get options
171
# @arg $1 string Name of function that has spec of parent function or script
172
# @arg $2 string Command of spec (`-` for root command trigger from CLI, otherwise is sub-command)
173
# @stdout Generated logic
174
#######################################
175
function __generate_logic {
176
  local spec command
5✔
177
  dybatpho::expect_args spec command -- "$@"
5✔
178
  [ "$(type -t "${spec}")" != 'function' ] && return
10✔
179
  shift 2
5✔
180

181
  local IFS=" "                           # For get list of options, separated by space
5✔
182
  local __rest=""                         # For get all rest arguments
5✔
183
  local __error="" __validate=""          # For get function name of custom error handler, validation and
5✔
184
  local __flags="" __params=""            # For get all flags and params of command
5✔
185
  local __on="1" __off="" __init="@empty" # For handle argument of param, effective for rest arguments and options
5✔
186
  local __export="true"                   # For handle export variable of `dybatpho::opts::*` commands via name
5✔
187
  local __optional="true"                 # For set flag is optional or required
5✔
188
  local __action="" __setup_action=""     # For get action after parse all options and action of each options in spec
5✔
189
  local __switch=""                       # For get switch of options
5✔
190
  local __has_sub_cmd="false"
5✔
191
  declare -a __sub_specs=()
10✔
192

193
  __print_get_arg() {
194
    __print_indent 4 "eval 'set -- $1' \${1+'\"\$@\"'}"
7✔
195
  }
196

197
  __print_rest() {
198
    __print_indent 4 'while [ $# -gt 0 ]; do'
10✔
199
    __print_indent 5 "${__rest}=\"\${${__rest}} \$1\""
10✔
200
    __print_indent 5 "shift"
10✔
201
    __print_indent 4 "done"
10✔
202
    __print_indent 4 "break"
10✔
203
    __print_indent 4 ";;"
10✔
204
  }
205

206
  # Initial all variables before get value of options
207
  local __done_initial
5✔
208
  __done_initial=false && "${spec}" "$*"
10✔
209
  __print_indent 0 "dybatpho::opts::parse::${spec}() {"
5✔
210
  # shellcheck disable=2016
211
  __print_indent 1 \
5✔
212
    "while OPTARG= && [ \"\${${__rest}}\" != end ] && [ \$# -gt 0 ]; do"
213
  __print_indent 2 "case \$1 in"
5✔
214
  __print_indent 3 "--?*=*)"
5✔
215
  __print_indent 4 "OPTARG=\$1; shift"
5✔
216
  __print_get_arg '"${OPTARG%%\=*}" "${OPTARG#*\=}"'
5✔
217
  __print_indent 4 ";;"
5✔
218
  __print_indent 3 "--no-*|--without-*)"
5✔
219
  __print_indent 4 "unset OPTARG"
5✔
220
  __print_indent 4 ";;"
5✔
221
  [ "${__params}" ] && {
5✔
222
    __print_indent 3 "-[${__params}]?*)"
×
223
    __print_indent 4 "OPTARG=\$1; shift"
×
224
    __print_get_arg '"${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}"'
×
225
    __print_indent 4 ";;"
×
226
  }
227
  [ "${__flags}" ] && {
5✔
228
    __print_indent 3 "-[${__flags}]?*) OPTARG=\$1; shift"
2✔
229
    __print_get_arg '"${OPTARG%"${OPTARG#??}"}" -"${OPTARG#??}"'
2✔
230
    __print_indent 4 \
2✔
231
      'case $2 in --*) set -- "$1" unknown "$2" && '"${__rest}"'=end; esac'
232
    __print_indent 4 'OPTARG='
2✔
233
    __print_indent 4 ';;'
2✔
234
  }
235
  __print_indent 2 "esac"
5✔
236

237
  # Get value of options
238
  __print_indent 2 'case $1 in'
5✔
239
  __done_initial=true && "${spec}" "$*"
10✔
240
  __print_indent 3 "--)"
5✔
241
  __print_indent 4 "shift"
5✔
242
  __print_rest
5✔
243
  __print_indent 3 "*)"
5✔
244
  if dybatpho::is false "${__has_sub_cmd}"; then
5✔
245
    __print_rest
5✔
246
  else
247
    __print_indent 4 "case \$1 in"
×
248
    for sub_spec in "${__sub_specs[@]}"; do
×
249
      __print_indent 5 "${sub_spec#*:})"
×
250
      __print_indent 6 "shift"
×
251
      __print_indent 6 "dybatpho::opts::parse::${sub_spec%%:*} \"\$@\""
×
252
      __print_indent 6 ";;"
×
253
    done
254
    __print_indent 5 "*)"
×
255
    __print_indent 6 'set "notcmd" "$1"'
×
256
    __print_indent 6 "break"
×
257
    __print_indent 6 ";;"
×
258
    __print_indent 4 "esac"
×
259
    __print_rest
×
260
  fi
261
  __print_indent 2 "esac"
5✔
262
  __print_indent 2 "shift"
5✔
263
  __print_indent 1 "done"
5✔
264

265
  # Show error messages if invalid, otherwise run action command
266
  __print_indent 1 '[ $# -eq 0 ] && {'
5✔
267
  __print_indent 2 'unset OPTARG'
5✔
268
  __print_indent 2 "${__setup_action}"
5✔
269
  __print_indent 2 'return 0'
5✔
270
  __print_indent 1 '}'
5✔
271
  __print_indent 1 'case $1 in'
5✔
272
  __print_indent 2 'unknown) set "Unrecognized option: $2" "$@" ;;'
5✔
273
  __print_indent 2 'noarg) set "Does not allow an argument: $2" "$@" ;;'
5✔
274
  __print_indent 2 'needarg) set "Requires an argument: $2" "$@" ;;'
5✔
275
  __print_indent 2 'notcmd) set "Invalid command: $2" "$@" ;;'
5✔
276
  __print_indent 2 '*) set "Validation error ($1): $2" "$@"'
5✔
277
  __print_indent 1 "esac"
5✔
278
  [ "${__error}" ] && __print_indent 1 "${__error}" '"$@" >&2 || exit $?'
5✔
279
  __print_indent 1 'dybatpho::die "$1" 1'
5✔
280
  __print_indent 0 "} # End of dybatpho::opts::parse::${spec}"
5✔
281

282
  # Generate sub-command logics
283
  for sub_spec in "${__sub_specs[@]}"; do
×
284
    __generate_logic "${sub_spec%%:*}" "${sub_spec#*:}" "$@"
×
285
  done
286

287
  # Trigger root spec
288
  if [[ "${command}" == "-" ]]; then
5✔
289
    local trigger="dybatpho::opts::parse::${spec}"
5✔
290
    for param in "$@"; do
22✔
291
      trigger+=" \"${param//\"/\\\"}\""
22✔
292
    done
293
    __print_indent 0 "${trigger}"
5✔
294
  fi
295

296
}
297

298
#######################################
299
# @description Get help description for options from spec
300
# @exitcode 0 exit code
301
#######################################
302
function __generate_help {
303
  eval "
×
304
    dybatpho::print 'Usage: ${0##*/} [options...] [arguments...]'
×
305
    dybatpho::print 'Options:'
×
306
  "
×
307
}
308

309
#######################################
310
# @description Add to switches list if flag/param has multiple switches
311
# @arg $1 switch Switch
312
#######################################
313
function __add_switch {
314
  __switch="${__switch}${__switch:+|}$1"
2✔
315
}
316

317
function __print_validate {
318
  set -- "${__validate}" "$1"
2✔
319
  [ "$1" ] && __print_indent 4 "$1 || { set -- ${1%% *}:\$? \"\$1\" $1; break; }"
2✔
320
  __print_indent 4 "$(__prepend_export "$2=\$OPTARG")"
4✔
321
}
322

323
# @section Spec functions
324
# @description Functions work in spec of script or function via `dybatpho::generate_from_spec`.
325

326
#######################################
327
# @description Setup global settings for getting options (mandatory) in spec
328
# of script or function
329
# @arg $1 string Description of sub-command/root command
330
# @arg $@ key:value Settings `key:value` for sub-command/root command
331
# @exitcode 0 exit code
332
#######################################
333
function dybatpho::opts::setup {
334
  local description
10✔
335
  dybatpho::expect_args description -- "$@"
10✔
336
  shift
10✔
337

338
  # HACK: __rest is defined in __generate_logic, so we need to define it here
339
  [ "${1#-}" ] && __rest="$1" || __rest="__rest"
20✔
340

341
  if dybatpho::is false "${__done_initial}"; then
10✔
342
    while dybatpho::still_has_args "$@" && shift; do
11✔
343
      __parse_key_value "$1" "__"
3✔
344
    done
345
    __define_var "${__rest}"
5✔
346
    __setup_action="${__action}"
5✔
347
  fi
348
}
349

350
# shellcheck disable=2016
351
#######################################
352
# @description Define an option that take no argument
353
# @arg $1 string Description of option to display
354
# @arg $2 string Variable name for getting option. `-` if want to omit
355
# @arg $@ switch|key:value Other switches and settings `key:value` of this option
356
# @exitcode 0 exit code
357
#######################################
358
function dybatpho::opts::flag {
359
  local description var
4✔
360
  dybatpho::expect_args description var -- "$@"
4✔
361

362
  __parse_opt false "$@"
4✔
363
  if dybatpho::is false "${__done_initial}"; then
4✔
364
    __define_var "${var}"
2✔
365
  else
366
    __print_indent 3 "${__switch})"
2✔
367
    __print_indent 4 '[ "${OPTARG:-}" ] && OPTARG=${OPTARG#*\=} && set "noarg" "$1" && break'
2✔
368
    __print_indent 4 "eval '[ \${OPTARG+x} ] &&:' && OPTARG=${__on} || OPTARG=${__off}"
2✔
369
    __print_validate "${var}" '$OPTARG'
2✔
370
    __print_indent 4 ";;"
2✔
371
  fi
372
}
373

374
# shellcheck disable=2016
375
#######################################
376
# @description Define an option that take an argument
377
# @arg $1 string Description of option to display
378
# @arg $2 string Variable name for getting option. `-` if want to omit
379
# @arg $@ switch|key:value Other switches and settings `key:value` of this option
380
# @exitcode 0 exit code
381
#######################################
382
function dybatpho::opts::param {
383
  local description var
×
384
  dybatpho::expect_args description var -- "$@"
×
385

386
  __parse_opt true "$@"
×
387
  if dybatpho::is false "${__done_initial}"; then
×
388
    __define_var "${var}"
×
389
  else
390
    __print_indent 3 "${__switch})"
×
391
    if dybatpho::is false "${__optional}"; then
×
392
      __print_indent 4 '[ $# -le 1 ] && set "needarg" "$1" && break'
×
393
      __print_indent 4 'OPTARG=$2'
×
394
    else
395
      __print_indent 4 'set -- "$1" "$@"'
×
396
      __print_indent 4 '[ ${OPTARG+x} ] && {'
×
397
      __print_indent 5 'case $1 in --no-*|--without-*) set "noarg" "${1%%\=*}"; break; esac'
×
398
      __print_indent 5 '[ "${OPTARG:-}" ] && { shift; OPTARG=$2; } ||' "OPTARG=${__on}"
×
399
      __print_indent 4 "} || OPTARG=${__off}"
×
400
    fi
401
    __print_validate "${var}" '$OPTARG'
×
402
    __print_indent 4 "shift"
×
403
    __print_indent 4 ";;"
×
404
  fi
405
}
406

407
#######################################
408
# @description Define an option that display only
409
# @arg $1 string Description of option to display
410
# @arg $@ switch|key:value Other switches and settings `key:value` of this option
411
# @exitcode 0 exit code
412
#######################################
413
function dybatpho::opts::disp {
414
  local description
×
415
  dybatpho::expect_args description -- "$@"
×
416

417
  __parse_opt false "$@"
×
418
  if ! dybatpho::is false "${__done_initial}"; then
×
419
    __print_indent 3 "${__switch})"
×
420
    [ "${__action}" ] && __print_indent 4 "${__action}"
×
421
    __print_indent 4 "exit 0"
×
422
    __print_indent 4 ";;"
×
423
  fi
424
}
425

426
#######################################
427
# @description Define a sub-command in spec
428
# @arg $1 string Name of function that has spec of sub-command
429
#######################################
430
function dybatpho::opts::cmd {
431
  local sub_cmd sub_spec
×
432
  dybatpho::expect_args sub_cmd sub_spec -- "$@"
×
433

434
  if dybatpho::is true "${__done_initial}"; then
×
435
    __has_sub_cmd="true"
×
436
    # shellcheck disable=2190
437
    __sub_specs+=("${sub_spec}:'${sub_cmd}'")
×
438
  fi
439
}
440

441
# @section Parse functions
442
# @description Functions to parse spec and put value of options to variable with corresponding name
443

444
#######################################
445
# @description Define spec of parent function or script, spec contains below commands
446
# @arg $1 string Name of function that has spec of parent function or script
447
# @exitcode 0 exit code
448
#######################################
449
function dybatpho::generate_from_spec {
450
  local spec
5✔
451
  dybatpho::expect_args spec -- "$@"
5✔
452
  shift
5✔
453

454
  local gen_file
5✔
455
  dybatpho::create_temp gen_file ".sh" "genopts"
5✔
456
  __generate_logic "${spec}" - "$@" >> "${gen_file}"
5✔
457
  if dybatpho::is true "${DYBATPHO_CLI_DEBUG}"; then
5✔
458
    dybatpho::debug_command "Generate script of \"${spec}\" - \"$*\"" "dybatpho::show_file '${gen_file}'"
1✔
459
  fi
460
  # shellcheck disable=1090
461
  . "${gen_file}"
5✔
462
}
463

464
#######################################
465
# @description Show help description of root command/sub-command
466
# @arg $1 string Name of function that has spec of parent function or script
467
# @stdout Help description
468
#######################################
469
function dybatpho::generate_help {
470
  local spec
×
471
  dybatpho::expect_args spec -- "$@"
×
472

473
  local gen_file
×
474
  dybatpho::create_temp gen_file ".sh" "genhelp"
×
475
  __generate_logic "${spec}" - "$@" >> "${gen_file}"
×
476
  dybatpho::cleanup_file_on_exit "${gen_file}"
×
477
  if dybatpho::is true "${DYBATPHO_CLI_DEBUG}"; then
×
478
    dybatpho::debug_command "Generate script of \"${spec}\" - \"$*\"" "dybatpho::show_file '${gen_file}'"
×
479
  fi
480
  # shellcheck disable=1090
481
  . "${gen_file}"
×
482
  __generate_help "${spec}"
×
483
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc