shoreman.sh

#!/bin/bash

shoreman is an implementation of the Procfile format. Inspired by the original foreman tool for ruby.

Make sure that any errors cause the script to exit immediately.

set -eo pipefail
[[ "$TRACE" ]] && set -x

Usage

Usage message that is displayed when --help is given as an argument.

usage() {
  echo "Usage: shoreman [procfile|Procfile] [envfile|.env]"
  echo "Run Procfiles using shell."
  echo
  echo "The shoreman script reads commands from [procfile] and starts up the"
  echo "processes that it describes."
}

Logging

For logging we want to prefix each entry with the current time, as well as the process name. This takes two arguments, the name of the process with its index, and then reads data from stdin, formats it, and sends it to stdout.

log() {
  local index="$2"
  local format="%s %s\t| %s"

We add colors when output is a terminal. SHOREMAN_COLORS can override it.

  if [ -t 1 -o "$SHOREMAN_COLORS" == "always" ] \
     && [ "$SHOREMAN_COLORS" != "never" ]; then

Bash colors start from 31 up to 37. We calculate what color the process gets based on its index.

    local color="$((31 + (index % 7)))"
    format="\033[0;${color}m%s %s\t|\033[0m %s"
  fi

  while read -r data
  do
    printf "$format\n" "$(date +"%H:%M:%S")" "$1" "$data"
  done
}

Running commands

When a process is started, we want to keep track of its pid so we can kill it when the parent process receives a signal, and so we can wait for it to finish before exiting the parent process.

store_pid() {
  pids="$pids $1"
}

This starts a command asynchronously and stores its pid in a list for use later on in the script.

start_command() {
  bash -c "$1" 2>&1 | log "$2" "$3" &
  pid="$(jobs -p %%)"
  store_pid "$pid"
}

Reading the .env file

The .env file needs to be a list of assignments like in a shell script. Shell-style comments are permitted.

load_env_file() {
  local env_file=${1:-'.env'}

Set a default port before loading the .env file

  export PORT=${PORT:-5000}

  if [[ -f "$env_file" ]]; then
    export $(grep "^[^#]*=.*" "$env_file" | xargs)
  fi
}

Reading the Procfile

The Procfile needs to be parsed to extract the process names and commands. The file is given on stdin, see the < at the end of this while loop.

run_procfile() {
  local procfile=${1:-'Procfile'}

We give each process an index to track its color. We start with 1, because it corresponds to green which is easier on the eye than red (0).

  local index=1
  while read line || [[ -n "$line" ]]; do
    if [[ -z "$line" ]] || [[ "$line" == \#* ]]; then continue; fi
    local name="${line%%:*}"
    local command="${line#*:[[:space:]]}"
    start_command "$command" "${name}" "$index"
    echo "'${command}' started with pid $pid" | log "${name}" "$index"
    index=$((index + 1))
  done < "$procfile"
}

Cleanup

When a SIGINT, SIGTERM or EXIT is received, this action is run, killing the child processes. The sleep stops STDOUT from pouring over the prompt, it should probably go at some point.

onexit() {
  echo "SIGINT received"
  echo "sending SIGTERM to all processes"
  kill $pids
  sleep 1
}

main() {
  local procfile="$1"
  local env_file="$2"

If the --help option is given, show the usage message and exit.

  expr -- "$*" : ".*--help" >/dev/null && {
    usage
    exit 0
  }

  load_env_file "$env_file"
  run_procfile "$procfile"

  trap onexit INT TERM

Wait for the children to finish executing before exiting.

  wait $pids
}

main "$@"
h