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

dynamotn / dybatpho / 14421963749

12 Apr 2025 05:43PM UTC coverage: 58.35% (+1.1%) from 57.292%
14421963749

push

github

dynamotn
feat: add commands for CLI options parsing

2 of 14 new or added lines in 2 files covered. (14.29%)

297 of 509 relevant lines covered (58.35%)

25.03 hits per line

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

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

40
# @section Functions are triggered by `dybatpho::generate_from_spec`
41

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

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

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

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

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

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

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

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

191
  __print_get_arg() {
192
    __print_indent 4 "eval 'set -- $1' \${1+'\"\$@\"'}"
×
193
  }
194

195
  __print_rest() {
196
    __print_indent 4 'while [ $# -gt 0 ]; do'
×
197
    __print_indent 5 "eval \"${__rest}=\\\"\${${__rest}} \\\${\$((OPTIND-\$#))}\\\"\""
×
198
    __print_indent 5 "shift"
×
199
    __print_indent 4 "done"
×
200
    __print_indent 4 "break"
×
201
    __print_indent 4 ";;"
×
202
  }
203

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

236
  # Get value of options
237
  __print_indent 2 'case $1 in'
×
238
  __done_initial=true && "${spec}" "$*"
×
239
  __print_indent 3 "--)"
×
240
  dybatpho::is false "${__has_sub_cmd}" && __print_indent 4 "shift"
×
241
  __print_rest
×
242
  __print_indent 3 "*)"
×
243
  if dybatpho::is false "${__has_sub_cmd}"; then
×
244
    __print_indent 4 "eval \"${__rest}=\\\"\${${__rest}} \\\${\$((OPTIND-\$#))}\\\"\""
×
245
    __print_indent 4 ";;"
×
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"
×
262
  __print_indent 2 "shift"
×
263
  __print_indent 1 "done"
×
264

265
  # Show error messages if invalid, otherwise run action command
266
  __print_indent 1 '[ $# -eq 0 ] && {'
×
267
  __print_indent 2 'OPTIND=1; unset OPTARG'
×
268
  __print_indent 2 "${__setup_action}"
×
269
  __print_indent 2 'return 0'
×
270
  __print_indent 1 '}'
×
271
  __print_indent 1 'case $1 in'
×
272
  __print_indent 2 'unknown) set "Unrecognized option: $2" "$@" ;;'
×
273
  __print_indent 2 'noarg) set "Does not allow an argument: $2" "$@" ;;'
×
274
  __print_indent 2 'needarg) set "Requires an argument: $2" "$@" ;;'
×
275
  __print_indent 2 'notcmd) set "Invalid command: $2" "$@" ;;'
×
276
  __print_indent 2 '*) set "Validation error ($1): $2" "$@"'
×
277
  __print_indent 1 "esac"
×
278
  [ "${__error}" ] && __print_indent 1 "${__error}" '"$@" >&2 || exit $?'
×
279
  __print_indent 1 'dybatpho::die "$1" 1'
×
280
  __print_indent 0 "} # End of dybatpho::opts::parse::${spec}"
×
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
×
289
    local trigger="dybatpho::opts::parse::${spec}"
×
290
    for param in "$@"; do
×
291
      trigger+=" '${param}'"
×
292
    done
293
    __print_indent 0 "${trigger}"
×
294
  fi
295

296
}
297

298
#######################################
299
# @description Add to switches list if flag/param has multiple switches
300
# @arg $1 switch Switch
301
#######################################
302
function __add_switch {
303
  __switch="${__switch}${__switch:+|}$1"
×
304
}
305

306
function __print_validate {
307
  set -- "${__validate}" "$1"
×
308
  [ "$1" ] && __print_indent 4 "$1 || { set -- ${1%% *}:\$? \"\$1\" $1; break; }"
×
309
  __print_indent 4 "$(__prepend_export "$2=\$OPTARG")"
×
310
}
311

312
# @section Functions work in spec of script or function via
313
# `dybatpho::generate_from_spec`.
314

315
#######################################
316
# @description Setup global settings for getting options (mandatory) in spec
317
# of script or function
318
# @arg $1 string Description of sub-command/root command
319
# @arg $@ key:value Settings `key:value` for sub-command/root command
320
# @exitcode 0 exit code
321
#######################################
322
function dybatpho::opts::setup {
323
  dybatpho::expect_args __description __rest -- "$@"
×
324
  shift
×
325

326
  [ "${__rest#-}" ] || __rest="__rest"
×
327
  if dybatpho::is false "${__done_initial}"; then
×
328
    while dybatpho::still_has_args "$@" && shift; do
×
329
      __parse_key_value "$1" "__"
×
330
    done
331
    __define_var "${__rest}"
×
332
    __setup_action="${__action}"
×
333
  fi
334
}
335

336
# shellcheck disable=2016
337
#######################################
338
# @description Define an option that take no argument
339
# @arg $1 string Description of option to display
340
# @arg $2 string Variable name for getting option. `-` if want to omit
341
# @arg $@ switch_or_key:value Other switches and settings `key:value` of this option
342
# @exitcode 0 exit code
343
#######################################
344
function dybatpho::opts::flag {
345
  local var
×
346
  dybatpho::expect_args __description var -- "$@"
×
347

348
  __parse_opt false "$@"
×
349
  if dybatpho::is false "${__done_initial}"; then
×
350
    __define_var "${var}"
×
351
  else
352
    __print_indent 3 "${__switch})"
×
353
    __print_indent 4 '[ "${OPTARG:-}" ] && OPTARG=${OPTARG#*\=} && set "noarg" "$1" && break'
×
354
    __print_indent 4 "eval '[ \${OPTARG+x} ] &&:' && OPTARG=${__on} || OPTARG=${__off}"
×
355
    __print_validate "${var}" '$OPTARG'
×
356
    __print_indent 4 ";;"
×
357
  fi
358
}
359

