CLI Applications

About Hiboot CLI Application

Hiboot CLI application is built on top of Cobra with Hiboot’s dependency injection and auto configuration capabilities.

Features

  • Dependency injection for commands
  • Sub-command support with hierarchical structure
  • Flag parsing and validation
  • Property value injection
  • Auto configuration support

Project Structure

.
├── cmd
│   ├── root.go
│   ├── root_test.go
│   ├── sub.go
│   └── sub_test.go
├── config
│   └── application.yml
├── main.go
└── main_test.go

Basic Example

main.go

package main

import (
	"github.com/hidevopsio/hiboot/pkg/app"
	"github.com/hidevopsio/hiboot/pkg/app/cli"
)

func main() {
	cli.NewApplication(NewRootCommand).
		SetProperty(app.BannerDisabled, true).
		Run()
}

Root Command

Every CLI application has a root command. Embed cli.RootCommand in your struct to mark it as the root:

package main

import (
	"fmt"

	"github.com/hidevopsio/hiboot/pkg/app/cli"
)

// RootCommand is the root command
type RootCommand struct {
	cli.RootCommand

	name    string
	verbose bool
}

// NewRootCommand creates the root command
func NewRootCommand() *RootCommand {
	c := new(RootCommand)
	c.Use = "myapp"
	c.Short = "My CLI application"
	c.Long = "A CLI application built with Hiboot"
	c.Example = `
myapp -n John
myapp --name John --verbose
`
	// Define flags
	c.PersistentFlags().StringVarP(&c.name, "name", "n", "World", "Name to greet")
	c.PersistentFlags().BoolVarP(&c.verbose, "verbose", "v", false, "Enable verbose output")
	return c
}

// Run executes the command
func (c *RootCommand) Run(args []string) error {
	if c.verbose {
		fmt.Printf("Verbose mode enabled\n")
	}
	fmt.Printf("Hello, %s!\n", c.name)
	return nil
}

Advanced Example with Sub-commands

main.go

package main

import (
	"github.com/hidevopsio/hiboot/pkg/app"
	"github.com/hidevopsio/hiboot/pkg/app/cli"
	"github.com/hidevopsio/hiboot/pkg/starter/logging"

	"myapp/cmd"
)

func main() {
	cli.NewApplication(cmd.NewRootCommand).
		SetProperty(app.BannerDisabled, true).
		SetProperty(app.ProfilesInclude, logging.Profile).
		Run()
}

cmd/root.go

package cmd

import (
	"github.com/hidevopsio/hiboot/pkg/app/cli"
	"github.com/hidevopsio/hiboot/pkg/log"
)

// RootCommand is the root command
type RootCommand struct {
	cli.RootCommand

	profile string
	timeout int

	// Property injection
	AppName string `value:"${app.name}"`
}

// NewRootCommand creates root command with sub-commands injected
func NewRootCommand(userCmd *userCommand, configCmd *configCommand) *RootCommand {
	c := new(RootCommand)
	c.Use = "myapp"
	c.Short = "My CLI application"
	c.Long = "A CLI application with sub-commands"

	pf := c.PersistentFlags()
	pf.StringVarP(&c.profile, "profile", "p", "dev", "Environment profile")
	pf.IntVarP(&c.timeout, "timeout", "t", 30, "Timeout in seconds")

	// Add sub-commands
	c.Add(userCmd, configCmd)
	return c
}

// Run executes when no sub-command is specified
func (c *RootCommand) Run(args []string) error {
	log.Infof("App: %s, Profile: %s, Timeout: %d", c.AppName, c.profile, c.timeout)
	return nil
}

cmd/user.go (Sub-command)

package cmd

import (
	"github.com/hidevopsio/hiboot/pkg/app"
	"github.com/hidevopsio/hiboot/pkg/app/cli"
	"github.com/hidevopsio/hiboot/pkg/log"
)

type userCommand struct {
	cli.SubCommand
}

func init() {
	app.Register(newUserCommand)
}

func newUserCommand(listCmd *listCommand, addCmd *addCommand) *userCommand {
	c := new(userCommand)
	c.Use = "user"
	c.Short = "User management"
	c.Long = "Manage users in the application"
	c.Add(listCmd, addCmd)
	return c
}

func (c *userCommand) Run(args []string) error {
	log.Info("User command - use a sub-command")
	return nil
}

cmd/list.go (Nested Sub-command)

package cmd

import (
	"fmt"

	"github.com/hidevopsio/hiboot/pkg/app"
	"github.com/hidevopsio/hiboot/pkg/app/cli"
)

type listCommand struct {
	cli.SubCommand

	all bool
}

func init() {
	app.Register(newListCommand)
}

func newListCommand() *listCommand {
	c := new(listCommand)
	c.Use = "list"
	c.Short = "List users"
	c.Long = "List all users in the system"
	c.Flags().BoolVarP(&c.all, "all", "a", false, "Show all users including inactive")
	return c
}

func (c *listCommand) Run(args []string) error {
	if c.all {
		fmt.Println("Listing all users (including inactive)...")
	} else {
		fmt.Println("Listing active users...")
	}
	return nil
}

Running the Application

# Run root command
go run main.go

# Run with flags
go run main.go --profile=prod --timeout=60

# Run sub-command
go run main.go user list

# Run sub-command with flags
go run main.go user list --all

# Get help
go run main.go --help
go run main.go user --help

Configuration

CLI applications can use configuration files just like web applications:

config/application.yml

app:
  name: my-cli-app

logging:
  level: info

Properties can be injected into commands using the value:"" tag:

type RootCommand struct {
	cli.RootCommand

	AppName  string `value:"${app.name}"`
	LogLevel string `value:"${logging.level}"`
}

Unit Testing

package cmd

import (
	"testing"

	"github.com/hidevopsio/hiboot/pkg/app/cli"
	"github.com/stretchr/testify/assert"
)

func TestRootCommand(t *testing.T) {
	testApp := cli.NewTestApplication(t, NewRootCommand)

	t.Run("should run root command", func(t *testing.T) {
		_, err := testApp.Run("--name", "Test")
		assert.NoError(t, err)
	})

	t.Run("should run with verbose flag", func(t *testing.T) {
		_, err := testApp.Run("--name", "Test", "--verbose")
		assert.NoError(t, err)
	})
}

What’s Next?