#!/usr/bin/env sh # log-aggregate.sh — 把工作日誌頁的條目彙總成報表數字(供 jsc-log:report 呼叫)。 # # 用法: # log-aggregate.sh <起 yyyy-MM-dd> <訖 yyyy-MM-dd> [日誌頁檔案 ...] # 檔案省略時讀標準輸入。可以一次傳多份 LOG_{HASH} 的內容,數字會跨頁加總。 # # 輸入格式即 templates/log-entry.md 的固定版型: # 條目標題「## {yyyy-MM-dd HH:mm} {摘要}」,底下是「| 欄位 | 內容 |」表格。 # 只認四個欄位:存取庫名稱、花費時間、token 用量、任務狀態。 # 起訖含頭含尾,比對的是條目標題那個日期。 # # 輸出(一行一項,KEY=值;值有多欄時以 TAB 分隔): # ENTRIES=<期間內條目數> # REPOS=<相異存取庫數> # REPO= 每個存取庫一行,依字典序 # ELAPSED_MINUTES=<分鐘數|無資料> 有填花費時間的條目才加總 # ELAPSED_ENTRIES=<有填花費時間的條目數> # ELAPSED_MISSING=<沒填花費時間的條目數> # TOKEN= 每個 CLI 一行,依 CLI 名字典序 # TOKEN_MISSING= # STATUS=<狀態><條目數> 每個狀態一行,依字典序 # # 無資料不估算(本腳本的硬規則,不是散文建議): # 花費時間欄空白、填「無」或「無資料」的條目,不進 ELAPSED_MINUTES,只進 ELAPSED_MISSING。 # 四個條目只有一個有時間時,總和就是那一個的時間,不乘四也不取平均。 # token 欄填 N/A 的條目同理,只進 TOKEN_MISSING。 # 全部條目都沒有時間時,ELAPSED_MINUTES 印「無資料」,不印 0——0 會被讀成「花了 0 分鐘」。 # # 結束碼: 0=成功 2=用法錯誤 3=期間內沒有條目(仍印出全零結果)4=讀不到輸入檔 set -u usage() { echo '用法:log-aggregate.sh <起 yyyy-MM-dd> <訖 yyyy-MM-dd> [日誌頁檔案 ...]' >&2 exit 2 } start="${1:-}"; [ -n "$start" ] || usage shift end="${1:-}"; [ -n "$end" ] || usage shift for d in "$start" "$end"; do case "$d" in [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]) ;; *) echo "日期格式要是 yyyy-MM-dd:$d" >&2; exit 2 ;; esac done if [ "$(printf '%s\n%s\n' "$start" "$end" | sort | head -n1)" != "$start" ]; then echo "起始日期比結束日期晚:$start 到 $end" >&2 exit 2 fi for f in "$@"; do [ -r "$f" ] || { echo "讀不到日誌頁檔案:$f" >&2; exit 4; } done aggregate() { awk -v start="$start" -v end="$end" ' function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s } function sortkeys(arr, out, i, j, n, t, k) { n = 0 for (k in arr) { n++; out[n] = k } for (i = 1; i < n; i++) for (j = i + 1; j <= n; j++) if (out[j] < out[i]) { t = out[i]; out[i] = out[j]; out[j] = t } return n } # 花費時間欄轉分鐘。認得「{h} 小時 {m} 分」、「{h} 小時」、「{m} 分」三種寫法。 # 一個數字都抓不到就回 -1,交給呼叫端算成「無資料」,不猜。 function minutes(v, h, m, tmp) { h = -1; m = -1 tmp = v if (match(tmp, /[0-9]+[ ]*小時/)) { h = substr(tmp, RSTART, RLENGTH) + 0 } if (match(tmp, /[0-9]+[ ]*分/)) { m = substr(tmp, RSTART, RLENGTH) + 0 } if (h < 0 && m < 0) return -1 if (h < 0) h = 0 if (m < 0) m = 0 return h * 60 + m } # 狀態只認三個字面值,其餘一律歸「其他」。先比對「部分完成」,它包含「完成」。 function status_of(v) { if (index(v, "部分完成") > 0) return "部分完成" if (index(v, "阻塞") > 0) return "阻塞" if (index(v, "完成") > 0) return "完成" return "其他" } BEGIN { FS = "|" entries = 0; repos_n = 0 elapsed_total = 0; elapsed_entries = 0; elapsed_missing = 0 token_missing = 0 in_range = 0 } /^##[ \t]/ { in_range = 0 if (match($0, /[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/)) { d = substr($0, RSTART, RLENGTH) if (d >= start && d <= end) { in_range = 1; entries++ } } next } in_range == 1 && /^[ \t]*\|/ { key = trim($2); val = trim($3) if (key == "存取庫名稱") { if (val != "" && val !~ /^\{/) { if (!(val in repos)) { repos[val] = 1; repos_n++ } } } else if (key == "花費時間") { mm = minutes(val) if (mm < 0) { elapsed_missing++ } else { elapsed_total += mm; elapsed_entries++ } } else if (key == "token 用量" || key == "Token 用量") { rest = val; got = 0 while (match(rest, /[A-Za-z][A-Za-z0-9_-]*[ ]*:[ ]*[0-9]+[ ]*\/[ ]*[0-9]+/)) { pair = substr(rest, RSTART, RLENGTH) rest = substr(rest, RSTART + RLENGTH) split(pair, a, ":") cli = trim(a[1]) split(a[2], b, "/") tin[cli] += trim(b[1]) + 0 tout[cli] += trim(b[2]) + 0 got = 1 } if (got == 0) token_missing++ } else if (key == "任務狀態") { if (val != "" && val !~ /^\{/) { st[status_of(val)]++ } } } END { printf "ENTRIES=%d\n", entries printf "REPOS=%d\n", repos_n n = sortkeys(repos, sorted) for (i = 1; i <= n; i++) printf "REPO=%s\n", sorted[i] if (elapsed_entries > 0) printf "ELAPSED_MINUTES=%d\n", elapsed_total else printf "ELAPSED_MINUTES=%s\n", "無資料" printf "ELAPSED_ENTRIES=%d\n", elapsed_entries printf "ELAPSED_MISSING=%d\n", elapsed_missing n = sortkeys(tin, sorted) for (i = 1; i <= n; i++) printf "TOKEN=%s\t%d\t%d\n", sorted[i], tin[sorted[i]], tout[sorted[i]] printf "TOKEN_MISSING=%d\n", token_missing n = sortkeys(st, sorted) for (i = 1; i <= n; i++) printf "STATUS=%s\t%d\n", sorted[i], st[sorted[i]] exit (entries > 0 ? 0 : 3) } ' "$@" } if [ "$#" -gt 0 ]; then aggregate "$@" else aggregate - fi