📚 bcl - Awesome Go Library for Configuration

Go Gopher mascot for bcl

BCL is a configuration language similar to HCL.

🏷️ Configuration
📂 Libraries for configuration parsing.
19 stars
View on GitHub 🔗

Detailed Description of bcl

BCL

Build Status Coverage Status Go Report Card Go Reference

Basic Configuration Language.

BCL is like HCL (the language driving Terraform, Packer and friends), but more basic:

  • no dollar-referenced variables; variable name can be used in an expression as it is
  • block starts with def keyword; seems to be more general compared to Terraform's resource keyword
  • dead-simple API: variables get evaluated automatically and fill the fields of the output structure; no strange limitations of where variables can be defined
  • variables with lexical scope
  • nested definitions
  • one-pass lexer, parser and VM executor
  • deserialization aka unmarshalling to static Go structs (possibly nested)

While being basic, BCL also has features reaching beyond HCL:

  • rich expressions operating on strings, int & float numbers, and booleans
  • planned: make the outside world accessible via environment variables, or via running a command and catching its output

Example:

BCL:

var domain = "acme.com"
var default_port    = 8400
var local_port_base = default_port + 1000

def tunnel "myservice-prod" {
	host = "prod" + "." + domain
	local_port  = local_port_base + 1
	remote_port = default_port
	enabled = true

	def extras {
		max_latency = 8.5 # [ms]
	}
}

Go:

type Tunnel struct {
	Name       string
	Host       string
	LocalPort  int
	RemotePort int
	Enabled    bool
	Extras     struct {
		MaxLatency float64
	}
}
var config []Tunnel

err := bcl.UnmarshalFile(file, &config)
// handle err
fmt.Println(strings.ReplaceAll(fmt.Sprintf("%+v", config), " ", "\n  "))

Output:

[{Name:myservice-prod
  Host:prod.acme.com
  LocalPort:9401
  RemotePort:8400
  Enabled:true
  Extras:{MaxLatency:8.5}}]

Syntax

BCL has statements and expressions.

A basic statement is def block_type [block_name] {...} to define a block with field = value expressions inside. Such block after running Interpret will be available as a Block with a map of fields, and can be further put into a static Go struct via CopyBlocks or Unmarshal. Blocks can be nested.

Both toplevel scope and a block can have variables created with the var x = expr statement, or just var x which leaves it uninitialized. Variables do not count when produding result Block structures, but they are taking part of the state flow.

Variables have lexical scope. Any block has access to the varables declared at the toplevel and also nested block have access to their parent's variables. There are no forward declarations.

Variables are mutable and can be mutated with the eval x = expr statement. This statement solely exists to allow evaluation of expressions in the context expecting stamenents, that is at the toplevel. Please note that inside the block the raw statements are allowed, for example field = value is actually an assignment expression. So, when in block, it's good to remember whether we are operating on fields or variables. This may be made more explicit in the future.

The last stament in the clan is print expr which is useful for debugging.

More on expressions below.

Expressions, data conversions

There are three basic types: numbers (int and float), strings and booleans.

Values in expressions know their types, although they are not enforced in the language; certain operations can cause runtime error.

Number arithmetics use int or float operations depending on the values involved; if any of the operands is float, then the int part is transparently converted to float. Complex numbers are not supported atm.

Strings can be concatenated with the plus +. If the right side of such plus is a number, it will be transparently coverted to string. However, the number plus string is an error.

Another string operator borrowed from numbers is asterisk *, this time the left side must be a string and right side just an int; the result is repeating the string given times.

Equality comparisons ==, != are allowed between all types, including mixing them. Obviously values of different non-number types are not equal.

Order comparisons <, >, , <=, >= are allowed between numbers and between strings, but not between mixed types.

There are boolean operators and, or, not behaving like in Python, or like &&, ||, ! in Ruby [1]: they are short-cirtuit and retain the type of an operand (1==1 and 42 will return 42). Non-boolean types can be a boolean operand; for this, there is a definition what is considered "falsey": false, nil, empty string, and zero, like in Python.

Boolean constants are true and false. Another constant is nil, value of an uninitialized variable (var a).

[1] Note that in Ruby ! has surprising priority, though.

Note on the parser

Versions up to v0.7.x used goyacc, since v0.8.0 there is a top-down Pratt parser with bytecode VM.

Cool stuff

Internals can be peeked in many ways, here is bytecode disassembly, execution trace with stack content, plus some stats:

./bcl -dts <<<'var x=1; def block{eval x=x+2; field=x}'
== /dev/stdin ==
0000    1:8  ONE
0001   1:20  DEFBLOCK      0 'block'         1 ''
0004   1:28  GETLOCAL      0
0006   1:30  CONST         2 '2'
0008      |  ADD
0009      |  SETLOCAL      0
0011      |  POP
0012   1:39  GETLOCAL      0
0014      |  SETFIELD      3 'field'
0016      |  POP
0017   1:40  ENDBLOCK
0018    2:1  POP
0019      |  RET
pstats.tokens:        20
pstats.localMax:       1
pstats.depthMax:       1
pstats.constants:      4
pstats.opsCreated:    13
pstats.codeBytes:     20
             0: 
0000    1:8  ONE
             1: [ 1 ]
0001   1:20  DEFBLOCK      0 'block'         1 ''
             1: [ 1 ]
0004   1:28  GETLOCAL      0
             2: [ 1 ][ 1 ]
0006   1:30  CONST         2 '2'
             3: [ 1 ][ 1 ][ 2 ]
0008      |  ADD
             2: [ 1 ][ 3 ]
0009      |  SETLOCAL      0
             2: [ 3 ][ 3 ]
0011      |  POP
             1: [ 3 ]
0012   1:39  GETLOCAL      0
             2: [ 3 ][ 3 ]
0014      |  SETFIELD      3 'field'
             2: [ 3 ][ 3 ]
0016      |  POP
             1: [ 3 ]
0017   1:40  ENDBLOCK
             1: [ 3 ]
0018    2:1  POP
             0: 
0019      |  RET
xstats.tosMax:         3
xstats.blockTosMax:    1
xstats.opsRead:       13
xstats.pcFinal:       20