Skip to content

Commit 664c740

Browse files
committed
refactor(cli): Separate commands into the cmd package
- Create a new `cmd` package to house all command-line logic. - Move each subcommand (run, start, status, results, help) into its own file within the `cmd` package (e.g., `cmd/run.go`). - Create `cmd/common.go` for shared helper functions. - The main `splunk-cli.go` now only contains the `main` function, which calls `cmd.Execute()`. This completes Stage 2 of the refactoring, making the CLI structure significantly more modular, scalable, and easier to maintain.
1 parent e505d38 commit 664c740

8 files changed

Lines changed: 518 additions & 450 deletions

File tree

cmd/common.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package cmd
2+
3+
import (
4+
"bufio"
5+
"errors"
6+
"flag"
7+
"fmt"
8+
"io"
9+
"os"
10+
"runtime"
11+
"strings"
12+
"syscall"
13+
14+
"splunk_cli/splunk"
15+
16+
"golang.org/x/term"
17+
)
18+
19+
// addCommonFlags defines flags common to all subcommands.
20+
func addCommonFlags(fs *flag.FlagSet, cfg *splunk.Config) {
21+
fs.StringVar(&cfg.Host, "host", cfg.Host, "Splunk server URL (or use SPLUNK_HOST env var)")
22+
fs.StringVar(&cfg.Token, "token", cfg.Token, "Splunk authentication token (or use SPLUNK_TOKEN env var)")
23+
fs.StringVar(&cfg.User, "user", cfg.User, "Splunk username (or use SPLUNK_USER env var)")
24+
fs.StringVar(&cfg.Password, "password", cfg.Password, "Splunk password (or use SPLUNK_PASSWORD env var)")
25+
fs.StringVar(&cfg.App, "app", cfg.App, "App context for the search (or use SPLUNK_APP env var)")
26+
fs.BoolVar(&cfg.Insecure, "insecure", cfg.Insecure, "Skip TLS certificate verification")
27+
fs.DurationVar(&cfg.HTTPTimeout, "http-timeout", cfg.HTTPTimeout, "Timeout for individual HTTP requests (e.g., '5s', '1m')")
28+
fs.BoolVar(&cfg.Debug, "debug", false, "Enable verbose debug logging")
29+
}
30+
31+
// getChoiceFromTTY reads a single line of input from the terminal, bypassing stdin.
32+
func getChoiceFromTTY() string {
33+
var reader *bufio.Reader
34+
if runtime.GOOS == "windows" {
35+
reader = bufio.NewReader(os.Stdin)
36+
} else {
37+
tty, err := os.Open("/dev/tty")
38+
if err != nil {
39+
fmt.Fprintf(os.Stderr, "Warning: could not open /dev/tty, falling back to stdin: %v", err)
40+
reader = bufio.NewReader(os.Stdin)
41+
} else {
42+
defer tty.Close()
43+
reader = bufio.NewReader(tty)
44+
}
45+
}
46+
choice, _ := reader.ReadString('\n')
47+
return strings.TrimSpace(choice)
48+
}
49+
50+
func printDebugConfig(cfg *splunk.Config, log *splunk.Logger) {
51+
maskedToken := ""
52+
if len(cfg.Token) > 8 {
53+
maskedToken = "toke..." + cfg.Token[len(cfg.Token)-4:]
54+
}
55+
maskedPassword := ""
56+
if cfg.Password != "" {
57+
maskedPassword = "********"
58+
}
59+
log.Debugf("Final configuration:")
60+
log.Debugf(" Host: %s", cfg.Host)
61+
log.Debugf(" Token: %s", maskedToken)
62+
log.Debugf(" User: %s", cfg.User)
63+
log.Debugf(" Password: %s", maskedPassword)
64+
log.Debugf(" App: %s", cfg.App)
65+
log.Debugf(" Insecure: %t", cfg.Insecure)
66+
log.Debugf(" HTTP Timeout: %s", cfg.HTTPTimeout)
67+
}
68+
69+
func promptForCredentials(cfg *splunk.Config) error {
70+
if cfg.Token != "" || (cfg.User != "" && cfg.Password != "") {
71+
return nil
72+
}
73+
74+
if cfg.User == "" {
75+
fmt.Fprintln(os.Stderr, "Authentication credentials were not provided.")
76+
fmt.Fprint(os.Stderr, "Enter Splunk authentication token: ")
77+
byteToken, err := term.ReadPassword(int(syscall.Stdin))
78+
if err != nil {
79+
return fmt.Errorf("could not read token: %w", err)
80+
}
81+
cfg.Token = string(byteToken)
82+
fmt.Fprintln(os.Stderr)
83+
} else if cfg.Password == "" {
84+
fmt.Fprintf(os.Stderr, "Enter Splunk password for '%s': ", cfg.User)
85+
bytePass, err := term.ReadPassword(int(syscall.Stdin))
86+
if err != nil {
87+
return fmt.Errorf("could not read password: %w", err)
88+
}
89+
cfg.Password = string(bytePass)
90+
fmt.Fprintln(os.Stderr)
91+
}
92+
return nil
93+
}
94+
95+
// getSplQuery determines the SPL query from either the --spl flag or --file flag.
96+
func getSplQuery(splFlag, fileFlag string) (string, error) {
97+
if splFlag != "" && fileFlag != "" {
98+
return "", errors.New("--spl and --file flags cannot be used at the same time")
99+
}
100+
if splFlag != "" {
101+
return splFlag, nil
102+
}
103+
if fileFlag != "" {
104+
var splBytes []byte
105+
var err error
106+
if fileFlag == "-" {
107+
splBytes, err = io.ReadAll(os.Stdin)
108+
} else {
109+
splBytes, err = os.ReadFile(fileFlag)
110+
}
111+
if err != nil {
112+
return "", fmt.Errorf("failed to read SPL from file '%s': %w", fileFlag, err)
113+
}
114+
return string(splBytes), nil
115+
}
116+
return "", errors.New("--spl or --file flag is required")
117+
}

