4
\$\begingroup\$

I have written a simple wrapper for docker build and docker run that allows me to build and run my image without having to use the docker command directly.

Here's the layout of my code:

esp8266
├── scripts
│   ├── config.sh  <- configuration
│   └── util.sh    <- utility functions
├── docker         <- docker image root
│   └── Dockerfile
├── build          <- script to build image
└── make           <- script to run image

And the files themselves:

scripts/config.sh

readonly DOCKER_IMAGE_NAME="jackwilsdon/esp8266"
readonly DOCKER_IMAGE_TAG="0.1.0"
readonly DOCKER_IMAGE_MOUNT="/mnt/data"

readonly DOCKER_LOCAL_SOURCE_DIRECTORY="docker"
readonly DOCKER_LOCAL_MOUNT_DIRECTORY="data"

scripts/util.sh

#!/usr/bin/env bash
#
# Utilities for esp8266 development container scripts

# usage: print [args...]
print() {
  echo "${CURRENT_SCRIPT}: $*"
}

# usage: print_raw [args...]
print_raw() {
  echo "$*"
}

# usage: error [args...]
error() {
  print "error: $*" >&2
}

# usage: error_raw [args...]
error_raw() {
  print_raw "$*" >&2
}

# usage: check_requirements
check_requirements() {
  if [[ -z "${CURRENT_SCRIPT}" ]]; then
    error "CURRENT_SCRIPT not set"
    exit 1
  fi

  if [[ -z "${CURRENT_DIRECTORY}" ]]; then
    error "CURRENT_DIRECTORY not set"
    exit 1
  fi

  if [[ -z "${DOCKER_BINARY}" ]]; then
    error "unable to find docker (DOCKER_BINARY not set)"
    return 1
  fi
}

# usage: docker [args...]
docker() {
  "${DOCKER_BINARY}" "$@"

  local exit_code="$?"

  if [[ "${exit_code}" -ne 0 ]]; then
    error "${DOCKER_BINARY} exited with code ${exit_code}"
    return 1
  fi
}

# usage: arg_or_default arg default [new arg value]
arg_or_default() {
  if [[ -n "$1" ]]; then
    if [[ "$#" -eq 3 ]]; then
      echo "$3"
    else
      echo "$1"
    fi
  else
    echo "$2"
  fi
}

build

#!/usr/bin/env bash
#
# Build esp8266 development container

readonly CURRENT_SCRIPT="$(basename -- ${BASH_SOURCE[0]})"
readonly CURRENT_DIRECTORY="$(cd "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly DOCKER_BINARY="$(command -v docker)"

source "${CURRENT_DIRECTORY}/scripts/config.sh"
source "${CURRENT_DIRECTORY}/scripts/util.sh"

# usage: usage [printer]
usage() {
  local printer="$(arg_or_default "$1" 'print_raw')"

  "${printer}" "usage: ${CURRENT_SCRIPT} [-h] [TAG]"
}

# usage: full_usage [printer]
full_usage() {
  local printer="$(arg_or_default "$1" 'print_raw')"

  usage "${printer}"
  "${printer}"
  "${printer}" 'Build tool for esp8266 development container'
  "${printer}"
  "${printer}" 'arguments:'
  "${printer}" '  -h                    show this help message and exit'
  "${printer}" '  TAG                   the tag of the image to build'
}


# usage: build_image [tag]
build_image() {
  # Generate image name
  local name="${DOCKER_IMAGE_NAME}:$(arg_or_default "$1" \
                                                    "${DOCKER_IMAGE_TAG}")"

  print "building image ${name}"

  # Run docker with the provided arguments
  docker build -t "${name}" \
                  "${CURRENT_DIRECTORY}/${DOCKER_LOCAL_SOURCE_DIRECTORY}"
}

# usage: main [-h] [-d DATA_DIRECTORY] [-t TAG] [ARGS...]
main() {
  check_requirements "$@" || exit 1

  while getopts ':h' OPT; do
    case "${OPT}" in
      h)
        full_usage
        exit 0
        ;;
      ?)
        full_usage
        print
        error "invalid argument: ${OPTARG}"
        exit 1
        ;;
    esac
  done

  shift $((OPTIND - 1))

  build_image "$@"
}

if [[ "$0" == "${BASH_SOURCE[0]}" ]]; then
  main "$@"
fi

make

#!/usr/bin/env bash
#
# Run esp8266 development container

readonly CURRENT_SCRIPT="$(basename -- ${BASH_SOURCE[0]})"
readonly CURRENT_DIRECTORY="$(cd "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly DOCKER_BINARY="$(command -v docker)"

source "${CURRENT_DIRECTORY}/scripts/config.sh"
source "${CURRENT_DIRECTORY}/scripts/util.sh"

# usage: usage [printer]
usage() {
  local printer="$(arg_or_default "$1" 'print_raw')"

  "${printer}" "usage: ${CURRENT_SCRIPT} [-h] [-d DATA_DIRECTORY] [-t TAG]" \
               "[ARGS...]"
}

