linebased

package module
v0.0.0-...-bd79248 Latest Latest
Warning

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

Go to latest
Published: Dec 6, 2025 License: MIT Imports: 11 Imported by: 0

README

linebased

Go Reference

A Go package for parsing and expanding line-based scripts.

echo hello world
define greet name
	echo Hello, $name!
greet Alice

Linebased provides both a parser and tooling for line-based DSLs. The parser extracts commands and their bodies with no quoting or escaping rules. The tooling includes an LSP server for editor integration.

Design

Linebased scripts are deliberately simple. There are no string escapes, no quoting rules, no operator precedence. A line is a command name followed by whatever text you want. That's it. The only reserved words are define and include, which provide templates and file composition.

This simplicity has a cost: you can't nest expressions or compute values inline. But it has a benefit: scripts are trivial to read, write, and debug. The parser does exactly what you expect because there's almost nothing it can do.

Multi-line bodies use tabs for continuation. Tabs are visible, unambiguous, and easy to type. Spaces at line start are a syntax error, which catches a common class of invisible bugs.

Templates exist because repetition is error-prone. Parameter substitution is the only form of abstraction provided. Templates cannot recurse, cannot redefine themselves, and cannot produce new definitions. These restrictions prevent the language from becoming a programming language.

The LSP server provides diagnostics, hover, and jump-to-definition. Editor support matters.

Example

# Define a reusable template
define greet name
	echo Hello, $name!

# Use it
greet Alice
greet Bob

# Include shared definitions (extension added automatically)
include helpers

Documentation

See the package documentation for syntax, semantics, and API reference.

Tooling

Install the linebased command:

go install blake.io/linebased/cmd/linebased@latest
Debugging with expand

See exactly what your templates produce:

$ linebased expand script.linebased
echo Hello, Alice!
echo Hello, Bob!

Add -x for shell-style tracing that shows each template call as it expands:

$ linebased expand -x script.linebased
+ script.linebased:7: outer hello
++ script.linebased:1: inner hello
echo inner: hello

The + signs show nesting depth—when outer calls inner which produces echo, you see the full expansion chain.

Coding agent instructions

For AI coding assistants (Claude, Copilot, etc.), the linebased command provides built-in guidance:

linebased agents

This outputs guidance on working with linebased files, emphasizing the expand command for debugging. Add the output to your CLAUDE.md or system prompt.

Editor support

Configure Neovim:

local lspconfig = require("lspconfig")
local configs = require("lspconfig.configs")

if not configs.linebased then
  configs.linebased = {
    default_config = {
      cmd = { "linebased", "lsp" },
      filetypes = { "linebased" },
    },
  }
end

lspconfig.linebased.setup({})

You'll get diagnostics, hovers, and jump-to-definition for .linebased files.

Documentation

Overview

Package linebased parses line-based scripts and provides template expansion.

A linebased script is a sequence of expressions. Each expression has a command name (the first word) and a body (everything that follows). No quotes, no escaping. The simplicity is the point: what you write is what you get.

echo hello world
set path /usr/local/bin

Multi-line bodies use tab-indented continuation lines:

sql query users
	SELECT id, name
	FROM users
	WHERE active = true

Templates let you define reusable expressions with parameters:

define greet name
	echo Hello, $name!
greet Alice
greet Bob

And includes let you compose scripts from multiple files:

include common.lb

Syntax

The grammar in EBNF:

script       = { expression } .
expression   = { comment } ( command | blankline ) .
comment      = "#" text newline .
command      = name [ whitespace text ] newline { continuation } .
continuation = tab text newline .
blankline    = newline .
name         = nonwhitespace { nonwhitespace } .
whitespace   = " " | tab .
nonwhitespace = (* any character except space, tab, newline *) .
text         = (* any characters except newline *) .
tab          = "\t" .
newline      = "\n" .

A command line begins with a name (one or more non-whitespace characters). Everything after the name through the newline becomes part of the body. Continuation lines must start with exactly one tab, which is stripped. Lines starting with space are syntax errors.

Comments

