If you've ever done a timed C++ coding assessment on a platform like HackerRank or DevSkiller, you know the friction isn't really the algorithm — it's the loop. Download a zip with a weird filename, unzip it, hunt for the project root, configure CMake, build, run GTest, fix one failing test, repeat... and somewhere in there you've burned ten minutes of your one-hour window just fighting the harness instead of writing code.
These platforms' in-browser editors are fine for quick problems, but for anything involving multiple files (headers, sources, a real test suite), I'd rather work in my own terminal and editor. The catch is that you still have to get the project out of the browser sandbox, build it locally with the exact same toolchain (CMake + GTest), and then package it back up in a way the grader will accept.
So I wrote three small bash scripts to remove that friction entirely. Sharing them here in case they save someone else the same ten minutes.
The workflow
- Download the project archive from the platform (zip or tar.gz, filename is whatever the platform gives you — often randomized)
- Extract it — script 1 handles this regardless of filename or archive type
- Iterate — script 2 configures CMake once, then repeatedly builds and runs GTest, optionally watching for file changes
- Package — script 3 strips build artifacts and any local helper scripts, then zips it back up under a name that won't collide with the original download, ready to re-upload
Script 1: extract_and_setup.sh
Most of these platforms hand you an archive with an unpredictable filename. This script extracts whatever you point it at (.tar, .tgz, .tar.gz, or .zip), figures out which directory it unpacked to by diffing the folder listing before and after, and drops the build script into it automatically.
#!/usr/bin/env bash
# extract_and_setup.sh
# Extracts $fname (tar, tgz, tar.gz, or zip) into the CURRENT folder,
# then copies run_build.sh into the directory that was created.
#
# Usage:
# ./extract_and_setup.sh <fname>
# ./extract_and_setup.sh project-download.tar.gz
set -euo pipefail
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <archive-file>" >&2
exit 1
fi
fname="$1"
if [[ ! -f "$fname" ]]; then
echo "File not found: $fname" >&2
exit 1
fi
# Snapshot directory contents before extracting so we can tell what's new.
before_listing="$(mktemp)"
find . -maxdepth 1 -mindepth 1 -type d | sort > "$before_listing"
echo "==> Extracting $fname into $(pwd)"
case "$fname" in
*.tar.gz|*.tgz)
tar -xzf "$fname"
;;
*.tar)
tar -xf "$fname"
;;
*.zip)
unzip -q "$fname"
;;
*)
echo "Unsupported archive type: $fname" >&2
exit 1
;;
esac
after_listing="$(mktemp)"
find . -maxdepth 1 -mindepth 1 -type d | sort > "$after_listing"
new_dir="$(comm -13 "$before_listing" "$after_listing" | head -n1 | sed 's|^\./||')"
rm -f "$before_listing" "$after_listing"
if [[ -z "$new_dir" ]]; then
echo "Could not determine the extracted directory (archive may not contain a top-level folder)." >&2
exit 1
fi
echo "==> Extracted into: $new_dir"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cp "$script_dir/run_build.sh" "$new_dir/run_build.sh"
chmod +x "$new_dir/run_build.sh"
echo "==> Copied run_build.sh into $new_dir/"
echo "==> Next: cd $new_dir && ./run_build.sh"
A small detail worth calling out: a bare .gz isn't supported here on purpose. Unlike .tar.gz, a plain .gz only ever wraps a single file — it has no concept of a directory tree, so it can't hold a multi-file project (headers, sources, tests, CMakeLists.txt). Assessment platforms package projects as .zip or .tar.gz, never bare .gz, so there was no reason to support a case that can't actually occur.
Script 2: run_build.sh
This is the one you'll run over and over while solving the problem. It configures CMake exactly once (it checks for CMakeCache.txt so repeated runs skip straight to make), builds with all your cores, and runs the GTest binary with colored output.
#!/usr/bin/env bash
# run_build.sh
# Repeatedly: configure (cmake) -> build (make) -> test (gtest).
# Lives inside the project directory; re-run any time after editing code.
#
# Usage:
# ./run_build.sh # configure once if needed, then build+test
# ./run_build.sh --clean # wipe build/ and reconfigure from scratch
# ./run_build.sh --watch # loop: build+test every time a source file changes
set -euo pipefail
CLEAN=0
WATCH=0
for arg in "$@"; do
case "$arg" in
--clean) CLEAN=1 ;;
--watch) WATCH=1 ;;
*) ;;
esac
done
if [[ "$CLEAN" -eq 1 ]]; then
echo "==> Cleaning build directory"
rm -rf build
fi
configure_build_test() {
mkdir -p build
cd build
if [[ ! -f CMakeCache.txt ]]; then
echo "==> Configuring (cmake)"
cmake .. -DCMAKE_BUILD_TYPE=Debug
fi
echo "==> Building (make)"
make -j"$(nproc)"
echo "==> Running tests (gtest)"
./unit_tests --gtest_color=yes
cd ..
}
if [[ "$WATCH" -eq 1 ]]; then
if ! command -v inotifywait >/dev/null 2>&1; then
echo "inotifywait not found. Install it with: sudo apt install -y inotify-tools" >&2
exit 1
fi
echo "==> Watch mode: rebuilding on changes in src/ include/ tests/ (Ctrl+C to stop)"
configure_build_test || true
while inotifywait -r -e modify,create,delete src include tests >/dev/null 2>&1; do
configure_build_test || true
done
else
configure_build_test
fi
The --watch flag (needs inotify-tools: sudo apt install inotify-tools) is the part I use the most. It rebuilds and reruns tests automatically every time you save a file, so you get a tight "edit → see red/green" loop without retyping the build command between every change — useful when you're working through a stub-by-stub implementation under a timer.
The || true after each build call is intentional: without it, set -e would kill the whole watch loop the moment a test fails, which during active development is most of the time. With it, a failing run just gets reported and the watcher keeps going.
Script 3: package_submission.sh
Once ./run_build.sh --clean passes green, you need to get a clean archive back to the platform — generally without your local build/ directory, compiled object files, or the helper scripts you added for your own workflow (those weren't part of the original scaffold, and submitting them is unnecessary noise at best).
This script works on a temporary staging copy, so your live working directory (and run_build.sh in it) is left completely untouched — you can keep developing after packaging without anything having been deleted out from under you.
#!/usr/bin/env bash
# package_submission.sh
# Run this from the directory ONE LEVEL ABOVE your project folder.
# It removes build/ and any compiled artifacts, then zips the project
# back up in a clean, upload-ready archive.
#
# Usage:
# ./package_submission.sh <project_dir>
# ./package_submission.sh my-project
# ./package_submission.sh my-project --format tar.gz # default: zip
#
# Output:
# <project_dir>-submission-ready.zip (or .tar.gz)
# sitting next to <project_dir>, ready to upload.
# (suffixed so it never collides with the original input archive)
set -euo pipefail
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <project_dir> [--format zip|tar.gz]" >&2
exit 1
fi
PROJECT_DIR="$1"
FORMAT="zip"
shift
while [[ $# -gt 0 ]]; do
case "$1" in
--format)
FORMAT="$2"
shift 2
;;
*)
echo "Unknown argument: $1" >&2
exit 1
;;
esac
done
if [[ ! -d "$PROJECT_DIR" ]]; then
echo "Directory not found: $PROJECT_DIR" >&2
exit 1
fi
# Strip trailing slash for clean naming.
PROJECT_DIR="${PROJECT_DIR%/}"
# Work on a throwaway copy so the live project (and your run_build.sh) is untouched.
STAGE_PARENT="$(mktemp -d)"
STAGE_DIR="$STAGE_PARENT/$PROJECT_DIR"
echo "==> Staging a clean copy of $PROJECT_DIR"
cp -r "$PROJECT_DIR" "$STAGE_DIR"
echo "==> Cleaning build artifacts"
rm -rf "${STAGE_DIR:?}/build"
find "$STAGE_DIR" -name "*.o" -delete
find "$STAGE_DIR" -name "CMakeCache.txt" -delete
find "$STAGE_DIR" -name "compile_commands.json" -delete
find "$STAGE_DIR" -type d -name "CMakeFiles" -prune -exec rm -rf {} +
echo "==> Removing local helper scripts (not part of original scaffold)"
rm -f "$STAGE_DIR/run_build.sh" "$STAGE_DIR/build_and_test.sh"
case "$FORMAT" in
zip)
OUT="$(pwd)/${PROJECT_DIR}-submission-ready.zip"
rm -f "$OUT"
echo "==> Zipping into $OUT"
(cd "$STAGE_PARENT" && zip -rq "$OUT" "$PROJECT_DIR" -x "*.git*")
;;
tar.gz)
OUT="$(pwd)/${PROJECT_DIR}-submission-ready.tar.gz"
rm -f "$OUT"
echo "==> Tarring into $OUT"
tar --exclude=".git" -czf "$OUT" -C "$STAGE_PARENT" "$PROJECT_DIR"
;;
*)
echo "Unsupported format: $FORMAT (use zip or tar.gz)" >&2
exit 1
;;
esac
rm -rf "$STAGE_PARENT"
echo "==> Done: $OUT"
echo "==> Ready to upload."
Putting it together
# 1. Extract whatever the platform handed you
chmod +x extract_and_setup.sh run_build.sh package_submission.sh
./extract_and_setup.sh "project-download (1).zip"
# 2. Build + iterate
cd project-download
./run_build.sh --watch
# ... implement, save, watch tests go green, Ctrl+C when done ...
# 3. Final clean check
./run_build.sh --clean
# 4. Package for upload
cd ..
./package_submission.sh project-download
# → project-download-submission-ready.zip, ready to drag back into the platform
Why this is worth the five minutes of setup
A timed assessment window is short, and every minute spent fighting cmake/make invocations or hunting for the right directory to zip is a minute not spent on the actual problem. None of these scripts are clever — they're just removing repetitive, error-prone manual steps (figuring out the extracted folder name, remembering whether you've already run cmake .., making sure build/ doesn't end up in your submission) so the only thing left to think about is the code.
If you do C++ take-homes with CMake + GTest reasonably often, this setup pays for itself the first time you use it. Feel free to adapt the paths and test runner name (./unit_tests) to whatever your specific harness expects.
Top comments (0)