godexer

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 17, 2025 License: MIT Imports: 22 Imported by: 0

README

godexer

[Declarative command/script executor for Go]

CI Lint Go Reference Go Report Card codecov Go Version

Overview

godexer is a small, extensible library to execute declarative “scripts” (pipelines) defined in YAML or JSON. It provides:

  • A registry of command types (exec, message, sleep, variable, writefile, foreach) and optional extras (include, SSH)
  • Templating for values and descriptions (Go text/template) with helper functions
  • Conditional execution via expressions (govaluate) and pluggable evaluator functions
  • Simple composition (include, foreach) and hooks-after for post-step logic

Use it to automate local/remote setup tasks, provisioning steps, bootstrapping, CI bits, or any repeatable sequence you want to keep in config but extend in Go.

Target audience: developers, SRE/DevOps engineers, and tool authors who prefer declarative workflows but need programmatic extension points.

Features

  • YAML/JSON scenarios with Go templates for values and descriptions
  • Built-ins: exec, message, sleep, variable, writefile, foreach
  • Conditions with requires: using evaluator functions
    • Defaults: file_exists, strlen, shell_escape
    • Version helpers via subpackage: version_lt/lte/gt/gte/eq
  • Variables: set and consume, capture command output
  • Hooks-after: register named callbacks invoked after steps
  • Extensible: register your own command types and value functions
  • Optional SSH commands (exec, scp writefile) via github.com/go-extras/godexer/ssh

Requirements

  • Go 1.25+ (CI runs on 1.25.x)

Installation

Add the library to your module:

go get github.com/go-extras/godexer@latest
# Optional extras
go get github.com/go-extras/godexer/ssh@latest
go get github.com/go-extras/godexer/version@latest

Quick start

A minimal scenario using built-ins and default evaluator functions:

ex, _ := godexer.NewWithScenario(`
commands:
  - type: message
    stepName: hello
    description: 'Hello, {{ index . "name" }}'
  - type: sleep
    stepName: wait
    description: 'Sleeping a moment'
    seconds: 1
`, godexer.WithDefaultEvaluatorFunctions())
_ = ex.Execute(map[string]any{"name": "world"})

Practical example with exec capturing output and a condition:

commands:
  - type: exec
    stepName: uname
    description: 'Capture uname'
    cmd: ["go", "version"]
    variable: go_version
  - type: message
    stepName: show
    description: 'Go version: {{ index . "go_version" }}'
    requires: 'strlen(go_version) > 0'

Run the runnable example in this repo:

go run ./example/local

See also the SSH example at example/ssh (requires an SSH server and key; see the file header for flags).

API documentation

Concepts and built-ins

  • Base fields (available on all commands): type, stepName, description, requires, callsAfter
  • exec: run a process; supports env, retries (attempts, delay), allowFail, capture to variable
  • message: prints description only
  • sleep: pause for N seconds
  • variable: set a variable from a literal or template
  • writefile: write rendered contents to a file
  • foreach: iterate over a slice/map; set keyVar/valueVar and run nested commands
  • include (opt-in): register the include command by wiring a storage

Register include with a filesystem:

cmds := godexer.GetRegisteredCommands()
cmds["include"] = godexer.NewIncludeCommandWithBasePath(os.DirFS("."), "./scripts")
ex, _ := godexer.NewWithScenario(scenario, godexer.WithCommandTypes(cmds))

Add version comparison helpers:

ex, _ := godexer.NewWithScenario(scn, godexer.WithDefaultEvaluatorFunctions(), version.WithVersionFuncs())

SSH commands (exec, scp writefile):

cmds := godexer.GetRegisteredCommands()
cmds["ssh_exec"] = sshexec.NewSSHExecCommand(client, os.Stdout, os.Stderr)
cmds["scp_writefile"] = sshexec.NewScpWriterFileCommand(client)
ex, _ := godexer.NewWithScenario(scn, godexer.WithCommandTypes(cmds))

Contributing

Contributions are welcome! Please:

  • Read CONTRIBUTING.md for details on workflow, code style, and testing
  • Open issues with clear steps to reproduce
  • For PRs: add/adjust tests, run go test -race ./..., keep go mod tidy, and address lint (golangci-lint run)

Links:

Project status

