Command-line Runners
Run CLI workloads after the runtime context is ready.
Use runtime.CommandLineRunner when the application should perform work after
configuration is loaded and the component graph is ready. A runner is useful for
CLI tools, imports, migrations, administrative commands, and local developer
utilities that still need dependency injection.
type ImportRunner struct {
service *ImportService
}
func NewImportRunner(service *ImportService) *ImportRunner {
return &ImportRunner{service: service}
}
func (r *ImportRunner) Run(ctx runtime.Context, args *runtime.Args) error {
file := args.OptionValues("file")
if len(file) == 0 {
return errors.New("missing --file")
}
return r.service.Import(ctx, file[0])
}Register the runner as a component:
func init() {
component.Register(NewImportRunner)
}Arguments
Arguments use --name=value for options. Non-option values are kept separately.
$ go run . --file=users.csv dry-runtype ImportRunner struct {
importer *UserImporter
}
func (r *ImportRunner) Run(ctx runtime.Context, args *runtime.Args) error {
files := args.OptionValues("file")
dryRun := slices.Contains(args.NonOptionArgs(), "dry-run")
for _, file := range files {
if err := r.importer.Import(ctx, file, dryRun); err != nil {
return err
}
}
return nil
}When to use runners
Use runners when the process should do work and exit, or when a startup command
should run before a server waits for shutdown. Avoid putting large CLI parsing
logic into main(); keep main() as the framework entrypoint and put the real
work into a runner component.
Dependency access
Runners are regular components, so dependencies belong in the constructor:
func NewImportRunner(
importer *UserImporter,
properties *ImportProperties,
) *ImportRunner {
return &ImportRunner{importer: importer, properties: properties}
}The Run method should focus on command arguments and execution, not manual
object creation.