Lines starting with # are comments. They attach to the following expression and are available in the Comment field of Expanded:

# Set the greeting message.
# This supports multiple lines.
echo Hello, World!

Comments inside template bodies work the same way.

Templates

Define templates with the "define" builtin. The first line names the template and lists parameters. Continuation lines form the template body:

define greet name
	echo Hello, $name!

Invoke templates by name. Arguments are split by whitespace, with the final argument consuming remaining text:

greet Alice           # echo Hello, Alice!
greet "Bob Smith"     # echo Hello, "Bob Smith"!   (quotes are literal)

Parameter references use $name or ${name}. The braced form allows adjacent text:

define shout word
	echo ${word}!!!

Templates can invoke other templates:

define inner x
	echo $x
define outer y
	inner $y
outer hello           # echo hello

Constraints:

  • Recursion is forbidden.
  • Templates cannot be redefined.
  • Expanded templates cannot contain "define".

Includes

The "include" builtin reads another file and processes it inline:

include helpers
include common

Include paths must be simple filenames without directory separators. The ".linebased" extension is added automatically, so "include helpers" opens "helpers.linebased" from the fs.FS passed to NewExpandingDecoder.

Included files can define templates used by the including file. Include cycles are detected and reported as errors.

Blank Lines

Blank lines produce expressions with empty names. This preserves the visual structure of the source:

echo first

echo second

The blank line between commands appears in the expression stream. Like commands, blank lines can have preceding comments.

Unknown Commands

Commands that are not templates pass through unchanged. This allows scripts to define their own command vocabulary:

define echo tail
echo hello           # your code interprets "echo"
custom arg1 arg2     # your code interprets "custom"

Error Handling

Errors during parsing or expansion are reported as ExpressionError with location information:

for expr, err := range linebased.Expand("script.lb", fsys) {
	if err != nil {
		log.Fatal(err)  // includes file:line
	}
	// process expr
}

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ParseArgs2

func ParseArgs2(s string) (a, b string)

ParseArgs2 splits s into two whitespace-separated arguments. The second argument contains any remaining text after the first split.

func ParseArgs3

func ParseArgs3(s string) (a, b, c string)

ParseArgs3 splits s into three whitespace-separated arguments. The third argument contains any remaining text after the first two splits.

Types

type Args

type Args []string

Args represents a list of arguments extracted from an expression's tail.

func ParseArgs

func ParseArgs(s string, n int) Args

ParseArgs splits s into at most n whitespace-separated arguments. The final argument contains any remaining text after the first n-1 splits. Returns empty if n is zero or s is empty.

func (Args) At

func (a Args) At(i int) string

At returns the i-th argument from the Args, trimmed of any trailing newline, or the empty string if i is out of bounds.

type Decoder

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

Decoder reads line-based expressions from an input stream.

func NewDecoder

func NewDecoder(r io.Reader) *Decoder

NewDecoder creates a new Decoder that reads from r.

func (*Decoder) Decode

func (d *Decoder) Decode() (Expression, error)

Decode reads the next Expression from the input and returns it. It returns io.EOF when there are no more expressions to read. It returns an error if the input is malformed.

type Expanded

type Expanded struct {
	Expression

	// File returns the source filename where this expression was parsed.
	File string

	// Stack contains the call stack of the template expansions that
	// produced this expression.
	Stack []Expanded
}

Expanded represents a parsed expression from the input stream, capturing both its content and context within the template expansion process.

Expressions are intended to be produced by [Expressions] or [Expand], not built manually.

func (*Expanded) Caller

func (e *Expanded) Caller() Expanded

Caller returns the immediate template call that produced the expression, or the zero Expression if the expression is top-level.

func (*Expanded) String

func (e *Expanded) String() string

String formats the expression as parseable source text. Reconstructs the original syntax with normalized whitespace, preserving continuation line structure when the tail starts with newlines.

For example:

echo hello world

The Name becomes "echo" and Tail becomes "hello world\n". String returns "echo hello world\n".

define greet name
	echo Hello, $name!