cmd/help.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package cmd
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"os"
7+
8+
"splunk_cli/splunk"
9+
)
10+
11+
func printUsage() {
12+
fmt.Fprintln(os.Stderr, "Usage: splunk-cli [global options] <command> [options]")
13+
fmt.Fprintln(os.Stderr, "\nA flexible CLI tool to interact with the Splunk REST API.")
14+
fmt.Fprintln(os.Stderr, "\nGlobal Options:")
15+
fmt.Fprintln(os.Stderr, " --config <path> Path to a custom configuration file")
16+
fmt.Fprintln(os.Stderr, " --version Print version information and exit")
17+
fmt.Fprintln(os.Stderr, "\nCommands:")
18+
fmt.Fprintln(os.Stderr, " run Run a search job synchronously and wait for results.")
19+
fmt.Fprintln(os.Stderr, " start Start a search job and print the SID immediately.")
20+
fmt.Fprintln(os.Stderr, " status Check the status of a running search job.")
21+
fmt.Fprintln(os.Stderr, " results Get the results of a completed search job.")
22+
fmt.Fprintln(os.Stderr, " help Show help for a specific command.")
23+
fmt.Fprintln(os.Stderr, "\nUse 'splunk-cli help <command>' for more information about a specific command.")
24+
}
25+
26+
func printHelp(args []string) {
27+
if len(args) == 0 {
28+
printUsage()
29+
return
30+
}
31+
cmd := args[0]
32+
var fs *flag.FlagSet
33+
dummyCfg := splunk.Config{}
34+
35+
// Create a global FlagSet to include --config and --version for help output
36+
globalFs := flag.NewFlagSet("global", flag.ContinueOnError)
37+
globalFs.String("config", "", "Path to a custom configuration file")
38+
globalFs.Bool("version", false, "Print version information and exit") // Also include version here for consistency
39+
40+
switch cmd {
41+
case "run":
42+
fs = flag.NewFlagSet("run", flag.ExitOnError)
43+
fs.String("spl", "", "SPL query to execute (cannot be used with --file)")
44+
fs.String("file", "", "Read SPL from a file ('-' for stdin)")
45+
fs.String("f", "", "Shorthand for --file")
46+
fs.String("earliest", "", "Search earliest time")
47+
fs.String("latest", "", "Search latest time")
48+
fs.Duration("timeout", 0, "Timeout for the run command")
49+
fs.Bool("silent", false, "Suppress progress messages")
50+
case "start":
51+
fs = flag.NewFlagSet("start", flag.ExitOnError)
52+
fs.String("spl", "", "SPL query to execute (cannot be used with --file)")
53+
fs.String("file", "", "Read SPL from a file ('-' for stdin)")
54+
fs.String("f", "", "Shorthand for --file")
55+
fs.String("earliest", "", "Search earliest time")
56+
fs.String("latest", "", "Search latest time")
57+
fs.Bool("silent", false, "Suppress progress messages")
58+
case "status":
59+
fs = flag.NewFlagSet("status", flag.ContinueOnError)
60+
fs.String("sid", "", "Search ID (SID) of the job")
61+
case "results":
62+
fs = flag.NewFlagSet("results", flag.ContinueOnError)
63+
fs.String("sid", "", "Search ID (SID) of the job")
64+
default:
65+
fmt.Fprintf(os.Stderr, "Error: Unknown command for help: %s", cmd)
66+
return
67+
}
68+
addCommonFlags(fs, &dummyCfg)
69+
fmt.Fprintf(os.Stderr, "Usage: splunk-cli %s [options]\n\nOptions for %s:\n", cmd, cmd)
70+
fs.PrintDefaults()
71+
fmt.Fprintln(os.Stderr, "\nGlobal Options:") // Print global options after command-specific ones
72+
globalFs.PrintDefaults()
73+
}

