Skip to content

Instantly share code, notes, and snippets.

@ernstki
Last active March 14, 2021 18:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ernstki/b782cc7f2a29ec01c1f4355f2dd312cc to your computer and use it in GitHub Desktop.
Save ernstki/b782cc7f2a29ec01c1f4355f2dd312cc to your computer and use it in GitHub Desktop.
Determine if a given version of Bash has a specific 'shopt' option using all available 'bash' image tags on Docker Hub
#!/bin/bash
#
# old version that may not work anymore; see docker-image-test.sh instead
#
# source: http://www.googlinux.com/list-all-tags-of-docker-image/index.html
# maybe API documentation?
# shellcheck disable=SC2086
REGISTRY=https://registry.hub.docker.com/v2/repositories/library
[[ $(uname -s) == Darwin ]] && SORT=gsort || SORT=sort
tags=()
image=${1:-bash}
option=${2:-direxpand}
# set CLEANUP=1 in the environment to remove all downloaded images
CLEANUP=${CLEANUP:-}
if test -t 1; then
YEP=$(tput bold)$(tput setaf 2) # bold, green
NOPE=$(tput bold)$(tput setaf 1) # bold, red
RESET=$(tput sgr0)
fi
readarray -t tags < <(
for (( i=1; $?==0; i++ )); do
curl -sL $REGISTRY/$image/tags/?page=$i \
| jq -r '.results[]["name"]' 2>&-
done \
| grep -v devel \
| $SORT -V
)
for tag in "${tags[@]}"; do
if docker run --rm bash:$tag bash -c "shopt | grep -q $option" 2>&-; then
echo "$YEP$tag$RESET: has '$option'"
else
echo "$NOPE$tag$RESET: doesn't have '$option'"
fi
(( CLEANUP )) && docker rmi bash:$tag 2>&-
done
#!/usr/bin/env bash
##
## Test if a command passes with all tags for a specific Docker image;
## e.g., which versions of Bash support 'shopt -s direxpand'
##
## Requires at least Bash 4.x to for 'mapfile' / 'readarray'; the /bin/bash
## that ships with macOS is not sufficient for this, so the shebang line
## is set to use whatever 'bash' is available in your PATH, from MacPorts
## or Homebrew or whatever; other OSes probably won't have such an ancient
## version of Bash, and likely won't have any problem.
##
## Additionally requires 'curl' and 'jq' (https://stedolan.github.io/jq/)
## and requires GNU sort to be available as 'gsort' on non-Linux platforms
## (as it is with 'coreutils' installed from MacPorts); edit the script at
## line ~44 if yours is named something else.
##
## Author: Kevin Ernst <ernstki -at- mail.uc.edu>
## Date: 13 March 2021
## License: MIT
##
## Hat tip:
## - http://www.googlinux.com/list-all-tags-of-docker-image/index.html
##
# shellcheck disable=SC2086
# set TRACE=1 in the environment to trace execution
(( TRACE )) && set -x
# terminate on uninitialized variables
set -u
# set DOCKER_IMAGE_TEST_IMAGE in the environment or use -i / --image to use an
# image besides 'bash' (the default)
IMAGE=${DOCKER_IMAGE_TEST_IMAGE:-bash}
# set DOCKER_IMAGE_TEST_CLEANUP=1 in the environment (or use -c / --cleanup)
# to remove all downloaded images when we're done
CLEANUP=${DOCKER_IMAGE_TEST_CLEANUP:-}
# set SHELL in the environment to use something besides bash
IMAGE_SHELL=${DOCKER_IMAGE_TEST_SHELL:-bash}
ME=${BASH_SOURCE[0]##*/}
GIST='https://gist.github.com/ernstki/b782cc7f2a29ec01c1f4355f2dd312cc'
REGISTRY='https://registry.hub.docker.com/v2/repositories/library'
# update this if your GNU sort is called something else
if [[ $(uname -s) == Linux ]]; then SORT=sort; else SORT=gsort; fi
# programs required to be available
MANIFEST=( $SORT jq curl )
# only use ANSI escapes if stdout is a terminal
if [[ -t 1 ]]; then
BOLD=$(tput bold)
UL=$(tput sgr 0 1)
YEP="$BOLD$(tput setaf 2)" # bold, green
NOPE="$BOLD$(tput setaf 1)" # bold, red
RESET=$(tput sgr0)
else
BOLD=; UL=; YEP=; NOPE=; RESET=
fi
ERROR="${NOPE}ERROR$RESET"
USAGE="
$BOLD$ME$RESET - test a command with all tags for a specific Docker image
${UL}usage$RESET
$(basename $0) [-h|--help] [-c|--cleanup] [-i|--image IMG] cmd [args...]
${UL}where$RESET
-h, --help prints this help ;-)
-c, --cleanup removes downloaded Docker images when we're through
-i, --image IMG uses a different Docker image from the default ($IMAGE)
-n, --dry-run don't run anything, just print what would happen
-q, --quiet don't print progress; don't print commands to be run
Optionally, set DOCKER_IMAGE_TEST_IMAGE or DOCKER_IMAGE_TEST_CLEANUP in
the environment to get the same behavior as '-c' and '-i'; to use a shell
besides $IMAGE_SHELL to run the command, set DOCKER_IMAGE_TEST_SHELL.
${UL}problems?$RESET
Report bugs at $GIST
"
cleanup=$CLEANUP
image=$IMAGE
dryrun=
# default to printing tag fetch progress and command to be run for each image
verbose=1
shell=$IMAGE_SHELL
tags=()
cmd=()
endofopts=
json=$(mktemp)
trap "\\rm '$json'" EXIT
# make sure all necessary programs are available
for passenger in "${MANIFEST[@]}"; do
if ! type "$passenger" >/dev/null 2>&1; then
echo "$ERROR: Missing required program '$passenger'." >&2
exit 1
fi
done
while (( $# )); do
case $1 in
-*)
# '--' signals end of options for this script to process itself
if [[ $1 == "--" ]]; then
endofopts=1; shift; continue
fi
if [[ $endofopts ]]; then
cmd+=("$1")
else
case $1 in
-h|--help)
echo "$USAGE"
exit
;;
-i|--image*)
if [[ $2 ]]; then
shift
image=$1
elif [[ $1 =~ --image=(.*) ]]; then
image=${BASH_REMATCH[1]}
else
echo "$ERROR: -i / --image requires an argument" >&2
exit 1
fi
;;
-c|--cleanup)
cleanup=1
;;
-n|--dryrun|--dry-run)
dryrun=1
;;
-q|--quiet)
verbose=
;;
*)
cmd+=("$1")
;;
esac
fi
;;
*)
cmd+=("$1")
;;
esac
shift
done
if [[ -z ${cmd:-} ]]; then
echo "$USAGE" >&2
echo "$ERROR: Expected (at least) a command to run." >&2
exit 1
fi
echo -n "Enumerating Docker registry tags for '$image'" >&2
readarray -t tags < <(
for (( i=1; $?==0; i++ )); do
# I thought you /used/ to be able keep going until '?page=$i' query
# string returned a 404, but now you have to check .next in the
# returned JSON
curl -sL "$REGISTRY/$image/tags/?page=$i" > "$json"
echo -n . >&2
jq -r '.results[]["name"]' "$json"
# returns non-zero if .next is null
jq -e .next "$json" >/dev/null 2>&1
done \
| grep -v devel \
| $SORT -V
)
echo -e " found ${#tags[*]} tags." >&2
(( verbose )) && echo >&2
for tag in "${tags[@]}"; do
# otherwise you have to hammer Ctrl+C a bunch of times
trap break INT
if (( dryrun )); then
echo docker run --rm "$image:$tag" "$shell" -c "${cmd[*]}" >&2
else
if (
(( verbose )) && set -x;
docker run --rm "$image:$tag" "$shell" -c "${cmd[*]}" 2>/dev/null
);
then
echo "$YEP$tag$RESET: test '${cmd[*]}' passed"
else
echo "$NOPE$tag$RESET: test '${cmd[*]}' failed"
fi
(( verbose )) && echo
(( cleanup )) && docker rmi bash:$tag 2>&-
fi
done
@ernstki
Copy link
Author

ernstki commented Mar 14, 2021

Here's an example from https://unix.stackexchange.com/a/464650/278323

When did you start being able to

declare -A a=([one]=1 [two]=2) 
adef=$(declare -p a) 

# copy associative array 'a' to 'b' using the output of 'declare -p a'
# chopped off at the '='
declare -A b=${adef#*=}

as opposed to the "longhand" form using eval?

eval "declare -A b=${adef#*=}" 

The answer is Bash 4.4, which you can determine with:

./docker-image-test.sh \
    set -e \; \
    declare -A 'a=([one]=1 [two]=2)' \; declare -p a \; \
    adef='$(declare -p a)' \; \
    declare -A 'b=${adef#*=}' \; declare -p b

Bash up to 4.0.x would either return an error (because associative arrays weren't supported), and up to 4.3.x would assign the string value of $adef to the numerical index ${b[0]}, yielding a result like this:

declare -A b='([0]="'\''([one]=\"1\" [two]=\"2\" )'\''" )'

Starting at 4.4, you'll see this (intended) result instead

declare -A b=([two]="2" [one]="1" )

The workaround using eval works on all Bashes 4.x and above, which have support for associative arrays. So if you need to support, say, Bash 4.2.something on CentOS 7, stick with the eval form.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment