Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Modules and Imports

mq provides several ways to organize and reuse code: module, import, and include.

Module

Defines a module to group related functions and prevent naming conflicts using the syntax module name: ... end.

module module_name:
  def function1(): ...
  def function2(): ...
end

Functions within a module can be accessed using qualified access syntax:

module_name::function1()

Examples

# Define a math module
module math:
  def add(a, b): a + b;
  def sub(a, b): a - b;
  def mul(a, b): a * b;
end

# Use functions from the module
| math::add(5, 3)  # Returns 8
| math::mul(4, 2)  # Returns 8

Import

Loads a module from an external file using the syntax import "module_path". The imported module is available with its defined name and can be accessed using qualified access syntax.

The import directive searches for .mq files in the following locations:

  • $HOME/.mq - User’s home directory mq folder
  • $ORIGIN/../lib/mq - Library directory relative to the source file
  • $ORIGIN/../lib - Parent lib directory relative to the source file
  • $ORIGIN - Current directory relative to the source file
import "module_name"

Examples

math.mq:

def add(a, b): a + b;
def sub(a, b): a - b;

main.mq:

# Import the math module
import "math"

# Use functions with qualified access
| math::add(10, 5)  # Returns 15
| math::sub(10, 5)  # Returns 5

Import Aliases

Use import "module_path" as alias to bind the module under a different name, useful for shortening long module paths or avoiding naming conflicts. Only the alias is bound; the module’s original name is not available.

import "math" as m

| m::add(10, 5)  # Returns 15
| m::sub(10, 5)  # Returns 5

Include

Loads functions from an external file directly into the current namespace using the syntax include "module_name". Unlike import, functions are available without a namespace prefix.

The include directive searches for .mq files in the same locations as import.

include "module_name"

Examples

math.mq:

def add(a, b): a + b;
def sub(a, b): a - b;

main.mq:

# Include math functions
include "math"

# Functions are available directly
| add(2, 3)  # Returns 5
| sub(10, 4) # Returns 6

Built-in modules

mq ships several built-in modules for parsing common structured data formats. They are available via import without any additional installation.

ModuleParse functionDescription
jsonjson::json_parse()Parses a JSON string
yamlyaml::yaml_parse()Parses a YAML string
tomltoml::toml_parse()Parses a TOML string
xmlxml::xml_parse()Parses an XML string
toontoon::toon_parse()Parses a Toon string
csvcsv::csv_parse(has_header)Parses CSV (, delimiter)
csvcsv::tsv_parse(has_header)Parses TSV (\t delimiter)
csvcsv::psv_parse(has_header)Parses PSV (| delimiter)

These modules are also used automatically when you process a file whose extension matches (see CLI auto-parsing).

Example

import "json"
| json::json_parse()

JSON Pointer

The json module also supports JSON Pointer (RFC 6901) references, built on get_path/set_path/del_path.

FunctionDescription
json::json_pointer_get(data, ptr)Returns the referenced value, or None if it does not exist
json::json_pointer_has(data, ptr)Returns true if the reference exists, even when its value is None
json::json_pointer_set(data, ptr, value)Sets the value; missing containers become dicts and - appends to arrays
json::json_pointer_del(data, ptr)Deletes the value
json::json_pointer_to_path(data, ptr)Converts a pointer to a path array for get_path/set_path/del_path
json::json_pointer_from_path(path)Converts a path array to a pointer, escaping ~ and /
import "json"
| let doc = {"a/b": [10, 20]}
| json::json_pointer_get(doc, "/a~1b/1")
# => 20

Combine with paths to list a pointer for every leaf: paths(doc) | map(json::json_pointer_from_path).

Markdown Builder (md)

The md module provides functions for constructing markdown nodes from scratch, rather than filtering or transforming existing ones. Each function returns a markdown value that can be combined with others using md::doc(), which merges nodes into a single markdown value. md::doc() accepts either a variable number of arguments or a single array, and flattens nested arrays automatically, so the result of map() (or any function returning a plain array of nodes) can be spliced in directly as children, and None entries (e.g. from conditional branches) are dropped.