Alpha. Expect rapid changes and occasional breaking updates while the API settles.

License

MIT © 2025 Denis Voytyuk — see LICENSE.md.

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var ExecCommandFn = exec.Command
View Source
var TimeSleep = time.Sleep

Functions

func GetRegisteredCommands

func GetRegisteredCommands() map[string]func(ectx *ExecutorContext) Command

func MaybeEvalValue

func MaybeEvalValue(val any, variables map[string]any) any

func NewIncludeCommand

func NewIncludeCommand(storage fs.ReadFileFS) func(ectx *ExecutorContext) Command

func NewIncludeCommandWithBasePath

func NewIncludeCommandWithBasePath(storage fs.ReadFileFS, basepath string) func(ectx *ExecutorContext) Command

func RegisterCommand

func RegisterCommand(name string, cmd func(ectx *ExecutorContext) Command)

func RegisterValueFunc

func RegisterValueFunc(name string, fn any)

RegisterValueFunc registers template value functions. Not safe for concurrent usage.

func ShellEscape

func ShellEscape(cmd string) string

func WithCommandTypes

func WithCommandTypes(types map[string]func(ectx *ExecutorContext) Command) func(ex *Executor)

func WithCommands

func WithCommands(cmds []Command) func(ex *Executor)

func WithDefaultEvaluatorFunctions

func WithDefaultEvaluatorFunctions() func(ex *Executor)

WithDefaultEvaluatorFunctions registers value evaluator functions.

There are 3 of them available: - `file_exists(filename string) (bool, error)` - returns true if file exists - `strlen(str string) (float64, error)` - returns the length of the string as a number - `shell_escape(str string) (string, error)` - returns the shell-escaped version of the string

func WithEvaluatorFunction

func WithEvaluatorFunction(name string, fn govaluate.ExpressionFunction) func(ex *Executor)

func WithEvaluatorFunctions

func WithEvaluatorFunctions(funcs map[string]govaluate.ExpressionFunction) func(ex *Executor)

func WithFS

func WithFS(fs afero.Fs) func(ex *Executor)

func WithHookAfter

func WithHookAfter(name string, hook func(variables map[string]any) error) func(ex *Executor)

func WithHooksAfter

func WithHooksAfter(hooks HooksAfter) func(ex *Executor)

func WithLogger

func WithLogger(log Logger) func(ex *Executor)

func WithRegisteredCommandTypes

func WithRegisteredCommandTypes() func(ex *Executor)

func WithStderr

func WithStderr(stderr io.Writer) func(ex *Executor)

func WithStdout

func WithStdout(stdout io.Writer) func(ex *Executor)

func WithStepNameSuffix

func WithStepNameSuffix(suffix string) func(ex *Executor)

Types

type BaseCommand

type BaseCommand struct {
	Type        string
	StepName    string
	Description string
	Requires    string
	CallsAfter  string
	Ectx        *ExecutorContext
	// contains filtered or unexported fields
}

func (*BaseCommand) DebugInfo

func (r *BaseCommand) DebugInfo() *CommandDebugInfo

func (*BaseCommand) GetDescription

func (r *BaseCommand) GetDescription(variables map[string]any) string

func (*BaseCommand) GetHookAfter

func (r *BaseCommand) GetHookAfter() string

func (*BaseCommand) GetRequires

func (r *BaseCommand) GetRequires() string

func (*BaseCommand) GetStepName

func (r *BaseCommand) GetStepName() string

func (*BaseCommand) SetDebugInfo

func (r *BaseCommand) SetDebugInfo(di *CommandDebugInfo)

type BeforeCommandExecuteCallback

type BeforeCommandExecuteCallback func(command Command, variables map[string]any)

type Buffer

type Buffer struct {
	// contains filtered or unexported fields
}

func (*Buffer) Read

func (b *Buffer) Read(p []byte) (n int, err error)

func (*Buffer) String

func (b *Buffer) String() string

func (*Buffer) Write

func (b *Buffer) Write(p []byte) (n int, err error)

type CombinedWriter

type CombinedWriter struct {
	// contains filtered or unexported fields
}

func NewCombinedWriter

func NewCombinedWriter(writers []io.Writer) *CombinedWriter

func (*CombinedWriter) Write

