The framework provides built-in dependency injection to share resources across commands. Register bindings once, and they're automatically injected into struct fields.
Register bindings with WithBindings, then declare fields without tags to receive injected values:
func main() {
db, _ := sql.Open("postgres", os.Getenv("DATABASE_URL"))
cli.ExecuteAndExit(ctx, &App{}, os.Args,
cli.WithBindings(
bind.Value(db),
),
)
}
type ServeCmd struct {
DB *sql.DB // injected automatically
Port int `flag:"port"` // NOT injected (has flag tag)
}
func (s *ServeCmd) Run(ctx context.Context) error {
rows, _ := s.DB.Query("SELECT 1")
// ...
}Four binding modes cover different dependency patterns:
Register a value matched by its concrete type:
db, _ := sql.Open("postgres", connStr)
cli.WithBindings(
bind.Value(db), // matches *sql.DB fields
)Register a value as an interface type:
type Cache interface {
Get(key string) string
Set(key, value string)
}
type RedisCache struct{ /* ... */ }
cache := &RedisCache{}
cli.WithBindings(
bind.Interface(cache, (*Cache)(nil)), // matches Cache fields
)The second argument is a nil pointer to the interface type — Go syntax for specifying the interface.
Lazy factory called on each injection:
cli.WithBindings(
bind.Provider(func() (*RequestID, error) {
return &RequestID{Value: uuid.New().String()}, nil
}),
)Each command receives a fresh instance. Useful for per-request resources.
Lazy factory called once, then cached:
cli.WithBindings(
bind.Singleton(func() (*sql.DB, error) {
return sql.Open("postgres", os.Getenv("DATABASE_URL"))
}),
)The factory runs on first injection; subsequent injections receive the cached value.
Fields without tags are eligible for injection:
type ServeCmd struct {
// Injected (no tags)
DB *sql.DB
Cache Cache
Logger *slog.Logger
// NOT injected (have tags)
Port int `flag:"port"`
Env string `arg:"env"`
Secret string `env:"SECRET"`
}The injector matches fields by:
- Exact type —
*sql.DBfield matchesbind.Value(db)where db is*sql.DB - Interface compatibility —
Cachefield matchesbind.Interface(v, (*Cache)(nil))
Bindings are also accessible via context in Run:
func (s *ServeCmd) Run(ctx context.Context) error {
db := bind.Get[*sql.DB](ctx)
cache := bind.Get[Cache](ctx)
// Or with existence check
logger, ok := bind.Lookup[*slog.Logger](ctx)
if !ok {
logger = slog.Default()
}
return nil
}This is useful when:
- You prefer explicit over implicit dependencies
- You need optional dependencies
- You're working with middleware or nested functions
cli.Args is automatically bound to remaining positional arguments:
type GrepCmd struct {
Pattern string `arg:"pattern"`
Args cli.Args // automatically populated — no tag needed
}
func (g *GrepCmd) Run(ctx context.Context) error {
for _, file := range g.Args {
// process file
}
return nil
}package main
import (
"context"
"database/sql"
"log/slog"
"os"
"github.com/bjaus/bind"
"github.com/bjaus/cli"
)
// Interfaces for abstraction
type Cache interface {
Get(key string) (string, bool)
Set(key, value string)
}
type Metrics interface {
Inc(name string)
Observe(name string, value float64)
}
func main() {
ctx := context.Background()
// Create shared resources
db, _ := sql.Open("postgres", os.Getenv("DATABASE_URL"))
cache := newRedisCache(os.Getenv("REDIS_URL"))
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
cli.ExecuteAndExit(ctx, &App{}, os.Args,
cli.WithBindings(
// Concrete types
bind.Value(db),
bind.Value(logger),
// Interface types
bind.Interface(cache, (*Cache)(nil)),
// Singleton — created once when first needed
bind.Singleton(func() (Metrics, error) {
return newPrometheusMetrics()
}),
// Provider — fresh instance each time
bind.Provider(func() (*RequestContext, error) {
return &RequestContext{
RequestID: uuid.New().String(),
StartTime: time.Now(),
}, nil
}),
),
)
}
type App struct {
Logger *slog.Logger // injected
}
func (a *App) Run(ctx context.Context) error {
return cli.ErrShowHelp
}
func (a *App) Subcommands() []cli.Commander {
return []cli.Commander{&ServeCmd{}, &MigrateCmd{}}
}
type ServeCmd struct {
// Injected dependencies
DB *sql.DB
Cache Cache
Logger *slog.Logger
Metrics Metrics
// Flags (not injected)
Port int `flag:"port" default:"8080"`
}
func (s *ServeCmd) Name() string { return "serve" }
func (s *ServeCmd) Run(ctx context.Context) error {
s.Metrics.Inc("server_starts")
s.Logger.Info("starting server", "port", s.Port)
// Use s.DB, s.Cache, etc.
return nil
}
type MigrateCmd struct {
// Only needs DB
DB *sql.DB
Logger *slog.Logger
// Flags
Version int `flag:"version" required:""`
}
func (m *MigrateCmd) Name() string { return "migrate" }
func (m *MigrateCmd) Run(ctx context.Context) error {
m.Logger.Info("running migration", "version", m.Version)
// Use m.DB
return nil
}Dependencies are injected:
- After flag parsing — so flag values are available
- Before Before hooks — so dependencies are ready for setup
- For all commands in the chain — parent and child commands
Parse flags → Inject dependencies → Before hooks → Run
Provider and Singleton factories can return errors:
cli.WithBindings(
bind.Singleton(func() (*sql.DB, error) {
db, err := sql.Open("postgres", connStr)
if err != nil {
return nil, fmt.Errorf("database connection failed: %w", err)
}
return db, nil
}),
)If a factory returns an error, execution stops and the error is returned from Execute.
Dependency injection simplifies testing — inject mocks instead of real dependencies:
func TestServeCmd(t *testing.T) {
mockDB := &MockDB{}
mockCache := &MockCache{}
cmd := &ServeCmd{}
err := cli.Execute(ctx, cmd, []string{"--port", "9000"},
cli.WithBindings(
bind.Value(mockDB),
bind.Interface(mockCache, (*Cache)(nil)),
),
)
assert.NoError(t, err)
assert.True(t, mockDB.WasCalled)
}Prefer interface bindings for testability:
// Good: interface allows mocking
type UserService interface {
GetUser(id int) (*User, error)
}
cli.WithBindings(
bind.Interface(svc, (*UserService)(nil)),
)
// Less flexible: concrete type
cli.WithBindings(
bind.Value(svc), // harder to mock
)cli.WithBindings(
// Good: created once
bind.Singleton(func() (*sql.DB, error) {
return sql.Open("postgres", connStr)
}),
// Bad: creates connection on every injection
// bind.Provider(func() (*sql.DB, error) { ... }),
)cli.WithBindings(
bind.Provider(func() (*TraceContext, error) {
return &TraceContext{
TraceID: generateTraceID(),
SpanID: generateSpanID(),
}, nil
}),
)Each command should only declare the dependencies it needs:
// Good: only declares what it uses
type ListCmd struct {
DB *sql.DB
}
// Avoid: declaring unused dependencies
type ListCmd struct {
DB *sql.DB
Cache Cache // unused
Metrics Metrics // unused
}- Lifecycle — Hooks that run before and after commands
- Flags — Flag parsing and environment variables
- Configuration — External config file support