Note: This module is under development. APIs and behavior may change without notice.

import "md"
| md::doc(
    md::h("My Project", 1),
    md::text("Run `cargo install mq`."),
    md::code("cargo install mq", "bash"),
    map(["fast", "composable", "jq-like"], fn(x): md::list(x);),
  )

Since the current value (self) is automatically passed when a call is missing an argument, builder calls also read naturally in pipeline position:

"My Project" | md::h(1)
# equivalent to md::h("My Project", 1)

Lists and tables are built the same way:

import "md"
| md::doc(
    # List
    md::list("Plain item"),
    md::list("Nested item", 1),
    md::list("Ordered item", 0, true),
    md::list("Checked item", 0, false, true),
    # Table
    md::table_row(["Name", "Age"]),
    md::table_align(["left", "right"]),
    md::table_row(["Alice", "30"]),
  )

Inline and heading helpers cover the rest of the common formatting cases:

import "md"
| md::doc(
    md::h2("Notes"),          # md::h1 .. md::h6
    md::highlight("key"),     # ==key==
    md::comment("todo"),      # <!-- todo -->
    md::wikilink("Page", "alias"),  # [[Page|alias]]
    md::embed("image.png"),   # ![[image.png]]
    md::escape("a*b_c"),      # a\*b\_c
    md::hard_break("a\nb"),   # single newlines become hard line breaks
  )

Reference indexing and resolution

md::reference_index(md_nodes) and md::resolve_references(md_nodes, index = None) index and resolve reference-style links ([text][ident], including the shortcut form [ident]), reference images (![alt][ident]), and footnotes ([^ident]) against their [ident]: url / [^ident]: ... definitions. Both need every document node at once — pass -A on the CLI, or pipe through nodes in an inline query/script.

reference_index groups every link and footnote definition by identifier:

import "md"
| md::reference_index(nodes)
# => {"links": {"ident": [{ident, label, url, title, location}, ...]}, "footnotes": {"ident": [{ident, location}, ...]}}

Each identifier maps to an array of every definition found for it, so a duplicate definition (more than one [ident]: line for the same ident) shows up as an array with more than one entry.

resolve_references walks every reference node and looks it up in the index, returning one dict per reference with its type, ident, label, location, whether it resolved to a definition, whether that definition is duplicate, the definition picked (CommonMark’s first-match-wins, or None if unresolved), and every candidate definitions:

import "md"
| md::resolve_references(nodes)
| filter(fn(r): !r[:resolved];)
# => reference nodes with no matching definition (for building linkcheck-style diagnostics)

index defaults to md::reference_index(md_nodes), resolving references against their own document. Pass an explicit index built from a different set of nodes to resolve references that got separated from their definitions — for example when a document is split or a section is moved into another file — which is exactly the case where a reference legitimately comes back unresolved.

Property-Based Test Generators (gen)

The gen module provides generator combinators for mq-test’s # @property(count, generators) annotation (see the mq-test crate’s README for the annotation itself). A generator is a plain function fn(seed): value; built on top of the seeded rand/rand_int/ random_string builtins, so the same seed always produces the same value. Combinators build bigger generators out of smaller ones, deriving an independent sub-seed for each part so nested generators don’t correlate with one another.

import "gen"
| (gen::int(1, 10))(42)
FunctionDescription
gen::int(min, max)A uniformly random integer in [min, max]
gen::float()A uniformly random float in [0, 1)
gen::bool()A random boolean
gen::string(charset, len)A random string of len characters drawn from charset
gen::const(value)Always value, ignoring the seed
gen::element(values)A uniformly random element of values
gen::one_of(gens)Picks one of gens and runs it
gen::array(gen, len)A fixed-length array of len values from gen
gen::array_of(gen, min_len, max_len)A variable-length array (min_len to max_len) of values from gen
gen::transform(gen, mapper)Transforms the value gen produces through mapper
gen::tuple(gens)One value per generator in gens, independently seeded

