sxcli.devSimple Extensible CLI
You are viewing an archived snapshot (v0.3.0). Switch to the current version →

Getting started

Install#

Add the framework to your module:

sh
sh
go get sxcli.dev/fw

Your first applet#

An applet is a service implementing Configured() error and Run() int, plus a config struct whose field values are its defaults. fw.Solo is the single-applet front door: no subcommands, no ceremony — the binary is the applet.

go
go
package main

import (
    "fmt"

    "sxcli.dev/fw"
)

// ID is the service's public handle: import-path-shaped, and a
// constant so compositions and inject tags can name it.
const ID = "example.com/myapp/hello"

type Config struct {
    Version uint32 `json:"version"`
    Name    string `json:"name" conf:"name,n" usage:"who to greet"`
}

type Hello struct{ cfg Config }

func (h *Hello) Configured() error { return nil }

func (h *Hello) Run() int {
    fmt.Println("hello,", h.cfg.Name)
    return 0
}

func main() {
    fw.Solo(fw.NewRegistration(ID,
        func() *Hello { return &Hello{cfg: Config{Version: 1, Name: "world"}} },
        func(h *Hello) *Config { return &h.cfg },
    ).Alias("hello"))
}

Three things in that registration carry weight:

  • The idexample.com/myapp/hello — is import-path-shaped and unique by construction. It is what code refers to: inject tags, compositions, Order.
  • The aliashello — is the operator's name for the service. It prefixes the env vars and names the config-file section.
  • Version is not decoration. Config schemas are versioned so that files written by an older build keep working; see Config migrations.

Run it#

With a single applet, the whole argument vector belongs to it:

sh
sh
$ hello                  # hello, world
$ hello --name gopher    # hello, gopher
$ hello -n gopher        # the short name from conf:"name,n"
$ HELLO__NAME=go hello   # derived: alias, two underscores, conf name
$ hello --help           # full argument schema, current effective values

The same field is settable from a config file, the environment, or the command line — precedence is defaults < config files < environment < arguments. In a config file the service's keys live in a section named after its alias, alongside the schema version:

json
json
{"hello": {"version": 1, "name": "gopher"}}

Growing past one applet#

When the binary needs more than one applet, packages catalog their services in init() and export an ID for others to name:

go
go
package hello

import (
    "fmt"

    "sxcli.dev/fw"
)

const ID = "example.com/myapp/hello"

type Config struct {
    Version uint32 `json:"version"`
    Name    string `json:"name" conf:"name,n" usage:"who to greet"`
}

type Hello struct{ cfg Config }

func (h *Hello) Configured() error { return nil }
func (h *Hello) Run() int          { fmt.Println("hello,", h.cfg.Name); return 0 }

func init() {
    fw.NewRegistration(ID,
        func() *Hello { return &Hello{cfg: Config{Version: 1, Name: "world"}} },
        func(h *Hello) *Config { return &h.cfg },
    ).Alias("hello").Register()
}

main then names what the binary takes:

go
go
package main

import (
    "sxcli.dev/fw"
    "sxcli.dev/fw/sink/console"

    "example.com/myapp/hello"
)

func main() {
    fw.Builder().
        Accept(hello.ID, console.ID).
        Main()
}

Note what is not happening: no blank imports. A package is imported because the composition names something it exports, so an import that no longer matters fails to compile instead of quietly changing behaviour. fw.Main() is the take-everything shorthand when you genuinely want the whole catalog.

JSON is the native config format — always enabled, nothing to accept. Adding configfmt/yaml to the composition teaches the binary .yaml and .yml on top of it; drop it and JSON configs keep working.

Next: Your config struct — every supported type and tag, and how each value is set from arguments, environment and files.