350 lines
9.3 KiB
Bash
Executable File
350 lines
9.3 KiB
Bash
Executable File
#!/bin/bash
|
|
# 统一来源总表读取与验证脚本
|
|
# 权威数据文件:source-registry.tsv(来源定义)、source-record-contract.tsv(字段契约)
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
CONTRACT_FILE="$SCRIPT_DIR/source-record-contract.tsv"
|
|
REGISTRY_FILE="$SCRIPT_DIR/source-registry.tsv"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
用法:
|
|
bash scripts/source-registry.sh fields
|
|
bash scripts/source-registry.sh list
|
|
bash scripts/source-registry.sh get <source_id>
|
|
bash scripts/source-registry.sh match-url <url>
|
|
bash scripts/source-registry.sh match-file <path>
|
|
bash scripts/source-registry.sh list-by-category <core_builtin|optional_adapter|manual_only>
|
|
bash scripts/source-registry.sh unique-dependencies <bundled|install_time|none>
|
|
bash scripts/source-registry.sh validate
|
|
EOF
|
|
}
|
|
|
|
require_file() {
|
|
local file="$1"
|
|
|
|
[ -f "$file" ] || {
|
|
echo "缺少文件:$file" >&2
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
expect_header() {
|
|
local file="$1"
|
|
local expected="$2"
|
|
local actual
|
|
|
|
actual="$(head -n 1 "$file")"
|
|
[ "$actual" = "$expected" ] || {
|
|
echo "表头不匹配:$file" >&2
|
|
echo "期望:$expected" >&2
|
|
echo "实际:$actual" >&2
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
validate_contract() {
|
|
require_file "$CONTRACT_FILE"
|
|
expect_header "$CONTRACT_FILE" $'field_name\trequiredness\tfilled_by\tvalue_rule'
|
|
|
|
awk -F '\t' '
|
|
BEGIN {
|
|
required["source_id"] = 1
|
|
required["source_label"] = 1
|
|
required["source_category"] = 1
|
|
required["input_mode"] = 1
|
|
required["raw_dir"] = 1
|
|
required["original_ref"] = 1
|
|
required["ingest_text"] = 1
|
|
required["adapter_name"] = 1
|
|
required["fallback_hint"] = 1
|
|
}
|
|
NR == 1 { next }
|
|
{
|
|
if ($1 == "" || $2 == "" || $3 == "" || $4 == "") {
|
|
printf("source-record-contract.tsv 第 %d 行存在空字段\n", NR) > "/dev/stderr"
|
|
failed = 1
|
|
}
|
|
|
|
seen[$1] += 1
|
|
}
|
|
END {
|
|
for (field in required) {
|
|
if (seen[field] != 1) {
|
|
printf("source-record-contract.tsv 缺少或重复字段:%s\n", field) > "/dev/stderr"
|
|
failed = 1
|
|
}
|
|
}
|
|
|
|
exit failed ? 1 : 0
|
|
}
|
|
' "$CONTRACT_FILE"
|
|
}
|
|
|
|
validate_registry() {
|
|
require_file "$REGISTRY_FILE"
|
|
expect_header "$REGISTRY_FILE" $'source_id\tsource_label\tsource_category\tinput_mode\tmatch_rule\traw_dir\tadapter_name\tdependency_name\tdependency_type\tfallback_hint'
|
|
|
|
awk -F '\t' '
|
|
NR == 1 { next }
|
|
{
|
|
if ($1 == "" || $2 == "" || $3 == "" || $4 == "" || $5 == "" || $6 == "" || $10 == "") {
|
|
printf("source-registry.tsv 第 %d 行存在空字段\n", NR) > "/dev/stderr"
|
|
failed = 1
|
|
}
|
|
|
|
if ($3 != "core_builtin" && $3 != "optional_adapter" && $3 != "manual_only") {
|
|
printf("source-registry.tsv 第 %d 行存在未知分类:%s\n", NR, $3) > "/dev/stderr"
|
|
failed = 1
|
|
}
|
|
|
|
if ($4 != "url" && $4 != "file" && $4 != "text" && $4 != "asset") {
|
|
printf("source-registry.tsv 第 %d 行存在未知输入模式:%s\n", NR, $4) > "/dev/stderr"
|
|
failed = 1
|
|
}
|
|
|
|
if ($4 == "url" && $5 !~ /^url_host:/) {
|
|
printf("source-registry.tsv 第 %d 行 URL 来源必须声明 url_host 规则:%s\n", NR, $5) > "/dev/stderr"
|
|
failed = 1
|
|
}
|
|
|
|
if ($4 == "file" && $5 !~ /^file_ext:/) {
|
|
printf("source-registry.tsv 第 %d 行文件来源必须声明 file_ext 规则:%s\n", NR, $5) > "/dev/stderr"
|
|
failed = 1
|
|
}
|
|
|
|
if ($4 == "text" && $5 !~ /^text:/) {
|
|
printf("source-registry.tsv 第 %d 行文本来源必须声明 text 规则:%s\n", NR, $5) > "/dev/stderr"
|
|
failed = 1
|
|
}
|
|
|
|
if ($4 == "asset" && $5 !~ /^asset:/) {
|
|
printf("source-registry.tsv 第 %d 行附件来源必须声明 asset 规则:%s\n", NR, $5) > "/dev/stderr"
|
|
failed = 1
|
|
}
|
|
|
|
if ($6 !~ /^raw\//) {
|
|
printf("source-registry.tsv 第 %d 行 raw_dir 必须位于 raw/ 下:%s\n", NR, $6) > "/dev/stderr"
|
|
failed = 1
|
|
}
|
|
|
|
if (seen[$1]++) {
|
|
printf("source-registry.tsv source_id 重复:%s\n", $1) > "/dev/stderr"
|
|
failed = 1
|
|
}
|
|
|
|
category_seen[$3] = 1
|
|
|
|
if ($3 == "optional_adapter") {
|
|
if ($7 == "-" || $8 == "-" || $9 == "none") {
|
|
printf("source-registry.tsv 第 %d 行 optional_adapter 缺少依赖信息\n", NR) > "/dev/stderr"
|
|
failed = 1
|
|
}
|
|
} else if ($7 != "-" || $8 != "-" || $9 != "none") {
|
|
printf("source-registry.tsv 第 %d 行非外挂来源不应声明依赖\n", NR) > "/dev/stderr"
|
|
failed = 1
|
|
}
|
|
}
|
|
END {
|
|
if (!category_seen["core_builtin"]) {
|
|
print "source-registry.tsv 缺少 core_builtin 来源" > "/dev/stderr"
|
|
failed = 1
|
|
}
|
|
|
|
if (!category_seen["optional_adapter"]) {
|
|
print "source-registry.tsv 缺少 optional_adapter 来源" > "/dev/stderr"
|
|
failed = 1
|
|
}
|
|
|
|
if (!category_seen["manual_only"]) {
|
|
print "source-registry.tsv 缺少 manual_only 来源" > "/dev/stderr"
|
|
failed = 1
|
|
}
|
|
|
|
exit failed ? 1 : 0
|
|
}
|
|
' "$REGISTRY_FILE"
|
|
}
|
|
|
|
print_contract() {
|
|
validate_contract
|
|
cat "$CONTRACT_FILE"
|
|
}
|
|
|
|
print_registry() {
|
|
validate_registry
|
|
cat "$REGISTRY_FILE"
|
|
}
|
|
|
|
get_source() {
|
|
local source_id="$1"
|
|
|
|
validate_registry
|
|
|
|
awk -F '\t' -v source_id="$source_id" '
|
|
NR == 1 { next }
|
|
$1 == source_id {
|
|
print
|
|
found = 1
|
|
}
|
|
END {
|
|
exit found ? 0 : 1
|
|
}
|
|
' "$REGISTRY_FILE"
|
|
}
|
|
|
|
extract_url_host() {
|
|
local url="$1"
|
|
local rest host
|
|
|
|
rest="${url#*://}"
|
|
if [ "$rest" = "$url" ]; then
|
|
rest="$url"
|
|
fi
|
|
|
|
rest="${rest#*@}"
|
|
host="${rest%%/*}"
|
|
host="${host%%\?*}"
|
|
host="${host%%#*}"
|
|
host="${host%%:*}"
|
|
|
|
printf '%s\n' "$host" | tr '[:upper:]' '[:lower:]'
|
|
}
|
|
|
|
host_matches_pattern() {
|
|
local host="$1"
|
|
local pattern="$2"
|
|
|
|
case "$host" in
|
|
"$pattern"|*."$pattern")
|
|
return 0
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
match_url() {
|
|
local url="$1"
|
|
local host row source_id source_label source_category input_mode match_rule raw_dir adapter_name dependency_name dependency_type fallback_hint
|
|
local fallback_row=""
|
|
local pattern pattern_list
|
|
|
|
validate_registry
|
|
host="$(extract_url_host "$url")"
|
|
|
|
while IFS=$'\t' read -r source_id source_label source_category input_mode match_rule raw_dir adapter_name dependency_name dependency_type fallback_hint; do
|
|
[ "$source_id" = "source_id" ] && continue
|
|
[ "$input_mode" = "url" ] || continue
|
|
|
|
pattern_list="${match_rule#url_host:}"
|
|
if [ "$pattern_list" = "*" ]; then
|
|
fallback_row="$(printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$source_id" "$source_label" "$source_category" "$input_mode" "$match_rule" "$raw_dir" "$adapter_name" "$dependency_name" "$dependency_type" "$fallback_hint")"
|
|
continue
|
|
fi
|
|
|
|
for pattern in ${pattern_list//,/ }; do
|
|
if host_matches_pattern "$host" "$pattern"; then
|
|
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$source_id" "$source_label" "$source_category" "$input_mode" "$match_rule" "$raw_dir" "$adapter_name" "$dependency_name" "$dependency_type" "$fallback_hint"
|
|
return 0
|
|
fi
|
|
done
|
|
done < "$REGISTRY_FILE"
|
|
|
|
[ -n "$fallback_row" ] || return 1
|
|
printf '%s\n' "$fallback_row"
|
|
}
|
|
|
|
match_file() {
|
|
local path="$1"
|
|
local lowered_path source_id source_label source_category input_mode match_rule raw_dir adapter_name dependency_name dependency_type fallback_hint
|
|
local extension_list extension
|
|
|
|
validate_registry
|
|
lowered_path="$(printf '%s\n' "$path" | tr '[:upper:]' '[:lower:]')"
|
|
|
|
while IFS=$'\t' read -r source_id source_label source_category input_mode match_rule raw_dir adapter_name dependency_name dependency_type fallback_hint; do
|
|
[ "$source_id" = "source_id" ] && continue
|
|
[ "$input_mode" = "file" ] || continue
|
|
|
|
extension_list="${match_rule#file_ext:}"
|
|
for extension in ${extension_list//,/ }; do
|
|
case "$lowered_path" in
|
|
*"$extension")
|
|
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$source_id" "$source_label" "$source_category" "$input_mode" "$match_rule" "$raw_dir" "$adapter_name" "$dependency_name" "$dependency_type" "$fallback_hint"
|
|
return 0
|
|
;;
|
|
esac
|
|
done
|
|
done < "$REGISTRY_FILE"
|
|
|
|
return 1
|
|
}
|
|
|
|
list_by_category() {
|
|
local category="$1"
|
|
|
|
validate_registry
|
|
|
|
awk -F '\t' -v category="$category" '
|
|
NR == 1 { next }
|
|
$3 == category { print }
|
|
' "$REGISTRY_FILE"
|
|
}
|
|
|
|
list_unique_dependencies() {
|
|
local dependency_type="$1"
|
|
|
|
validate_registry
|
|
|
|
awk -F '\t' -v dependency_type="$dependency_type" '
|
|
NR == 1 { next }
|
|
$9 == dependency_type && $8 != "-" { print $8 }
|
|
' "$REGISTRY_FILE" | sort -u
|
|
}
|
|
|
|
command_name="${1:-}"
|
|
|
|
case "$command_name" in
|
|
fields)
|
|
[ "$#" -eq 1 ] || { usage; exit 1; }
|
|
print_contract
|
|
;;
|
|
list)
|
|
[ "$#" -eq 1 ] || { usage; exit 1; }
|
|
print_registry
|
|
;;
|
|
get)
|
|
[ "$#" -eq 2 ] || { usage; exit 1; }
|
|
get_source "$2"
|
|
;;
|
|
match-url)
|
|
[ "$#" -eq 2 ] || { usage; exit 1; }
|
|
match_url "$2"
|
|
;;
|
|
match-file)
|
|
[ "$#" -eq 2 ] || { usage; exit 1; }
|
|
match_file "$2"
|
|
;;
|
|
list-by-category)
|
|
[ "$#" -eq 2 ] || { usage; exit 1; }
|
|
list_by_category "$2"
|
|
;;
|
|
unique-dependencies)
|
|
[ "$#" -eq 2 ] || { usage; exit 1; }
|
|
list_unique_dependencies "$2"
|
|
;;
|
|
validate)
|
|
[ "$#" -eq 1 ] || { usage; exit 1; }
|
|
validate_contract
|
|
validate_registry
|
|
;;
|
|
*)
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|