360
# shellcheck disable=2016
361
#######################################
362
# @description Define an option that take an argument
363
# @arg $1 string Description of option to display
364
# @arg $2 string Variable name for getting option. `-` if want to omit
365
# @arg $@ switch_or_key:value Other switches and settings `key:value` of this option
366
# @exitcode 0 exit code
367
#######################################
368
function dybatpho::opts::param {
369
  local var
×
370
  dybatpho::expect_args __description var -- "$@"
×
371

372
  __parse_opt true "$@"
×
373
  if dybatpho::is false "${__done_initial}"; then
×
374
    __define_var "${var}"
×
375
  else
376
    __print_indent 3 "${__switch})"
×
377
    if dybatpho::is false "${__optional}"; then
×
378
      __print_indent 4 '[ $# -le 1 ] && set "needarg" "$1" && break'
×
379
      __print_indent 4 'OPTARG=$2'
×
380
    else
381
      __print_indent 4 'set -- "$1" "$@"'
×
382
      __print_indent 4 '[ ${OPTARG+x} ] && {'
×
383
      __print_indent 5 'case $1 in --no-*|--without-*) set "noarg" "${1%%\=*}"; break; esac'
×
384
      __print_indent 5 '[ "${OPTARG:-}" ] && { shift; OPTARG=$2; } ||' "OPTARG=${__on}"
×
385
      __print_indent 4 "} || OPTARG=${__off}"
×
386
    fi
387
    __print_validate "${var}" '$OPTARG'
×
388
    __print_indent 4 "shift"
×
389
    __print_indent 4 ";;"
×
390
  fi
391
}
392

393
#######################################
394
# @description Define an option that display only
395
# @arg $1 string Description of option to display
396
# @arg $@ switch_or_key:value Other switches and settings `key:value` of this option
397
# @exitcode 0 exit code
398
#######################################
399
function dybatpho::opts::disp {
400
  dybatpho::expect_args __description -- "$@"
×
401

402
  __parse_opt false "$@"
×
403
  if ! dybatpho::is false "${__done_initial}"; then
×
404
    __print_indent 3 "${__switch})"
×
405
    [ "${__action}" ] && __print_indent 4 "${__action}"
×
406
    __print_indent 4 "exit 0"
×
407
    __print_indent 4 ";;"
×
408
  fi
409
}
410

411
#######################################
412
# @description Define a sub-command in spec
413
# @arg $1 string Name of function that has spec of sub-command
414
#######################################
415
function dybatpho::opts::cmd {
416
  local sub_cmd sub_spec
×
417
  dybatpho::expect_args sub_cmd sub_spec -- "$@"
×
418

419
  if dybatpho::is true "${__done_initial}"; then
×
420
    __has_sub_cmd="true"
×
421
    # shellcheck disable=2190
422
    __sub_specs+=("${sub_spec}:'${sub_cmd}'")
×
423
  fi
424
}
425

426
# @section Functions to parse spec and put value of options to variable with
427
# corresponding name
428

429
#######################################
430
# @description Define spec of parent function or script, spec contains below commands
431
# @arg $1 string Name of function that has spec of parent function or script
432
# @exitcode 0 exit code
433
#######################################
434
function dybatpho::generate_from_spec {
435
  local spec
×
436
  dybatpho::expect_args spec -- "$@"
×
437
  shift
×
438

439
  local pid="$$"
×
440
  local gen_file=$(mktemp --tmpdir="${TMPDIR:-/tmp}" "dybatpho_genopts-${pid}-XXXXX.sh")
×
441
  __generate_logic "${spec}" - "$@" >> "${gen_file}"
×
442
  dybatpho::cleanup_file_on_exit "${gen_file}"
×
443
  dybatpho::debug_command "Generate script of \"${spec}\" - \"$*\"" "dybatpho::show_file '${gen_file}'"
×
444
  # shellcheck disable=1090
445
  . "${gen_file}"
×
446
}
447

448
#######################################
449
# @description Get help description for option with a spec from
450
# `dybatpho::opts::flag`, `dybatpho::opts::param`
451
# @arg $1 bool Flag that defined option that take argument in spec
452
# @arg $@ string Arguments pass from `dybatpho::opts::(flag|param)`
453
# @exitcode 0 exit code
454
#######################################
455
function __generate_help {
NEW
456
  eval "
×
NEW
457
    dybatpho::print 'Usage: ${0##*/} [options...] [arguments...]'
×
NEW
458
    dybatpho::print 'Options:'
×
NEW
459
  "
×
460
}
461

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

NEW
471
  local gen_file=$(mktemp --tmpdir="${TMPDIR:-/tmp}" "dybatpho_genhelp-${pid}-XXXXX.sh")
×
NEW
472
  __generate_logic "${spec}" - "$@" >> "${gen_file}"
×
NEW
473
  dybatpho::cleanup_file_on_exit "${gen_file}"
×
NEW
474
  dybatpho::debug_command "Generate script of \"${spec}\" - \"$*\"" "dybatpho::show_file '${gen_file}'"
×
475
  # shellcheck disable=1090
NEW
476
  . "${gen_file}"
×
NEW
477
  __generate_help "${spec}"
×
478
}
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