114 lines
2.7 KiB
Go
114 lines
2.7 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// loadJSON parses smartctl -j output. If leading noise precedes the object
|
|
// (rare, but some controllers emit warnings before the JSON), it retries from
|
|
// the first '{'.
|
|
func loadJSON(raw string) map[string]interface{} {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return nil
|
|
}
|
|
var m map[string]interface{}
|
|
if err := json.Unmarshal([]byte(raw), &m); err == nil {
|
|
return m
|
|
}
|
|
if i := strings.IndexByte(raw, '{'); i >= 0 {
|
|
if err := json.Unmarshal([]byte(raw[i:]), &m); err == nil {
|
|
return m
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// jObj navigates nested maps by key path, returning the leaf map or nil.
|
|
func jObj(m map[string]interface{}, keys ...string) map[string]interface{} {
|
|
cur := m
|
|
for _, k := range keys {
|
|
if cur == nil {
|
|
return nil
|
|
}
|
|
v, ok := cur[k].(map[string]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
cur = v
|
|
}
|
|
return cur
|
|
}
|
|
|
|
// jInt returns an *int for a numeric leaf (JSON numbers decode as float64).
|
|
func jInt(m map[string]interface{}, keys ...string) *int {
|
|
v := jLeaf(m, keys...)
|
|
switch t := v.(type) {
|
|
case float64:
|
|
n := int(t)
|
|
return &n
|
|
case string:
|
|
if n, err := strconv.Atoi(strings.TrimSpace(t)); err == nil {
|
|
return &n
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// jStr returns a trimmed string leaf, or "".
|
|
func jStr(m map[string]interface{}, keys ...string) string {
|
|
if s, ok := jLeaf(m, keys...).(string); ok {
|
|
return strings.TrimSpace(s)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// jBoolPtr returns *bool for a boolean leaf.
|
|
func jBoolPtr(m map[string]interface{}, keys ...string) *bool {
|
|
if b, ok := jLeaf(m, keys...).(bool); ok {
|
|
return &b
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// jLeaf returns the raw value at the key path (the final key looked up in its
|
|
// parent map), or nil when any segment along the path is missing.
|
|
func jLeaf(m map[string]interface{}, keys ...string) interface{} {
|
|
if len(keys) == 0 {
|
|
return nil
|
|
}
|
|
parent := jObj(m, keys[:len(keys)-1]...)
|
|
if parent == nil {
|
|
return nil
|
|
}
|
|
return parent[keys[len(keys)-1]]
|
|
}
|
|
|
|
var leadingInt = regexp.MustCompile(`^\s*(\d+)`)
|
|
|
|
// firstInt extracts the leading run of digits from a string ("345 hours" -> 345).
|
|
// It stops at the first non-digit, so for comma-grouped numbers ("12,345") use
|
|
// parseIntLoose, which strips separators first.
|
|
func firstInt(s string) (int, bool) {
|
|
m := leadingInt.FindStringSubmatch(s)
|
|
if m == nil {
|
|
return 0, false
|
|
}
|
|
n, err := strconv.Atoi(m[1])
|
|
return n, err == nil
|
|
}
|
|
|
|
// parseIntLoose strips commas/spaces and parses an integer anywhere in s.
|
|
func parseIntLoose(s string) (int, bool) {
|
|
s = strings.TrimSpace(strings.ReplaceAll(s, ",", ""))
|
|
// Take the leading run of digits (and optional sign).
|
|
m := regexp.MustCompile(`-?\d+`).FindString(s)
|
|
if m == "" {
|
|
return 0, false
|
|
}
|
|
n, err := strconv.Atoi(m)
|
|
return n, err == nil
|
|
}
|