func (w *CombinedWriter) Write(p []byte) (n int, err error)

type Command

type Command interface {
	GetRequires() string
	GetStepName() string
	GetHookAfter() string
	GetDescription(variables map[string]any) string
	Execute(variables map[string]any) error
}

func NewExecCommand

func NewExecCommand(ectx *ExecutorContext) Command

func NewForeachCommand

func NewForeachCommand(ectx *ExecutorContext) Command

func NewMessageCommand

func NewMessageCommand(ectx *ExecutorContext) Command

func NewPassword

func NewPassword(ectx *ExecutorContext) Command

func NewSleepCommand

func NewSleepCommand(ectx *ExecutorContext) Command

func NewSubExecuteCommand

func NewSubExecuteCommand(ectx *ExecutorContext) Command

func NewVariableCommand

func NewVariableCommand(ectx *ExecutorContext) Command

func NewWriterFileCommand

func NewWriterFileCommand(ectx *ExecutorContext) Command

type CommandAwareError

type CommandAwareError struct {
	// contains filtered or unexported fields
}

func NewCommandAwareError

func NewCommandAwareError(err error, cmd Command, variables map[string]any) *CommandAwareError

func (*CommandAwareError) Cause

func (e *CommandAwareError) Cause() error

func (*CommandAwareError) Command

func (e *CommandAwareError) Command() Command

func (*CommandAwareError) Error

func (e *CommandAwareError) Error() string

func (*CommandAwareError) Unwrap

func (e *CommandAwareError) Unwrap() error

func (*CommandAwareError) Variables

func (e *CommandAwareError) Variables() map[string]any

type CommandDebugInfo

type CommandDebugInfo struct {
	ID       int
	Contents []byte
}

type DebugInfoer

type DebugInfoer interface {
	DebugInfo() *CommandDebugInfo
	SetDebugInfo(*CommandDebugInfo)
}

type ExecCommand

type ExecCommand struct {
	BaseCommand
	Cmd       []string
	Variable  string
	AllowFail bool
	Env       []string

	// the following parameters will allow retrying the command
	Attempts int // if 0 or 1, no retry
	Delay    int // seconds
}

func (*ExecCommand) Execute

func (r *ExecCommand) Execute(variables map[string]any) error

type Executor

type Executor struct {
	// contains filtered or unexported fields
}

func New

func New(opts ...Option) *Executor

func NewWithScenario

func NewWithScenario(scenario string, opts ...Option) (*Executor, error)
Example

ExampleNewWithScenario demonstrates creating and running a minimal scenario with default evaluator functions, and reading a variable set by the script.

package main

import (
	"fmt"

	"github.com/go-extras/godexer"
)

func main() {
	const script = `
commands:
  - type: message
    stepName: hello
    description: 'Hello, {{ index . "name" }}'
  - type: variable
    stepName: set
    variable: greeting
    value: 'Hi, {{ index . "name" }}'
`

	ex, err := godexer.NewWithScenario(script, godexer.WithDefaultEvaluatorFunctions())
	if err != nil {
		fmt.Println("error:", err)
		return
	}

	vars := map[string]any{"name": "John"}
	_ = ex.Execute(vars)

	fmt.Println("Greeting:", vars["greeting"])
}
Output:

Greeting: Hi, John

func (*Executor) AppendScenario

func (ex *Executor) AppendScenario(scenario string) error

func (*Executor) CommandTypeFn

func (ex *Executor) CommandTypeFn(typ string) (func(ectx *ExecutorContext) Command, bool)

func (*Executor) CommandTypes

func (ex *Executor) CommandTypes() map[string]func(ectx *ExecutorContext) Command

func (*Executor) Execute

func (ex *Executor) Execute(variables map[string]any) (err error)

Execute runs installation script commands/actions according to the provided params map.

func (*Executor) RegisterEvaluatorFunction

func (ex *Executor) RegisterEvaluatorFunction(name string, fn func(args ...any) (any, error)) *Executor

RegisterEvaluatorFunction registers an evaluator function. The function must follow the signature: `func(args ...any) (any, error)`.

func (*Executor) SetBeforeCommandExecuteCallback

func (ex *Executor) SetBeforeCommandExecuteCallback(cb BeforeCommandExecuteCallback) *Executor

