1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
|
#!/bin/sh
# Check that the daemon source and docs/PROTOCOL.md have not drifted apart:
#
# 1. every command in the `g_cmd_<domain>[]` tables (src/commands.c and
# src/cmd_*.c) is listed in a PROTOCOL.md table whose first column is
# "Command", and vice versa;
# 2. every error code passed to fail()/failf()/add_error() in src/*.c is
# documented in PROTOCOL.md section 5.3, and vice versa (documented but
# unused codes are warnings only, some are reserved).
#
# Run from the repo root: sh scripts/check-consistency.sh
# Exits 0 when there is no drift, 1 when drift is found, 2 on parse errors.
#
# Environment overrides (used by the self-tests):
# REPO_ROOT repository root, default "." (run from the repo root)
# COMMANDS_C primary command table source, default $REPO_ROOT/src/commands.c
# CMDS_GLOB shell glob of command table sources,
# default "$COMMANDS_C $REPO_ROOT/src/cmd_*.c"
# PROTOCOL_MD protocol document, default $REPO_ROOT/docs/PROTOCOL.md
# SRC_GLOB shell glob of sources scanned for error codes,
# default $REPO_ROOT/src/*.c
#
# Commands deliberately kept out of the PROTOCOL.md tables. Prefer documenting
# the command in a table over adding it here. Keep sorted, space separated.
ALLOW_UNDOCUMENTED_COMMANDS=""
set -eu
export LC_ALL=C
REPO_ROOT=${REPO_ROOT:-.}
COMMANDS_C=${COMMANDS_C:-$REPO_ROOT/src/commands.c}
CMDS_GLOB=${CMDS_GLOB:-$COMMANDS_C $REPO_ROOT/src/cmd_*.c}
PROTOCOL_MD=${PROTOCOL_MD:-$REPO_ROOT/docs/PROTOCOL.md}
SRC_GLOB=${SRC_GLOB:-$REPO_ROOT/src/*.c}
if [ ! -f "$COMMANDS_C" ]; then
echo "check-consistency: command source not found: $COMMANDS_C" >&2
exit 2
fi
if [ ! -f "$PROTOCOL_MD" ]; then
echo "check-consistency: protocol document not found: $PROTOCOL_MD" >&2
exit 2
fi
tmp=$(mktemp -d "${TMPDIR:-/tmp}/bokf-consistency-XXXXXX")
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
# --- extract what the code implements --------------------------------------
for f in $CMDS_GLOB; do
[ -f "$f" ] || continue
awk '
/const struct command g_cmd_[a-z_]+\[\][[:space:]]*=/ { in_table = 1; next }
in_table && /^[[:space:]]*\};/ { in_table = 0 }
in_table && /^[[:space:]]*\{[[:space:]]*"/ {
line = $0
sub(/^[^{]*\{[[:space:]]*"/, "", line)
sub(/".*/, "", line)
if (line != "")
print line
}
' "$f"
done | sort -u > "$tmp/impl_commands"
if [ ! -s "$tmp/impl_commands" ]; then
echo "check-consistency: no commands parsed from $CMDS_GLOB" >&2
exit 2
fi
# --- extract what the docs promise -----------------------------------------
# Only rows of tables whose header first cell is "Command" count; prose,
# argument names and other tables must not produce command names. The
# generated catalogue (gen_protocol) is skipped so every command still needs
# a hand-written table row with its result.
awk '
/<!-- generated:commands begin -->/ { skip = 1 }
/<!-- generated:commands end -->/ { skip = 0; next }
skip { next }
/^\|/ {
n = split($0, cells, "|")
cell = cells[2]
gsub(/^[[:space:]]+/, "", cell)
gsub(/[[:space:]]+$/, "", cell)
if (cell == "Command") { in_table = 1; next }
if (in_table) {
rest = cell
while (match(rest, /`[a-z][a-z0-9_.]*`/)) {
print substr(rest, RSTART + 1, RLENGTH - 2)
rest = substr(rest, RSTART + RLENGTH)
}
}
next
}
{ in_table = 0 }
' "$PROTOCOL_MD" | sort -u > "$tmp/doc_commands"
if [ ! -s "$tmp/doc_commands" ]; then
echo "check-consistency: no command tables parsed from $PROTOCOL_MD" >&2
exit 2
fi
# Error codes are the backticked ALL_CAPS words inside section 5.3.
awk '
/^###[[:space:]]+5\.3/ { in_section = 1; next }
in_section && /^###/ { in_section = 0 }
in_section {
rest = $0
while (match(rest, /`[A-Z][A-Z0-9_]+`/)) {
print substr(rest, RSTART + 1, RLENGTH - 2)
rest = substr(rest, RSTART + RLENGTH)
}
}
' "$PROTOCOL_MD" | sort -u > "$tmp/doc_codes"
if [ ! -s "$tmp/doc_codes" ]; then
echo "check-consistency: no error codes parsed from $PROTOCOL_MD 5.3" >&2
exit 2
fi
# --- compare ----------------------------------------------------------------
: > "$tmp/allow_commands"
for name in $ALLOW_UNDOCUMENTED_COMMANDS; do
printf '%s\n' "$name" >> "$tmp/allow_commands"
done
sort -u -o "$tmp/allow_commands" "$tmp/allow_commands"
cat "$tmp/doc_commands" "$tmp/allow_commands" | sort -u > "$tmp/known_commands"
comm -23 "$tmp/impl_commands" "$tmp/known_commands" > "$tmp/cmd_undocumented"
comm -13 "$tmp/impl_commands" "$tmp/doc_commands" > "$tmp/cmd_unimplemented"
for f in $SRC_GLOB; do
[ -f "$f" ] || continue
grep -E '(^|[^A-Za-z0-9_])(fail|failf|add_error)[[:space:]]*\(' "$f" |
grep -oE '"[A-Z][A-Z0-9_]{1,}"' || true
done | tr -d '"' | sort -u > "$tmp/used_codes"
if [ ! -s "$tmp/used_codes" ]; then
echo "check-consistency: no error-code call sites parsed from $SRC_GLOB" >&2
exit 2
fi
comm -23 "$tmp/used_codes" "$tmp/doc_codes" > "$tmp/code_undocumented"
comm -13 "$tmp/used_codes" "$tmp/doc_codes" > "$tmp/code_unused"
# --- report -----------------------------------------------------------------
status=0
problems=0
warnings=0
section() {
echo
echo "== $1 =="
}
section "commands: implemented in $CMDS_GLOB but not documented in $PROTOCOL_MD"
if [ -s "$tmp/cmd_undocumented" ]; then
while IFS= read -r name; do
echo "command: undocumented: $name"
problems=$((problems + 1))
done < "$tmp/cmd_undocumented"
status=1
else
echo "(none)"
fi
section "commands: documented in $PROTOCOL_MD but not implemented"
if [ -s "$tmp/cmd_unimplemented" ]; then
while IFS= read -r name; do
echo "command: documented but unimplemented: $name"
problems=$((problems + 1))
done < "$tmp/cmd_unimplemented"
status=1
else
echo "(none)"
fi
section "error codes: used in src but not documented in $PROTOCOL_MD 5.3"
if [ -s "$tmp/code_undocumented" ]; then
while IFS= read -r code; do
echo "error-code: undocumented: $code"
problems=$((problems + 1))
done < "$tmp/code_undocumented"
status=1
else
echo "(none)"
fi
section "warnings (not failures)"
if [ -s "$tmp/code_unused" ]; then
while IFS= read -r code; do
echo "warning: documented but unused in src: $code"
warnings=$((warnings + 1))
done < "$tmp/code_unused"
echo "(documented codes may be reserved for future use)"
else
echo "(none)"
fi
echo
if [ "$status" -eq 0 ]; then
echo "consistency: ok ($problems problems, $warnings warnings)"
else
echo "consistency: FAILED ($problems problems, $warnings warnings)"
fi
exit "$status"
|