cmd/results.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package cmd
2+
3+
import (
4+
"errors"
5+
"flag"
6+
"fmt"
7+
8+
"splunk_cli/splunk"
9+
)
10+
11+
func resultsCmd(args []string, baseCfg splunk.Config) error {
12+
fs := flag.NewFlagSet("results", flag.ExitOnError)
13+
sid := fs.String("sid", "", "Search ID (SID) of the job")
14+
silent := fs.Bool("silent", false, "Suppress progress messages")
15+
addCommonFlags(fs, &baseCfg)
16+
fs.Parse(args)
17+
18+
if *sid == "" {
19+
return errors.New("--sid is a required argument for 'results'")
20+
}
21+
if baseCfg.Host == "" {
22+
return errors.New("--host is required")
23+
}
24+
if err := promptForCredentials(&baseCfg); err != nil {
25+
return err
26+
}
27+
28+
client, err := splunk.NewClient(&baseCfg, *silent)
29+
if err != nil {
30+
return err
31+
}
32+
if baseCfg.Debug {
33+
printDebugConfig(&baseCfg, client.Log)
34+
}
35+
36+
done, jobState, _, err := client.JobStatus(*sid)
37+
if err != nil {
38+
return err
39+
}
40+
if !done {
41+
return fmt.Errorf("job %s is not complete yet (state: %s)", *sid, jobState)
42+
}
43+
if jobState == "FAILED" {
44+
return fmt.Errorf("cannot get results, job %s failed", *sid)
45+
}
46+
47+
client.Log.Println("Fetching results...")
48+
results, err := client.Results(*sid)
49+
if err != nil {
50+
return err
51+
}
52+
fmt.Println(results)
53+
return nil
54+
}

cmd/root.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package cmd
2+
3+
import (
4+
"errors"
5+
"flag"
6+
"fmt"
7+
"os"
8+
"strings"
9+
"time"
10+
11+
"splunk_cli/splunk"
12+
)
13+
14+
// These variables are set by the linker.
15+
var (
16+
Version = "dev"
17+
Commit = "none"
18+
Date = "unknown"
19+
)
20+
21+
func Execute() {
22+
var showVersion bool
23+
var configPath string
24+
25+
flag.BoolVar(&showVersion, "version", false, "Print version information and exit")
26+
flag.StringVar(&configPath, "config", "", "Path to a custom configuration file")
27+
flag.Parse()
28+
29+
if showVersion {
30+
fmt.Printf("splunk-cli version %s\ncommit %s\nbuilt at %s", Version, Commit, Date)
31+
os.Exit(0)
32+
}
33+
34+
if len(os.Args) < 2 {
35+
printUsage()
36+
os.Exit(1)
37+
}
38+
39+
log := &splunk.Logger{}
40+
baseCfg, cfgPath, err := splunk.LoadConfigFromFile(configPath)
41+
if err != nil {
42+
log.Printf("Warning: could not load config file at %s: %v", cfgPath, err)
43+
}
44+
45+
if baseCfg.HTTPTimeout == 0 {
46+
baseCfg.HTTPTimeout = 30 * time.Second
47+
}
48+
49+
splunk.ProcessEnvVars(&baseCfg)
50+
51+
var cmdErr error
52+
switch os.Args[1] {
53+
case "run":
54+
cmdErr = runCmd(os.Args[2:], baseCfg)
55+
case "start":
56+
cmdErr = startCmd(os.Args[2:], baseCfg)
57+
case "status":
58+
cmdErr = statusCmd(os.Args[2:], baseCfg)
59+
case "results":
60+
cmdErr = resultsCmd(os.Args[2:], baseCfg)
61+
case "help":
62+
printHelp(os.Args[2:])
63+
case "--help", "-h":
64+
printUsage()
65+
default:
66+
if len(os.Args) > 1 && strings.HasPrefix(os.Args[1], "-") {
67+
printUsage()
68+
cmdErr = errors.New("a command (run, start, etc.) is required before flags")
69+
} else {
70+
cmdErr = fmt.Errorf("unknown command: %s", os.Args[1])
71+
}
72+
}
73+
74+
if cmdErr != nil {
75+
fmt.Fprintf(os.Stderr, "Error: %v", cmdErr)
76+
os.Exit(1)
77+
}
78+
}

0 commit comments

Comments
 (0)