#!/bin/bash
#
# runformat - format this project's C++ sources with Uncrustify.
#
# Usage:
# ./runformat # format using the configured Uncrustify
# ./runformat --install # download, build, and use Uncrustify locally
# ./runformat --install --install-dir /abs/path
# ./runformat --expected-uncrustify-version # print ONLY the expected Uncrustify version
#
# You may also set:
# UNCRUSTIFY=/abs/path/to/uncrustify # use a specific binary
# UNCRUSTIFY_INSTALL_DIR=/abs/path # where `--install` will install
#
# Requirements:
# - All developers must use the *exact* same Uncrustify version to avoid format churn.
# - Either:
# * Have `uncrustify` in PATH, or
# * Set env var UNCRUSTIFY=/absolute/path/to/uncrustify, or
# * Run `./runformat --install` to fetch & build the pinned version locally.
#
# Notes:
# - The local install lives under: ./.runformat-uncrustify/uncrustify--install
# - The config file is expected at: ./.uncrustify.cfg
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
UNCRUSTIFY_VERSION="0.80.1"
UNCRUSTIFY_HASH="6bf662e05c4140dd4df5e45d6690cad96b4ef23c293b85813f5c725bbf1894d0"
UNCRUSTIFY_WORK_DIR="${SCRIPT_DIR}/.runformat-uncrustify"
# Allow external install dir override (arg or env). If not set, default under work dir.
DEFAULT_INSTALL_DIR="${UNCRUSTIFY_WORK_DIR}/uncrustify-${UNCRUSTIFY_VERSION}-install"
UNCRUSTIFY_INSTALL_DIR="${UNCRUSTIFY_INSTALL_DIR:-$DEFAULT_INSTALL_DIR}"
UNCRUSTIFY_BIN="${UNCRUSTIFY_INSTALL_DIR}/bin/uncrustify"
# Allow override via env; default to local pinned build path.
UNCRUSTIFY="${UNCRUSTIFY:-$UNCRUSTIFY_BIN}"
UNCRUSTIFY_CONFIG="${SCRIPT_DIR}/.uncrustify.cfg"
err() { echo -e >&2 "ERROR: $@\n"; }
die() { err $@; exit 1; }
install_uncrustify() {
local root="uncrustify-${UNCRUSTIFY_VERSION}"
local file="${root}.tar.gz"
local url="https://github.com/uncrustify/uncrustify/releases/download/${root}/${file}"
mkdir -p "${UNCRUSTIFY_WORK_DIR}"
echo "Downloading ${file}..."
curl -fsSL -o "${UNCRUSTIFY_WORK_DIR}/${file}" "${url}"
(
cd "${UNCRUSTIFY_WORK_DIR}"
echo "${UNCRUSTIFY_HASH} ${file}" > "${file}.sha256"
sha256sum -c "${file}.sha256"
rm -f "${file}.sha256"
command -v cmake >/dev/null 2>&1 || die "cmake executable not found."
echo "Extracting archive..."
rm -rf "${root}" "${root}-build"
mkdir -p "${root}"
tar -xzf "${file}" --strip-components=1 -C "${root}"
echo "Configuring (prefix: ${UNCRUSTIFY_INSTALL_DIR})..."
cmake \
-DCMAKE_BUILD_TYPE:STRING=Release \
-DCMAKE_INSTALL_PREFIX:PATH="${UNCRUSTIFY_INSTALL_DIR}" \
-S "${root}" -B "${root}-build"
echo "Building & installing..."
cmake --build "${root}-build" --config Release --target install --parallel
)
echo "Installed Uncrustify to: ${UNCRUSTIFY_INSTALL_DIR}"
}
print_usage_and_exit() {
sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//'
exit 0
}
# Print ONLY expected Uncrustify version (no extra text).
print_expected_uncrustify_version_and_exit() {
printf '%s\n' "$UNCRUSTIFY_VERSION"
exit 0
}
# --------------------------
# Argument parsing
# --------------------------
DO_INSTALL=0
PRINT_EXPECTED_UNCRUSTIFY_VERSION=0
# Accept: --install, --install-dir , -h/--help
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
print_usage_and_exit
;;
--install)
DO_INSTALL=1
shift
;;
--install-dir)
[[ $# -ge 2 ]] || die "$1 requires a path argument"
UNCRUSTIFY_INSTALL_DIR="$(readlink -m "$2" 2>/dev/null || realpath -m "$2")"
UNCRUSTIFY_BIN="${UNCRUSTIFY_INSTALL_DIR}/bin/uncrustify"
# Only update UNCRUSTIFY default if user hasn't explicitly set it
if [[ "${UNCRUSTIFY:-}" != "${UNCRUSTIFY_BIN}" ]]; then
UNCRUSTIFY="${UNCRUSTIFY_BIN}"
fi
shift 2
;;
--expected-uncrustify-version)
PRINT_EXPECTED_UNCRUSTIFY_VERSION=1
shift
;;
*)
# ignore unrecognized positional args for now
shift
;;
esac
done
if [[ "$DO_INSTALL" -eq 1 ]]; then
install_uncrustify
# Ensure we use the freshly installed binary for this run
UNCRUSTIFY="$UNCRUSTIFY_BIN"
fi
# If requested, print ONLY the expected Uncrustify version and exit.
if [[ "$PRINT_EXPECTED_UNCRUSTIFY_VERSION" -eq 1 ]]; then
print_expected_uncrustify_version_and_exit
fi
# --------------------------
# Validate & run
# --------------------------
# Check Uncrustify availability
if ! command -v "$UNCRUSTIFY" >/dev/null 2>&1; then
err "Uncrustify executable not found: $UNCRUSTIFY"
die "Add it to PATH, set UNCRUSTIFY=/path/to/uncrustify, or run: $0 --install [--install-dir DIR]"
fi
# Version check
DETECTED_VERSION="$("$UNCRUSTIFY" --version 2>&1 | grep -oE '[0-9]+(\.[0-9]+)*' | head -n1 || true)"
echo "Detected Uncrustify: ${DETECTED_VERSION:-unknown}"
if [[ "$DETECTED_VERSION" != "${UNCRUSTIFY_VERSION}" ]]; then
die "Expected Uncrustify ${UNCRUSTIFY_VERSION}. Re-run with --install (and optionally --install-dir) or set UNCRUSTIFY."
fi
# Config check
[[ -f "$UNCRUSTIFY_CONFIG" ]] || die "Uncrustify config not found at: $UNCRUSTIFY_CONFIG"
# Run formatter
echo "Running formatter..."
$UNCRUSTIFY -c "$UNCRUSTIFY_CONFIG" -l CPP --no-backup --replace *.cpp *.h
# Show diff and fail if changes exist
echo "Checking for formatting changes..."
git diff --exit-code || {
echo
echo "Formatting changes were applied. Please review and commit."
exit 1
}
echo "Formatting is clean."