#!/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 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."
}
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
}
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"
}
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
}
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"
}
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 "$@"