#!/usr/bin/env bash # Which locally built image is in play. # # ./scripts/image.sh list every arena image on this machine # ./scripts/image.sh resolve the tag versions.env names # ./scripts/image.sh resolve newest the most recent build # ./scripts/image.sh resolve c836f26 the build made from that commit # ./scripts/image.sh verify newest resolve it, and say what it is # ./scripts/image.sh diff 4e5403f c836f26 what changed between two builds # # A machine that has built this repo a few times has a dozen tags on it, spanning # more than one Suramadu version, and the tag string is the only thing telling # them apart. Reading a tag is guesswork; this script answers instead. # # `resolve` prints one reference on stdout and nothing else, so anything that # needs an image can ask here rather than composing IMAGE_NAME and IMAGE_TAG for # itself: # # IMAGE="$(scripts/image.sh resolve "$SELECTOR")" # # Selectors: # # (nothing), current the tag versions.env names for this commit # newest the most recently built image # an exact tag, as `list` prints it # a git sha, short or long; matches the sha in the tag # : a full reference, taken as given # # Every command is read-only. Nothing here builds, pushes or removes an image - # what to delete is your call, so `list` reports what is on disk and stops there. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # shellcheck source=../versions.env . "$ROOT/versions.env" say() { printf 'image: %s\n' "$*" >&2; } die() { say "$*"; exit 1; } usage() { cat <<'USAGE' image.sh - which locally built image is in play image.sh list every arena image on this machine image.sh resolve [] print one reference, and nothing else image.sh verify [] resolve it, and say what it is image.sh diff [--deep] what changed between two builds selectors (nothing), current the tag versions.env names for this commit newest the most recently built image an exact tag, as `list` prints it a git sha, short or long; matches the sha in the tag : a full reference, taken as given --deep runs one throwaway container per image to read versions.env and the container scripts out of them. Everything else is `docker image inspect`. USAGE } # One temp directory per run, cleaned up on the way out. A RETURN trap per # function would be tidier to read but traps are not scoped to a function, so # the second one to be set wins and the first directory is never removed. # # It sets $TMP rather than printing it: a command substitution runs in a # subshell, so the EXIT trap would fire there and delete the directory before # the caller had written anything into it. TMP="" tmpdir() { if [ -z "$TMP" ]; then TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT fi } # --- facts a tag carries ----------------------------------------------------- # versions.env builds the tag as mm-sur-, where is # the branch, a dash, and a 12-character sha - with the branch dropped on a # detached HEAD and "-dirty" appended when the tree was not clean. # # So the commit is the LAST dash-separated field, never the third: branch names # contain dashes, and this repo's branches are all `claude/some-long-name`. Tags # from before the ref was part of the name (mm0.51.0-ws26.4.5) end in a version # rather than hex, and get no commit at all, which is the right answer for them. tag_commit() { local tag="${1%-dirty}" last last="${tag##*-}" case "$last" in "" | *[!0-9a-f]*) return 0 ;; esac # Seven is git's own floor for an abbreviated sha. Below it, a short lowercase # field like "abc" is far more likely to be part of a branch name. [ "${#last}" -ge 7 ] || return 0 printf '%s' "$last" } # The commit an image came from: the tag first, then the revision label the build # baked in. They agree on everything built since the tag started carrying a ref; # the label is what answers for the tags from before that, which is most of what # is on a machine that has been building this repo for a week. image_commit() { local sha sha="$(tag_commit "$1")" [ -n "$sha" ] || sha="$(tag_commit "${2:-}")" printf '%s' "$sha" } tag_is_dirty() { case "$1" in *-dirty) return 0 ;; esac return 1 } # A tag naming a commit nobody can `git show` is a build nobody can reproduce. # scripts/deploy.sh refuses to ship one; here it is only reported, because an # unreachable commit on a local image is normal - it is usually a rebased branch. commit_in_repo() { [ -n "$1" ] || return 1 git -C "$ROOT" cat-file -e "${1}^{commit}" 2>/dev/null } commit_subject() { git -C "$ROOT" log -1 --format=%s "$1" 2>/dev/null || true } # A selector and a tag's commit match when either is a prefix of the other: the # tag carries 12 characters, `git log --oneline` prints 7, and people paste all # 40. Four is the shortest prefix worth trusting to mean one commit. sha_matches() { local sel="$1" have="$2" [ -n "$have" ] || return 1 [ "${#sel}" -ge 4 ] || return 1 case "$have" in "$sel"*) return 0 ;; esac case "$sel" in "$have"*) return 0 ;; esac return 1 } # --- formatting -------------------------------------------------------------- # Integer arithmetic only, so this needs nothing beyond bash. KiB is here for # `diff`: two builds off the same base differ by a metadata layer, and "0.0 MiB # larger" is not an answer. human_bytes() { local b="$1" unit scaled if [ "$b" -lt 1048576 ]; then scaled=$(( (b * 10 + 512) / 1024 )); unit=KiB elif [ "$b" -lt 1073741824 ]; then scaled=$(( (b * 10 + 524288) / 1048576 )); unit=MiB else scaled=$(( (b * 10 + 536870912) / 1073741824 )); unit=GiB fi printf '%d.%d %s' $(( scaled / 10 )) $(( scaled % 10 )) "$unit" } # 2026-08-07T17:44:54.895042045-04:00 -> 2026-08-07 17:44 short_time() { printf '%s %s' "${1:0:10}" "${1:11:5}"; } short_id() { local id="${1#sha256:}"; printf '%s' "${id:0:12}"; } # --- reading the daemon ------------------------------------------------------ need_docker() { command -v docker >/dev/null || die "docker is not installed" } # id|size|created|revision, for one reference. The revision label is what # build.sh baked in from BUILD_REF, so it is the image's own copy of the commit - # worth having next to the one parsed out of the tag. inspect_fields() { docker image inspect --format \ '{{.Id}}|{{.Size}}|{{.Created}}|{{with index .Config.Labels "org.opencontainers.image.revision"}}{{.}}{{end}}' \ "$1" } image_exists() { docker image inspect "$1" >/dev/null 2>&1; } # scripts/run.sh has a one-line version of this check. The difference is what it # can say afterwards: the usual cause of a miss is that versions.env names a tag # for THIS commit and no build has produced it yet, which reads as "not built" # when what changed was the commit. require_image() { local ref="$1" sel="${2:-}" if image_exists "$ref"; then return 0; fi say "no such image on this machine: $ref" if [ -z "$sel" ] || [ "$sel" = current ]; then say "that is the tag versions.env names for this commit - build it:" say " ./scripts/build.sh" fi say "./scripts/image.sh list shows what is on disk" exit 1 } # Every tag of IMAGE_NAME, grouped by image id, newest first. # # Grouping matters for the totals: build.sh tags each build `latest` as well as # its immutable tag, so two rows share one set of layers. A row per tag would # imply disk usage that is not there. # # The sort is ours rather than `docker image ls`'s, because timestamps here are # not unique - three builds of this repo carry the same Created value down to the # nanosecond, and docker orders those ties differently from one run to the next, # so the same command printed a different "newest" each time. Sorting on the # second and then on the tag makes the output the same twice in a row; which of # a tied set is really the newest build is a question for git, and `newest` # answers it below. IMG_LOADED=0 G_TAG=() # the tag that best names the group G_ALIAS=() # its other tags, space separated G_ID=() G_SIZE=() G_CREATED=() G_REV=() img_load() { if [ "$IMG_LOADED" = 1 ]; then return 0; fi IMG_LOADED=1 need_docker local tags=() mapfile -t tags < <(docker image ls "$IMAGE_NAME" --format '{{.Tag}}' | grep -vx '' || true) if [ "${#tags[@]}" -eq 0 ]; then return 0; fi local refs=() t for t in "${tags[@]}"; do refs+=("$IMAGE_NAME:$t"); done # One inspect for the whole list. Its output is one line per argument, in # order, which is the only thing tying a line back to its tag - so if the # counts disagree, something removed an image while this was running and the # rows would be attached to the wrong tags. local rows=() mapfile -t rows < <(docker image inspect --format \ '{{.Id}}|{{.Size}}|{{.Created}}|{{with index .Config.Labels "org.opencontainers.image.revision"}}{{.}}{{end}}' \ "${refs[@]}") if [ "${#rows[@]}" -ne "${#tags[@]}" ]; then die "the image list changed while it was being read; run this again" fi # Sort on the whole second only. Docker prints RFC 3339 with the trailing # zeros trimmed, so the fractional part is not a fixed width and comparing it # as text is not comparing time. local order=() when mapfile -t order < <( for i in "${!tags[@]}"; do when="${rows[$i]#*|}"; when="${when#*|}" printf '%s\t%s\t%s\n' "${when:0:19}" "${tags[$i]}" "$i" done | sort -t$'\t' -k1,1r -k2,2 ) local row i id size created rev g found for row in "${order[@]}"; do i="${row##*$'\t'}" IFS='|' read -r id size created rev <<<"${rows[$i]}" found=-1 for g in "${!G_ID[@]}"; do if [ "${G_ID[$g]}" = "$id" ]; then found="$g"; break; fi done if [ "$found" -lt 0 ]; then G_TAG+=("${tags[$i]}"); G_ALIAS+=(""); G_ID+=("$id") G_SIZE+=("$size"); G_CREATED+=("$created"); G_REV+=("$rev") continue fi # `latest` moves with every build and cannot say which one it is, so a tag # that carries a commit takes the row and the moving one becomes the alias. if [ -z "$(tag_commit "${G_TAG[$found]}")" ] && [ -n "$(tag_commit "${tags[$i]}")" ]; then G_ALIAS[$found]="${G_ALIAS[$found]:+${G_ALIAS[$found]} }${G_TAG[$found]}" G_TAG[$found]="${tags[$i]}" else G_ALIAS[$found]="${G_ALIAS[$found]:+${G_ALIAS[$found]} }${tags[$i]}" fi done } # The newest build, as a group index. # # Timestamps tie, so the first row is only a candidate. Among everything built in # the same second, the newest build is the one whose commit has all the others # behind it in git - which is a question the timestamps cannot answer and git # always can. If they are not on one line of history there is no newest, and # saying so beats picking whichever the daemon happened to list first. newest_group() { img_load [ "${#G_TAG[@]}" -gt 0 ] || die "no $IMAGE_NAME image on this machine; run ./scripts/build.sh" local tied=() g for g in "${!G_TAG[@]}"; do if [ "${G_CREATED[$g]:0:19}" = "${G_CREATED[0]:0:19}" ]; then tied+=("$g"); fi done if [ "${#tied[@]}" -eq 1 ]; then printf '%s' "${tied[0]}"; return 0; fi # How many of the others each candidate has behind it. The tip has all of them. local best=-1 bestscore=-1 score other ca cb ties=0 for g in "${tied[@]}"; do ca="$(image_commit "${G_TAG[$g]}" "${G_REV[$g]}")" score=0 for other in "${tied[@]}"; do [ "$other" != "$g" ] || continue cb="$(image_commit "${G_TAG[$other]}" "${G_REV[$other]}")" if [ -n "$ca" ] && [ -n "$cb" ] \ && git -C "$ROOT" merge-base --is-ancestor "$cb" "$ca" 2>/dev/null; then score=$(( score + 1 )) fi done if [ "$score" -gt "$bestscore" ]; then best="$g"; bestscore="$score"; ties=0 elif [ "$score" -eq "$bestscore" ]; then ties=$(( ties + 1 )) fi done if [ "$ties" -gt 0 ]; then say "${#tied[@]} images were built in the same second and git cannot order them:" for g in "${tied[@]}"; do say " ${G_TAG[$g]}"; done die "name one of them instead of 'newest'" fi printf '%s' "$best" } # Index of the group holding a tag, or nothing. group_of_tag() { local want="$1" g t for g in "${!G_TAG[@]}"; do if [ "${G_TAG[$g]}" = "$want" ]; then printf '%s' "$g"; return 0; fi for t in ${G_ALIAS[$g]}; do if [ "$t" = "$want" ]; then printf '%s' "$g"; return 0; fi done done } # --- resolve ----------------------------------------------------------------- resolve_selector() { local sel="${1:-}" g hits=() case "$sel" in "" | current) printf '%s:%s' "$IMAGE_NAME" "$IMAGE_TAG" return 0 ;; *:*) # Already a full reference. Nothing to derive, and second-guessing a # repository someone typed in full is not this script's business. printf '%s' "$sel" return 0 ;; newest) # Loaded here rather than only inside newest_group: that call is a command # substitution, and a subshell cannot hand the group arrays back. img_load g="$(newest_group)" printf '%s:%s' "$IMAGE_NAME" "${G_TAG[$g]}" return 0 ;; esac img_load # An exact tag wins over a sha. It is unambiguous, and it is the string `list` # printed, so what you copied is what you get. g="$(group_of_tag "$sel")" if [ -n "$g" ]; then printf '%s:%s' "$IMAGE_NAME" "$sel" return 0 fi for g in "${!G_TAG[@]}"; do if sha_matches "$sel" "$(image_commit "${G_TAG[$g]}" "${G_REV[$g]}")"; then hits+=("$g"); fi done case "${#hits[@]}" in 0) die "no tag and no build matches '$sel'; ./scripts/image.sh list shows what is on disk" ;; 1) printf '%s:%s' "$IMAGE_NAME" "${G_TAG[${hits[0]}]}"; return 0 ;; esac say "'$sel' matches more than one build:" for g in "${hits[@]}"; do say " ${G_TAG[$g]}"; done die "name one of them" } # --- commands ---------------------------------------------------------------- cmd_resolve() { [ "$#" -le 1 ] || die "usage: image.sh resolve []" local ref ref="$(resolve_selector "${1:-}")" require_image "$ref" "${1:-}" printf '%s\n' "$ref" } cmd_verify() { [ "$#" -le 1 ] || die "usage: image.sh verify []" local ref sel="${1:-}" ref="$(resolve_selector "$sel")" require_image "$ref" "$sel" local id size created rev sha subject g IFS='|' read -r id size created rev < <(inspect_fields "$ref") sha="$(image_commit "${ref##*:}" "$rev")" printf '%s\n' "$ref" printf ' id %s\n' "$(short_id "$id")" printf ' size %s (docker image inspect .Size)\n' "$(human_bytes "$size")" printf ' built %s\n' "$(short_time "$created")" printf ' ref %s (baked into the image as a label)\n' "${rev:--}" if [ -z "$sha" ]; then printf ' commit none in the tag, and no revision label either\n' elif commit_in_repo "$sha"; then subject="$(commit_subject "$sha")" printf ' commit %s %s\n' "$sha" "$subject" else printf ' commit %s NOT in this repo - nothing can rebuild this image\n' "$sha" fi if tag_is_dirty "${ref##*:}"; then printf ' built from an uncommitted tree; the commit above is not what it contains\n' fi img_load g="$(group_of_tag "${ref##*:}")" if [ -n "$g" ] && [ -n "${G_ALIAS[$g]}" ]; then printf ' also %s\n' "${G_ALIAS[$g]}" fi } cmd_list() { [ "$#" -eq 0 ] || die "usage: image.sh list" img_load if [ "${#G_TAG[@]}" -eq 0 ]; then say "no $IMAGE_NAME image on this machine; run ./scripts/build.sh" return 0 fi local g w=3 t total=0 orphans=0 mark sha state for t in "${G_TAG[@]}"; do if [ "${#t}" -gt "$w" ]; then w="${#t}"; fi done # By group rather than by tag: the tag versions.env names can be an alias of # the row, and the row is still the image it means. local current; current="$(group_of_tag "$IMAGE_TAG")" printf '%-2s %-*s %-12s %10s %-16s %-12s %s\n' \ '' "$w" TAG ID SIZE BUILT COMMIT 'IN REPO' for g in "${!G_TAG[@]}"; do sha="$(image_commit "${G_TAG[$g]}" "${G_REV[$g]}")" if [ -z "$sha" ]; then state='-' elif commit_in_repo "$sha"; then state=yes else state=no orphans=$(( orphans + 1 )) fi mark=' ' if [ "$g" = "$current" ]; then mark='*'; fi printf '%-2s %-*s %-12s %10s %-16s %-12s %s\n' \ "$mark" "$w" "${G_TAG[$g]}" "$(short_id "${G_ID[$g]}")" \ "$(human_bytes "${G_SIZE[$g]}")" "$(short_time "${G_CREATED[$g]}")" \ "${sha:--}" "$state" if [ -n "${G_ALIAS[$g]}" ]; then printf ' also tagged: %s\n' "${G_ALIAS[$g]}" fi total=$(( total + G_SIZE[g] )) done printf '\n%d image(s), %s total, counted once per image id\n' \ "${#G_TAG[@]}" "$(human_bytes "$total")" if [ -n "$current" ]; then printf '* %s is what versions.env names for this commit\n' "$IMAGE_TAG" else printf '* nothing here is %s, which is what versions.env names for this commit\n' "$IMAGE_TAG" fi if [ "$orphans" -gt 0 ]; then printf '%d image(s) name a commit this repo does not have; nothing can rebuild them\n' "$orphans" fi report_dangling } # Untagged images are not necessarily arena's - a rebuild of anything on this # machine leaves one behind. Reported rather than removed: which of them is worth # keeping is not a decision a script gets to make. report_dangling() { local ids=() sizes=() n s total=0 mapfile -t ids < <(docker image ls -f dangling=true --format '{{.ID}}' || true) n="${#ids[@]}" if [ "$n" -eq 0 ]; then return 0; fi mapfile -t sizes < <(docker image inspect --format '{{.Size}}' "${ids[@]}" 2>/dev/null || true) for s in "${sizes[@]}"; do total=$(( total + s )); done printf '%d untagged image(s) hold %s, from any project on this machine:\n' \ "$n" "$(human_bytes "$total")" printf ' docker image ls -f dangling=true\n' } # --- diff -------------------------------------------------------------------- # The same trick scripts/run.sh uses to read a scenario out of an image: one # throwaway container, no entrypoint, one command that reads files and exits. # versions.env is copied into the image, so this is the build's own record of # what it pinned rather than an inference from the tag. # # Capped on purpose. This cats a file and hashes a handful of scripts; if it ever # needs more than a core and half a gigabyte, something is wrong with it. DEEP_READ=' echo "=== /opt/arena/versions.env" cat /opt/arena/versions.env 2>/dev/null || echo "(this image has no versions.env)" echo "=== container scripts (sha256)" find /opt/arena/container -name "*.sh" 2>/dev/null | sort \ | xargs -r sha256sum 2>/dev/null | sed "s|/opt/arena/||" \ || echo "(no container scripts)" ' deep_read() { docker run --rm --cpus 1 --memory 512m --entrypoint /bin/sh "$1" -c "$DEEP_READ" } cmd_diff() { local deep=0 if [ "${1:-}" = --deep ]; then deep=1; shift; fi [ "$#" -eq 2 ] || die "usage: image.sh diff [--deep] " local a b a="$(resolve_selector "$1")"; require_image "$a" "$1" b="$(resolve_selector "$2")"; require_image "$b" "$2" local aid asize acreated arev bid bsize bcreated brev IFS='|' read -r aid asize acreated arev < <(inspect_fields "$a") IFS='|' read -r bid bsize bcreated brev < <(inspect_fields "$b") printf 'a %s\n %s %s %s\n' "$a" "$(short_id "$aid")" \ "$(human_bytes "$asize")" "$(short_time "$acreated")" printf 'b %s\n %s %s %s\n\n' "$b" "$(short_id "$bid")" \ "$(human_bytes "$bsize")" "$(short_time "$bcreated")" if [ "$aid" = "$bid" ]; then printf 'Both selectors name the same image.\n' return 0 fi local delta=$(( bsize - asize )) word=larger if [ "$delta" -lt 0 ]; then delta=$(( -delta )); word=smaller; fi if [ "$delta" -eq 0 ]; then printf 'size the same, %s\n' "$(human_bytes "$asize")" else printf 'size b is %s %s (%d bytes)\n' "$(human_bytes "$delta")" "$word" "$delta" fi diff_labels "$a" "$b" diff_commits "$(image_commit "${a##*:}" "$arev")" "$(image_commit "${b##*:}" "$brev")" diff_layers "$a" "$b" if [ "$deep" = 1 ]; then diff_deep "$a" "$b"; fi } # Go's template iterates a map in key order, so both sides come out sorted and a # plain line comparison is enough. The key names moved when Webswing Lite became # Suramadu, which is exactly the kind of difference a fixed list of keys hides. diff_labels() { tmpdir; local tmp="$TMP" local fmt='{{range $k, $v := .Config.Labels}}{{$k}}={{$v}}{{"\n"}}{{end}}' docker image inspect --format "$fmt" "$1" >"$tmp/labels-a" docker image inspect --format "$fmt" "$2" >"$tmp/labels-b" if diff -q "$tmp/labels-a" "$tmp/labels-b" >/dev/null; then printf 'labels identical\n' return 0 fi printf 'labels\n' diff --unchanged-line-format= --old-line-format=' - %L' --new-line-format=' + %L' \ "$tmp/labels-a" "$tmp/labels-b" || true } # The revision label is the build's own copy of BUILD_REF, so the range below is # between the commits the images were actually built from. diff_commits() { local ca="$1" cb="$2" n if [ -z "$ca" ] || [ -z "$cb" ]; then printf 'commits at least one image does not name a commit\n' return 0 fi if ! commit_in_repo "$ca" || ! commit_in_repo "$cb"; then printf 'commits %s -> %s, and this repo does not have both\n' "$ca" "$cb" return 0 fi n="$(git -C "$ROOT" rev-list --count "$ca..$cb" 2>/dev/null || echo 0)" printf 'commits %s -> %s, %s commit(s) forward\n' "$ca" "$cb" "$n" git -C "$ROOT" log --oneline --max-count=20 "$ca..$cb" 2>/dev/null | sed 's/^/ /' || true } # Two builds off the same base share every layer up to the first thing that # changed, and where that is says whether the diff is a rebuild of the world or a # metadata bump. .RootFS.Layers is the cheap answer; docker history names the # step. diff_layers() { tmpdir; local tmp="$TMP" local fmt='{{range .RootFS.Layers}}{{.}}{{"\n"}}{{end}}' docker image inspect --format "$fmt" "$1" >"$tmp/layers-a" docker image inspect --format "$fmt" "$2" >"$tmp/layers-b" local la=() lb=() common=0 i mapfile -t la <"$tmp/layers-a" mapfile -t lb <"$tmp/layers-b" for i in "${!la[@]}"; do if [ "$i" -ge "${#lb[@]}" ] || [ "${la[$i]}" != "${lb[$i]}" ]; then break; fi common=$(( common + 1 )) done printf 'layers %d shared, then %d in a and %d in b\n' \ "$common" "$(( ${#la[@]} - common ))" "$(( ${#lb[@]} - common ))" # docker history prints newest first; reversed, step 1 is the base image and # the numbering lines up with reading the Dockerfile top to bottom. local ha=() hb=() mapfile -t ha < <(docker history --no-trunc --format '{{.CreatedBy}}' "$1" | tac) mapfile -t hb < <(docker history --no-trunc --format '{{.CreatedBy}}' "$2" | tac) for i in "${!ha[@]}"; do if [ "$i" -ge "${#hb[@]}" ] || [ "${ha[$i]}" != "${hb[$i]}" ]; then printf 'history first difference at step %d of %d\n' "$(( i + 1 ))" "${#ha[@]}" printf ' a %.100s\n' "${ha[$i]}" if [ "$i" -lt "${#hb[@]}" ]; then printf ' b %.100s\n' "${hb[$i]}"; fi return 0 fi done printf 'history identical for all %d step(s)\n' "${#ha[@]}" } diff_deep() { tmpdir; local tmp="$TMP" printf '\ninside the images\n' # One at a time, and each exits as soon as it has read what it came for. deep_read "$1" >"$tmp/deep-a" deep_read "$2" >"$tmp/deep-b" if diff -q "$tmp/deep-a" "$tmp/deep-b" >/dev/null; then printf ' versions.env and the container scripts are identical\n' return 0 fi diff -u --label a --label b "$tmp/deep-a" "$tmp/deep-b" | sed 's/^/ /' || true } # --- entry ------------------------------------------------------------------- main() { local cmd=list if [ "$#" -gt 0 ]; then cmd="$1"; shift; fi case "$cmd" in list) cmd_list "$@" ;; resolve) cmd_resolve "$@" ;; verify) cmd_verify "$@" ;; diff) cmd_diff "$@" ;; -h | --help | help) usage ;; *) die "unknown command '$cmd'; there is list, resolve, verify and diff" ;; esac } # Sourcing this file defines the helpers and runs nothing, which is how # tests/shell/test-image.sh exercises the parsing where there is no docker # daemon - the image's test stage, for one. if [ "${BASH_SOURCE[0]}" = "${0}" ]; then main "$@" fi