diff --git a/bake b/bake index 5b6aec07..8d2ffdbc 100755 --- a/bake +++ b/bake @@ -9,24 +9,15 @@ SCRIPT_FILE="$(basename "$SCRIPT_PATH")" # 进入脚本所在目录,这样上下文就是本项目了 cd "$SCRIPT_DIR" || exit 200 -bake.upgrade(){ - mkdir -p "$SCRIPT_DIR/vendor" - echo "bake -> bake.upgrade ▶︎【curl -o $SCRIPT_DIR/bake.bash https://github.com/chen56/bake/raw/main/bake.bash】" - curl -L -o "$SCRIPT_DIR/vendor/bake.bash" https://github.com/chen56/bake/raw/main/bake.bash ; -} -if ! [[ -f "$SCRIPT_DIR/vendor/bake.bash" ]]; then - bake.upgrade -fi - # include common script -source "$SCRIPT_DIR/vendor/bake.bash" +source "packages/you_bake/bake.bash" ########################################## # 应用的命令脚本 , 公共函数和全局变量 ########################################## declare -A _pkgs=( - ["bake"]="$SCRIPT_DIR" + ["bake"]="$SCRIPT_DIR/packages/you_bake" ["learn_dart"]="$SCRIPT_DIR/notes/learn_dart" ["you"]="$SCRIPT_DIR" ["note_dart"]="$SCRIPT_DIR/packages/note_dart" @@ -35,20 +26,6 @@ declare -A _pkgs=( ["shell"]="$SCRIPT_DIR/notes/shell" ) -bake.cmd --cmd pkgs --desc " mono project manage: ./$SCRIPT_FILE pkgs " -ls(){ - for pkg in ${!_pkgs[*]} ; do - echo "$pkg:${_pkgs[$pkg]}" - done -} - -bake.cmd --cmd pkgs.run --desc " run cmd on all pkg,Usage: ./$SCRIPT_FILE pkgs run [any cmd]" -run(){ - for pkg in ${!_pkgs[*]} ; do - # 子shell内执行,防止环境感染 - ( cd "${_pkgs[$pkg]}" || exit 200 ; _run "$@" ; ) - done -} # 运行所用项目的某子命令 # Usage: _run_all_package @@ -103,37 +80,6 @@ _func_exists(){ # 应用的命令脚本 ########################################## -bake.cmd --cmd root --desc "$( cat <<-EOF - - ___ _ _ _ _ _ _ -| __|| | _ _ | |_ | |_ ___ _ _ | \| | ___ | |_ ___ -| _| | || || || _|| _|/ -_)| '_| | . |/ _ \| _|/ -_) -|_| |_| \_._| \__| \__|\___||_| |_|\_|\___/ \__|\___| - -flutter-note build tools. -https://github.com/chen56/note -PWD: $PWD - -Usage: - ./bake [cmd] [opts] [args...] - -Examples: - ./bake # same as './bake -h' - ./bake -h # show all commands help - ./bake -h --debug # show all commands help , include internal function - - ./bake flutter dev # == cd notes/flutter_web && flutter run --device-id macos - - ./bake test # test all projects - ./bake build # defalut build == flutter build web --web-renderer html - ./bake preview # defalut preview == start server at web build - ./bake test # test all projects - - ./bake pkgs ls # show all mono project - ./bake pkgs run pwd # run "pwd" on all mono project dir - ./bake pkgs run flutter pub get # run "flutter pub get" on all mono project dir -EOF -)" # 根项目,主要是bin/辅助工具等 you.run() { _run "$@";} @@ -143,6 +89,10 @@ you.upgrade() ( you.run dart pub upgrade ;) you.test() ( you.run dart test;) you.build() ( you.run dart compile exe bin/notecli.dart ;) +# bash common script lib +bake.run() { _run "$@";} +bake.test() ( packages/you_bake/test.bash test;) + note_dart.run() ( cd packages/note_dart || return 200 && _run "$@") note_dart.install() ( note_dart.run flutter pub get) note_dart.clean() ( note_dart.run flutter clean; rm -rf build;) @@ -180,6 +130,17 @@ upgrade() { _run_all_package upgrade;} clean() { _run_all_package clean; } test() { _run_all_package test; } gen() { _run_all_package gen; } +ls(){ + for pkg in ${!_pkgs[*]} ; do + echo "$pkg:${_pkgs[$pkg]}" + done +} +run(){ + for pkg in ${!_pkgs[*]} ; do + # 子shell内执行,防止环境感染 + ( cd "${_pkgs[$pkg]}" || exit 200 ; _run "$@" ; ) + done +} # github 发布时使用,参考[.github/workflows/*.yaml] docker.build() ( @@ -203,5 +164,40 @@ info() { #################################################### # app entry script & _root cmd #################################################### +bake.cmd --cmd ls --desc " mono project manage: ./$SCRIPT_FILE pkgs " +bake.cmd --cmd run --desc " run cmd on all pkg,Usage: ./$SCRIPT_FILE pkgs run [any cmd]" + +bake.cmd --cmd root --desc "$( cat <<-EOF + + ___ _ _ _ _ _ _ +| __|| | _ _ | |_ | |_ ___ _ _ | \| | ___ | |_ ___ +| _| | || || || _|| _|/ -_)| '_| | . |/ _ \| _|/ -_) +|_| |_| \_._| \__| \__|\___||_| |_|\_|\___/ \__|\___| + +https://github.com/chen56/you/flutter_web + +PWD: $PWD + +Usage: + ./bake [cmd] [opts] [args...] + +Examples: + ./bake # same as './bake -h' + ./bake -h # show all commands help + ./bake -h --debug # show all commands help , include internal function + + ./bake flutter dev # == cd notes/flutter_web && flutter run --device-id macos + + ./bake test # test all projects + ./bake build # defalut build == flutter build web --web-renderer html + ./bake preview # defalut preview == start server at web build + ./bake test # test all projects + + ./bake pkgs ls # show all mono project + ./bake pkgs run pwd # run "pwd" on all mono project dir + ./bake pkgs run flutter pub get # run "flutter pub get" on all mono project dir +EOF +)" + bake.go "$@" diff --git a/notes/shell/common.bash b/notes/shell/common.bash index b66b042d..6429d453 100755 --- a/notes/shell/common.bash +++ b/notes/shell/common.bash @@ -24,8 +24,8 @@ SCRIPT_FILE="$(basename "$SCRIPT_PATH")" _install_bake(){ mkdir -p "$SCRIPT_DIR/vendor" - echo "$SCRIPT_PATH -> _install_bake ▶︎【curl -o $SCRIPT_DIR/bake.bash https://github.com/chen56/bake/raw/main/bake.bash】" - curl -L -o "$SCRIPT_DIR/vendor/bake.bash" https://github.com/chen56/bake/raw/main/bake.bash ; + echo "$SCRIPT_PATH -> _install_bake ▶︎【curl -o $SCRIPT_DIR/bake.bash https://github.com/chen56/you/raw/main/packages/you_bake/bake.bash】" + curl -L -o "$SCRIPT_DIR/vendor/bake.bash" https://github.com/chen56/you/raw/main/packages/you_bake/bake.bash ; } if ! [[ -f "$SCRIPT_DIR/vendor/bake.bash" ]]; then _install_bake diff --git a/packages/you_bake/README.md b/packages/you_bake/README.md new file mode 100644 index 00000000..3da4c6ee --- /dev/null +++ b/packages/you_bake/README.md @@ -0,0 +1,117 @@ +# bake + + +```bash + +# bake == (bash)ake == 去Make的bash tool +# +# https://github.com/chen56/you/packages/you_bake +# +# bake 是个简单的命令行工具,以替代Makefile的子命令功能 +# make工具的主要特点是处理文件依赖进行增量编译,但flutter、golang、java、js项目的build工具 +# 太厉害了,这几年唯一还在用Makefile的理由就是他的子命令机制: "make build"、 +# "make run", 可以方便的自定义单一入口的父子命令,但Makefile本身的语法套路也很复杂, +# 很多批处理还是要靠bash, 这就尴尬了,工具太多,麻烦!本脚本尝试彻底摆脱使用Makefile。 +# 经尝试,代码很少啊 ,核心代码几百行啊,父子命令二三百行左右,option解析二三百行左右,功能足够了: +# +# bake命令规则: +# 1. 函数即命令,所有bake内的函数均可以在脚本外运行: +# ./bake [all function] # bake内的所有函数均可以在脚本外直接运行 +# ./bake info # 比如这个内部函数, 看bake内部变量,调试脚本用 +# ./bake test # 你如果定义过test()函数,就可以这样运行 +# 2. 带"."的函数,形成父子命令: +# web.build(){ echo "build web app"; } +# web.test(){ echo "build web app"; } +# 可以这样调用 +# ./bake web -h # 运行子命令,或看帮助 +# ./bake web.build -h # 上面等同 +# ./bake web test -h # 运行子命令,或看帮助 +# ./bake web.test -h # 与上面等同 +# 3. 特殊的root命令表示根命令 +# ./bake # 如果有root()函数,就执行它 +# 4. 像其他高级语言的cli工具一样,用简单变量就可以获取命令option: +# # a. 先在bake文件里里定义app options +# bake.opt --cmd build --long "target" --type string +# # b. 解析和使用option +# function build() { +# eval "$(bake.parse "$@")"; +# echo "build ... your option:target: $target"; +# } +# # c. 调用看看: +# ./bake build --target "macos" +# 5. bake尽量不依赖bash以外的其他工具,包括linux coreutils,更简单通用,但由于用了关联数组等 +# 依赖bash4+ +# 6. 有两种用法: +# - 这个文件copy走,把你的脚本放到本脚本最后即可. +# - 在你的脚本里直接curl下载本脚本后 source即可。 +# 范例可以看实际案例: +# - https://github.com/chen56/note/blob/main/bake +# - https://github.com/chen56/younpc/blob/main/bake + + +# TODO +# 1. 当前 无法判断错误命令:./bake no_this_cmd ,因为不知道这是否是此命令的参数, +# 需要设置设一个简单的规则:只有叶子命令才能正常执行,这样非叶子命令就不需要有参数 +# 2. 当前 无法判断错误options:./bake --no_this_opt ,同上 +# 3. 类似flutter run [no-]pub 反向选项 +# TODO +# 利用extdebug , declare -F xxx 可显示行号,这下顺序解决了! +# https://www.gnu.org/software/bash/manual/bash.html#The-Shopt-Builtin +# + + +``` + + +## 范例 + +首先建立一个空脚本文件,可以叫bake,复制下面代码进去: + +```bash +#!/usr/bin/env bash + +# 我们都知道bash没什么有效的包管理工具,但没关系,curl就是包管理工具,在你的脚本复制下面模版 +# 脚本动态安装bake.bash依赖到: vendor/bake.bash +if ! [[ -f "./vendor/bake.bash" ]]; then + mkdir -p "./vendor" + curl -L -o "./vendor/bake.bash" https://github.com/chen56/you/raw/main/packages/you_bake/bake.bash ; +fi +source "./vendor/bake.bash" + +# 定义一些二级命令 +install(){ echo "install deps"; } +clean(){ echo "clean project"; } +dev(){ echo "run dev mode"; } +preview(){ echo "run preview mode"; } +test(){ echo "test project"; } + +# 更深层的命令: `./bake build -h` , `./bake build all` +build.all(){ echo "build all"; } +build.macos(){ echo "build macos package"; } +build.web(){ echo "build web package"; } +build.android(){ echo "build android package"; } +build.ios(){ echo "build ios package"; } + +# bake走起 +bake.go "$@" +``` + +运行`chmod +x ./bake ` 再运行它:`./bake -h` ,你会看到bake已经帮你组织好了子命令,就像docker/git等父子命令一样使用: + +```bash +$ ./bake + +Available Options: + --debug -d bool required:[false] debug mode, print more internal info + --help -h bool required:[false] print help, show all commands + +Available Commands: + build + clean + dev + install + preview + test +``` + +查看更多范例:[examples](./examples/) diff --git a/packages/you_bake/bake.bash b/packages/you_bake/bake.bash new file mode 100755 index 00000000..1f603696 --- /dev/null +++ b/packages/you_bake/bake.bash @@ -0,0 +1,733 @@ +#!/usr/bin/env bash +set -o errtrace # -E trap inherited in sub script +set -o errexit # -e +set -o functrace # -T If set, any trap on DEBUG and RETURN are inherited by shell functions +set -o pipefail # default pipeline status==last command status, If set, status=any command fail +#set -o nounset # -u: don't use it ,it is crazy, 1.bash version is diff Behavior 2.we need like this: ${arr[@]+"${arr[@]}"} + +_bake_version=v0.4.20240406 + +# It can run normally on macos +# bake == (bash)ake == 去Make的bash tool +# +# https://github.com/chen56/you/packages/you_bake +# +# bake 是个简单的命令行工具,以替代Makefile的子命令功能 +# make工具的主要特点是处理文件依赖进行增量编译,但flutter、golang、java、js项目的build工具 +# 太厉害了,这几年唯一还在用Makefile的理由就是他的子命令机制: "make build"、 +# "make run", 可以方便的自定义单一入口的父子命令,但Makefile本身的语法套路也很复杂, +# 很多批处理还是要靠bash, 这就尴尬了,工具太多,麻烦!本脚本尝试彻底摆脱使用Makefile。 +# 经尝试,代码很少啊 ,核心代码几百行啊,父子命令二三百行左右,option解析二三百行左右,功能足够了: +# +# bake命令规则: +# 1. 函数即命令,所有bake内的函数均可以在脚本外运行: +# ./bake [all function] # bake内的所有函数均可以在脚本外直接运行 +# ./bake info # 比如这个内部函数, 看bake内部变量,调试脚本用 +# ./bake test # 你如果定义过test()函数,就可以这样运行 +# 2. 带"."的函数,形成父子命令: +# web.build(){ echo "build web app"; } +# web.test(){ echo "build web app"; } +# 可以这样调用 +# ./bake web -h # 运行子命令,或看帮助 +# ./bake web.build -h # 上面等同 +# ./bake web test -h # 运行子命令,或看帮助 +# ./bake web.test -h # 与上面等同 +# 3. 特殊的root命令表示根命令 +# ./bake # 如果有root()函数,就执行它 +# 4. 像其他高级语言的cli工具一样,用简单变量就可以获取命令option: +# # a. 先在bake文件里里定义app options +# bake.opt --cmd build --long "target" --type string +# # b. 解析和使用option +# function build() { +# eval "$(bake.parse "$@")"; +# echo "build ... your option:target: $target"; +# } +# # c. 调用看看: +# ./bake build --target "macos" +# 5. bake尽量不依赖bash以外的其他工具,包括linux coreutils,更简单通用,但由于用了关联数组等 +# 依赖bash4+ +# 6. 有两种用法: +# - 这个文件copy走,把你的脚本放到本脚本最后即可. +# - 在你的脚本里直接curl下载本脚本后 source即可。 +# 范例可以看实际案例: +# - https://github.com/chen56/note/blob/main/bake +# - https://github.com/chen56/younpc/blob/main/bake + +############################################################# +# bake head 变量定义区 +# bake.bash 在头head定义变量,尾tail执行初始化,中间只有函数 +############################################################# + +# check bake dependencies +if ((BASH_VERSINFO[0] < 4 || (\ + BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4))); then + echo "Error: It's 2082 ,Your bash is still this version(BASH_VERSINFO: ${BASH_VERSINFO[*]}),Please install bash 4.4+: + apt install bash # ubuntu + brew install bash # mac" + exit 124 # =>http code 424 +fi + +# On Mac OS, readlink -f doesn't work, so use.bake._real_path get the real path of the file +bake._real_path() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" ; } + +# bake context +BAKE_PATH="$(bake._real_path "${BASH_SOURCE[0]}")" +# shellcheck disable=SC2034 +BAKE_DIR="$(dirname "$BAKE_PATH")" +BAKE_FILE="$(basename "$BAKE_PATH")" + +# defalut option:bake --debug --help +# bake.parse 会按bake.opt的定义动态生成相应变量,这里事先声明,是为了备注其存在 +# 请参考[bake.parse] +__debug=false +__help=false + + +# Simulating object-oriented data structures with flat associative arrays +# use ./bake _self to see internal var +# save all other data +declare -A _bake_data + +# only save all commands, we use it to build command tree +# it is cache cmd tree from _bake_data +declare -A _bake_cmds + + + +########################################## +# bake internal function +########################################## + + +bake._on_error() { + bake._error "ERROR - trapped an error: ↑ , trace: ↓" + local i=0 + local stackInfo + while true; do + stackInfo=$(caller $i 2>&1 && true) && true + if [[ $? != 0 ]]; then return 0; fi + + # 一行调用栈 '97 bake.build ./note/bake' + # 解析后 => 行号no=97 , 报错的函数func=bake.build , file=./note/bake + local no func file + IFS=' ' read -r no func file <<<"$stackInfo" + + # 打印出可读性强的信息: + # => ./note/bake:38 -> bake.build + printf "%s\n" "$(bake._real_path $file):$no -> $func" >&2 + + i=$((i + 1)) + done +} + + +# replace $HOME with "~" +# Usage: bake._pwd +# Examples: 当前目录如果是"/Users/chen/git/note/" +# 转成更简单易读的 => "~/git/note/" +bake._pwd() { echo "${PWD/#$HOME/\~}" ; } + +# 报错后终止程序,类似于其他语言的throw Excpetion +# 因set -o errexit 后,程序将在return 1 时退出, +# 退出前被‘trap bake._on_error ERR’捕获并显示错误堆栈 +# Usage: bake._throw +bake._throw(){ + bake._log FATAL "$@" + # set -o errexit 后,程序将退出,退出前被trap bake._on_error Err捕获并显示错误堆栈 + return 200 +} +bake._error() { + bake._log ERROR "$@" +} +bake._info() { + bake._log INFO "$@" +} +bake._debug() { + # shellcheck disable=SC2154 + if [[ "${__debug}" == true ]]; then + bake._log DEBUG "$@" + fi +} +# Usage: bake._log DEBUG "错误消息" +bake._log(){ + local level="$1" + echo -e "$level $(date "+%F %T") $(bake._pwd)\$ ${FUNCNAME[1]}() : $*" >&2 +} + + + + +# Usage: bake._str_cutLeft +# bake._path_dirname a/b/c a/b => c +bake._str_cutLeft() { printf "${1#$2}"; } + +# 分割字符串 +# Usage: bake._str_split [delimiter:default /] +# Example: bake._str_split "a/b/c" "/" +# => $'a\nb\nc' # 会把字符串分割为以换行符间隔的字符串 +bake._str_split() { + #${delimiter:-DEFAULT} unset 或null 都给默认值 + # https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html + local str="$1" + local delimiter="${2:-/}" + + # use <() process-substitution + # or here string <<< "" its add newline + local arr + # https://helpmanual.io/builtin/readarray/ + # -d The first character of delim is used to terminate each input line, rather than newline. + # -t Remove a trailing delim (default newline) from each line read. + readarray -t -d "$delimiter" arr <<< "${str}" + # same as: for i in "${arr[@]}"; do echo "$i" done + printf '%s\n' "${arr[@]}" +} + +# Usage: bake._str_revertLines <<< "$(echo -e "a\nb\nc")" => "c\nb\na" +bake._str_revertLines() { + # cat xxx | tail -r; # macos bsd only, not work on linux + # so use sed + sed '1!G;h;$!d' # sed magic +} + +# Usage: bake._path_dirname [delimiter:default /] +# similar command dirname, but diff: +# dirname a => '.' +# bake._path_dirname a => '' +# +# Example: delimiter: +# bake._path_dirname a.b.c "." => "a.b" +# bake._path_dirname a "." => "" +bake._path_dirname() { + local path="$1" + local delimiter="${2:-/}" + +# bake._path_dirname level1_path "." => "" + if [[ "$path" != *"$delimiter"* ]]; then + return + fi + echo "${path%"$delimiter"*}" +} +# Usage: bake._path_first [delimiter:default /] +# bake._path_first a.b.c . => a +bake._path_first() { + local pathLikeStr="$1" delimiter="${2:-/}" + # if /a/b/c , call :bake._path_first a/b/c / + if [[ "$pathLikeStr" == "$delimiter"* ]]; then + local removeDelim + removeDelim=$(bake._str_cutLeft "$pathLikeStr" "$delimiter") + printf "$delimiter$(bake._path_first "$removeDelim" "$delimiter")" + else # a/b/c + printf "${pathLikeStr%%$delimiter*}" + fi +} +# similar command basename +# Usage: bake._path_basename [delimiter:default /] +bake._path_basename() { + local pathLikeStr="$1" delimiter="${2:-/}" + # ${1##*/} => ## left remove until last "/" + echo "${pathLikeStr##*/}" +} + +# Samples: +# bake._cmd_children +# => list root children +# bake._cmd_children tests +# => list test children +bake._cmd_children() ( + local path="${1:-root}" # default arg is root + + # ./bake bake info列出命令注册表,_bake_cmds数组长这样: + # cmd - bake.opt = bake.opt + # cmd - bake.parse = bake.parse + # cmd - bake.str_escape = bake.str_escape + # cmd - bake.str_unescape = bake.str_unescape + # cmd - bake.version = bake.version + # cmd - root = PARENT_CMD_NOT_FUNC + # cmd - study = PARENT_CMD_NOT_FUNC + # cmd - study.array = study.array + # cmd - study.declare = study.declare + + # 单独列出root的子命令 + if [[ "$path" == root ]]; then + for full_name in "${!_bake_cmds[@]}"; do + # 不包含'.'的命令是一级命令,即root的子命令 + [[ $full_name == *"."* ]] && continue; + [[ $full_name == "root" ]] && continue; + + echo "$full_name" + done + return 0 + fi + + # ${!_bake_data[@]}: get all array keys + for key in "${!_bake_cmds[@]}"; do + # if start $path + if [[ "$key" == "$path."* && "$key" != "$path" && "$key" != "root" ]]; then + # https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html + # remove prefix : key:a.b.c leftPathToBeCut:a => b.c + child=$(bake._str_cutLeft "$key" "$path.") + # remove suffix: b.c => c + child=$(bake._path_first "$child" ".") + printf '%s\n' "$child" + fi + done # | sort -u +) + +# Usage: bake._cmd_up_chain +# sample: bake._cmd_up_chain a.b => "a.b", "a", "root" +bake._cmd_up_chain() { + local path="${1:-root}" + local up="$path" + while [[ "$up" != "" ]]; do + printf '%s\n' "$up" + up=$(bake._path_dirname "$up" ".") + done + if [[ "$path" != "root" ]]; then printf "root\n"; fi +} +# Usage: bake._cmd_down_chain +# reverse of bake._cmd_up_chain +bake._cmd_down_chain() { + bake._cmd_up_chain "$1" | bake._str_revertLines +} + + +# 列出命令所有选项,子命令会继承父命令选项 +# Usage: bake._opts +# Examples: +# ./test.bash bake._opts bake.opt +# bake.opt/opts/short +# bake.opt/opts/cmd +# bake.opt/opts/default +# bake.opt/opts/desc +# bake.opt/opts/long +# bake.opt/opts/required +# bake.opt/opts/type +# root/opts/debug +# root/opts/help +# root/opts/interactive +bake._opts() { + local cmd=$1 + local upCmds + readarray -t upCmds <<<"$(bake._cmd_up_chain "$cmd")" + + local key + for key in "${!_bake_data[@]}"; do + if [[ "${_bake_data["$key"]}" != "type:opt" ]]; then continue; fi + local upCmd + for upCmd in "${upCmds[@]}"; do + if [[ "$key" == "$upCmd/opts/"* ]]; then + printf '%s\n' "$key" + fi + done + done | sort +} + +# only use by bake.opt, +# because "bake.opt" is meta function, use this func to add self +bake._opt_internal_add() { + local cmd="$1" long="$2" short="$3" type="$4" required="$5" default="$6" desc="$7" + _bake_data["$cmd/opts/$long"]="type:opt" + _bake_data["$cmd/opts/$long/long"]="$long" + _bake_data["$cmd/opts/$long/short"]="$short" + _bake_data["$cmd/opts/$long/type"]="$type" + _bake_data["$cmd/opts/$long/required"]="$required" + _bake_data["$cmd/opts/$long/default"]="$default" + _bake_data["$cmd/opts/$long/desc"]="$desc" +} + + +# Usage: bake._cmd_register +# ensure all cmd register +bake._cmd_register() { + local functionName + + while IFS=$'\n' read -r functionName; do + if [[ "$functionName" == */* ]]; then + echo "error: function $functionName() can not contains '/' " >&2 + return 1 + fi + local upCmd + for upCmd in $(bake._cmd_up_chain "$functionName"); do + # if upCmd is a function , set upCmd value to data path + if compgen -A function | grep -q "^$upCmd$"; then + _bake_cmds["$upCmd"]="$upCmd" + else + _bake_cmds["$upCmd"]="PARENT_CMD_NOT_FUNC" + fi + done + + # declare -F | grep "declare -f" 列出函数列表 + # => declare -f bake.cmd + # cut : + # -d " " => 指定delim分割符 + # -f 3 => 指定list列出第3个字段即函数名 + done <<< "$(declare -F | grep "declare -f" | cut -d " " -f 3) " +} + +# 显示一条命令的帮助 +# Usage: bake._help +# Examples: bake._help deploy #显示deploy的帮助: +bake._help() { + local cmd="${1:?bake._help() required 'cmd' arg, Usage: bake._help }" + shift + + echo + + if [[ "$cmd" == "root" ]] ;then + echo "Running:【: $(bake._pwd)/$BAKE_FILE $*】" + else + echo "Running:【$(bake._pwd)/$BAKE_FILE $cmd $*】" + fi + + # shellcheck disable=SC2154 + if [[ "$__debug" == true ]] ;then + echo "============================" + echo "__debug : $__debug" + echo "__help : $__help" + echo "\$* : 【$*】" + echo "BASH_SOURCE : 【${BASH_SOURCE[*]}】" + echo "========================================" + fi + + echo + echo "${_bake_data["${cmd}/desc"]}" + echo + + echo "Available Options:" + for optPath in $(bake._opts "$cmd"); do + local opt + opt=$(bake._path_basename "$optPath") + local long=${_bake_data["$optPath/long"]} + local type=${_bake_data["$optPath/type"]} + local required=${_bake_data["$optPath/required"]} + local short=${_bake_data["$optPath/short"]} + local default=${_bake_data["$optPath/default"]} + local desc="${_bake_data["$optPath/desc"]}" + + local optArgDesc="" + if [[ "$type" == "string" ]]; then + if [[ "$default" != "" ]]; then + optArgDesc+="<$type:${default}>" + else + optArgDesc="<$type>" + fi + fi + + printf " --%-20s -%-2s %-6s required:[%s] %b\n" "$long $optArgDesc" "$short" "$type" "$required" "$desc" + done + + echo " +Available Commands:" + for subCmd in $(bake._cmd_children "$cmd"); do + + # only show public cmd if not verbose + # '_'起头的命令和'bake'命令,只有debug模式才打印出来 + if [[ ("$subCmd" == _* || "$subCmd" == bake*) ]]; then + if [[ "$__debug" != "true" ]]; then + continue + fi + fi + + local subCmdPath="$cmd/$subCmd" + [[ "$cmd" == "root" ]] && subCmdPath="$subCmd" + + local desc="${_bake_data["$subCmdPath/desc"]}" + desc="$(echo -e "$desc" | head -n 1 )" # backslash escapes interpretation + + + local maxLengthOfCmd=0 + for child in $(bake._cmd_children "$cmd"); do + if ((${#child} > maxLengthOfCmd)); then maxLengthOfCmd=${#child}; fi + done + printf " %-$((maxLengthOfCmd))s ${desc}\n" "${subCmd}" + done +} + + +_bake_go_parse() { + # parse cmd : + # ./bake pub get -v -b + # -> { cmd:"pub.get", args:"-v -b" } + # ./bake -h + # -> { cmd:"", args:"-h" } + local cmd nextCmd arg + for arg in "$@"; do + nextCmd="$([[ "$cmd" == "" ]] && echo "$arg" || echo "$cmd.$arg")" + if [[ "${_bake_cmds["$nextCmd"]}" == "" ]]; then break; fi + cmd="$nextCmd" + shift + done + + if [[ "$cmd" == "" ]]; then cmd="root"; fi + eval "$(bake.parse "$cmd" "$@")" + + # shellcheck disable=SC2154 + if [[ "$__help" == "true" ]]; then + echo bake._help "$cmd" "$@" + return 0 + fi + + # if fileExist then show help + if ! declare -F "$cmd" | grep "$cmd" &>/dev/null 2>&1; then + if [[ "${_bake_cmds["$cmd"]}" == "PARENT_CMD_NOT_FUNC" ]]; then + echo bake._help "$cmd" "$@" + return 0 + fi + bake._throw "Error: 404 ,cmd '${cmd}' not define, please define cmd function '${cmd}()'" + fi + + echo "$cmd" "$@" +} + +########################################## +# bake api function +# 下面都是公开函数 +########################################## + + +# bake.opt (public api) +# 为cmd配置option +# Examples: +# bake.opt --cmd "build" --long "is_zip" --type bool --required --short z --default true --desc "is_zip, build项目时是否压缩" +# bake.opt自己的options: +# cmd: 参数作用的命令全名 +# long: 参数长名,可以 ./bake build --is_zip 这样使用 +# type: 类型,目前支持 bool|string|list +# required: 是否必须提供,不提供将报错 +# short: 参数短名, 可以 ./bake build -z 这样使用 +# default: 缺省值, 未指定参数时,使用此值 +# desc: 参数帮助,将显示在‘./bake build -h’命令帮助里 +# 参考[bake.parse] +bake.opt() { + local __long __short __type __required __cmd __default __desc + # 本函数自己的options定义在本文件最下方 + eval "$(bake.parse "$@")" + if [[ "$__long" == "" ]]; then + echo "error: option required [--long]" >&2 && return 1 + fi + if [[ "$__type" == "" ]]; then + echo "error: option required [--type]" >&2 && return 1 + fi + if [[ "$__type" != "bool" && "$__type" != "string" && "$__type" != "list" ]]; then + echo "error: option [--type] must in [bool|string|list] " >&2 && return 1 + fi + bake._opt_internal_add "$__cmd" "$__long" "$__short" "$__type" "${__required:-false}" "$__default" "$__desc" +} + +# bake.opt (public api) +# 像其他高级语言的cli工具一样,用简单变量就可以获取名称化的命令参数: +# 支持bool,string,list三种参数,用法如下: +# 你的./bake脚本里: +# bake.opt --cmd build --long "is_zip" --type bool +# bake.opt --cmd build --long "target" --type string +# bake.opt --cmd build --long "files" --type string +# function build() { +# # 模版代码,把生成的脚本eval出来 +# eval "$(bake.parse "$@")"; + +# echo "is_zip:$is_zip, target:$target, hosts:${hosts[@]}"; +# } +# 调用: +# ./bake build --target "macos" --is_zip --host host1 --host2 +# 调用结果是'bake.parse "$@"'将生成如下脚本: +# --------------------------------------------------------- +# declare __is_zip=true +# declare __target="macos" +# declare __hosts=("host1" "host2") +# declare __option_count=7 +# shift 6 +# --------------------------------------------------------- +# eval后,就可以直接使用变量了, 在函数中declare,不带-g参数默认为local变量,不会影响全局环境。 +# +# Usage: 固定格式:bake.parse "$@" +# 参考:[bake.opt] +bake.parse() { + local cmd="${FUNCNAME[1]}" + + # key is -h --help ... candidate words , + # value is optPath + declare -A allOptOnCmdChain + # collect opt from command chain : root>pub>pub.get + # root option first , priority low -> priority high: + for optPath in $(bake._opts "$cmd" | bake._str_revertLines); do + local opt + opt=$(bake._path_basename "$optPath") + local short + short=${_bake_data["$optPath/short"]} + allOptOnCmdChain["--$opt"]="${optPath}" + if [[ "$short" != "" ]]; then allOptOnCmdChain["-$short"]="${optPath}"; fi + done + + # dynamic opt variable map : optPath:optVarName + # Why use dynamic variables: because the variable long is not fixed + # and We want to manipulate arrays(list type opt) more conveniently + local -A optVars + local totalArgs="$#" + # while all args , until it is not opt + while (($# > 0)); do + # match $1 arg in allOptOnCmdChain, guess $1 is a "-h" "-help" ... + local optPath + optPath=${allOptOnCmdChain["$1"]} + # if next arg not a opt , parsing complete; + if [[ "${optPath}" == "" ]]; then break; fi + + # __ prefix : avoid conflicts + optVars["$optPath"]="__$(bake._path_basename "$optPath")" + declare "${optVars["$optPath"]}" + # reference to the current dynamic opt variable + declare -n currentOptValue=${optVars["$optPath"]} + + local optType=${_bake_data["$optPath/type"]} + case $optType in + bool) + currentOptValue=true + shift 1 + ;; + string) + [[ ! "${2+declare}" ]] && echo "parse error: opt need a value: $arg " >&2 && return 1 + currentOptValue="$2" + shift 2 + ;; + list) + [[ ! "${2+declare}" ]] && echo "parse error: opt need a value: $arg " >&2 && return 1 + currentOptValue+=("$2") # array add + shift 2 + ;; + *) + echo "parse error: not support $optPath.type: <$optType> " >&2 + return 1 + ;; + esac + done + for optPath in "${!optVars[@]}"; do + declare -p "${optVars["$optPath"]}" + done + echo "shift $(( totalArgs - $# ))" +} + + +# bake.cmd (public api) +# 注册一个命令的帮助信息 +# Examples: +# bake.cmd --cmd build --desc "build project" +# 尤其是可以配置root命令以定制根命令的帮助信息,比如: +# bake.cmd --cmd root \ +# --desc "flutter-note cli." +# 这样就可以用'./your_script -h' 查看根帮助了 +bake.cmd() { + local __cmd __desc + # 模版代码,放到每个需要使用option的函数中,然后就可以使用option了 + eval "$(bake.parse "$@")" + + if [[ "$__cmd" == "" ]]; then + echo "error: bake.cmd [--cmd] required " >&2 + return 1 + fi + _bake_data["$__cmd/desc"]="$__desc" +} + + +# list bake var info(public api),use for debug +# Usage: info +bake.info() { + +cat <<- EOF + + .----------------. .----------------. .----------------. .----------------. +| .--------------. || .--------------. || .--------------. || .--------------. | +| | ______ | || | __ | || | ___ ____ | || | _________ | | +| | |_ _ \ | || | / \ | || | |_ ||_ _| | || | |_ ___ | | | +| | | |_) | | || | / /\ \ | || | | |_/ / | || | | |_ \_| | | +| | | __'. | || | / ____ \ | || | | __'. | || | | _| _ | | +| | _| |__) | | || | _/ / \ \_ | || | _| | \ \_ | || | _| |___/ | | | +| | |_______/ | || ||____| |____|| || | |____||____| | || | |_________| | | +| | | || | | || | | || | | | +| '--------------' || '--------------' || '--------------' || '--------------' | + '----------------' '----------------' '----------------' '----------------' + +EOF + echo '# bake info & internal var' + echo + echo '## _bake_cmds' + for key in "${!_bake_cmds[@]}"; do + printf "%-60s = %q\n" "_bake_cmds[$key]" "${_bake_cmds["$key"]:0:100}" + done | sort + echo + echo '## _bake_data' + for key in "${!_bake_data[@]}"; do + printf "%-60s = %q\n" "_bake_data[$key]" "${_bake_data["$key"]:0:100}" + done | sort + echo + echo '## options' + echo "help = $__help" + echo "debug = $__debug" + echo + echo '## env' + echo "BASH_SOURCE = ${BASH_SOURCE[*]}" + echo +} + +# 入口 (public api) +bake.go() { + # init register all cmd + bake._cmd_register + + # ⚠️ 注意!本函数把外部参数作为命令执行,所以如果有变量一定要__xxxx__格式,避免影响外部程序 + # ⚠️ 高危场景:在a命令内执行传来的参数是高危操作,假设 : + # 1> a(){ local x="a()inner"; $@ ; } + # 2> b(){ echo "b()这里你猜能看到a的内部变量吗:x: $x" ; } + # 3> a b + # 打印结果 => 这里你猜能看到a的内部变量吗:x: a()inner + # + # 行3"a b",将使a函数在其内部执行b函数,这是高危操作,因为a函数的上下文暴露在b函数里,local也不例外 + # 所以我们用另一个函数_bake_go_parse隔离环境 + eval "$(_bake_go_parse "$@")" +} + + +# bake内部版本(public api) +bake.version(){ + echo "$_bake_version" +} + + +############################################################# +# bake tail 初始化区 +# bake.bash 在头head定义变量,尾tail执行初始化,中间只有函数 +############################################################# + +# Add the error catch first +#https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html#index-trap +trap "bake._on_error" ERR + +# 低级模式定义bake.opt函数的options +# bake.opt函数是注册options用的, 它自己也需要options,所以只能这样低级模式定义,吃自己的狗粮 +bake._opt_internal_add bake.opt "cmd" "" "string" "true" "" "cmd name" +bake._opt_internal_add bake.opt "long" "" "string" "true" "" "option long option: --port --host ..." +bake._opt_internal_add bake.opt "type" "" "string" "true" "" "option type [bool|string|list]" +bake._opt_internal_add bake.opt "required" "" "bool" "false" "false" "option required [true|false],default[false]" +bake._opt_internal_add bake.opt "short" "" "string" "false" "" "option short option: -a -b -d ..." +bake._opt_internal_add bake.opt "default" "" "string" "false" "" "option default" +bake._opt_internal_add bake.opt "desc" "" "string" "false" "" "option desc" + +# !!! 这之后,再也不用 低级option模式bake._opt_internal_add + +# 高级模式定义bake.cmd的options +bake.opt --cmd "bake.cmd" --long "cmd" --type string --desc "cmd function " +bake.opt --cmd "bake.cmd" --long "desc" --type string --desc "cmd desc, show in help" + +# root is special cmd(you can define it), bake add some common options to this cmd, you can add yourself options +bake.opt --cmd root --long "help" --short h --type bool --default false --desc "print help, show all commands" +bake.opt --cmd root --long "debug" --short d --type bool --default false --desc "debug mode, print more internal info" + +# TODO 非lib模式,即直接执行bake.bash,暂时未搞好 +if ((${#BASH_SOURCE[@]} == 1)); then + bake.go "$@" +fi + +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# bake common script end line. +# The above code is common code that is not related to the specific app, +# if you want to define app-related commands, +# please put them below or other file. +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! \ No newline at end of file diff --git a/packages/you_bake/examples/1.hello/bake b/packages/you_bake/examples/1.hello/bake new file mode 100755 index 00000000..8a664ee0 --- /dev/null +++ b/packages/you_bake/examples/1.hello/bake @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +######################################################## +# 范例:示范父子命令 +# 运行 ./bake -h 查看本文件的效果和帮助 +######################################################## + + +######################################################## +# 本节为模版代码,每个copy一下即可,主要是自动下载bake.bash依赖 +######################################################## + + +# 模版脚本,用来获取本脚本的realpath +# 我在我的所有脚本头都会放这个函数,方便得到真正的当前脚本目录 +# On Mac OS, readlink -f doesn't work, so use._real_path get the real path of the file +_real_path() ( + cd "$(dirname "$1")" || exit 200 + declare file + file="$PWD/$(basename "$1")" + while [[ -L "$file" ]]; do + file="$(readlink "$file")" + cd -P "$(dirname "$file")" || exit 200 + file="$PWD/$(basename "$file")" + done + echo "$file" +) + +SCRIPT_PATH="$(_real_path "${BASH_SOURCE[0]}")" +SCRIPT_DIR="$(dirname "$SCRIPT_PATH")" + +# 脚本动态安装bake.bash依赖到: vendor/bake.bash +_install_bake(){ + mkdir -p "$SCRIPT_DIR/vendor" + echo "$SCRIPT_PATH -> _install_bake ▶︎【curl -o $SCRIPT_DIR/bake.bash https://github.com/chen56/you/raw/main/packages/you_bake/bake.bash】" + curl -L -o "$SCRIPT_DIR/vendor/bake.bash" https://github.com/chen56/you/raw/main/packages/you_bake/bake.bash ; +} +if ! [[ -f "$SCRIPT_DIR/vendor/bake.bash" ]]; then + _install_bake +fi + +# include common script +source "$SCRIPT_DIR/vendor/bake.bash" + + +########################################## +# app cmd script +# 应用的命令脚本 +########################################## + +install(){ _install_bake ; } +clean(){ echo "clean project"; } +dev(){ echo "run dev mode"; } +preview(){ echo "run preview mode"; } +test(){ echo "test project"; } + +# 更深层的子命令: `./bake build -h` , `./bake build all` +build.all(){ echo "build all"; } +build.macos(){ echo "build macos package"; } +build.web(){ echo "build web package"; } +build.android(){ echo "build android package"; } +build.ios(){ echo "build ios package"; } + + +#################################################### +# bake entry +#################################################### +bake.go "$@" diff --git a/packages/you_bake/examples/1.hello/vendor/bake.bash b/packages/you_bake/examples/1.hello/vendor/bake.bash new file mode 100644 index 00000000..1f603696 --- /dev/null +++ b/packages/you_bake/examples/1.hello/vendor/bake.bash @@ -0,0 +1,733 @@ +#!/usr/bin/env bash +set -o errtrace # -E trap inherited in sub script +set -o errexit # -e +set -o functrace # -T If set, any trap on DEBUG and RETURN are inherited by shell functions +set -o pipefail # default pipeline status==last command status, If set, status=any command fail +#set -o nounset # -u: don't use it ,it is crazy, 1.bash version is diff Behavior 2.we need like this: ${arr[@]+"${arr[@]}"} + +_bake_version=v0.4.20240406 + +# It can run normally on macos +# bake == (bash)ake == 去Make的bash tool +# +# https://github.com/chen56/you/packages/you_bake +# +# bake 是个简单的命令行工具,以替代Makefile的子命令功能 +# make工具的主要特点是处理文件依赖进行增量编译,但flutter、golang、java、js项目的build工具 +# 太厉害了,这几年唯一还在用Makefile的理由就是他的子命令机制: "make build"、 +# "make run", 可以方便的自定义单一入口的父子命令,但Makefile本身的语法套路也很复杂, +# 很多批处理还是要靠bash, 这就尴尬了,工具太多,麻烦!本脚本尝试彻底摆脱使用Makefile。 +# 经尝试,代码很少啊 ,核心代码几百行啊,父子命令二三百行左右,option解析二三百行左右,功能足够了: +# +# bake命令规则: +# 1. 函数即命令,所有bake内的函数均可以在脚本外运行: +# ./bake [all function] # bake内的所有函数均可以在脚本外直接运行 +# ./bake info # 比如这个内部函数, 看bake内部变量,调试脚本用 +# ./bake test # 你如果定义过test()函数,就可以这样运行 +# 2. 带"."的函数,形成父子命令: +# web.build(){ echo "build web app"; } +# web.test(){ echo "build web app"; } +# 可以这样调用 +# ./bake web -h # 运行子命令,或看帮助 +# ./bake web.build -h # 上面等同 +# ./bake web test -h # 运行子命令,或看帮助 +# ./bake web.test -h # 与上面等同 +# 3. 特殊的root命令表示根命令 +# ./bake # 如果有root()函数,就执行它 +# 4. 像其他高级语言的cli工具一样,用简单变量就可以获取命令option: +# # a. 先在bake文件里里定义app options +# bake.opt --cmd build --long "target" --type string +# # b. 解析和使用option +# function build() { +# eval "$(bake.parse "$@")"; +# echo "build ... your option:target: $target"; +# } +# # c. 调用看看: +# ./bake build --target "macos" +# 5. bake尽量不依赖bash以外的其他工具,包括linux coreutils,更简单通用,但由于用了关联数组等 +# 依赖bash4+ +# 6. 有两种用法: +# - 这个文件copy走,把你的脚本放到本脚本最后即可. +# - 在你的脚本里直接curl下载本脚本后 source即可。 +# 范例可以看实际案例: +# - https://github.com/chen56/note/blob/main/bake +# - https://github.com/chen56/younpc/blob/main/bake + +############################################################# +# bake head 变量定义区 +# bake.bash 在头head定义变量,尾tail执行初始化,中间只有函数 +############################################################# + +# check bake dependencies +if ((BASH_VERSINFO[0] < 4 || (\ + BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4))); then + echo "Error: It's 2082 ,Your bash is still this version(BASH_VERSINFO: ${BASH_VERSINFO[*]}),Please install bash 4.4+: + apt install bash # ubuntu + brew install bash # mac" + exit 124 # =>http code 424 +fi + +# On Mac OS, readlink -f doesn't work, so use.bake._real_path get the real path of the file +bake._real_path() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" ; } + +# bake context +BAKE_PATH="$(bake._real_path "${BASH_SOURCE[0]}")" +# shellcheck disable=SC2034 +BAKE_DIR="$(dirname "$BAKE_PATH")" +BAKE_FILE="$(basename "$BAKE_PATH")" + +# defalut option:bake --debug --help +# bake.parse 会按bake.opt的定义动态生成相应变量,这里事先声明,是为了备注其存在 +# 请参考[bake.parse] +__debug=false +__help=false + + +# Simulating object-oriented data structures with flat associative arrays +# use ./bake _self to see internal var +# save all other data +declare -A _bake_data + +# only save all commands, we use it to build command tree +# it is cache cmd tree from _bake_data +declare -A _bake_cmds + + + +########################################## +# bake internal function +########################################## + + +bake._on_error() { + bake._error "ERROR - trapped an error: ↑ , trace: ↓" + local i=0 + local stackInfo + while true; do + stackInfo=$(caller $i 2>&1 && true) && true + if [[ $? != 0 ]]; then return 0; fi + + # 一行调用栈 '97 bake.build ./note/bake' + # 解析后 => 行号no=97 , 报错的函数func=bake.build , file=./note/bake + local no func file + IFS=' ' read -r no func file <<<"$stackInfo" + + # 打印出可读性强的信息: + # => ./note/bake:38 -> bake.build + printf "%s\n" "$(bake._real_path $file):$no -> $func" >&2 + + i=$((i + 1)) + done +} + + +# replace $HOME with "~" +# Usage: bake._pwd +# Examples: 当前目录如果是"/Users/chen/git/note/" +# 转成更简单易读的 => "~/git/note/" +bake._pwd() { echo "${PWD/#$HOME/\~}" ; } + +# 报错后终止程序,类似于其他语言的throw Excpetion +# 因set -o errexit 后,程序将在return 1 时退出, +# 退出前被‘trap bake._on_error ERR’捕获并显示错误堆栈 +# Usage: bake._throw +bake._throw(){ + bake._log FATAL "$@" + # set -o errexit 后,程序将退出,退出前被trap bake._on_error Err捕获并显示错误堆栈 + return 200 +} +bake._error() { + bake._log ERROR "$@" +} +bake._info() { + bake._log INFO "$@" +} +bake._debug() { + # shellcheck disable=SC2154 + if [[ "${__debug}" == true ]]; then + bake._log DEBUG "$@" + fi +} +# Usage: bake._log DEBUG "错误消息" +bake._log(){ + local level="$1" + echo -e "$level $(date "+%F %T") $(bake._pwd)\$ ${FUNCNAME[1]}() : $*" >&2 +} + + + + +# Usage: bake._str_cutLeft +# bake._path_dirname a/b/c a/b => c +bake._str_cutLeft() { printf "${1#$2}"; } + +# 分割字符串 +# Usage: bake._str_split [delimiter:default /] +# Example: bake._str_split "a/b/c" "/" +# => $'a\nb\nc' # 会把字符串分割为以换行符间隔的字符串 +bake._str_split() { + #${delimiter:-DEFAULT} unset 或null 都给默认值 + # https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html + local str="$1" + local delimiter="${2:-/}" + + # use <() process-substitution + # or here string <<< "" its add newline + local arr + # https://helpmanual.io/builtin/readarray/ + # -d The first character of delim is used to terminate each input line, rather than newline. + # -t Remove a trailing delim (default newline) from each line read. + readarray -t -d "$delimiter" arr <<< "${str}" + # same as: for i in "${arr[@]}"; do echo "$i" done + printf '%s\n' "${arr[@]}" +} + +# Usage: bake._str_revertLines <<< "$(echo -e "a\nb\nc")" => "c\nb\na" +bake._str_revertLines() { + # cat xxx | tail -r; # macos bsd only, not work on linux + # so use sed + sed '1!G;h;$!d' # sed magic +} + +# Usage: bake._path_dirname [delimiter:default /] +# similar command dirname, but diff: +# dirname a => '.' +# bake._path_dirname a => '' +# +# Example: delimiter: +# bake._path_dirname a.b.c "." => "a.b" +# bake._path_dirname a "." => "" +bake._path_dirname() { + local path="$1" + local delimiter="${2:-/}" + +# bake._path_dirname level1_path "." => "" + if [[ "$path" != *"$delimiter"* ]]; then + return + fi + echo "${path%"$delimiter"*}" +} +# Usage: bake._path_first [delimiter:default /] +# bake._path_first a.b.c . => a +bake._path_first() { + local pathLikeStr="$1" delimiter="${2:-/}" + # if /a/b/c , call :bake._path_first a/b/c / + if [[ "$pathLikeStr" == "$delimiter"* ]]; then + local removeDelim + removeDelim=$(bake._str_cutLeft "$pathLikeStr" "$delimiter") + printf "$delimiter$(bake._path_first "$removeDelim" "$delimiter")" + else # a/b/c + printf "${pathLikeStr%%$delimiter*}" + fi +} +# similar command basename +# Usage: bake._path_basename [delimiter:default /] +bake._path_basename() { + local pathLikeStr="$1" delimiter="${2:-/}" + # ${1##*/} => ## left remove until last "/" + echo "${pathLikeStr##*/}" +} + +# Samples: +# bake._cmd_children +# => list root children +# bake._cmd_children tests +# => list test children +bake._cmd_children() ( + local path="${1:-root}" # default arg is root + + # ./bake bake info列出命令注册表,_bake_cmds数组长这样: + # cmd - bake.opt = bake.opt + # cmd - bake.parse = bake.parse + # cmd - bake.str_escape = bake.str_escape + # cmd - bake.str_unescape = bake.str_unescape + # cmd - bake.version = bake.version + # cmd - root = PARENT_CMD_NOT_FUNC + # cmd - study = PARENT_CMD_NOT_FUNC + # cmd - study.array = study.array + # cmd - study.declare = study.declare + + # 单独列出root的子命令 + if [[ "$path" == root ]]; then + for full_name in "${!_bake_cmds[@]}"; do + # 不包含'.'的命令是一级命令,即root的子命令 + [[ $full_name == *"."* ]] && continue; + [[ $full_name == "root" ]] && continue; + + echo "$full_name" + done + return 0 + fi + + # ${!_bake_data[@]}: get all array keys + for key in "${!_bake_cmds[@]}"; do + # if start $path + if [[ "$key" == "$path."* && "$key" != "$path" && "$key" != "root" ]]; then + # https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html + # remove prefix : key:a.b.c leftPathToBeCut:a => b.c + child=$(bake._str_cutLeft "$key" "$path.") + # remove suffix: b.c => c + child=$(bake._path_first "$child" ".") + printf '%s\n' "$child" + fi + done # | sort -u +) + +# Usage: bake._cmd_up_chain +# sample: bake._cmd_up_chain a.b => "a.b", "a", "root" +bake._cmd_up_chain() { + local path="${1:-root}" + local up="$path" + while [[ "$up" != "" ]]; do + printf '%s\n' "$up" + up=$(bake._path_dirname "$up" ".") + done + if [[ "$path" != "root" ]]; then printf "root\n"; fi +} +# Usage: bake._cmd_down_chain +# reverse of bake._cmd_up_chain +bake._cmd_down_chain() { + bake._cmd_up_chain "$1" | bake._str_revertLines +} + + +# 列出命令所有选项,子命令会继承父命令选项 +# Usage: bake._opts +# Examples: +# ./test.bash bake._opts bake.opt +# bake.opt/opts/short +# bake.opt/opts/cmd +# bake.opt/opts/default +# bake.opt/opts/desc +# bake.opt/opts/long +# bake.opt/opts/required +# bake.opt/opts/type +# root/opts/debug +# root/opts/help +# root/opts/interactive +bake._opts() { + local cmd=$1 + local upCmds + readarray -t upCmds <<<"$(bake._cmd_up_chain "$cmd")" + + local key + for key in "${!_bake_data[@]}"; do + if [[ "${_bake_data["$key"]}" != "type:opt" ]]; then continue; fi + local upCmd + for upCmd in "${upCmds[@]}"; do + if [[ "$key" == "$upCmd/opts/"* ]]; then + printf '%s\n' "$key" + fi + done + done | sort +} + +# only use by bake.opt, +# because "bake.opt" is meta function, use this func to add self +bake._opt_internal_add() { + local cmd="$1" long="$2" short="$3" type="$4" required="$5" default="$6" desc="$7" + _bake_data["$cmd/opts/$long"]="type:opt" + _bake_data["$cmd/opts/$long/long"]="$long" + _bake_data["$cmd/opts/$long/short"]="$short" + _bake_data["$cmd/opts/$long/type"]="$type" + _bake_data["$cmd/opts/$long/required"]="$required" + _bake_data["$cmd/opts/$long/default"]="$default" + _bake_data["$cmd/opts/$long/desc"]="$desc" +} + + +# Usage: bake._cmd_register +# ensure all cmd register +bake._cmd_register() { + local functionName + + while IFS=$'\n' read -r functionName; do + if [[ "$functionName" == */* ]]; then + echo "error: function $functionName() can not contains '/' " >&2 + return 1 + fi + local upCmd + for upCmd in $(bake._cmd_up_chain "$functionName"); do + # if upCmd is a function , set upCmd value to data path + if compgen -A function | grep -q "^$upCmd$"; then + _bake_cmds["$upCmd"]="$upCmd" + else + _bake_cmds["$upCmd"]="PARENT_CMD_NOT_FUNC" + fi + done + + # declare -F | grep "declare -f" 列出函数列表 + # => declare -f bake.cmd + # cut : + # -d " " => 指定delim分割符 + # -f 3 => 指定list列出第3个字段即函数名 + done <<< "$(declare -F | grep "declare -f" | cut -d " " -f 3) " +} + +# 显示一条命令的帮助 +# Usage: bake._help +# Examples: bake._help deploy #显示deploy的帮助: +bake._help() { + local cmd="${1:?bake._help() required 'cmd' arg, Usage: bake._help }" + shift + + echo + + if [[ "$cmd" == "root" ]] ;then + echo "Running:【: $(bake._pwd)/$BAKE_FILE $*】" + else + echo "Running:【$(bake._pwd)/$BAKE_FILE $cmd $*】" + fi + + # shellcheck disable=SC2154 + if [[ "$__debug" == true ]] ;then + echo "============================" + echo "__debug : $__debug" + echo "__help : $__help" + echo "\$* : 【$*】" + echo "BASH_SOURCE : 【${BASH_SOURCE[*]}】" + echo "========================================" + fi + + echo + echo "${_bake_data["${cmd}/desc"]}" + echo + + echo "Available Options:" + for optPath in $(bake._opts "$cmd"); do + local opt + opt=$(bake._path_basename "$optPath") + local long=${_bake_data["$optPath/long"]} + local type=${_bake_data["$optPath/type"]} + local required=${_bake_data["$optPath/required"]} + local short=${_bake_data["$optPath/short"]} + local default=${_bake_data["$optPath/default"]} + local desc="${_bake_data["$optPath/desc"]}" + + local optArgDesc="" + if [[ "$type" == "string" ]]; then + if [[ "$default" != "" ]]; then + optArgDesc+="<$type:${default}>" + else + optArgDesc="<$type>" + fi + fi + + printf " --%-20s -%-2s %-6s required:[%s] %b\n" "$long $optArgDesc" "$short" "$type" "$required" "$desc" + done + + echo " +Available Commands:" + for subCmd in $(bake._cmd_children "$cmd"); do + + # only show public cmd if not verbose + # '_'起头的命令和'bake'命令,只有debug模式才打印出来 + if [[ ("$subCmd" == _* || "$subCmd" == bake*) ]]; then + if [[ "$__debug" != "true" ]]; then + continue + fi + fi + + local subCmdPath="$cmd/$subCmd" + [[ "$cmd" == "root" ]] && subCmdPath="$subCmd" + + local desc="${_bake_data["$subCmdPath/desc"]}" + desc="$(echo -e "$desc" | head -n 1 )" # backslash escapes interpretation + + + local maxLengthOfCmd=0 + for child in $(bake._cmd_children "$cmd"); do + if ((${#child} > maxLengthOfCmd)); then maxLengthOfCmd=${#child}; fi + done + printf " %-$((maxLengthOfCmd))s ${desc}\n" "${subCmd}" + done +} + + +_bake_go_parse() { + # parse cmd : + # ./bake pub get -v -b + # -> { cmd:"pub.get", args:"-v -b" } + # ./bake -h + # -> { cmd:"", args:"-h" } + local cmd nextCmd arg + for arg in "$@"; do + nextCmd="$([[ "$cmd" == "" ]] && echo "$arg" || echo "$cmd.$arg")" + if [[ "${_bake_cmds["$nextCmd"]}" == "" ]]; then break; fi + cmd="$nextCmd" + shift + done + + if [[ "$cmd" == "" ]]; then cmd="root"; fi + eval "$(bake.parse "$cmd" "$@")" + + # shellcheck disable=SC2154 + if [[ "$__help" == "true" ]]; then + echo bake._help "$cmd" "$@" + return 0 + fi + + # if fileExist then show help + if ! declare -F "$cmd" | grep "$cmd" &>/dev/null 2>&1; then + if [[ "${_bake_cmds["$cmd"]}" == "PARENT_CMD_NOT_FUNC" ]]; then + echo bake._help "$cmd" "$@" + return 0 + fi + bake._throw "Error: 404 ,cmd '${cmd}' not define, please define cmd function '${cmd}()'" + fi + + echo "$cmd" "$@" +} + +########################################## +# bake api function +# 下面都是公开函数 +########################################## + + +# bake.opt (public api) +# 为cmd配置option +# Examples: +# bake.opt --cmd "build" --long "is_zip" --type bool --required --short z --default true --desc "is_zip, build项目时是否压缩" +# bake.opt自己的options: +# cmd: 参数作用的命令全名 +# long: 参数长名,可以 ./bake build --is_zip 这样使用 +# type: 类型,目前支持 bool|string|list +# required: 是否必须提供,不提供将报错 +# short: 参数短名, 可以 ./bake build -z 这样使用 +# default: 缺省值, 未指定参数时,使用此值 +# desc: 参数帮助,将显示在‘./bake build -h’命令帮助里 +# 参考[bake.parse] +bake.opt() { + local __long __short __type __required __cmd __default __desc + # 本函数自己的options定义在本文件最下方 + eval "$(bake.parse "$@")" + if [[ "$__long" == "" ]]; then + echo "error: option required [--long]" >&2 && return 1 + fi + if [[ "$__type" == "" ]]; then + echo "error: option required [--type]" >&2 && return 1 + fi + if [[ "$__type" != "bool" && "$__type" != "string" && "$__type" != "list" ]]; then + echo "error: option [--type] must in [bool|string|list] " >&2 && return 1 + fi + bake._opt_internal_add "$__cmd" "$__long" "$__short" "$__type" "${__required:-false}" "$__default" "$__desc" +} + +# bake.opt (public api) +# 像其他高级语言的cli工具一样,用简单变量就可以获取名称化的命令参数: +# 支持bool,string,list三种参数,用法如下: +# 你的./bake脚本里: +# bake.opt --cmd build --long "is_zip" --type bool +# bake.opt --cmd build --long "target" --type string +# bake.opt --cmd build --long "files" --type string +# function build() { +# # 模版代码,把生成的脚本eval出来 +# eval "$(bake.parse "$@")"; + +# echo "is_zip:$is_zip, target:$target, hosts:${hosts[@]}"; +# } +# 调用: +# ./bake build --target "macos" --is_zip --host host1 --host2 +# 调用结果是'bake.parse "$@"'将生成如下脚本: +# --------------------------------------------------------- +# declare __is_zip=true +# declare __target="macos" +# declare __hosts=("host1" "host2") +# declare __option_count=7 +# shift 6 +# --------------------------------------------------------- +# eval后,就可以直接使用变量了, 在函数中declare,不带-g参数默认为local变量,不会影响全局环境。 +# +# Usage: 固定格式:bake.parse "$@" +# 参考:[bake.opt] +bake.parse() { + local cmd="${FUNCNAME[1]}" + + # key is -h --help ... candidate words , + # value is optPath + declare -A allOptOnCmdChain + # collect opt from command chain : root>pub>pub.get + # root option first , priority low -> priority high: + for optPath in $(bake._opts "$cmd" | bake._str_revertLines); do + local opt + opt=$(bake._path_basename "$optPath") + local short + short=${_bake_data["$optPath/short"]} + allOptOnCmdChain["--$opt"]="${optPath}" + if [[ "$short" != "" ]]; then allOptOnCmdChain["-$short"]="${optPath}"; fi + done + + # dynamic opt variable map : optPath:optVarName + # Why use dynamic variables: because the variable long is not fixed + # and We want to manipulate arrays(list type opt) more conveniently + local -A optVars + local totalArgs="$#" + # while all args , until it is not opt + while (($# > 0)); do + # match $1 arg in allOptOnCmdChain, guess $1 is a "-h" "-help" ... + local optPath + optPath=${allOptOnCmdChain["$1"]} + # if next arg not a opt , parsing complete; + if [[ "${optPath}" == "" ]]; then break; fi + + # __ prefix : avoid conflicts + optVars["$optPath"]="__$(bake._path_basename "$optPath")" + declare "${optVars["$optPath"]}" + # reference to the current dynamic opt variable + declare -n currentOptValue=${optVars["$optPath"]} + + local optType=${_bake_data["$optPath/type"]} + case $optType in + bool) + currentOptValue=true + shift 1 + ;; + string) + [[ ! "${2+declare}" ]] && echo "parse error: opt need a value: $arg " >&2 && return 1 + currentOptValue="$2" + shift 2 + ;; + list) + [[ ! "${2+declare}" ]] && echo "parse error: opt need a value: $arg " >&2 && return 1 + currentOptValue+=("$2") # array add + shift 2 + ;; + *) + echo "parse error: not support $optPath.type: <$optType> " >&2 + return 1 + ;; + esac + done + for optPath in "${!optVars[@]}"; do + declare -p "${optVars["$optPath"]}" + done + echo "shift $(( totalArgs - $# ))" +} + + +# bake.cmd (public api) +# 注册一个命令的帮助信息 +# Examples: +# bake.cmd --cmd build --desc "build project" +# 尤其是可以配置root命令以定制根命令的帮助信息,比如: +# bake.cmd --cmd root \ +# --desc "flutter-note cli." +# 这样就可以用'./your_script -h' 查看根帮助了 +bake.cmd() { + local __cmd __desc + # 模版代码,放到每个需要使用option的函数中,然后就可以使用option了 + eval "$(bake.parse "$@")" + + if [[ "$__cmd" == "" ]]; then + echo "error: bake.cmd [--cmd] required " >&2 + return 1 + fi + _bake_data["$__cmd/desc"]="$__desc" +} + + +# list bake var info(public api),use for debug +# Usage: info +bake.info() { + +cat <<- EOF + + .----------------. .----------------. .----------------. .----------------. +| .--------------. || .--------------. || .--------------. || .--------------. | +| | ______ | || | __ | || | ___ ____ | || | _________ | | +| | |_ _ \ | || | / \ | || | |_ ||_ _| | || | |_ ___ | | | +| | | |_) | | || | / /\ \ | || | | |_/ / | || | | |_ \_| | | +| | | __'. | || | / ____ \ | || | | __'. | || | | _| _ | | +| | _| |__) | | || | _/ / \ \_ | || | _| | \ \_ | || | _| |___/ | | | +| | |_______/ | || ||____| |____|| || | |____||____| | || | |_________| | | +| | | || | | || | | || | | | +| '--------------' || '--------------' || '--------------' || '--------------' | + '----------------' '----------------' '----------------' '----------------' + +EOF + echo '# bake info & internal var' + echo + echo '## _bake_cmds' + for key in "${!_bake_cmds[@]}"; do + printf "%-60s = %q\n" "_bake_cmds[$key]" "${_bake_cmds["$key"]:0:100}" + done | sort + echo + echo '## _bake_data' + for key in "${!_bake_data[@]}"; do + printf "%-60s = %q\n" "_bake_data[$key]" "${_bake_data["$key"]:0:100}" + done | sort + echo + echo '## options' + echo "help = $__help" + echo "debug = $__debug" + echo + echo '## env' + echo "BASH_SOURCE = ${BASH_SOURCE[*]}" + echo +} + +# 入口 (public api) +bake.go() { + # init register all cmd + bake._cmd_register + + # ⚠️ 注意!本函数把外部参数作为命令执行,所以如果有变量一定要__xxxx__格式,避免影响外部程序 + # ⚠️ 高危场景:在a命令内执行传来的参数是高危操作,假设 : + # 1> a(){ local x="a()inner"; $@ ; } + # 2> b(){ echo "b()这里你猜能看到a的内部变量吗:x: $x" ; } + # 3> a b + # 打印结果 => 这里你猜能看到a的内部变量吗:x: a()inner + # + # 行3"a b",将使a函数在其内部执行b函数,这是高危操作,因为a函数的上下文暴露在b函数里,local也不例外 + # 所以我们用另一个函数_bake_go_parse隔离环境 + eval "$(_bake_go_parse "$@")" +} + + +# bake内部版本(public api) +bake.version(){ + echo "$_bake_version" +} + + +############################################################# +# bake tail 初始化区 +# bake.bash 在头head定义变量,尾tail执行初始化,中间只有函数 +############################################################# + +# Add the error catch first +#https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html#index-trap +trap "bake._on_error" ERR + +# 低级模式定义bake.opt函数的options +# bake.opt函数是注册options用的, 它自己也需要options,所以只能这样低级模式定义,吃自己的狗粮 +bake._opt_internal_add bake.opt "cmd" "" "string" "true" "" "cmd name" +bake._opt_internal_add bake.opt "long" "" "string" "true" "" "option long option: --port --host ..." +bake._opt_internal_add bake.opt "type" "" "string" "true" "" "option type [bool|string|list]" +bake._opt_internal_add bake.opt "required" "" "bool" "false" "false" "option required [true|false],default[false]" +bake._opt_internal_add bake.opt "short" "" "string" "false" "" "option short option: -a -b -d ..." +bake._opt_internal_add bake.opt "default" "" "string" "false" "" "option default" +bake._opt_internal_add bake.opt "desc" "" "string" "false" "" "option desc" + +# !!! 这之后,再也不用 低级option模式bake._opt_internal_add + +# 高级模式定义bake.cmd的options +bake.opt --cmd "bake.cmd" --long "cmd" --type string --desc "cmd function " +bake.opt --cmd "bake.cmd" --long "desc" --type string --desc "cmd desc, show in help" + +# root is special cmd(you can define it), bake add some common options to this cmd, you can add yourself options +bake.opt --cmd root --long "help" --short h --type bool --default false --desc "print help, show all commands" +bake.opt --cmd root --long "debug" --short d --type bool --default false --desc "debug mode, print more internal info" + +# TODO 非lib模式,即直接执行bake.bash,暂时未搞好 +if ((${#BASH_SOURCE[@]} == 1)); then + bake.go "$@" +fi + +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# bake common script end line. +# The above code is common code that is not related to the specific app, +# if you want to define app-related commands, +# please put them below or other file. +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! \ No newline at end of file diff --git a/packages/you_bake/examples/2.define_cmd_help/bake b/packages/you_bake/examples/2.define_cmd_help/bake new file mode 100755 index 00000000..14b9e20c --- /dev/null +++ b/packages/you_bake/examples/2.define_cmd_help/bake @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +######################################################## +# 范例:示范父子命令,并为每个命令定义帮助 +# 运行 ./bake -h 查看本文件的效果和帮助 +######################################################## + + +######################################################## +# 本节为模版代码,每个copy一下即可,主要是自动下载bake.bash依赖 +######################################################## + + +# 模版脚本,用来获取本脚本的realpath +# 我在我的所有脚本头都会放这个函数,方便得到真正的当前脚本目录 +# On Mac OS, readlink -f doesn't work, so use._real_path get the real path of the file +_real_path() ( + cd "$(dirname "$1")" + declare file="$PWD/$(basename "$1")" + while [[ -L "$file" ]]; do + file="$(readlink "$file")" + cd -P "$(dirname "$file")" + file="$PWD/$(basename "$file")" + done + echo "$file" +) + +SCRIPT_PATH="$(_real_path "${BASH_SOURCE[0]}")" +SCRIPT_DIR="$(dirname "$SCRIPT_PATH")" +SCRIPT_FILE="$(basename "$SCRIPT_PATH")" + +# 脚本动态安装bake.bash依赖到: vendor/bake.bash +_install_bake(){ + mkdir -p "$SCRIPT_DIR/vendor" + echo "$SCRIPT_PATH -> _install_bake ▶︎【curl -o $SCRIPT_DIR/bake.bash https://github.com/chen56/you/raw/main/packages/you_bake/bake.bash】" + curl -L -o "$SCRIPT_DIR/vendor/bake.bash" https://github.com/chen56/you/raw/main/packages/you_bake/bake.bash ; +} +if ! [[ -f "$SCRIPT_DIR/vendor/bake.bash" ]]; then + _install_bake +fi + +# include common script +source "$SCRIPT_DIR/vendor/bake.bash" + + +########################################## +# app cmd script +# 应用的命令脚本 +########################################## + + +# 用bake.cmd命令定义root帮助 +bake.cmd --cmd root --desc "$( cat <<-EOF + +bake cli example. + +https://github.com/chen56/younpc + +Usage: + ./$SCRIPT_FILE [cmd] [opts] [args...] + +Examples: + ./${SCRIPT_FILE} # same as './${SCRIPT_FILE} -h' + ./${SCRIPT_FILE} --help # show all commands help + ./${SCRIPT_FILE} -h --debug # show all commands help , include internal function + + ./${SCRIPT_FILE} test # test all pkgs + ./${SCRIPT_FILE} build # defalut build == flutter build web --web-renderer html + ./${SCRIPT_FILE} preview # defalut preview == run server at web build + ./${SCRIPT_FILE} test # test all pkgs + +EOF + )" + + +# 用bake.cmd命令定义其他命令帮助 +bake.cmd --cmd install --desc "install deps" +bake.cmd --cmd clean --desc "clean project" +bake.cmd --cmd dev --desc "run dev mode" +bake.cmd --cmd preview --desc "run preview mode" +bake.cmd --cmd test --desc "test project" +bake.cmd --cmd build --desc "build project" + +install(){ _install_bake ; } +clean(){ echo "clean project"; } +dev(){ echo "run dev mode"; } +preview(){ echo "run preview mode"; } +test(){ echo "test project"; } +build(){ echo "build project"; } + +#################################################### +# bake entry +#################################################### +bake.go "$@" diff --git a/packages/you_bake/examples/2.define_cmd_help/vendor/bake.bash b/packages/you_bake/examples/2.define_cmd_help/vendor/bake.bash new file mode 100644 index 00000000..f1fe15b4 --- /dev/null +++ b/packages/you_bake/examples/2.define_cmd_help/vendor/bake.bash @@ -0,0 +1,679 @@ +#!/usr/bin/env bash +set -o errtrace # -E trap inherited in sub script +set -o errexit # -e +set -o functrace # -T If set, any trap on DEBUG and RETURN are inherited by shell functions +set -o pipefail # default pipeline status==last command status, If set, status=any command fail +#set -o nounset # -u: don't use it ,it is crazy, 1.bash version is diff Behavior 2.we need like this: ${arr[@]+"${arr[@]}"} + +_bake_version=v0.3.20240327 + +# v0.2.20230528 - It can run normally on macos +# bake == (bash)ake == 去Make的bash tool +# +# https://github.com/chen56/bake +# +# bake 是个简单的命令行工具,以替代Makefile的子命令功能 +# make工具的主要特点是处理文件依赖进行增量编译,但flutter、golang、java、js项目的build工具 +# 太厉害了,这几年唯一还在用Makefile的理由就是他的子命令机制: "make build"、 +# "make run", 可以方便的自定义单一入口的父子命令,但Makefile本身的语法套路也很复杂, +# 很多批处理还是要靠bash, 这就尴尬了,工具太多,麻烦!本脚本尝试彻底摆脱使用Makefile。 +# 经尝试,代码很少啊 ,核心代码几百行啊,父子命令二三百行左右,option解析二三百行左右,功能足够了: +# +# bake命令规则: +# 1. 函数即命令,所有bake内的函数均可以在脚本外运行: +# ./bake [all function] # bake内的所有函数均可以在脚本外直接运行 +# ./bake info # 比如这个内部函数, 看bake内部变量,调试脚本用 +# ./bake test # 你如果定义过test()函数,就可以这样运行 +# 2. 带"."的函数,形成父子命令: +# web.build(){ echo "build web app"; } +# web.test(){ echo "build web app"; } +# 可以这样调用 +# ./bake web -h # 运行子命令,或看帮助 +# ./bake web.build -h # 上面等同 +# ./bake web test -h # 运行子命令,或看帮助 +# ./bake web.test -h # 与上面等同 +# 3. 特殊的root命令表示根命令 +# ./bake # 如果有root()函数,就执行它 +# 4. 像其他高级语言的cli工具一样,用简单变量就可以获取命令option: +# # a. 先在bake文件里里定义app options +# bake.opt --cmd build --name "target" --type string +# # b. 解析和使用option +# function build() { +# eval "$(bake.parse "${FUNCNAME[0]}" "$@")"; +# echo "build ... your option:target: $target"; +# } +# # c. 调用看看: +# ./bake build --target "macos" +# 5. bake尽量不依赖bash以外的其他工具,包括linux coreutils,更简单通用,但由于用了关联数组等 +# 依赖bash4+ +# 6. 有两种用法: +# - 这个文件copy走,把你的脚本放到本脚本最后即可. +# - 在你的脚本里直接curl下载本脚本后 source即可。 +# 范例可以看实际案例: +# - https://github.com/chen56/note/blob/main/bake +# - https://github.com/chen56/younpc/blob/main/bake +# todo +# 1. 当前 无法判断错误命令:./bake no_this_cmd ,因为不知道这是否是此命令的参数, +# 需要设置设一个简单的规则:只有叶子命令才能正常执行,这样非叶子命令就不需要有参数 +# 2. 当前 无法判断错误options:./bake --no_this_opt ,同上 +# 3. 类似flutter run [no-]pub 反向选项 +# + + +# check bake dependencies +if ((BASH_VERSINFO[0] < 4 || (\ + BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4))); then + echo "Error: It's 2082 ,Your bash is still this version(BASH_VERSINFO: ${BASH_VERSINFO[*]}),Please install bash 4.4+: + apt install bash # ubuntu + brew install bash # mac" + exit 1 +fi +# On Mac OS, readlink -f doesn't work, so use.bake._real_path get the real path of the file +bake._real_path() ( + cd "$(dirname "$1")" + file="$PWD/$(basename "$1")" + while [[ -L "$file" ]]; do + file="$(readlink "$file")" + cd -P "$(dirname "$file")" + file="$PWD/$(basename "$file")" + done + echo "$file" +) + +# bake context +BAKE_PATH="$(bake._real_path "${BASH_SOURCE[0]}")" +BAKE_DIR="$(dirname "$BAKE_PATH")" +BAKE_FILE="$(basename "$BAKE_PATH")" +cd "${BAKE_DIR}" # set workdir +declare debug=false +declare help=false + +bake._on_error() { + bake._error "ERROR - trapped an error: ↑ , trace: ↓" + local i=0 + local stackInfo + while true; do + stackInfo=$(caller $i 2>&1 && true) && true + if [[ $? != 0 ]]; then return 0; fi + + # 一行调用栈 '97 bake.build ./note/bake' + # 解析后 => 行号no=97 , 报错的函数func=bake.build , file=./note/bake + local no func file + IFS=' ' read -r no func file <<<"$stackInfo" + + # 打印出可读性强的信息: + # => ./note/bake:38 -> bake.build + printf "%s\n" "$(bake._real_path $file):$no -> $func" >&2 + + i=$((i + 1)) + done +} +# Add the error catch first +#https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html#index-trap +trap "bake._on_error" ERR + + +# replace $HOME with "~" +# Usage: bake._pwd +# Examples: 当前目录如果是"/Users/chen/git/note/" +# 转成更简单易读的 => "~/git/note/" +bake._pwd() { echo "${PWD/#$HOME/\~}" ; } + +# 报错后终止程序,类似于其他语言的throw Excpetion +# 因set -o errexit 后,程序将在return 1 时退出, +# 退出前被‘trap bake._on_error ERR’捕获并显示错误堆栈 +# Usage: bake._throw +bake._throw(){ + bake._error "$@" + # set -o errexit 后,程序将退出,退出前被trap bake._on_error Err捕获并显示错误堆栈 + return 1 +} +bake._error() { + if [[ "${_LOG_LEVELS[@]:0}" != *"$LOG"* ]]; then return 0; fi + bake._log "$@" +} +bake._info() { + bake._log "$@" +} +bake._debug() { + if [[ "${debug}" != true ]]; then return 0; fi + bake._log "$@" +} +# Usage: bake._log DEBUG "错误消息" +bake._log(){ + local level; + level=$1 + if [[ "${_LOG_LEVELS[@]:2}" != *"$LOG"* ]]; then return 0; fi + echo -e "$level $(date "+%F %T") $(bake._pwd)\$ ${FUNCNAME[1]}() : $*" >&2 +} + + +########################################## +# bake common script +########################################## + +# Simulating object-oriented data structures with flat associative arrays +# use ./bake _self to see internal var +# save all other data +declare -A _bake_data + +# only save all commands, we use it to build command tree +# it is cache cmd tree from _bake_data +declare -A _bake_cmds + +TYPE_CMD="type:cmd" + +########################################## +# bake common function +########################################## + +# Usage: bake._str_cutLeft +# bake._path_dirname a/b/c a/b => c +bake._str_cutLeft() { printf "${1#$2}"; } + +# Usage: bake.str.split [delimiter:default /] +bake._str_split() { + local str=$1 delimiter=${2:-/} + # # use <() process-substitution + # # don't use <<< "" its add newline + local arr + readarray -t arr < <(printf '%s' "${str//$delimiter/$'\n'}") + printf '%s\n' "${arr[@]}" +} + +# Usage: bake._str_revertLines <<< "$(echo -e "a\nb\nc")" => "c\nb\na" +bake._str_revertLines() { + # cat xxx | tail -r; # macos bsd only, not work on linux + # so use sed + sed '1!G;h;$!d' # sed magic +} + +# Usage: bake._path_dirname [delimiter:default /] +# similar command dirname, but +# dirname root is ".", only work with "/" +# bake._path_dirname root is "" , can set delimiter +# bake._path_dirname a.b.c . => a.b +bake._path_dirname() { + local pathLikeStr="$1" delimiter="${2:-/}" + if [[ "$pathLikeStr" != *"$delimiter"* ]]; then + return + fi + printf '%s' "${pathLikeStr%$delimiter*}" +} +# Usage: bake._path_first [delimiter:default /] +# bake._path_first a.b.c . => a +bake._path_first() { + local pathLikeStr="$1" delimiter="${2:-/}" + # if /a/b/c , call :bake._path_first a/b/c / + if [[ "$pathLikeStr" == "$delimiter"* ]]; then + local removeDelim + removeDelim=$(bake._str_cutLeft "$pathLikeStr" "$delimiter") + printf "$delimiter$(bake._path_first "$removeDelim" "$delimiter")" + else # a/b/c + printf "${pathLikeStr%%$delimiter*}" + fi +} +# similar command basename +# Usage: bake._path_basename [delimiter:default /] +bake._path_basename() { + local pathLikeStr="$1" delimiter="${2:-/}" + # ${1##*/} => ## left remove until last "/" + printf "${pathLikeStr##*$delimiter}" +} + +# Usage: bake._data_children +# return children name +bake._data_children() { + local path="$1" + # ${!_bake_data[@]}: get all array keys + local key + for key in "${!_bake_data[@]}"; do + # if start $path + if [[ "$key" == "$path"* ]]; then + # https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html + # remove prefix : key:build/opts/dir/type/x leftPathToBeCut:build/opts => dir/type/x + local child=$(bake._str_cutLeft "$key" "$path/") + # remove suffix: dir/type/x => dir + child=$(bake._path_first "$child" "/") + printf '%s\n' "$child" + fi + done | sort -u +} + +bake._cmd_childrenNameMaxLength() { + local cmd="$1" maxLengthOfCmd=0 + for child in $(bake._cmd_children "$cmd"); do + if ((${#child} > maxLengthOfCmd)); then maxLengthOfCmd=${#child}; fi + done + printf "$maxLengthOfCmd" +} +bake._cmd_children() ( + local path="$1" + if [[ "$path" == root ]]; then + path="" + fi + + # ${!_bake_data[@]}: get all array keys + for key in "${!_bake_cmds[@]}"; do + # if start $path + if [[ "$key" == "$path"* && "$key" != "$path" && "$key" != "root" ]]; then + # https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html + # remove prefix : key:a.b.c leftPathToBeCut:a => b.c + child=$(bake._str_cutLeft "$key" "$path.") + # remove suffix: b.c => c + child=$(bake._path_first "$child" ".") + printf '%s\n' "$child" + fi + done | sort -u +) + +# Usage: bake._cmd_up_chain +# sample: bake._cmd_up_chain a.b => "a.b", "a", "root" +bake._cmd_up_chain() { + local path="${1:-root}" + local up="$path" + while [[ "$up" != "" ]]; do + printf '%s\n' "$up" + up=$(bake._path_dirname "$up" ".") + done + if [[ "$path" != "root" ]]; then printf "root\n"; fi +} +# Usage: bake._cmd_down_chain +# reverse of bake._cmd_up_chain +bake._cmd_down_chain() { + bake._cmd_up_chain "$1" | bake._str_revertLines +} + +# Usage: bake._opt_cmd_chain_opts +# Examples: bake._opt_cmd_chain_opts bake.info +# return optionDataPath list +bake._opt_cmd_chain_opts() { + local cmd=$1 + local upCmds + readarray -t upCmds <<<"$(bake._cmd_up_chain "$cmd")" + + local key + for key in "${!_bake_data[@]}"; do + if [[ "${_bake_data["$key"]}" != "type:opt" ]]; then continue; fi + local upCmd + for upCmd in "${upCmds[@]}"; do + if [[ "$key" == "$upCmd/opts/"* ]]; then + printf '%s\n' "$key" + fi + done + done | sort +} + +# only use by bake.opt, +# because "bake.opt" is meta function, use this func to add self +bake._opt_internal_add() { + local cmd="$1" opt="$2" type="$3" required="$4" default="$5" abbr="$6" desc="$7" + _bake_data["$cmd/opts/$opt"]="type:opt" + _bake_data["$cmd/opts/$opt/name"]="$opt" + _bake_data["$cmd/opts/$opt/type"]="$type" + _bake_data["$cmd/opts/$opt/required"]="$required" + _bake_data["$cmd/opts/$opt/abbr"]="$abbr" + _bake_data["$cmd/opts/$opt/default"]="$default" + _bake_data["$cmd/opts/$opt/desc"]="$desc" +} + + +# Usage: bake._cmd_register +# ensure all cmd register +bake._cmd_register() { + local functionName + + while IFS=$'\n' read -r functionName; do + if [[ "$functionName" == */* ]]; then + echo "error: function $functionName() can not contains '/' " >&2 + return 1 + fi + local upCmd + for upCmd in $(bake._cmd_up_chain "$functionName"); do + # if upCmd is a function , set upCmd value to data path + if compgen -A function | grep -q "^$upCmd$"; then + _bake_cmds["$upCmd"]="$upCmd" + else + _bake_cmds["$upCmd"]="PARENT_CMD_NOT_FUNC" + fi + done + + # list all function name + # declare -F | awk {'print $3'} == compgen -A function + # declare -f func1 -> func1 + # declare -fx func2 -> func2 +# done <<<"$(compgen -A function)" + done <<<"$(declare -F | grep "declare -f" | awk {'print $3'})" +} + +# 显示一条命令的帮助 +# Usage: bake._show_cmd_help +# Examples: bake._show_cmd_help deploy #显示deploy的帮助: +bake._show_cmd_help() { + local cmd="$1" + + if [[ "$cmd" == "" ]]; then + bake._throw "bake._show_cmd_help need a arg: bake._show_cmd_help [cmd]" + fi + + shift + + eval "$(bake.parse "${FUNCNAME[0]}" "$@")" + + echo + + if [[ "$cmd" == "root" ]] ;then + echo "Running:【: $(bake._pwd)/$BAKE_FILE $@】" + else + echo "Running:【$(bake._pwd)/$BAKE_FILE $cmd $@】" + fi + + echo + echo "${_bake_data["${cmd}/desc"]}" + echo + + echo "Available Options:" + for optPath in $(bake._opt_cmd_chain_opts "$cmd"); do + local opt=$(bake._path_basename "$optPath") + local name=${_bake_data["$optPath/name"]} + local type=${_bake_data["$optPath/type"]} + local required=${_bake_data["$optPath/required"]} + local abbr=${_bake_data["$optPath/abbr"]} + local default=${_bake_data["$optPath/default"]} + local desc="${_bake_data["$optPath/desc"]}" + + local optArgDesc="" + if [[ "$type" == "string" ]]; then + if [[ "$default" != "" ]]; then + optArgDesc+="<$type:${default}>" + else + optArgDesc="<$type>" + fi + fi + + printf " --%-20s -%-2s %-6s required:[%s] %b\n" "$name $optArgDesc" "$abbr" "$type" "$required" "$desc" + done + + echo " +Available Commands:" + local maxLengthOfCmd + maxLengthOfCmd="$(bake._cmd_childrenNameMaxLength "$cmd")" + + for subCmd in $(bake._cmd_children "$cmd"); do + + # only show public cmd if not verbose + # '_'起头的命令和'bake'命令,只有debug模式才打印出来 + if [[ ("$subCmd" == _* || "$subCmd" == bake*) ]]; then + if [[ "$debug" != "true" ]]; then + continue + fi + fi + + local subCmdPath="$cmd/$subCmd" + [[ "$cmd" == "root" ]] && subCmdPath="$subCmd" + + local desc="${_bake_data["$subCmdPath/desc"]}" + desc="$(echo -e "$desc" | head -n 1 )" # backslash escapes interpretation + + printf " %-$((maxLengthOfCmd))s ${desc}\n" "${subCmd}" + done +} + + +# 为cmd配置参数(public api) +# Examples: +# bake.opt --cmd "build" --name "is_zip" --type bool --required --abbr z --default true --desc "is_zip, build项目时是否压缩" +# 每个参数可以配置如下信息: +# cmd: 参数作用的命令全名 +# name: 参数长名,可以 ./bake build --is_zip 这样使用 +# type: 类型,目前支持 bool|string|list +# required: 是否必须提供,不提供将报错 +# abbr: 参数短名, 可以 ./bake build -z 这样使用 +# default: 缺省值, 未指定参数时,使用此值 +# desc: 参数帮助,将显示在‘./bake build -h’命令帮助里 +# 参考[bake.parse] +bake._opt_internal_add bake.opt "cmd" "string" "true" "" "" "cmd name" +bake._opt_internal_add bake.opt "name" "string" "true" "" "" "option name" +bake._opt_internal_add bake.opt "type" "string" "true" "" "" "option type [bool|string|list]" +bake._opt_internal_add bake.opt "required" "bool" "false" "false" "false" "option required [true|false],default[false]" +bake._opt_internal_add bake.opt "abbr" "string" "false" "" "" "option abbr" +bake._opt_internal_add bake.opt "default" "string" "false" "" "" "option abbr" +bake._opt_internal_add bake.opt "desc" "string" "false" "" "" "option desc" +bake.opt() { + eval "$(bake.parse ""${FUNCNAME[0]}"" "$@")" + if [[ "$name" == "" ]]; then + echo "error: option [--name] required " >&2 && return 1 + fi + if [[ "$type" == "" ]]; then + echo "error: option [--type] required " >&2 && return 1 + fi + if [[ "$type" != "bool" && "$type" != "string" && "$type" != "list" ]]; then + echo "error: option [--type] must in [bool|string|list] " >&2 && return 1 + fi + bake._opt_internal_add "$cmd" "$name" "$type" "${required:-false}" "$default" "$abbr" "$desc" +} + +# bake.opt (public api) +# 像其他高级语言的cli工具一样,用简单变量就可以获取名称化的命令参数: +# 支持bool,string,list三种参数,用法如下: +# 你的./bake脚本里: +# bake.opt --cmd build --name "is_zip" --type bool +# bake.opt --cmd build --name "target" --type string +# bake.opt --cmd build --name "files" --type string +# function build() { +# # 模版代码,把生成的脚本eval出来 +# eval "$(bake.parse "${FUNCNAME[0]}" "$@")"; + +# echo "is_zip:$is_zip, target:$target, hosts:${hosts[@]}"; +# } +# 调用: +# ./bake build --target "macos" --is_zip --host host1 --host2 +# 调用结果是'bake.parse "${FUNCNAME[0]}" "$@"'将生成如下脚本: +# --------------------------------------------------------- +# declare is_zip=true; +# declare target="macos"; +# declare hosts=("host1" "host2"); +# declare optShift=7; +# --------------------------------------------------------- +# eval后,就可以直接使用变量了, 在函数中declare,不带-g参数默认为local变量,不会影响全局环境。 +# +# Usage: bake.parse [arg1] [arg2] ... +# 参考:[bake.opt] +bake.parse() { + local cmd="${1}" + if [[ "$cmd" == "" ]]; then + bake._throw "bake.parse函数需提供cmd参数, Usage: bake.parse [arg1] [arg2]" ; + fi + + shift; # shift cmd arg, left is options + + # key is -h --help ... candidate words , + # value is optPath + declare -A allOptOnCmdChain + # collect opt from command chain : root>pub>pub.get + # root option first , priority low -> priority high: + for optPath in $(bake._opt_cmd_chain_opts "$cmd" | bake._str_revertLines); do + local opt + opt=$(bake._path_basename "$optPath") + local abbr + abbr=${_bake_data["$optPath/abbr"]} + allOptOnCmdChain["--$opt"]="${optPath}" + if [[ "$abbr" != "" ]]; then allOptOnCmdChain["-$abbr"]="${optPath}"; fi + done + + # dynamic opt variable map : optPath:optVarName + # Why use dynamic variables: because the variable name is not fixed + # and We want to manipulate arrays(list type opt) more conveniently + local -A optVars + local totalArgs="$#" + # while all args , until it is not opt + while (($# > 0)); do + # match $1 arg in allOptOnCmdChain, guess $1 is a "-h" "-help" ... + local optPath + optPath=${allOptOnCmdChain["$1"]} + # if next arg not a opt , parsing complete; + if [[ "${optPath}" == "" ]]; then break; fi + + # _opt_value_ prefix : avoid conflicts in the current context + optVars["$optPath"]="_opt_value_$(bake._path_basename "$optPath")" + declare "${optVars["$optPath"]}" + # reference to the current dynamic opt variable + declare -n currentOptValue=${optVars["$optPath"]} + + local optType=${_bake_data["$optPath/type"]} + case $optType in + bool) + currentOptValue=true + shift 1 + ;; + string) + [[ ! "${2+declare}" ]] && echo "parse error: opt need a value: $arg " >&2 && return 1 + currentOptValue="$2" + shift 2 + ;; + list) + [[ ! "${2+declare}" ]] && echo "parse error: opt need a value: $arg " >&2 && return 1 + currentOptValue+=("$2") # array add + shift 2 + ;; + *) + echo "parse error: not support $optPath.type: <$optType> " >&2 + return 1 + ;; + esac + done + + local resultStr + for optPath in "${!optVars[@]}"; do + local declareStr + declareStr=$(declare -p "${optVars["$optPath"]}") + resultStr+="${declareStr/#*_opt_value_/declare };\n" + done + resultStr+="declare optShift=$((totalArgs - $#));\n" + echo -e "$resultStr" # echo -e : unescapes backslash +} + + +# bake.cmd (public api) +# 注册一个命令的帮助信息 +# Examples: +# bake.cmd --cmd build --desc "build project" +# 尤其是可以配置root命令以定制根命令的帮助信息,比如: +# bake.cmd --cmd root \ +# --desc "flutter-note cli." +# 这样就可以用'./your_script -h' 查看根帮助了 +bake.opt --cmd "bake.cmd" --name "cmd" --type string --desc "cmd, function name" +bake.opt --cmd "bake.cmd" --name "desc" --type string --desc "cmd desc, show in help" +bake.cmd() { + # 模版代码,放到每个需要使用option的函数中,然后就可以使用option了 + eval "$(bake.parse "${FUNCNAME[0]}" "$@")" + + if [[ "$cmd" == "" ]]; then + echo "error: bake.cmd [--cmd] required " >&2 + return 1 + fi + _bake_data["$cmd/desc"]="$desc" +} + + +# list bake var info(public api),use for debug +# Usage: info +bake.info() { + +cat <<- EOF + + .----------------. .----------------. .----------------. .----------------. +| .--------------. || .--------------. || .--------------. || .--------------. | +| | ______ | || | __ | || | ___ ____ | || | _________ | | +| | |_ _ \ | || | / \ | || | |_ ||_ _| | || | |_ ___ | | | +| | | |_) | | || | / /\ \ | || | | |_/ / | || | | |_ \_| | | +| | | __'. | || | / ____ \ | || | | __'. | || | | _| _ | | +| | _| |__) | | || | _/ / \ \_ | || | _| | \ \_ | || | _| |___/ | | | +| | |_______/ | || ||____| |____|| || | |____||____| | || | |_________| | | +| | | || | | || | | || | | | +| '--------------' || '--------------' || '--------------' || '--------------' | + '----------------' '----------------' '----------------' '----------------' + +EOF + echo '# bake info & internal var' + echo + echo '## _bake_cmds' + echo + for key in "${!_bake_cmds[@]}"; do + printf "cmd - %-40s = %q\n" "$key" "${_bake_cmds["$key"]:0:100}" + done | sort + echo + echo '## _bake_data' + echo + for key in "${!_bake_data[@]}"; do + printf "data - %-40s = %q\n" "$key" "${_bake_data["$key"]:0:100}" + done | sort + echo + echo '## options' + echo + echo "help = $help" + echo "debug = $debug" + echo + +} + +# 入口 (public api) +bake.go() { + # init register all cmd + + bake._cmd_register + + # parse cmd : + # ./bake pub get -v -b + # -> { cmd:"pub.get", args:"-v -b" } + # ./bake -h + # -> { cmd:"", args:"-h" } + local cmd nextCmd arg + for arg in "$@"; do + nextCmd="$([[ "$cmd" == "" ]] && echo "$arg" || echo "$cmd.$arg")" + if [[ "${_bake_cmds["$nextCmd"]}" == "" ]]; then break; fi + cmd="$nextCmd" + shift + done + + if [[ "$cmd" == "" ]]; then cmd="root"; fi + eval "$(bake.parse "$cmd" "$@")" + + if [[ "$help" == "true" ]]; then + bake._show_cmd_help "$cmd" "$@" + return 0 + fi + + # if fileExist then show help + if ! declare -F "$cmd" | grep "$cmd" &>/dev/null 2>&1; then + if [[ "${_bake_cmds["$cmd"]}" == "PARENT_CMD_NOT_FUNC" ]]; then + bake._show_cmd_help "$cmd" "$@" + return 0 + fi + bake._throw "Error: 404 ,cmd '${cmd}' not define, please define cmd function '${cmd}()'" + fi + + $cmd "$@" +} + +# root is special cmd(you can define it), bake add some common options to this cmd, you can add yourself options +bake.opt --cmd root --name "help" --abbr h --type bool --default false --desc "print help, show all commands" +bake.opt --cmd root --name "debug" --abbr d --type bool --default false --desc "debug mode, print more internal info" + +# BASH_SOURCE > 1 , means bake import from other script, it is lib mode +# lib mod is not load app function, so we need to stop here +if ((${#BASH_SOURCE[@]} > 1)); then + bake._debug "【${BAKE_FILE}】 call by other script【$(printf " ▶︎ %s" "${BASH_SOURCE[@]}")】, lib mode on, not load below app script" >&2 +fi + +# bake内部版本(public api) +bake.version(){ + echo "$_bake_version" +} + +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# bake common script end line. +# The above code is common code that is not related to the specific app, +# if you want to define app-related commands, +# please put them below or other file. +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! \ No newline at end of file diff --git a/packages/you_bake/examples/3.mono_projects/bake b/packages/you_bake/examples/3.mono_projects/bake new file mode 100755 index 00000000..e79e71d4 --- /dev/null +++ b/packages/you_bake/examples/3.mono_projects/bake @@ -0,0 +1,158 @@ +#!/usr/bin/env bash + + +######################################################## +# 范例:示范一个稍微复杂点的多模块mono项目 +# 参考:https://github.com/chen56/younpc/blob/main/bake +# 运行 ./bake -h 查看本文件的效果和帮助 +######################################################## + + +######################################################## +# 本节为模版代码,每个copy一下即可,主要是自动下载bake.bash依赖 +######################################################## + +# 模版脚本,用来获取本文件的realpath +# 我在我的所有脚本头都会放这个函数,方便得到真正的当前脚本目录 +# On Mac OS, readlink -f doesn't work, so use._real_path get the real path of the file +_real_path() ( + cd "$(dirname "$1")" + declare file="$PWD/$(basename "$1")" + while [[ -L "$file" ]]; do + file="$(readlink "$file")" + cd -P "$(dirname "$file")" + file="$PWD/$(basename "$file")" + done + echo "$file" +) +SCRIPT_PATH="$(_real_path "${BASH_SOURCE[0]}")" +SCRIPT_DIR="$(dirname "$SCRIPT_PATH")" +SCRIPT_FILE="$(basename "$SCRIPT_PATH")" + +# 脚本动态安装bake.bash依赖 +_install_bake(){ + mkdir -p "$SCRIPT_DIR/vendor" + echo "$SCRIPT_PATH -> _install_bake ▶︎【curl -o $SCRIPT_DIR/bake.bash https://github.com/chen56/bake/raw/main/bake.bash】" + curl -L -o "$SCRIPT_DIR/vendor/bake.bash" https://github.com/chen56/bake/raw/main/bake.bash ; +} +if ! [[ -f "$SCRIPT_DIR/vendor/bake.bash" ]]; then + _install_bake +fi + +# include common script +source "$SCRIPT_DIR/vendor/bake.bash" + + + +########################################## +# app cmd script +# 应用的命令脚本 +########################################## + +# 模仿一个mono多包项目 +declare -A pkgs=( + ["app"]="$SCRIPT_DIR/app" + ["kous"]="$SCRIPT_DIR/kous" + ["app_devtools"]="$SCRIPT_DIR/app_devtools" +) + +# 定义根命令的帮助 +bake.cmd --cmd root --desc "$( cat <<-EOF + +example cli tools. mono项目,包含多个子项目: ${!pkgs[@]} + +https://github.com/chen56/bake + +Usage: + ./$SCRIPT_FILE [cmd] [opts] [args...] + +Examples: + ./${SCRIPT_FILE} # same as './${SCRIPT_FILE} -h' + ./${SCRIPT_FILE} --help # show all commands help + ./${SCRIPT_FILE} -h --debug # show all commands help , include internal function + + ./${SCRIPT_FILE} test # test all pkgs + ./${SCRIPT_FILE} build # defalut build == flutter build web --web-renderer html + ./${SCRIPT_FILE} preview # defalut preview == run server at web build + ./${SCRIPT_FILE} test # test all pkgs + + ./${SCRIPT_FILE} all -h # show all mono pkg commands help + ./${SCRIPT_FILE} all ls # run "ls" on all mono pkgs + + ./${SCRIPT_FILE} app install # run app pkg install + ./${SCRIPT_FILE} app test # run app pkg install +EOF + )" + + +# run一条命令,先print上下文信息,再执行 +# Usage: run +# Example: +# ------------------------------------ +# $ ./bake run pwd +# /Users/x/git/younpc/bake:733 -> bake.go ▶︎【pwd】 +# /Users/x/git/younpc +# ------------------------------------ +run() { + local caller_line=$(caller 0 | awk '{print $1}') + echo "$SCRIPT_PATH:$caller_line -> ${FUNCNAME[1]} ▶︎【$@】" + "$@" + return $? +} + + +## if function not exist return 1; +# Usage: _exist_func +# Example: _exist_func app.build +# => return 0 +_exist_func(){ + local func="$1" + if ! ( declare -F "$func" | grep "$func" &>/dev/null 2>&1; ) then + return 1; + fi +} + +bake.cmd --cmd all --desc " run cmd on all mono pkg, Usage: ./$SCRIPT_FILE all [any command]" +all() { for name in "${!pkgs[@]}"; do run echo "在 <${name}> 目录模仿运行: $@" ; done ; } + +# app子项目 +bake.cmd --cmd app --desc " pkg, Usage: ./$SCRIPT_FILE app [cmd]" +app.run(){ run echo "在 <${pkgs[app]}> 目录模仿运行: $@" ; } +app.install(){ app.run flutter pub get ; } +app.clean(){ app.run flutter clean ; } +app.dev(){ app.run flutter run --no-pub --device-id macos ; } +app.build(){ app.run flutter build macos --release --tree-shake-icons "$@"; } +app.preview(){ app.run open "${pkgs[app]}/build/macos/Build/Products/Release/younpc.app" ; } + +# app_devtools子项目 +bake.cmd --cmd app_devtools --desc " pkg, Usage: ./$SCRIPT_FILE app_devtools [cmd]" +app_devtools.run(){ run echo "在 <${pkgs[app_devtools]}>目录模仿运行: $@" ; } +app_devtools.install(){ cd "${pkgs[app_devtools]}" && run flutter pub get ; } +app_devtools.dev(){ cd "${pkgs[app_devtools]}" && run flutter run -d Chrome --dart-define=use_simulated_environment=true ; } +app_devtools.build(){ cd "${pkgs[app_devtools]}" && dart run devtools_extensions build_and_copy --source=. --dest="${pkgs[app_devtools]}/extension/devtools" ; } + +# 服务器子项目 +bake.cmd --cmd server --desc " pkg, Usage: ./$SCRIPT_FILE server [cmd]" +server.run(){ run echo "在 <${pkgs[server]}> 目录模仿运行: $@" ; } +server.clean(){ server.run "nothing clean" ; } +server.install(){ server.run go mod tidy ; } + +install(){ + _install_bake + run git lfs install + for pkg in "${!pkgs[@]}"; do + if _exist_func "$pkg.install" ; then "$pkg.install" ; fi + done +} + +clean(){ + for pkg in "${!pkgs[@]}"; do + if _exist_func "$pkg.clean" ; then "$pkg.clean" ; fi + done +} + +#################################################### +# app entry script & _root cmd +#################################################### +bake.go "$@" + diff --git a/packages/you_bake/examples/3.mono_projects/vendor/bake.bash b/packages/you_bake/examples/3.mono_projects/vendor/bake.bash new file mode 100644 index 00000000..f1fe15b4 --- /dev/null +++ b/packages/you_bake/examples/3.mono_projects/vendor/bake.bash @@ -0,0 +1,679 @@ +#!/usr/bin/env bash +set -o errtrace # -E trap inherited in sub script +set -o errexit # -e +set -o functrace # -T If set, any trap on DEBUG and RETURN are inherited by shell functions +set -o pipefail # default pipeline status==last command status, If set, status=any command fail +#set -o nounset # -u: don't use it ,it is crazy, 1.bash version is diff Behavior 2.we need like this: ${arr[@]+"${arr[@]}"} + +_bake_version=v0.3.20240327 + +# v0.2.20230528 - It can run normally on macos +# bake == (bash)ake == 去Make的bash tool +# +# https://github.com/chen56/bake +# +# bake 是个简单的命令行工具,以替代Makefile的子命令功能 +# make工具的主要特点是处理文件依赖进行增量编译,但flutter、golang、java、js项目的build工具 +# 太厉害了,这几年唯一还在用Makefile的理由就是他的子命令机制: "make build"、 +# "make run", 可以方便的自定义单一入口的父子命令,但Makefile本身的语法套路也很复杂, +# 很多批处理还是要靠bash, 这就尴尬了,工具太多,麻烦!本脚本尝试彻底摆脱使用Makefile。 +# 经尝试,代码很少啊 ,核心代码几百行啊,父子命令二三百行左右,option解析二三百行左右,功能足够了: +# +# bake命令规则: +# 1. 函数即命令,所有bake内的函数均可以在脚本外运行: +# ./bake [all function] # bake内的所有函数均可以在脚本外直接运行 +# ./bake info # 比如这个内部函数, 看bake内部变量,调试脚本用 +# ./bake test # 你如果定义过test()函数,就可以这样运行 +# 2. 带"."的函数,形成父子命令: +# web.build(){ echo "build web app"; } +# web.test(){ echo "build web app"; } +# 可以这样调用 +# ./bake web -h # 运行子命令,或看帮助 +# ./bake web.build -h # 上面等同 +# ./bake web test -h # 运行子命令,或看帮助 +# ./bake web.test -h # 与上面等同 +# 3. 特殊的root命令表示根命令 +# ./bake # 如果有root()函数,就执行它 +# 4. 像其他高级语言的cli工具一样,用简单变量就可以获取命令option: +# # a. 先在bake文件里里定义app options +# bake.opt --cmd build --name "target" --type string +# # b. 解析和使用option +# function build() { +# eval "$(bake.parse "${FUNCNAME[0]}" "$@")"; +# echo "build ... your option:target: $target"; +# } +# # c. 调用看看: +# ./bake build --target "macos" +# 5. bake尽量不依赖bash以外的其他工具,包括linux coreutils,更简单通用,但由于用了关联数组等 +# 依赖bash4+ +# 6. 有两种用法: +# - 这个文件copy走,把你的脚本放到本脚本最后即可. +# - 在你的脚本里直接curl下载本脚本后 source即可。 +# 范例可以看实际案例: +# - https://github.com/chen56/note/blob/main/bake +# - https://github.com/chen56/younpc/blob/main/bake +# todo +# 1. 当前 无法判断错误命令:./bake no_this_cmd ,因为不知道这是否是此命令的参数, +# 需要设置设一个简单的规则:只有叶子命令才能正常执行,这样非叶子命令就不需要有参数 +# 2. 当前 无法判断错误options:./bake --no_this_opt ,同上 +# 3. 类似flutter run [no-]pub 反向选项 +# + + +# check bake dependencies +if ((BASH_VERSINFO[0] < 4 || (\ + BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4))); then + echo "Error: It's 2082 ,Your bash is still this version(BASH_VERSINFO: ${BASH_VERSINFO[*]}),Please install bash 4.4+: + apt install bash # ubuntu + brew install bash # mac" + exit 1 +fi +# On Mac OS, readlink -f doesn't work, so use.bake._real_path get the real path of the file +bake._real_path() ( + cd "$(dirname "$1")" + file="$PWD/$(basename "$1")" + while [[ -L "$file" ]]; do + file="$(readlink "$file")" + cd -P "$(dirname "$file")" + file="$PWD/$(basename "$file")" + done + echo "$file" +) + +# bake context +BAKE_PATH="$(bake._real_path "${BASH_SOURCE[0]}")" +BAKE_DIR="$(dirname "$BAKE_PATH")" +BAKE_FILE="$(basename "$BAKE_PATH")" +cd "${BAKE_DIR}" # set workdir +declare debug=false +declare help=false + +bake._on_error() { + bake._error "ERROR - trapped an error: ↑ , trace: ↓" + local i=0 + local stackInfo + while true; do + stackInfo=$(caller $i 2>&1 && true) && true + if [[ $? != 0 ]]; then return 0; fi + + # 一行调用栈 '97 bake.build ./note/bake' + # 解析后 => 行号no=97 , 报错的函数func=bake.build , file=./note/bake + local no func file + IFS=' ' read -r no func file <<<"$stackInfo" + + # 打印出可读性强的信息: + # => ./note/bake:38 -> bake.build + printf "%s\n" "$(bake._real_path $file):$no -> $func" >&2 + + i=$((i + 1)) + done +} +# Add the error catch first +#https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html#index-trap +trap "bake._on_error" ERR + + +# replace $HOME with "~" +# Usage: bake._pwd +# Examples: 当前目录如果是"/Users/chen/git/note/" +# 转成更简单易读的 => "~/git/note/" +bake._pwd() { echo "${PWD/#$HOME/\~}" ; } + +# 报错后终止程序,类似于其他语言的throw Excpetion +# 因set -o errexit 后,程序将在return 1 时退出, +# 退出前被‘trap bake._on_error ERR’捕获并显示错误堆栈 +# Usage: bake._throw +bake._throw(){ + bake._error "$@" + # set -o errexit 后,程序将退出,退出前被trap bake._on_error Err捕获并显示错误堆栈 + return 1 +} +bake._error() { + if [[ "${_LOG_LEVELS[@]:0}" != *"$LOG"* ]]; then return 0; fi + bake._log "$@" +} +bake._info() { + bake._log "$@" +} +bake._debug() { + if [[ "${debug}" != true ]]; then return 0; fi + bake._log "$@" +} +# Usage: bake._log DEBUG "错误消息" +bake._log(){ + local level; + level=$1 + if [[ "${_LOG_LEVELS[@]:2}" != *"$LOG"* ]]; then return 0; fi + echo -e "$level $(date "+%F %T") $(bake._pwd)\$ ${FUNCNAME[1]}() : $*" >&2 +} + + +########################################## +# bake common script +########################################## + +# Simulating object-oriented data structures with flat associative arrays +# use ./bake _self to see internal var +# save all other data +declare -A _bake_data + +# only save all commands, we use it to build command tree +# it is cache cmd tree from _bake_data +declare -A _bake_cmds + +TYPE_CMD="type:cmd" + +########################################## +# bake common function +########################################## + +# Usage: bake._str_cutLeft +# bake._path_dirname a/b/c a/b => c +bake._str_cutLeft() { printf "${1#$2}"; } + +# Usage: bake.str.split [delimiter:default /] +bake._str_split() { + local str=$1 delimiter=${2:-/} + # # use <() process-substitution + # # don't use <<< "" its add newline + local arr + readarray -t arr < <(printf '%s' "${str//$delimiter/$'\n'}") + printf '%s\n' "${arr[@]}" +} + +# Usage: bake._str_revertLines <<< "$(echo -e "a\nb\nc")" => "c\nb\na" +bake._str_revertLines() { + # cat xxx | tail -r; # macos bsd only, not work on linux + # so use sed + sed '1!G;h;$!d' # sed magic +} + +# Usage: bake._path_dirname [delimiter:default /] +# similar command dirname, but +# dirname root is ".", only work with "/" +# bake._path_dirname root is "" , can set delimiter +# bake._path_dirname a.b.c . => a.b +bake._path_dirname() { + local pathLikeStr="$1" delimiter="${2:-/}" + if [[ "$pathLikeStr" != *"$delimiter"* ]]; then + return + fi + printf '%s' "${pathLikeStr%$delimiter*}" +} +# Usage: bake._path_first [delimiter:default /] +# bake._path_first a.b.c . => a +bake._path_first() { + local pathLikeStr="$1" delimiter="${2:-/}" + # if /a/b/c , call :bake._path_first a/b/c / + if [[ "$pathLikeStr" == "$delimiter"* ]]; then + local removeDelim + removeDelim=$(bake._str_cutLeft "$pathLikeStr" "$delimiter") + printf "$delimiter$(bake._path_first "$removeDelim" "$delimiter")" + else # a/b/c + printf "${pathLikeStr%%$delimiter*}" + fi +} +# similar command basename +# Usage: bake._path_basename [delimiter:default /] +bake._path_basename() { + local pathLikeStr="$1" delimiter="${2:-/}" + # ${1##*/} => ## left remove until last "/" + printf "${pathLikeStr##*$delimiter}" +} + +# Usage: bake._data_children +# return children name +bake._data_children() { + local path="$1" + # ${!_bake_data[@]}: get all array keys + local key + for key in "${!_bake_data[@]}"; do + # if start $path + if [[ "$key" == "$path"* ]]; then + # https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html + # remove prefix : key:build/opts/dir/type/x leftPathToBeCut:build/opts => dir/type/x + local child=$(bake._str_cutLeft "$key" "$path/") + # remove suffix: dir/type/x => dir + child=$(bake._path_first "$child" "/") + printf '%s\n' "$child" + fi + done | sort -u +} + +bake._cmd_childrenNameMaxLength() { + local cmd="$1" maxLengthOfCmd=0 + for child in $(bake._cmd_children "$cmd"); do + if ((${#child} > maxLengthOfCmd)); then maxLengthOfCmd=${#child}; fi + done + printf "$maxLengthOfCmd" +} +bake._cmd_children() ( + local path="$1" + if [[ "$path" == root ]]; then + path="" + fi + + # ${!_bake_data[@]}: get all array keys + for key in "${!_bake_cmds[@]}"; do + # if start $path + if [[ "$key" == "$path"* && "$key" != "$path" && "$key" != "root" ]]; then + # https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html + # remove prefix : key:a.b.c leftPathToBeCut:a => b.c + child=$(bake._str_cutLeft "$key" "$path.") + # remove suffix: b.c => c + child=$(bake._path_first "$child" ".") + printf '%s\n' "$child" + fi + done | sort -u +) + +# Usage: bake._cmd_up_chain +# sample: bake._cmd_up_chain a.b => "a.b", "a", "root" +bake._cmd_up_chain() { + local path="${1:-root}" + local up="$path" + while [[ "$up" != "" ]]; do + printf '%s\n' "$up" + up=$(bake._path_dirname "$up" ".") + done + if [[ "$path" != "root" ]]; then printf "root\n"; fi +} +# Usage: bake._cmd_down_chain +# reverse of bake._cmd_up_chain +bake._cmd_down_chain() { + bake._cmd_up_chain "$1" | bake._str_revertLines +} + +# Usage: bake._opt_cmd_chain_opts +# Examples: bake._opt_cmd_chain_opts bake.info +# return optionDataPath list +bake._opt_cmd_chain_opts() { + local cmd=$1 + local upCmds + readarray -t upCmds <<<"$(bake._cmd_up_chain "$cmd")" + + local key + for key in "${!_bake_data[@]}"; do + if [[ "${_bake_data["$key"]}" != "type:opt" ]]; then continue; fi + local upCmd + for upCmd in "${upCmds[@]}"; do + if [[ "$key" == "$upCmd/opts/"* ]]; then + printf '%s\n' "$key" + fi + done + done | sort +} + +# only use by bake.opt, +# because "bake.opt" is meta function, use this func to add self +bake._opt_internal_add() { + local cmd="$1" opt="$2" type="$3" required="$4" default="$5" abbr="$6" desc="$7" + _bake_data["$cmd/opts/$opt"]="type:opt" + _bake_data["$cmd/opts/$opt/name"]="$opt" + _bake_data["$cmd/opts/$opt/type"]="$type" + _bake_data["$cmd/opts/$opt/required"]="$required" + _bake_data["$cmd/opts/$opt/abbr"]="$abbr" + _bake_data["$cmd/opts/$opt/default"]="$default" + _bake_data["$cmd/opts/$opt/desc"]="$desc" +} + + +# Usage: bake._cmd_register +# ensure all cmd register +bake._cmd_register() { + local functionName + + while IFS=$'\n' read -r functionName; do + if [[ "$functionName" == */* ]]; then + echo "error: function $functionName() can not contains '/' " >&2 + return 1 + fi + local upCmd + for upCmd in $(bake._cmd_up_chain "$functionName"); do + # if upCmd is a function , set upCmd value to data path + if compgen -A function | grep -q "^$upCmd$"; then + _bake_cmds["$upCmd"]="$upCmd" + else + _bake_cmds["$upCmd"]="PARENT_CMD_NOT_FUNC" + fi + done + + # list all function name + # declare -F | awk {'print $3'} == compgen -A function + # declare -f func1 -> func1 + # declare -fx func2 -> func2 +# done <<<"$(compgen -A function)" + done <<<"$(declare -F | grep "declare -f" | awk {'print $3'})" +} + +# 显示一条命令的帮助 +# Usage: bake._show_cmd_help +# Examples: bake._show_cmd_help deploy #显示deploy的帮助: +bake._show_cmd_help() { + local cmd="$1" + + if [[ "$cmd" == "" ]]; then + bake._throw "bake._show_cmd_help need a arg: bake._show_cmd_help [cmd]" + fi + + shift + + eval "$(bake.parse "${FUNCNAME[0]}" "$@")" + + echo + + if [[ "$cmd" == "root" ]] ;then + echo "Running:【: $(bake._pwd)/$BAKE_FILE $@】" + else + echo "Running:【$(bake._pwd)/$BAKE_FILE $cmd $@】" + fi + + echo + echo "${_bake_data["${cmd}/desc"]}" + echo + + echo "Available Options:" + for optPath in $(bake._opt_cmd_chain_opts "$cmd"); do + local opt=$(bake._path_basename "$optPath") + local name=${_bake_data["$optPath/name"]} + local type=${_bake_data["$optPath/type"]} + local required=${_bake_data["$optPath/required"]} + local abbr=${_bake_data["$optPath/abbr"]} + local default=${_bake_data["$optPath/default"]} + local desc="${_bake_data["$optPath/desc"]}" + + local optArgDesc="" + if [[ "$type" == "string" ]]; then + if [[ "$default" != "" ]]; then + optArgDesc+="<$type:${default}>" + else + optArgDesc="<$type>" + fi + fi + + printf " --%-20s -%-2s %-6s required:[%s] %b\n" "$name $optArgDesc" "$abbr" "$type" "$required" "$desc" + done + + echo " +Available Commands:" + local maxLengthOfCmd + maxLengthOfCmd="$(bake._cmd_childrenNameMaxLength "$cmd")" + + for subCmd in $(bake._cmd_children "$cmd"); do + + # only show public cmd if not verbose + # '_'起头的命令和'bake'命令,只有debug模式才打印出来 + if [[ ("$subCmd" == _* || "$subCmd" == bake*) ]]; then + if [[ "$debug" != "true" ]]; then + continue + fi + fi + + local subCmdPath="$cmd/$subCmd" + [[ "$cmd" == "root" ]] && subCmdPath="$subCmd" + + local desc="${_bake_data["$subCmdPath/desc"]}" + desc="$(echo -e "$desc" | head -n 1 )" # backslash escapes interpretation + + printf " %-$((maxLengthOfCmd))s ${desc}\n" "${subCmd}" + done +} + + +# 为cmd配置参数(public api) +# Examples: +# bake.opt --cmd "build" --name "is_zip" --type bool --required --abbr z --default true --desc "is_zip, build项目时是否压缩" +# 每个参数可以配置如下信息: +# cmd: 参数作用的命令全名 +# name: 参数长名,可以 ./bake build --is_zip 这样使用 +# type: 类型,目前支持 bool|string|list +# required: 是否必须提供,不提供将报错 +# abbr: 参数短名, 可以 ./bake build -z 这样使用 +# default: 缺省值, 未指定参数时,使用此值 +# desc: 参数帮助,将显示在‘./bake build -h’命令帮助里 +# 参考[bake.parse] +bake._opt_internal_add bake.opt "cmd" "string" "true" "" "" "cmd name" +bake._opt_internal_add bake.opt "name" "string" "true" "" "" "option name" +bake._opt_internal_add bake.opt "type" "string" "true" "" "" "option type [bool|string|list]" +bake._opt_internal_add bake.opt "required" "bool" "false" "false" "false" "option required [true|false],default[false]" +bake._opt_internal_add bake.opt "abbr" "string" "false" "" "" "option abbr" +bake._opt_internal_add bake.opt "default" "string" "false" "" "" "option abbr" +bake._opt_internal_add bake.opt "desc" "string" "false" "" "" "option desc" +bake.opt() { + eval "$(bake.parse ""${FUNCNAME[0]}"" "$@")" + if [[ "$name" == "" ]]; then + echo "error: option [--name] required " >&2 && return 1 + fi + if [[ "$type" == "" ]]; then + echo "error: option [--type] required " >&2 && return 1 + fi + if [[ "$type" != "bool" && "$type" != "string" && "$type" != "list" ]]; then + echo "error: option [--type] must in [bool|string|list] " >&2 && return 1 + fi + bake._opt_internal_add "$cmd" "$name" "$type" "${required:-false}" "$default" "$abbr" "$desc" +} + +# bake.opt (public api) +# 像其他高级语言的cli工具一样,用简单变量就可以获取名称化的命令参数: +# 支持bool,string,list三种参数,用法如下: +# 你的./bake脚本里: +# bake.opt --cmd build --name "is_zip" --type bool +# bake.opt --cmd build --name "target" --type string +# bake.opt --cmd build --name "files" --type string +# function build() { +# # 模版代码,把生成的脚本eval出来 +# eval "$(bake.parse "${FUNCNAME[0]}" "$@")"; + +# echo "is_zip:$is_zip, target:$target, hosts:${hosts[@]}"; +# } +# 调用: +# ./bake build --target "macos" --is_zip --host host1 --host2 +# 调用结果是'bake.parse "${FUNCNAME[0]}" "$@"'将生成如下脚本: +# --------------------------------------------------------- +# declare is_zip=true; +# declare target="macos"; +# declare hosts=("host1" "host2"); +# declare optShift=7; +# --------------------------------------------------------- +# eval后,就可以直接使用变量了, 在函数中declare,不带-g参数默认为local变量,不会影响全局环境。 +# +# Usage: bake.parse [arg1] [arg2] ... +# 参考:[bake.opt] +bake.parse() { + local cmd="${1}" + if [[ "$cmd" == "" ]]; then + bake._throw "bake.parse函数需提供cmd参数, Usage: bake.parse [arg1] [arg2]" ; + fi + + shift; # shift cmd arg, left is options + + # key is -h --help ... candidate words , + # value is optPath + declare -A allOptOnCmdChain + # collect opt from command chain : root>pub>pub.get + # root option first , priority low -> priority high: + for optPath in $(bake._opt_cmd_chain_opts "$cmd" | bake._str_revertLines); do + local opt + opt=$(bake._path_basename "$optPath") + local abbr + abbr=${_bake_data["$optPath/abbr"]} + allOptOnCmdChain["--$opt"]="${optPath}" + if [[ "$abbr" != "" ]]; then allOptOnCmdChain["-$abbr"]="${optPath}"; fi + done + + # dynamic opt variable map : optPath:optVarName + # Why use dynamic variables: because the variable name is not fixed + # and We want to manipulate arrays(list type opt) more conveniently + local -A optVars + local totalArgs="$#" + # while all args , until it is not opt + while (($# > 0)); do + # match $1 arg in allOptOnCmdChain, guess $1 is a "-h" "-help" ... + local optPath + optPath=${allOptOnCmdChain["$1"]} + # if next arg not a opt , parsing complete; + if [[ "${optPath}" == "" ]]; then break; fi + + # _opt_value_ prefix : avoid conflicts in the current context + optVars["$optPath"]="_opt_value_$(bake._path_basename "$optPath")" + declare "${optVars["$optPath"]}" + # reference to the current dynamic opt variable + declare -n currentOptValue=${optVars["$optPath"]} + + local optType=${_bake_data["$optPath/type"]} + case $optType in + bool) + currentOptValue=true + shift 1 + ;; + string) + [[ ! "${2+declare}" ]] && echo "parse error: opt need a value: $arg " >&2 && return 1 + currentOptValue="$2" + shift 2 + ;; + list) + [[ ! "${2+declare}" ]] && echo "parse error: opt need a value: $arg " >&2 && return 1 + currentOptValue+=("$2") # array add + shift 2 + ;; + *) + echo "parse error: not support $optPath.type: <$optType> " >&2 + return 1 + ;; + esac + done + + local resultStr + for optPath in "${!optVars[@]}"; do + local declareStr + declareStr=$(declare -p "${optVars["$optPath"]}") + resultStr+="${declareStr/#*_opt_value_/declare };\n" + done + resultStr+="declare optShift=$((totalArgs - $#));\n" + echo -e "$resultStr" # echo -e : unescapes backslash +} + + +# bake.cmd (public api) +# 注册一个命令的帮助信息 +# Examples: +# bake.cmd --cmd build --desc "build project" +# 尤其是可以配置root命令以定制根命令的帮助信息,比如: +# bake.cmd --cmd root \ +# --desc "flutter-note cli." +# 这样就可以用'./your_script -h' 查看根帮助了 +bake.opt --cmd "bake.cmd" --name "cmd" --type string --desc "cmd, function name" +bake.opt --cmd "bake.cmd" --name "desc" --type string --desc "cmd desc, show in help" +bake.cmd() { + # 模版代码,放到每个需要使用option的函数中,然后就可以使用option了 + eval "$(bake.parse "${FUNCNAME[0]}" "$@")" + + if [[ "$cmd" == "" ]]; then + echo "error: bake.cmd [--cmd] required " >&2 + return 1 + fi + _bake_data["$cmd/desc"]="$desc" +} + + +# list bake var info(public api),use for debug +# Usage: info +bake.info() { + +cat <<- EOF + + .----------------. .----------------. .----------------. .----------------. +| .--------------. || .--------------. || .--------------. || .--------------. | +| | ______ | || | __ | || | ___ ____ | || | _________ | | +| | |_ _ \ | || | / \ | || | |_ ||_ _| | || | |_ ___ | | | +| | | |_) | | || | / /\ \ | || | | |_/ / | || | | |_ \_| | | +| | | __'. | || | / ____ \ | || | | __'. | || | | _| _ | | +| | _| |__) | | || | _/ / \ \_ | || | _| | \ \_ | || | _| |___/ | | | +| | |_______/ | || ||____| |____|| || | |____||____| | || | |_________| | | +| | | || | | || | | || | | | +| '--------------' || '--------------' || '--------------' || '--------------' | + '----------------' '----------------' '----------------' '----------------' + +EOF + echo '# bake info & internal var' + echo + echo '## _bake_cmds' + echo + for key in "${!_bake_cmds[@]}"; do + printf "cmd - %-40s = %q\n" "$key" "${_bake_cmds["$key"]:0:100}" + done | sort + echo + echo '## _bake_data' + echo + for key in "${!_bake_data[@]}"; do + printf "data - %-40s = %q\n" "$key" "${_bake_data["$key"]:0:100}" + done | sort + echo + echo '## options' + echo + echo "help = $help" + echo "debug = $debug" + echo + +} + +# 入口 (public api) +bake.go() { + # init register all cmd + + bake._cmd_register + + # parse cmd : + # ./bake pub get -v -b + # -> { cmd:"pub.get", args:"-v -b" } + # ./bake -h + # -> { cmd:"", args:"-h" } + local cmd nextCmd arg + for arg in "$@"; do + nextCmd="$([[ "$cmd" == "" ]] && echo "$arg" || echo "$cmd.$arg")" + if [[ "${_bake_cmds["$nextCmd"]}" == "" ]]; then break; fi + cmd="$nextCmd" + shift + done + + if [[ "$cmd" == "" ]]; then cmd="root"; fi + eval "$(bake.parse "$cmd" "$@")" + + if [[ "$help" == "true" ]]; then + bake._show_cmd_help "$cmd" "$@" + return 0 + fi + + # if fileExist then show help + if ! declare -F "$cmd" | grep "$cmd" &>/dev/null 2>&1; then + if [[ "${_bake_cmds["$cmd"]}" == "PARENT_CMD_NOT_FUNC" ]]; then + bake._show_cmd_help "$cmd" "$@" + return 0 + fi + bake._throw "Error: 404 ,cmd '${cmd}' not define, please define cmd function '${cmd}()'" + fi + + $cmd "$@" +} + +# root is special cmd(you can define it), bake add some common options to this cmd, you can add yourself options +bake.opt --cmd root --name "help" --abbr h --type bool --default false --desc "print help, show all commands" +bake.opt --cmd root --name "debug" --abbr d --type bool --default false --desc "debug mode, print more internal info" + +# BASH_SOURCE > 1 , means bake import from other script, it is lib mode +# lib mod is not load app function, so we need to stop here +if ((${#BASH_SOURCE[@]} > 1)); then + bake._debug "【${BAKE_FILE}】 call by other script【$(printf " ▶︎ %s" "${BASH_SOURCE[@]}")】, lib mode on, not load below app script" >&2 +fi + +# bake内部版本(public api) +bake.version(){ + echo "$_bake_version" +} + +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# bake common script end line. +# The above code is common code that is not related to the specific app, +# if you want to define app-related commands, +# please put them below or other file. +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! \ No newline at end of file diff --git a/packages/you_bake/test.bash b/packages/you_bake/test.bash new file mode 100755 index 00000000..dda6c690 --- /dev/null +++ b/packages/you_bake/test.bash @@ -0,0 +1,559 @@ +#!/usr/bin/env bash +set -o errtrace # -E trap inherited in sub script +set -o errexit # -e +set -o functrace # -T If set, any trap on DEBUG and RETURN are inherited by shell functions +set -o pipefail # default pipeline status==last command status, If set, status=any command fail +#set -o nounset # -u: don't use it ,it is crazy, 1.bash version is diff Behavior 2.we need like this: ${arr[@]+"${arr[@]}"} + +######################################################## +# 本测试命令和使用普通bake 应用脚本的模式一模一样 +# 运行测试: ./tests.bash test +# 列出子命令: ./tests.bash -h +######################################################## + + +bake._real_path() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" ; } + +TEST_PATH="$(bake._real_path "${BASH_SOURCE[0]}")" +TEST_DIR="$(dirname "$TEST_PATH")" +TEST_FILE="$(basename "$TEST_PATH")" + +source "$TEST_DIR/bake.bash" + + +_________________________test_framework_start(){ echo x;} + +assert_fail() { + echo "$@" >&2 +} + + +@is(){ + + + local actual="$1" expected="$2" msg="$3" + if [[ "$actual" != "$expected" ]] ; then + local error_message; + # shellcheck disable=SC2261 + error_message=$(cat <--------------------------- +expected: [$(echo -e "$expected")] +actual : [$(echo -e "$actual")] +----------------------------------<再看看echo -E的结果---------------------------- +expected: [$(echo -E "$expected")] +actual : [$(echo -E "$actual")] +--------------------------------------------------------------------------- +$( diff -y <(echo -E "$expected") <(echo -E "$actual") || true ) +================================================================================ + +ERROR_END +) + echo -E "$error_message" >&2 + + echo "$-:assert fail, is open vimdiff check details: (y|yes)" + # shellcheck disable=SC2154 + # __interactive is root option + if [[ "$__interactive" == true ]];then + IFS= read -p "进入vimdiff看细节?打开vimdiff输入(y|Y)" -n 1 -r is_open_diff + if [[ "$is_open_diff" == "y" || "$is_open_diff" == "Y" ]]; then + vimdiff <(echo -E "$expected") <(echo -E "$actual") + fi + fi + # TODO 应该自己打印堆栈,指出出错的test,这需要定制返回值为4xx + return 100 + fi +} +@contains(){ + local actual="$1" expected="$2" msg="$3" + if [[ "$actual" != *"$expected"* ]] ; then + assert_fail "assert fail: $msg + actual : [$actual] + is not contains: [$expected]" + return 2 + fi +} + + +# Usage: assert [msg] +# Sample: assert $(( 1+1 )) @is "2" +assert(){ + local actual="$1" op="$2" expected="$3" msg="$4" + "$op" "$actual" "$expected" "$msg" +} + + + +______________________________some_lib(){ echo x;} + +# 这里放一些以后可能用到的函数,暂时未实验好 + + +# escape to 'xxx' or $'xxx' +# https://www.gnu.org/software/bash/manual/bash.html#ANSI_002dC-Quoting +bake.str_escape() { + # from 2016 bash 4.4 + # ${parameter@Q} : quoted in a format that can be reused as input + # to 'xxx' or $'xxx' + printf '%s\n' "${1@Q}" +} +# unescape from 'xxx' or $'xxx' +bake.str_unescape() { + local str=${1} + # $'xx' => xx + if [[ "$str" == "\$'"*"'" ]]; then + str="${str:2:-1}" + # 'xx' => xx + elif [[ "${str}" == "'"*"'" ]]; then + str="${str:1:-1}" + fi + # from 2016 bash 4.4 + # ${parameter@E} expanded as with the $'...' quoting mechansim + printf '%s' "${str@E}" +} + +# TODO 模仿http错误 +# 报错后终止程序,类似于其他语言的_throw Excpetion +# 因set -o errexit 后,程序将在return 1 时退出, +# 退出前被‘trap bake._on_error ERR’捕获并显示错误堆栈 +# Usage: _throw +_shell_code_to_http_code(){ return $(($1 + 200)) ; } +_http_code_to_shell_code(){ return $(($1 - 200)) ; } +_todo_throw(){ + local http_code="$1"; + local running_info="${SCRIPT_PATH} -> ${FUNCNAME[1]} ▶︎【$*】" + if ! (( http_code >301 && http_code <=555 )); then + echo "$running_info " + echo " => error : http_code参数要在301~555之间,模仿http状态码, Usage: _throw " + return "$(_http_code_to_shell_code "$http_code")" + fi + shift + echo "$running_info" + # set -o errexit 后,程序将退出,退出前被trap bake._on_error Err捕获并显示错误堆栈 + return "$(_http_code_to_shell_code "$http_code")" +} + +______________________________study_start(){ echo x;} + +# IFS : https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_05 +tests.study_string_escape(){ + # $'' 语法 + assert $'1\n2' @is '1 +2' + assert '1\n2' @is '1\n2' + + assert $'1 +2' @is $'1\n2' + + x="1 +2" + assert "$(printf '%q' "$x" )" @is "$'1\n2'" + + assert "$(printf '%q' "$x" )" @is $(echo -n "${x@Q}") +} + +study.read(){ + local expected=""; + echo -e "1\n2" | while read -r x ; do + expected+="$1," + done + assert "$expected" @is '1,2,' +} +study.function_return(){ + ( + s(){ + printf "%s" "a b +c d"; + } + str="$(s)" + assert "${str}" @is $'a b\nc d' + printf "${str}" | od -c -td1 + read -a arr <<< "${str}" + assert "${#arr[@]}" @is 2 + ) + + ( + # 需重新构造更清洗的用例 + q(){ + printf "%q" "a b +c d"; + } + str="$(q)" + assert "${str}" @is "$'a b\nc d'" + # od:这是一个用来查看文件或其他输入的八进制或十六进制表示形式的工具。在这个例子中,使用 -c 参数告诉 od 以字符形式显示输入, + # 每个字符及其对应的ASCII码或Unicode码都会被展示出来。-t d1 表示输出格式为十进制 (d), + # 每个字节视为一个单位 (1),因此对于ASCII字符,这将显示每个字符对应的十进制ASCII码。 + echo "${str}" | od -c -td1 + assert "$(printf '%q' "$str")" @is "$'a b\nc d'" + + + + read -a arr <<< "${str}" + # read 按IFS默认$' \n\t',即用, ,分词 + # 由于printf "%q"会对非打印字符进行转义,已经被转为\和n,所以read分词时, + # 并没有任何 + assert "${#arr[@]}" @is 3 + assert "$(printf '[%s]' "${arr[@]}" )" @is "[$'a][bnc][d']" + + IFS=$'\n' read -a arr <<< "${str}" + # 我们重设分词符号只有$'\n', 但其实已被转义,所以并没有任何 + assert "${#arr[@]}" @is 1 + assert "$(printf '[%s]' "${arr[@]}" )" @is "[$'a bnc d']" + + # 函数的开发者有职责自己先区分好,我这个函数的输出,什么算是1行,算是1行的,就用printf "%q"转义 + # 那么,函数的调用者就有机会决定怎么分词 + # 函数的调用者有职责,对函数返回的字符串,自己先区分好,我拿到的函数输出,要不要分行处理 + # 就用IFS=$'\n' read -a arr <<< "${str}"分词 + ) +} +# Usage: pipe [args...] +# callback: callback +# Sample: cat file | pipe echo +study.pipe(){ + local func="$1" ; shift; + for arg in "$@"; do + $func "$arg" + done + if ! [[ -t 0 ]]; then + while read line; do + $func "$line" + done + fi +} + +____________________________test_start(){ echo x;} + +tests.api_cmd_parse(){ + bake.opt --cmd "tests.api_cmd_parse" --long stringOpt --type string + bake.opt --cmd "tests.api_cmd_parse" --long boolOpt --type bool + bake.opt --cmd "tests.api_cmd_parse" --long listOpt --type list + + assert "$(bake.parse --boolOpt )" @is 'declare -- __boolOpt="true" +shift 1' + assert "$(bake.parse --stringOpt "1 2" )" @is 'declare -- __stringOpt="1 2" +shift 2' + + # list type option + assert "$(bake.parse --listOpt "a 1" --listOpt "b 2" )" @is 'declare -a __listOpt=([0]="a 1" [1]="b 2") +shift 4' + + + # no exists option + assert "$(bake.parse --no_exists_opt)" @is "shift 0" +} + +tests.api_opt(){ + bake.opt --cmd "tests.opt.add" --long boolopt --type bool +} + +tests.api_opt_value_parse_and_get_value(){ + bake.opt --cmd "tests.api_opt_value_parse_and_get_value" --long xxx --type string + + # 模拟shell参数 + set -- --xxx chen + eval "$(bake.parse "$@")" + # shellcheck disable=SC2154 + assert "$__xxx" @is "chen" +} + + +function tests.str_escape() { + assert "$(bake.str_escape $'1' )" @is "'1'" + assert "$(bake.str_escape $'1 ')" @is "'1 '" + assert "$(bake.str_escape $'1 2 "')" @is "'1 2 \"'" + assert "$(bake.str_escape $'1 "')" @is "'1 \"'" + assert "$(bake.str_escape $'1 2\n')" @is "\$'1 2\n'" +} +function tests.str_unescape() { + assert "$(bake.str_unescape "$(bake.str_escape $'1' )")" @is $'1' + assert "$(bake.str_unescape "$(bake.str_escape $'1 2' )")" @is $'1 2' + assert "$(bake.str_unescape "$(bake.str_escape $'1 " ' )")" @is $'1 " ' + assert "$(bake.str_unescape "$(bake.str_escape $'1 \n ' )")" @is $'1 \n ' +} + +# shellcheck disable=SC2046 +# shellcheck disable=SC2031 +# shellcheck disable=SC2016 +study.declare(){ + + ( + declare a=1 b=2 + assert "$a" @is "1" + assert "$b" @is "2" + ) + + ( + # dynamic declare + s='a=1 b=2' + declare $(printf "%s" "$s") + assert "$a" @is "1" + assert "$b" @is "2" + ) + ( + s='a=1 b=2' + declare $(printf "%s" "$s") + assert "$a" @is "1" + assert "$b" @is "2" + ) + + ( + # declare ansi c quoting $'' + declare $'a=1 2' + assert "$a" @is "1 2" + + # but not work with real ansi c quoting + err=$( declare $(printf "%s" "a=$'1 2'") 2>&1 ) || true + assert "$err" @contains "2'': not a valid identifier" + + ) + + ( + + IFS=$'\n' + declare $(printf "%s" "a=$'1 2' +b=$'3 4'") + assert "$a" @is "$'1 2'" + assert "$b" @is "$'3 4'" + ) + + ( + # 目前看declare的脚本注射是比较安全的 + # "|| true" 防止set -o errexit + err=$( declare $(printf "%s" 'a=1 b=2 ls -al -- ll') 2>&1 ) || true + assert "$err" @contains "-al': not a valid identifier" + + err=$( declare $(printf "%s" 'a=$(ls -al)') 2>&1 ) || true + assert "$err" @contains "-al)': not a valid identifier" + + err=$( declare $(printf "%s" 'a=b $(ls)=x') 2>&1 ) || true + assert "$err" @contains $"ls)=x': not a valid identifier" + + err=$( declare $(printf "%s" 'a=b `ls`=x') 2>&1 ) || true + assert "$err" @contains "=x': not a valid identifier" + ) + +} + +# shellcheck disable=SC2046 +# shellcheck disable=SC2031 +# shellcheck disable=SC2016 + +# eval 用起来比declare 省心多了 +study.eval(){ + ( + eval "a=$'1 2' b=$'3 4'" + assert "$a" @is "1 2" + assert "$b" @is "3 4" + ) + + { + # 包含换行符也没问题 + eval "a=$'1 2' + b=$'3 4'" + assert "$a" @is "1 2" + assert "$b" @is "3 4" + } + + ( + # 数组也没问题 + eval "a=($'1 2' + $'3\n4')" + assert "${#a[@]}" @is 2 + assert "${a[0]}" @is "1 2" + assert "${a[1]}" @is $'3\n4' + ) + + { + # 关联数组也没问题 + eval "declare -A a=([a]=$'1\n2' [b]=$'2' )" + assert "${#a[@]}" @is 2 + assert "${a[a]}" @is $'1\n2' + assert "${a[b]}" @is $'2' + } + ( + # 用$'' ANSI C Quoting格式看来也是安全的 + eval "a=$'1 2' b=$'3 4 script inject not work'" + assert "$a" @is "1 2" + assert "$b" @is "3 4 script inject not work" + ) +} + +study.array(){ + a=("line1 a +line2" x) + assert "$(printf "[%s]" "${a[*]}")" @is $'[line1][a][line2][x]' + assert "$(printf "[%s]" ${a[*]})" @is $'[line1][a][line2][x]' + # 由于"$@" 特殊语法,数组可已包含空格换行符, 不影响分词 + assert "$(printf "[%s]" "${a[@]}")" @is $'[line1 a\nline2][x]' +} + +####################################################### +## bake test case +####################################################### + +tests.assert_sample(){ + assert $((1+1)) @is 2 +} + +tests.path_dirname(){ + assert "$(bake._path_dirname a/b/c '/')" @is "a/b" + assert "$(bake._path_dirname a '/')" @is "" + assert "$(bake._path_dirname "" '/')" @is "" + + # abstract path + assert "$(bake._path_dirname /a/b/c '/')" @is "/a/b" + assert "$(bake._path_dirname /a '/')" @is "" + assert "$(bake._path_dirname / '/')" @is "" +} +tests.path_first(){ + assert "$(bake._path_first a/b/c '/')" @is "a" + assert "$(bake._path_first a '/')" @is "a" + assert "$(bake._path_first '' '/')" @is "" + + assert "$(bake._path_first /a/b/c '/')" @is "/a" +} + +tests.path_basename(){ + assert "$(bake._path_basename a/b/c '/')" @is "c" + assert "$(bake._path_basename a '/')" @is "a" + assert "$(bake._path_basename "" '/')" @is "" + + # abstract path + assert "$(bake._path_basename "/a" '/')" @is "a" + assert "$(bake._path_basename "/" '/')" @is "" +} + +tests.str_cutLeft(){ + assert "$(bake._str_cutLeft a/b/c 'a/b/')" @is "c" + assert "$(bake._str_cutLeft a/b/c '')" @is "a/b/c" + + assert "$(bake._str_cutLeft /a/b/c '/')" @is "a/b/c" + + assert "$(bake._str_cutLeft a/b/c 'notStart')" @is "a/b/c" + assert "$(bake._str_cutLeft a/b/c '/')" @is "a/b/c" +} + + +tests.cmd_up_chain(){ + assert "$(bake._cmd_up_chain a.b)" @is "a.b +a +root" + assert "$(bake._cmd_up_chain 'root')" @is "root" + assert "$(bake._cmd_up_chain '')" @is "root" +} + +tests.cmd_children(){ + assert "$(bake._cmd_children test)" @is "" + assert "$(bake._cmd_children tests)" @contains "cmd_children" +} + +tests.str_split(){ + assert "$(bake._str_split "a/b" '/')" @is "a +b" + assert "$(bake._str_split "a/b/" '/')" @is "a +b" + + # abstract path + assert "$(bake._str_split "/a/b" '/')" @is " +a +b" + assert "$(bake._str_split "/a/b/" '/')" @is " +a +b" + + + + # 包含破坏性特殊字符 + # $'string' 形式的字符序列被视为一种特殊类型的单引号。序列扩展为字符串,字符串中的反斜杠转义字符按照 ANSI C 标准指定进行替换。 + # https://www.gnu.org/software/bash/manual/bash.html#ANSI_002dC-Quoting + assert "$(bake._str_split $'a\nb/c' "/" )" @is "a +b +c" + assert "$(bake._str_split $'a\n/b' "/" )" @is "a + +b" + + # default delimiter + assert "$(bake._str_split "a/b" )" @is "a +b" + + # other delimiter + assert "$(bake._str_split "a.b" '.')" @is "a +b" +} + +tests.cmd_register(){ + bake._cmd_register + assert "$(bake.info | grep tests.cmd_register)" \ + @contains "tests.cmd_register" +} + +tests.opt_cmd_chain_opts(){ + assert "$(bake._opts "root")" @is \ +"root/opts/debug +root/opts/help +root/opts/interactive" + + # "include parent option" + assert "$(bake._opts "bake.opt")" @is \ +"bake.opt/opts/cmd +bake.opt/opts/default +bake.opt/opts/desc +bake.opt/opts/long +bake.opt/opts/required +bake.opt/opts/short +bake.opt/opts/type +root/opts/debug +root/opts/help +root/opts/interactive" +} + + +# TODO bake._cmd_children 命令可以改造为既可以输出全称也可以输出短名,还可以设置depth展示层级 +# 查找出所有"tests."开头的函数并执行 +# 这种测试有点麻烦,不如bake.test +function test(){ + while IFS=$'\n' read -r functionName ; do + [[ "$functionName" != tests.* ]] && continue ; + # run test + printf "test: %s %-50s" "${TEST_PATH}" "$functionName()" + # TIMEFORMAT: https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html + # %R==real %U==user %S==sys %P==(user+sys)/real + TIMEFORMAT="real %R user %U sys %S percent %P" + ( + # 隔离test在子shell里,防止环境互相影响 + time "$functionName" ; + )# 2>&1 + # done <<< "$(compgen -A function)" # 还是declare -F 再过滤保险 + done <<<"$(declare -F | grep "declare -f" | awk {'print $3'})" +} + +############################################################# +# tail 初始化区 +# 在头head定义变量,尾tail执行初始化,中间只有函数 +############################################################# + + +bake.cmd --cmd root --desc " +$(cat <