The Name becomes "define" and Tail becomes "greet name\necho Hello, $name!\n". String returns "define greet name\necho Hello, $name!\n".

func (*Expanded) Where

func (e *Expanded) Where() string

Where returns a location string showing where this expression appears and executes. The format is filename:line: template@localline where template identifies the execution context and localline shows the line within that template.

For example, "example.txt:42: bar@5" means the expression appears at line 42 of "example.txt" and executes as line 5 within template "bar". Top-level expressions show "main" as the template.

type ExpandingDecoder

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

ExpandingDecoder reads expressions from a linebased script, expanding any templates defined in-line. It is analogous to Decoder but produces Expanded values with template expansion applied.

Create an ExpandingDecoder with NewExpandingDecoder, then call ExpandingDecoder.Decode repeatedly until it returns io.EOF.

func NewExpandingDecoder

func NewExpandingDecoder(name string, fsys fs.FS) *ExpandingDecoder

NewExpandingDecoder creates an ExpandingDecoder that reads from the named file in fsys, expanding any templates defined in-line.

Include paths are rooted at fsys. For example, if main.lb contains "include lib/utils.lb", the decoder opens "lib/utils.lb" from fsys directly. There is no relative path resolution - all includes are absolute paths within the filesystem.

Expressions with names that do not match a template are passed through as-is. Invalid expansions are reported as ExpressionError.

func (*ExpandingDecoder) Decode

func (d *ExpandingDecoder) Decode() (Expanded, error)

Decode reads and returns the next expanded expression. It returns io.EOF when there are no more expressions. After Decode returns an error (other than io.EOF), subsequent calls return the same error.

func (*ExpandingDecoder) SetRoot

func (d *ExpandingDecoder) SetRoot(root string)

SetRoot sets a prefix for file paths in error messages. This is useful when the fsys is rooted at a subdirectory but you want error messages to show paths relative to a parent directory (e.g., the module root).

For example, if fsys is rooted at "." but tests are in "pkg/testdata/", calling SetRoot("pkg/") will cause error messages to show "pkg/testdata/file.lb" instead of just "testdata/file.lb".

type Expression

type Expression struct {
	// Line is the line number where the expression body starts (1-indexed).
	// This is the line number of the command or blank line, not the preceding comments.
	Line int

	// Comment contains any leading comment lines (including the leading '#')
	// and blank lines that immediately preceded this expression.
	// Each line is terminated with a newline when present.
	Comment string

	// Name is the command name of the expression,
	// which is the first word of the command line.
	Name string

	// Body is everything in the expression after the command name,
	// including continuation lines, each without their leading tab.
	Body string
}

Expression represents a line-based expression consisting of an optional command with continuation lines, preceded by zero or more comment lines.

func (Expression) ParseArgs

func (e Expression) ParseArgs(n int) Args

ParseArgs splits the tail into at most n whitespace-separated arguments. The final argument contains any remaining text after the first n-1 splits. Returns empty if n is zero or the tail is empty.

type ExpressionError

type ExpressionError struct {
	Expanded       // The expression where the error occurred.
	Err      error // The error.
}

ExpressionError reports an error that occurred while expanding or executing a linebased expression along with the expression that caused it.

func (*ExpressionError) Error

func (e *ExpressionError) Error() string

Error reports the error message prefixed with the expression location.

func (*ExpressionError) Unwrap

func (e *ExpressionError) Unwrap() error

type SyntaxError

type SyntaxError struct {
	Line    int    // line number (1-indexed)
	Message string // error message without line prefix
	Err     error  // underlying error, if any
}

SyntaxError represents a syntax error in the input.

func (*SyntaxError) Error

func (e *SyntaxError) Error() string

func (*SyntaxError) Unwrap

func (e *SyntaxError) Unwrap() error

Directories

Path Synopsis
Package checks provides helpers for checking values in linebased scripts.
Package checks provides helpers for checking values in linebased scripts.
cmd
linebased command
Command linebased provides tooling for linebased files.
Command linebased provides tooling for linebased files.

Jump to

Keyboard shortcuts

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