Introduction

Confetti comes with a handy command-line interface that provides a SOLID solution for building commands quickly and keeping them organized.

Usage

To view a list of all available commands, run the script without further commands:

go run main.go

Or, if the application is built, use the executable binary:

main

Writing Commands

Your First Command

In Confetti Framework, each command implements the Command interface:

type Command interface {
    Name() string
    Description() string
    Handle() error
}

Each command must define a name, description, and a handler function to execute its logic. Commands should be placed inside the command package and registered in cmd/api/main.go.

Defining Input Expectations with the Standard Go Approach

Flags

Commands can accept flags to modify their behavior. The idiomatic Go way is to use the built-in flag package.

Options With Values

For example, to create a command that accepts a name flag:

package command

import (
    "flag"
    "fmt"
    "os"
)

type UserCreate struct{}

func (u UserCreate) Name() string {
    return "user:create"
}

func (u UserCreate) Description() string {
    return "Create a new user with the specified name"
}

func (u UserCreate) Handle() error {
    // Define a flag for the user's name.
    name := flag.String("name", "", "Name of the user")
    // Alternatively, you can also support a shorthand flag.
    n := flag.String("n", "", "Name of the user (shorthand)")
    
    // Parse command-line flags.
    flag.Parse()

    // Prefer the long flag if provided, otherwise use the short flag.
    userName := *name
    if userName == "" {
        userName = *n
    }

    if userName == "" {
        fmt.Fprintln(os.Stderr, "Error: --name or -n flag is required")
        os.Exit(1)
    }

    fmt.Println("Name provided:", userName)
    return nil
}

You can run the command as follows:

go run main.go user:create --name "John Doe"

Flag Arrays

When a flag expects multiple input values, accept a comma-separated string and split it:

package command

import (
    "flag"
    "fmt"
    "os"
    "strings"
)

type MailSend struct{}

func (m MailSend) Name() string {
    return "mail:send"
}

func (m MailSend) Description() string {
    return "Send mail to a list of user IDs"
}

func (m MailSend) Handle() error {
    // Define a flag that accepts a comma-separated list of IDs.
    idsStr := flag.String("ids", "", "Comma-separated list of user IDs (e.g., 1,2,3)")
    flag.Parse()

    if *idsStr == "" {
        fmt.Fprintln(os.Stderr, "Error: --ids flag is required")
        os.Exit(1)
    }

    // Split the string into a slice.
    ids := strings.Split(*idsStr, ",")
    fmt.Println("IDs provided:", ids)
    return nil
}

Checking If a Flag Is Provided

In the Go approach, you check whether a flag was provided by validating its value (e.g., checking for an empty string):

package command

import (
    "flag"
    "fmt"
    "os"
)

type CheckFlag struct{}

func (c CheckFlag) Name() string {
    return "flag:check"
}

func (c CheckFlag) Description() string {
    return "Check if the name flag is provided"
}

func (c CheckFlag) Handle() error {
    name := flag.String("name", "", "Name of the user")
    flag.Parse()

    if *name != "" {
        fmt.Println("Name provided:", *name)
    } else {
        fmt.Fprintln(os.Stderr, "Error: --name flag is required")
        os.Exit(1)
    }
    return nil
}

Command I/O

Retrieving Input

Using the standard library, simply retrieve the flag values as shown above.

Prompting For Input

For interactive input (when flags are not enough), use Go’s standard input methods:

package command

import (
    "bufio"
    "fmt"
    "os"
)

type Interactive struct{}

func (i Interactive) Name() string {
    return "interactive:prompt"
}

func (i Interactive) Description() string {
    return "Prompt the user for input interactively"
}

func (i Interactive) Handle() error {
    reader := bufio.NewReader(os.Stdin)
    fmt.Print("Enter your name: ")
    name, _ := reader.ReadString('\n')
    fmt.Println("Hello,", name)
    return nil
}

Writing Output

Output to the console using:

fmt.Println("Hello, World!")

For error messages, write to standard error:

fmt.Fprintln(os.Stderr, "An error occurred")

Registering Commands

When building a CLI with multiple commands, register each command in cmd/api/main.go:

package main

import (
    "yourproject/command"
)

var commands = []command.Command{
    command.UserCreate{},
    command.MailSend{},
    command.CheckFlag{},
    command.Interactive{},
    command.AppStatus{},
}

func main() {
    // Your logic to select and execute the command based on input.
}

Example: Checking Application Status and Uptime

Below is an example of a command that checks the application status and uptime, with flag parsing performed directly in the Handle() method:

package command

import (
    "flag"
    "fmt"
    "os"
    "time"
)

type AppStatus struct {
    startTime time.Time
}

func (s AppStatus) Name() string {
    return "app:status"
}

func (s AppStatus) Description() string {
    return "Check the current status of the application and uptime"
}

func (s AppStatus) Handle() error {
    // Define a flag for demonstration purposes.
    dummyFlag := flag.String("dummy", "", "A dummy flag for example purposes")
    
    // Parse the command-line flags.
    flag.Parse()
    
    // Validate that the dummy flag is provided.
    if *dummyFlag == "" {
        fmt.Fprintln(os.Stderr, "Error: --dummy flag is required")
        os.Exit(1)
    }
    
    // Calculate and print the application uptime.
    uptime := time.Since(s.startTime)
    fmt.Printf("Application is running. Uptime: %s\n", uptime)
    
    return nil
}

In this example, flag parsing and validation are done within the Handle() method. If the --dummy flag is not provided, an error is printed to standard error and the application exits. Otherwise, the command calculates and displays the application uptime.

Contributors: reindert-vetter, Reindert, Reindert Vetter, Vaggelis Yfantis