From efe8d0fa2d6ab110f8beafb7a884ee55af04bcd7 Mon Sep 17 00:00:00 2001 From: David Kebler Date: Fri, 14 Apr 2023 21:27:40 -0700 Subject: [PATCH] refactor: generate Dockerfile from a template refactor src/ to be just a basic build add examples write up help.md and usage subcommand add test folder for dev testing improve install script many numerous improved to build script, moved portions of build script to functions in helpers.lib --- .gitignore | 4 +- Dockerfile | 63 -- Dockerfile.d/Dockerfile.tpl | 30 + Dockerfile.d/create | 5 + Dockerfile.d/init.tpl | 86 ++ Dockerfile.d/init/dirs.sh | 25 + Dockerfile.d/init/entrypoint.tpl | 29 + Dockerfile.d/init/help.sh | 14 + Dockerfile.d/init/image-info.sh | 5 + Dockerfile.d/init/map-host-id.sh | 9 + Dockerfile.d/init/profile.sh | 12 + Dockerfile.d/init/start.sh | 17 + Dockerfile.d/packages.tpl | 20 + build | 255 ++--- docker-bake.hcl | 42 +- examples/.env | 6 - examples/aliases | 13 - examples/build | 3 + examples/example.build | 1 - examples/example.env | 16 +- examples/private.env | 6 + examples/publish.env | 5 + examples/try | 2 + install | 29 +- lib/bash-tpl | 1296 ++++++++++++++++++++++++ lib/cmds/01-image-name | 68 -- lib/cmds/01-image-name.sh | 50 + lib/cmds/help.md | 89 ++ lib/cmds/{image-info => image-info.sh} | 0 lib/cmds/{image-push => image-push.sh} | 0 lib/cmds/image-tag | 31 - lib/cmds/image-tag.sh | 37 + lib/cmds/try | 150 --- lib/cmds/try.sh | 181 ++++ lib/cmds/usage | 34 - lib/cmds/usage.sh | 14 + lib/helpers.lib | 174 +++- lib/load.sh | 2 +- readme.md | 50 +- src/init/bin/base-entrypoint | 6 - src/init/bin/base-start | 19 - src/init/bin/entrypoint | 1 - src/init/bin/host-id-map.sh | 7 - src/init/bin/start | 1 - src/init/build.env | 7 + src/init/common/init.sh | 33 +- src/init/common/permitmod | 8 - src/init/env/build.env | 4 - src/init/env/run.env | 2 +- src/init/image.info | 5 + src/init/init.sh | 2 +- src/packages/alpine/packages.sh | 7 +- src/packages/common/packages | 4 +- test/build.env | 2 + test/test | 1 + test/test.env | 11 + 56 files changed, 2301 insertions(+), 692 deletions(-) delete mode 100644 Dockerfile create mode 100644 Dockerfile.d/Dockerfile.tpl create mode 100755 Dockerfile.d/create create mode 100644 Dockerfile.d/init.tpl create mode 100644 Dockerfile.d/init/dirs.sh create mode 100644 Dockerfile.d/init/entrypoint.tpl create mode 100644 Dockerfile.d/init/help.sh create mode 100644 Dockerfile.d/init/image-info.sh create mode 100644 Dockerfile.d/init/map-host-id.sh create mode 100644 Dockerfile.d/init/profile.sh create mode 100644 Dockerfile.d/init/start.sh create mode 100644 Dockerfile.d/packages.tpl delete mode 100644 examples/.env delete mode 100755 examples/aliases create mode 100755 examples/build delete mode 100644 examples/example.build create mode 100644 examples/private.env create mode 100644 examples/publish.env create mode 100755 examples/try create mode 100755 lib/bash-tpl delete mode 100755 lib/cmds/01-image-name create mode 100755 lib/cmds/01-image-name.sh create mode 100644 lib/cmds/help.md rename lib/cmds/{image-info => image-info.sh} (100%) rename lib/cmds/{image-push => image-push.sh} (100%) delete mode 100755 lib/cmds/image-tag create mode 100755 lib/cmds/image-tag.sh delete mode 100755 lib/cmds/try create mode 100755 lib/cmds/try.sh delete mode 100644 lib/cmds/usage create mode 100644 lib/cmds/usage.sh delete mode 100755 src/init/bin/base-entrypoint delete mode 100755 src/init/bin/base-start delete mode 120000 src/init/bin/entrypoint delete mode 100644 src/init/bin/host-id-map.sh delete mode 120000 src/init/bin/start create mode 100644 src/init/build.env delete mode 100644 src/init/common/permitmod delete mode 100644 src/init/env/build.env create mode 100644 src/init/image.info mode change 100644 => 100755 src/init/init.sh create mode 100644 test/build.env create mode 100755 test/test create mode 100644 test/test.env diff --git a/.gitignore b/.gitignore index f64df3f..4288ac8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ _opt/ .src TODO.md mnt/ -logs/ \ No newline at end of file +logs/ +Dockerfile +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 32a9ba7..0000000 --- a/Dockerfile +++ /dev/null @@ -1,63 +0,0 @@ -# syntax=docker/dockerfile:latest -ARG BASE_IMAGE -FROM $BASE_IMAGE -ARG BASE_IMAGE -ARG SYSADMIN_PW -ARG LINUX_DISTRO=alpine -WORKDIR /build - -# PACKAGES -RUN --mount=type=bind,source=.src/packages,target=/build/packages \ -<> /etc/profile - tail /etc/profile - echo "%%%%%%%%%%%%%%%%%%%%%%%%%%%" - fi - echo -e "\n ************* End Initialzation ************************" -eot -# END INITIALIZATION - -# default command -CMD ["/bin/bash", "-l"] -# default -WORKDIR /opt - - diff --git a/Dockerfile.d/Dockerfile.tpl b/Dockerfile.d/Dockerfile.tpl new file mode 100644 index 0000000..2aa6c00 --- /dev/null +++ b/Dockerfile.d/Dockerfile.tpl @@ -0,0 +1,30 @@ +# syntax=docker/dockerfile:latest +ARG BASE_IMAGE +FROM $BASE_IMAGE +ARG BASE_IMAGE +ARG SYSADMIN_PW +ARG VERBOSE +ARG LINUX_DISTRO=alpine +WORKDIR /build + +# PACKAGES +RUN --mount=type=bind,source=.src/packages,target=/build/packages \ +< /dev/null || return 1 + source <(../lib/bash-tpl Dockerfile.tpl ) | grep -v '^# ' > ../Dockerfile + echo " ************* uci build Dockerfile created *****************" + popd > /dev/null || return 2 diff --git a/Dockerfile.d/init.tpl b/Dockerfile.d/init.tpl new file mode 100644 index 0000000..7f2fa68 --- /dev/null +++ b/Dockerfile.d/init.tpl @@ -0,0 +1,86 @@ +#!/bin/bash +% + if [[ $REBUILD == "init" ]]; then + echo "## Busting Cache, Forcing Rebuild $(date)" + fi +% +quiet () { + if [[ $VERBOSE ]]; then $@; fi +} +quiet echo -e "\n ************************************************* \n" +quiet echo "****** Initializing Image with build source ******" +cd init +pwd; quiet ls -la +export BUILDING=true +export BUILD_DIR=$PWD +export SHELL=/bin/bash +export BIN_DIR=/opt/bin +mkdir -p $BIN_DIR +echo "export BIN_DIR=${BIN_DIR}" >> /tmp/profile +echo 'export PATH=$BIN_DIR:$PATH' >> /tmp/profile + +echo " ##### creating entrypoint script ###" +cat << EOE >$BIN_DIR/entrypoint +.INCLUDE ./init/entrypoint.tpl +EOE +chmod +x $BIN_DIR/entrypoint +quiet echo '------ default entrypoint -----' +quiet ls -la $BIN_DIR/entrypoint +quiet cat $BIN_DIR/entrypoint +quiet echo "------------" + +echo " ##### creating default start script ###" +cat << "EOS" >$BIN_DIR/start +.INCLUDE ./init/start.sh +EOS +chmod -R +x $BIN_DIR/start +quiet echo "--- DEFAULT START SCRIPT in $BIN_DIR/start ---" +quiet cat $BIN_DIR/start +quiet echo "-----------------------------------" + +echo " ##### creating map host id script ###" +cat << "EOM" >$BIN_DIR/map-host-id +.INCLUDE ./init/map-host-id.sh +EOM +chmod +x $BIN_DIR/map-host-id + +[[ -f image.info ]] && cp image.info /opt + +.INCLUDE ./init/dirs.sh + +if [[ -f build.env ]]; then + echo "-- sourcing /build/build.env --" + quiet ls -la + quiet cat build.env + quiet echo "----------------------" + source build.env +fi + +if [[ -f ./init.sh ]]; then + echo "############## Running Script init.sh of build source #################" + quiet echo "----- build environment ------" + quiet env + quiet echo "----- env ------" + quiet echo "-------------------- init.sh ------------------------------" + quiet cat ./init.sh + quiet echo "-------------------------------------------------------------" + # init.sh must have shebang and be executable + if ! $SHELL ./init.sh; then return 1; fi + echo "############## Finished running init.sh build script #########################" +fi + +.INCLUDE ./init/profile.sh + +echo "****** creating user and group 'host' with ids 1000 *****" +groupadd -g 1000 host +useradd -r -g host -u 1000 host +# map host id now based on build environment +if [[ $VOLUME_DIRS ]]; then + echo "*** creating and configuring volume directories ***" + echo $VOLUME_DIRS + mkdir -p $VOLUME_DIRS + $BIN_DIR/map-host-id + chmod -R g+rw $VOLUME_DIRS +fi + +echo -e "\n ************* End Initialzation ************************" \ No newline at end of file diff --git a/Dockerfile.d/init/dirs.sh b/Dockerfile.d/init/dirs.sh new file mode 100644 index 0000000..d5f2860 --- /dev/null +++ b/Dockerfile.d/init/dirs.sh @@ -0,0 +1,25 @@ +if [[ -d env/ ]]; then + export ENV_DIR=/opt/env + echo "############## Adding Environment Directroy $ENV_DIR #################" + echo "export ENV_DIR=${ENV_DIR}" >> /tmp/profile + quiet echo "copying env/ to $ENV_DIR" + /bin/cp -R -p env/. $ENV_DIR + quiet ls -la $ENV_DIR +fi +if [[ -d bin/ ]]; then + echo "############## Copying to Binary Directroy $BIN_DIR #################" + quiet echo "copying bin/ to $BIN_DIR" + /bin/cp -R -p bin/. $BIN_DIR + # chmod -R +x $BIN_DIR + quiet ls -la $BIN_DIR +fi + +if [[ -d lib/ ]]; then + export LIB_DIR=/opt/lib + echo "############## Adding Library Directroy $LIB_DIR #################" + echo "export LIB_DIR=${LIB_DIR}" >> /tmp/profile + quiet echo "copying lib/ to $LIB_DIR" + /bin/cp -R -p lib/. $LIB_DIR + chmod -R +x $LIB_DIR + quiet ls -la $LIB_DIR +fi \ No newline at end of file diff --git a/Dockerfile.d/init/entrypoint.tpl b/Dockerfile.d/init/entrypoint.tpl new file mode 100644 index 0000000..0dbbb57 --- /dev/null +++ b/Dockerfile.d/init/entrypoint.tpl @@ -0,0 +1,29 @@ +#!/bin/bash +# to maintain variable $ in container script espcape with \$ +# otherwise subtitution will happen during build +case "\$1" in +maphostid) +shift 1 +/bin/bash -l -c '\$BIN_DIR/map-host-id \$@' \$0 "\$@" +;; +shell) +/bin/bash -c "cd \${INITIAL_DIR:-/opt}; exec bash -l" +;; +help) +.INCLUDE ./init/help.sh +;; +image) +.INCLUDE ./init/image-info.sh +;; +script) +shift 1 +cat | /bin/bash -l +;; +${ENTRYPOINT_CMD:-start}) +shift 1 +/bin/bash -l -c '${ENTRYPOINT_CMD_PATH:-$BIN_DIR/start} \$@' \$0 "\$@" +;; +*) +/bin/bash -l -c '"\$@"' \$0 "\$@" +;; +esac \ No newline at end of file diff --git a/Dockerfile.d/init/help.sh b/Dockerfile.d/init/help.sh new file mode 100644 index 0000000..b3d52d9 --- /dev/null +++ b/Dockerfile.d/init/help.sh @@ -0,0 +1,14 @@ +cat < +otherwise you can pass any shell command such as 'ls -la' +the current container custom command is > ${ENTRYPOINT_CMD:-start} +and the script for that command is in ${ENTRYPOINT_CMD_PATH:-$BIN_DIR/start} +----- +you can replace this start script with your own +your own script in $BIN_DIR/start in your build source directory +or set the \$ENTRYPOINT_CMD and \$ENTRYPOINT_CMD_PATH variables +It is possible to override the container entrypoint with your own +but is not recommmended as then a login shell will not be used +and critical environment variables will not be set +HELP \ No newline at end of file diff --git a/Dockerfile.d/init/image-info.sh b/Dockerfile.d/init/image-info.sh new file mode 100644 index 0000000..0fce780 --- /dev/null +++ b/Dockerfile.d/init/image-info.sh @@ -0,0 +1,5 @@ +if [[ -f /opt/image.info ]]; then +echo -e "\n--------- image info found at /opt/image.info----------" +cat /opt/image.info +echo -e "\n****************************" +fi diff --git a/Dockerfile.d/init/map-host-id.sh b/Dockerfile.d/init/map-host-id.sh new file mode 100644 index 0000000..7814226 --- /dev/null +++ b/Dockerfile.d/init/map-host-id.sh @@ -0,0 +1,9 @@ +#!/bin/bash +if [[ $VOLUME_DIRS ]]; then + echo changing ownership of directories $VOLUME_DIRS + echo to ${HOST_MAP:-"host:host"} + declare usesudo + [[ ! $EUID -eq 0 ]] && usesudo=sudo + $usesudo chown -R ${HOST_MAP:-"host:host"} $VOLUME_DIRS + ls -la $VOLUME_DIRS +fi \ No newline at end of file diff --git a/Dockerfile.d/init/profile.sh b/Dockerfile.d/init/profile.sh new file mode 100644 index 0000000..2ab4521 --- /dev/null +++ b/Dockerfile.d/init/profile.sh @@ -0,0 +1,12 @@ +[[ -f $ENV_DIR/run.env ]] && echo 'source $ENV_DIR/run.env' >> /tmp/profile + +while read line; do +if ! grep -q "$line" /etc/profile; then + quiet echo added $line to /etc/profile + echo $line >> /etc/profile +fi +done < /tmp/profile +# echo "echo /etc/profile has been sourced" >> /etc/profile +quiet echo "&&&&&&& last 10 of /etc/profile &&&&&" +quiet tail /etc/profile +quiet echo "%%%%%%%%%%%%%%%%%%%%%%%%%%%" \ No newline at end of file diff --git a/Dockerfile.d/init/start.sh b/Dockerfile.d/init/start.sh new file mode 100644 index 0000000..2628408 --- /dev/null +++ b/Dockerfile.d/init/start.sh @@ -0,0 +1,17 @@ +#!/bin/bash +#***** CONTAINER DEFAULT CUSTOM SCRIPT ******************" +case "$1" in +sub1) +echo this would be a subcommand #1 +echo with arguments $@ +;; +sub2) +shift 1 +echo this would be a subcommand #1 +echo with arguments $@ +;; +*) +echo "running this command $*" +echo within login shell +/bin/bash -c '"$@"' $0 "$@" +esac \ No newline at end of file diff --git a/Dockerfile.d/packages.tpl b/Dockerfile.d/packages.tpl new file mode 100644 index 0000000..fd02466 --- /dev/null +++ b/Dockerfile.d/packages.tpl @@ -0,0 +1,20 @@ +% + if [[ $REBUILD == "packages" ]]; then + echo "## Busting Cache, Forcing Rebuild $(date)" + fi +% +echo -e "\n ************************************************* \n" +echo "Building Image from Base: $BASE_IMAGE" +echo "Distro: $LINUX_DISTRO" +echo " ---- running packages install script ---" +if [[ $LINUX_DISTRO == "alpine" ]]; then + echo "-------------------------------" + echo "adding shadow bash and bash completion coreutils for alpine" + echo "to be compatible with other distros" + apk add --no-cache shadow bash bash-completion coreutils + echo "-------------------------------" +fi +cd packages +/bin/sh ./packages.sh +cd .. +echo -e "\n********************************************************" \ No newline at end of file diff --git a/build b/build index 4213537..4d2cb12 100755 --- a/build +++ b/build @@ -1,46 +1,56 @@ #!/bin/bash -docker_image_build () { +udbuild () { -local targets=(dev arm64 amd64 deploy private multi) -local verbose; local log_dir; local no_prompt -local efile +local targets=(dev arm64 amd64 publish multi default) +local log_dir; local no_prompt +local append_efile declare OPTION; declare OPTARG; declare OPTIND BDIR=$(dirname "$(realpath "$BASH_SOURCE")") export BDIR # load script library source $BDIR/lib/load.sh +BUILD_EFILE="" # check for subcommands first case "$1" in try) - shift 1 - # type try_container - try_container "$@" + shift 1; try_container "$@"; return $? ;; + load_env_file) + echo -e "@@@@@@ loading build environment file for external use @@@@@@" + BUILD_EFILE=$(echo -- "$@" | grep -oP -- '(?<=-e )[^ ]*') + source_env_file "$BUILD_EFILE" + echo -e "@@@@@@@@@@@@@@@@@ returning to calling script @@@@@@@@@@@@@@@" return $? ;; - image_name) + build_src) shift 1; get_build_src "$@"; return $? ;; + help) + ;& + --help) + ;& + -help) shift 1; usage "$@"; return $? ;; + source) type udbuild; return $? ;; + image) shift 1 - image_name "$@" - return $? - ;; - tag) - shift 1 - image_tag "$@" - return $? - ;; - push) - shift 1 - image_push "$@" - return $? - ;; - info) - shift 1 - [[ $1 == "arch" ]] && { shift 1; image_arch "$@"; return $?; } - [[ $1 == "exists" ]] && { shift 1; image_exists "$@"; return $?; } - [[ $1 == "id" ]] && { shift 1; image_id "$@"; return $?; } - image_info "$@"; return $? + case "$1" in + name) shift 1; image_name "$@" ;; + tag) shift 1; image_tag "$@" ;; + push) shift 1; image_push "$@" ;; + delete) shift 1; image_delete "$@" ;; + info) + shift 1 + case "$1" in + arch) shift 1; image_arch "$@" ;; + exists) shift 1; image_exists "$@" ;; + tags) shift 1; image_tags "$@" ;; + id) shift 1; image_id "$@" ;; + * ) image_info "$@" + esac + ;; + *) echo no image subcommand $1 ;; + esac + return $? ;; esac @@ -52,20 +62,30 @@ exit_abnormal() { # Function: Exit with error. [[ -z "$PS1" ]] || no_prompt=true overwrite=true -while getopts 'g:e:b:d:t:ncr:u:plhs:avo' OPTION; do +while getopts 'fg:e:b:d:t:nc:r:u:lhs:a:voi:p' OPTION; do # echo processing: option:$OPTION argument:$OPTARG index:$OPTIND remaining:${@:$OPTIND} case "$OPTION" in + i) + IMAGE_INFO=$OPTARG + ;; e) - if source_env_file $OPTARG; then efile=true; else return 2; fi + BUILD_EFILE=$OPTARG + if ! source_env_file $BUILD_EFILE; then return 2; fi ;; o) unset overwrite ;; v) - verbose=true + VERBOSE=true ;; a) - # automated - script is to be run without prompt (non-interactive) + append_efile=$OPTARG + ;; + f) + REBUILD=init + ;; + p) + echo "build script will be run WITHOUT user prompts (i.e. non-interactive)" no_prompt=true ;; b) @@ -82,7 +102,7 @@ while getopts 'g:e:b:d:t:ncr:u:plhs:avo' OPTION; do ;; l) # append distro name to image name - append_distro=true + APPEND_DISTRO=true ;; t) TARGET=$OPTARG @@ -94,10 +114,7 @@ while getopts 'g:e:b:d:t:ncr:u:plhs:avo' OPTION; do RUSER=$OPTARG ;; c) - try=true - ;; - p) - push=true + TRY_CMD=$OPTARG ;; n) nocache="--no-cache" @@ -118,70 +135,27 @@ done shift $((OPTIND - 1)) -[[ ! $efile ]] && source_env_file +[[ ! $BUILD_EFILE ]] && source_env_file -# processing the build source directory -if [[ ! $BUILD_SRC ]]; then - echo no BUILD_SRC directory specified - echo using present directory $PWD - BUILD_SRC=$PWD +if ! get_build_src; then + if [[ $no_prompt ]] ; then + echo aborting the build... + echo -e "\e[1;31mNOTE: use '_default_' to explicitly use build source in uci-docker-build repo\e[1;37m" + return 2 + else + echo "Do you want to use the uci-docker-build default build source" + echo "at $BDIR/src " + read -n 1 -p "instead? [y]=>" REPLY + [[ $REPLY != "y" ]] && echo -e "\n" && return 2 + BUILD_SRC=$BDIR/src + echo -e "\n\e[1;31mNOTE: use '_default_' to explicitly use build source in uci-docker-build repo\e[1;37m" + fi fi -if [[ ! $(isAbsPath $BUILD_SRC) ]] ; then - if [[ ${BUILD_SRC} == "_base_" ]]; then - BUILD_SRC=${BDIR}/src - else - BUILD_SRC=$(realpath ${BUILD_SRC}) - fi -fi - -if [[ ! ( -d $BUILD_SRC/packages && -d $BUILD_SRC/init ) ]]; then -echo -e "\e[1;31minvalid build source directory" - echo $BUILD_SRC - echo -e "it does not contain packages and init subirectories\e[1;37m" - if [[ ! $(basename $BUILD_SRC) == "src" ]]; then - echo checking in src/ subdirectory for source - if [[ ( -d $BUILD_SRC/src/packages && -d $BUILD_SRC/src/init ) ]]; then - BUILD_SRC=$BUILD_SRC/src - echo found source in src/ subdirectory changing to source directory to - echo $BUILD_SRC - else - echo -e "\e[1;31mERROR: no build source directory at $BUILD_SRC" - echo -e "with init/ and packages/ subdirectores was found\e[1;37m" - if [[ $no_prompt ]] ; then - echo aborting the build... - echo -e "\e[1;31mNOTE: use '_base_' to explicitly use build source in uci-docker-build repo\e[1;37m" - return 2 - else - echo "Do you want to use the uci-docker-build repo source scripts" - echo "at $BDIR/src " - read -n 1 -p "instead? [y]=>" REPLY - [[ $REPLY != "y" ]] && echo -e "\n" && return 2 - BUILD_SRC=$BDIR/src - echo -e "\n\e[1;31mNOTE: use '_base_' to explicitly use build source in uci-docker-build repo\e[1;37m" - fi - fi - fi -fi -# done processing build source directory - -log_dir=$PWD/logs -mkdir -p $log_dir - - pushd "$BDIR" > /dev/null || return 3 - +TARGET=${TARGET:-default} [[ ! "${targets[@]}" =~ $TARGET ]] && echo $TARGET is not a valid target && echo valid targets are: ${targets[@]} && exit 4 -LINUX_DISTRO=${LINUX_DISTRO:-alpine} - -if [[ $BASE_IMAGE ]]; then -echo determining DISTRO of base image: $BASE_IMAGE -LINUX_DISTRO=$(docker_image_distro $BASE_IMAGE) - [[ ! $LINUX_DISTRO ]] && echo "unable to get base image OS for: $BASE_IMAGE, aborting build" && return 5 - echo $BASE_IMAGE is built from distro $LINUX_DISTRO - else - BASE_IMAGE=$LINUX_DISTRO -fi +get_distro IMAGE_NAME=$(make_image_name $@) @@ -199,90 +173,83 @@ if [[ $(image_exists $IMAGE_NAME) ]]; then fi ARCH=$(get_arch) +log_dir=$PWD/logs +mkdir -p $log_dir +[[ $TARGET == "dev" ]] && VERBOSE=true export BASE_IMAGE export TAG export IMAGE_NAME export LINUX_DISTRO export BUILD_SRC -export KEEP -export SYSADMIN_PW export ARCH +export VERBOSE -echo -e "\e[1;37m********************" -echo "Using scripts source directory at $BUILD_SRC" -echo "Building with base image: $BASE_IMAGE" -#todo based on target form image names -echo "Outputing to image name => $IMAGE_NAME<-arch>:${TAG:-latest}" -[[ $push || $TARGET == "private" ]] && echo "Will push image to ${REPO:-hub.docker.com}" -[[ $TARGET == "deploy" ]] && echo "Will build and push both amd64 and arm64 images to hub.docker.com" -[[ $TARGET == "dev" || ! $TARGET ]] && echo "Building image for local machine with architecture $ARCH" -echo "Linux Distro: $LINUX_DISTRO" -echo "Using build target: ${TARGET:-default}" -echo "Build Command: docker buildx --builder ${builder} bake ${nocache} ${TARGET}" -if [[ $verbose ]]; then - echo -e "\n---------------------------------" - docker buildx bake --print $TARGET - echo -e "\n---------------------------------" - echo "build scripts at $BUILD_SRC to be copied to ${BUILD_DIR:-/build} in container ***** " - ls -la $BUILD_SRC - echo -e "\n----- base init script init.sh ------" - cat $BUILD_SRC/init.sh - echo -e "\n---------------------------------" -fi -echo -e "u********************\e[0;37m" +build_info if [[ ! $no_prompt ]]; then read -n 1 -p "do you want to continue [y]=>" REPLY [[ $REPLY != "y" ]] && echo -e "\n" && return 4 fi -builder=default -if [[ $TARGET == "deploy" ]]; then - builder=deploy - if ! docker buildx ls | grep -q deploy ; then - echo multiarch deploy builder does not exist, creating with docker-container driver - docker buildx create --name deploy --driver docker-container >/dev/null - docker buildx ls | grep deploy - fi +if ! source $BDIR/Dockerfile.d/create; then +echo unable to create Dockerfile from template, aborting build +return 3 fi -[[ $TARGET == "private" && ! $REPO ]] && echo "must use '-r ' if building to private repo" && exit 3 +builder=default +if [[ $TARGET == "publish" ]]; then + builder=publish + pushd "$BDIR" > /dev/null || return 3 + if ! docker buildx ls | grep -q publish ; then + echo publish builder does not exist, creating with docker-container driver + docker buildx create --name publish --driver docker-container >/dev/null + docker buildx ls | grep publish + fi + popd > /dev/null || return 4 +fi -# copy source directory to temporary .src/ subdirectory -# MUST either be readable by all or group readable by docker group -rm -rf $BDIR/.src +# copy or bind build source directory to temporary .src/ subdirectory in build repo +[[ -d $BDIR/.src ]] && rm -rf $BDIR/.src +if [[ $(which rsync 2> /dev/null ) ]]; then rsync -aAru ${BUILD_SRC:-src}/ $BDIR/.src -ls -la $BDIR/.src +else +echo no rsync copying with cp +/bin/cp -a ${BUILD_SRC:-src}/. $BDIR/.src > /dev/null 2>&1 +fi + +if [[ -f $append_efile ]]; then +/bin/cp "$append_efile" "$BDIR/.src/init/env/_build.env_" +echo 'source $ENV_DIR/_build.env_' >> $BDIR/.src/init/build.env +fi + +pushd "$BDIR" > /dev/null || return 3 + +######### RUNNING THE DOCKER BUILD COMMAND ###################### echo running build command: docker buildx --builder ${builder} bake ${nocache} ${TARGET} docker buildx --builder ${builder} bake ${nocache} ${TARGET} 2>&1 | tee "$log_dir/${IMAGE_NAME//\//-}build.log" [[ $? == 0 ]] && echo succcess building image $IMAGE_NAME || exit_abnormal 5 +popd > /dev/null || return 4 rm -rf $BDIR/.src -popd > /dev/null - -if [[ ($try || $TARGET == "dev") ]] && [[ ! $no_prompt ]]; then +if [[ ($TRY_CMD || $TARGET == "dev") ]]; then echo trying newly built image in a container - try_container -m opt $([[ $TARGET == "deploy" ]] && echo -p) $IMAGE_NAME + echo name before try $IMAGE_NAME + try_container build -m opt $([[ $TARGET == "publish" ]] && echo -p) ${TRY_CMD:-shell} fi if [[ $TARGET == "private" ]]; then - # echo pushing arm64 image $IMAGE_NAME to ${REPO:-docker hub} + echo pushing arm64 image $IMAGE_NAME to ${REPO:-docker hub} image_push -a -r $REPO $IMAGE_NAME - # echo pushing amd image $IMAGE_NAME to ${REPO:-docker hub} + echo pushing amd image $IMAGE_NAME to ${REPO:-docker hub} image_push -r $REPO $IMAGE_NAME - else - if [[ $push && (! $TARGET == "dev") ]];then - # echo pushing $IMAGE_NAME to ${REPO:-docker hub} - image_push $([[ $TARGET == "arm" ]] && echo -a) -r $REPO $IMAGE_NAME - fi fi } # if script was executed then call the function -(return 0 2>/dev/null) || docker_image_build $@ +(return 0 2>/dev/null) || udbuild "$@" diff --git a/docker-bake.hcl b/docker-bake.hcl index c5bd5af..94ab768 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -14,12 +14,15 @@ variable "BASE_IMAGE" { variable "SYSADMIN_PW" { default = "" } +variable "VERBOSE" { + default = "" +} variable "ARCH" { default = "" } function "tag" { params = [suffix] - result = [format("${IMAGE_NAME}%s:${TAG}", notequal("", suffix) ? "-${suffix}" : "")] + result = [format("${IMAGE_NAME}%s:${TAG}", notequal("${ARCH}", suffix) ? "-${suffix}" : "")] } # groups group "dev" { @@ -28,10 +31,7 @@ group "dev" { group "default" { targets = ["${ARCH}"] } -group "deploy" { - targets = ["multi"] -} -group "private" { +group "multi" { targets = [ "amd64", "arm64" @@ -39,7 +39,7 @@ group "private" { } # intended for use with default local docker builder # uses 'dev' group in docker-bake.hcl -# assume dev machine is amd64 machine +# assume dev and default build for architecture of local machine target "amd64" { context = "." dockerfile = "Dockerfile" @@ -47,9 +47,10 @@ target "amd64" { LINUX_DISTRO = "${LINUX_DISTRO}" BASE_IMAGE = "${BASE_IMAGE}" TAG = "${TAG}" + VERBOSE = "${VERBOSE}" SYSADMIN_PW = "${SYSADMIN_PW}" } - tags = tag("") + tags = tag("amd64") platforms = ["linux/amd64"] } @@ -61,28 +62,11 @@ target "arm64" { platforms = ["linux/arm64"] } -# must use with docker-container driver for multiarch image deployment to registry -# uses 'deploy' group in docker-bake.hcl -target "multi" { +# must use with docker-container driver for multiarch image publishment to registry +# uses 'publish' group in docker-bake.hcl +target "publish" { inherits = ["amd64"] - tags = tag("") + tags = ["${IMAGE_NAME}:${TAG}"] platforms = ["linux/amd64", "linux/arm64"] output = ["type=registry"] -} - - -// variable "RUSER" { -// default = "" -// } -// function "user" { -// params = [] -// result = [notequal("", RUSER) ? "${RUSER}/" : ""] -// } - -// function "tagamd" { -// params = [] -// result = [ -// format("%s${LINUX_DISTRO}:${TAG}", notequal("", RUSER) ? "${RUSER}/" : ""), -// format("%s${LINUX_DISTRO}-amd64:${TAG}", notequal("", RUSER) ? "${RUSER}/" : "") -// ] -// } \ No newline at end of file +} \ No newline at end of file diff --git a/examples/.env b/examples/.env deleted file mode 100644 index c169afd..0000000 --- a/examples/.env +++ /dev/null @@ -1,6 +0,0 @@ -SYSADMIN_PW=ucommandit -# default is alpine -# LINUX_DISTRO=alpine -RUSER=ucommandit -# TARGET=deploy -BUILD_SRC=../src diff --git a/examples/aliases b/examples/aliases deleted file mode 100755 index 770cfdc..0000000 --- a/examples/aliases +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -# three ways to invoke with --no-cache - -# inline -# NO_CACHE=true ./build "$@" - -# with -n option (prefered) -./build -n "$@" -alias rebuild="build -nfunction_list" - -# as export -#export NO_CACHE=true -#./build "$@" diff --git a/examples/build b/examples/build new file mode 100755 index 0000000..b02c7ad --- /dev/null +++ b/examples/build @@ -0,0 +1,3 @@ +# invokes try with the example environment file +# assumes image already built +udbuild -e example.env "$@" diff --git a/examples/example.build b/examples/example.build deleted file mode 100644 index f6ef0ba..0000000 --- a/examples/example.build +++ /dev/null @@ -1 +0,0 @@ -dbuild -e example.env "$@" diff --git a/examples/example.env b/examples/example.env index 7db779a..8ad4e01 100644 --- a/examples/example.env +++ b/examples/example.env @@ -1,17 +1,20 @@ + +# for easy use copy this file to .env and it will be sourced +# otherwise invoke `udbuild -e example.env` # using a filename of just .env will load it by default # LINUX_DISTRO ignored if BASE_IMAGE is set -LINUX_DISTRO=alpine +# LINUX_DISTRO=alpine # BASE_IMAGE="dockerhubuser/mybase" # tag is 'latest' by default # TAG=1.0.0 -# will be prepended to image name with /, used mostly for deploying -RUSER=dockerhubuser +# will be prepended to image name with /, used mostly for publishing +RUSER=testing # default is hub.docker.com # REPO=my.priviate.repo.net # if using base source this will set the pw for the sysadmin user in the image SYSADMIN_PW=ucommandit -# default target is dev -# TARGET=deploy +# default target is "default" +# TARGET=publish BUILD_SRC=../src # looks for /init and /packages in present directory by default # also looks in src/ subdirectory @@ -19,9 +22,6 @@ BUILD_SRC=../src # use '_base_' to force using the uci-docker-build build source # BUILD_SRC=._base_ # in the image where the build scripts are put /build by default -# BUILD_DIR=/opt/build -# keep the build scripts in the image. default is to remove them after build -# KEEP=true diff --git a/examples/private.env b/examples/private.env new file mode 100644 index 0000000..220dd00 --- /dev/null +++ b/examples/private.env @@ -0,0 +1,6 @@ +# this would push build image to a custome git server that supports packages like gitea +# LINUX_DISTRO=alpine +RUSER=ucommandit +TARGET=private +REPO=git.mygitserver.net +BUILD_SRC=../src diff --git a/examples/publish.env b/examples/publish.env new file mode 100644 index 0000000..1f3201f --- /dev/null +++ b/examples/publish.env @@ -0,0 +1,5 @@ +# LINUX_DISTRO=alpine +# this will publish both arm and amd version to docker hub +RUSER=ucommandit +TARGET=publish +BUILD_SRC=../src diff --git a/examples/try b/examples/try new file mode 100755 index 0000000..57e389b --- /dev/null +++ b/examples/try @@ -0,0 +1,2 @@ +# invokes build with the example environment file +udbuild try -e example.env ${@:-shell} diff --git a/install b/install index ce07ebe..0108f3c 100755 --- a/install +++ b/install @@ -11,19 +11,26 @@ declare -a a="(${PATH//:/ })" for i in ${a[*]}; do [[ $i == $parent ]] && found=true; done if [[ $found ]]; then echo creating a link \'$cmd\' in \'$parent\' to \'$builder\' - if ln -ns $builder/build $target; then + if [[ -f $target ]]; then + echo "$target already exists do you want to overwrite? (y/n) " + read -e ans + [[ ! $ans == "y" ]] && exit 1 + fi + if ln -fns $builder/build $target; then [[ ! $(command -v $cmd) ]] && echo FATAL: link failed $cmd not found in path \ || echo install success: try \'$cmd -h\' now - else + else echo Error creating link echo if \': Permission denied\' 'then' run \'sudo ./install\' fi - else - echo $parent not in current path - echo $PATH - echo link to script not created - echo "add the following export somewhere in your shell (e.g. ~/.bashrc)" - echo "export UDBUILD=$builder/build" - echo 'and then use $UDBUILD to invoke the build script ( e.g $UDBUILD -e mybuild.env)' - echo "or rerun this script using a directory in the system path (e.g ./install /usr/bin build)" - fi \ No newline at end of file + else + echo "Install failed: $parent not in current path" + echo $PATH + echo "link to script not created. your install options are:" + echo "1. add $parent to your PATH" + echo "2. rerun this script using a directory in the system path (e.g ./install /usr/bin build)" + echo "3. add the following export somewhere in your shell (e.g. ~/.bashrc)" + echo " export UDBUILD=$builder/build" + echo ' and then use $UDBUILD to invoke the build script' + echo ' ( e.g $UDBUILD -e Smybuild.env)' +fi \ No newline at end of file diff --git a/lib/bash-tpl b/lib/bash-tpl new file mode 100755 index 0000000..fcb1271 --- /dev/null +++ b/lib/bash-tpl @@ -0,0 +1,1296 @@ +#!/usr/bin/env bash +####################################################################### +# SPDX-License-Identifier: MIT +# Copyright (c) 2021 TekWizely & co-authors +# +# Use of this source code is governed by the MIT license. +# See the accompanying LICENSE file, if present, or visit: +# https://opensource.org/licenses/MIT +####################################################################### +VERSION="v0.7.0" +####################################################################### +# Bash-TPL: A Smart, Lightweight shell script templating engine +# +# Lets you mark up textual files with shell commands and variable +# replacements, while minimally impacting your original file layout. +# +# Templates are compiled into shell scripts that you can invoke +# (along with variables, arguments, etc.) to generate complete and +# well-formatted output text files. +# +# Smart +# +# Encourages you to use extra indentation to write clean, well- +# formatted templates, and smartly removes the indentations from the +# generated template scripts. +# +# This results in both templates that are easily readable and +# maintainable, and generated text files that look as good as if they +# were written by hand. +# +# NOTE: Consistent Formatting +# +# The key to success with Bash-TPL indentation fix-up logic is +# Consistent Formatting; using consistent indentation throughout your +# template will yield best results. +# +# Learn More: +# https://github.com/TekWizely/bash-tpl +####################################################################### + +function usage() { + cat << USAGE +Bash-TPL is a smart, lightweight shell script templating engine + +usage: bash-tpl [flags] [--] file + bash-tpl [flags] - + cat file | bash-tpl [flags] + +options: + -h, --help + show help screen + --version + show version + -o, --output-file + write to specified file (default: stdout) + -- treat remaining options as positional arguments + - read from stdin + +customize delimiters: + --tag-delims 'xx xx' + set tag delimiters (default: '<% %>') + --tag-stmt-delim 'x' + set tag statement delimiter (default: '%') + --stmt-delim 'x+' + set statement delimiter (default: '%') + --stmt-block-delims 'x+ x+' + set statement block delimiters + defaults to statement delimiter if not explicitly set + --txt-delim + --text-delim 'x+[ ]?' (single trailing space allowed) + set text delimiter (default: '% ') + --dir-delim + --directive-delim 'x+' + set directive delimiter (default: '.') + --cmt-delim + --comment-delim 'x+' + set template comment delimiter + defaults to directive delimiter + '#' if not explicitly set + --reset-delims + reset all delimiters to defaults + delim options provided after this option are honored + +supported environment variables: + BASH_TPL_TAG_DELIMS + BASH_TPL_TAG_STMT_DELIM + BASH_TPL_STMT_DELIM + BASH_TPL_STMT_BLOCK_DELIMS + BASH_TPL_TEXT_DELIM + BASH_TPL_DIR_DELIM + BASH_TPL_CMT_DELIM + +example: + $ echo 'Hello <% \$NAME %>' > test.tpl + $ NAME="Chuck Norris" source <( bash-tpl test.tpl ) + + Hello Chuck Norris + +learn more: https://github.com/TekWizely/bash-tpl + +USAGE +} + +####################################################################### +# Delim Functions +####################################################################### + +TAG_DELIM_REGEX='^([^[:blank:]])([^[:blank:]]) ([^[:blank:]])([^[:blank:]])$' +TAG_STMT_DELIM_REGEX='^([^[:blank:]])$' +STMT_DELIM_REGEX='^([^[:blank:]]+)$' +STMT_BLOCK_DELIM_REGEX='^([^[:blank:]]+) ([^[:blank:]]+)$' +STMT_BLOCK_TEXT_REGEX='^([^[:blank:]]+\ ?)$' # Optional trailing ' ' + +## +# reset_delims +# +function reset_delims() { + TAG_START_DELIM1='<' + TAG_START_DELIM2='%' + TAG_STOP_DELIM1='%' + TAG_STOP_DELIM2='>' + + TAG_STMT_DELIM='%' + + STMT_DELIM='%' + + TEXT_DELIM_UNDEFINED=1 + TEXT_DELIM='' + + STMT_BLOCK_DELIM_UNDEFINED=1 + STMT_BLOCK_START_DELIM='' + STMT_BLOCK_STOP_DELIM='' + + DIRECTIVE_DELIM='.' + + COMMENT_DELIM_UNDEFINED=1 + COMMENT_DELIM='' +} + +## +# parse_tag_delims +# $1 = delims +# $2 = src (for error msg) +# +function parse_tag_delims() { + if [[ "${1}" =~ $TAG_DELIM_REGEX ]]; then + TAG_START_DELIM1="${BASH_REMATCH[1]}" + TAG_START_DELIM2="${BASH_REMATCH[2]}" + TAG_STOP_DELIM1="${BASH_REMATCH[3]}" + TAG_STOP_DELIM2="${BASH_REMATCH[4]}" + else + echo "Error: Invalid or missing tag delimiter values for ${2-tag delims}: '${1}'" >&2 + exit 1 + fi +} + +## +# parse_tag_stmt_delims +# $1 = delim +# $2 = src (for error msg) +# +function parse_tag_stmt_delim() { + if [[ "${1}" =~ $TAG_STMT_DELIM_REGEX ]]; then + TAG_STMT_DELIM="${BASH_REMATCH[1]}" + else + echo "Error: Invalid or missing tag stmt delimiter value for ${2-tag stmt delim}: '${1}'" >&2 + exit 1 + fi +} + +## +# parse_stmt_delim +# $1 = delim +# $2 = src (for error msg) +# +function parse_stmt_delim() { + if [[ "${1}" =~ $STMT_DELIM_REGEX ]]; then + STMT_DELIM="${BASH_REMATCH[1]}" + else + echo "Error: Invalid or missing stmt delimiter value for ${2:-stmt delim}: '${1}'" >&2 + exit 1 + fi +} + +## +# parse_stmt_block_delims +# $1 = delims +# $2 = src (for error msg) +# +function parse_stmt_block_delims() { + if [[ "${1}" =~ $STMT_BLOCK_DELIM_REGEX ]]; then + STMT_BLOCK_START_DELIM="${BASH_REMATCH[1]}" + STMT_BLOCK_STOP_DELIM="${BASH_REMATCH[2]}" + STMT_BLOCK_DELIM_UNDEFINED='' + else + echo "Error: Invalid or missing stmt-block delimiter values for ${2:-stmt-block delims}: '${1}'" >&2 + exit 1 + fi +} + +## +# parse_text_delim - Uses STMT delim regex +# $1 = delim +# $2 = src (for error msg) +# +function parse_text_delim() { + if [[ "${1}" =~ $STMT_BLOCK_TEXT_REGEX ]]; then + TEXT_DELIM="${1}" + TEXT_DELIM_UNDEFINED='' + else + echo "Error: Invalid or missing text delimiter value for ${2:-txt delim}: '${1}'" >&2 + exit 1 + fi +} + +## +# parse_directive_delim - Uses STMT delim regex +# $1 = delim +# $2 = src (for error msg) +# +function parse_directive_delim() { + if [[ "${1}" =~ $STMT_DELIM_REGEX ]]; then + DIRECTIVE_DELIM="${1}" + else + echo "Error: Invalid or missing directive delimiter value for ${2:-dir delim}: '${1}'" >&2 + exit 1 + fi +} + +## +# parse_comment_delim - Uses STMT delim regex +# $1 = delim +# $2 = src (for error msg) +# +function parse_comment_delim() { + if [[ "${1}" =~ $STMT_DELIM_REGEX ]]; then + COMMENT_DELIM="${1}" + COMMENT_DELIM_UNDEFINED='' + else + echo "Error: Invalid or missing comment delimiter value for ${2:-cmt delim}: '${1}'" >&2 + exit 1 + fi +} + +## +# reset_template_regexes +# +function reset_template_regexes() { + # Fixup STMT_BLOCK delims - Default to STMT_DELIM if not set + # + if [[ -n "${STMT_BLOCK_DELIM_UNDEFINED}" ]]; then + STMT_BLOCK_START_DELIM="${STMT_DELIM}" + STMT_BLOCK_STOP_DELIM="${STMT_DELIM}" + fi + + # Fixup TEXT delim - Default to STMT_DELIM followed by ' ' if not set + # + if [[ -n "${TEXT_DELIM_UNDEFINED}" ]]; then + TEXT_DELIM="${STMT_DELIM} " # Note trailing space (' ') + fi + + # Fixup COMMENT delim - Default to STMT_DELIM followed by '#' if not set + # + if [[ -n "${COMMENT_DELIM_UNDEFINED}" ]]; then + COMMENT_DELIM="${STMT_DELIM}#" + fi + + # + # Create regexes + # + + local d ds d1 d2 d3 d4 + + d="${DIRECTIVE_DELIM}" + escape_regex d + DIRECTIVE_REGEX="^([[:blank:]]*)${d}([a-zA-Z_-]+)(.*)\$" + + d="${COMMENT_DELIM}" + escape_regex d + COMMENT_REGEX="^([[:blank:]]*)${d}" + + d="${STMT_DELIM}" + escape_regex d + STATEMENT_REGEX="^([[:blank:]]*)${d}[[:blank:]]+(.+)\$" + + d="${STMT_BLOCK_START_DELIM}" + escape_regex d + STATEMENT_BLOCK_START_REGEX="^([[:blank:]]*)${d}[[:blank:]]*\$" + + d="${STMT_BLOCK_STOP_DELIM}" + escape_regex d + STATEMENT_BLOCK_STOP_REGEX="^([[:blank:]]*)${d}[[:blank:]]*\$" + + d="${TEXT_DELIM}" + escape_regex d + STATEMENT_BLOCK_TEXT_REGEX="^([[:blank:]]*)${d}([[:blank:]]*[^[:blank:]](.*))\$" + + TEXT_REGEX='^([[:blank:]]*)([^[:blank:]](.*))?$' + + d1="${TAG_START_DELIM1}" + escape_regex d1 + + d2="${TAG_START_DELIM2}" + escape_regex d2 + + d3="${TAG_STOP_DELIM1}" + escape_regex d3 + + d4="${TAG_STOP_DELIM2}" + escape_regex d4 + + ds="${TAG_STMT_DELIM}" + escape_regex ds + + TAG_TEXT_REGEX="^([^${d1}]+|${d1}$|${d1}[^${d1}${d2}]+)(.*)" + + TAG_STD_REGEX="^${d1}${d2}((([^${d3}])|(${d3}[^${d4}]))*)${d3}${d4}(.*)" + + TAG_QUOTE_REGEX="^${d1}${d2}\"((([^\"])|(\"[^${d3}])|(\"${d3}[^${d4}]))*)\"${d3}${d4}(.*)" + + TAG_STATEMENT_REGEX="^${d1}${d2}${ds}((([^${d3}])|(${d3}[^${d4}]))*)${d3}${d4}(.*)" + + # printf "# ---> delim regexes:" + # printf "# STATEMENT_BLOCK_START_REGEX: '%s'\n" "${STATEMENT_BLOCK_START_REGEX}" + # printf "# STATEMENT_BLOCK_STOP_REGEX: '%s'\n" "${STATEMENT_BLOCK_STOP_REGEX}" + # printf "# COMMENT_REGEX: '%s'\n" "${COMMENT_REGEX}" + # printf "# DIRECTIVE_REGEX: '%s'\n" "${DIRECTIVE_REGEX}" + # printf "# STATEMENT_REGEX: '%s'\n" "${STATEMENT_REGEX}" + # printf "# TAG_TEXT_REGEX: '%s'\n" "${TAG_TEXT_REGEX}" + # printf "# TAG_STD_REGEX: '%s'\n" "${TAG_STD_REGEX}" + # printf "# TAG_QUOTE_REGEX: '%s'\n" "${TAG_QUOTE_REGEX}" + # printf "# TAG_STATEMENT_REGEX: '%s'\n" "${TAG_STATEMENT_REGEX}" +} + +####################################################################### +# Misc Functions +####################################################################### + +## +# trim +# usage: trim varname +# NOTE: Expects value to NOT contain '\n' +# +function trim() { + read -r "$1" <<< "${!1}"$'\n' +} + +## +# escape_regex +# usage: escape_regex varname +# +function escape_regex() { + local result + # shellcheck disable=SC2001 # Too complex for ${variable//search/replace} + # shellcheck disable=SC2016 # Not using expansion, prefer single quotes + # shellcheck disable=SC2034 # ref is used + result=$(sed 's/[][\.|$(){}?+*^]/\\&/g' <<< "${!1}") + printf -v "${1}" "%s" "${result}" +} + +## +# normalize_directive +# usage: normalize_directive varname +# +function normalize_directive() { + local result + # shellcheck disable=SC2034 # ref is used + result=$(tr 'a-z_' 'A-Z-' <<< "${!1}") + printf -v "${1}" "%s" "${result}" +} + +####################################################################### +# STATES +####################################################################### + +STATES=() # empty => DEFAULT +STATE="DEFAULT" # [ DEFAULT, MAYBE_TXT_BLOCK, TXT_BLOCK, START_STMT_BLOCK, STMT_BLOCK ] + +## +# push_state +# +function push_state() { + STATES+=("${STATE}") + STATE="${1}" +} + +## +# pop_state +# +function pop_state() { + if [[ ${#STATES[@]} -gt 0 ]]; then + STATE="${STATES[${#STATES[@]} - 1]}" + unset "STATES[${#STATES[@]}-1]" + else + STATE="DEFAULT" + fi +} + +####################################################################### +# TEXT_INDENTS +####################################################################### + +TEXT_INDENTS=() # empty => "" +TEXT_INDENT="" + +## +# push_text_indent +# +function push_text_indent() { + TEXT_INDENTS+=("${TEXT_INDENT}") + TEXT_INDENT="${1}" +} + +## +# pop_text_indent +# +function pop_text_indent() { + if [[ ${#TEXT_INDENTS[@]} -gt 0 ]]; then + TEXT_INDENT="${TEXT_INDENTS[${#TEXT_INDENTS[@]} - 1]}" + unset "TEXT_INDENTS[${#TEXT_INDENTS[@]} - 1]" + else + TEXT_INDENT="" + fi +} + +####################################################################### +# STATEMENT_INDENTS +####################################################################### + +STATEMENT_INDENTS=() # empty => "" +STATEMENT_INDENT="" + +## +# push_statement_indent +# +function push_statement_indent() { + STATEMENT_INDENTS+=("${STATEMENT_INDENT}") + STATEMENT_INDENT="${1}" +} + +## +# pop_statement_indent +# +function pop_statement_indent() { + if [[ ${#STATEMENT_INDENTS[@]} -gt 0 ]]; then + STATEMENT_INDENT="${STATEMENT_INDENTS[${#STATEMENT_INDENTS[@]} - 1]}" + unset "STATEMENT_INDENTS[${#STATEMENT_INDENTS[@]} - 1]" + else + STATEMENT_INDENT="" + fi +} + +####################################################################### +# BLOCK_INDENTS +####################################################################### + +BLOCK_INDENTS=() # empty => "" +BLOCK_INDENT="" + +## +# push_block_indent +# +function push_block_indent() { + BLOCK_INDENTS+=("${BLOCK_INDENT}") + BLOCK_INDENT="${1}" +} + +## +# pop_block_indent +# +function pop_block_indent() { + if [[ ${#BLOCK_INDENTS[@]} -gt 0 ]]; then + BLOCK_INDENT="${BLOCK_INDENTS[${#BLOCK_INDENTS[@]} - 1]}" + unset "BLOCK_INDENTS[${#BLOCK_INDENTS[@]} - 1]" + else + BLOCK_INDENT="" + fi +} + +####################################################################### +# Print Functions +####################################################################### + +## +# print_statement +# $1 = leading indentation +# $2 = statement +# +function print_statement() { + local indent="${1}" + if [[ "${indent}" == "${BLOCK_INDENT}"* ]]; then + indent="${indent/#$BLOCK_INDENT/}" + fi + printf "%s\n" "${BASE_STMT_INDENT}${STATEMENT_INDENT}${indent}${2}" +} + +## +# print_text - generates a printf statement for template text +# $1 = leading text indentation +# $2 = text +# $3 = leading stmt indentation [OPTIONAL] +# +function print_text() { + local indent="${1}" + if [[ "${indent}" == "${BLOCK_INDENT}"* ]]; then + indent="${indent/#$BLOCK_INDENT/}" + fi + process_tags "${3-${BLOCK_INDENT}}" "${BASE_TEXT_INDENT}${TEXT_INDENT}${indent}${2}" +} + +####################################################################### +# Process Functions +####################################################################### + +## +# process_tags +# $1 = statement indentation +# $2 = full line of text to process +# +function process_tags() { + local stmt_indent line args arg quoted + stmt_indent="${1}" + line="${2}" + args="" + while [ -n "${line}" ]; do + # echo "# LINE @ START: $(declare -p line)" >&2 + if [[ "${line}" =~ $TAG_TEXT_REGEX ]]; then + # echo "# TEXT TAG MATCH: $(declare -p BASH_REMATCH)" >&2 + printf -v quoted "%q" "${BASH_REMATCH[1]}" + args="${args}${quoted}" + line="${BASH_REMATCH[2]}" + elif [[ "${line}" =~ $TAG_QUOTE_REGEX ]]; then + # echo "# QUOTE TAG MATCH: $(declare -p BASH_REMATCH)" >&2 + args="${args}\"${BASH_REMATCH[1]}\"" + line="${BASH_REMATCH[6]}" + elif [[ "${line}" =~ $TAG_STATEMENT_REGEX ]]; then + # echo "# STMT TAG MATCH: $(declare -p BASH_REMATCH)" >&2 + arg="${BASH_REMATCH[1]}" + trim arg + args="${args}\"\$(${arg})\"" + line="${BASH_REMATCH[5]}" + # Check standard regex last as it's a super-set of quote and stmt regex + # + elif [[ "${line}" =~ $TAG_STD_REGEX ]]; then + # echo "# STD TAG MATCH: $(declare -p BASH_REMATCH)" >&2 + arg="${BASH_REMATCH[1]}" + trim arg + args="${args}\"${arg}\"" + line="${BASH_REMATCH[5]}" + # Assume next character is TEXT - extract and process remainder + # + elif [[ "${line}" =~ (.)(.*) ]]; then + # echo "# DEFAULT: Assuming first char is TEXT: $(declare -p line)" + printf -v quoted "%q" "${BASH_REMATCH[1]}" + args="${args}${quoted}" + line="${BASH_REMATCH[2]}" + fi + # echo "# LINE @ END: $(declare -p line)" >&2 + done + local stmt + if [ -n "${args}" ]; then + printf -v stmt "printf \"%%s\\\\n\" %s" "${args}" + else + printf -v stmt "printf \"\\\\n\"" + fi + print_statement "${stmt_indent}" "${stmt}" +} + +DELIM_DIR_TAG_REGEX='[Tt][Aa][Gg]\s*=\s*"([^"]*)"' +DELIM_DIR_TAG_STMT_REGEX='[Tt][Aa][Gg][_-]?[Ss][Tt][Mm][Tt]\s*=\s*"([^"]*)"' +DELIM_DIR_STMT_REGEX='[Ss][Tt][Mm][Tt]\s*=\s*"([^"]*)"' +DELIM_DIR_STMT_BLOCK_REGEX='[Ss][Tt][Mm][Tt][_-]?[Bb][Ll][Oo][Cc][Kk]\s*=\s*"([^"]*)"' +DELIM_DIR_TXT_REGEX='[Tt][Xx][Tt]\s*=\s*"([^"]*)"' +DELIM_DIR_TEXT_REGEX='[Tt][Ee][Xx][Tt]\s*=\s*"([^"]*)"' +DELIM_DIR_DIR_REGEX='[Dd][Ii][Rr]\s*=\s*"([^"]*)"' +DELIM_DIR_DIRECTIVE_REGEX='[Dd][Ii][Rr][Ee][Cc][Tt][Ii][Vv][Ee]\s*=\s*"([^"]*)"' +DELIM_DIR_CMT_REGEX='[Cc][Mm][Tt]\s*=\s*"([^"]*)"' +DELIM_DIR_COMMENT_REGEX='[Cc][Oo][Mm][Mm][Ee][Nn][Tt]\s*=\s*"([^"]*)"' + +## +# process_directive +# $1 = leading_indent +# $2 = directive +# $3 = directive arg(s) +# +function process_directive() { + local directive + directive="${2}" + normalize_directive directive + case "${directive}" in + INCLUDE) + local indent="${1}" + if [[ "${indent}" == "${BLOCK_INDENT}"* ]]; then + indent="${indent/#$BLOCK_INDENT/}" + fi + local args args_arr + args="${3}" + trim args + declare -a args_arr="(${args})" + # shellcheck disable=SC2128 # We choose BASH_SOURCE vs BASH_SOURCE[0] for compatability + "${BASH_SOURCE}" \ + --text-indent "${BASE_TEXT_INDENT}${TEXT_INDENT}${indent}" \ + --stmt-indent "${BASE_STMT_INDENT}${STATEMENT_INDENT}${indent}" \ + --block-indent "${BASE_BLOCK_INDENT}${BLOCK_INDENT}" \ + --tag-delims "${TAG_START_DELIM1}${TAG_START_DELIM2} ${TAG_STOP_DELIM1}${TAG_STOP_DELIM2}" \ + --tag-stmt-delim "${TAG_STMT_DELIM}" \ + --stmt-delim "${STMT_DELIM}" \ + --stmt-block-delims "${STMT_BLOCK_START_DELIM} ${STMT_BLOCK_STOP_DELIM}" \ + --txt-delim "${TEXT_DELIM}" \ + --dir-delim "${DIRECTIVE_DELIM}" \ + --cmt-delim "${COMMENT_DELIM}" \ + "${args_arr[@]}" + ;; + DELIMS) + # TAG + # + if [[ "${3}" =~ $DELIM_DIR_TAG_REGEX ]]; then + parse_tag_delims "${BASH_REMATCH[1]}" 'DELIMS TAG directive' + fi + # TAG-STMT + # + if [[ "${3}" =~ $DELIM_DIR_TAG_STMT_REGEX ]]; then + parse_tag_stmt_delim "${BASH_REMATCH[1]}" 'DELIMS TAG-STMT directive' + fi + # STMT + # + if [[ "${3}" =~ $DELIM_DIR_STMT_REGEX ]]; then + parse_stmt_delim "${BASH_REMATCH[1]}" 'DELIMS STMT directive' + fi + # STMT-BLOCK + # + if [[ "${3}" =~ $DELIM_DIR_STMT_BLOCK_REGEX ]]; then + parse_stmt_block_delims "${BASH_REMATCH[1]}" '"DELIMS STMT-BLOCK directive' + fi + # TEXT + # + if [[ "${3}" =~ $DELIM_DIR_TXT_REGEX || "${3}" =~ $DELIM_DIR_TEXT_REGEX ]]; then + parse_text_delim "${BASH_REMATCH[1]}" 'DELIMS TEXT directive' + fi + # DIRECTIVE + # + if [[ "${3}" =~ $DELIM_DIR_DIR_REGEX || "${3}" =~ $DELIM_DIR_DIRECTIVE_REGEX ]]; then + parse_directive_delim "${BASH_REMATCH[1]}" 'DELIMS DIR directive' + fi + # COMMENT + # + if [[ "${3}" =~ $DELIM_DIR_CMT_REGEX || "${3}" =~ $DELIM_DIR_COMMENT_REGEX ]]; then + parse_comment_delim "${BASH_REMATCH[1]}" 'DELIMS CMT directive' + fi + # Apply changes + # + reset_template_regexes + ;; + RESET-DELIMS) + reset_delims + reset_template_regexes + ;; + *) # unsupported directive + echo "Error: Unknown directive: '${directive}' - Skipping" >&2 + ;; + esac +} + +function debug_array() { + printf "[" + local need_comma="" + while [[ ${#@} -gt 0 ]]; do + if [ -n "${need_comma:-}" ]; then + printf ", " + fi + if [ -n "${1:-}" ]; then + printf "'%q'" "${1}" + else + printf "''" + fi + need_comma=1 + shift + done + printf "]" +} +## +# debug_state logs the state of the global variables. +# To use this set the BASH_TPL_DEBUG variable to a non-empty value +# when invoking the script. +# TODO Track source input file+line numbers +# $1 = template line | EOF +# +function debug_state() { + printf "#<< ---------------\n" + printf "#LINE TEXT : '%s'\n" "${1-}" + printf "#STATE : %s\n" "${STATE:-}" + printf "#STATES : %s\n" "$(debug_array "${STATES[@]}")" + printf "#TEXT_INDENT : %q\n" "${TEXT_INDENT:-}" + printf "#TEXT_INDENTS : %s\n" "$(debug_array "${TEXT_INDENTS[@]}")" + printf "#STATEMENT_INDENT : %q\n" "${STATEMENT_INDENT:-}" + printf "#STATEMENT_INDENTS: %s\n" "$(debug_array "${STATEMENT_INDENTS[@]}")" + printf "#BLOCK_INDENT : %q\n" "${BLOCK_INDENT:-}" + printf "#BLOCK_INDENTS : %s\n" "$(debug_array "${BLOCK_INDENTS[@]}")" + printf "#TEXT_BLOCK_LINES_INDENT : %q\n" "${TEXT_BLOCK_LINES_INDENT}" + printf "#TEXT_BLOCK_LINES_INDENT_SET : %q\n" "${TEXT_BLOCK_LINES_INDENT_SET}" + printf "#STATEMENT_BLOCK_LINES_INDENT : %q\n" "${STATEMENT_BLOCK_LINES_INDENT}" + printf "#STATEMENT_BLOCK_LINES_INDENT_STATE : %q\n" "${STATEMENT_BLOCK_LINES_INDENT_STATE}" + printf "#>> ---------------\n" +} + +function process_line() { + [ -n "${BASH_TPL_DEBUG:-}" ] && debug_state ${@+"$@"} + state_"${STATE}" ${@+"$@"} +} + +function process_stdin() { + local line + while IFS="" read -r line || [ -n "${line}" ]; do + process_line "${line}" + done + # EOF - Notify states + # Call with no args + # + while [[ "${STATE}" != "DEFAULT" ]]; do + process_line + done + process_line # DEFAULT +} + +####################################################################### +# State Handler Functions +####################################################################### + +## +# state_DEFAULT +# Not inside any blocks +# Assumes *_INDENT and STATE arrays are empty +# +function state_DEFAULT() { + [[ ${#@} -gt 0 ]] || return # Exit early on EOF + # Line is a statement + # + if [[ "${1}" =~ $STATEMENT_REGEX ]]; then + print_statement "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" + push_statement_indent "${BASH_REMATCH[1]}" + push_state "MAYBE_TXT_BLOCK" + # Line is a statement block start + # + elif [[ "${1}" =~ $STATEMENT_BLOCK_START_REGEX ]]; then + push_statement_indent "${BASH_REMATCH[1]}" + push_state "START_STMT_BLOCK" + # Line is a directive + # + elif [[ "${1}" =~ $DIRECTIVE_REGEX ]]; then + process_directive "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}" + # Line is a comment + # + elif [[ "${1}" =~ $COMMENT_REGEX ]]; then + : # Comments do not generate output + # Line is text + # NOTE : Check LAST because regex always matches + # + elif [[ "${1}" =~ $TEXT_REGEX ]]; then + print_text "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" + fi +} + +TEXT_BLOCK_LINES=() +TEXT_BLOCK_LINES_INDENT="" +TEXT_BLOCK_LINES_INDENT_SET="" + +## +# state_MAYBE_TXT_BLOCK +# Previous line was a statement +# We might be starting a text block +# NOTE: Assumes +# push_state "MAYBE_TXT_BLOCK" +# push_statement_indent +# +function state_MAYBE_TXT_BLOCK() { + # If there's a line to process (i.e. not EOF) + # + if [[ ${#@} -gt 0 ]]; then + # Current line is empty + # Considered text block content, + # but doesn't contribute to indentation tracking + # + if [[ "${1}" == "" ]]; then + TEXT_BLOCK_LINES+=("${1}") # Save line + return + # Current line is a block-end statement, + # i.e. it's a statement at the same indentation as the start statement + # + elif [[ "${1}" =~ $STATEMENT_REGEX && "${BASH_REMATCH[1]}" == "${STATEMENT_INDENT}" ]]; then + # We've saved a FULL text block ! + # Use computed indentation + # + push_text_indent "${TEXT_INDENT}${STATEMENT_INDENT/#$BLOCK_INDENT/}" # Additive + push_statement_indent "${TEXT_BLOCK_LINES_INDENT}" + push_block_indent "${TEXT_BLOCK_LINES_INDENT}" + local state_marker=${#STATES[@]} # Save for cleanup + push_state "TXT_BLOCK" + # Text blocks can be nested, so save lines and cleanup *before* processing + # + local lines=("${TEXT_BLOCK_LINES[@]}") + TEXT_BLOCK_LINES=() + TEXT_BLOCK_LINES_INDENT="" + TEXT_BLOCK_LINES_INDENT_SET="" + # Process saved lines now in new state + # + local line + for line in "${lines[@]}"; do + process_line "${line}" + done + # Clean up our TXT_BLOCK state and any other danglers + # + while [[ ${#STATES[@]} -gt $state_marker ]]; do + process_line # EOF + done + # Clean up our MAYBE_TXT_BLOCK state + # + pop_statement_indent + pop_state + # Process close block in parent context + # + process_line "${1}" + return + # Capture line indentation for tracking + # TEXT_REGEX is perfect for this, so just re-use it + # NOTE: Regex always matches + # + elif [[ "${1}" =~ $TEXT_REGEX ]]; then + TEXT_BLOCK_LINES+=("${1}") # Save line + # If current line is indented + # + if [[ "${BASH_REMATCH[1]}" != "${STATEMENT_INDENT}" && "${BASH_REMATCH[1]}" == "${STATEMENT_INDENT}"* ]]; then + # If first time through + # + if [[ "${TEXT_BLOCK_LINES_INDENT_SET}" == "" ]]; then + # Track current indentation + # + TEXT_BLOCK_LINES_INDENT="${BASH_REMATCH[1]}" + TEXT_BLOCK_LINES_INDENT_SET="1" + return + # If current line is indented SAME OR LESS than tracked + # + elif [[ "${TEXT_BLOCK_LINES_INDENT}" == "${BASH_REMATCH[1]}"* ]]; then + # Update tracked indentation (may set to same value) + # + TEXT_BLOCK_LINES_INDENT="${BASH_REMATCH[1]}" + return + # If current line is indented MORE than tracked + # + elif [[ "${BASH_REMATCH[1]}" == "${TEXT_BLOCK_LINES_INDENT}"* ]]; then + # No change + # + return + # Neither line is a subset of the other + # + else + : # Here for completeness + fi + # Current line is NOT indented + # + else + : # Here for completeness + fi + fi + # EOF + # + else + : # Fall through + fi + # If we haven't returned by now, then we're not in a text block + # Discard saved state and process saved lines + # + pop_statement_indent + pop_state + # Text blocks can be nested, so save lines and cleanup *before* processing + # + local lines=("${TEXT_BLOCK_LINES[@]}") + # Clean up + # + TEXT_BLOCK_LINES=() + TEXT_BLOCK_LINES_INDENT="" + TEXT_BLOCK_LINES_INDENT_SET="" + # Process saved lines now in parent context + # TODO "push" these back onto primary line-processing stream? + # + local line + for line in "${lines[@]}"; do + process_line "${line}" + done +} + +## +# state_TXT_BLOCK +# NOTE: Assumes +# Called within MAYBE_TXT_BLOCK with a complete block to process +# Will NOT be called with TXT Block Close +# Every line has a minimum indentation of BLOCK_INDENT +# push_state TXT_BLOCK +# push_text_indent +# push_statement_indent +# push_block_indent +# +function state_TXT_BLOCK() { + # EOF + # + if [[ ${#@} -eq 0 ]]; then + # End of text block + # Discard saved state + # + pop_text_indent + pop_statement_indent + pop_block_indent + pop_state + # Current line is empty + # + elif [[ "${1}" == "" ]]; then + process_tags "${BLOCK_INDENT}" "" + # Current line is a statement + # + elif [[ "${1}" =~ $STATEMENT_REGEX ]]; then + print_statement "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" + push_statement_indent "${BASH_REMATCH[1]}" + push_state "MAYBE_TXT_BLOCK" + # Current line is a statement Start Block + # + elif [[ "${1}" =~ $STATEMENT_BLOCK_START_REGEX ]]; then + push_statement_indent "${BASH_REMATCH[1]}" + push_state "START_STMT_BLOCK" + # Current line is a directive + # + elif [[ "${1}" =~ $DIRECTIVE_REGEX ]]; then + process_directive "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}" + # Line is a comment + # + elif [[ "${1}" =~ $COMMENT_REGEX ]]; then + : # Comments do not generate output + # Line is text + # NOTE: Regex always matches + # + elif [[ "${1}" =~ $TEXT_REGEX ]]; then + print_text "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" + fi +} + +STATEMENT_BLOCK_LINES=() +STATEMENT_BLOCK_LINES_INDENT="" +STATEMENT_BLOCK_LINES_INDENT_STATE="" + +## +# state_START_STMT_BLOCK +# NOTE: Assumes +# push_state "START_STMT_BLOCK" +# push_statement_indent +# +function state_START_STMT_BLOCK() { + # If there's a line to process (i.e. not EOF) + # + if [[ ${#@} -gt 0 ]]; then + # Current line is empty + # Considered statement block content, + # but doesn't contribute to indentation tracking + # + if [[ "${1}" == "" ]]; then + STATEMENT_BLOCK_LINES+=("${1}") # Save line + return + # Current line is a statement block end + # + elif [[ "${1}" =~ $STATEMENT_BLOCK_STOP_REGEX ]]; then + # If indentation does not match block-open, then error + # TODO Track line numbers for better reporting + # + if [[ "${BASH_REMATCH[1]}" != "${STATEMENT_INDENT}" ]]; then + echo "Error: stmt-block close indentation does not match open" >&2 + exit 1 + fi + # We've saved a FULL statement block ! + # Is it fully indented? + # + if [[ "${STATEMENT_BLOCK_LINES_INDENT_STATE}" == "1" ]]; then + # Use computed indentation + # + push_text_indent "${TEXT_INDENT}${STATEMENT_INDENT/#$BLOCK_INDENT/}" # Additive + push_block_indent "${STATEMENT_BLOCK_LINES_INDENT}" + else + # If not consistently indented, default to no indent + # TODO Print warning? + # + push_text_indent "" + pop_statement_indent + push_statement_indent "" + push_block_indent "" + fi + # Process the saved lines + # NOTE: Statement block end (+ cleanup) will be processed by STMT_BLOCK handler + # + pop_state + push_state "STMT_BLOCK" + # Process saved lines now in new state + # Statement blocks do not nest, so we use global and cleanup *after* + # + local line + for line in "${STATEMENT_BLOCK_LINES[@]}"; do + process_line "${line}" + done + # Clean up our STMT_BLOCK state + # + process_line # EOF + # Clean up + # + STATEMENT_BLOCK_LINES=() + STATEMENT_BLOCK_LINES_INDENT="" + STATEMENT_BLOCK_LINES_INDENT_STATE="" + # Capture line indentation for tracking + # TEXT_REGEX is perfect for this, so just re-use it + # NOTE: Regex always matches + # + elif [[ "${1}" =~ $TEXT_REGEX ]]; then + STATEMENT_BLOCK_LINES+=("${1}") # Save line + # If current line is indented (or even) + # + if [[ "${BASH_REMATCH[1]}" == "${STATEMENT_INDENT}"* ]]; then + # If first time through + # + if [[ "${STATEMENT_BLOCK_LINES_INDENT_STATE}" == "" ]]; then + # Track current indentation + # + STATEMENT_BLOCK_LINES_INDENT="${BASH_REMATCH[1]}" + STATEMENT_BLOCK_LINES_INDENT_STATE="1" + # If still working with fully indented block + # + elif [[ "${STATEMENT_BLOCK_LINES_INDENT_STATE}" == "1" ]]; then + # If current line is indented SAME OR LESS than tracked + # + if [[ "${STATEMENT_BLOCK_LINES_INDENT}" == "${BASH_REMATCH[1]}"* ]]; then + # Update tracked indentation (may set to same value) + # + STATEMENT_BLOCK_LINES_INDENT="${BASH_REMATCH[1]}" + # If current line is indented MORE than tracked + # + elif [[ "${BASH_REMATCH[1]}" == "${STATEMENT_BLOCK_LINES_INDENT}"* ]]; then + # No change + # + : + # Neither line is a subset of the other + # + else + STATEMENT_BLOCK_LINES_INDENT_STATE="2" + fi + fi + # Current line is NOT indented (or even) + # + else + STATEMENT_BLOCK_LINES_INDENT_STATE="3" + fi + fi + # EOF + # + else + # EOF before close block reached is an error + # TODO Track line numbers for better reporting + # + echo "Error: Missing stmt-block close ('${STMT_BLOCK_STOP_DELIM}')" >&2 + exit 1 + fi +} + +## +# state_STMT_BLOCK +# NOTE: Assumes +# Called within START_STMT_BLOCK with a complete block to process +# Will NOT be called with STMT Block Close +# push_state STMT_BLOCK +# push_text_indent +# push_statement_indent +# push_block_indent +# +function state_STMT_BLOCK() { + # EOF + # + if [[ ${#@} -eq 0 ]]; then + # End of statement block + # Discard saved state + # + pop_text_indent + pop_statement_indent + pop_block_indent + pop_state + # Current line is empty + # + elif [[ "${1}" == "" ]]; then + # TODO Do we need a flag to print BASE_STMT_INDENT when included? + # + printf "\n" + # Line is text + # + elif [[ "${1}" =~ $STATEMENT_BLOCK_TEXT_REGEX ]]; then + print_text "${BLOCK_INDENT}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[1]}" + # Line is assumed to be a statement + # TEXT_REGEX is perfect for this, so just re-use it + # NOTE: Regex always matches + # + elif [[ "${1}" =~ $TEXT_REGEX ]]; then + print_statement "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" + fi +} + +####################################################################### +# Main +####################################################################### + +function version() { + printf "%s\n" "${VERSION}" +} + +## +# parse_env_delims - Set Delims from Environment Vars +# +function parse_env_delims() { + if [ -n "${BASH_TPL_TAG_DELIMS}" ]; then + parse_tag_delims "${BASH_TPL_TAG_DELIMS}" "BASH_TPL_TAG_DELIMS" + fi + + if [ -n "${BASH_TPL_TAG_STMT_DELIM}" ]; then + parse_tag_stmt_delim "${BASH_TPL_TAG_STMT_DELIM}" "BASH_TPL_TAG_STMT_DELIM" + fi + + if [ -n "${BASH_TPL_STMT_DELIM}" ]; then + parse_stmt_delim "${BASH_TPL_STMT_DELIM}" "BASH_TPL_STMT_DELIM" + fi + + if [ -n "${BASH_TPL_STMT_BLOCK_DELIMS}" ]; then + parse_stmt_block_delims "${BASH_TPL_STMT_BLOCK_DELIMS}" "BASH_TPL_STMT_BLOCK_DELIMS" + fi + + if [ -n "${BASH_TPL_TEXT_DELIM}" ]; then + parse_text_delim "${BASH_TPL_TEXT_DELIM}" "BASH_TPL_TEXT_DELIM" + fi + + if [ -n "${BASH_TPL_DIR_DELIM}" ]; then + parse_directive_delim "${BASH_TPL_DIR_DELIM}" "BASH_TPL_DIR_DELIM" + fi + + if [ -n "${BASH_TPL_CMT_DELIM}" ]; then + parse_comment_delim "${BASH_TPL_CMT_DELIM}" "BASH_TPL_CMT_DELIM" + fi +} + +## +# parse_args +# $@ args to parse +# +# Stores positional args in global array __ARGS[@] +# +function parse_args() { + __ARGS=() # Global + while (($#)); do + case "$1" in + -h | --help) + usage + exit 0 + ;; + --version) + version + exit 0 + ;; + -o | --output-file) + if [ -n "${2}" ]; then + OUTPUT_FILE="${2}" + else + echo "Error: Invalid or missing value for --output-file: '${2}'" >&2 + exit 1 + fi + shift 2 + ;; + --reset-delims) + # Reset delims immediately - Any delim flags after this will be honored + # + reset_delims + shift + ;; + --tag-delims) + parse_tag_delims "$2" "$1" + shift 2 + ;; + --tag-stmt-delim) + parse_tag_stmt_delim "$2" "$1" + shift 2 + ;; + --stmt-delim) + parse_stmt_delim "$2" "$1" + shift 2 + ;; + --stmt-block-delims) + parse_stmt_block_delims "$2" "$1" + shift 2 + ;; + --txt-delim | --text-delim) + parse_text_delim "$2" "$1" + shift 2 + ;; + --dir-delim | --directive-delim) + parse_directive_delim "$2" "$1" + shift 2 + ;; + --cmt-delim | --comment-delim) + parse_comment_delim "$2" "$1" + shift 2 + ;; + --text-indent) + BASE_TEXT_INDENT="${2}" + shift 2 + ;; + --stmt-indent) + BASE_STMT_INDENT="${2}" + shift 2 + ;; + --block-indent) + BASE_BLOCK_INDENT="${2}" + shift 2 + ;; + -) + __ARGS+=("$1") + shift + ;; + --) + shift + while (($#)); do + __ARGS+=("$1") + shift + done + ;; + --* | -*) # unsupported flags + echo "Error: unknown flag: '$1'; use -h for help" >&2 + exit 1 + ;; + *) # preserve positional arguments + __ARGS+=("$1") + shift + ;; + esac + done +} + +function main() { + + OUTPUT_FILE="" + + BASE_TEXT_INDENT="" + BASE_STMT_INDENT="" + BASE_BLOCK_INDENT="" + + reset_delims + parse_env_delims + + parse_args "$@" + set -- "${__ARGS[@]}" + unset __ARGS + + # No file argument + # + if [[ -z "${1}" ]]; then + # Nothing waiting on stdin + # + if [[ -t 0 ]]; then + usage + exit 1 + fi + else + # File argument is explicitly stdin + # + if [[ "${1}" == '-' ]]; then + shift + else + # File argument points to non-existing/readable file + # + if [[ ! -r "${1}" ]]; then + echo "File not found: '${1}'" >&2 + exit 1 + fi + # File argument is good, re-route it to stdin + # + exec < "${1}" + shift + fi + fi + + reset_template_regexes + + if [[ -n "${OUTPUT_FILE}" ]]; then + exec > "${OUTPUT_FILE}" + fi + + process_stdin +} + +# Only process main logic if not being sourced (ie tested) +# +(return 0 2> /dev/null) || main "$@" \ No newline at end of file diff --git a/lib/cmds/01-image-name b/lib/cmds/01-image-name deleted file mode 100755 index 8352383..0000000 --- a/lib/cmds/01-image-name +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash - -image_name () { - -local tag; local efile - -# generate a full image name with tag -# $1 name, $2 user(or repo), $3 repo - -# [[ $# -lt 1 ]] && echo "image base name required" && exit - -declare OPTION; declare OPTARG; declare OPTIND -while getopts 'e:ag:r:u:' OPTION; do -# echo processing: option:$OPTION argument:$OPTARG index:$OPTIND remaining:${@:$OPTIND} -case "$OPTION" in -e) - efile=$OPTARG - ;; -r) - REPO=$OPTARG - ;; -u) - RUSER=$OPTARG -;; -g) - TAG=$OPTARG - ;; -a) # add -arm64 to image - arm=arm64 - ;; -*) echo unknown run option -$OPTARG - echo "USAGE: tag " - echo "available options: -a add -arm64 to tag, -d delete tag " -;; -esac -done - -shift $((OPTIND - 1)) -echo $efile -if [[ $efile ]]; then - [[ ! -f $efile ]] && efile=$SDIR/$efile - if [[ -f $efile ]]; then - source $efile - [[ ! $? -eq 0 ]] && echo source of $efile failed, exiting && return 2 - else - echo no environment file at $efile, exiting - return 2 - fi - echo "----------" - echo loaded environment filen $efile - cat $efile - echo "----------" -fi - -tag=$( echo $1 | cut -s -d ":" -f2) -tag=${tag:-$TAG} -name=${1%:*} -user=${2:-$RUSER} -repo=${3:-$REPO} - -tag=$([[ $repo ]] && echo ${repo}/)$([[ $user ]] && echo ${user}/)$name$([[ $arm ]] && echo -arm64):${TAG:-latest} - -echo $tag - -} - -# if script was executed then call the function -(return 0 2>/dev/null) || image_name $@ \ No newline at end of file diff --git a/lib/cmds/01-image-name.sh b/lib/cmds/01-image-name.sh new file mode 100755 index 0000000..d7e90b1 --- /dev/null +++ b/lib/cmds/01-image-name.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +image_name () { + +local tag; local efile; local suffix + +# generate a full image name with tag +# $1 name, $2 user(or repo), $3 repo + +# [[ $# -lt 1 ]] && echo "image base name required" && exit + +declare OPTION; declare OPTARG; declare OPTIND +while getopts 'e:s:g:r:u:' OPTION; do +# echo processing: option:$OPTION argument:$OPTARG index:$OPTIND remaining:${@:$OPTIND} +case "$OPTION" in +e) + efile=$OPTARG + ;; +u) + RUSER=$OPTARG +;; +g) + TAG=$OPTARG + ;; +s) # add -arm64 to image + suffix=$OPTARG + ;; +*) echo unknown image-name option -$OPTARG + echo "USAGE: image_name " + echo "available options: -s : add - , -g: tag, -u: repo user, -e: env file" +;; +esac +done + +shift $((OPTIND - 1)) + + +source_env_file $efile + +tag=$( echo $1 | cut -s -d ":" -f2) +TAG=${tag:-$TAG} +name=${1%:*} +shift + +get_distro +echo $(make_image_name $name $@)$([[ $suffix ]] && echo -$suffix):${TAG:-latest} +} + +# if script was executed then call the function +(return 0 2>/dev/null) || image_name $@ \ No newline at end of file diff --git a/lib/cmds/help.md b/lib/cmds/help.md new file mode 100644 index 0000000..2e56677 --- /dev/null +++ b/lib/cmds/help.md @@ -0,0 +1,89 @@ + +# UCOMMANDIT DOCKER BUILD SCRIPT + + Image Build Script: Creates one or more images using a target per the docker-bake.hcl file + +## USAGE + + `udbuild