#!/usr/bin/env bash
set -Eeuo pipefail
# Usage:
# ./extract-leaf-oids.sh [output-file]
#
# Example:
# ./extract-leaf-oids.sh module_name.oid
#
# Output format:
# "objectName" "1.3.6.1.4.1...."
OUTPUT_FILE="${1:-leaf-oids.oid}"
DEFAULT_MIBDIRS="$HOME/.snmp/mibs\
:/usr/share/snmp/mibs\
:/usr/share/snmp/mibs/iana\
:/usr/share/snmp/mibs/ietf\
:/usr/share/mibs/site\
:/usr/share/mibs/iana\
:/usr/share/mibs/ietf\
:/usr/share/mibs/netsnmp\
:$PWD"
# You can override this by exporting MIBDIRS before running the script.
MIB_PATHS="${MIBDIRS:-$DEFAULT_MIBDIRS}"
TEMP_DIR="$(mktemp -d)"
RAW_FILE="$TEMP_DIR/all-oids.raw"
PARSED_FILE="$TEMP_DIR/all-oids.tsv"
LEAF_FILE="$TEMP_DIR/leaf-oids.tsv"
ERROR_FILE="${OUTPUT_FILE}.errors"
cleanup() {
rm -rf "$TEMP_DIR"
}
trap cleanup EXIT
if ! command -v snmptranslate >/dev/null 2>&1; then
echo "Error: snmptranslate is not installed or is not in PATH." >&2
exit 1
fi
echo "Loading all MIB modules..." >&2
if ! snmptranslate \
-Pu \
-Tz \
-M "$MIB_PATHS" \
-m ALL \
>"$RAW_FILE" 2>"$ERROR_FILE"
then
echo "Error: snmptranslate failed." >&2
echo "See: $ERROR_FILE" >&2
exit 1
fi
# Convert:
# "sysDescr" "1.3.6.1.2.1.1.1"
#
# Into:
# sysDescr<TAB>1.3.6.1.2.1.1.1
awk -F'"' '
NF >= 5 &&
$2 != "" &&
$4 ~ /^\.?[0-9]+(\.[0-9]+)*$/ {
oid = $4
sub(/^\./, "", oid)
print $2 "\t" oid
}
' "$RAW_FILE" >"$PARSED_FILE"
if [[ ! -s "$PARSED_FILE" ]]; then
echo "Error: no valid OIDs were parsed from snmptranslate output." >&2
echo "See: $ERROR_FILE" >&2
exit 1
fi
# An OID is not a leaf when it is a prefix/ancestor of another OID.
#
# Example:
# 1.3.6 non-leaf
# 1.3.6.1 non-leaf
# 1.3.6.1.2 leaf, provided no longer OID starts with 1.3.6.1.2.
awk -F'\t' '
BEGIN {
OFS = "\t"
}
{
name = $1
oid = $2
# Save each unique name/OID pair.
pair = name SUBSEP oid
if (!(pair in pair_seen)) {
pair_seen[pair] = 1
count++
names[count] = name
oids[count] = oid
}
oid_exists[oid] = 1
}
END {
# Mark every ancestor of every OID as a non-leaf.
for (oid in oid_exists) {
component_count = split(oid, components, ".")
prefix = components[1]
for (i = 2; i < component_count; i++) {
prefix = prefix "." components[i]
non_leaf[prefix] = 1
}
}
# Print only OIDs that were never marked as ancestors.
for (i = 1; i <= count; i++) {
if (!(oids[i] in non_leaf)) {
print names[i], oids[i]
}
}
}
' "$PARSED_FILE" >"$LEAF_FILE"
# Sort by numeric OID and restore the snmptranslate -Tz-like format.
LC_ALL=C sort \
-t$'\t' \
-k2,2V \
-k1,1 \
"$LEAF_FILE" |
awk -F'\t' '{
printf "\"%s\" \"%s\"\n", $1, $2
}' >"$OUTPUT_FILE"
TOTAL_COUNT="$(wc -l <"$PARSED_FILE")"
LEAF_COUNT="$(wc -l <"$OUTPUT_FILE")"
echo "Finished." >&2
echo "Parsed objects: $TOTAL_COUNT" >&2
echo "Leaf objects: $LEAF_COUNT" >&2
echo "Output: $OUTPUT_FILE" >&2
if [[ -s "$ERROR_FILE" ]]; then
echo "MIB parser warnings: $ERROR_FILE" >&2
else
rm -f "$ERROR_FILE"
fi