# usage: full_usage [printer]
full_usage() {
  local printer="$(arg_or_default "$1" 'print_raw')"

  usage "${printer}"
  "${printer}"
  "${printer}" 'Make tool for esp8266 development container'
  "${printer}"
  "${printer}" 'arguments:'
  "${printer}" '  -h                    show this help message and exit'
  "${printer}" '  -d DATA_DIRECTORY     mount point for /mnt/data inside the'
  "${printer}" '                        container'
  "${printer}" '  -t TAG                the tag of the image to run'
  "${printer}" '  ARGS...               the command to run in the container'
}


# usage: run_image [data_directory] [tag] [args...]
run_image() {
  local arguments=(run --rm --interactive --tty --entrypoint /sbin/my_init)

  # Generate volume and image names
  local volume="$(arg_or_default "$1" \
               "${CURRENT_DIRECTORY}/${DOCKER_LOCAL_MOUNT_DIRECTORY}")"
  local name="${DOCKER_IMAGE_NAME}:$(arg_or_default "$2" \
                                                    "${DOCKER_IMAGE_TAG}")"

  # Add volume and name to arguments
  arguments+=(-v "${volume}:/mnt/data" ${name})

  # Remove the first two arguments from the argument list
  shift 2

  # Run docker with the provided arguments
  docker "${arguments[@]}" -- "$@"
}

# usage: main [-h] [-d DATA_DIRECTORY] [-t TAG] [ARGS...]
main() {
  check_requirements "$@" || exit 1

  local docker_volume=''
  local docker_image_tag=''

  while getopts ':hd:t:' OPT; do
    case "${OPT}" in
      h)
        full_usage
        exit 0
        ;;
      d)
        docker_volume="${OPTARG}"
        ;;
      t)
        docker_image_tag="${OPTARG}"
        ;;
      ?)
        full_usage error_raw
        error_raw
        error "invalid argument: ${OPTARG}"
        exit 1
        ;;
    esac
  done

  shift $((OPTIND - 1))

  run_image "${docker_volume}" "${docker_image_tag}" "$@"
}

if [[ "$0" == "${BASH_SOURCE[0]}" ]]; then
  main "$@"
fi

This image is primarily designed for development and ease of use, which is why I have created these shell scripts.

Here's some examples of how to use the scripts:

$ ./build -h
usage: build [-h] [TAG]

Build tool for esp8266 development container

arguments:
  -h                    show this help message and exit
  TAG                   the tag of the image to build
$ ./build example_tag
build: building image jackwilsdon/esp8266:example_tag
Sending build context to Docker daemon 3.584 kB
Step 1 : FROM phusion/baseimage:latest
 ---> c39664f3d4e5
Step 2 : MAINTAINER Jack Wilsdon <[email protected]>
 ---> Using cache
 ---> 4ec300220c6c
...
Step 7 : VOLUME /mnt/data
 ---> Using cache
 ---> f9f8a21a0d16
Successfully built f9f8a21a0d16
$ ./make -h
usage: make [-h] [-d DATA_DIRECTORY] [-t TAG] [ARGS...]

Make tool for esp8266 development container

arguments:
  -h                    show this help message and exit
  -d DATA_DIRECTORY     mount point for /mnt/data inside the
                        container
  -t TAG                the tag of the image to run
  ARGS...               the command to run in the container
$ ./make whoami
*** Running /etc/my_init.d/00_regen_ssh_host_keys.sh...
*** Running /etc/rc.local...
*** Booting runit daemon...
*** Runit started as PID 9
*** Running whoami...
root
*** whoami exited with status 0.
*** Shutting down runit daemon (PID 9)...
*** Killing all processes...

I'm rather new to shell scripting, however I'd like to ensure my scripts are as robust as possible, is there anything I can do to improve their robustness?

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Overloading the meaning of make might come back and bite you one day. \$\endgroup\$
    – tripleee
    Commented Jan 11, 2017 at 11:51

2 Answers 2

2
\$\begingroup\$

Looking very good. Honestly I only have nitpicks.


The purpose of the 3rd argument of arg_or_default is unclear. From the comment in the code I don't understand it, and it's not used by any of the scripts. Either explain it better in the comment, or drop it, since you're not using it anyway.


The == operator in [[ is for pattern matching. When you don't need to match a pattern but a literal string, as in [[ "$0" == "${BASH_SOURCE[0]}" ]], then you can replace it with a single =.


You don't need to double-quote the $? variable, it's always safe.

You don't need to quote string literals like print_raw.

You don't need the single-quote pairs when setting a variable to empty as in docker_volume='', you can write simply docker_volume=

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the feedback! I didn't know you could omit the quotes for empty variables, cool! \$\endgroup\$ Commented Sep 27, 2016 at 23:10
2
\$\begingroup\$

This is excellent. My only suggestion would be to replace:

readonly CURRENT_SCRIPT="$(basename -- ${BASH_SOURCE[0]})"

with:

readonly CURRENT_SCRIPT="$(basename -- "${BASH_SOURCE[0]}")"

to avoid basename misbehaving if the script name contains a whitespace.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.