In Part 1, we set up the skeleton. Now let’s give it bones.
We’ll add a scan command that reads a config file and does something marginally useful.
Adding a Subcommand
// cmd/scan.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var scanCmd = &cobra.Command{
Use: "scan [path]",
Short: "Scan a directory for issues",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
path := args[0]
verbose, _ := cmd.Flags().GetBool("verbose")
if verbose {
fmt.Printf("Scanning %s with config: %s\n", path, viper.ConfigFileUsed())
}
return runScan(path)
},
}
func init() {
scanCmd.Flags().BoolP("verbose", "v", false, "enable verbose output")
rootCmd.AddCommand(scanCmd)
}
Configuration with Viper
Viper gives us config file support, environment variable binding, and flag overrides — all with zero drama:
// cmd/config.go
package cmd
import (
"github.com/spf13/viper"
)
func initConfig() {
viper.SetConfigName(".ddd")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AddConfigPath("$HOME")
viper.AutomaticEnv()
viper.SetEnvPrefix("DDD")
_ = viper.ReadInConfig()
}
# .ddd.yaml
scan:
exclude:
- vendor/
- .git/
max_depth: 10
output:
format: "json"
color: true
Testing Commands
// cmd/scan_test.go
package cmd
import (
"bytes"
"testing"
)
func TestScanCommand(t *testing.T) {
buf := new(bytes.Buffer)
rootCmd.SetOut(buf)
rootCmd.SetArgs([]string{"scan", ".", "--verbose"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
Up Next
In Part 3, we’ll add proper integration tests and set up GoReleaser for automated releases.
fparch/ddd-cli View the complete source on GitHub ↗Building internal tools or developer platforms? I help teams ship faster. fitzpatricksoftware.com
I help teams modernize legacy systems with AI-assisted development guardrails that keep the agents productive and the production safe. fitzpatricksoftware.com
