Untitled
unknown
plain_text
a month ago
23 kB
9
No Index
in a year
#!/bin/bash
set -euo pipefail
###############################################################################
# startdocker-all.sh
# Maintainer: user
# Date: 2025-09-16
# Version: 1.18
###############################################################################
# --------------------------- config ------------------------------------------
# Show timestamps in logs (true or false)
SHOW_TIMESTAMPS=false
# Write output to a logfile as well (true or false)
LOG_TO_FILE=false
LOGFILE="/var/log/startdocker-all.log"
# Journal retention window
JOURNAL_RETENTION="2d"
# Max retries for docker operations per directory
RETRIES=3
RETRY_DELAY=5
# Minimum free disk space percent at Docker root
MIN_FREE_PCT=5
# -----------------------------------------------------------------------------
# Colors
setup_colors() {
if [[ -n "${NO_COLOR:-}" || ! -t 1 ]]; then
RED=""; GREEN=""; YELLOW=""; BLUE=""; CYAN=""; MAGENTA=""; BOLD=""; RESET=""
return
fi
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
BOLD='\033[1m'
RESET='\033[0m'
}
setup_colors
ts_prefix() {
if [[ "${SHOW_TIMESTAMPS}" == "true" ]]; then
printf "%s " "$(date '+%Y-%m-%d %H:%M:%S')"
fi
}
log() { echo -e "${GREEN}$(ts_prefix)$*${RESET}"; }
log_warn() { echo -e "${YELLOW}$(ts_prefix)WARNING: $*${RESET}"; }
log_err() { echo -e "${RED}$(ts_prefix)ERROR: $*${RESET}" >&2; }
line() { echo -e
"${CYAN}------------------------------------------------${RESET}"; }
# Optional file logging
if [[ "${LOG_TO_FILE}" == "true" ]]; then
logdir="$(dirname "$LOGFILE")"
mkdir -p "$logdir" 2>/dev/null || true
if touch "$LOGFILE" 2>/dev/null && [[ -w "$LOGFILE" ]]; then
exec > >(tee -a "$LOGFILE") 2>&1
else
log_warn "Cannot write to $LOGFILE, continuing without file logging."
fi
fi
# Prevent overlap (per-user lock in /tmp)
LOCKFILE="/tmp/startdocker-all.${UID}.lock"
if exec 9>>"$LOCKFILE"; then
if ! flock -n 9 2>/dev/null; then
log_err "Another instance is running (lock: ${LOCKFILE}). Exiting."
exit 1
fi
else
log_warn "Cannot open lock file ${LOCKFILE}; continuing without lock."
fi
log "Starting docker maintenance on host $(hostname -f)"
HOSTNAME_SHORT=$(hostname -s)
declare -a directories
case "$HOSTNAME_SHORT" in
machine1)
directories=(
"/opt/audiobookshelf"
"/opt/overseerr"
"/opt/openwebui"
"/opt/portainer_agent"
"/opt/prowlarr"
"/opt/sabnzbd"
"/opt/tinyMediaManager"
"/opt/Tautulli"
)
;;
machine2)
directories=(
"/opt/booklore"
"/opt/calibre-web"
"/opt/digikam"
"/opt/docker-handbrake"
"/opt/ebook2audiobook"
"/opt/freshrss"
"/opt/handbrake-web"
"/opt/immich"
"/opt/karakeep-app"
"/opt/paperless-ngx"
"/opt/photoprism"
"/opt/portainer"
"/opt/portainer_agent"
"/opt/speedtest-tracker"
"/opt/syncthing"
)
;;
monitor)
directories=(
"/opt/docker-handbrake"
"/opt/dockmon"
"/opt/handbrake-web"
"/opt/immich-remote-learning"
"/opt/portainer_agent"
)
;;
worker)
directories=(
"/opt/docker-handbrake"
"/opt/handbrake-web"
"/opt/immich-remote-learning"
)
;;
blue)
directories=(
"/opt/bazarr"
"/opt/docker-handbrake"
"/opt/emby"
"/opt/FlareSolverr"
"/opt/handbrake-web"
"/opt/immich-remote-learning"
"/opt/portainer_agent"
)
;;
*)
log_err "Unknown host: $HOSTNAME_SHORT. Exiting."
exit 1
;;
esac
# Preflight
if ! command -v docker >/dev/null 2>&1; then
log_err "Docker is not installed or not in PATH."
exit 1
fi
if ! docker info >/dev/null 2>&1; then
log_err "Docker daemon is not reachable. Is the service running?"
exit 1
fi
if ! docker compose version >/dev/null 2>&1; then
log_err "Docker Compose plugin not available."
exit 1
fi
# Disk space info
DOCKER_ROOT=$(docker info --format '{{ .DockerRootDir }}' 2>/dev/null || echo
"/var/lib/docker")
if [[ -d "$DOCKER_ROOT" ]]; then
USE_PCT=$(df -P "$DOCKER_ROOT" | awk 'NR==2 {gsub("%","",$5); print $5}')
FREE_PCT=$((100 - USE_PCT))
if (( FREE_PCT < MIN_FREE_PCT )); then
log_warn "Free space at $DOCKER_ROOT: ${FREE_PCT}% (below
${MIN_FREE_PCT}%)."
else
log "Free space at $DOCKER_ROOT: ${BOLD}${FREE_PCT}%${RESET}"
fi
fi
# ---------- size helpers (for summary) ----------
to_bytes() {
local s="${1:-0B}"
local num unit
num="$(sed -E 's/^([0-9.]+).*/\1/' <<<"$s")"
unit="$(sed -E 's/^[0-9.]+ *([A-Za-z]+).*/\1/' <<<"$s" | tr '[:upper:]'
'[:lower:]')"
[[ -z "$num" ]] && num=0
case "$unit" in
b|"") awk -v n="$num" 'BEGIN{printf "%.0f", n}' ;;
kb) awk -v n="$num" 'BEGIN{printf "%.0f", n*1000}' ;;
kib) awk -v n="$num" 'BEGIN{printf "%.0f", n*1024}' ;;
mb) awk -v n="$num" 'BEGIN{printf "%.0f", n*1000*1000}' ;;
mib) awk -v n="$num" 'BEGIN{printf "%.0f", n*1024*1024}' ;;
gb) awk -v n="$num" 'BEGIN{printf "%.0f", n*1000*1000*1000}' ;;
gib) awk -v n="$num" 'BEGIN{printf "%.0f", n*1024*1024*1024}' ;;
tb) awk -v n="$num" 'BEGIN{printf "%.0f", n*1000*1000*1000*1000}' ;;
tib) awk -v n="$num" 'BEGIN{printf "%.0f", n*1024*1024*1024*1024}' ;;
*) awk -v n="$num" 'BEGIN{printf "%.0f", n}' ;;
esac
}
fmt_bytes() {
local b="${1:-0}"
local -a u=(B KiB MiB GiB TiB)
local i=0
while (( b >= 1024 && i < ${#u[@]}-1 )); do b=$((b/1024)); ((i++)); done
printf "%d%s" "$b" "${u[$i]}"
}
extract_reclaimed_sizes() {
sed -nE 's/.*Total (reclaimed space|space reclaimed):
*([0-9.]+[[:space:]]*[A-Za-z]+).*/\2/ip'
}
sum_reclaimed_from_text() {
local out="$1" total=0
while IFS= read -r sz; do
(( total += $(to_bytes "$sz") ))
done < <(extract_reclaimed_sizes <<<"$out")
echo "$total"
}
sum_freed_from_journal_output() {
local out="$1" total=0
while IFS= read -r sz; do
(( total += $(to_bytes "$sz") ))
done < <(echo "$out" | sed -nE
's/.*freed[[:space:]]+([0-9.]+[[:space:]]*[A-Za-z]+).*/\1/p')
echo "$total"
}
DOCKER_RECLAIMED_BYTES=0
JOURNAL_FREED_BYTES=0
# ---------- prune (capture outputs for summary) ----------
log "Running docker prune tasks"
prune_and_report() {
local name="$1"; shift
local out; out="$("$@" 2>&1 || true)"
local bytes; bytes="$(sum_reclaimed_from_text "$out")"
echo -e "${BLUE}${name}${RESET}: reclaimed $(fmt_bytes "${bytes}")"
echo "$out" | awk 'NF>0 && !/Total (reclaimed space|space reclaimed):/{print}'
echo
DOCKER_RECLAIMED_BYTES=$(( DOCKER_RECLAIMED_BYTES + bytes ))
}
prune_and_report "Images" docker image prune -f
prune_and_report "Containers" docker container prune -f
prune_and_report "Volumes" docker volume prune -f
#prune_and_report "Networks" docker network prune -f
#prune_and_report "Builder" docker builder prune -f --filter 'until=24h'
# Journal cleanup with summary capture
JOURNAL_SUMMARY_MODE="skipped"
JOURNAL_SUMMARY_NOTE="journalctl not found"
clean_journal() {
local retention="${JOURNAL_RETENTION:-2d}"
if ! command -v journalctl >/dev/null 2>&1; then
log "journalctl not found. Skipping journal vacuum."
JOURNAL_SUMMARY_MODE="skipped"; JOURNAL_SUMMARY_NOTE="journalctl not found"
return 0
fi
if [[ $EUID -eq 0 ]]; then
log "Cleaning system journal logs, keeping ${BOLD}${retention}${RESET}"
jout="$(journalctl --vacuum-time="${retention}" 2>&1 || true)"
echo "$jout"
JOURNAL_FREED_BYTES=$(sum_freed_from_journal_output "$jout")
if [[ -n "$jout" ]]; then
JOURNAL_SUMMARY_MODE="system"; JOURNAL_SUMMARY_NOTE="via root, retention
${retention}"
else
JOURNAL_SUMMARY_MODE="failed"; JOURNAL_SUMMARY_NOTE="system vacuum error"
fi
return 0
fi
if command -v sudo >/dev/null 2>&1; then
log "Cleaning system journal logs with sudo, keeping
${BOLD}${retention}${RESET}"
jout="$(sudo -n journalctl --vacuum-time="${retention}" 2>&1 || true)"
if grep -q . <<<"$jout"; then
echo "$jout"
JOURNAL_FREED_BYTES=$(sum_freed_from_journal_output "$jout")
JOURNAL_SUMMARY_MODE="system"; JOURNAL_SUMMARY_NOTE="via sudo, retention
${retention}"
return 0
else
log "Passwordless sudo not allowed for journalctl (or it failed). Falling
back to user journal."
fi
else
log "sudo not available. Falling back to user journal."
fi
log "Cleaning user journal logs, keeping ${BOLD}${retention}${RESET}"
jout="$(journalctl --user --vacuum-time="${retention}" 2>&1 || true)"
echo "$jout"
JOURNAL_FREED_BYTES=$(sum_freed_from_journal_output "$jout")
if grep -q . <<<"$jout"; then
JOURNAL_SUMMARY_MODE="user"; JOURNAL_SUMMARY_NOTE="retention ${retention}"
else
JOURNAL_SUMMARY_MODE="failed"; JOURNAL_SUMMARY_NOTE="user vacuum error"
fi
}
clean_journal
# ---------- helpers ----------
declare -A BEFORE_TAG_BY_REPO # key: "dir|repo" -> tag
declare -A AFTER_TAG_BY_REPO # key: "dir|repo" -> tag
declare -A BEFORE_ID_BY_REPO # key: "dir|repo" -> image ID
declare -A AFTER_ID_BY_REPO # key: "dir|repo" -> image ID
declare -A BEFORE_VER_BY_REPO # key: "dir|repo" -> version label
declare -A AFTER_VER_BY_REPO # key: "dir|repo" -> version label
declare -A DIR_STATUS # processed|skipped|no-compose|failed
declare -A SERVICE_URLS # key: "dir|service" -> "http://host:port[,
http://host:port2...]"
k() { echo "$1|$2"; }
split_repo_tag() {
local full="$1"
local no_digest="${full%%@*}"
local repo="$no_digest"
local tag="latest"
if [[ "$no_digest" == *:* ]]; then
tag="${no_digest##*:}"
repo="${no_digest%:*}"
fi
echo "$repo|$tag"
}
list_compose_images() {
if docker compose config --images >/dev/null 2>&1; then
docker compose config --images | awk 'NF>0'
else
docker compose config 2>/dev/null | awk
'/^[[:space:]]*image:[[:space:]]*/{print $2}'
fi
}
has_compose_file() {
[[ -f "docker-compose.yml" || -f "docker-compose.yaml" || -f "compose.yml" ||
-f "compose.yaml" ]]
}
compose_retry() {
local tries=0
local desc=$1
shift
until "$@" ; do
tries=$((tries + 1))
if (( tries >= RETRIES )); then
log_err "Failed: ${desc} after ${RETRIES} attempts"
return 1
fi
log_warn "Retry ${tries}/${RETRIES}: ${desc}. Sleeping ${RETRY_DELAY}s."
sleep "${RETRY_DELAY}"
done
return 0
}
# Image ID and label helpers
image_id_of() {
local img="$1"
docker image inspect "$img" --format '{{.Id}}' 2>/dev/null || echo ""
}
image_label_version_of() {
local img="$1"
docker image inspect "$img" --format '{{index .Config.Labels
"org.opencontainers.image.version"}}' 2>/dev/null || echo ""
}
check_stack_health() {
local dir="$1"
local stack
stack="$(basename "$dir")"
# Let containers settle for healthchecks
sleep 5
# List services for this compose stack
mapfile -t services < <(docker compose ps --services 2>/dev/null || true)
if ((${#services[@]} == 0)); then
log_warn "${stack}: no services reported for health check"
return
fi
local unhealthy=0
local svc
local host
host="$HOSTNAME_SHORT"
for svc in "${services[@]}"; do
# Container IDs for this service
mapfile -t containers < <(docker compose ps -q "$svc" 2>/dev/null || true)
if ((${#containers[@]} == 0)); then
log_warn "${stack}: service ${svc} has no running containers"
unhealthy=1
continue
fi
local svc_unhealthy=0
local cid
# Deduplicate ports per service
local -A svc_ports=()
for cid in "${containers[@]}"; do
# Inspect status and healthcheck
local inspect status health
inspect="$(docker inspect --format '{{.State.Status}} {{if
.State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' "$cid"
2>/dev/null || echo "unknown unknown")"
read -r status health <<<"$inspect"
# Get published host ports (no jq)
local hostports
hostports="$(docker inspect --format '{{range $p, $bindings :=
.NetworkSettings.Ports}}{{if $bindings}}{{range $b := $bindings}}{{$b.HostPort}}
{{end}}{{end}}{{end}}' "$cid" 2>/dev/null || true)"
if [[ -n "$hostports" ]]; then
for port in $hostports; do
# Only record a given port once per service
if [[ -z "${svc_ports[$port]+x}" ]]; then
svc_ports["$port"]=1
local url="http://${host}:${port}"
log "${stack}: service ${svc} -> ${url}"
fi
done
else
log "${stack}: service ${svc} has no published ports"
fi
# Health logic
if [[ "$status" != "running" ]]; then
log_warn "${stack}: service ${svc} container ${cid:0:12} not running
(state '${status}')"
svc_unhealthy=1
continue
fi
# Treat "starting" as informational, not a failure
if [[ "$health" == "starting" ]]; then
log "${stack}: service ${svc} container ${cid:0:12} health status
'starting' (still initializing)"
elif [[ "$health" != "healthy" && "$health" != "no-healthcheck" ]]; then
log_warn "${stack}: service ${svc} container ${cid:0:12} health status
'${health}'"
svc_unhealthy=1
fi
done
# Save URLs for summary, if any
local urls=""
local port
for port in "${!svc_ports[@]}"; do
local url="http://${host}:${port}"
if [[ -z "$urls" ]]; then
urls="${url}"
else
urls="${urls}, ${url}"
fi
done
if [[ -n "$urls" ]]; then
SERVICE_URLS["$dir|$svc"]="$urls"
fi
if (( svc_unhealthy != 0 )); then
unhealthy=1
fi
done
if (( unhealthy == 0 )); then
log "${stack}: all services running and healthy"
fi
}
# ---------- main ----------
for dir in "${directories[@]}"; do
if [[ ! -d "$dir" ]]; then
log_err "Directory missing: $dir"
DIR_STATUS["$dir"]="skipped"
line
continue
fi
echo -e "${MAGENTA}${BOLD}Processing:${RESET} ${BOLD}${BLUE}$dir${RESET}"
pushd "$dir" >/dev/null || { log_err "Cannot enter: $dir";
DIR_STATUS["$dir"]="skipped"; line; continue; }
if ! has_compose_file; then
log_warn "No compose file. Skipping."
DIR_STATUS["$dir"]="no-compose"
popd >/dev/null
line
continue
fi
mapfile -t IMAGES < <(list_compose_images)
for img in "${IMAGES[@]:-}"; do
read -r repo tag <<<"$(split_repo_tag "$img" | tr '|' ' ')"
BEFORE_TAG_BY_REPO["$(k "$dir" "$repo")"]="$tag"
BEFORE_ID_BY_REPO["$(k "$dir" "$repo")"]="$(image_id_of "$img")"
BEFORE_VER_BY_REPO["$(k "$dir" "$repo")"]="$(image_label_version_of "$img")"
done
set +e
compose_retry "docker compose pull in $dir" docker compose pull
rc_pull=$?
compose_retry "docker compose stop in $dir" docker compose stop
rc_stop=$?
compose_retry "docker compose up in $dir" docker compose up -d
--remove-orphans
rc_up=$?
set -e
if (( rc_pull != 0 || rc_stop != 0 || rc_up != 0 )); then
log_err "Compose step failed in $dir"
DIR_STATUS["$dir"]="failed"
else
DIR_STATUS["$dir"]="processed"
check_stack_health "$dir"
fi
mapfile -t IMAGES_AFTER < <(list_compose_images)
for img in "${IMAGES_AFTER[@]:-}"; do
read -r repo tag <<<"$(split_repo_tag "$img" | tr '|' ' ')"
AFTER_TAG_BY_REPO["$(k "$dir" "$repo")"]="$tag"
AFTER_ID_BY_REPO["$(k "$dir" "$repo")"]="$(image_id_of "$img")"
AFTER_VER_BY_REPO["$(k "$dir" "$repo")"]="$(image_label_version_of "$img")"
done
echo -e "${GREEN}Done:${RESET} ${BOLD}${BLUE}$dir${RESET}"
popd >/dev/null
line
done
# ---------- summary ----------
set +e
set +u
echo
echo -e "${BOLD}${CYAN}Run Summary${RESET}"
processed=0; skipped=0; nocompose=0; failed=0; updated=0
declare -a UPD_LINES
declare -a NOCHANGE_STACKS
declare -a SKIPLINES
for dir in "${directories[@]}"; do
status="skipped"
if [[ -n "${DIR_STATUS[$dir]+x}" ]]; then
status="${DIR_STATUS[$dir]}"
fi
case "$status" in
processed) ((processed++)) ;;
skipped) ((skipped++)) ;;
no-compose)((nocompose++)) ;;
failed) ((failed++)) ;;
esac
stack="$(basename "$dir")"
if [[ "$status" != "processed" ]]; then
case "$status" in
skipped) SKIPLINES+=("${BOLD}${BLUE}${stack}${RESET} ${YELLOW}- skipped
(directory missing)${RESET}") ;;
no-compose) SKIPLINES+=("${BOLD}${BLUE}${stack}${RESET} ${YELLOW}- skipped
(no compose file)${RESET}") ;;
failed) SKIPLINES+=("${BOLD}${BLUE}${stack}${RESET} ${RED}- compose
failed${RESET}") ;;
esac
continue
fi
declare -A REPOS=()
for keymap in "${!BEFORE_TAG_BY_REPO[@]}"; do
d="${keymap%%|*}"; r="${keymap#*|}"
[[ "$d" == "$dir" ]] && REPOS["$r"]=1
done
for keymap in "${!AFTER_TAG_BY_REPO[@]}"; do
d="${keymap%%|*}"; r="${keymap#*|}"
[[ "$d" == "$dir" ]] && REPOS["$r"]=1
done
changed_any=0
for repo in "${!REPOS[@]}"; do
key="${dir}|$repo"
old_tag="${BEFORE_TAG_BY_REPO[$key]:-}"
new_tag="${AFTER_TAG_BY_REPO[$key]:-}"
svc="$(basename "$repo")"
svc_fmt="${BOLD}${svc}${RESET}"
if [[ -n "$old_tag" && -n "$new_tag" && "$old_tag" != "$new_tag" ]]; then
((updated++)); changed_any=1
UPD_LINES+=("${GREEN}${BOLD}${stack}${RESET} - ${GREEN}${svc_fmt}
${YELLOW}${old_tag}${RESET} ${GREEN}-> ${new_tag}${RESET}")
else
old_id="${BEFORE_ID_BY_REPO[$key]:-}"
new_id="${AFTER_ID_BY_REPO[$key]:-}"
if [[ -n "$old_id" && -n "$new_id" && "$old_id" != "$new_id" ]]; then
((updated++)); changed_any=1
old_ver="${BEFORE_VER_BY_REPO[$key]:-}"
new_ver="${AFTER_VER_BY_REPO[$key]:-}"
if [[ -n "$old_ver" || -n "$new_ver" ]]; then
if [[ -n "$old_ver" && -n "$new_ver" && "$old_ver" != "$new_ver" ]];
then
UPD_LINES+=("${GREEN}${BOLD}${stack}${RESET} - ${GREEN}${svc_fmt}
Updated image, tag unchanged${RESET} (${YELLOW}${old_ver}${RESET} ${GREEN}->
${new_ver}${RESET}, tag '${new_tag}')")
else
UPD_LINES+=("${GREEN}${BOLD}${stack}${RESET} - ${GREEN}${svc_fmt}
Updated image, tag unchanged${RESET} (version '${new_ver:-${old_ver}}', tag
'${new_tag}')")
fi
else
UPD_LINES+=("${GREEN}${BOLD}${stack}${RESET} - ${GREEN}${svc_fmt}
Updated image, tag unchanged${RESET} (tag '${new_tag}')")
fi
fi
fi
done
if (( changed_any == 0 )); then
NOCHANGE_STACKS+=("${BOLD}${BLUE}${stack}${RESET}")
fi
done
if ((${#NOCHANGE_STACKS[@]} > 0)); then
echo -e "${BOLD}No container version changes${RESET}"
for s in "${NOCHANGE_STACKS[@]}"; do echo -e "${s}"; done
echo
fi
if ((${#UPD_LINES[@]} > 0)); then
echo -e "${BOLD}Container Version Updates${RESET}"
for l in "${UPD_LINES[@]}"; do echo -e "$l"; done
echo
fi
if ((${#SKIPLINES[@]} > 0)); then
echo -e "${BOLD}Skipped / Errors${RESET}"
for x in "${SKIPLINES[@]}"; do echo -e "$x"; done
echo
fi
if ((${#SERVICE_URLS[@]} > 0)); then
echo -e "${BOLD}Service URLs${RESET}"
for dir in "${directories[@]}"; do
stack="$(basename "$dir")"
for key in "${!SERVICE_URLS[@]}"; do
d="${key%%|*}"
svc="${key#*|}"
if [[ "$d" == "$dir" ]]; then
urls="${SERVICE_URLS[$key]}"
echo -e "${BOLD}${BLUE}${stack}${RESET} ${BOLD}${svc}${RESET}: ${urls}"
fi
done
done
echo
fi
echo -e "Processed: ${BOLD}${processed}${RESET} Skipped:
${BOLD}${skipped}${RESET} No compose: ${BOLD}${nocompose}${RESET} Failed:
${BOLD}${failed}${RESET}"
echo -e "Containers updated: ${BOLD}${updated}${RESET}"
echo -e "Docker prune reclaimed: ${BOLD}$(fmt_bytes
"${DOCKER_RECLAIMED_BYTES}")${RESET}"
echo -e "Journal vacuum freed: ${BOLD}$(fmt_bytes
"${JOURNAL_FREED_BYTES}")${RESET}"
case "${JOURNAL_SUMMARY_MODE:-skipped}" in
system) echo -e "Journal cleanup: ${BOLD}system${RESET}
(${JOURNAL_SUMMARY_NOTE:-})" ;;
user) echo -e "Journal cleanup: ${BOLD}user${RESET}
(${JOURNAL_SUMMARY_NOTE:-})" ;;
skipped)echo -e "Journal cleanup: skipped (${JOURNAL_SUMMARY_NOTE:-})" ;;
failed) echo -e "Journal cleanup: ${BOLD}failed${RESET}
(${JOURNAL_SUMMARY_NOTE:-})" ;;
*) echo -e "Journal cleanup: unknown" ;;
esac
echo
log "All done."
# Restore strict mode (no further code expected).
set -u
set -e
Editor is loading...
Leave a Comment