gen::tuple is what # @property(...) calls under the hood to turn an array of per-parameter generators into one argument list per iteration — a failing iteration’s reported index doubles as the seed to reproduce it with, by calling gen::tuple(generators)(seed) again with the same generators array.

Shrinking (gen::shrink_value, gen::shrink)

When a # @property(...) case fails, mq-test automatically searches for a smaller, simpler failing example before reporting it. The failure message shows both the shrunk arguments and the original ones, so the report stays easy to reason about even when the raw seed produced a large or convoluted value:

FunctionDescription
gen::shrink_value(value)One step of “smaller” candidates for value (numbers toward 0, strings/arrays toward shorter, dicts toward simpler values)
gen::shrink(args, is_interesting)Searches the array args for a smaller array that’s still is_interesting (a fn(args): bool)
import "gen"
| gen::shrink([10], fn(a): a[0] > 3;)
# => [4]

Both are also usable directly (not just through # @property(...)) by hand-written property tests.

HTTP Imports

When mq is built with the http-import feature, import and include accept HTTP/HTTPS URLs in addition to local file names.

Security note: HTTP imports are disabled by default and must be enabled with --allow-http-import. Once enabled, only URLs under github.com/harehare (resolved to raw.githubusercontent.com/harehare) are allowed; importing from any other domain also requires --allowed-domain. This is a separate permission from --allow-net, which only gates the http()/http_request() builtins, not module resolution.

Plain URL

import "https://example.com/mymod.mq"

GitHub shorthand

The scheme can be omitted for GitHub repositories. mq automatically maps the path to raw.githubusercontent.com.

github.com/{owner}/{path}[@{version}]
ShorthandResolved URL
github.com/alice/mymodraw.githubusercontent.com/alice/mymod/HEAD/mymod.mq
github.com/alice/mymod.mqraw.githubusercontent.com/alice/mymod.mq/HEAD/mymod.mq
github.com/alice/[email protected]raw.githubusercontent.com/alice/mymod/v1.0/mymod.mq
github.com/alice/repo/lib/[email protected]raw.githubusercontent.com/alice/repo/v2.0/lib/util.mq

Example:

mq --allow-http-import 'import "github.com/harehare/kdl.mq" | kdl::kdl_parse("title \"Hello, World!\"")'

Caching

Fetched modules are cached in {system_cache_dir}/mq/ as {md5(url)}.mq files.

  • Versioned URLs (e.g. @v0.1.0): cached indefinitely — the tag content is immutable.
  • Mutable refs (HEAD, main, master, or no version): cached on first fetch. Pass --refresh-modules on the command line to discard the cache and re-fetch.

Lock file (mq.lock)

  • First use of a URL (fetched or served from the cache): recorded in mq.lock.
  • Later use, content unchanged: succeeds silently.
  • Later use, content changed: fails with an error explaining the mismatch. Re-run with --refresh-modules to accept the new content and update mq.lock (for a versioned/tagged URL, whose cache --refresh-modules doesn’t touch, use --clear-cache instead).
  • --refresh-modules / --clear-cache also drop the corresponding entries from mq.lock (mutable-ref entries only, and all entries respectively), matching their disk-cache behavior.
  • Pass --no-lockfile to disable the check entirely (no file is read or written).
  • Pass --lockfile <path> to use a different location instead of ./mq.lock. Missing parent directories are created automatically. Mutually exclusive with --no-lockfile.

Commit mq.lock alongside scripts that use HTTP imports so CI and teammates fetch the exact content you locked, the same way package-lock.json/deno.lock work.

CLI options

FlagDescription
--allow-http-importEnable HTTP module imports. Disabled by default; import/include of a github.com/... or https://... URL fails without this.
--refresh-modulesDiscard cached mutable-ref modules and re-fetch them, updating their mq.lock entries.
--allowed-domain <domain>Allow HTTP imports from an additional domain beyond the default (raw.githubusercontent.com/harehare). Repeat to add multiple domains. Has no effect unless --allow-http-import (or --allow-all) is also passed.
--no-lockfileDisable the mq.lock integrity check/update.
--frozenFail instead of recording a new mq.lock entry. Use in CI once mq.lock is committed. Mutually exclusive with --no-lockfile.
--lockfile <path>Use <path> instead of ./mq.lock.

Examples:

# Enable HTTP imports, restricted to the built-in default domain
mq --allow-http-import 'self' file.md

# Force re-fetch of any HEAD/branch modules, accepting new content into mq.lock
mq --allow-http-import --refresh-modules 'self' file.md

# Only allow imports from example.com (in addition to the built-in default)
mq --allow-http-import --allowed-domain example.com 'self' file.md

# Allow multiple domains
mq --allow-http-import --allowed-domain example.com --allowed-domain raw.githubusercontent.com 'self' file.md

# Use a different lock file location
mq --allow-http-import --lockfile config/mq.lock 'self' file.md

# Skip the mq.lock check entirely
mq --no-lockfile 'self' file.md

# CI: fail if a script tries to import a URL not already recorded in the committed mq.lock
mq --allow-http-import --frozen 'self' file.md

Network and File-Write Capabilities

http(method, url) / http(method, url, body) / http(method, url, headers) / http(method, url, body, headers) and write_file(path, content) are disabled by default and must be explicitly enabled with --allow-net / --allow-write. Calling them without the corresponding flag raises a runtime error explaining how to opt in.

method is a string or symbol ("post" or :post) and accepts any HTTP method — get, post, put, delete, patch, head, and so on. The optional body argument is sent as the request body regardless of method. The optional headers argument is a dict of string to string (e.g. {"Content-Type": "application/json"}) applied to the request.

http_get(url, headers = {}), http_post(url, body, headers = {}), http_put(url, body, headers = {}), http_patch(url, body, headers = {}), http_delete(url, headers = {}), and http_head(url, headers = {}) are convenience wrappers around http(:get, url, headers), http(:post, url, body, headers), and so on, for the most common cases — headers defaults to {} and can be omitted.

http_all([...requests]) issues a batch of HTTP requests and returns an array of response bodies in the same order. Each request is a dict with a required url and optional method (defaults to get), body, and headers keys, e.g.:

mq --allow-net 'http_all([{"url": "https://example.com/a"}, {"url": "https://example.com/b", "method": "post", "body": "{}", "headers": {"Content-Type": "application/json"}}])'

Requests in the batch run concurrently when the runtime supports it, so fanning out to multiple endpoints is faster than calling http() in a loop. The same URL/domain policy applies as for http(): HTTPS only, and --allow-net (with or without a domain allowlist) is required.

Security note: http only accepts https:// URLs and is routed through the same SSRF-hardened client used for HTTP imports — no automatic redirects, and DNS results are filtered to publicly routable addresses, so a loopback/private/link-local address can’t be reached even with --allow-net set.

# Blocked by default
mq 'http_get("https://example.com")'

# Enabled explicitly
mq --allow-net 'http_get("https://example.com")'
mq --allow-net 'http(:delete, "https://example.com/resource/1")'
mq --allow-net 'http(:post, "https://example.com", "{}", {"Content-Type": "application/json"})'
mq --allow-net 'http_post("https://example.com", "{}", {"Content-Type": "application/json"})'
mq --allow-write 'write_file("out.md", "# Hello")'

--allow-net also accepts a domain allowlist, restricting http/http_* calls to just those domains (and any path under them) instead of granting unrestricted network access:

# Restrict to just example.com
mq --allow-net=example.com 'http_get("https://example.com")'

# Repeat the flag, or comma-separate, to allow more than one domain
mq --allow-net=example.com,api.example.org 'http_get("https://api.example.org")'

Comparison

Featuremoduleimportinclude
PurposeDefine a moduleLoad external moduleLoad external functions
AccessQualified access (module::func)Qualified access (module::func)Direct access (func)
Use caseOrganize code within a fileReuse modules across filesSimple function sharing