common.sh 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. # mac-ops common helpers
  2. # Source from any script: source "$(dirname "$0")/_lib/common.sh"
  3. #
  4. # Provides:
  5. # - Semantic exit codes
  6. # - log_pass / log_fail / log_warn / log_info — [TAG] message lines
  7. # - JSON-mode emitters (mac_emit_check, mac_emit_section, mac_emit_summary)
  8. # - Mode flags parsing: --json --redact --quiet
  9. # - Reuses net-ops _lib/redact.sh patterns via the shared term.sh
  10. set -u
  11. # Semantic exit codes
  12. EXIT_OK=0
  13. EXIT_ERROR=1
  14. EXIT_USAGE=2
  15. EXIT_NOT_FOUND=3
  16. EXIT_VALIDATION=4
  17. EXIT_PRECONDITION=5
  18. EXIT_TIMEOUT=6
  19. EXIT_UNAVAILABLE=7
  20. # Mode flags (set by parse_common_flags)
  21. JSON_MODE="${JSON_MODE:-0}"
  22. REDACT="${REDACT:-0}"
  23. QUIET="${QUIET:-0}"
  24. VERBOSE="${VERBOSE:-0}"
  25. # Running counters
  26. PASS_COUNT=0
  27. FAIL_COUNT=0
  28. WARN_COUNT=0
  29. INFO_COUNT=0
  30. FIRST_FAIL=""
  31. CURRENT_SECTION=""
  32. parse_common_flags() {
  33. for a in "$@"; do
  34. case "$a" in
  35. --json) JSON_MODE=1 ;;
  36. --redact) REDACT=1 ;;
  37. --quiet|-q) QUIET=1 ;;
  38. --verbose|-v) VERBOSE=1 ;;
  39. esac
  40. done
  41. }
  42. # JSON-safe escaper for strings
  43. _json_escape() {
  44. local s="$1"
  45. s="${s//\\/\\\\}"
  46. s="${s//\"/\\\"}"
  47. s="${s//$'\n'/\\n}"
  48. s="${s//$'\r'/\\r}"
  49. s="${s//$'\t'/\\t}"
  50. printf '%s' "$s"
  51. }
  52. # Section header — sets CURRENT_SECTION and prints a banner (or JSON record).
  53. section() {
  54. CURRENT_SECTION="$1"
  55. if [[ "$JSON_MODE" -eq 1 ]]; then
  56. printf '{"type":"section","name":"%s"}\n' "$(_json_escape "$1")"
  57. else
  58. [[ "$QUIET" -eq 1 ]] || { echo; echo "=== $1 ==="; }
  59. fi
  60. }
  61. # Check result emitters
  62. log_pass() {
  63. PASS_COUNT=$((PASS_COUNT + 1))
  64. if [[ "$JSON_MODE" -eq 1 ]]; then
  65. printf '{"type":"check","section":"%s","label":"%s","status":"pass","detail":"%s"}\n' \
  66. "$(_json_escape "$CURRENT_SECTION")" "$(_json_escape "$1")" "$(_json_escape "${2:-}")"
  67. else
  68. echo "[PASS] $1${2:+ :: $2}"
  69. fi
  70. }
  71. log_fail() {
  72. FAIL_COUNT=$((FAIL_COUNT + 1))
  73. [[ -z "$FIRST_FAIL" ]] && FIRST_FAIL="[$CURRENT_SECTION] $1"
  74. if [[ "$JSON_MODE" -eq 1 ]]; then
  75. printf '{"type":"check","section":"%s","label":"%s","status":"fail","detail":"%s"}\n' \
  76. "$(_json_escape "$CURRENT_SECTION")" "$(_json_escape "$1")" "$(_json_escape "${2:-}")"
  77. else
  78. echo "[FAIL] $1${2:+ :: $2}"
  79. fi
  80. }
  81. log_warn() {
  82. WARN_COUNT=$((WARN_COUNT + 1))
  83. if [[ "$JSON_MODE" -eq 1 ]]; then
  84. printf '{"type":"check","section":"%s","label":"%s","status":"warn","detail":"%s"}\n' \
  85. "$(_json_escape "$CURRENT_SECTION")" "$(_json_escape "$1")" "$(_json_escape "${2:-}")"
  86. else
  87. echo "[WARN] $1${2:+ :: $2}"
  88. fi
  89. }
  90. log_info() {
  91. INFO_COUNT=$((INFO_COUNT + 1))
  92. if [[ "$JSON_MODE" -eq 1 ]]; then
  93. printf '{"type":"check","section":"%s","label":"%s","status":"info","detail":"%s"}\n' \
  94. "$(_json_escape "$CURRENT_SECTION")" "$(_json_escape "$1")" "$(_json_escape "${2:-}")"
  95. else
  96. echo "[INFO] $1${2:+ :: $2}"
  97. fi
  98. }
  99. # Free-form info text — text in default mode, suppressed in JSON
  100. note() {
  101. [[ "$JSON_MODE" -eq 1 ]] && return 0
  102. [[ "$QUIET" -eq 1 ]] && return 0
  103. echo "$@"
  104. }
  105. emit_summary() {
  106. if [[ "$JSON_MODE" -eq 1 ]]; then
  107. printf '{"type":"summary","pass":%d,"fail":%d,"warn":%d,"info":%d,"first_fail":"%s"}\n' \
  108. "$PASS_COUNT" "$FAIL_COUNT" "$WARN_COUNT" "$INFO_COUNT" "$(_json_escape "$FIRST_FAIL")"
  109. else
  110. echo
  111. echo "=== SUMMARY ==="
  112. echo " PASS: $PASS_COUNT FAIL: $FAIL_COUNT WARN: $WARN_COUNT INFO: $INFO_COUNT"
  113. if [[ -n "$FIRST_FAIL" ]]; then
  114. echo " First failure: $FIRST_FAIL"
  115. else
  116. echo " No failures."
  117. fi
  118. fi
  119. }
  120. # Redact filter: same regex set as net-ops's, plus macOS-specific patterns.
  121. # Preserves Tailscale's 100.100.100.100 and public DNS anchors.
  122. redact_filter() {
  123. if [[ "$REDACT" -eq 0 ]]; then cat; return; fi
  124. perl -pe '
  125. s/100\.100\.100\.100/__TS_MAGIC__/g;
  126. s/\b10\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/10.X.X.X/g;
  127. s/\b172\.(1[6-9]|2[0-9]|3[01])\.\d{1,3}\.\d{1,3}\b/172.X.X.X/g;
  128. s/\b192\.168\.\d{1,3}\.\d{1,3}\b/192.168.X.X/g;
  129. s/\b100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.\d{1,3}\.\d{1,3}\b/100.X.X.X/g;
  130. s/\b169\.254\.\d{1,3}\.\d{1,3}\b/169.254.X.X/g;
  131. s/\b[0-9a-fA-F]{2}([:-])[0-9a-fA-F]{2}\1[0-9a-fA-F]{2}\1[0-9a-fA-F]{2}\1[0-9a-fA-F]{2}\1[0-9a-fA-F]{2}\b/XX:XX:XX:XX:XX:XX/g;
  132. s/\b[a-z0-9-]+\.ts\.net\b/REDACTED.ts.net/g;
  133. # macOS specifics: hostnames matching <name>.local or <name>.lan
  134. s/\b([a-zA-Z0-9-]+)\.local\b/HOSTNAME.local/g;
  135. # macOS serial numbers (12-char base32-ish, only when prefixed by Serial)
  136. s/Serial(?:Number)?[:= ]+\K[A-Z0-9]{10,14}/REDACTED/g;
  137. # UUIDs (long volume / device identifiers)
  138. s/\b[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\b/UUID-REDACTED/gi;
  139. s/__TS_MAGIC__/100.100.100.100/g;
  140. '
  141. }
  142. # Self-reinvoke filter — same pattern as net-ops to handle --redact + --json
  143. # compose. Strips --redact from inner argv to prevent infinite recursion.
  144. maybe_filter_self() {
  145. [[ "$REDACT" -eq 1 ]] || [[ "$JSON_MODE" -eq 1 ]] || return 0
  146. [[ "${_MACOPS_FILTERED:-0}" -eq 1 ]] && return 0
  147. export _MACOPS_FILTERED=1
  148. local cleaned_args=()
  149. for a in "$@"; do [[ "$a" != "--redact" ]] && cleaned_args+=("$a"); done
  150. if [[ "$JSON_MODE" -eq 1 ]] && [[ "$REDACT" -eq 1 ]]; then
  151. "$0" ${cleaned_args[@]+"${cleaned_args[@]}"} | grep '^{' | redact_filter
  152. elif [[ "$JSON_MODE" -eq 1 ]]; then
  153. "$0" ${cleaned_args[@]+"${cleaned_args[@]}"} | grep '^{'
  154. else
  155. "$0" ${cleaned_args[@]+"${cleaned_args[@]}"} | redact_filter
  156. fi
  157. exit "${PIPESTATUS[0]}"
  158. }
  159. # Convenience: macOS major version (12, 13, 14, 15, 26...)
  160. macos_major() {
  161. sw_vers -productVersion 2>/dev/null | awk -F. '{print $1}'
  162. }
  163. # Convenience: am I on Apple Silicon?
  164. is_apple_silicon() {
  165. [[ "$(uname -m)" == "arm64" ]]
  166. }