Last active
January 29, 2026 08:47
-
-
Save erikhuizinga/7b9e8e6cd16e1bcc39f3a3de96b9ed2e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| exec path/to/gradle-live-filter.sh "$@" | |
| # Put this file somewhere on your "$PATH" for easy access. | |
| # Make this executable using 'chmod +x gradle-live-filter'. | |
| # Update the path/to/ the .sh file. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then | |
| cat <<'USAGE' | |
| Usage: | |
| ./gradlew build --console=plain | gradle-live-filter.sh | |
| Filters live Gradle task output while preserving warnings/errors. | |
| USAGE | |
| exit 0 | |
| fi | |
| awk ' | |
| function flush_group() { | |
| if (group_count == 0) { | |
| return | |
| } | |
| if (group_count == 1) { | |
| print group_line | |
| fflush() | |
| } else { | |
| summary = group_count " lines filtered (" | |
| first = 1 | |
| for (i = 1; i <= status_order_count; i++) { | |
| status = status_order[i] | |
| count = status_counts[status] | |
| if (count > 0) { | |
| if (!first) { | |
| summary = summary ", " | |
| } | |
| summary = summary count " " status | |
| first = 0 | |
| } | |
| } | |
| summary = summary ")" | |
| print summary | |
| fflush() | |
| } | |
| group_count = 0 | |
| group_line = "" | |
| delete status_counts | |
| } | |
| BEGIN { | |
| status_order_count = 5 | |
| status_order[1] = "SKIPPED" | |
| status_order[2] = "UP-TO-DATE" | |
| status_order[3] = "FROM-CACHE" | |
| status_order[4] = "NO-SOURCE" | |
| status_order[5] = "DISABLED" | |
| } | |
| { | |
| line = $0 | |
| sub(/\r$/, "", line) | |
| trimmed = line | |
| sub(/[[:space:]]+$/, "", trimmed) | |
| match_line = trimmed | |
| gsub(/\033\[[0-9;]*[[:alpha:]]/, "", match_line) | |
| sub(/^[[:space:]]+/, "", match_line) | |
| if (match_line ~ /^> Task / && match(match_line, /(UP-TO-DATE|FROM-CACHE|SKIPPED|NO-SOURCE|DISABLED)/)) { | |
| status = substr(match_line, RSTART, RLENGTH) | |
| group_count++ | |
| status_counts[status]++ | |
| if (group_count == 1) { | |
| group_line = line | |
| } | |
| next | |
| } | |
| flush_group() | |
| print line | |
| fflush() | |
| } | |
| END { | |
| flush_group() | |
| } | |
| ' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -u | |
| set -o pipefail | |
| test_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| default_filter="${test_dir}/gradle-live-filter.sh" | |
| filter_script="${FILTER_SCRIPT:-${default_filter}}" | |
| if [[ ! -f "${filter_script}" ]]; then | |
| echo "error: filter script not found: ${filter_script}" >&2 | |
| exit 2 | |
| fi | |
| fail() { | |
| echo "error: $*" >&2 | |
| return 1 | |
| } | |
| assert_eq() { | |
| local expected="$1" | |
| local actual="$2" | |
| local context="${3:-}" | |
| if [[ "${actual}" != "${expected}" ]]; then | |
| if [[ -n "${context}" ]]; then | |
| fail "expected '${expected}' got '${actual}': ${context}" | |
| else | |
| fail "expected '${expected}' got '${actual}'" | |
| fi | |
| fi | |
| } | |
| with_tmp() { | |
| local tmp | |
| tmp="$(mktemp -d)" || return 1 | |
| ( | |
| trap 'rm -rf -- "${tmp}"' EXIT | |
| "$@" "${tmp}" | |
| ) | |
| } | |
| copy_filter_script() { | |
| local tmp="$1" | |
| local script_path="${tmp}/gradle-live-filter.sh" | |
| cp "${filter_script}" "${script_path}" | |
| chmod +x "${script_path}" | |
| echo "${script_path}" | |
| } | |
| run_filter() { | |
| local tmp="$1" | |
| local input="$2" | |
| local script | |
| script="$(copy_filter_script "${tmp}")" | |
| printf '%s' "${input}" | "${script}" | |
| } | |
| failures=0 | |
| cases=0 | |
| run_case() { | |
| local name="$1" | |
| shift | |
| cases=$((cases + 1)) | |
| if ( "$@" ); then | |
| echo "ok - ${name}" | |
| else | |
| echo "not ok - ${name}" | |
| failures=$((failures + 1)) | |
| fi | |
| } | |
| test_condenses_status_lines() { | |
| with_tmp test_condenses_status_lines_impl | |
| } | |
| test_condenses_status_lines_impl() { | |
| local tmp="$1" | |
| local input=$'> Task :app:compileJava UP-TO-DATE\n> Task :app:test SKIPPED\n> Task :app:jar UP-TO-DATE' | |
| local expected=$'3 lines filtered (1 SKIPPED, 2 UP-TO-DATE)' | |
| local output | |
| output="$(run_filter "${tmp}" "${input}")" | |
| assert_eq "${expected}" "${output}" "status lines should be condensed" | |
| } | |
| test_passes_through_non_task_lines() { | |
| with_tmp test_passes_through_non_task_lines_impl | |
| } | |
| test_passes_through_non_task_lines_impl() { | |
| local tmp="$1" | |
| local input=$'Starting build\n> Configure project :app\nBUILD SUCCESSFUL in 1s' | |
| local output | |
| output="$(run_filter "${tmp}" "${input}")" | |
| assert_eq "${input}" "${output}" "non-task lines should pass through" | |
| } | |
| test_single_task_line_not_condensed() { | |
| with_tmp test_single_task_line_not_condensed_impl | |
| } | |
| test_single_task_line_not_condensed_impl() { | |
| local tmp="$1" | |
| local input=$'> Task :app:compileJava UP-TO-DATE' | |
| local output | |
| output="$(run_filter "${tmp}" "${input}")" | |
| assert_eq "${input}" "${output}" "single task line should not be condensed" | |
| } | |
| test_condenses_with_reasons_and_spaces() { | |
| with_tmp test_condenses_with_reasons_and_spaces_impl | |
| } | |
| test_condenses_with_reasons_and_spaces_impl() { | |
| local tmp="$1" | |
| local input=$' > Task :app:compileJava UP-TO-DATE (no changes)\n\t> Task :app:test SKIPPED (disabled)' | |
| local expected=$'2 lines filtered (1 SKIPPED, 1 UP-TO-DATE)' | |
| local output | |
| output="$(run_filter "${tmp}" "${input}")" | |
| assert_eq "${expected}" "${output}" "reasons and spacing should be handled" | |
| } | |
| test_handles_ansi_codes() { | |
| with_tmp test_handles_ansi_codes_impl | |
| } | |
| test_handles_ansi_codes_impl() { | |
| local tmp="$1" | |
| local input=$'\x1b[32m> Task :app:compileJava\x1b[0m \x1b[33mUP-TO-DATE\x1b[0m\n\x1b[32m> Task :app:test\x1b[0m \x1b[33mSKIPPED\x1b[0m' | |
| local expected=$'2 lines filtered (1 SKIPPED, 1 UP-TO-DATE)' | |
| local output | |
| output="$(run_filter "${tmp}" "${input}")" | |
| assert_eq "${expected}" "${output}" "ANSI codes should be ignored" | |
| } | |
| test_preserves_warnings_with_keywords() { | |
| with_tmp test_preserves_warnings_with_keywords_impl | |
| } | |
| test_preserves_warnings_with_keywords_impl() { | |
| local tmp="$1" | |
| local input=$'warning: cache SKIPPED\nerror: build UP-TO-DATE' | |
| local output | |
| output="$(run_filter "${tmp}" "${input}")" | |
| assert_eq "${input}" "${output}" "warnings with keywords should pass through" | |
| } | |
| test_flushes_on_non_task_line() { | |
| with_tmp test_flushes_on_non_task_line_impl | |
| } | |
| test_flushes_on_non_task_line_impl() { | |
| local tmp="$1" | |
| local input=$'> Task :app:compileJava UP-TO-DATE\nSome log output\n> Task :app:test SKIPPED' | |
| local output | |
| output="$(run_filter "${tmp}" "${input}")" | |
| assert_eq "${input}" "${output}" "non-task line should flush groups" | |
| } | |
| test_blank_line_flushes_groups() { | |
| with_tmp test_blank_line_flushes_groups_impl | |
| } | |
| test_blank_line_flushes_groups_impl() { | |
| local tmp="$1" | |
| local input=$'> Task :app:one UP-TO-DATE\n> Task :app:two SKIPPED\n\n> Task :app:three UP-TO-DATE' | |
| local expected=$'2 lines filtered (1 SKIPPED, 1 UP-TO-DATE)\n\n> Task :app:three UP-TO-DATE' | |
| local output | |
| output="$(run_filter "${tmp}" "${input}")" | |
| assert_eq "${expected}" "${output}" "blank line should flush groups" | |
| } | |
| test_flushes_trailing_group() { | |
| with_tmp test_flushes_trailing_group_impl | |
| } | |
| test_flushes_trailing_group_impl() { | |
| local tmp="$1" | |
| local input=$'Some log output\n> Task :app:compileJava UP-TO-DATE\n> Task :app:test UP-TO-DATE' | |
| local expected=$'Some log output\n2 lines filtered (2 UP-TO-DATE)' | |
| local output | |
| output="$(run_filter "${tmp}" "${input}")" | |
| assert_eq "${expected}" "${output}" "trailing task group should be condensed" | |
| } | |
| test_summary_orders_statuses() { | |
| with_tmp test_summary_orders_statuses_impl | |
| } | |
| test_summary_orders_statuses_impl() { | |
| local tmp="$1" | |
| local input=$'> Task :app:one FROM-CACHE\n> Task :app:two DISABLED\n> Task :app:three NO-SOURCE\n> Task :app:four SKIPPED' | |
| local expected=$'4 lines filtered (1 SKIPPED, 1 FROM-CACHE, 1 NO-SOURCE, 1 DISABLED)' | |
| local output | |
| output="$(run_filter "${tmp}" "${input}")" | |
| assert_eq "${expected}" "${output}" "summary should follow status order" | |
| } | |
| test_preserves_failed_task_lines() { | |
| with_tmp test_preserves_failed_task_lines_impl | |
| } | |
| test_preserves_failed_task_lines_impl() { | |
| local tmp="$1" | |
| local input=$'> Task :app:compileJava UP-TO-DATE\n> Task :app:compileTestJava FAILED\n> Task :app:test SKIPPED' | |
| local output | |
| output="$(run_filter "${tmp}" "${input}")" | |
| assert_eq "${input}" "${output}" "failed task lines must pass through" | |
| } | |
| run_case "condenses status lines" test_condenses_status_lines | |
| run_case "passes through non-task lines" test_passes_through_non_task_lines | |
| run_case "single task line not condensed" test_single_task_line_not_condensed | |
| run_case "condenses reasons and spacing" test_condenses_with_reasons_and_spaces | |
| run_case "handles ANSI codes" test_handles_ansi_codes | |
| run_case "preserves warnings with keywords" test_preserves_warnings_with_keywords | |
| run_case "flushes on non-task line" test_flushes_on_non_task_line | |
| run_case "blank line flushes groups" test_blank_line_flushes_groups | |
| run_case "flushes trailing group" test_flushes_trailing_group | |
| run_case "summary orders statuses" test_summary_orders_statuses | |
| run_case "preserves failed task lines" test_preserves_failed_task_lines | |
| if [[ "${failures}" -ne 0 ]]; then | |
| exit 1 | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment