From c698546b948bad75a1239598fc70185627dd68a5 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Tue, 25 Aug 2026 18:06:42 -0700 Subject: [PATCH 1/8] hack: remove get_ci_vm.sh The script sets up a Cirrus-CI VM in GCP, and conmon stopped using Cirrus in e0c56ec ("ci: replace Cirrus CI with GitHub Actions"). Its --setup path sources ./contrib/cirrus/lib.sh and runs ./contrib/cirrus/setup_environment.sh, both of which were deleted in 42cecdf ("Cirrus: Remove disused scripts"), so that path cannot work at all any more. Signed-off-by: Kir Kolyshkin Co-Authored-By: Claude Opus 5 --- hack/get_ci_vm.sh | 65 ----------------------------------------------- 1 file changed, 65 deletions(-) delete mode 100755 hack/get_ci_vm.sh diff --git a/hack/get_ci_vm.sh b/hack/get_ci_vm.sh deleted file mode 100755 index 322be3bd..00000000 --- a/hack/get_ci_vm.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env bash - -# -# For help and usage information, simply execute the script w/o any arguments. -# -# This script is intended to be run by Red Hat podman developers who need -# to debug problems specifically related to Cirrus-CI automated testing. -# It requires that you have been granted prior access to create VMs in -# google-cloud. For non-Red Hat contributors, VMs are available as-needed, -# with supervision upon request. - -set -e - -SCRIPT_FILEPATH=$(realpath "${BASH_SOURCE[0]}") -SCRIPT_DIRPATH=$(dirname "$SCRIPT_FILEPATH") -REPO_DIRPATH=$(realpath "$SCRIPT_DIRPATH/../") - -# Help detect if we were called by get_ci_vm container -GET_CI_VM="${GET_CI_VM:-0}" -in_get_ci_vm() { - if ((GET_CI_VM==0)); then - echo "Error: $1 is not intended for use in this context" - exit 2 - fi -} - -# get_ci_vm APIv1 container entrypoint calls into this script -# to obtain required repo. specific configuration options. -if [[ "$1" == "--config" ]]; then - in_get_ci_vm "$1" - cat < /dev/stderr - source ./contrib/cirrus/lib.sh - echo "+ Running environment setup" > /dev/stderr - ./contrib/cirrus/setup_environment.sh -else - # Create and access VM for specified Cirrus-CI task - mkdir -p $HOME/.config/gcloud/ssh - podman run -it --rm \ - --tz=local \ - -e NAME="$USER" \ - -e SRCDIR=/src \ - -e GCLOUD_ZONE="$GCLOUD_ZONE" \ - -e DEBUG="${DEBUG:-0}" \ - -v $REPO_DIRPATH:/src:O \ - -v $HOME/.config/gcloud:/root/.config/gcloud:z \ - -v $HOME/.config/gcloud/ssh:/root/.ssh:z \ - quay.io/libpod/get_ci_vm:latest "$@" -fi From 780a0cb6de3ab0ba5b9900225570b8fa2e76cec3 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Tue, 25 Aug 2026 17:40:58 -0700 Subject: [PATCH 2/8] Use shfmt for shell scripts Add an .editorconfig based on the one podman uses, reformat every shell script in the tree with shfmt, and add a CI job that keeps it that way. The .editorconfig is podman's file minus space_redirects, so redirections keep the shape they already have here. The [[shell]] section is what lets shfmt pick up hack/github-actions-setup, which is a shell script with a shebang and no extension; that section is an shfmt extension rather than something editorconfig itself understands. See https://github.com/podman-container-tools/podman/pull/28785. shfmt itself runs the way runc runs it: a make target using a pinned container image, so that a local run and a CI run cannot disagree about formatting, plus a localshfmt target for whoever already has shfmt installed. The CI job is then just "make shfmt". shfmt finds the files to format on its own, .bats files included, so no file list has to be maintained anywhere. Signed-off-by: Kir Kolyshkin Co-Authored-By: Claude Opus 5 --- .editorconfig | 20 ++++++++ .github/workflows/validate.yml | 11 +++++ .rpmbuild/prepare.sh | 2 +- Makefile | 17 +++++++ test/01-basic.bats | 2 +- test/03-k8s-log-rotation.bats | 4 +- test/04-runtime.bats | 10 ++-- test/05-oom-detection.bats | 6 +-- test/06-exec-exit-status.bats | 3 +- test/06-log-management.bats | 4 +- test/06-priority-parsing.bats | 18 +++---- test/07-attach.bats | 14 +++--- test/08-exec.bats | 5 +- test/10-ctrl.bats | 6 +-- test/run-tests.sh | 90 +++++++++++++++++----------------- test/setup_suite.bash | 2 +- test/test_helper.bash | 43 ++++++++-------- 17 files changed, 152 insertions(+), 105 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..6630fd50 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# See https://EditorConfig.org + +# This is a top-most EditorConfig file. +root = true + +# Ignore any "vendor" directories. +[**/vendor/**] +ignore = true + +# Bash, bats, and sh files use 4 spaces for indentation. +[*.{bash,bats,sh}] +indent_style = space +indent_size = 4 + +# This is for shell scripts with shebang but no extensions. +# Not yet supported by editorconfig[1], only by shfmt. +# https://github.com/editorconfig/editorconfig/issues/404 +[[shell]] +indent_style = space +indent_size = 4 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 3fd0bffd..768304d9 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -58,6 +58,16 @@ jobs: sudo apt-get install -y cppcheck libglib2.0-dev pkg-config make cppcheck + shfmt: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Check shell script formatting + run: make shfmt + meson-build: runs-on: ubuntu-latest timeout-minutes: 10 @@ -78,6 +88,7 @@ jobs: - cppcheck - lint - meson-build + - shfmt runs-on: ubuntu-latest steps: - run: echo "All jobs completed" diff --git a/.rpmbuild/prepare.sh b/.rpmbuild/prepare.sh index 339da39e..4293971b 100644 --- a/.rpmbuild/prepare.sh +++ b/.rpmbuild/prepare.sh @@ -16,7 +16,7 @@ sed "s,#COMMIT#,${COMMIT},; s,#SHORTCOMMIT#,${COMMIT_SHORT},; s,#COMMITNUM#,${COMMIT_NUM},; s,#COMMITDATE#,${COMMIT_DATE}," \ - contrib/spec/conmon.spec.in > contrib/spec/conmon.spec + contrib/spec/conmon.spec.in >contrib/spec/conmon.spec mkdir build/ git archive --prefix "conmon-${COMMIT_SHORT}/" --format "tar.gz" HEAD -o "build/conmon-${COMMIT_SHORT}.tar.gz" diff --git a/Makefile b/Makefile index 2181f2b8..6bf44276 100644 --- a/Makefile +++ b/Makefile @@ -182,6 +182,23 @@ cppcheck: --suppress=missingIncludeSystem --suppress=checkersReport \ $(CPPCHECK_FLAGS) src/ +# Formatting for shell scripts. Uses a pinned image so that everyone, CI +# included, gets identical output; run localshfmt to use a locally installed shfmt. +# The list of files to format is shfmt's own, and covers .bats files as well +# as shell scripts with a shebang and no extension. +CONTAINER_ENGINE ?= podman +SHFMT_IMAGE ?= docker.io/mvdan/shfmt:v3.13.1 + +.PHONY: shfmt +shfmt: + $(CONTAINER_ENGINE) run $(CONTAINER_ENGINE_RUN_FLAGS) \ + --rm -v $(CURDIR):/src:z -w /src \ + $(SHFMT_IMAGE) -d -w . + +.PHONY: localshfmt +localshfmt: + shfmt -d -w . + .PHONY: fmt fmt: git ls-files -z \*.c \*.h | xargs -0 clang-format -i diff --git a/test/01-basic.bats b/test/01-basic.bats index 5bd33052..da522611 100644 --- a/test/01-basic.bats +++ b/test/01-basic.bats @@ -171,4 +171,4 @@ teardown() { --log-path "k8s-file:$LOG_PATH" --log-path "$invalid_log_driver:$LOG_PATH" assert_failure assert_output_contains "No such log driver $invalid_log_driver" -} \ No newline at end of file +} diff --git a/test/03-k8s-log-rotation.bats b/test/03-k8s-log-rotation.bats index 114c3342..3d967678 100644 --- a/test/03-k8s-log-rotation.bats +++ b/test/03-k8s-log-rotation.bats @@ -59,10 +59,10 @@ run_conmon_k8s_file() { # Test k8s log format handling local k8s_content='2023-07-23T18:00:00.000000000Z stdout F Test log message' - echo "$k8s_content" > "$LOG_PATH" + echo "$k8s_content" >"$LOG_PATH" # Verify content preservation local content content=$(<"$LOG_PATH") [ "$content" = "$k8s_content" ] -} \ No newline at end of file +} diff --git a/test/04-runtime.bats b/test/04-runtime.bats index 65db6215..4792b641 100644 --- a/test/04-runtime.bats +++ b/test/04-runtime.bats @@ -18,7 +18,7 @@ teardown() { # Check that log file was created [ -f "$LOG_PATH" ] run cat "$LOG_PATH" - assert "${output}" =~ "hello from ubi10" "'hello from ubi10' found in the log" + assert "${output}" =~ "hello from ubi10" "'hello from ubi10' found in the log" } @test "runtime: container execution with different log drivers" { @@ -26,7 +26,7 @@ teardown() { run_conmon_with_default_args --log-path "journald:" run journalctl --user CONTAINER_ID_FULL="$CTR_ID" - assert "${output}" =~ "hello from ubi10" "'hello from ubi10' found in the journald" + assert "${output}" =~ "hello from ubi10" "'hello from ubi10' found in the journald" } @test "runtime: container execution with multiple log drivers" { @@ -37,10 +37,10 @@ teardown() { [ -f "$LOG_PATH" ] run cat "$LOG_PATH" - assert "${output}" =~ "hello from ubi10" "'hello from ubi10' found in the log" + assert "${output}" =~ "hello from ubi10" "'hello from ubi10' found in the log" run journalctl --user CONTAINER_ID_FULL="$CTR_ID" - assert "${output}" =~ "hello from ubi10" "'hello from ubi10' found in the journald" + assert "${output}" =~ "hello from ubi10" "'hello from ubi10' found in the journald" } @test "runtime: container with log size limit" { @@ -54,7 +54,7 @@ teardown() { [ -f "$LOG_PATH" ] run cat "$LOG_PATH" - assert "${output}" !~ "hello from ubi10 11" "'hello from ubi10 11' not in the logs" + assert "${output}" !~ "hello from ubi10 11" "'hello from ubi10 11' not in the logs" } @test "runtime: invalid runtime binary should fail" { diff --git a/test/05-oom-detection.bats b/test/05-oom-detection.bats index cd501a14..18c83ed6 100644 --- a/test/05-oom-detection.bats +++ b/test/05-oom-detection.bats @@ -26,7 +26,7 @@ create_mock_memory_events() { local oom_kill_count="$2" local events_file="$MOCK_CGROUP_PATH/memory.events" - cat > "$events_file" << EOF + cat >"$events_file" < "$events_file" << EOF + cat >"$events_file" </dev/null 2>&1 echo "Integration test passed: exec exit codes work correctly" -} \ No newline at end of file +} diff --git a/test/06-log-management.bats b/test/06-log-management.bats index 9f267c41..f123f09a 100644 --- a/test/06-log-management.bats +++ b/test/06-log-management.bats @@ -51,7 +51,7 @@ run_conmon_k8s_log() { run_conmon_k8s_log --log-size-max 1024 assert_success [ -f "$LOG_PATH" ] - [ ! -f "$LOG_PATH.1" ] # No backup files in truncation mode + [ ! -f "$LOG_PATH.1" ] # No backup files in truncation mode } @test "log management: should enable rotation with proper flags" { @@ -66,7 +66,7 @@ run_conmon_k8s_log() { # Test k8s log format preservation local k8s_entry='2023-07-23T18:00:00.000000000Z stdout F Test log message' - echo "$k8s_entry" > "$LOG_PATH" + echo "$k8s_entry" >"$LOG_PATH" local content content=$(<"$LOG_PATH") diff --git a/test/06-priority-parsing.bats b/test/06-priority-parsing.bats index 0a47ccdd..69bb7365 100644 --- a/test/06-priority-parsing.bats +++ b/test/06-priority-parsing.bats @@ -15,7 +15,7 @@ teardown() { run_conmon_journald() { local extra_args=("$@") run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$RUNTIME_BINARY" \ - --log-path "journald:" --bundle "$BUNDLE_PATH" "${extra_args[@]}" + --log-path "journald:" --bundle "$BUNDLE_PATH" "${extra_args[@]}" } # Helper function to create a test script with priority messages @@ -23,7 +23,7 @@ create_test_script() { local script_name="$1" local script_content="$2" - echo "$script_content" > "$ROOTFS/$script_name" + echo "$script_content" >"$ROOTFS/$script_name" chmod +x "$ROOTFS/$script_name" } @@ -33,12 +33,12 @@ update_container_args() { # Use jq to update the config.json with new args, fallback to manual edit if jq not available if command -v jq >/dev/null 2>&1; then - jq ".process.args = [\"$script_path\"]" "$BUNDLE_PATH/config.json" > "$BUNDLE_PATH/config.json.tmp" && \ - mv "$BUNDLE_PATH/config.json.tmp" "$BUNDLE_PATH/config.json" + jq ".process.args = [\"$script_path\"]" "$BUNDLE_PATH/config.json" >"$BUNDLE_PATH/config.json.tmp" && + mv "$BUNDLE_PATH/config.json.tmp" "$BUNDLE_PATH/config.json" else # Fallback: recreate config with new args local temp_config=$(mktemp) - sed "s|\"args\": \[.*\]|\"args\": [\"$script_path\"]|" "$BUNDLE_PATH/config.json" > "$temp_config" + sed "s|\"args\": \[.*\]|\"args\": [\"$script_path\"]|" "$BUNDLE_PATH/config.json" >"$temp_config" mv "$temp_config" "$BUNDLE_PATH/config.json" fi } @@ -64,7 +64,7 @@ run_priority_test() { if [ "$verify_journal" = "true" ] && [ -f "$CONMON_PID_FILE" ]; then local conmon_pid conmon_pid=$(cat "$CONMON_PID_FILE") - sleep 1 # Give journald time to process + sleep 1 # Give journald time to process local journal_output journal_output=$(get_conmon_journal_output "$conmon_pid") @@ -212,13 +212,13 @@ echo "<6>Info message"' # Test with both k8s-file and journald logging if command -v journalctl >/dev/null 2>&1; then run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$RUNTIME_BINARY" \ - --bundle "$BUNDLE_PATH" --log-path "$LOG_PATH" --log-path "journald:" --terminal + --bundle "$BUNDLE_PATH" --log-path "$LOG_PATH" --log-path "journald:" --terminal assert_success [ -f "$LOG_PATH" ] else # Just test k8s-file if journald not available run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$RUNTIME_BINARY" \ - --bundle "$BUNDLE_PATH" --log-path "$LOG_PATH" --terminal + --bundle "$BUNDLE_PATH" --log-path "$LOG_PATH" --terminal assert_success [ -f "$LOG_PATH" ] fi @@ -228,7 +228,7 @@ echo "<6>Info message"' @test "priority parsing: error handling with invalid log path" { # Test error handling when journald is not available run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$RUNTIME_BINARY" \ - --bundle "$BUNDLE_PATH" --log-path "invalid_driver:" --terminal 2>&1 || true + --bundle "$BUNDLE_PATH" --log-path "invalid_driver:" --terminal 2>&1 || true # Use assert_output_contains for specific error checking if [ "$status" -ne 0 ]; then diff --git a/test/07-attach.bats b/test/07-attach.bats index 94b7fa46..8cf2c34f 100644 --- a/test/07-attach.bats +++ b/test/07-attach.bats @@ -20,8 +20,8 @@ teardown() { # Check that log file was created assert_file_exists "$LOG_PATH" run cat "$LOG_PATH" - assert "${output}" =~ "Hello there!" "'Hello there!' found in the log" - assert "${output}" =~ "Container stopped!" "'Container stopped!' found in the log" + assert "${output}" =~ "Hello there!" "'Hello there!' found in the log" + assert "${output}" =~ "Container stopped!" "'Container stopped!' found in the log" assert_file_not_exists "$ATTACH_PATH" } @@ -37,7 +37,7 @@ teardown() { # Check that log file was created assert_file_exists "$LOG_PATH" run cat "$LOG_PATH" - assert "${output}" =~ "Container stopped!" "'Container stopped!' found in the log" + assert "${output}" =~ "Container stopped!" "'Container stopped!' found in the log" } @test "attach: unix socket remains open with --leave-stdin-open" { @@ -48,12 +48,12 @@ teardown() { # Check that log file was created assert_file_exists "$LOG_PATH" run cat "$LOG_PATH" - assert "${output}" =~ "Hello there!" "'Hello there!' found in the log" - assert "${output}" !~ "Container stopped!" "'Container stopped!' not found in the log" + assert "${output}" =~ "Hello there!" "'Hello there!' found in the log" + assert "${output}" !~ "Container stopped!" "'Container stopped!' not found in the log" assert_file_exists "$ATTACH_PATH" echo "Hello there again!" | socat STDIN "UNIX:${ATTACH_PATH},socktype=5" run cat "$LOG_PATH" - assert "${output}" =~ "Hello there again!" "'Hello there again!' found in the log" - assert "${output}" !~ "Container stopped!" "'Container stopped!' not found in the log" + assert "${output}" =~ "Hello there again!" "'Hello there again!' found in the log" + assert "${output}" !~ "Container stopped!" "'Container stopped!' not found in the log" } diff --git a/test/08-exec.bats b/test/08-exec.bats index 3878b966..a26d1a5e 100644 --- a/test/08-exec.bats +++ b/test/08-exec.bats @@ -32,12 +32,12 @@ teardown() { # Check that the main process noticed the /tmp/test.txt. assert_file_exists "$LOG_PATH" run cat "$LOG_PATH" - assert "${output}" =~ "Hello there!" "'Hello there!' found in the log" + assert "${output}" =~ "Hello there!" "'Hello there!' found in the log" # Check that the exec process output is stored in the log. assert_file_exists "$LOG_PATH.exec" run cat "$LOG_PATH.exec" - assert "${output}" =~ "Hello from exec!" "'Hello from exec!' found in the log" + assert "${output}" =~ "Hello from exec!" "'Hello from exec!' found in the log" } @test "exec: --exec-attach without no --api-version" { @@ -186,7 +186,6 @@ teardown() { assert_json "${output}" =~ '"data": 0' } - @test "exec: runtime failure is reported to the sync pipe" { start_conmon_with_default_args --log-path "k8s-file:$LOG_PATH" wait_for_runtime_status "$CTR_ID" running diff --git a/test/10-ctrl.bats b/test/10-ctrl.bats index a3859a81..9b3d26fd 100644 --- a/test/10-ctrl.bats +++ b/test/10-ctrl.bats @@ -20,7 +20,7 @@ test_ctl_command() { wait_for_runtime_status "$CTR_ID" running local main_conmon_pid=$CONMON_PID - echo "$command" > ${CTL_PATH} + echo "$command" >${CTL_PATH} start_conmon_with_default_args \ --log-path "k8s-file:$LOG_PATH.exec" \ @@ -95,7 +95,7 @@ test_resize_command_ok() { # Remove the log. rm -f $LOG_PATH # The control message should reopen/recreate it. - echo "2 1 1" > ${CTL_PATH} + echo "2 1 1" >${CTL_PATH} start_conmon_with_default_args \ --log-path "k8s-file:$LOG_PATH.exec" \ @@ -179,7 +179,7 @@ test_resize_command_ok() { wait_for_log_line "$LOG_PATH" "before rotation" # The control message should rotate the log - echo "2 1 1" > ${CTL_PATH} + echo "2 1 1" >${CTL_PATH} run_conmon_with_default_args \ --log-path "k8s-file:$LOG_PATH.exec" \ diff --git a/test/run-tests.sh b/test/run-tests.sh index 09c08861..cd2a50bb 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -20,7 +20,7 @@ YELLOW='\033[1;33m' NC='\033[0m' # No Color usage() { - cat << EOF + cat <& /dev/null; then + if ! command -v bats >&/dev/null; then missing_deps+=("bats") fi - if ! command -v socat >& /dev/null; then + if ! command -v socat >&/dev/null; then missing_deps+=("socat") fi @@ -126,47 +126,47 @@ main() { # Parse command line arguments while [[ $# -gt 0 ]]; do case $1 in - -h|--help) - usage - exit 0 - ;; - -c|--conmon) - CONMON_BINARY="$2" - shift 2 - ;; - -r|--runtime) - RUNTIME_BINARY="$2" - shift 2 - ;; - -v|--verbose) - verbose=true - shift - ;; - -t|--tap) - tap=true - shift - ;; - -j|--jobs) - jobs="$2" - shift 2 - ;; - --filter) - filter="$2" - shift 2 - ;; - -s|--strict) - CONMON_TEST_STRICT=1 - shift - ;; - *.bats) - test_files+=("$1") - shift - ;; - *) - log_error "Unknown option: $1" - usage - exit 1 - ;; + -h | --help) + usage + exit 0 + ;; + -c | --conmon) + CONMON_BINARY="$2" + shift 2 + ;; + -r | --runtime) + RUNTIME_BINARY="$2" + shift 2 + ;; + -v | --verbose) + verbose=true + shift + ;; + -t | --tap) + tap=true + shift + ;; + -j | --jobs) + jobs="$2" + shift 2 + ;; + --filter) + filter="$2" + shift 2 + ;; + -s | --strict) + CONMON_TEST_STRICT=1 + shift + ;; + *.bats) + test_files+=("$1") + shift + ;; + *) + log_error "Unknown option: $1" + usage + exit 1 + ;; esac done @@ -191,7 +191,7 @@ main() { # Add any additional BATS options from environment if [[ -n "$BATS_OPTIONS" ]]; then - read -ra additional_opts <<< "$BATS_OPTIONS" + read -ra additional_opts <<<"$BATS_OPTIONS" bats_args+=("${additional_opts[@]}") fi diff --git a/test/setup_suite.bash b/test/setup_suite.bash index 242e1453..b3ed0fde 100644 --- a/test/setup_suite.bash +++ b/test/setup_suite.bash @@ -47,7 +47,7 @@ setup_suite() { suite_fail "failed to create a container from $UBI10_MICRO_IMAGE" return 1 fi - if ! podman export "$ctr" > "$CONMON_TEST_ROOTFS_TAR"; then + if ! podman export "$ctr" >"$CONMON_TEST_ROOTFS_TAR"; then podman rm "$ctr" >/dev/null 2>&1 || true suite_fail "failed to export the rootfs from $UBI10_MICRO_IMAGE" return 1 diff --git a/test/test_helper.bash b/test/test_helper.bash index 7aae2e98..b2bf00b1 100644 --- a/test/test_helper.bash +++ b/test/test_helper.bash @@ -99,11 +99,11 @@ generate_process_spec() { fi if [[ -z "$BUNDLE_PATH" || ! -e "$BUNDLE_PATH" ]]; then die "The BUNDLE_PATH directory does not exist. Ensure 'generate_process_spec'" \ - " is called after the 'setup_test_env'" + " is called after the 'setup_test_env'" fi local config_path="$BUNDLE_PATH/process.json" - cat > "$config_path" << EOF + cat >"$config_path" < "$config_path" << EOF + cat >"$config_path" <&2 - printf "#| FAIL: %s\n" "$testname" >&2 - printf "#| expected: %s%s\n" "$op" "${expect_split_q[0]}" >&2 + printf "#/vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n" >&2 + printf "#| FAIL: %s\n" "$testname" >&2 + printf "#| expected: %s%s\n" "$op" "${expect_split_q[0]}" >&2 local line for line in "${expect_split_q[@]:1}"; do - printf "#| > %s%s\n" "$ws" "$line" >&2 + printf "#| > %s%s\n" "$ws" "$line" >&2 done - printf "#| actual: %s%s\n" "$ws" "${actual_split_q[0]}" >&2 + printf "#| actual: %s%s\n" "$ws" "${actual_split_q[0]}" >&2 for line in "${actual_split_q[@]:1}"; do - printf "#| > %s%s\n" "$ws" "$line" >&2 + printf "#| > %s%s\n" "$ws" "$line" >&2 done - printf "#\\^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n" >&2 + printf "#\\^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n" >&2 bail-now } function die() { # FIXME: handle multi-line output - echo "#/vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv" >&2 - echo "#| FAIL: $*" >&2 + echo "#/vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv" >&2 + echo "#| FAIL: $*" >&2 echo "#\\^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" >&2 bail-now } From a5929b3dec48fbaa203da811cdec508345fe219f Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Tue, 25 Aug 2026 17:55:41 -0700 Subject: [PATCH 3/8] test: quote shell parameter expansions Found by shellcheck (SC2086). None of these were broken in practice, since the paths involved never contain whitespace or glob characters, but quoting them is what keeps that true. get_conmon_journal_output() is the one place where the word splitting was load-bearing: its level filter is either empty or "-p ", which has to reach journalctl as two separate arguments. Quoting it as it stood would have passed it as one, so it becomes an array instead. Signed-off-by: Kir Kolyshkin Co-Authored-By: Claude Opus 5 --- test/04-runtime.bats | 6 +++--- test/06-exec-exit-status.bats | 18 +++++++++--------- test/08-exec.bats | 12 ++++++------ test/10-ctrl.bats | 8 ++++---- test/test_helper.bash | 8 ++++---- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/test/04-runtime.bats b/test/04-runtime.bats index 4792b641..8714e30a 100644 --- a/test/04-runtime.bats +++ b/test/04-runtime.bats @@ -87,7 +87,7 @@ teardown() { # Check that the pid is sent to the sync pipe. wait_for_syncpipe_output 1 - run cat $TEST_TMPDIR/syncpipe-output + run cat "$TEST_TMPDIR/syncpipe-output" CONTAINER_PID=$(cat "$CONTAINER_PIDFILE") assert_json "${output}" =~ "\"pid\": $CONTAINER_PID" } @@ -114,13 +114,13 @@ teardown() { # Give conmon some time to run the runtime and fail. sleep 1 - assert_file_exists $CONMON_PID_FILE + assert_file_exists "$CONMON_PID_FILE" CONMON_PID=$(cat "$CONMON_PID_FILE") wait_for_conmon_exit "$CONMON_PID" # Check that the error is sent to the sync pipe. wait_for_syncpipe_output 1 - run cat $TEST_TMPDIR/syncpipe-output + run cat "$TEST_TMPDIR/syncpipe-output" assert_json "${output}" =~ "\"pid\": -1" assert_json "${output}" =~ "\"message\":" assert_json "${output}" =~ "runc create failed" diff --git a/test/06-exec-exit-status.bats b/test/06-exec-exit-status.bats index 50f9bb2f..1a9a60b1 100755 --- a/test/06-exec-exit-status.bats +++ b/test/06-exec-exit-status.bats @@ -54,7 +54,7 @@ teardown() { # of them has any business taking long, and an unbounded one hangs the # whole suite -- bats runs tests serially, and a command substitution # waits for stdout to be closed, which a misbehaving conmon may never do. - run timeout 10 podman --conmon $conmon_path run --rm "$UBI10_MICRO_IMAGE" true + run timeout 10 podman --conmon "$conmon_path" run --rm "$UBI10_MICRO_IMAGE" true if [ "$status" -ne 0 ]; then die "cannot create test containers with podman: $output" fi @@ -63,37 +63,37 @@ teardown() { # Create a test container local container_id - container_id=$(timeout 60 podman --conmon $conmon_path run -dt "$UBI10_MICRO_IMAGE" sleep 30) + container_id=$(timeout 60 podman --conmon "$conmon_path" run -dt "$UBI10_MICRO_IMAGE" sleep 30) if [ -z "$container_id" ]; then die "failed to create test container" fi # Test 1: Success case - if ! timeout 60 podman --conmon $conmon_path exec "$container_id" true; then - timeout 60 podman --conmon $conmon_path rm -f "$container_id" >/dev/null 2>&1 + if ! timeout 60 podman --conmon "$conmon_path" exec "$container_id" true; then + timeout 60 podman --conmon "$conmon_path" rm -f "$container_id" >/dev/null 2>&1 echo "FAIL: true command should succeed" return 1 fi # Test 2: Failure case - this would fail with the regression - if timeout 60 podman --conmon $conmon_path exec "$container_id" false; then - timeout 60 podman --conmon $conmon_path rm -f "$container_id" >/dev/null 2>&1 + if timeout 60 podman --conmon "$conmon_path" exec "$container_id" false; then + timeout 60 podman --conmon "$conmon_path" rm -f "$container_id" >/dev/null 2>&1 echo "FAIL: false command should fail (regression detected!)" echo "This indicates the fc0a342 regression where all exec commands return 0" return 1 fi # Test 3: Custom exit code - this would return 0 with the regression - if timeout 60 podman --conmon $conmon_path exec "$container_id" sh -c 'exit 42'; then - timeout 60 podman --conmon $conmon_path rm -f "$container_id" >/dev/null 2>&1 + if timeout 60 podman --conmon "$conmon_path" exec "$container_id" sh -c 'exit 42'; then + timeout 60 podman --conmon "$conmon_path" rm -f "$container_id" >/dev/null 2>&1 echo "FAIL: 'exit 42' should fail with code 42 (regression detected!)" echo "This indicates the fc0a342 regression where all exec commands return 0" return 1 fi # Clean up - timeout 60 podman --conmon $conmon_path rm -f "$container_id" >/dev/null 2>&1 + timeout 60 podman --conmon "$conmon_path" rm -f "$container_id" >/dev/null 2>&1 echo "Integration test passed: exec exit codes work correctly" } diff --git a/test/08-exec.bats b/test/08-exec.bats index a26d1a5e..6ff34eef 100644 --- a/test/08-exec.bats +++ b/test/08-exec.bats @@ -87,8 +87,8 @@ teardown() { wait_for_runtime_status "$CTR_ID" stopped # Check that the conmon wrote something back. - assert_file_exists $TEST_TMPDIR/attachpipe-output - run cat $TEST_TMPDIR/attachpipe-output + assert_file_exists "$TEST_TMPDIR/attachpipe-output" + run cat "$TEST_TMPDIR/attachpipe-output" assert_json "${output}" =~ '"data": 0' } @@ -134,7 +134,7 @@ teardown() { assert "${output}" !~ "Hello from exec!" # Trigger second write to startpipeline. - touch $TEST_TMPDIR/startpipe-continue + touch "$TEST_TMPDIR/startpipe-continue" # The exec should start now. wait_for_runtime_status "$CTR_ID" stopped @@ -159,7 +159,7 @@ teardown() { # Check that the conmon wrote something back. wait_for_syncpipe_output 1 - run cat $TEST_TMPDIR/syncpipe-output + run cat "$TEST_TMPDIR/syncpipe-output" assert_json "${output}" =~ '"exit_code": 0' } @@ -180,7 +180,7 @@ teardown() { # There should be two values with "data" key. The first one is the PID and # the second one is the exit code. wait_for_syncpipe_output 2 - run cat $TEST_TMPDIR/syncpipe-output + run cat "$TEST_TMPDIR/syncpipe-output" CONTAINER_PID=$(cat "$CONTAINER_PIDFILE") assert_json "${output}" =~ "\"data\": $CONTAINER_PID" assert_json "${output}" =~ '"data": 0' @@ -205,7 +205,7 @@ teardown() { # The failure has to be reported rather than silently swallowed, and the # report has to carry what the runtime said about it. wait_for_syncpipe_output 1 - run cat $TEST_TMPDIR/syncpipe-output + run cat "$TEST_TMPDIR/syncpipe-output" assert_json "${output}" =~ '"data": -1' assert "${output}" =~ "exec failed" } diff --git a/test/10-ctrl.bats b/test/10-ctrl.bats index 9b3d26fd..182af4f7 100644 --- a/test/10-ctrl.bats +++ b/test/10-ctrl.bats @@ -20,7 +20,7 @@ test_ctl_command() { wait_for_runtime_status "$CTR_ID" running local main_conmon_pid=$CONMON_PID - echo "$command" >${CTL_PATH} + echo "$command" >"${CTL_PATH}" start_conmon_with_default_args \ --log-path "k8s-file:$LOG_PATH.exec" \ @@ -93,9 +93,9 @@ test_resize_command_ok() { local main_conmon_pid=$CONMON_PID # Remove the log. - rm -f $LOG_PATH + rm -f "$LOG_PATH" # The control message should reopen/recreate it. - echo "2 1 1" >${CTL_PATH} + echo "2 1 1" >"${CTL_PATH}" start_conmon_with_default_args \ --log-path "k8s-file:$LOG_PATH.exec" \ @@ -179,7 +179,7 @@ test_resize_command_ok() { wait_for_log_line "$LOG_PATH" "before rotation" # The control message should rotate the log - echo "2 1 1" >${CTL_PATH} + echo "2 1 1" >"${CTL_PATH}" run_conmon_with_default_args \ --log-path "k8s-file:$LOG_PATH.exec" \ diff --git a/test/test_helper.bash b/test/test_helper.bash index b2bf00b1..122f6f71 100644 --- a/test/test_helper.bash +++ b/test/test_helper.bash @@ -62,12 +62,12 @@ get_conmon_journal_output() { return 0 fi - local level_filter="" + local level_filter=() if [[ "$level" != "-1" ]]; then - level_filter="-p $level" + level_filter=(-p "$level") fi - journalctl -q --no-pager $level_filter _COMM=conmon _PID="$pid" 2>/dev/null || echo "" + journalctl -q --no-pager "${level_filter[@]}" _COMM=conmon _PID="$pid" 2>/dev/null || echo "" } # Create a temporary directory for test @@ -575,7 +575,7 @@ _start_pipe_reader() { { exec {r}<"$pipe_path" while IFS= read -r -u "$r" line; do - echo "$line" >>$output_file + echo "$line" >>"$output_file" done } & } From b970aa4aa84f2334bacf5435aa71c14191bd292d Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Tue, 25 Aug 2026 17:55:51 -0700 Subject: [PATCH 4/8] test: declare and assign separately Found by shellcheck (SC2155). "local x=$(cmd)" makes the exit status of local, which is always zero, mask the exit status of cmd. None of these four checked that status, so the bug is latent rather than real, but the next person to add "set -e" or a status check would not get what they expect. Signed-off-by: Kir Kolyshkin Co-Authored-By: Claude Opus 5 --- test/06-exec-exit-status.bats | 3 ++- test/06-priority-parsing.bats | 3 ++- test/test_helper.bash | 6 ++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/test/06-exec-exit-status.bats b/test/06-exec-exit-status.bats index 1a9a60b1..1bfba83d 100755 --- a/test/06-exec-exit-status.bats +++ b/test/06-exec-exit-status.bats @@ -42,7 +42,8 @@ teardown() { fi # Use the conmon binary from the build (using absolute path) - local conmon_path="$(dirname "$CONMON_BINARY")/conmon" + local conmon_path + conmon_path="$(dirname "$CONMON_BINARY")/conmon" if [ ! -f "$conmon_path" ]; then die "conmon binary not found for integration testing at $conmon_path" diff --git a/test/06-priority-parsing.bats b/test/06-priority-parsing.bats index 69bb7365..91171b73 100644 --- a/test/06-priority-parsing.bats +++ b/test/06-priority-parsing.bats @@ -37,7 +37,8 @@ update_container_args() { mv "$BUNDLE_PATH/config.json.tmp" "$BUNDLE_PATH/config.json" else # Fallback: recreate config with new args - local temp_config=$(mktemp) + local temp_config + temp_config=$(mktemp) sed "s|\"args\": \[.*\]|\"args\": [\"$script_path\"]|" "$BUNDLE_PATH/config.json" >"$temp_config" mv "$temp_config" "$BUNDLE_PATH/config.json" fi diff --git a/test/test_helper.bash b/test/test_helper.bash index 122f6f71..3ae04dd2 100644 --- a/test/test_helper.bash +++ b/test/test_helper.bash @@ -716,12 +716,14 @@ function assert() { # bash %q is really nice, except for the way it backslashes spaces local -a expect_split_q for line in "${expect_split[@]}"; do - local q=$(printf "%q" "$line" | sed -e 's/\\ / /g') + local q + q=$(printf "%q" "$line" | sed -e 's/\\ / /g') expect_split_q+=("$q") done local -a actual_split_q for line in "${actual_split[@]}"; do - local q=$(printf "%q" "$line" | sed -e 's/\\ / /g') + local q + q=$(printf "%q" "$line" | sed -e 's/\\ / /g') actual_split_q+=("$q") done From 939ed71f62e337a02bd4b2985151db0df834d542 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Tue, 25 Aug 2026 17:56:03 -0700 Subject: [PATCH 5/8] test: spell out the printf padding trick Found by shellcheck (SC2140, SC2183). Both places build a 65535 character line the same way, and both were sloppy about it: The %*s conversion takes a width and a string, so "printf '%*s' 65535" was relying on printf treating the missing second argument as empty. Pass the empty string explicitly. In 02-ctr-logs.bats the whole command was also a double quoted string containing a bare "65535", which merely concatenated into the intended text rather than quoting anything. Drop the inner quotes. Signed-off-by: Kir Kolyshkin Co-Authored-By: Claude Opus 5 --- test/02-ctr-logs.bats | 4 ++-- test/10-ctrl.bats | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/02-ctr-logs.bats b/test/02-ctr-logs.bats index 9843752c..88f07052 100644 --- a/test/02-ctr-logs.bats +++ b/test/02-ctr-logs.bats @@ -153,7 +153,7 @@ run_conmon_with_log_opts() { @test "ctr logs: journald partial message" { # Print a message longer than the conmon buffer. # It should split it into multiple partial messages. - setup_container_env "printf '%*s' "65535" | tr ' ' '#'" + setup_container_env "printf '%*s' 65535 '' | tr ' ' '#'" run_conmon_with_default_args \ --log-path "journald:" @@ -164,7 +164,7 @@ run_conmon_with_log_opts() { @test "ctr logs: k8s partial message" { # Print a message longer than the conmon buffer. # It should split it into multiple partial messages. - setup_container_env "printf '%*s' "65535" | tr ' ' '#'" + setup_container_env "printf '%*s' 65535 '' | tr ' ' '#'" run_conmon_with_default_args \ --log-path "k8s-file:$LOG_PATH" diff --git a/test/10-ctrl.bats b/test/10-ctrl.bats index 182af4f7..893ee8d1 100644 --- a/test/10-ctrl.bats +++ b/test/10-ctrl.bats @@ -79,7 +79,7 @@ test_resize_command_ok() { @test "ctrl: resize the terminal, too long line" { # Generate a very long line, longer than conmon's buffer. - long_line=$(printf '%*s' "65535" | tr ' ' "#") + long_line=$(printf '%*s' 65535 '' | tr ' ' "#") test_resize_command_fail "1 2 2 $long_line" } From 317eb98a20354ef4f6bc805fe7cf50957230903b Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Tue, 25 Aug 2026 17:56:13 -0700 Subject: [PATCH 6/8] test: move skip_if_no_journald above its callers Found by shellcheck (SC2218). The function sat at the bottom of the file while seven tests called it. bats sources the whole file before running any test, so this worked, but shellcheck reads the file as an ordinary script and cannot know that. Moving the definition next to the other helpers is also just easier to read. Signed-off-by: Kir Kolyshkin Co-Authored-By: Claude Opus 5 --- test/06-priority-parsing.bats | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/06-priority-parsing.bats b/test/06-priority-parsing.bats index 91171b73..af32a2b4 100644 --- a/test/06-priority-parsing.bats +++ b/test/06-priority-parsing.bats @@ -76,6 +76,18 @@ run_priority_test() { fi } +# Helper function to skip test if journald is not available +skip_if_no_journald() { + if ! command -v journalctl >/dev/null 2>&1; then + skip "journalctl not available - skipping journald test" + fi + + # Also check if systemd logging is compiled in + if ! $CONMON_BINARY --help 2>&1 | grep -q "journald"; then + skip "conmon not compiled with journald support" + fi +} + @test "priority parsing: valid priority prefixes are parsed correctly" { skip_if_no_journald @@ -236,15 +248,3 @@ echo "<6>Info message"' assert_output_contains "log driver" fi } - -# Helper function to skip test if journald is not available -skip_if_no_journald() { - if ! command -v journalctl >/dev/null 2>&1; then - skip "journalctl not available - skipping journald test" - fi - - # Also check if systemd logging is compiled in - if ! $CONMON_BINARY --help 2>&1 | grep -q "journald"; then - skip "conmon not compiled with journald support" - fi -} From 0e01994f70f65ca3b5c4c078a9fbd8024c58d286 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Tue, 25 Aug 2026 17:56:34 -0700 Subject: [PATCH 7/8] test: silence the shellcheck warnings that do not apply Five places where shellcheck is reading the code correctly but cannot know the context, so they get a directive with the reason: - two single quoted strings (SC2016) hold script text meant to be expanded somewhere else, once in the container and once in a child shell that inherits an exported TEST_TMPDIR; - two files source /etc/os-release (SC1091), which shellcheck cannot follow because it is not part of the tree; - in test_helper.bash, status and output (SC2154) come from bats' run, and variables such as VALID_PATH (SC2034) are only referenced by the .bats files that load the helper. Signed-off-by: Kir Kolyshkin Co-Authored-By: Claude Opus 5 --- hack/github-actions-setup | 1 + test/06-priority-parsing.bats | 4 +++- test/08-exec.bats | 2 ++ test/run-tests.sh | 1 + test/test_helper.bash | 5 +++++ 5 files changed, 12 insertions(+), 1 deletion(-) diff --git a/hack/github-actions-setup b/hack/github-actions-setup index 9e251bdf..d0d59e3e 100755 --- a/hack/github-actions-setup +++ b/hack/github-actions-setup @@ -124,6 +124,7 @@ apt_get() { } install_packages() { + # shellcheck source=/dev/null . /etc/os-release CRIU_REPO="https://download.opensuse.org/repositories/devel:/tools:/criu/xUbuntu_$VERSION_ID" diff --git a/test/06-priority-parsing.bats b/test/06-priority-parsing.bats index af32a2b4..102dbb86 100644 --- a/test/06-priority-parsing.bats +++ b/test/06-priority-parsing.bats @@ -177,7 +177,9 @@ echo ""' @test "priority parsing: all valid priority levels 0-7" { skip_if_no_journald - # Create script that tests all valid priority levels + # Create script that tests all valid priority levels. The body runs in + # the container, so $i must not be expanded here. + # shellcheck disable=SC2016 create_test_script "test_all_priorities.sh" '#!/bin/bash for i in {0..7}; do echo "<$i>Priority level $i message" diff --git a/test/08-exec.bats b/test/08-exec.bats index 6ff34eef..d63e0699 100644 --- a/test/08-exec.bats +++ b/test/08-exec.bats @@ -112,6 +112,8 @@ teardown() { printf 'start conmon\n' >&$w # Wait for the main test process to do initial asserts. It will signal # to this process by file creation. + # TEST_TMPDIR is exported, so let the child shell expand it. + # shellcheck disable=SC2016 timeout 5 bash -c 'while [ ! -f $TEST_TMPDIR/startpipe-continue ]; do sleep 0.1; done;' # Do the second write to really start the conmon. printf 'start attach\n' >&$w diff --git a/test/run-tests.sh b/test/run-tests.sh index cd2a50bb..a64d1159 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -98,6 +98,7 @@ show_environment() { log_info "Running tests with:" log_info " kernel: $(uname -srm)" if [[ -r /etc/os-release ]]; then + # shellcheck source=/dev/null log_info " distro: $(. /etc/os-release && echo "$PRETTY_NAME")" fi log_info " conmon: $CONMON_BINARY: $("$CONMON_BINARY" --version 2>&1 | tr '\n' ' ' | sed 's/ *$//')" diff --git a/test/test_helper.bash b/test/test_helper.bash index 3ae04dd2..652106ea 100644 --- a/test/test_helper.bash +++ b/test/test_helper.bash @@ -2,6 +2,11 @@ # Common test helper functions for conmon BATS tests +# status and output are set by bats' run, and several variables defined here +# are only referenced by the .bats files that load this one, so shellcheck +# cannot see either when it looks at this file on its own. +# shellcheck disable=SC2034,SC2154 + # Provide basic assertion functions if not available assert_success() { if [ "$status" -ne 0 ]; then From 9df0e8021e743798221826bc65982d746f4b2dcd Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Tue, 25 Aug 2026 17:57:02 -0700 Subject: [PATCH 8/8] ci: add shellcheck Run it the same way as shfmt: a make target using a pinned container image, a localshellcheck target for whoever has shellcheck installed, and a CI job that is just "make shellcheck". Pinning matters more here than it does for shfmt, since what shellcheck reports moves between releases and ubuntu-latest is several versions behind. The tree is clean as of the preceding commits. Signed-off-by: Kir Kolyshkin Co-Authored-By: Claude Opus 5 --- .github/workflows/validate.yml | 11 +++++++++++ Makefile | 17 +++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 768304d9..b3234e71 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -68,6 +68,16 @@ jobs: - name: Check shell script formatting run: make shfmt + shellcheck: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Lint shell scripts + run: make shellcheck + meson-build: runs-on: ubuntu-latest timeout-minutes: 10 @@ -88,6 +98,7 @@ jobs: - cppcheck - lint - meson-build + - shellcheck - shfmt runs-on: ubuntu-latest steps: diff --git a/Makefile b/Makefile index 6bf44276..709af446 100644 --- a/Makefile +++ b/Makefile @@ -199,6 +199,23 @@ shfmt: localshfmt: shfmt -d -w . +# Shell script linting, run the same way as shfmt above. .bats files have no +# shebang, hence -s bash. +SHELLCHECK_IMAGE ?= docker.io/koalaman/shellcheck:v0.11.0 +# The last entry is a shell script with a shebang and no extension, which +# git ls-files cannot match by pattern. +SHELL_SRC := $(shell git ls-files '*.bash' '*.bats' '*.sh') hack/github-actions-setup + +.PHONY: shellcheck +shellcheck: + $(CONTAINER_ENGINE) run $(CONTAINER_ENGINE_RUN_FLAGS) \ + --rm -v $(CURDIR):/src:z -w /src \ + $(SHELLCHECK_IMAGE) -s bash $(SHELL_SRC) + +.PHONY: localshellcheck +localshellcheck: + shellcheck -s bash $(SHELL_SRC) + .PHONY: fmt fmt: git ls-files -z \*.c \*.h | xargs -0 clang-format -i