diff --git a/.build-scripts/tag-release.sh b/.build-scripts/tag-release.sh new file mode 100755 index 0000000..379d05f --- /dev/null +++ b/.build-scripts/tag-release.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# shellcheck shell=bash +set -euo pipefail +[[ "${XDEBUG:-0}" =~ ^[1yYtT] ]] && set -x + +IMAGE_TAG_SEP="@" + +IMAGE_NAME="${1:-}" +[[ -n "${IMAGE_NAME:-}" ]] || { echo "IMAGE_NAME is empty" >&2; exit 1; } +shift + +IMAGE_TAG="${1:-}" +if [[ -z "${IMAGE_TAG:-}" ]]; then + IMAGE_TAG="latest" + echo "IMAGE_TAG is empty, using default: ${IMAGE_TAG}" >&2 +else + shift +fi + +FULL_CONTAINER_NAME="${IMAGE_NAME}:${IMAGE_TAG}" + +GIT_TAG="${IMAGE_NAME}${IMAGE_TAG_SEP}${IMAGE_TAG}" + +[[ -n "${GIT_TAG:-}" ]] || { echo "GIT_TAG is empty" >&2; exit 1; } + +comment="${1:-}" +if [[ -z "${comment:-}" ]]; then + comment="${FULL_CONTAINER_NAME}" + echo "comment is empty, using default: ${comment}" >&2 +else + shift +fi + +git_tag_args=() +git_commit_args=() +if [[ -n "${comment:-}" ]]; then + git_tag_args+=(-m "${comment}") + git_commit_args+=(-m "${comment}") +fi + +for arg in "$@"; do + git_tag_args+=(-m "${arg}") + git_commit_args+=(-m "${arg}") +done + +echo "git_tag_args: ${git_commit_args[*]}" >&2 +echo "git_commit_args: ${git_commit_args[*]}" >&2 + +if git tag -l "${GIT_TAG:-}" | grep -q "^${GIT_TAG:-}$"; then + echo "git tag ${GIT_TAG} already exists" >&2 + if [[ -t 1 ]]; then + choice=y + read -rp "Do you want to continue? [Y/n] " choice + [[ "${choice:-y}" =~ ^[Yy]$ ]] || exit 1 + fi +fi + +if [[ -t 1 ]]; then + echo "git tag -fa ${GIT_TAG} ${git_tag_args[*]}" + choice=y + read -rp "Do you want to continue? [Y/n] " choice + [[ "${choice:-y}" =~ ^[Yy]$ ]] || exit 1 +fi + +git commit --allow-empty "${git_commit_args[@]}" +git push +git tag -fa "${GIT_TAG}" "${git_tag_args[@]}" + +echo 'Tag contents:' +git tag -l --format='%(contents)' "$(git describe --tags --abbrev=0 || true)" + +if [[ -t 1 ]]; then + choice=y + + if git remote get-url origin 2>/dev/null 1>&2; then + read -rp "Do you want to push to origin? [Y/n] " choice + [[ "${choice:-y}" =~ ^[Yy]$ ]] && git push -f origin "${GIT_TAG}" + fi + + if git remote get-url upstream 2>/dev/null 1>&2; then + read -rp "Do you want to push to upstream? [Y/n] " choice + [[ "${choice:-y}" =~ ^[Yy]$ ]] && git push -f upstream "${GIT_TAG}" + fi +fi diff --git a/.build-scripts/write-apptainer-labels.sh b/.build-scripts/write-apptainer-labels.sh new file mode 100755 index 0000000..18c6a6f --- /dev/null +++ b/.build-scripts/write-apptainer-labels.sh @@ -0,0 +1,84 @@ +#!/bin/sh +# shellcheck shell=sh + +# Write the build labels to the build labels path for both the org.label-schema and org.opencontainers.image formats + +[ -n "${XDEBUG:-}" ] && set -x + +write_to_build_labels() { + while [ $# -gt 1 ]; do + eval "[ -n \"\${$#}\" ] && printf '%s %s\n' \"$1\" \"\${$#}\"" >>"${BUILD_LABELS_PATH:-/dev/stdout}" + shift + done + return 0 +} + +write_apptainer_labels() { + #[ -n "${APPTAINER_ROOTFS:-}" ] || return 1 # Exit if not in an apptainer build + #BUILD_LABELS_PATH="${BUILD_LABELS_PATH:-${APPTAINER_ROOTFS:+${APPTAINER_ROOTFS}/.build.labels}}" # Set the default build labels path + if [ -n "${APPTAINER_ROOTFS:-}" ]; then + BUILD_LABELS_PATH="${BUILD_LABELS_PATH:-${APPTAINER_ROOTFS}/.build.labels}" + else + BUILD_LABELS_PATH="${BUILD_LABELS_PATH:-/dev/stdout}" + fi + + # Try to fill in the build labels via git if not already set and git is available + if git tag >/dev/null 2>&1; then + IMAGE_VCS_URL="${IMAGE_VCS_URL:-$(git remote get-url origin || true)}" # Set the default VCS URL to the origin remote + [ -z "${IMAGE_URL:-}" ] && [ -n "${IMAGE_VCS_URL:-}" ] && IMAGE_URL="${IMAGE_VCS_URL%%.git}" # Set the default URL to the VCS URL without the .git extension + IMAGE_VCS_REF="${IMAGE_VCS_REF:-$(git rev-parse --short HEAD || true)}" # Set the default VCS ref to the short hash of HEAD + + IMAGE_GIT_TAG="${GITHUB_REF_NAME:-"$(git tag --points-at HEAD --list '*@*' --sort=-"creatordate:iso" || true)"}" # Set the default git tag to the most recent tag matching the format *@* sorted by date + + if [ -n "${IMAGE_GIT_TAG:-}" ]; then + if [ -z "${IMAGE_TAG:-}" ]; then + IMAGE_TAG="$(echo "${IMAGE_GIT_TAG:-}" | sed -nE 's/.*[@]//; s/^v//; 1p')" + [ -z "${IMAGE_TAG:-}" ] && IMAGE_TAG='latest' + fi + + if [ -n "${IMAGE_TITLE:-}" ]; then + IMAGE_TITLE="$(echo "${IMAGE_GIT_TAG}" | sed -nE 's/[@].*$//; 1p')" + fi + fi + fi + IMAGE_TAG="${IMAGE_TAG:-latest}" # Set the default tag to latest if no tag was found + IMAGE_TITLE="${IMAGE_TITLE:-"$(basename "${PWD}")"}" # Set the default title to the current directory name + IMAGE_VERSION="${IMAGE_VERSION:-${IMAGE_TAG:-}}" # Set the default version to the tag if set, otherwise the tag if set, otherwise empty + + # If no image vendor is set, try to set it to the GitHub organization: + if [ -z "${IMAGE_VENDOR:=${IMAGE_VENDOR:-}}" ]; then + # If the GitHub organization is not set, try to set it to the GitHub organization of the upstream remote: + [ -z "${GH_ORG:-}" ] && GH_ORG="$(git remote get-url upstream | sed -n 's/.*github.com[:/]\([^/]*\)\/.*/\1/p' || true)" + # If the GitHub organization is not set, try to set it to the GitHub organization of the origin remote: + [ -z "${GH_ORG:-}" ] && GH_ORG="$(git remote get-url origin | sed -n 's/.*github.com[:/]\([^/]*\)\/.*/\1/p' || true)" + + # Assign the image vendor to the GitHub organization or username if it is set, otherwise leave it empty: + IMAGE_VENDOR="${GH_ORG:-}" + + # If the GitHub organization is set to uw-psych, set the image vendor to the University of Washington Department of Psychology: + [ "${IMAGE_VENDOR:-}" = 'uw-psych' ] && IMAGE_VENDOR='University of Washington Department of Psychology' + fi + + # Try to set image author from GITHUB_REPOSITORY_OWNER if not set: + IMAGE_AUTHOR="${IMAGE_AUTHOR:-${GITHUB_REPOSITORY_OWNER:-}}" + + # If no image author is set, try to set it to the git author via git config: + if [ -z "${IMAGE_AUTHOR:-}" ] && command -v git >/dev/null 2>&1; then + [ -n "${IMAGE_AUTHOR_EMAIL:-}" ] || IMAGE_AUTHOR_EMAIL="$(git config --get user.email || git config --get github.email || true)" + [ -n "${IMAGE_AUTHOR_NAME:-}" ] || IMAGE_AUTHOR_NAME="$(git config --get user.name || git config --get github.user || true)" + IMAGE_AUTHOR="${IMAGE_AUTHOR_NAME:+${IMAGE_AUTHOR_NAME} }<${IMAGE_AUTHOR_EMAIL:-}>" + fi + + # Write the build labels to the build labels path for both the org.label-schema and org.opencontainers.image formats: + write_to_build_labels "org.label-schema.title" "org.opencontainers.image.title" "${IMAGE_TITLE:-}" + write_to_build_labels "org.label-schema.url" "org.opencontainers.image.url" "${IMAGE_URL:-}" + write_to_build_labels "org.label-schema.vcs-ref" "org.opencontainers.image.revision" "${IMAGE_VCS_REF:-}" + write_to_build_labels "org.label-schema.vcs-url" "org.opencontainers.image.source" "${IMAGE_VCS_URL:-}" + write_to_build_labels "org.label-schema.vendor" "org.opencontainers.image.vendor" "${IMAGE_VENDOR:-}" + write_to_build_labels "MAINTAINER" "maintainer" "org.opencontainers.image.authors" "${IMAGE_AUTHOR:-}" + write_to_build_labels "org.label-schema.description" "org.opencontainers.image.description" "${IMAGE_DESCRIPTION:-}" + write_to_build_labels "org.label-schema.usage" "org.opencontainers.image.documentation" "${IMAGE_DOCUMENTATION:-}" + write_to_build_labels "org.label-schema.version" "org.opencontainers.image.version" "${IMAGE_VERSION:-}" +} + +! (return 0 2>/dev/null) || write_apptainer_labels "$@" diff --git a/.github/workflows/apptainer-image.yml b/.github/workflows/apptainer-image.yml new file mode 100644 index 0000000..9aa1b57 --- /dev/null +++ b/.github/workflows/apptainer-image.yml @@ -0,0 +1,110 @@ +name: Apptainer Build +on: + push: + tags: + - "*@*" + +defaults: + run: + shell: bash + +env: + APPTAINER_VERSION: 1.2.5 + ORAS_VERSION: 1.1.0 + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + name: Build Apptainer image + permissions: + contents: write + packages: write + + steps: + - name: Download Apptainer + run: | + set -eux -o pipefail + curl -o "apptainer-${APPTAINER_VERSION}.deb" -L https://github.com/apptainer/apptainer/releases/download/v${APPTAINER_VERSION}/apptainer_${APPTAINER_VERSION}_amd64.deb + export DEBIAN_FRONTEND=noninteractive + sudo apt-get update -yq || echo "Couldn't update apt packages. Will attempt installation without update" >&2 + sudo dpkg --install --force-depends "apptainer-${APPTAINER_VERSION}.deb" && sudo apt-get install --fix-broken --yes --quiet + apptainer >&2 --version && echo >&2 "Apptainer installed successfully!" + apptainer remote login -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} oras://ghcr.io && echo "Logged in to remote registry successfully" >&2 + + - name: Install ORAS + run: | + set -eux -o pipefail + curl -o "oras_${ORAS_VERSION}.tar.gz" -L "https://github.com/oras-project/oras/releases/download/v${ORAS_VERSION}/oras_${ORAS_VERSION}_linux_amd64.tar.gz" + + # Install the executable: + tar -xvf oras_${ORAS_VERSION}.tar.gz && chmod +x oras && sudo mv oras /usr/local/bin/oras + sudo mv "${DOWNLOAD_PATH}" /usr/local/bin/oras && sudo chmod +x /usr/local/bin/oras & oras >&2 version && echo >&2 "oras installed successfully!" + oras login -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} ghcr.io && echo "Logged in to remote registry successfully" >&2 + + - name: Check out code for the container build + uses: actions/checkout@v4 + + - name: Build Container + run: | + set -eux -o pipefail + if [[ "${GITHUB_REF_TYPE:-}" == "tag" ]] && [[ "${GITHUB_REF}" =~ ^.*@.*$ ]]; then + [[ -z "${IMAGE_NAME:-}" ]] && IMAGE_NAME="${GITHUB_REF%%@*}" && IMAGE_NAME="${IMAGE_NAME##refs/tags/}" + [[ -z "${IMAGE_TAG:-}" ]] && IMAGE_TAG="${GITHUB_REF##*@}" && IMAGE_TAG="${IMAGE_TAG##*v}" + fi + + [[ -z "${IMAGE_NAME:-}" ]] && IMAGE_NAME="${GITHUB_REPOSITORY##*/}" + [[ -z "${IMAGE_TAG:-}" ]] && IMAGE_TAG="$(date +%s)" + + if [[ -d "${IMAGE_NAME}" ]] && [[ -f "${IMAGE_NAME}/Singularity" ]]; then + cd "${IMAGE_NAME}" + echo "Using Singularity file in ${PWD}" >&2 + elif [[ -f Singularity ]]; then + echo "Using Singularity file in root directory" >&2 + else + echo "No Singularity file found in \"${IMAGE_NAME:-}\" or root directory" >&2 + exit 1 + fi + + IMAGE_PATH="${GITHUB_WORKSPACE}/${IMAGE_NAME}".sif + + echo "IMAGE_NAME=${IMAGE_NAME}" >> $GITHUB_ENV + echo "IMAGE_TAG=${IMAGE_TAG}" >> $GITHUB_ENV + echo "IMAGE_PATH=${IMAGE_PATH}" >> $GITHUB_ENV + + echo "IMAGE_NAME=${IMAGE_NAME}" >&2 + echo "IMAGE_TAG=${IMAGE_TAG}" >&2 + echo "IMAGE_PATH=${IMAGE_PATH}" >&2 + + apptainer build --nv --fix-perms --disable-cache --force "${IMAGE_PATH}" Singularity + + echo "Container built successfully" >&2 + + echo "Container size:" >&2 + du -h "${IMAGE_PATH}" >&2 + + echo "Container labels:" >&2 + apptainer inspect "${IMAGE_PATH}" >&2 + + - name: Push Container + run: | + set -eux -o pipefail + + if [[ "${GITHUB_REF_TYPE:-}" == "tag" ]] && [[ "${GITHUB_REF}" =~ ^.*@.*$ ]]; then + [[ -z "${IMAGE_NAME:-}" ]] && IMAGE_NAME="${GITHUB_REF%%@*}" && IMAGE_NAME="${IMAGE_NAME##refs/tags/}" + [[ -z "${IMAGE_TAG:-}" ]] && IMAGE_TAG="${GITHUB_REF##*@}" && IMAGE_TAG="${IMAGE_TAG##*v}" + fi + + [[ -z "${IMAGE_NAME:-}" ]] && IMAGE_NAME="${GITHUB_REPOSITORY##*/}" + [[ -z "${IMAGE_TAG:-}" ]] && IMAGE_TAG="$(date +%s)" + + # Log in: + apptainer remote login -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} oras://ghcr.io + + # Push the image: + apptainer push -U "${IMAGE_PATH}" oras://ghcr.io/${{ github.repository }}/${IMAGE_NAME}:${IMAGE_TAG} + + # Tag the image as latest if it's not a pre-release: + if [[ "${IMAGE_TAG}" != "latest" ]] && [[ ! "${IMAGE_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-.+$ ]]; then + oras tag -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} ghcr.io/${{ github.repository }}/${IMAGE_NAME}:${IMAGE_TAG} latest + fi + echo "Done" >&2 diff --git a/.github/workflows/build-documentation.yml b/.github/workflows/build-documentation.yml new file mode 100644 index 0000000..995a8f1 --- /dev/null +++ b/.github/workflows/build-documentation.yml @@ -0,0 +1,54 @@ +name: Apptainer Build +on: + push: + branches: + - main + paths: + - README.md.esh + - .github/workflows/scripts/esh + - .github/workflows/build-documentation.yml + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + name: Build documentation + permissions: write-all + steps: + - name: Check out code for the container build + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Build documentation + shell: bash + run: | + set -eux -o pipefail + + # Run esh to fill in the variables: + "${GITHUB_WORKSPACE}/.github/workflows/scripts/esh" "${GITHUB_WORKSPACE}/README.md.esh" > "${GITHUB_WORKSPACE}/README.md" + + # Stage files: + git add README.md + + # Build additions to commit message: + commit_args=() + + # Get the last commit message, if any: + last_commit_msg="$(git log -1 --pretty=format:%B || true)" + + if git diff --staged --name-only "README.md" | grep -Fq "README.md"; then + commit_args+=(-m "GITHUB_ACTION=\"${GITHUB_ACTION:-}\": Templated \"README.md\" for GITHUB_REPOSITORY=\"${GITHUB_REPOSITORY}\"") + fi + + if (( "${#commit_args[@]}" > 1 )); then + + # Don't append to commit message: + if [[ "${GITHUB_REF_TYPE:-}" == "tag" ]] && [[ "${GITHUB_REF}" =~ ^.*@.*$ ]]; then + echo "Not appending commit messages because this is a tagged release" >&2 + commit_args=() + fi + + # Set up git config for push: + git config --local user.email "${{ github.event.sender.id }}+${{ github.event.sender.login }}@users.noreply.github.com" + git config --local user.name ${{ github.event.sender.login }} + git commit -a "${last_commit_msg:+-m ${last_commit_msg:-}}" "${commit_args[@]}" && git push --force + fi diff --git a/.github/workflows/scripts/esh b/.github/workflows/scripts/esh new file mode 100755 index 0000000..a5e13bc --- /dev/null +++ b/.github/workflows/scripts/esh @@ -0,0 +1,388 @@ +#!/bin/sh +# vim: set ts=4: +# esh - https://github.com/jirutka/esh +# The MIT License + +# Copyright 2017-present Jakub Jirutka . + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +#---help--- +# USAGE: +# esh [options] [--] [...] +# esh <-h | -V> +# +# Process and evaluate an ESH template. +# +# ARGUMENTS: +# Path of the template file or "-" to read from STDIN. +# Variable(s) specified as = to pass into the +# template (the have higher priority than environment +# variables). +# +# OPTIONS: +# -d Don't evaluate template, just dump a shell script. +# -o Output file or "-" for STDOUT. Defaults to "-". +# -s Command name or path of the shell to use for template +# evaluation. It must not contain spaces. +# Defaults to "/bin/sh". +# -h Show this help message and exit. +# -V Print version and exit. +# +# ENVIRONMENT: +# ESH_AWK Command name of path of the awk program to use. +# It must not contain spaces. Defaults to "awk". +# ESH_MAX_DEPTH Maximum include depth. Defaults to 3. +# ESH_SHELL Same as -s. +# +# EXIT STATUS: +# 0 Clean exit, no error has encountered. +# 1 Generic error. +# 10 Invalid usage. +# 11 ESH syntax error. +# 12 Include error: file not found. +# 13 Include error: exceeded max include depth (ESH_MAX_DEPTH). +# +# Please report bugs at . +#---help--- +set -eu + +readonly PROGNAME='esh' +readonly VERSION='0.3.2' +readonly SCRIPTPATH="$0" + +AWK_CONVERTER=$(cat <<'AWK' +function fail(code, msg) { + state = "ERROR" + # FIXME: /dev/stderr is not portable + printf("%s: %s\n", line_info(), msg) > "/dev/stderr" + exit code +} +function line_info() { + return FILENAME ? (filenames[depth] ":" linenos[depth]) : "(init)" # (init) if inside BEGIN +} +# IMPORTANT: This is the only function that should print a newline. +function puts(str) { + print(line_info()) > MAP_FILE + print(str) +} +function fputs(str) { + printf("%s", str) +} +function trim(str) { + gsub(/^[ \t\r\n]+|[ \t\r\n]+$/, "", str) + return str +} +function read(len, _str) { + if (len == "") { + _str = buff + buff = "" + } else if (len > 0) { + _str = substr(buff, 1, len) + buff = substr(buff, len + 1, length(buff)) + } + return _str +} +function skip(len) { + buff = substr(buff, len + 1, length(buff)) +} +function flush(len, _str) { + _str = read(len) + + if (state == "TEXT") { + gsub("'", "'\\''", _str) + } + if (state != "COMMENT") { + fputs(_str) + } +} +function file_exists(filename, _junk) { + if ((getline _junk < filename) >= 0) { + close(filename) + return 1 + } + return 0 +} +function dirname(path) { + return sub(/\/[^\/]+\/*$/, "/", path) ? path : "" +} +function include(filename) { + if (index(filename, "/") != 1) { # if does not start with "/" + filename = dirname(filenames[depth]) filename + } + if (!file_exists(filename)) { + fail(12, "cannot include " filename ": not a file or not readable") + } + if (depth > MAX_DEPTH) { + fail(13, "cannot include " filename ": exceeded maximum depth of " MAX_DEPTH) + } + buffs[depth] = buff + states[depth] = state + filenames[depth + 1] = filename + depth++ + + init() + while ((getline buff < filename) > 0) { + if (print_nl && state != "COMMENT") { + puts("") + } + process_line() + } + end_text() + close(filename) + + depth-- + buff = buffs[depth] + state = states[depth] +} +function init() { + buff = "" + linenos[depth] = 0 + print_nl = 0 + start_text() +} +function start_text() { + puts("") + fputs("printf '%s' '") + state = "TEXT" +} +function end_text() { + if (state != "TEXT") { return } + puts("' #< " line_info()) + state = "UNDEF" +} +function process_line() { + print_nl = 1 + linenos[depth]++ + + while (buff != "") { + print_nl = 1 + + if (state == "TEXT" && match(buff, /<%/)) { + flush(RSTART - 1) # print buff before "<%" + skip(2) # skip "<%" + + flag = substr(buff, 1, 1) + if (flag != "%") { + end_text() + } + if (flag == "%") { # <%% + skip(1) + fputs("<%") + } else if (flag == "=") { # <%= + skip(1) + fputs("__print ") + state = "TAG" + } else if (flag == "+") { # <%+ + if (!match(buff, /[^%]%>/)) { + fail(11, "syntax error: <%+ must be closed on the same line") + } + filename = trim(substr(buff, 2, match(buff, /.-?%>/) - 1)) + skip(RSTART) + include(filename) + state = "TAG" + } else if (flag == "#") { # <%# + state = "COMMENT" + } else { + state = "TAG" + } + } else if (state != "TEXT" && match(buff, /%>/)) { + flag = RSTART > 1 ? substr(buff, RSTART - 1, 1) : "" + + if (flag == "%") { # %%> + flush(RSTART - 2) + skip(1) + flush(2) + } else if (flag == "-") { # -%> + flush(RSTART - 2) + skip(3) + print_nl = 0 + } else { # %> + flush(RSTART - 1) + skip(2) + } + if (flag != "%") { + start_text() + } + } else { + flush() + } + } +} +BEGIN { + FS = "" + depth = 0 + + puts("#!" (SHELL ~ /\// ? SHELL : "/usr/bin/env " SHELL)) + puts("set -eu") + puts("if ( set -o pipefail 2>/dev/null ); then set -o pipefail; fi") + puts("__print() { printf '%s' \"$*\"; }") + + split(VARS, _lines, /\n/) + for (_i in _lines) { + puts(_lines[_i]) + } + init() +} +{ + if (NR == 1) { + filenames[0] = FILENAME # this var is not defined in BEGIN so we must do it here + } + buff = $0 + process_line() + + if (print_nl && state != "COMMENT") { + puts("") + } +} +END { + end_text() +} +AWK +) +AWK_ERR_FILTER=$(cat <<'AWK' +function line_info(lno, _line, _i) { + while ((getline _line < MAPFILE) > 0 && _i++ < lno) { } + close(MAPFILE) + return _line +} +{ + if (match($0, "^" SRCFILE ":( line)? ?[0-9]+:") && match(substr($0, 1, RLENGTH), /[0-9]+:$/)) { + lno = substr($0, RSTART, RLENGTH - 1) + 0 + msg = substr($0, RSTART + RLENGTH + 1) # v-- some shells duplicate filename + msg = index(msg, SRCFILE ":") == 1 ? substr(msg, length(SRCFILE) + 3) : msg + print(line_info(lno) ": " msg) + } else if ($0 != "") { + print($0) + } +} +AWK +) +readonly AWK_CONVERTER AWK_ERR_FILTER + +print_help() { + sed -En '/^#---help---/,/^#---help---/p' "$SCRIPTPATH" | sed -E 's/^# ?//; 1d;$d;' +} + +filter_shell_stderr() { + $ESH_AWK \ + -v SRCFILE="$1" \ + -v MAPFILE="$2" \ + -- "$AWK_ERR_FILTER" +} + +evaluate() { + local srcfile="$1" + local mapfile="$2" + + # This FD redirection magic is for swapping stdout/stderr back and forth. + exec 3>&1 + { set +e; $ESH_SHELL "$srcfile" 2>&1 1>&3; echo $? >>"$mapfile"; } \ + | filter_shell_stderr "$srcfile" "$mapfile" >&2 + exec 3>&- + + return $(tail -n 1 "$mapfile") +} + +convert() { + local input="$1" + local vars="$2" + local map_file="${3:-"/dev/null"}" + + $ESH_AWK \ + -v MAX_DEPTH="$ESH_MAX_DEPTH" \ + -v SHELL="$ESH_SHELL" \ + -v MAP_FILE="$map_file" \ + -v VARS="$vars" \ + -- "$AWK_CONVERTER" "$input" +} + +process() { + local input="$1" + local vars="$2" + local evaluate="${3:-yes}" + local ret=0 tmpfile mapfile + + if [ "$evaluate" = yes ]; then + tmpfile=$(mktemp) + mapfile=$(mktemp) + + convert "$input" "$vars" "$mapfile" > "$tmpfile" || ret=$? + test $ret -ne 0 || evaluate "$tmpfile" "$mapfile" || ret=$? + + rm -f "$tmpfile" "$mapfile" + else + convert "$input" "$vars" || ret=$? + fi + return $ret +} + +: ${ESH_AWK:="awk"} +: ${ESH_MAX_DEPTH:=3} +: ${ESH_SHELL:="/bin/sh"} +EVALUATE='yes' +OUTPUT='' + +while getopts ':dho:s:V' OPT; do + case "$OPT" in + d) EVALUATE=no;; + h) print_help; exit 0;; + o) OUTPUT="$OPTARG";; + s) ESH_SHELL="$OPTARG";; + V) echo "$PROGNAME $VERSION"; exit 0;; + '?') echo "$PROGNAME: unknown option: -$OPTARG" >&2; exit 10;; + esac +done +shift $(( OPTIND - 1 )) + +if [ $# -eq 0 ]; then + printf "$PROGNAME: %s\n\n" 'missing argument ' >&2 + print_help >&2 + exit 10 +fi + +INPUT="$1"; shift +if [ "$INPUT" != '-' ] && ! [ -f "$INPUT" -a -r "$INPUT" ]; then + echo "$PROGNAME: can't read $INPUT: not a file or not readable" >&2; exit 10 +fi + +# Validate arguments. +for arg in "$@"; do + case "$arg" in + *=*) ;; + *) echo "$PROGNAME: illegal argument: $arg" >&2; exit 10;; + esac +done + +# Format variables into shell variable assignments. +vars=''; for item in "$@"; do + vars="$vars\n${item%%=*}='$( + printf %s "${item#*=}" | $ESH_AWK "{ gsub(/'/, \"'\\\\\\\''\"); print }" + )'" +done + +export ESH="$0" + +if [ "${OUTPUT#-}" ]; then + tmpfile="$(mktemp)" + trap 'rm -f -- "$tmpfile"' EXIT HUP INT TERM + process "$INPUT" "$vars" "$EVALUATE" > "$tmpfile" + cat "$tmpfile" > "$OUTPUT" +else + process "$INPUT" "$vars" "$EVALUATE" +fi