|
| 1 | +// Package spl contains pure helpers for shaping SPL queries before they are |
| 2 | +// sent to Splunk's REST API. |
| 3 | +package spl |
| 4 | + |
| 5 | +import ( |
| 6 | + "fmt" |
| 7 | + "strings" |
| 8 | +) |
| 9 | + |
| 10 | +// PrependMode controls how Wrap decides whether to add a leading "search " |
| 11 | +// command to a user-supplied SPL string. |
| 12 | +type PrependMode string |
| 13 | + |
| 14 | +const ( |
| 15 | + // ModeAuto skips the "search " prefix when the input already starts with |
| 16 | + // the "search" command (followed by whitespace or end-of-string) or with |
| 17 | + // a "|" pipe leader. Everything else (bare terms, quoted phrases, |
| 18 | + // backtick macros) gets the prefix. |
| 19 | + ModeAuto PrependMode = "auto" |
| 20 | + |
| 21 | + // ModePipeOnly skips the prefix only when the input starts with "|". |
| 22 | + // This matches the historical default — passing `search index=foo` |
| 23 | + // produces the double-search artifact `search search index=foo`. |
| 24 | + ModePipeOnly PrependMode = "pipe-only" |
| 25 | + |
| 26 | + // ModeOff never prepends. The caller must supply a complete SPL, |
| 27 | + // including any leading "search" / generating command / "|" leader. |
| 28 | + ModeOff PrependMode = "off" |
| 29 | +) |
| 30 | + |
| 31 | +// DefaultMode is the prepend mode used when neither config nor flag specifies |
| 32 | +// one. ModePipeOnly preserves historical splunk-cli behavior so existing |
| 33 | +// workflows keep working without change. |
| 34 | +const DefaultMode = ModePipeOnly |
| 35 | + |
| 36 | +// Wrap returns the SPL string ready for submission, applying "search " |
| 37 | +// prepend according to mode. The original input is preserved verbatim |
| 38 | +// when no prefix is added (whitespace and all). |
| 39 | +func Wrap(spl string, mode PrependMode) string { |
| 40 | + if mode == "" { |
| 41 | + mode = DefaultMode |
| 42 | + } |
| 43 | + if mode == ModeOff { |
| 44 | + return spl |
| 45 | + } |
| 46 | + trimmed := strings.TrimSpace(spl) |
| 47 | + if strings.HasPrefix(trimmed, "|") { |
| 48 | + return spl |
| 49 | + } |
| 50 | + if mode == ModeAuto && hasLeadingSearchCommand(trimmed) { |
| 51 | + return spl |
| 52 | + } |
| 53 | + return "search " + spl |
| 54 | +} |
| 55 | + |
| 56 | +// hasLeadingSearchCommand reports whether s begins with the SPL command |
| 57 | +// "search" followed by whitespace or end-of-string. Tokens that merely |
| 58 | +// start with "search" (for example searchengine=foo) return false. |
| 59 | +func hasLeadingSearchCommand(s string) bool { |
| 60 | + const kw = "search" |
| 61 | + if !strings.HasPrefix(s, kw) { |
| 62 | + return false |
| 63 | + } |
| 64 | + if len(s) == len(kw) { |
| 65 | + return true |
| 66 | + } |
| 67 | + switch s[len(kw)] { |
| 68 | + case ' ', '\t', '\n', '\r': |
| 69 | + return true |
| 70 | + } |
| 71 | + return false |
| 72 | +} |
| 73 | + |
| 74 | +// ParseMode normalizes user-supplied input (CLI flag value or TOML field) |
| 75 | +// into a PrependMode. Empty string maps to DefaultMode. Unknown values |
| 76 | +// return an error so misconfiguration fails loudly rather than silently |
| 77 | +// falling back. |
| 78 | +func ParseMode(s string) (PrependMode, error) { |
| 79 | + switch strings.ToLower(strings.TrimSpace(s)) { |
| 80 | + case "": |
| 81 | + return DefaultMode, nil |
| 82 | + case string(ModeAuto): |
| 83 | + return ModeAuto, nil |
| 84 | + case string(ModePipeOnly): |
| 85 | + return ModePipeOnly, nil |
| 86 | + case string(ModeOff): |
| 87 | + return ModeOff, nil |
| 88 | + } |
| 89 | + return "", fmt.Errorf("invalid prepend mode %q (want auto | pipe-only | off)", s) |
| 90 | +} |
0 commit comments