func (*Executor) WithCommands

func (ex *Executor) WithCommands(cmds []Command, opts ...Option) *Executor

func (*Executor) WithScenario

func (ex *Executor) WithScenario(scenario string, opts ...Option) (*Executor, error)

type ExecutorContext

type ExecutorContext struct {
	Fs       afero.Fs
	Stdout   io.Writer
	Stderr   io.Writer
	Executor *Executor
	Logger   Logger
}

type ForeachCommand

type ForeachCommand struct {
	BaseCommand
	RawCommands []json.RawMessage `json:"commands"`
	// Value that contains slice or map
	Iterable any `json:"iterable"`
	// Script variable that contains slice or map (unused if iterable is set)
	Variable string `json:"variable"`
	// Script variable that will be created for the key at the iteration (default: key)
	KeyVar string `json:"keyVar"`
	// Script variable that will be created for the value at the iteration (default: value)
	ValueVar string `json:"valueVar"`
	// Script variable that will be created for the parent vars at the iteration (default: parent)
	ParentVar string `json:"parentVar"`
	// contains filtered or unexported fields
}

ForeachCommand allows defining a set of commands that will be run for each item in the given map or slice contained in the `variable`.

func (*ForeachCommand) Execute

func (r *ForeachCommand) Execute(variables map[string]any) error

type HooksAfter

type HooksAfter map[string]func(variables map[string]any) error

type IncludeCommand

type IncludeCommand struct {
	SubExecuteCommand
	// File to include.
	File string `json:"file"`
	// Variables to be passed to the included script.
	Variables map[string]any `json:"variables"`
	// If noMergeVars is true, variables that will be set in the included script
	// will be stored in a newly created "%step_name%_vars" var.
	// Otherwise, they will be placed in the global script namespace.
	NoMergeVars bool `json:"noMergeVars"`
	// contains filtered or unexported fields
}

func (*IncludeCommand) Execute

func (r *IncludeCommand) Execute(variables map[string]any) error

type Logger

type Logger interface {
	Debugf(format string, args ...any)
	Infof(format string, args ...any)
	Printf(format string, args ...any)
	Warnf(format string, args ...any)
	Warningf(format string, args ...any)
	Errorf(format string, args ...any)
	Fatalf(format string, args ...any)
	Panicf(format string, args ...any)

	Debug(args ...any)
	Info(args ...any)
	Print(args ...any)
	Warn(args ...any)
	Warning(args ...any)
	Error(args ...any)
	Fatal(args ...any)
	Panic(args ...any)

	Tracef(format string, args ...any)
	Trace(args ...any)
}

Logger is an interface that is compatible with logrus, but doesn't require logrus to be used

type MessageCommand

type MessageCommand struct {
	BaseCommand
}

func (*MessageCommand) Execute

func (*MessageCommand) Execute(_ map[string]any) error

type Option

type Option func(*Executor)

type PasswordCommand

type PasswordCommand struct {
	BaseCommand
	Variable string
	Length   int
	Charset  string
}

func (*PasswordCommand) Execute

func (s *PasswordCommand) Execute(variables map[string]any) error

type RawScenario

type RawScenario struct {
	Commands []json.RawMessage `json:"commands"`
}

type SleepCommand

type SleepCommand struct {
	BaseCommand
	Seconds int
}

func (*SleepCommand) Execute

func (s *SleepCommand) Execute(_ map[string]any) error

type SubExecuteCommand

type SubExecuteCommand struct {
	BaseCommand
	RawCommands []json.RawMessage `json:"commands"`
}

SubExecuteCommand is designed to be used by other command types one of the possible usages is an implementation of an include command

func (*SubExecuteCommand) Execute

func (r *SubExecuteCommand) Execute(variables map[string]any) error

type VariableCommand

type VariableCommand struct {
	BaseCommand
	Variable string
	Value    any
}

func (*VariableCommand) Execute

func (s *VariableCommand) Execute(variables map[string]any) error

type WriteFileCommand

type WriteFileCommand struct {
	BaseCommand
	Contents    string
	File        string
	Permissions string
}

func (*WriteFileCommand) Execute

func (r *WriteFileCommand) Execute(variables map[string]any) error

Directories

Path Synopsis
example
local command
ssh command
internal
scp

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL