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

Introduction

This guide will teach you about mq, a command-line tool for querying and transforming Markdown files using a syntax inspired by jq. You’ll learn how to select specific elements, filter content, apply transformations, and compose these operations into powerful one-liners or reusable scripts.

Let’s get started.

Installation

The quickest way to install mq is via the install script:

curl -sSL https://mqlang.org/install.sh | bash

On macOS and Linux, you can also use Homebrew:

brew install mq

For other installation methods including Cargo, pre-built binaries, Docker, and more, see the Install page.

Your First Query

Once installed, let’s try a simple query. Save this file as hello.md:

# Hello

Welcome to **mq**.

## Getting Started

Install it, then run your first query.

## Features

- Select headings
- Filter nodes
- Transform content

Now run mq to extract all headings:

$ mq '.h' hello.md
# Hello
## Getting Started
## Features

Use to_text() to get just the heading text:

$ mq '.h | to_text' hello.md
Hello
Getting Started
Features

You can narrow it down to a specific level, for example only h2:

$ mq '.h2 | to_text' hello.md
Getting Started
Features

Queries are composable with |, just like a Unix pipeline.

What’s Next

With the basics covered, the Getting Started section walks through installation options, syntax, and common patterns. When you’re ready to look up specific behavior, the Reference covers every selector, operator, and built-in function in detail.

Getting Started

Getting Started

This section guides you through the installation of mq.

Install

Quick Install

curl -sSL https://mqlang.org/install.sh | bash
# Install the debugger
curl -sSL https://mqlang.org/install.sh | bash -s -- --with-debug

The installer will:

  • Download the latest mq binary for your platform
  • Install it to ~/.local/bin/
  • Update your shell profile to add mq to your PATH

Cargo

# Install from crates.io
cargo install mq-run
# Install from Github
cargo install --git https://github.com/harehare/mq.git mq-run --tag v0.8.5
# Latest Development Version
cargo install --git https://github.com/harehare/mq.git mq-run --bin mq
# Install the debugger
cargo install --git https://github.com/harehare/mq.git mq-run --bin mq-dbg --features="debugger"
# Install using binstall
cargo binstall [email protected]

Binaries

You can download pre-built binaries from the GitHub releases page:

# macOS (Apple Silicon)
curl -L https://github.com/harehare/mq/releases/download/v0.8.5/mq-aarch64-apple-darwin -o /usr/local/bin/mq && chmod +x /usr/local/bin/mq
# Linux x86_64
curl -L https://github.com/harehare/mq/releases/download/v0.8.5/mq-x86_64-unknown-linux-gnu -o /usr/local/bin/mq && chmod +x /usr/local/bin/mq
# Linux arm64
curl -L https://github.com/harehare/mq/releases/download/v0.8.5/mq-aarch64-unknown-linux-gnu -o /usr/local/bin/mq && chmod +x /usr/local/bin/mq
# Windows (PowerShell)
Invoke-WebRequest -Uri https://github.com/harehare/mq/releases/download/v0.8.5/mq-x86_64-pc-windows-msvc.exe -OutFile "$env:USERPROFILE\bin\mq.exe"

Homebrew

# Using Homebrew (macOS and Linux)
$ brew install mq

Docker

$ docker run --rm ghcr.io/harehare/mq:0.8.5

mq-lsp (Language Server)

The mq Language Server provides IDE features like completion, hover, and diagnostics for mq query files.

Quick Install

curl -sSL https://mqlang.org/install_lsp.sh | bash

Cargo

# Install from crates.io
cargo install mq-lsp
# Install from Github
cargo install --git https://github.com/harehare/mq.git mq-lsp --tag v0.8.5
# Latest Development Version
cargo install --git https://github.com/harehare/mq.git mq-lsp
# Install using binstall
cargo binstall [email protected]

Binaries

You can download pre-built binaries from the GitHub releases page:

# macOS (Apple Silicon)
curl -L https://github.com/harehare/mq/releases/download/v0.8.5/mq-lsp-aarch64-apple-darwin -o /usr/local/bin/mq-lsp && chmod +x /usr/local/bin/mq-lsp
# Linux x86_64
curl -L https://github.com/harehare/mq/releases/download/v0.8.5/mq-lsp-x86_64-unknown-linux-gnu -o /usr/local/bin/mq-lsp && chmod +x /usr/local/bin/mq-lsp
# Linux arm64
curl -L https://github.com/harehare/mq/releases/download/v0.8.5/mq-lsp-aarch64-unknown-linux-gnu -o /usr/local/bin/mq-lsp && chmod +x /usr/local/bin/mq-lsp
# Windows (PowerShell)
Invoke-WebRequest -Uri https://github.com/harehare/mq/releases/download/v0.8.5/mq-lsp-x86_64-pc-windows-msvc.exe -OutFile "$env:USERPROFILE\bin\mq-lsp.exe"

Shell Completion

mq can generate shell completion scripts via the completion subcommand. Supported shells: bash, elvish, fish, nushell, powershell, zsh.

# Bash (add to ~/.bashrc)
echo 'source <(mq completion bash)' >> ~/.bashrc

# Zsh (add to ~/.zshrc)
echo 'source <(mq completion zsh)' >> ~/.zshrc

# Fish
mq completion fish > ~/.config/fish/completions/mq.fish

# Elvish (add to ~/.config/elvish/rc.elv)
mq completion elvish >> ~/.config/elvish/rc.elv

# PowerShell (add to your PowerShell profile)
mq completion powershell >> $PROFILE

Nushell

Generate the completion script and source it from your config.nu:

mkdir -p ~/.config/nushell/completions
mq completion nushell > ~/.config/nushell/completions/mq.nu

Then add the following line to ~/.config/nushell/config.nu:

source ~/.config/nushell/completions/mq.nu

Restart Nushell (or run source ~/.config/nushell/config.nu) to enable completions.

Visual Studio Code Extension

You can install the VSCode extension from the Visual Studio Marketplace.

Chrome Extension

The mq Chrome extension converts the page you’re viewing to Markdown and runs mq queries against it from the toolbar popup. Queries run automatically after editing, or immediately with Cmd/Ctrl+Enter; results can be copied or downloaded as Markdown. It isn’t on the Chrome Web Store yet, so install it unpacked:

git clone https://github.com/harehare/mq.git
cd mq/packages/mq-chrome-extension
pnpm install
pnpm build

Then in Chrome, open chrome://extensions, enable Developer mode, click Load unpacked, and select packages/mq-chrome-extension/.output/chrome-mv3. See the package README for details and known limitations.

Neovim

You can install the Neovim plugin by following the instructions in the mq.nvim README.

Obsidian

You can install the mq plugin from the Obsidian Community Plugins directory. It runs mq queries directly inside Obsidian, fully client-side via WebAssembly.

GitHub Actions

You can use mq in your GitHub Actions workflows with the Setup mq action:

steps:
  - uses: actions/checkout@v4
  - uses: harehare/setup-mq@v1
  - run: mq '.code' README.md

MCP (Model Context Protocol) server

mq supports an MCP server for integration with LLM applications.

See the MCP documentation for more information.

Python

You can use mq in Python through the markdown-query package:

# Install from PyPI
$ pip install markdown-query

npm

You can use mq in npm through the mq-web package:

$ npm i mq-web

Web crawler

Quick Install

curl -sSL https://mqlang.org/install_crawler.sh | bash

The installer will:

  • Download the latest mq-crawl binary for your platform
  • Install it to ~/.local/bin/
  • Verify the checksum of the downloaded binary
  • Update your shell profile to add mq-crawl to your PATH

Homebrew

brew install harehare/tap/mq-crawl

Cargo

cargo install mq-crawler

See the Web Crawler page for usage details.

Development

Prerequisites

Setting up the development environment

Clone the repository:

git clone https://github.com/harehare/mq.git
cd mq

Install development dependencies:

# Using cargo
cargo install just wasm-pack

Or if you prefer using asdf:

# Using asdf
asdf install

Or if you prefer using Nix (see Using Nix below for details):

direnv allow  # or: nix develop

Using Nix

If you have Nix with flakes enabled, the repository includes a flake.nix that provides a fully reproducible development environment with Rust (matching rust-toolchain.toml), just, wasm-pack, rust-analyzer, and all other required tools.

The repository also includes a .envrc file, so if you have direnv installed, run the following once and the environment will be activated automatically whenever you enter the directory:

direnv allow

If you prefer not to use direnv, you can enter the shell manually:

nix develop

Common development tasks

Here are some useful commands to help you during development:

# Run the CLI with the provided arguments
just run '.code'

# Run formatting, linting and all tests
just test-all

# Run formatter and linter
just lint

# Build the project in release mode
just build

# Update documentation
just docs

Check the just --list for more available commands and build options.

Syntax Highlighting

Using bat

bat is a cat clone with syntax highlighting and Git integration. You can use mq’s Sublime syntax file to enable syntax highlighting for mq files in bat.

Setting up mq syntax highlighting

  1. Create the bat syntax directory if it doesn’t exist:
mkdir -p "$(bat --config-dir)/syntaxes"
  1. Copy the mq syntax file:
# Clone the mq repository or download mq.sublime-syntax
curl -o "$(bat --config-dir)/syntaxes/mq.sublime-syntax" \
  https://raw.githubusercontent.com/harehare/mq/main/assets/mq.sublime-syntax
  1. Rebuild bat’s cache:
bat cache --build

Usage

Now you can use bat to display mq files with syntax highlighting:

# View an mq file with syntax highlighting
bat query.mq

Example

Create a sample mq file:

cat > example.mq << 'EOF'
# This is a comment
def greet(name):
  s"Hello, ${name}!"
end

.h | .text | greet("World")
EOF

View it with syntax highlighting:

bat example.mq

Helix

Helix discovers languages, tree-sitter grammars, and LSP servers through its languages.toml config file rather than a dedicated plugin. mq syntax highlighting is provided by tree-sitter-mq, and language features (completion, hover, diagnostics) by mq-lsp.

Setting up mq support

  1. Add the following to languages.toml in your Helix config directory (~/.config/helix/languages.toml):
[[language]]
name = "mq"
scope = "source.mq"
file-types = ["mq"]
comment-token = "#"
language-servers = ["mq-lsp"]
indent = { tab-width = 2, unit = "  " }

[language-server.mq-lsp]
command = "mq-lsp"

[[grammar]]
name = "mq"
source = { git = "https://github.com/harehare/tree-sitter-mq", rev = "main" }
  1. Fetch and build the grammar:
hx --grammar fetch
hx --grammar build
  1. Copy the highlight queries from tree-sitter-mq into your Helix runtime directory, since Helix does not bundle queries for externally configured grammars:
mkdir -p ~/.config/helix/runtime/queries/mq
curl -o ~/.config/helix/runtime/queries/mq/highlights.scm \
  https://raw.githubusercontent.com/harehare/tree-sitter-mq/main/queries/highlights.scm
  1. Make sure mq-lsp is installed and on your PATH.

  2. Verify the setup:

hx --health mq

This should report the tree-sitter grammar as found and mq-lsp as configured.

Editor Support

In addition to bat, mq syntax highlighting is available for:

  • Visual Studio Code: Install the mq extension
  • Obsidian: Install the mq plugin
  • Helix: See the Helix section above for languages.toml setup

Playground

An Online Playground is available, powered by WebAssembly.

Interactive TUI

The Text-based User Interface (TUI) provides an interactive way to explore and query Markdown files directly in your terminal.

Quick Install

curl -fsSL https://raw.githubusercontent.com/harehare/mq-tui/main/bin/install.sh | bash
$ mq tui file.md

TUI Features

  • Interactive Querying: Enter and edit queries in real-time with immediate feedback
  • Detail View: Examine the structure of selected markdown nodes in depth
  • Navigation: Browse through query results with keyboard shortcuts
  • Query History: Access and reuse previous queries

TUI Key Bindings

KeyAction
: (colon)Enter query mode
EnterExecute query
Esc / qExit query mode / Exit app
/k, /jNavigate results
dToggle detail view
? / F1Show help screen
Ctrl+lClear query
PgUp/PgDnPage through results
Home/EndJump to first/last result

Repository

The source code and further documentation for mqt are available on GitHub:

https://github.com/harehare/mqt

Web crawler

mq-crawler is a web crawler that fetches HTML content from websites, converts it to Markdown format, and processes it with mq queries.

Key Features

  • HTML to Markdown conversion: Automatically converts crawled HTML pages to clean Markdown
  • robots.txt compliance: Respects robots.txt rules for ethical web crawling
  • mq-lang integration: Processes content with mq-lang queries for filtering and transformation
  • Configurable crawling: Customizable delays, domain restrictions, and link discovery
  • Flexible output: Save to files or output to stdout
  • Headless Chrome: Built-in headless Chrome for JavaScript-heavy sites (no external server needed)
  • WebDriver support: Browser-based crawling via Selenium WebDriver
  • Domain filtering: Restrict crawling to specific domains
  • Sitemap ingestion: Seed the crawl frontier from a sitemap.xml (or sitemap index) up front
  • Max pages limit: Cap the total number of pages visited to bound resource usage
  • Checkpoint & resume: Periodically snapshot crawl progress and resume an interrupted crawl later
  • Retry with backoff: Automatically retries failed requests (network errors, 429, 5xx) with exponential backoff
  • Custom headers & cookies: Send custom HTTP headers and cookies with every request
  • Authentication: Basic and bearer-token authentication for protected sites

Installation

Quick Install

curl -sSL https://mqlang.org/install_crawler.sh | bash

The installer will:

  • Download the latest mq-crawl binary for your platform
  • Install it to ~/.local/bin/
  • Verify the checksum of the downloaded binary
  • Update your shell profile to add mq-crawl to your PATH

After installation, restart your terminal or source your shell profile, then verify:

mq-crawl --version

Homebrew

brew install harehare/tap/mq-crawl

Cargo

cargo install mq-crawler

Binaries

You can download pre-built binaries from the GitHub releases page.

Usage

mq-crawl [OPTIONS] <URL>

Options

OptionDescriptionDefault
-o, --output <OUTPUT>Directory to save markdown files (stdout if not specified)stdout
-d, --crawl-delay <SECONDS>Delay between requests in seconds1
-c, --concurrency <N>Number of concurrent workers1
--depth <DEPTH>Maximum crawl depth (0 = start URL only)unlimited
--max-pages <N>Maximum total number of pages to visit before stoppingunlimited
--checkpoint-path <PATH>File to periodically save crawl progress to (JSON), enabling --resume-from
--checkpoint-interval-pages <N>Pages crawled between checkpoint saves (requires --checkpoint-path)20
--resume-from <PATH>Resume a crawl from a checkpoint file written via --checkpoint-path
-q, --mq-query <QUERY>mq-lang query for processing content
--robots-path <PATH>Custom robots.txt file path
--allowed-domains <DOMAINS>Comma-separated list of extra domains to crawl; the start URL’s domain is always includedstart domain only
--sitemap <SITEMAP_URL>URL of a sitemap.xml (or sitemap index) to enumerate additional seed URLs from
--max-retries <N>Maximum retry attempts for failed requests (network errors, 429, 5xx)3
--retry-initial-backoff <SECONDS>Delay before the first retry0.5
--retry-max-backoff <SECONDS>Maximum delay between retries10
--retry-backoff-multiplier <FLOAT>Multiplier applied to the retry delay after each failed attempt2
--header <KEY: VALUE>Custom HTTP header to send with every request (repeatable); non-browser crawling only
--cookie <NAME=VALUE>Cookie to send with every request (repeatable); non-browser crawling only
--basic-auth <USER:PASS>HTTP Basic authentication credentials; non-browser crawling only
--bearer-token <TOKEN>Bearer token for Authorization header; non-browser crawling only
--headlessUse built-in headless Chrome (Chrome/Chromium must be installed)
--chrome-path <PATH>Path to Chrome/Chromium executable (requires --headless)auto-detect
-U, --webdriver-url <URL>External WebDriver URL for browser-based crawling
--page-load-timeout <SECONDS>Timeout for loading a single page30
--script-timeout <SECONDS>Timeout for executing scripts on the page10
--implicit-timeout <SECONDS>Timeout for element finding5
--extract-scripts-as-code-blocksExtract <script> tags as code blocks
--generate-front-matterGenerate YAML front matter from page metadata
--use-title-as-h1Use the HTML <title> as the first H1 heading
-f, --format <FORMAT>Output format: text or jsontext

Examples

# Basic crawling to stdout
mq-crawl https://example.com

# Save to directory with custom delay
mq-crawl -o ./output -d 2 https://example.com

# Limit crawl depth and use concurrent workers
mq-crawl --depth 2 -c 3 https://example.com

# Process with mq-lang query
mq-crawl -q '.h | select(contains("News"))' https://example.com

# Extract code blocks from a docs site
mq-crawl -q '.code' https://docs.example.com

Domain Filtering

By default, only the start URL’s domain is crawled. Use --allowed-domains to include additional domains:

# Also crawl docs.example.com and blog.example.com
# The start URL's domain is always included automatically
mq-crawl --allowed-domains docs.example.com,blog.example.com https://example.com

Sitemap Ingestion

Use --sitemap to seed the crawl frontier with every URL listed in a sitemap.xml, in addition to the start URL. Sitemap index files (<sitemapindex>) are followed recursively. Discovered URLs still respect robots.txt, --allowed-domains, and --depth:

mq-crawl --sitemap https://example.com/sitemap.xml https://example.com

# Combine with --depth 0 to crawl exactly the pages listed in the sitemap
# without following any links.
mq-crawl --depth 0 --sitemap https://example.com/sitemap.xml https://example.com

Limiting Crawl Size

Use --max-pages to cap the total number of pages visited (queued, in-flight, or crawled), independent of --depth. This is useful as a safety valve on deep or link-heavy sites:

# Stop after visiting 500 pages, regardless of depth
mq-crawl --max-pages 500 https://example.com

Checkpoint & Resume

For large crawls that may be interrupted (network loss, process restart, --max-pages cutoff), use --checkpoint-path to periodically save the visited set and pending frontier to a JSON file, and --resume-from to continue from it later:

# Save a checkpoint every 20 pages (default) to crawl-state.json
mq-crawl --checkpoint-path crawl-state.json https://example.com

# Save more frequently
mq-crawl --checkpoint-path crawl-state.json --checkpoint-interval-pages 5 https://example.com

# Resume an interrupted crawl from the last checkpoint
mq-crawl --resume-from crawl-state.json --checkpoint-path crawl-state.json https://example.com

A checkpoint is also written once the crawl stops for any reason (queue exhausted, --max-pages reached), so --resume-from reflects the true end state of the previous run. Pass --checkpoint-path alongside --resume-from if you want the resumed run to keep checkpointing as it continues.

Retry & Backoff

Failed requests (network errors, 429 Too Many Requests, and 5xx server errors) are retried automatically with exponential backoff:

# Retry up to 5 times, starting at a 1s delay and doubling up to a 30s cap
mq-crawl --max-retries 5 --retry-initial-backoff 1 --retry-max-backoff 30 https://example.com

# Disable retries entirely
mq-crawl --max-retries 0 https://example.com

Custom Headers, Cookies & Authentication

Use --header, --cookie, --basic-auth, or --bearer-token to crawl sites that require authentication. These apply to standard (non-browser) crawling only — they are ignored with --headless or -U/--webdriver-url:

# Custom header
mq-crawl --header "X-Api-Key: secret" https://example.com

# One or more cookies (combined into a single Cookie header)
mq-crawl --cookie "session=abc123" --cookie "theme=dark" https://example.com

# HTTP Basic authentication
mq-crawl --basic-auth alice:s3cret https://example.com

# Bearer token authentication
mq-crawl --bearer-token eyJhbGciOi... https://example.com

Headless Chrome

For JavaScript-heavy sites, use the built-in headless Chrome without an external server:

# Use built-in headless Chrome (Chrome or Chromium must be installed)
mq-crawl --headless https://spa-example.com

# Specify a custom Chrome/Chromium executable path
mq-crawl --headless --chrome-path /usr/bin/chromium https://spa-example.com

WebDriver

Alternatively, use an external Selenium WebDriver server:

# Start Selenium server first
# docker run -d -p 4444:4444 selenium/standalone-chrome

# Crawl with WebDriver
mq-crawl -U http://localhost:4444 https://spa-example.com

# Custom timeouts
mq-crawl -U http://localhost:4444 \
  --page-load-timeout 60 \
  --script-timeout 30 \
  --implicit-timeout 10 \
  https://spa-example.com

HTML to Markdown Options

# Generate YAML front matter with metadata
mq-crawl --generate-front-matter https://example.com

# Use page title as H1 heading
mq-crawl --use-title-as-h1 https://example.com

# Extract <script> tags as code blocks
mq-crawl --extract-scripts-as-code-blocks https://example.com

# Combine options
mq-crawl --generate-front-matter --use-title-as-h1 -o ./docs https://example.com

Output Formats

# Output as JSON
mq-crawl --format json https://example.com

# Output as plain text (default)
mq-crawl --format text https://example.com

MCP

The mq MCP server enables integration with AI applications that support the Model Context Protocol (MCP). This server provides tools for processing Markdown content using mq queries.

Overview

The MCP server exposes four main tools:

  • html_to_markdown - Converts HTML to Markdown and applies mq queries
  • extract_markdown - Extracts content from Markdown using mq queries
  • available_functions - Lists available mq functions
  • available_selectors - Lists available mq selectors

Configuration

Claude Desktop

Add the following to your Claude Desktop configuration file:

{
  "mcpServers": {
    "mq": {
      "command": "/path/to/mq",
      "args": ["mcp"]
    }
  }
}

Claude Code

$ claude mcp add mq-mcp -- mq mcp

VS Code

Add the following to your VS Code settings:

{
  "mcp": {
    "servers": {
      "mq-mcp": {
        "type": "stdio",
        "command": "/path/to/mq",
        "args": ["mcp"]
      }
    }
  }
}

Replace /path/to/mq with the actual path to your mq binary.

Usage

Converting HTML to Markdown

The html_to_markdown tool converts HTML content to Markdown format and applies an optional mq query:

html_to_markdown({
  "html": "<h1>Title</h1><p>Content</p>",
  "query": ".h1"
})

Extracting from Markdown

The extract_markdown tool processes Markdown content with mq queries:

extract_markdown({
  "markdown": "# Title\n\nContent",
  "query": ".h1"
})

Getting Available Functions

The available_functions tool returns all available mq functions:

available_functions()

Returns JSON with function names, descriptions, parameters, and examples.

Getting Available Selectors

The available_selectors tool returns all available mq selectors:

available_selectors()

Returns JSON with selector names, descriptions, and parameters.

Query Examples

Common mq queries you can use with the MCP tools:

  • .h1 - Select all h1 headings
  • select(.code.lang == "js") - Select JavaScript code blocks
  • .text - Extract all text content
  • select(.h1, .h2) - Select h1 and h2 headings
  • select(not(.code)) - Select everything except code blocks

Web API

mq-web-api is an HTTP/REST server that exposes mq queries over the network. It provides a curl-friendly shortcut endpoint, a JSON API, an OpenAPI specification, and a Swagger UI.

Overview

The server exposes the following endpoints:

MethodPathDescription
GET/healthHealth check
POST/{query}Curl-friendly shortcut: query in the path, raw body (Markdown/HTML/XML/JSON/CSV/…)
GET/api/v1/queryExecute a query (query-string parameters)
POST/api/v1/queryExecute a query (JSON body)
POST/api/v1/batchExecute a query against multiple documents in one request
POST/api/v1/checkType-check a query
POST/api/v1/formatFormat a query
GET/api/v1/functionsList builtin mq functions
GET/api/v1/selectorsList builtin mq selectors
POST/api/v1/lintLint a query
GET/api/v1/openapi.jsonOpenAPI specification
GET/docsSwagger UI

Legacy paths (/api/query, /api/check, /api/format, /openapi.json) redirect permanently to the /api/v1/ equivalents.

A public instance is hosted at https://api.mqlang.org/ for quick trials (rate-limited, see Rate Limiting). For production or higher-volume use, self-host the server yourself (see Usage below).

Configuration

All settings are controlled through environment variables.

Server

VariableDefaultDescription
HOST0.0.0.0Bind address
PORT8080Bind port
RUST_LOGmq_web_api=debug,tower_http=debugLog level filter
LOG_FORMATjsonLog format: json or text
CORS_ORIGINS*Comma-separated allowed origins
QUERY_TIMEOUT_SECONDS10Max seconds a single query may run before it’s aborted
MAX_REQUEST_BODY_SIZE10485760Max accepted HTTP request body size, in bytes. Larger requests get 413 Payload Too Large
REQUEST_TIMEOUT_SECONDS30Max seconds a single HTTP request may take end-to-end (routing, auth, rate limiting, handler). Exceeding it returns 408 Request Timeout

Rate Limiting

VariableDefaultDescription
RATE_LIMIT_REQUESTS_PER_WINDOW100Maximum requests per window
RATE_LIMIT_WINDOW_SIZE_SECONDS3600Window size in seconds
RATE_LIMIT_CLEANUP_INTERVAL_SECONDS3600Expired-entry cleanup interval

Authentication

Disabled by default. When AUTH_ENABLED=true, every endpoint except /health, /docs, and the OpenAPI spec requires Authorization: Bearer <key>. Keys carry a scope (read: functions/selectors/check/format/lint, query: /api/v1/query, /api/v1/batch, /{query}) and an optional rate limit override enforced independently of the IP-based limit above.

VariableDefaultDescription
AUTH_ENABLEDfalseRequire an API key on protected endpoints
API_KEYSComma-separated keys; each gets both scopes and no rate limit override
API_KEYS_FILEPath to a JSON file for per-key name/scopes/rate limit (takes precedence over API_KEYS)

API_KEYS_FILE format:

[
  { "key": "sk_live_...", "name": "acme-corp", "scopes": ["read", "query"], "rate_limit_per_window": 5000 },
  { "key": "sk_live_...", "name": "acme-readonly", "scopes": ["read"] }
]

scopes defaults to ["read", "query"] when omitted; rate_limit_per_window defaults to the server-wide RATE_LIMIT_REQUESTS_PER_WINDOW.

OpenTelemetry (requires otel feature)

VariableDefaultDescription
OTEL_EXPORTER_OTLP_ENDPOINTOTLP exporter endpoint (e.g. http://localhost:4317)
OTEL_SERVICE_NAMEmq-web-apiService name reported to the collector

Usage

Running locally

# Default settings
cargo run --bin mq-web-api

# Custom host and port
HOST=localhost PORT=3000 cargo run --bin mq-web-api

# Text-format logs
LOG_FORMAT=text cargo run --bin mq-web-api

# Restrict CORS origins
CORS_ORIGINS="https://example.com,https://app.example.com" cargo run --bin mq-web-api

# Enable OpenTelemetry
cargo run --features otel --bin mq-web-api

Docker

Build and run from the workspace root:

docker build -f crates/mq-web-api/Dockerfile.vercel -t mq-web-api .
docker run -p 8080:8080 mq-web-api

With custom configuration:

docker run -p 3000:3000 \
  -e PORT=3000 \
  -e LOG_FORMAT=text \
  -e CORS_ORIGINS="https://example.com" \
  mq-web-api

Examples

Curl-friendly shortcut

The mq query goes in the URL path; the input content is the raw request body.

curl --data-binary @doc.md https://api.mqlang.org/.h1

html, xml, and json input are auto-detected from the body’s leading bytes. Other formats (csv, tsv, psv, yaml, toml, hcl, toon) need an explicit input_format:

curl --data-binary @page.html https://api.mqlang.org/.h1
curl --data-binary @data.json 'https://api.mqlang.org/json::json_to_markdown_table()'
curl --data-binary @data.csv 'https://api.mqlang.org/csv::csv_to_markdown_table()?input_format=csv'

Use --data-binary, not -d/--data. curl -d @file strips newlines from the file, which breaks Markdown/HTML/XML parsing. --data-binary sends the file exactly as-is.

For queries that need modules, args, or aggregate, use POST /api/v1/query instead.

Execute a query (GET)

curl "http://localhost:8080/api/v1/query?query=.h&input=%23%20Title%0A%0AContent&input_format=markdown"

Execute a query (POST)

curl -X POST http://localhost:8080/api/v1/query \
  -H "Content-Type: application/json" \
  -d '{
    "query": ".h",
    "input": "# Title\n\nContent",
    "input_format": "markdown"
  }'

Batch query (multiple documents in one request)

POST /api/v1/batch runs one query against multiple documents in a single request, avoiding an HTTP round trip per document. Each document is processed independently — one failing document doesn’t fail the others, and items in the response is ordered like inputs (max 100 entries).

curl -X POST http://localhost:8080/api/v1/batch \
  -H "Content-Type: application/json" \
  -d '{
    "query": ".h1",
    "inputs": ["# Doc One\n\nBody.", "# Doc Two\n\nBody."],
    "input_format": "markdown"
  }'

Type-check a query

curl -X POST http://localhost:8080/api/v1/check \
  -H "Content-Type: application/json" \
  -d '{"query": "upcase | downcase"}'

Format a query

curl -X POST http://localhost:8080/api/v1/format \
  -H "Content-Type: application/json" \
  -d '{"query": "if(a):1 elif(b):2 else:3"}'

Lint a query

curl -X POST http://localhost:8080/api/v1/lint \
  -H "Content-Type: application/json" \
  -d '{"query": "let x = .h1 | .text"}'

Authenticated request (when AUTH_ENABLED=true)

curl -H "Authorization: Bearer sk_live_..." \
  -X POST http://localhost:8080/api/v1/query \
  -H "Content-Type: application/json" \
  -d '{"query": ".h1", "input": "# Title"}'

List builtin functions and selectors

curl http://localhost:8080/api/v1/functions
curl http://localhost:8080/api/v1/selectors

Features

FeatureDefaultDescription
use_mimallocenabledUse mimalloc as the global allocator
oteldisabledEnable OpenTelemetry tracing via OTLP

See the mq-web-api crate for the full README and source.

Debugger

The mq debugger allows you to step through execution, set breakpoints, and inspect the state of your mq programs during runtime. This is particularly useful for debugging complex queries and understanding how data flows through your transformations.

Installation

To use the debugger, you need to install mq with the debugger feature enabled.

Quick Install

curl -sSL https://mqlang.org/install.sh | bash -s -- --with-debug

Cargo

You can do this by building from source:

cargo install --git https://github.com/harehare/mq.git mq-run --bin mq-dbg

Alternatively, if a prebuilt binary is available for your platform, download it from the releases page and ensure it is in your PATH.

Homebrew

If you use Homebrew, you can install the debugger-enabled mq with:

brew install harehare/tap/mq-dbg

Getting Started

The debugger is available through the mq-dbg binary when the debugger feature is enabled.

# Enable debugging for an mq script
mq-dbg -f your-script.mq input.md

Debugger Interface

Once the debugger starts, you’ll see a prompt (mqdbg) where you can enter debugging commands. The debugger will automatically display the current source code location with line numbers, highlighting the current execution point.

   10| def process_headers() {
=> 11|   . | select(.type == "heading")
   12|     | map(.level)
   13| }
(mqdbg)

Available Commands

The debugger supports the following commands:

CommandAliasDescription
stepsStep into the next expression, diving into function calls
nextnStep over the current expression, skipping over function calls
finishfRun until the current function returns
continuecContinue normal execution until the next breakpoint

Breakpoint Commands

CommandAliasDescription
breakpoint [line]b [line]Set a breakpoint at the specified line number
breakpointbList all active breakpoints
clear [id]cl [id]Clear a specific breakpoint by ID
clearclClear all breakpoints

Inspection Commands

CommandAliasDescription
infoiDisplay current environment variables and context
listlShow source code around the current execution point
long-listllShow the entire source code with line numbers
backtracebtPrint the current call stack

Control Commands

CommandAliasDescription
help-Display help information for all commands
quitqQuit the debugger and exit

Setting Breakpoints

You can set breakpoints in several ways:

Interactive Breakpoints

You can set breakpoints interactively from the debugger prompt:

(mqdbg) breakpoint 15

(mqdbg) breakpoint
Breakpoints:
  [1] 15:10 (enabled)

Programmatic Breakpoints

You can also set breakpoints directly in your mq code using the breakpoint() function:

def process_data(items) {
   breakpoint()  # Execution will pause here when debugger is attached
   | items | filter(fn(item): item == "test")
}

When the debugger encounters a breakpoint() function call during execution, it will automatically pause and enter interactive debugging mode.

Note: The breakpoint() function only has an effect when running under the debugger (mq-dbg). In normal execution (mq), it is ignored and has no impact on performance.

Stopping on Errors

Pass --stop-on-error to drop into the debugger prompt whenever an error propagates uncaught (i.e. not caught by try/catch), instead of only stopping at breakpoints:

mq-dbg --stop-on-error -f your-script.mq input.md

The error is still reported and evaluation still stops afterward; --stop-on-error just gives you a chance to inspect the call stack and variables (via backtrace, info, etc.) before that happens.

External Subcommands

You can extend mq with custom subcommands by placing executable files starting with mq- in ~/.local/bin/ or anywhere in your PATH.

Command Resolution

When you run mq <subcommand>, mq searches for an executable named mq-<subcommand> in the following order:

  1. ~/.local/bin/ directory
  2. Directories in PATH

The first match found is used.

Listing Available Subcommands

Use the --list flag to see all available subcommands:

mq --list

This makes it easy to add your own tools and workflows to mq without modifying the core binary.

External Tools

The following external tools are available to extend mq’s functionality:

  • mq-check - A syntax and semantic checker for mq files.
  • mq-content-lint - A Markdown linter that uses mq queries to enforce both built-in rules and custom project-specific content checks.
  • mq-conv - A CLI tool for converting various file formats to Markdown.
  • mq-crawler - A web crawler that extracts structured data from websites and outputs it in Markdown format.
  • mq-db - Markdown-specialized embedded database with interval-indexed block storage and hierarchical query support.
  • mq-docs - A documentation generator for mq functions and selectors.
  • mq-fmt- Formatter for mq query language (.mq) files.
  • mq-http - A lightweight HTTP server that executes mq scripts for each request.
  • mq-lint - Static analysis linter for mq files (correctness, style, complexity, selector, and module rules).
  • mq-lsp - Language Server Protocol (LSP) implementation for mq query files, providing IDE features like completion, hover, and diagnostics.
  • mq-mcp - Model Context Protocol (MCP) server implementation for AI assistants.
  • mq-mount - Mounts Markdown files as a virtual filesystem, where headings become directories and section bodies become content.md files.
  • mq-serve - A browser-based Markdown viewer with mq query support.
  • mq-task - Task runner using mq for Markdown-based task definitions.
  • mq-tui - Terminal User Interface (TUI) for interactive mq query.
  • mq-update - Update mq binary to the latest version.
  • mq-view - Viewer for Markdown content.

AI Assistant Integration

  • MCP: mq-mcp provides a Model Context Protocol server, enabling mq to be used from any MCP-compatible AI assistant.
  • Skill: The processing-markdown skill adds mq-aware assistance directly to your AI coding workflow.

Language Bindings

mq provides language bindings for several programming languages, allowing you to integrate mq’s Markdown processing capabilities directly into your applications.

Available Bindings

LanguageRepository
Elixirmq_elixir
Pythonmq-python
Rubymq-ruby
Javamq-java
Gomq-go
.NETmqnet

Example

This page demonstrates practical examples of mq queries for common Markdown processing tasks. Each example includes the query, explanation, and typical use cases.

Basic Element Selection

Select All Headings

Extract all headings from a markdown document:

.h

Input example:

# Main Title
## Section 1
### Subsection 1.1
## Section 2

Output: Returns all heading elements with their levels and text.

Extract Specific Table Row

Extract the second row from a markdown table:

.[1][]

Input example:

| Name  | Age | City |
| ----- | --- | ---- |
| Alice | 30  | NYC  |
| Bob   | 25  | LA   |

Output: Returns ["Bob", "25", "LA"]

Extract Specific List

Extract the second list from the document:

.[1]

Code Block Operations

Exclude Code Blocks

Filter out all code blocks from a document, keeping only prose content:

select(!.code)

Input example:

This is a paragraph.

```js
console.log("code");
```

Another paragraph.

Output: Returns only the paragraph elements, excluding the code block.

Extract JavaScript Code Blocks

Select only code blocks with a specific language:

select(.code.lang == "js")

Input example:

```js
const x = 1;
```

```python
x = 1
```

```js
const y = 2;
```

Output: Returns only the two JavaScript code blocks.

Extract Language Names

Get a list of all programming languages used in code blocks:

.code.lang

Example output: ["js", "python", "rust", "bash"]

Extract MDX Components

Select all MDX components (JSX-like elements in Markdown):

select(is_mdx())

Input example:

Regular paragraph.

<CustomComponent prop="value" />

Another paragraph.

<AnotherComponent>
  Content
</AnotherComponent>

Output: Returns only the MDX component elements.

Get all URLs from markdown links:

.link.url

Input example:

Check out [mq](https://mqlang.org) and [GitHub](https://github.com).

Example output: ["https://mqlang.org", "https://github.com"]

Advanced Markdown Processing

Generate Table of Contents

Create a hierarchical table of contents from headings:

.h
| let link = to_link("#" + to_text(self), to_text(self), "")
| let level = .h.depth
| if (!is_none(level)): to_md_list(link, level - 1)

Input example:

# Introduction
## Getting Started
### Installation
## Usage

Output:

- [Introduction](#introduction)
  - [Getting Started](#getting-started)
    - [Installation](#installation)
  - [Usage](#usage)

Generate XML Sitemap

Create an XML sitemap from markdown files:

def sitemap(item, base_url):
    let path = replace(to_text(item), ".md", ".html")
    | let loc = base_url + path
    | s"<url>
  <loc>${loc}</loc>
  <priority>1.0</priority>
  </url>"
end

Usage example:

$ mq 'sitemap(__FILE__, "https://example.com")' docs/**/*.md

Example output:

<url>
  <loc>https://example.com/docs/intro.html</loc>
  <priority>1.0</priority>
</url>

Section Operations

The section module provides functions for splitting and filtering Markdown documents by section. There are three ways to use it:

StyleSyntaxNotes
importimport "section" then section::fn()Namespaced — recommended
includeinclude "section" then fn()No namespace prefix
-A flagmq -A 'section::fn()'Aggregate mode: processes all nodes at once

Note: Section functions need all document nodes at once. Use -A on the command line, or nodes in inline queries. If you forget and call a section::* function on a single node, mq prints a warning on stderr (e.g. mq: section functions expect all document nodes; got a single node. Pass -A on the command line or pipe through nodes first.) and treats the node as a one-element array instead of silently producing meaningless results.

Extract Sections by Title

-A flag (command line):

$ mq -A 'section::section("Installation")' README.md

import + nodes (inline query or script):

import "section"
| nodes
| section::section("Installation")

include (no namespace prefix):

include "section"
| nodes
| section("Installation")

Section objects are automatically expanded to Markdown nodes in CLI output, so collect() is not needed.

Note (code usage): When using the section module from Rust or other code (not the CLI), section objects are plain dicts and must be explicitly converted with section::collect():

import "section"
| nodes
| section::section("Installation")
| section::collect()

Input example:

# Introduction

Welcome to the project.

## Installation

Run the following command.

## Usage

Use the tool like this.

Output:

## Installation

Run the following command.

Extract Body Only

Use bodies() to get section content without the header:

$ mq -A 'section::section("Installation") | section::bodies() | first' README.md

Output: Returns only the body nodes of the “Installation” section, without the ## header.

Filter by Heading Level

Use by_level() to filter sections by heading level. Accepts a number or a range:

# h2 sections only
$ mq -A 'section::sections() | section::by_level(2)' README.md

# h1 and h2 sections (1..2 includes both)
$ mq -A 'section::sections() | section::by_level(1..2)' README.md

Input example:

# Chapter 1

Intro.

## Section 1.1

Detail.

# Chapter 2

Content.

by_level(1) output:

# Chapter 1

Intro.

# Chapter 2

Content.

Split Document by Header Level

Split a document into sections at a specific heading level and flatten back to Markdown:

$ mq -A 'section::split(2) | section::collect()' README.md

Or with nodes:

import "section"
| nodes
| section::split(2)
| section::collect()

Generate Table of Contents from Sections

$ mq -A 'section::sections() | section::toc()' README.md

Input example:

# Introduction

## Getting Started

### Prerequisites

## Advanced Usage

Output: ["- Introduction", " - Getting Started", " - Prerequisites", " - Advanced Usage"]

Filter Sections with Content

Filter sections that have content beyond the header:

$ mq -A 'section::sections() | filter(fn(s): section::has_body(s);) | section::titles()' README.md

Input example:

# Introduction

Welcome to the project.

## Empty Section

## Usage

Use the tool like this.

Output: ["Introduction", "Usage"]

Delete a Section by Heading

Use filter_sections() with a negated predicate to drop sections whose title matches, then flatten back to Markdown with collect():

$ mq -A 'section::filter_sections(fn(s): section::title(s) != "Deprecated";) | section::collect()' README.md

Or with nodes:

import "section"
| nodes
| section::filter_sections(fn(s): section::title(s) != "Deprecated";)
| section::collect()

Input example:

# Introduction

Welcome to the project.

## Installation

Run the following command.

## Deprecated

Do not use this anymore.

Output:

# Introduction

Welcome to the project.

## Installation

Run the following command.

Table Operations

The table module provides functions for extracting and transforming Markdown tables.

Note: Table functions need all document nodes at once. Use -A on the command line, or nodes in inline queries. Unlike the section module, import "table" must be written explicitly.

Extract Tables

-A flag (command line):

$ mq -A 'import "table" | table::tables()' README.md

import + nodes (inline query or script):

import "table"
| nodes
| table::tables()

Table objects are automatically expanded to Markdown nodes in CLI output, so to_markdown() is not needed.

Note (code usage): When using the table module from Rust or other code (not the CLI), table objects are plain dicts and must be explicitly converted with table::to_markdown():

import "table"
| nodes
| table::tables()
| table::to_markdown

Input example:

| Name  | Age |
| ----- | --- |
| Alice | 30  |
| Bob   | 25  |

Output:

| Name  | Age |
| ----- | --- |
| Alice | 30  |
| Bob   | 25  |

Add a Row to a Table

$ mq -A 'import "table" | table::tables() | first | table::add_row(["Charlie", "35"])' README.md

Input example:

| Name  | Age |
| ----- | --- |
| Alice | 30  |

Output:

| Name    | Age |
| ------- | --- |
| Alice   | 30  |
| Charlie | 35  |

Convert Table to CSV

$ mq -A 'import "table" | table::tables() | first | table::to_csv' README.md

Output: Returns the table as a CSV string.

Reshape a Table: Wide to Long (pivot_longer)

Unpivot a set of columns into name/value row pairs, keeping the remaining columns as identifiers. value_columns is an array of column indices; names_to and values_to (both optional) name the two new columns.

$ mq -A 'import "table" | let t = first(table::tables()) | table::pivot_longer(t, [1, 2, 3], "quarter", "score")' README.md

Input example:

| Name  | Q1 | Q2 | Q3 |
| ----- | -- | -- | -- |
| Alice | 10 | 20 | 30 |
| Bob   | 5  | 15 | 25 |

Output:

| Name  | quarter | score |
| ----- | ------- | ----- |
| Alice | Q1      | 10    |
| Alice | Q2      | 20    |
| Alice | Q3      | 30    |
| Bob   | Q1      | 5     |
| Bob   | Q2      | 15    |
| Bob   | Q3      | 25    |

Reshape a Table: Long to Wide (pivot_wider)

The inverse of pivot_longer: spread a key/value column pair back out into one column per distinct key, grouping rows by the remaining identifier columns. names_from is the column index whose distinct values become new headers; values_from is the column index supplying the values. Combinations missing from the input become empty cells.

$ mq -A 'import "table" | table::tables() | first | table::pivot_wider(1, 2)' README.md

Input example:

| Name  | quarter | score |
| ----- | ------- | ----- |
| Alice | Q1      | 10    |
| Alice | Q2      | 20    |
| Alice | Q3      | 30    |
| Bob   | Q1      | 5     |
| Bob   | Q2      | 15    |
| Bob   | Q3      | 25    |

Output:

| Name  | Q1 | Q2 | Q3 |
| ----- | -- | -- | -- |
| Alice | 10 | 20 | 30 |
| Bob   | 5  | 15 | 25 |

Custom Functions and Programming

Define Custom Function

Create reusable functions for complex transformations:

def snake_to_camel(x):
  let words = split(x, "_")
  | foreach (word, words):
      let first_char = upcase(first(word))
      | let rest_str = downcase(slice(word, 1, len(word)))
      | s"${first_char}${rest_str}";
  | join("")
end
| snake_to_camel("hello_world")

Example input: "user_name" Example output: "UserName"

Map Over Arrays

Transform each element in an array:

map([1, 2, 3, 4, 5], fn(x): x + 1;)

Example output: [2, 3, 4, 5, 6]

Filter Arrays

Select elements that meet a condition:

filter([5, 15, 8, 20, 3], fn(x): x > 10;)

Example output: [15, 20]

Fold Arrays

Combine array elements into a single value:

fold([1, 2, 3, 4], 0, fn(acc, x): acc + x;)

Example output: 10

File Processing

CSV to Markdown Table

Convert CSV data to a formatted markdown table:

$ mq 'csv::csv_to_markdown_table()' example.csv

Use case: Convert spreadsheet data to markdown format for documentation. The csv_parse(true) treats the first row as headers.

Input example (example.csv):

Name,Age,City
Alice,30,NYC
Bob,25,LA

Example output:

| Name  | Age | City |
| ----- | --- | ---- |
| Alice | 30  | NYC  |
| Bob   | 25  | LA   |

Merge Multiple Files

Combine multiple markdown files with file path separators:

$ mq -S 's"\n${__FILE__}\n"' 'identity()' docs/books/**/**.md

The -S flag adds a separator between files, and __FILE__ is a special variable containing the current file path.

Example output:

docs/intro.md

# Introduction
...

docs/usage.md

# Usage
...

Process Files in Parallel

Process large numbers of files efficiently:

$ mq -P 5 '.h1' docs/**/*.md

LLM Workflows

Extract Context for LLM Prompts

Extract specific sections to create focused context for LLM inputs:

select(.h || .code) | self[:10]

Example: Extract first 10 sections with headings or code for a code review prompt.

Document Statistics

$ mq -A 'let headers = count_by(fn(x): x | select(.h);)
| let paragraphs = count_by(fn(x): x | select(.text);)
| let code_blocks = count_by(fn(x): x | select(.code);)
| let links = count_by(fn(x): x | select(.link);)
| s"Headers: ${headers}, Paragraphs: ${paragraphs}, Code: ${code_blocks}, Links: ${links}"'' docs/books/**/**.md

Generate Documentation Index

.h
| let level = .h.level
| let text = to_text(self)
| let indent = repeat("  ", level - 1)
| let anchor = downcase(replace(text, " ", "-"))
| if (!is_empty(text)): s"${indent}- [${text}](#${anchor})"

Frontmatter Operations

Extract frontmatter metadata from markdown files:

.yaml | frontmatter()

Modules

Standard Library

Standard modules are built into mq — use them with include or import, no installation needed.

ModuleDescription
jsonJSON parser and formatter
yamlYAML 1.2 parser and formatter
tomlTOML parser and formatter
xmlXML parser and formatter
htmlHTML parser and formatter (requires the css-selector build feature)
csvCSV / TSV parser and formatter
cborCBOR binary format support
semverSemantic versioning (SemVer) utilities
sectionMarkdown section extraction helpers
tableTable rendering utilities
fuzzyFuzzy string matching
toonTOON format support
testTesting framework (assert_eq, assert_true, …)

Extension Modules

These modules extend mq with additional parsers, utilities, and domain-specific languages. All modules support HTTP Import — no local installation required.

import "github.com/harehare/<module-name>"

Type to search by name or description, or click a category to filter.

ModuleCategoryDescription
json5.mqFormat ParsersJSON5 — comments, trailing commas, unquoted keys
hcl.mqFormat ParsersHCL (HashiCorp Configuration Language) — blocks, labels, attributes
pkl.mqFormat ParsersPKL — Apple's configuration language, with type annotations and collection literals
kdl.mqFormat ParsersKDL document language
ini.mqFormat ParsersINI file parser and serializer
ndjson.mqFormat ParsersNDJSON / JSON Lines
logfmt.mqFormat Parserslogfmt structured log lines (key=value)
cron.mqFormat ParsersCron expression parser and human-readable descriptions
jwt.mqFormat ParsersJWT decoder — inspect header and payload without verification
okf.mqFormat ParsersOKF (Open Knowledge Format) reader/writer — concept documents, cross-links, citations, log/index entries
url.mqFormat ParsersURL parsing, building, and relative-resolution utilities for mq.
changelog.mqFormat ParsersKeep a Changelog Markdown parser and serializer
dotenv.mqFormat Parsers.env file parser and serializer — quotes, comments, and escape sequences
jsonpath.mqFormat ParsersJSONPath (RFC 9535-style) query engine for mq's parsed JSON values
jsonschema.mqFormat ParsersJSON Schema validator
xpath.mqFormat ParsersAbbreviated XPath query engine for xml.mq's parsed value tree
codeowners.mqFormat ParsersGitHub CODEOWNERS parser and matcher
feed.mqFormat ParsersRSS 2.0 / Atom feed parser
gitignore.mqFormat Parsers.gitignore pattern parser and matcher
asciidoc.mqFormat ParsersAsciiDoc to Markdown converter
jsonld.mqFormat ParsersJSON-LD <script type="application/ld+json"> extractor
typst.mqFormat ParsersTypst markup parser — headings and single-line function-call statements
gomod.mqFormat ParsersTypst A Go module manifest (go.mod/go.sum) parser implemented as an mq module
sbom.mqFormat ParsersSPDX and CycloneDX SBOM (Software Bill of Materials) parser
fixedwidth.mqFormat ParsersFixed-width (fixed-length) text parser
mermaid.mqDiagram & GraphMermaid diagrams — flowchart, sequence, pie, class
dot.mqDiagram & GraphGraphviz DOT — nodes, edges, attributes
graphql.mqDiagram & GraphGraphQL SDL — types, enums, interfaces, unions
tree.mqDiagram & GraphA tree-rendering utility module for mq
dockerfile.mqDevOps & InfrastructureDockerfile instruction parser
k8s.mqDevOps & InfrastructureKubernetes manifest parser — metadata, containers, images, ports, resources
gha.mqDevOps & InfrastructureGitHub Actions workflow parser — jobs, steps, triggers, matrix
openapi.mqDevOps & InfrastructureOpenAPI 3.x spec parser — paths, operations, schemas, security schemes
aws.mqDevOps & InfrastructureAWS CLI / SDK JSON response processor — filter, extract, and render Markdown tables for EC2, S3, IAM, Lambda, RDS, ECS, EKS, and 50+ other services
ansi.mqTerminal & TextANSI terminal escape code utilities
case.mqTerminal & TextString case conversion utilities implemented as an mq module
emoji.mqTerminal & TextGitHub-style emoji shortcode <-> Unicode emoji conversion
qrcode.mqGeneratorsQR Code (ISO/IEC 18004) encoder — ASCII-art and SVG rendering
badge.mqGeneratorsshields.io badge generator
sparkline.mqGeneratorsA pure Unicode sparkline renderer
lisp.mqInterpreters & ExamplesScheme-like Lisp interpreter
bf.mqInterpreters & ExamplesBrainfuck interpreter
jq.mqInterpreters & ExamplesImplementation of the jq JSON processor, written in mq
parser_combinator.mqLibraries & ToolkitsA small parser-combinator toolkit, in the spirit of Rust's nom
diff.mqLibraries & ToolkitsText and array diffing utilities, built on mq's native Myers-diff engine
template.mqLibraries & ToolkitsA lightweight Mustache/Handlebars-style templating engine
returns.mqLibraries & ToolkitsResult and Maybe types for railway-oriented error handling, inspired by dry-python/returns

Cookbook

The Cookbook is a collection of task-first recipes for mq. Where the Example page walks through mq’s features, each Cookbook page starts from a real problem (“I want to do X”) and gets straight to a working query.

How a recipe is structured

Every recipe follows the same four parts:

  • Goal: the problem being solved, in one sentence.
  • Prerequisites: anything the input or environment needs (a module import, an mq flag, a document shape).
  • Query: the mq query or command to run.
  • Output: the result you should see.

Selecting and filtering content

Working with sections

Working with tables

Functions and data

Fetching over the network

Multi-file and LLM workflows

Looking for a full tour of mq’s selectors and built-in functions instead? See the Example page.

Generate a table of contents from headings

Goal: Turn every heading in a document into a nested, linked table of contents.

Prerequisites: None. Works on any document containing headings.

Query

.h
| let text = to_text()
| let anchor = downcase(replace(text, " ", "-"))
| let link = to_link("#" + anchor, text, "")
| let level = .h.depth
| if (!is_none(level)): to_md_list(link, level - 1)
$ mq '.h | let text = to_text() | let anchor = downcase(replace(text, " ", "-")) | let link = to_link("#" + anchor, text, "") | let level = .h.depth | if (!is_none(level)): to_md_list(link, level - 1)' README.md

Input

# Introduction
## Getting Started
### Installation
## Usage

Output

- [Introduction](#introduction)
  - [Getting Started](#getting-started)
    - [Installation](#installation)
  - [Usage](#usage)

Notes

  • .h.depth gives the heading level (1 for #, 2 for ##, …); it’s used here to control the list’s indentation via to_md_list(item, level).
  • Gotcha: to_link(url, to_text(), "") without lowercasing/hyphenating first produces broken anchors for multi-word headings: #Getting Started (space and all) instead of #getting-started, which mq even wraps in angle brackets (<#Getting Started>) since it isn’t a valid bare link target. Always slugify (downcase + replace(" ", "-")) before building the anchor.
  • This assumes GitHub-style slugs. Headings with punctuation or non-ASCII text need a more thorough slugifier than a single replace.

Extract code blocks by language

Goal: Pull out only the code blocks written in a specific language, e.g. to review all the JavaScript snippets in a doc.

Prerequisites: None.

Query

select(.code.lang == "js")
$ mq 'select(.code.lang == "js")' README.md

Input

```js
const x = 1;
```

```python
x = 1
```

```js
const y = 2;
```

Output

Returns only the two JavaScript code blocks (const x = 1; and const y = 2;), dropping the Python one.

Notes

  • Shorthand: .code("js") selects the same nodes as select(.code.lang == "js"). The language selector doubles as a filter when called with an argument.
  • To see which languages appear in a document at all, use .code.lang on its own. It prints one language per code block, in document order, with duplicates (js, python, js, …), not a deduplicated list. Pipe through -A ... | unique_by(fn(x): x;) if you need the distinct set.
  • To strip code blocks out and keep only prose, invert the condition: select(!.code).

Extract a specific row from a table

Goal: Pull a single row out of a Markdown table by its position.

Prerequisites: None.

Query

.[2][]
$ mq '.[2][]' README.md

Input

| Name  | Age | City |
| ----- | --- | ---- |
| Alice | 30  | NYC  |
| Bob   | 25  | LA   |

Output

| Bob | 25 | LA |

Notes

  • Row indexing starts at the header: index 0 is the header row, 1 is the first data row (Alice), 2 is the second (Bob).
  • To get the cells as plain text instead of a rendered row, pipe through to_text: mq '.[2][] | to_text' README.md prints Bob, 25, LA on separate lines. -F json dumps the full node structure (position info and all), not just the values, so it’s rarely what you want here.
  • Need the row as a keyed record instead ({"Name": "Bob", "Age": "25", "City": "LA"})? See Extract all tables from a document and use table::to_array, which returns rows in that shape without the header-offset indexing above.

Get the Nth item from every list

Goal: Pull out, say, the second item of every list in a document, useful for sanity-checking that parallel lists stay in sync.

Prerequisites: None.

Query

.[1]
$ mq '.[1]' README.md

Input

- A1
- A2
- A3

1. B1
2. B2

Output

- A2
2. B2

Notes

  • Indexing is 0-based and applies independently to each list node in the document. .[1] here returns the second bullet (A2) from the first list and the second ordered item (B2) from the second list.
  • A list shorter than the requested index is skipped rather than erroring: .[2] against the input above would return only - A3, since the ordered list has no third item.

Extract MDX components

Goal: Pull out every JSX-like component from an MDX document, e.g. to audit which components a doc site actually uses.

Prerequisites: Parse the file as MDX with -I mdx (plain .md parsing does not recognize JSX syntax).

Query

select(is_mdx())
$ mq -I mdx 'select(is_mdx())' page.mdx

Input

Regular paragraph.

<CustomComponent prop="value" />

Another paragraph.

<AnotherComponent>
  Content
</AnotherComponent>

Output

<CustomComponent prop="value" />
<AnotherComponent>Content</AnotherComponent>

Notes

  • Files with an .mdx extension are parsed as MDX automatically; -I mdx is only needed when the content doesn’t have that extension (e.g. piped from stdin).

  • .name gives just the tag, e.g. CustomComponent. Combined with unique_by, this gives the distinct set of components a doc actually uses:

    $ mq -A -I mdx 'nodes | filter(fn(n): is_mdx(n);) | map(fn(n): n.name;) | unique_by(fn(x): x;)' page.mdx
    

Extract all URLs from links

Goal: Collect every link target in a document, handy for a broken-link checker or a quick inventory of external references.

Prerequisites: None.

Query

.link.url
$ mq '.link.url' README.md

Input

Check out [mq](https://mqlang.org) and [GitHub](https://github.com).

Output

https://mqlang.org
https://github.com

Notes

  • Want the link’s visible text instead? Use .link.value in place of .link.url. (.link.title is the separate, optional "title" attribute from [text](url "title") syntax, usually empty.)
  • Default output is already one URL per line, so pipe it straight into xargs -I{} curl -sfI {} to check for broken links.

Find images missing alt text

Goal: Spot images with no alt text, a quick accessibility check before publishing.

Prerequisites: None.

Query

select(.image.alt == "")
$ mq 'select(.image.alt == "")' README.md

Just the file paths, for a punch list:

$ mq 'select(.image.alt == "") | .image.url' README.md

Input

![A cute cat](cat.png)

![](missing-alt.png)

![Team photo](team.jpg)

Output

![](missing-alt.png)

Notes

  • .image.alt on its own skips images with empty alt text (selectors drop falsy matches), so it’s only useful for listing the alt text that does exist. To find the gaps, filter explicitly with select(.image.alt == "").
  • Gotcha: a non-matching node’s result is None after select, and None results are what actually get suppressed from output, not “filtering” in a control-flow sense. Plain field access on that None (like .image.url above) stays None and stays suppressed. But routing it through something that produces a real value even for None input, such as string concatenation (__FILE__ + ": " + .image.url) or a function like is_none(self), “launders” it into a non-None result, and it prints for every node, defeating the filter. Keep transformations after select limited to plain field/selector access, or restructure so the select is the last step.

Extract blockquotes as pull quotes

Goal: Pull every blockquote out of an article as plain text, handy for picking pull quotes to share on social media, or for a “quotes from this post” summary.

Prerequisites: None.

Query

.blockquote | to_text
$ mq '.blockquote | to_text' post.md

Input

# Article

Some intro text.

> This is a great pull quote worth sharing.

More text here.

> Another quote.
> Spanning two lines.

Output

This is a great pull quote worth sharing.
Another quote.
Spanning two lines.

Notes

  • Drop | to_text to keep the > Markdown syntax instead of plain text.
  • A blockquote spanning multiple lines (like the second one above) comes out as one multi-line result, not split into separate quotes. Group by blank lines in the source if you need them separated.
  • Gotcha: a nested blockquote (> > text) gets its text glued to the outer quote’s with no space or newline between them, e.g. Outer quote.Nested quote.. Check for >>-style input before relying on this for anything more than flat blockquotes.

Extract footnote definitions

Goal: Collect every footnote definition in a document, e.g. to pull out a paper’s citations as a standalone reference list.

Prerequisites: None.

Query

.footnote
$ mq '.footnote' paper.md

Input

# Doc

Here is a claim[^1] and another[^2].

[^1]: First source.
[^2]: Second source.

Output

[^1]: First source.
[^2]: Second source.

Notes

  • This selects the footnote definitions (the [^1]: ... lines), not the inline reference marks ([^1]) in the body text.
  • Pipe through to_text to strip the [^n]: marker and get just the citation text.
  • Works the same for named labels ([^note]: ...), not just numeric ones.

Find raw HTML blocks

Goal: Locate embedded raw HTML in a Markdown document, useful before converting to a stricter Markdown flavor (e.g. plain CommonMark) or a renderer that doesn’t allow raw HTML passthrough.

Prerequisites: None.

Query

.html
$ mq '.html' README.md

Input

# Doc

Some text.

<div class="callout">
  <strong>Note:</strong> important info.
</div>

More text.

Output

<div class="callout">
  <strong>Note:</strong> important info.
</div>

Notes

  • Run across a whole docs tree (mq '.html' docs/**/*.md) as a quick audit of how much raw HTML you’d need to rewrite before switching renderers.
  • Gotcha: inline HTML tags inside a paragraph (e.g. text <span>inline</span> text) are matched too, but the opening and closing tags come back as two separate .html nodes (<span class="x"> and </span>), and the text between them is dropped entirely, since that’s a plain text node this selector doesn’t touch. .html is reliable for auditing standalone HTML blocks; for inline HTML mixed into prose, treat the count as a rough signal rather than a clean extraction.

Extract a section by its heading

Goal: Pull just one section (say, the “Installation” section of a README) out of a larger document, heading included.

Prerequisites: The section module. Section functions need all document nodes at once, not one node at a time, so pass -A on the command line or pipe through nodes in a script.

Query

-A flag (command line):

$ mq -A 'section::section("Installation")' README.md

import + nodes (inline query or script):

import "section"
| nodes
| section::section("Installation")

include (no namespace prefix):

include "section"
| nodes
| section("Installation")

Input

# Introduction

Welcome to the project.

## Installation

Run the following command.

## Usage

Use the tool like this.

Output

## Installation

Run the following command.

Notes

  • Section objects are automatically expanded back to Markdown nodes in CLI output, so section::collect isn’t needed there. If you forget -A/nodes and call section::* on a single node, mq prints a warning on stderr and treats that node as a one-element array instead of silently giving you a meaningless result.

  • Calling the section module from Rust or other code (not the CLI)? Section objects are plain dicts there and need an explicit section::collect to turn back into Markdown:

    import "section"
    | nodes
    | section::section("Installation")
    | section::collect
    
  • Want the body without the ## heading itself? Add | section::bodies | first.

  • Need every section instead of one by name? Use section::sections, optionally filtered with section::by_level(2).

Keep only sections at a given heading level

Goal: Drop deeper subsections and keep just the top-level (or any one level) sections of a document.

Prerequisites: The section module. Section functions need all document nodes at once, so pass -A on the command line, or nodes in a script.

Query

$ mq -A 'section::sections | section::by_level(1)' README.md

by_level also accepts a range:

$ mq -A 'section::sections | section::by_level(1..2)' README.md

Input

# Chapter 1

Intro.

## Section 1.1

Detail.

# Chapter 2

Content.

Output (by_level(1))

# Chapter 1

Intro.

# Chapter 2

Content.

Notes

  • by_level(2) on the same input would return ## Section 1.1 only, since Chapter 1 and Chapter 2 are level 1.

Split a document into chunks at a heading level

Goal: Break a long document into standalone chunks wherever a heading of a given level appears, useful for feeding sections to an LLM one at a time, or generating one output file per chunk downstream.

Prerequisites: The section module, via -A or nodes.

Query

$ mq -A 'section::split(2)' README.md

Input

# Chapter 1

Intro.

## Section 1.1

Detail 1.1

## Section 1.2

Detail 1.2

# Chapter 2

Content.

## Section 2.1

Detail 2.1

Output

## Section 1.1

Detail 1.1

## Section 1.2

Detail 1.2

# Chapter 2

Content.

## Section 2.1

Detail 2.1

Notes

  • Gotcha: content before the first heading at the split level is dropped: here, # Chapter 1 and Intro. disappear because they precede the first ##. Once the first split point is found, every following heading (at or above that level) starts a new chunk normally, which is why # Chapter 2 survives. If you need that leading content preserved, prepend it to the output yourself, or split at level 1 instead.
  • split(1) on the same input is a no-op here, since every section is already anchored under a level-1 heading.
  • section::collect (flattening section objects back to plain Markdown nodes) isn’t needed for CLI output. mq expands section objects to Markdown automatically there. It’s only required when consuming section objects from Rust or other embedding code; see the equivalent note in Extract a section by its heading.

Delete a section by its heading

Goal: Drop a section you no longer want (e.g. remove a “Deprecated” section before publishing docs) while leaving the rest of the document intact.

Prerequisites: The section module, via -A or nodes.

Query

$ mq -A 'section::filter_sections(fn(s): section::title(s) != "Deprecated";)' README.md

Or with nodes:

import "section"
| nodes
| section::filter_sections(fn(s): section::title(s) != "Deprecated";)

Input

# Introduction

Welcome to the project.

## Installation

Run the following command.

## Deprecated

Do not use this anymore.

Output

# Introduction

Welcome to the project.

## Installation

Run the following command.

Notes

  • filter_sections keeps a section when the predicate returns true, so the same pattern works for any condition, e.g. matching against a list of titles to drop, instead of a single != check.
  • section::collect (flattening section objects back to plain Markdown nodes) isn’t needed here. mq expands section objects to Markdown automatically in query output. It’s only required when consuming section objects from Rust or other embedding code; see the equivalent note in Extract a section by its heading.

Find sections that have no content

Goal: Spot placeholder or empty sections, headings with nothing written under them yet, as a quick doc-completeness check.

Prerequisites: The section module, via -A or nodes.

Query

$ mq -A 'section::sections | filter(fn(s): !section::has_body(s);) | section::titles' README.md

Input

# Introduction

Welcome to the project.

## Empty Section

## Usage

Use the tool like this.

Output

Empty Section

Notes

  • Flip the predicate (drop the !) to list sections with content instead, e.g. as a sanity check that nothing got filtered out by mistake.
  • “No content” means no non-heading nodes directly under it. A subsection’s own text doesn’t count as its parent’s content: a heading followed only by a deeper heading (# Parent then ## Child with text under Child) still flags Parent as empty.

Extract all tables from a document

Goal: Get every table in a document as structured table objects, ready for further transformation (add a row, convert to CSV, reshape, …).

Prerequisites: The table module, via -A or nodes. Unlike the section module, import "table" must be written explicitly. It isn’t auto-imported.

Query

$ mq -A 'import "table" | table::tables' README.md

Or with nodes:

import "table"
| nodes
| table::tables

Input

| Name  | Age |
| ----- | --- |
| Alice | 30  |

Output

| Name  | Age |
| ----- | --- |
| Alice | 30  |

Notes

  • Table objects print back as Markdown automatically in CLI output, so table::to_markdown isn’t needed here. It is needed when using the table module from Rust or other embedding code, where table objects stay plain dicts, and since table::tables returns an array (there can be more than one table), narrow to a single table first: table::tables | first | table::to_markdown.
  • Chain | first after table::tables when you only want the first table, as the other table recipes in this Cookbook do.

Add a row to a table

Goal: Append a new row of data to an existing Markdown table.

Prerequisites: The table module, via -A or nodes.

Query

$ mq -A 'import "table" | table::tables | first | table::add_row(["Charlie", "35"])' README.md

Input

| Name  | Age |
| ----- | --- |
| Alice | 30  |

Output

| Name    | Age |
| ------- | --- |
| Alice   | 30  |
| Charlie | 35  |

Notes

  • add_row takes a plain array of cell values, in column order. It will error if the array’s length doesn’t match the table’s column count.
  • Column widths are re-computed to fit the new data, which is why Alice gets re-padded alongside Charlie.
  • No table::to_markdown call needed here. mq expands table objects to Markdown automatically in query output. See Extract all tables from a document for when to_markdown actually is required.

Convert a Markdown table to CSV

Goal: Turn a table in a doc into CSV, e.g. to pull it into a spreadsheet.

Prerequisites: The table module, via -A or nodes.

Query

$ mq -A 'import "table" | table::tables | first | table::to_csv' README.md

Input

| Name  | Age |
| ----- | --- |
| Alice | 30  |

Output

Name,Age
Alice,30

Using a different delimiter

to_csv takes an optional delimiter, so the same function produces TSV or PSV output too:

$ mq -A 'import "table" | table::tables | first | table::to_csv(self, "\t")' README.md
Name	Age
Alice	30

Notes

  • Fields containing the delimiter, a quote, or a newline are quoted and escaped per RFC 4180 automatically. A cell holding "Hi, there" becomes """Hi, there""" in the output, no manual escaping needed.
  • A document with more than one table? table::tables returns all of them as an array; pick the one you want with first, or by index with (table::tables())[1] for the second table.
  • Want structured data instead of a CSV string, e.g. to feed into join_by or another array builtin? Use table::to_array instead, which returns an array of dicts keyed by header text.
  • Going the other direction? See Convert CSV to a Markdown table.

Convert CSV to a Markdown table

Goal: Turn a .csv file into a formatted Markdown table for documentation.

Prerequisites: None, .csv files are parsed automatically.

Query

$ mq 'csv::csv_to_markdown_table' example.csv

Input (example.csv)

Name,Age,City
Alice,30,NYC
Bob,25,LA

Output

| Name | Age | City |
| --- | --- | --- |
| Alice | 30 | NYC |
| Bob | 25 | LA |

Notes

  • Columns aren’t padded to equal width. That’s still valid Markdown, and most renderers display it identically to a hand-aligned table.
  • csv_to_markdown_table also accepts data already built in a query, either an array of dicts ([{"name": "Alice", "age": 30}], keys become headers) or an array of arrays with the header as the first row ([["name", "age"], ["Alice", 30]]).
  • Quoted fields in the source CSV, including ones containing the delimiter, are unquoted correctly before being placed in the table.
  • Going the other direction? See Convert a Markdown table to CSV.

Reshape a table between wide and long form

Goal: Pivot a table from one-column-per-category (wide) to one-row-per-category (long), or back. The same reshape spreadsheet tools call “unpivot”/“pivot”.

Prerequisites: The table module, via -A or nodes.

Wide to long (pivot_longer)

Unpivot a set of columns into name/value row pairs, keeping the remaining columns as identifiers. value_columns is an array of column indices; names_to and values_to (both optional) name the two new columns.

$ mq -A 'import "table" | let t = first(table::tables()) | table::pivot_longer(t, [1, 2, 3], "quarter", "score")' README.md

Input:

| Name  | Q1 | Q2 | Q3 |
| ----- | -- | -- | -- |
| Alice | 10 | 20 | 30 |
| Bob   | 5  | 15 | 25 |

Output:

| Name  | quarter | score |
| ----- | ------- | ----- |
| Alice | Q1      | 10    |
| Alice | Q2      | 20    |
| Alice | Q3      | 30    |
| Bob   | Q1      | 5     |
| Bob   | Q2      | 15    |
| Bob   | Q3      | 25    |

Long to wide (pivot_wider)

The inverse: spread a key/value column pair back out into one column per distinct key, grouping rows by the remaining identifier columns. names_from is the column index whose distinct values become new headers; values_from supplies the values. Combinations missing from the input become empty cells.

$ mq -A 'import "table" | table::tables | first | table::pivot_wider(1, 2)' README.md

Feeding the long output above back in with pivot_wider(1, 2) reproduces the original wide table.

Notes

  • pivot_longer and pivot_wider are exact inverses of each other for well-formed input, so use whichever direction matches the shape you’re starting from.
  • No table::to_markdown call needed here. mq expands table objects to Markdown automatically in query output. See Extract all tables from a document for when to_markdown actually is required.

Write a reusable custom function

Goal: Package a multi-step transformation (like converting snake_case to CamelCase) into a named function you can call like a builtin.

Prerequisites: None.

Query

def snake_to_camel(x):
  let words = split(x, "_")
  | foreach (word, words):
      let first_char = upcase(first(word))
      | let rest_str = downcase(slice(word, 1, len(word)))
      | s"${first_char}${rest_str}";
  | join("")
end
| snake_to_camel("hello_world")
$ mq -I null 'def snake_to_camel(x): ... end | snake_to_camel("hello_world")'

Output

HelloWorld

Notes

  • def ... end must appear before its first use in the pipeline.
  • Reuse the same function across many invocations: put it in a .mq module file and load it with -M path/to/module (path without the .mq extension, function used unqualified), or put the file on a search path with -L dir and import "module" (used as module::fn(...)).

Transform, filter, and reduce arrays

Goal: Apply the usual map/filter/fold trio to arrays inside an mq query, useful once a query has collected values into a list and you need to post-process them.

Prerequisites: None.

Map: transform each element

$ mq -I null 'map([1, 2, 3, 4, 5], fn(x): x + 1;)'
[2, 3, 4, 5, 6]

Filter: keep elements matching a condition

$ mq -I null 'filter([5, 15, 8, 20, 3], fn(x): x > 10;)'
[15, 20]

Fold: combine elements into a single value

$ mq -I null 'fold([1, 2, 3, 4], 0, fn(acc, x): acc + x;)'
10

Notes

  • The array can also come from the pipe instead of being passed explicitly, and the three chain together: [5, 15, 8, 20, 3] | filter(fn(x): x > 10;) | fold(0, fn(acc, x): acc + x;) gives 35.
  • These compose naturally with Markdown selectors, e.g. .h.depth | filter(fn(x): x <= 2;) to keep only the depths of h1/h2 headings collected across a document (with -A).

Extract frontmatter metadata

Goal: Pull a document’s YAML frontmatter out as structured data, e.g. to read title/tags for a static-site index.

Prerequisites: None.

Query

.yaml | frontmatter
$ mq '.yaml | frontmatter' post.md

Input

---
title: Hello
tags: [a, b]
---

# Body

Output

{"title": "Hello", "tags": ["a", "b"]}

Notes

  • Grab a single field by chaining get("title"), e.g. .yaml | frontmatter | get("title"). A plain .title selector won’t work, since that’s a Markdown-node selector, not a dict-key accessor. Bracket access also works (frontmatter()["title"]), but only with the parentheses kept. frontmatter["title"] (no parens) tries to index the function itself and errors.
  • Gotcha: -F json/-F csv don’t give a clean metadata index here. Since the result is coming from a Markdown document, mq wraps the dict in a synthetic node before serializing it (e.g. -F json gives {"type": "Text", "value": "{\"title\": ...}"} instead of the dict’s fields directly). Default output doesn’t have this problem: mq '.yaml | frontmatter' docs/**/*.md prints one JSON object per file, one per line, which you can pipe into jq -s to build an array, or process as JSON Lines directly.

Bump a version string (or any text) across a file

Goal: Replace every occurrence of a string (like a version number) throughout a document, without losing the parts of the document that don’t match.

Prerequisites: The -U flag.

Query

$ mq -U 'select(contains("1.2.0")) | replace("1.2.0", "1.3.0")' CHANGELOG.md

Input

# My Project v1.2.0

Install version 1.2.0 to get started.

See the changelog for details.

Output

# My Project v1.3.0

Install version 1.3.0 to get started.

See the changelog for details.

Notes

  • Without -U, mq prints only the nodes that matched select(...): here, the “See the changelog for details.” paragraph would silently disappear from the output, since it never matched the contains("1.2.0") filter.
  • With -U, mq prints the whole document back out, with only the matched-and-transformed nodes changed. This is the mode you want whenever the query’s job is “edit part of this file,” not “extract part of this file.”
  • -U writes to stdout, not the file itself. To edit a file on disk, redirect to a temp file and move it back: mq -U '...' file.md > file.md.tmp && mv file.md.tmp file.md.

Previewing changes first

Before piping -U output into a file, preview what would change with --diff, which prints a unified diff instead of the full content:

$ mq -U --diff 'select(contains("1.2.0")) | replace("1.2.0", "1.3.0")' CHANGELOG.md
--- CHANGELOG.md
+++ CHANGELOG.md
@@ -1,4 +1,4 @@
-# My Project v1.2.0
+# My Project v1.3.0

-Install version 1.2.0 to get started.
+Install version 1.3.0 to get started.

 See the changelog for details.

--diff never writes to the file, just as -U never has on its own. It exits with code 1 if anything would change (0 otherwise), which is handy for a CI check. With multiple files, each gets its own diff headed by its path.

Track task-list (checkbox) progress

Goal: Count how many - [x] items are checked off out of the total in a Markdown task list, e.g. to report progress on a TODO list or project checklist.

Prerequisites: -A, since the counts are computed across the whole document at once.

Query

List only the completed items:

$ mq 'select(.list.checked == true)' TODO.md

Count done vs. total:

$ mq -A 'let total = count_by(fn(x): x | select(.list);)
| let done = count_by(fn(x): x | select(.list.checked == true);)
| s"${done}/${total} done"' TODO.md

Input

# TODO

- [x] Write docs
- [ ] Add tests
- [x] Fix bug
- [ ] Ship release

Output

2/4 done

Notes

  • Shorthand: .done and .todo select checked/unchecked task-list items directly: .done is equivalent to select(.list.checked == true), and .todo to select(.list.checked == false). The counting query can use select(.done) in place of select(.list.checked == true) the same way.
  • .list.checked is true/false for task-list items and absent (None) for a plain bullet or numbered item, so select(.list) alone counts every list item regardless of whether it’s a checkbox.
  • Swap == true for == false (or .done for .todo) to list what’s left to do instead of what’s done.

Count words in a document

Goal: Get a total word count for a Markdown file, e.g. to estimate reading time for a blog post.

Prerequisites: -A, since the total is a sum across every node.

Query

$ mq -A 'nodes
| map(fn(n): to_text(n) | split(" ") | len;)
| fold(0, fn(acc, x): acc + x;)' post.md

Input

# Title

This is a short paragraph with some words in it.

## Section

Another paragraph here, with more words to count for the estimate.

Output

23

Notes

  • Gotcha: calling to_text(self) directly on the whole -A array (instead of per-node inside map) does not give clean plain text. It keeps heading markers like #/## and joins nodes with commas. Convert each node to text individually with to_text(n) inside map, then sum the per-node word counts, as above.
  • This is a rough count (splitting on single spaces), not a linguistically precise word count, good enough for a reading-time estimate, not for billing by the word.

Inline local images as base64

Goal: Make a document self-contained by embedding its local image files directly into it as data: URIs, handy for pasting a doc somewhere that can’t resolve relative image paths (a Slack message, a single-file export).

Prerequisites: The --allow-read flag, since this reads image files from disk. Only local images are touched; URLs with a scheme (https://, …) or already-data: URIs are left alone.

Query

$ mq --allow-read=. 'select(.image) | embed_images(., ".")' doc.md

Input

![Logo](logo.png)

![External](https://example.com/pic.png)

Output

![Logo](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQAY3Y2wAAAAAElFTkSuQmCC)

![External](https://example.com/pic.png)

Notes

  • Unlike most mq functions, embed_images takes the node as an explicit first argument, not implicitly through the pipe: embed_images(., base_dir), not embed_images(base_dir) alone. Calling it as embed_images(base_dir) silently returns base_dir itself instead of erroring, which is easy to miss.
  • The base directory (second argument, default ".") is where relative image paths like logo.png are resolved from. It’s usually the directory containing the Markdown file, not the current working directory, if they differ.
  • Combine with -U to keep the rest of the document (non-image nodes) intact in the output.

Merge multiple Markdown files into one stream

Goal: Concatenate several files, with the source path visible before each one, to review a set of docs in one pass.

Prerequisites: None.

Query

$ mq -S 's"\n${__FILE__}\n"' 'identity' docs/**/*.md

Input

docs/intro.md:

# Introduction
Welcome.

docs/usage.md:

# Usage
Use it like this.

Output


docs/intro.md
# Introduction
Welcome.

docs/usage.md
# Usage
Use it like this.

Notes

  • -S <query> inserts the result of <query> as a separator between files; __FILE__ is a built-in variable holding the current file’s path.
  • identity passes each node through unchanged, swap it for a real query to filter/transform while merging.
  • This runs the query once per file and stitches the results together with -S. To instead run one query across all files at once, e.g. to count something over the whole set, use --eval-all. It has no per-file separator, so it’s for aggregation, not this kind of readable concatenation.

Control when large file sets run in parallel

Goal: Understand and tune mq’s automatic parallel processing for batches of many files, so a big run doesn’t stall on I/O one file at a time.

Prerequisites: None.

Query

By default, mq automatically switches to parallel processing once a run covers more than 10 files, no flag needed:

$ mq '.h1' docs/**/*.md

Use -P <n> to change that threshold, e.g. force parallel mode even for a small batch while testing:

$ mq -P 1 '.h1' docs/**/*.md

Or raise it, to keep small-to-medium batches sequential (useful if the query has ordering side effects):

$ mq -P 50 '.h1' docs/**/*.md

Notes

  • -P is a threshold (files to process before switching to parallel), not a worker count. It doesn’t limit how many files run concurrently.
  • Output order across files is not guaranteed once parallel processing kicks in. If order matters, raise the threshold above your file count, or sort downstream.

Generate an XML sitemap from Markdown files

Goal: Build a sitemap entry per file from a set of Markdown docs, based on each file’s path.

Prerequisites: None.

Query

$ mq 'def sitemap(item, base_url):
    let path = replace(to_text(item), ".md", ".html")
    | let loc = base_url + path
    | s"<url>
  <loc>${loc}</loc>
  <priority>1.0</priority>
  </url>"
end
| nodes
| first
| sitemap(__FILE__, "https://example.com/")' docs/**/*.md

Output

<url>
  <loc>https://example.com/docs/intro.html</loc>
  <priority>1.0</priority>
  </url>
<url>
  <loc>https://example.com/docs/usage.html</loc>
  <priority>1.0</priority>
  </url>

Notes

  • Gotcha: without a query that consumes all of a file’s nodes first, mq re-runs the whole pipeline once per node in the file. Since sitemap here ignores its piped input and only reads __FILE__, that means one duplicate <url> entry per node. nodes | first collapses each file down to a single evaluation before calling sitemap, so you get exactly one entry per file.

Trim a document down to LLM-sized context

Goal: Pull a bounded, high-signal slice of a document (headings and code, capped at N matches) to fit into an LLM prompt without dumping the whole file.

Prerequisites: -A (or nodes in a script), so the result can be collected into one array before capping its length.

Query

$ mq -A 'nodes | filter(fn(n): n | select(.h || .code) | !is_none();) | take(5)' README.md

Output

## Why mq?

## Features

## Installation

### Quick Install

```bash
curl -sSL https://mqlang.org/install.sh | bash

## Notes

- Gotcha: `select(.h || .code)` filters correctly when streaming per-node (mq's default, one query run per node), but *not* when applied directly to an already-collected array. Apply it inside a `filter(fn(n): n | select(...) | !is_none();)` lambda instead once you've called `nodes`.
- `take(n)` (implicitly `take(self, n)`) caps the result at the first `n` elements; `self[:n]` (array slicing, note this needs `self`, not `.`, before the brackets) does the same thing here and can be used interchangeably.
- Swap the predicate for whatever signal matters for the prompt, e.g. `n | select(.h || .link) | !is_none()` to extract structure plus references instead of code.

Generate document statistics

Goal: Get a quick count of headers, paragraphs, code blocks, and links in a document, a lightweight completeness/complexity check.

Prerequisites: -A, since the counts are computed across the whole document at once.

Query

$ mq -A 'let headers = count_by(fn(x): x | select(.h);)
| let paragraphs = count_by(fn(x): x | select(.text);)
| let code_blocks = count_by(fn(x): x | select(.code);)
| let links = count_by(fn(x): x | select(.link);)
| s"Headers: ${headers}, Paragraphs: ${paragraphs}, Code: ${code_blocks}, Links: ${links}"' README.md

Output

Headers: 15, Paragraphs: 48, Code: 7, Links: 18

(Numbers above are from running this query against mq’s own README.md; they’ll drift as the README changes, so treat them as illustrative, not exact.)

Notes

  • count_by(fn(x): x | select(...);) counts how many of the document’s nodes match the predicate. Swap the selector to count anything else (.code.lang == "js" for JS blocks specifically, etc.).

Fetch a web page and filter it

Goal: Pull a live web page straight into an mq pipeline and slice it down to just the parts you need (headings, links, …) — no separate curl + parsing script.

Prerequisites: The --allow-net flag, since outbound requests are disabled by default. --allow-net=DOMAIN restricts requests to just that domain (and any path under it); a bare --allow-net allows any host. Only https:// URLs are accepted — the client is SSRF-hardened (no automatic redirects, and loopback/private/link-local addresses are blocked even with --allow-net set). There’s no file to read here, so also pass -I null (null input); without it, mq has zero input nodes to run the pipeline against and the query never executes. Add -A too, so the query — and any http_get()/http() call inside it — runs exactly once instead of once per top-level input node.

Query

$ mq -I null -A --allow-net=mqlang.org 'http_get("https://mqlang.org") | from_html | .h(1..2)'

Output

# Query. Filter.

## What is mq?

## Why mq?

## Features

## Subcommands

## Try mq right now

Notes

  • from_html() converts the fetched HTML body to Markdown and parses it into the same kind of node array mq builds from a .md file, so any selector or function that works on a Markdown document works here too.
  • http_get(url, headers = {}) is a convenience wrapper for http(:get, url, headers). Matching wrappers exist for the other verbs (http_post, http_put, http_patch, http_delete, http_head), and http_all([{"url": ...}, ...]) fetches a batch of URLs concurrently for the same pattern applied to a list of pages.
  • Gotcha: chaining a second selector directly off an in-memory array like from_html()’s result doesn’t drop non-matching nodes — it leaves a None placeholder for each one instead, so .link.url produces a result padded with blanks. Use select(.link) | .url instead, which filters first and then projects; this differs from running mq on a real file, where each top-level node streams through the pipeline separately and non-matches are dropped automatically.
  • The headings above are whatever mqlang.org’s homepage has today — expect this exact output to drift as the site changes.
  • -A matters most once this pattern is adapted to run over real file input instead of -I null: mq normally evaluates the query once per top-level node, so a http_get()/http() call inside it would fire once per node in the document(s). -A aggregates all input into a single array first, so the query body — including any network calls — runs exactly once regardless of how many nodes or files are involved.

Reference

Reference

This is a reference documentation for the mq.

CLI

The mq command-line interface provides tools for querying and manipulating markdown content. Below is the complete reference for all available commands and options.

Usage: mq [OPTIONS] [QUERY OR FILE] [FILES]... [COMMAND]

Commands:
  repl        Start a REPL session for interactive query execution. Optional FILES are combined as the initial input (same file/format handling as `mq QUERY FILES...`)
  completion  Generate a shell completion script and print it to stdout
  help        Show documentation for a builtin function, selector, standard module, standard-module function, or the `examples` topic

Arguments:
  [QUERY OR FILE]  
  [FILES]...       

Options:
  -A, --aggregate
          Aggregate all input files/content into a single array
  -f, --from-file
          load filter from the file
  -I, --input-format <INPUT_FORMAT>
          Set input format [possible values: markdown, mdx, html, text, null, raw, bytes, cbor, csv, gron, json, psv, toml, toon, tsv, xml, yaml]
      --csv-delimiter <CHAR>
          Custom delimiter for `-I csv` input (a single ASCII character). Has no effect on `-I tsv`/`-I psv`, which use a fixed tab/pipe delimiter by design; pass `-I csv` with this flag instead if you need a different delimiter (e.g. `;`)
      --no-header
          Treat csv/tsv/psv input as headerless: each row becomes an array of values instead of a dict keyed by header names. Applies to `-I csv`, `-I tsv`, and `-I psv`
  -L, --directory <MODULE_DIRECTORIES>
          Search modules from the directory
  -M, --module-names <MODULE_NAMES>
          Load additional modules from specified files
  -m, --import-module-names <IMPORT_MODULE_NAMES>
          Import modules by name, making them available as `name::fn()` in queries
      --args <NAME> <VALUE>
          Sets a named string argument. NAME is accessible directly in queries, and also via ARGS."named" when --args or --argv is given
      --argjson <NAME> <JSON_VALUE>
          Sets a named JSON argument. NAME is accessible directly in queries
      --rawfile <NAME> <FILE>
          Sets file contents that can be referenced at runtime
      --slurpfile <NAME> <FILE>
          Sets a named argument from a JSON file. NAME is bound to an array of every JSON value found in FILE (jq --slurpfile compatible), so a file containing a single JSON value becomes a one-element array
      --stream
          Enable streaming mode for processing large files line by line
      --watch
          Watch the input file(s) for changes and automatically re-run the query whenever they change. Requires at least one input file (stdin cannot be watched). With --from-file, the query file is watched too. Runs until interrupted (Ctrl-C); a query error is printed to stderr and watching continues rather than exiting
      --eval-all
          Evaluate the query once against all input files combined (like yq's `eval-all`), instead of once per file. Enables cross-file aggregation in a single query
      --allow-http-import
          Allow `import`/`include` to fetch modules over HTTP(S). Disabled by default
      --allowed-domain <ALLOWED_DOMAINS>
          Allow HTTP imports from additional domain(s) beyond the default. Has no effect unless `--allow-http-import` (or `--allow-all`) is also passed. Use `github.com/{user}/{repo}` to allow a specific repository (expanded automatically), or a plain domain like `example.com` to allow any path under that host. Repeat to allow multiple extra domains
      --refresh-modules
          Force re-fetch of mutable-ref (HEAD/branch) HTTP-imported modules, ignoring the local cache. Versioned (tagged) modules are never re-fetched regardless of this flag
      --clear-cache
          Remove all HTTP module cache including versioned (tagged) modules and lock files. Use this to fully reset the cache when something goes wrong
      --no-lockfile
          Disable the mq.lock integrity check for HTTP imports. By default a fetched URL's content is checked against mq.lock, and a mismatch is rejected unless --refresh-modules is also passed
      --frozen
          Fail instead of recording a new mq.lock entry. `--frozen`; use in CI so a new module's content is only ever trusted during a reviewable local run whose mq.lock diff gets committed, not silently during CI
      --lockfile <PATH>
          Path to the mq.lock file used for HTTP import integrity checks. Defaults to ./mq.lock (relative to the current directory)
  -N, --allow-net[=<DOMAIN>...]
          Allow the `http` function to make outbound HTTPS requests. Disabled by default; requests are HTTPS-only and blocked from reaching loopback/private/link-local addresses regardless of this flag. Pass with no value to allow any domain, or `--allow-net=DOMAIN` (repeat the flag, or comma-separate, to add more) to restrict requests to just those domains (and any path under them). The `=` is required so a bare domain after the flag isn't swallowed as a query/file positional instead
  -R, --allow-read[=<PATH>...]
          Allow the `read_file`/`read_file_bytes`/`collection`/`file_exists`/`embed_images` functions to read from the filesystem. Disabled by default. Pass with no value to allow reading anywhere, or `--allow-read=PATH` (files or directories; repeat the flag, or comma-separate, to add more) to restrict reads to just those paths and their descendants. The `=` is required so a bare path after the flag isn't swallowed as a query/file positional instead
  -W, --allow-write[=<PATH>...]
          Allow the `write_file`/`extract_images` functions to write to the filesystem. Disabled by default. Pass with no value to allow writing anywhere, or `--allow-write=PATH` (files or directories; repeat the flag, or comma-separate, to add more) to restrict writes to just those paths and their descendants. The `=` is required so a bare path after the flag isn't swallowed as a query/file positional instead
      --allow-run[=<COMMAND>...]
          Allow the `system` function to execute external commands. Disabled by default. Commands run directly (never through a shell), so shell metacharacters in arguments are never interpreted. Pass with no value to allow any command, or `--allow-run=COMMAND` (repeat the flag, or comma-separate, to add more) to restrict execution to just those commands. The `=` is required so a bare command after the flag isn't swallowed as a query/file positional instead
  -E, --allow-env[=<NAME>...]
          Allow `$VAR`/`${$VAR}` interpolation and debugger logpoints to read environment variables. Disabled by default. Pass with no value to allow reading any variable, or `--allow-env=NAME` (repeat the flag, or comma-separate, to add more) to restrict access to just those names. The `=` is required so a bare name after the flag isn't swallowed as a query/file positional instead
  -a, --allow-all
          Grant every sandboxed permission at once (read/write/net/run/env), and also enable HTTP module imports as if --allow-http-import were passed. Disabled by default. Cannot be combined with the individual --allow-* flags above
  -F, --output-format <OUTPUT_FORMAT>
          Set output format. When omitted, inferred from the `-o`/`--output` file extension if given (e.g. `.json` -> json, `.csv` -> csv), else defaults to markdown [possible values: markdown, html, text, json, table, grep, gron, raw, csv, toml, toon, xml, yaml, shell, none]
  -U, --update
          Update matching Markdown nodes and write the result to stdout
      --diff
          With --update, print a unified diff instead of the transformed content; nothing is written. Multiple files are diffed one at a time with their path in the headers; stdin is labeled `<stdin>`. Exits 1 if anything would change
      --unbuffered
          Unbuffered output
      --list-style <LIST_STYLE>
          Set the list style for markdown output [default: dash] [possible values: dash, plus, star]
      --link-title-style <LINK_TITLE_STYLE>
          Set the link title surround style for markdown output [default: double] [possible values: double, single, paren]
      --link-url-style <LINK_URL_STYLE>
          Set the link URL surround style for markdown links [default: none] [possible values: none, angle]
  -S, --separator <QUERY>
          Specify a query to insert between files as a separator
  -o, --output <FILE>
          Output to the specified file
      --atomic-output <ATOMIC_OUTPUT>
          Write `-o`/`--output` atomically via a same-directory temp file + fsync + rename, so a crash or full disk mid-write can't truncate the target [default: auto] [possible values: auto, always, never]
  -C, --color-output
          Colorize markdown output
  -B, --before-context <NUM>
          Show NUM nodes before each match. Only effective with -F grep
      --after-context <NUM>
          Show NUM nodes after each match. Only effective with -F grep
      --context <NUM>
          Show NUM nodes before and after each match. Only effective with -F grep
  -e, --exit-status
          Exit with code 1 if the last output value is false, null, or the output is empty. Mirrors jq's --exit-status / -e flag
  -c, --count
          Output only the count of matching (non-None) results. Mirrors grep -c. With multiple files, prints "filename: N" per file and "total: N" at the end
      --skip <N>
          Skip the first N matching results before outputting
      --limit <N>
          Limit output to at most N results
      --no-position
          Omit Markdown node position information from structured output (json, table, gron, csv, toml, toon, xml, yaml). Reduces output size when source line/column spans aren't needed
      --compact
          Print JSON on a single line, without pretty-printing. Only valid with -F json
      --indent <N>
          Number of spaces per indent level in pretty-printed output (0-7), jq's `--indent`. Only valid with -F json or -F xml; default is 2
      --tab
          Indent pretty-printed output with tabs instead of spaces, jq's `--tab`. Only valid with -F json or -F xml
  -T, --format <FORMAT>
          Set both input and output format at once (shorthand for `-I FORMAT -F FORMAT`). An explicit `-I`/`-F` overrides this for that side. Only accepts formats valid on both sides; e.g. `-I mdx` or `-F table` still require the dedicated flag [possible values: markdown, html, text, json, gron, raw, csv, toml, toon, xml, yaml]
      --list
          List all available subcommands (built-in and external)
  -P <PARALLEL_THRESHOLD>
          Number of files to process before switching to parallel processing [default: 10]
      --argv [<ARGV>...]
          Positional string arguments, available as ARGS."positional" in queries
  -O, --optimize-level <OPTIMIZE_LEVEL>
          Optimization level for AST transformations (none = no changes, basic = constant folding and dead-branch elimination, full = all passes) [default: none] [possible values: none, basic, full]
      --timeout <SECONDS>
          Maximum time in seconds allowed for query evaluation before aborting (e.g. 0.5, 5). No timeout by default
  -h, --help
          Print help (see more with '--help')
  -V, --version
          Print version

# Examples

mq 'query' file.md

Run `mq help examples` for more usage examples, or `mq help <name>` for
function/selector/module docs.

Types and Values

Values

  • 42 (a number)
  • "Hello, world!" (a string)
  • b"abc" (a bytes literal)
  • :value (a symbol)
  • [1, 2, 3], array(1, 2, 3) (an array)
  • {"a": 1, "b": 2, "c": 3}, dict(["a", 1], ["b", 2], ["c", 3]) (a dictionary)
  • true, false (a boolean)
  • None

Types

TypeDescriptionExamples
NumberRepresents numeric values.1, 3.14, -42
StringRepresents sequences of characters, including Unicode code points and escape sequences in the form of \{0x000}."hello", "123", "😊", "\u{1F600}"
BytesRepresents a raw byte sequence. Written with a b prefix. Only ASCII characters are allowed unescaped.b"abc", b"\xf0\x9f\x99\x82", b""
SymbolRepresents immutable, interned identifiers prefixed with :. Used for constant values and keys.:value, :success, :error, :ok
BooleanRepresents truth values.true, false
ArrayRepresents ordered collections of values.[1, 2, 3], array(1, 2, 3)
DictRepresents key-value mappings (dictionaries).{"a": 1, "b": 2}, dict(["a", 1], ["b", 2])
FunctionRepresents executable code.def foo(): 42; let name = def foo(): 42;

Byte String Literals

Byte string literals use the b"..." syntax and represent raw sequences of bytes (u8 values).

b"hello"            # 5-byte sequence [104, 101, 108, 108, 111]
b"\xf0\x9f\x99\x82" # 4-byte emoji encoded as raw bytes
b""                 # empty byte sequence

Allowed characters

Only ASCII characters (code points 0–127) may appear unescaped inside a byte literal. Non-ASCII characters such as é or 😊 must be written using \xNN hex escapes:

# Correct — use \xNN for non-ASCII bytes
b"\xc3\xa9"    # UTF-8 encoding of 'é' (2 bytes: 0xc3, 0xa9)

# Wrong — non-ASCII characters are not accepted in b"..."
# b"é"          ← syntax error; use \xNN escapes instead

Supported escape sequences

EscapeByte value
\\0x5c (backslash)
\"0x22 (double quote)
\n0x0a (newline)
\r0x0d (carriage return)
\t0x09 (tab)
\00x00 (null)
\xNNArbitrary byte (two hex digits)

Common operations

b"abc" | len          # 3  — byte length, not character count
b"abc" | type         # "bytes"
b"abc" == b"abc"      # true
b"abc" | is_empty     # false
b""    | is_empty     # true

Accessing Values

Array Index Access

Arrays can be accessed using square bracket notation with zero-based indexing:

let arr = [1, 2, 3, 4, 5]

arr[0]     # Returns 1 (first element)
arr[2]     # Returns 3 (third element)
arr[6]     # Returns None

You can also use the get function explicitly:

get(arr, 0)    # Same as arr[0]
arr | get(2)    # Same as arr[2]

Array Slice Access

Arrays support slice notation to extract subarrays using the arr[start:end] syntax:

let arr = [1, 2, 3, 4, 5]

arr[1:4]    # Returns [2, 3, 4] (elements from index 1 to 3)
arr[0:3]    # Returns [1, 2, 3] (first three elements)
arr[2:5]    # Returns [3, 4, 5] (elements from index 2 to end)

Slice indices work as follows:

  • start: The starting index (inclusive)
  • end: The ending index (exclusive)
  • Both indices are zero-based
  • If start or end is out of bounds, it will be clamped to valid range
let arr = [1, 2, 3, 4, 5]

arr[0:2]    # Returns [1, 2]
arr[3:10]   # Returns [4, 5] (end index clamped to array length)
arr[2:2]    # Returns [] (empty slice when start equals end)

Dictionary Key Access

Dictionaries can be accessed using square bracket notation with keys:

let d = {"name": "Alice", "age": 30, "city": "Tokyo"}

d["name"]   # Returns "Alice"
d["age"]    # Returns 30
d["city"]   # Returns "Tokyo"

You can also use the get function explicitly:

get(d, "name")   # Same as di["name"]
d | get("age")    # Same as d["age"]

Spread Operator

The ... spread operator expands an array or dict inline inside an array or dict literal.

let a = [1, 2, 3]
| let b = [4, 5, 6]
| let c = [...a, ...b]      # [1, 2, 3, 4, 5, 6]
| let d = [0, ...a, 99]     # [0, 1, 2, 3, 99]
let base = {x: 1, y: 2}
| let merged = {...base, y: 99, z: 3}   # {x: 1, y: 99, z: 3}

When the same key appears more than once, later keys override earlier ones, including keys coming from a spread. Spreading None contributes nothing; spreading any other non-array (in [...]) or non-dict (in {...}) value is a type error.

Dynamic Access

Both arrays and dictionaries support dynamic access using variables:

let arr = [10, 20, 30]
| let index = 1
| arr[index]     # Returns 20

let d = {"x": 100, "y": 200}
| let key = "x"
| d[key]      # Returns 100

Environment Variables

A module handling environment-specific functionality.

  • __FILE__: Contains the path to the file currently being processed.
  • __FILE_NAME__: Contains the name of the file currently being processed (without the path).
  • __FILE_STEM__: Contains the stem of the file currently being processed (filename without extension).

Conditionals and Comparisons

Conditionals and Comparisons

Conditional expressions and comparison operators in mq allow for decision-making based on the evaluation of conditions, enabling dynamic behavior in your queries.

Conditionals

mq supports standard conditional operations through the following functions:

  • and(a, b), a && b - Returns true if both a and b are true
  • or(a, b), a || b - Returns true if either a or b is true
  • not(a), !a - Returns true if a is false

Examples

# Basic comparisons
and(true, true, true)
true && true && true
# => true
or(true, false, true)
true || false || true
# => true
not(false)
!false
# => true

Comparisons

mq provides comparison functionality through built-in functions.

Basic Comparisons

Standard comparison operators are supported:

  • eq(a, b), a == b - Returns true if a equals b
  • ne(a, b), a != b - Returns true if a does not equal b
  • gt(a, b), a > b - Returns true if a is greater than b
  • gte(a, b), a >= b - Returns true if a is greater than or equal to b
  • lt(a, b), a < b - Returns true if a is less than b
  • lte(a, b), a <= b - Returns true if a is less than or equal to b

Examples

# Basic comparisons
1 == 1
# => true
2 > 1
# => true
"a" <= "b"
# => true

# String comparisons
"hello" == "hello"
# => true
"xyz" > "abc"
# => true

# Numeric comparisons
5.5 >= 5.0
# => true
-1 < 0
# => true

# Logical operations
and(true, false)
# => false
or(true, false)
# => true
not(false)
# => true

# Complex conditions
and(x > 0, x < 10)
# =>  true if 0 < x < 10

Regex Match Operator

The =~ operator tests whether a string matches a regular expression pattern. It returns true if the pattern matches and false otherwise.

Syntax

string =~ pattern

This is equivalent to calling is_regex_match(string, pattern).

Examples

# Basic regex match
"hello world" =~ "hello"
# => true

"hello world" =~ "^world"
# => false

# Match digits
"abc123" =~ "[0-9]+"
# => true

"abc" =~ "^[0-9]+$"
# => false

# Use in a conditional
"foo bar" | if (. =~ "foo"): "matched" else: "no match"
# => matched

# Complex patterns
"2024-01-15" =~ "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
# => true

Not Regex Match Operator

The !~ operator tests whether a string does not match a regular expression pattern. It returns true if the pattern does not match and false otherwise.

Syntax

string !~ pattern

This is equivalent to calling is_not_regex_match(string, pattern).

Examples

# Basic not regex match
"hello world" !~ "bye"
# => true

"hello world" !~ "hello"
# => false

# Match digits
"abc" !~ "[0-9]+"
# => true

# Use in a conditional
"foo bar" | if (. !~ "baz"): "not matched" else: "match"
# => not matched

try-catch

The try-catch expression allows you to handle errors gracefully by providing a fallback value when an expression fails.

Syntax

try <expr> catch <expr>
try <expr> catch(<binder>) <expr>

Behavior

  • If the try expression succeeds, its result is returned
  • If the try expression fails (produces an error), the catch expression is evaluated instead
  • The catch expression receives the same input as the try expression
  • With catch(<binder>), <binder> is bound to a dict describing the failure (currently {"message": <string>}) for the duration of the catch expression
  • break/continue inside the try expression are not treated as errors; they propagate to the enclosing loop instead of triggering catch

Examples

Basic Error Handling

# When the expression succeeds
try: "value" catch: "unknown"

# When the expression fails
try: get("missing") catch: "default"

Chaining with Pipe

# Try to parse as JSON, fallback to raw string
try: from_json() catch: self

# Complex fallback logic
try: do get("data") | from_json(); catch: []

Nested Try-Catch

# Multiple fallback levels
try: get("primary") catch: try: get("secondary") catch: "default"

Error Binder

# Bind the failure to `e` and inspect its message
try: error("boom") catch(e): e["message"]
# => "boom"

Error Suppression (?)

The error suppression operator ? provides a concise way to handle errors by returning None when an expression fails, instead of raising an error. This is equivalent to using a regular try-catch with a default fallback.

Examples

# Equivalent to a regular try-catch with a default value
get("missing")?

In this example, if get("missing") fails, the result will be None rather than an error.

Syntax

Syntax

This section outlines the syntax rules in mq, providing a clear reference for writing valid code.

Comments

Similar to jq, comments starting with # are doc-comments.

# doc-comment
let value = add(2, 3);

Control flow

If Expression

The if expression evaluates a condition and executes code based on the result:

 if (x == 1):
   "one"
 elif (x == 2):
   "two"
 else:
   "other"
 if (x == 1):
   do "one" | upcase;
 elif (x == 2):
   do "TWO" | downcase;
 else:
   do
    "other" | upcase
   end
 if (x == 1):
   "one"

The if expression can be nested and chained with elif and else clauses. The conditions must evaluate to boolean values.

Unless Expression

The unless expression executes code only when a condition is false — the inverse of a single-branch if:

unless (x == 1):
  "not one"
unless (x == 1):
  "not one"
# => None (when x == 1, the body is not evaluated)

While Expression

The while loop repeatedly executes code while a condition is true:

let x = 5 |
while (x > 0):
  let x = x - 1 | x
end
# => 0

You can use break: <expr> to return a value from a while loop:

var x = 10 |
while (x > 0):
  x = x - 1 |
  if(eq(x, 3)):
    break: "Found three!"
  else:
    x
end
# => "Found three!"

Until Expression

The until loop repeatedly executes code while a condition is false — the inverse of while:

var x = 0 |
until (x >= 5):
  x = x + 1 | x
end
# => 5

Like while, until supports break/break: <expr> and continue inside its body.

Foreach Expression

The foreach loop iterates over elements in an array:

let items = array(1, 2, 3) |
foreach (x, items):
   sub(x, 1)
end
# => array(0, 1, 2)

You can use break: <expr> to exit early and return a specific value instead of an array:

let items = array(1, 2, 3, 4, 5) |
foreach (x, items):
  if(x > 3):
    break: "Found value greater than 3"
  else:
    x
end
# => "Found value greater than 3"

Foreach loops are useful for:

  • Processing arrays element by element
  • Mapping operations across collections
  • Filtering and transforming data

Loop Expression

The loop expression creates an infinite loop that continues until explicitly terminated with break:

var x = 0 |
loop:
  x = x + 1 |
  if(x > 5):
    break
  else:
    x
end
# => 5

The loop can be controlled using break to exit the loop and continue to skip to the next iteration:

var x = 0 |
loop:
  x = x + 1 |
  if(x < 3):
    continue
  elif(x > 5):
    break
  else:
    x
end
# => 5

The break statement can return a value from a loop using the break: <expr> syntax. This allows loops to be used as expressions that produce a specific value when exited:

var x = 0 |
loop:
  x = x + 1 |
  if(x > 5):
    break: "Found it!"
  else:
    x
end
# => "Found it!"

Loop expressions are useful for:

  • Implementing infinite loops with conditional exits
  • Creating retry mechanisms
  • Processing until a specific condition is met
  • Complex iteration patterns that don’t fit while or foreach

Pattern Matching

The match expression enables pattern matching on values, providing a powerful way to destructure and handle different data types.

Basic Syntax

match (value):
  | pattern: body
end

The match expression evaluates the value and compares it against a series of patterns. The first matching pattern’s body is executed.

Literal Patterns

Match against specific values:

match (x):
  | 1: "one"
  | 2: "two"
  | _: "other"
end

Literal patterns support:

  • Numbers: 1, 2.5, -10
  • Strings: "hello", "world"
  • Booleans: true, false
  • None: none

Type Patterns

Match based on value type using the :type_name syntax:

match (value):
  | :string: "is string"
  | :number: "is number"
  | :array: "is array"
  | :dict: "is dict"
  | :bool: "is boolean"
  | :none: "is none"
  | _: "other type"
end

Available type patterns:

  • :string - Matches string values
  • :number - Matches numeric values
  • :array - Matches array values
  • :dict - Matches dictionary values
  • :bool - Matches boolean values
  • :markdown - Matches markdown values
  • :none - Matches none value

Markdown Node-Kind Patterns

Match a :markdown value against a specific node kind using the same names as selectors (without the leading .):

match (node):
  | :h1: "top-level heading"
  | :h2: "section heading"
  | :code: "code block"
  | :list: "list"
  | :text: "text"
  | _: "other node"
end

Any node-kind name accepted by a selector (h1..h6, h, code, list, text, strong, emphasis, link, table, and so on) can be used. A pattern matches only when the value is a markdown node of that kind; other values fall through to the next arm.

Inside a matched arm, . refers to the matched node itself, so attribute selectors like .depth, .lang, or .ordered can be used directly in the guard or body without binding a variable:

match (node):
  | :h if (.depth > 2): "deep heading"
  | :code if (.lang == "rust"): upcase(.value)
  | :code: .value
  | :list if (.ordered): "ordered list"
  | :list: "unordered list"
  | _: "other node"
end

Array Patterns

Destructure arrays and bind elements to variables:

match (arr):
  | []: "empty array"
  | [x]: x
  | [x, y]: add(x, y)
  | [first, second, third]: first
  | [first, ..rest]: first
end

Array patter features:

  • Match exact length: [x, y] matches arrays with exactly 2 elements
  • Rest pattern: ..rest captures remaining elements
  • Empty array: [] matches empty arrays
  • Variable binding: Elements are bound to named variables

Rest Pattern Example

match (arr):
  | [head, ..tail]: tail
end
# array(1, 2, 3, 4) => array(2, 3, 4)

Dict Patterns

Destructure dictionaries and extract values:

match (obj):
  | {name, age}: name
  | {x, y}: add(x, y)
  | {}: "empty dict"
  | _: "no match"
end

Dict pattern features:

  • Extract specific keys: {name, age} binds values to variables
  • Partial matching: Matches dicts that have at least the specified keys
  • Empty dict: {} matches empty dictionaries

Example with Object

let person = {"name": "Alice", "age": 30, "city": "Tokyo"} |
match (person):
  | {name, age}: s"${name} is ${age} years old"
  | {name}: name
  | _: "unknown"
end
# => "Alice is 30 years old"

Or Patterns

Match multiple patterns in a single arm using ||:

match (x):
  | 1 || 2 || 3: "small"
  | 4 || 5: "medium"
  | _: "other"
end

Or patterns can combine any pattern types:

# Literal or patterns
match (status):
  | "ok" || "success": "all good"
  | "error" || "fail" || "failure": "something went wrong"
  | _: "unknown"
end

# Type or patterns
match (value):
  | :string || :number: "primitive"
  | :array || :dict: "collection"
  | _: "other"
end

# Mixed literal and type
match (x):
  | 0 || false || none: "falsy"
  | _: "truthy"
end

The first alternative that matches is used. The overall arm matches if any of the alternatives match.

Variable Binding

Bind the matched value to a variable:

match (value):
  | x: x + 1
end

Variable binding captures the entire value and makes it available in the body expression.

Wildcard Pattern

The underscore _ matches any value:

match (x):
  | 1: "one"
  | 2: "two"
  | _: "something else"
end

Use the wildcard pattern as the last arm to handle all remaining cases.

Guards

Add conditions to patterns using if:

match (n):
  | x if (x > 0): "positive"
  | x if (x < 0): "negative"
  | _: "zero"
end

Guards allow you to:

  • Add complex conditions to patterns
  • Filter matched values
  • Combine pattern matching with boolean logic

Guard Examples

# Match even numbers
match (n):
  | x if (x % 2 == 0): "even"
  | _: "odd"
end

# Match array with positive numbers
match (arr):
  | [x, ..] if (x > 0): "starts with positive"
  | _: "other"
end

Multiple Arms

Combine multiple patterns for comprehensive matching:

match (value):
  | 0: "zero"
  | x if (x > 0): "positive"
  | x if (x < 0): "negative"
  | :string: "text"
  | []: "empty array"
  | [x, ..rest]: "non-empty array"
  | {}: "empty dict"
  | _: "something else"
end

Pattern Matching vs If Expressions

Pattern matching provides several advantages over if expressions:

Using If Expressions

if (type_of(x) == "number"):
  if (x == 0):
    "positive number"
  elif (x < 0):
    "negative number"
  else:
    "zero"
elif (type_of(x) == "array"):
  if (len(x) == 0):
    "empty array"
  else:
    "non-empty array"
else:
  "other"

Using Pattern Matching

match (x):
  | n if (n > 0): "positive number"
  | n if (n < 0): "negative number"
  | 0: "zero"
  | []: "empty array"
  | [_, ..rest]: "non-empty array"
  | _: "other"
end

Practical Examples

Processing Different Data Types

def describe(value):
  match (value):
    | :none: "nothing"
    | :bool: "true or false"
    | x if (gt(x, 100)): "big number"
    | :number: "small number"
    | "": "empty string"
    | :string: "text"
    | []: "empty list"
    | [x]: s"list with one item: ${x}"
    | [_, ..rest]: "list with multiple items"
    | {}: "empty object"
    | _: "dictionary"
  end
end

Extracting Data from Structures

def get_first_name(user):
  match (user):
    | {name}: name
    | _: "unknown"
  end

Handling API Responses

def handle_response(response):
  match (response):
    | {status, data} if (eq(status, 200)): data
    | {status, error} if (eq(status, 404)): s"Not found: ${error}"
    | {status, error} if (eq(status, 500)): s"Server error: ${error}"
    | _: "Unknown response"
  end
end

Classifying HTTP Status Codes with Or Patterns

def classify_status(code):
  match (code):
    | 200 || 201 || 204: "success"
    | 301 || 302 || 307 || 308: "redirect"
    | 400 || 422: "client error"
    | 401 || 403: "auth error"
    | 404: "not found"
    | 500 || 502 || 503: "server error"
    | _: "unknown"
  end
end

Dispatching on Markdown Node Kinds

def describe_node(node):
  match (node):
    | :h1: "top-level heading"
    | :h2: "section heading"
    | :h if (.depth > 2): "deep heading"
    | :code if (.lang == "rust"): upcase(.value)
    | :code: .value
    | :list if (.ordered): "ordered list"
    | :list: "unordered list"
    | :text: .value
    | _: "other node"
  end
end

Operator

Pipe Operator

A functional operator that allows chaining multiple filter operations together.

Usage

The pipe operator (|) enables sequential processing of filters, where the output of one filter becomes the input of the next filter.

Examples

# Basic pipe usage
42 | add(1) | mul(2)
# => 86

# Multiple transformations
let mul2 = def mul2(x): mul(x, 2);
let gt4 = def gt4(x): gt(x, 4);
array(1, 2, 3) | map(mul2) | filter(gt4)
# => [6]

# Function composition
let double = def _double(x): mul(x, 2);
let add_one = def _add_one(x): add(x, 1);
5 | double(self) | add_one(self)
# => 11

Shift Operators

The shift operators (<< and >>) perform different operations depending on the type of the operand.

Left Shift (<<)

The left shift operator (<<) maps to the shift_left(value, amount) builtin function.

Operand typeBehavior
NumberBitwise left shift: multiplies the value by 2^amount
StringRemoves amount characters from the start of the string
ArrayAppends the value to the end of the array
Markdown HeadingDecreases the heading depth by amount (promotes the heading, e.g. ###), minimum depth is 1

Examples

# Bitwise left shift on numbers
1 << 2
# => 4

shift_left(1, 3)
# => 8

# Remove characters from the start of a string
shift_left("hello", 2)
# => "llo"

"hello" << 2
# => "llo"

# Promote a heading (decrease depth)
let md = do to_markdown("## Heading 2") | first; |
md << 1
# => # Heading 2

Right Shift (>>)

The right shift operator (>>) maps to the shift_right(value, amount) builtin function.

Operand typeBehavior
NumberBitwise right shift on the truncated integer value (shifts the bits right by amount)
StringRemoves amount characters from the end of the string
ArrayAdds the value to the beginning of the array
Markdown HeadingIncreases the heading depth by amount (demotes the heading, e.g. ###), maximum depth is 6

Examples

# Bitwise right shift on numbers
4 >> 2
# => 1

shift_right(8, 2)
# => 2

# Remove characters from the end of a string
shift_right("hello", 2)
# => "hel"

"hello" >> 2
# => "hel"

# Demote a heading (increase depth)
let md = do to_markdown("# Heading 1") | first; |
md >> 1
# => ## Heading 1

Conversion Operator (@)

The conversion operator (@) converts a value to a different type or format. It maps to the convert(value, type) builtin function.

Usage

value @ type

The type operand can be a symbol or a string that specifies the target format. The supported conversion targets are:

Type (symbol)Type (string)Behavior
:h1"#"Convert to a Markdown heading level 1
:h2"##"Convert to a Markdown heading level 2
:h3"###"Convert to a Markdown heading level 3
:h4"####"Convert to a Markdown heading level 4
:h5"#####"Convert to a Markdown heading level 5
:h6"######"Convert to a Markdown heading level 6
:htmlConvert Markdown to an HTML string
:textExtract the plain text content of a node
:shShell-escape the value for safe use in shell commands
:base64Encode the value as a Base64 string
:uriURL-encode the value
:uridURL-decode the value
">"Convert to a Markdown blockquote
"-"Convert to a Markdown list item
"~~"Convert to a Markdown strikethrough
"<url>" (a valid URL string)Convert to a Markdown link with the given URL
"**"Convert to a Markdown strong/bold
"--"Convert to a Markdown horizontal rule

Examples

# Convert a string to a Markdown heading
"Hello World" @ :h1
# => # Hello World

"Hello World" @ :h2
# => ## Hello World

# Convert using string syntax
"Hello World" @ "##"
# => ## Hello World

# Convert to a blockquote
"Important note" @ ">"
# => > Important note

# Convert to a list item
"Item one" @ "-"
# => - Item one

# Convert to a strikethrough
"old text" @ "~~"
# => ~~old text~~

# Convert to a Markdown link
"mq" @ "https://harehare.github.io/mq"
# => [mq](https://harehare.github.io/mq)

# Convert Markdown to HTML
let md = do to_markdown("# Hello") | first; |
md @ :html
# => "<h1>Hello</h1>"

# Extract plain text from a Markdown node
let md = do to_markdown("## Hello World") | first; |
md @ :text
# => "Hello World"

# Shell-escape a string for safe use in shell
"hello world" @ :sh
# => 'hello world'

"safe-string" @ :sh
# => safe-string

# Encode to Base64
"hello" @ :base64
# => "aGVsbG8="

# URL-encode a string
"hello world" @ :uri
# => "hello%20world"

.. Operator

The range operator (..) creates sequences of consecutive values between a start and end point.

Usage

The range operator generates arrays of values from a starting point to an ending point (inclusive). It works with both numeric values and characters.

Examples

# Numeric ranges
1..5
# => [1, 2, 3, 4, 5]

# Character ranges
"a".."e"
# => ["a", "b", "c", "d", "e"]

# Using ranges with other operations
1..3 | map(fn(x): mul(x, 2);)
# => [2, 4, 6]

# Reverse ranges
5..1
# => [5, 4, 3, 2, 1]

# Single element range
3..3
# => [3]

Assignment Operators

Assignment operators are used to assign values to variables and combine assignment with arithmetic or logical operations.

Simple Assignment

The basic assignment operator (=) assigns a value to a variable.

Usage

let x = 10 |
let name = "mq" |
let items = [1, 2, 3]

Update Operator (|=)

The update operator (|=) applies an expression to a selected value and updates it in place.

Usage

<selector.value> |= expr

The left side specifies what to update using a selector, and the right side is the expression that transforms the value.

Examples

# Change the language of all code blocks to rust
.code.lang |= "rust"

# Update a heading level
.h.depth |= 2

Compound Assignment Operators

Compound assignment operators combine an arithmetic or logical operation with assignment, providing a shorthand for updating variables.

Addition Assignment (+=)

Adds a value to a variable and assigns the result back to the variable.

var x = 10 |
x += 5
# => x is now 15

var count = 0 |
count += 1
# => count is now 1

Subtraction Assignment (-=)

Subtracts a value from a variable and assigns the result back to the variable.

var x = 10 |
x -= 3
# => x is now 7

var balance = 100 |
balance -= 25
# => balance is now 75

Multiplication Assignment (*=)

Multiplies a variable by a value and assigns the result back to the variable.

var x = 5 |
x *= 3
# => x is now 15

var price = 100 |
price *= 1.1
# => price is now 110

Division Assignment (/=)

Divides a variable by a value and assigns the result back to the variable.

var x = 20 |
x /= 4
# => x is now 5

var total = 100 |
total /= 2
# => total is now 50

Modulo Assignment (%=)

Computes the remainder of dividing a variable by a value and assigns the result back to the variable.

var x = 17 |
x %= 5
# => x is now 2

var count = 23 |
count %= 10
# => count is now 3

Floor Division Assignment (//=)

Divides a variable by a value, floors the result (rounds down to the nearest integer), and assigns it back to the variable.

var x = 17 |
x //= 5
# => x is now 3

var count = 23 |
count //= 10
# => count is now 2

Functions

mq supports named functions defined with def, anonymous functions defined with fn, and the -> arrow shorthand.

Named Functions

Named functions are defined with def and can be called by name throughout the program.

Syntax

A function body can be terminated with ; or end:

def function_name(parameters):
  program;

def function_name(parameters):
  program
end

Examples

# Using semicolon terminator
def double(x):
  mul(x, 2);

# Using end terminator
def double(x):
  mul(x, 2)
end

# Function with conditional logic
def is_positive(x):
  gt(x, 0);

# Composition of functions
def add_then_double(x, y):
  add(x, y) | double(self);

Default Parameters

def function_name(param1, param2=default_value):
  program;
# Function with default parameter
def greet(name, greeting="Hello"):
  greeting + " " + name;

# Using end terminator
def greet(name, greeting="Hello"):
  greeting + " " + name
end

# Using default value
greet("Alice")
# Output: "Hello Alice"

# Overriding default value
greet("Bob", "Hi")
# Output: "Hi Bob"

# Default value can be an expression
def add_with_offset(x, offset=10 + 5):
  x + offset;

add_with_offset(20)
# Output: 35

Variadic Parameters

A function can accept a variable number of arguments using a * prefix on its last parameter. The variadic parameter collects all remaining arguments into an array.

def function_name(param1, *rest):
  program;
# Collect all arguments into an array
def all_args(*args):
  args;

all_args(1, 2, 3)
# Output: [1, 2, 3]

# Combine regular and variadic parameters
def first_and_rest(a, *rest):
  rest;

first_and_rest(1, 2, 3)
# Output: [2, 3]

# Variadic parameter is an empty array when no extra arguments are passed
def first_and_rest(a, *rest):
  rest;

first_and_rest(1)
# Output: []

A variadic parameter:

  • Must be the last parameter in the parameter list
  • Can only be declared once per function

Anonymous Functions

Anonymous functions (lambda expressions) are defined with fn or the -> shorthand, and can be passed as arguments, assigned to variables, or used inline.

Syntax

A function body can be terminated with ; or end:

fn(parameters): program;

fn(parameters): program end

The -> syntax is a shorthand alias for fn:

->(parameters): program;

->(parameters): program end

Examples

# Basic anonymous function
nodes | map(fn(x): add(x, "1");)

# Using end terminator
nodes | map(fn(x): add(x, "1") end)

# Using arrow syntax
nodes | map(->(x): add(x, "1");)

# As a callback
nodes | .[] | sort_by(fn(x): to_text(x);)

# Assigned to a variable
let multiply = fn(x, factor=2): x * factor;

Default Parameters

fn(param1, param2=default_value): program;
# Anonymous function with default parameter
let multiply = fn(x, factor=2): x * factor;

# Using end terminator
let multiply = fn(x, factor=2): x * factor end

# Using default value
multiply(10)
# Multiplies by 2 (default factor)

# Overriding default value
multiply(10, 3)
# Multiplies by 3

# Using in callbacks
[1, 2] | map(fn(x, prefix="Item: "): prefix + to_text(x);)

Variadic Parameters

Anonymous functions support variadic parameters the same way named functions do:

let sum_all = fn(*args): sum(args);

sum_all(1, 2, 3)
# Output: 6

Rules

  • Parameters with default values must come after parameters without default values
  • Default values are evaluated when the function is called, not when it is defined
  • Default values can be any valid expression
  • A variadic parameter (*name) must be the last parameter and can appear at most once

Pipeline Expressions As Arguments

Pipeline expressions can be passed directly as function arguments. The pipeline is treated as one argument until the next comma or closing parenthesis.

array("a" | upcase(), "b" | upcase())
# Output: ["A", "B"]

Parenthesis-Free Calls

Functions with 0 or 1 required parameters can be called without parentheses when used as pipeline steps.

  • A 0-argument function invoked without () is called with no explicit arguments.
  • A 1-argument function invoked without () receives the current pipeline value as its implicit argument.

This only applies in pipeline position (as a pipeline step). When a function is passed as a value to another function (e.g., map(arr, f)), no auto-call occurs and the function reference is preserved.

# 0-arg function: called without parentheses
def greet(): "Hello!";
| greet # equivalent to greet()
# Output: "Hello!"

# 1-arg function: current value is passed implicitly
def double(x): x * 2;
| 5 | double      # equivalent to 5 | double(5), i.e., double(5)
# Output: 10

# Builtin functions also support paren-free calls
"hello world" | upcase    # equivalent to upcase("hello world")
# Output: "HELLO WORLD"

[1, None, 2] | compact | len  # chained paren-free calls
# Output: 2

# Function references are preserved when passed as arguments
map(["a", "b"], upcase)   # upcase is NOT auto-called here; it's passed as a callback
# Output: ["A", "B"]

See Also

  • mq help <name> - Signature, parameter/return types, description, examples, and required capability for any builtin function or standard-module function, e.g. mq help map. Pass --json for machine-readable output, or --markdown for Markdown — queryable with mq itself, e.g. mq help section --markdown | mq 'select(.code.lang == "mq")' to pull out every example query in a module’s docs.
  • mq help <module> - Overview and function list for a standard module, e.g. mq help section or mq help table. Use module::function (e.g. mq help section::section) for a function whose name collides with its own module.
  • Builtin selectors and functions - Complete list of available selectors and functions

Environment variables

Environment variables can be referenced using $XXX syntax, where XXX represents the name of the environment variable. For example:

  • $PATH - References the PATH environment variable
  • $HOME - References the HOME environment variable
  • $USER - References the current user’s username

This syntax is commonly used in shell scripts and configuration files to access system-level environment variables.

Sandboxing

$VAR/${$VAR} references (and debugger logpoints that read an env var) are disabled by default, matching --allow-read/--allow-write/--allow-net/--allow-run. Pass --allow-env to allow reading any environment variable, or --allow-env=NAME (repeat the flag, or comma-separate, to add more) to restrict access to just those names:

# Allow any $VAR reference
mq --allow-env '$HOME' README.md

# Allow only $HOME
mq --allow-env=HOME '$HOME' README.md

--allow-all grants --allow-read/--allow-write/--allow-net/--allow-run/--allow-env all at once, including $VAR access.

Color Configuration

NO_COLOR

When set to a non-empty value, disables all colored output regardless of the -C flag. This follows the NO_COLOR standard.

# Disable colored output
NO_COLOR=1 mq -C '.h' README.md

MQ_COLORS

Customizes the colors used when -C (color output) is enabled. The format is a colon-separated list of key=value pairs, where each value is a semicolon-separated list of SGR (Select Graphic Rendition) parameters.

# Make headings bold red, code blocks blue
export MQ_COLORS="heading=1;31:code=34"
mq -C '.h' README.md

Only the specified keys are overridden; unspecified keys use the default colors. Invalid entries are silently ignored.

Available Keys

KeyDescriptionDefault
headingHeadings (#, ##, etc.)bold cyan (1;36)
codeFenced code blocksgreen (32)
code_inlineInline codegreen (32)
emphasisItalic text (*text*)italic yellow (3;33)
strongBold text (**text**)bold (1)
linkLinks ([text](url))underline blue (4;34)
link_urlLink URLsblue (34)
imageImages (![alt](url))magenta (35)
blockquoteBlockquote markers (>)dim (2)
deleteStrikethrough (~~text~~)red dim (31;2)
hrHorizontal rules (---)dim (2)
htmlInline HTMLdim (2)
frontmatterYAML/TOML frontmatterdim (2)
listList markers (-, *, 1.)yellow (33)
tableTable separatorsdim (2)
mathMath expressions ($...$)green (32)

Common SGR Codes

CodeEffect
0Reset
1Bold
2Dim
3Italic
4Underline
31Red
32Green
33Yellow
34Blue
35Magenta
36Cyan
37White

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()

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"]),
  )

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.

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

Variable Declarations

Let

The let binds an immutable value to an identifier for later use:

# Binds 42 to x
let x = 42
# Uses x in an expression
| let y = x + 1
# Binds `add` function to z
| let z = do let z = fn(x): x + 1; | z(1);

Once a variable is declared with let, its value cannot be changed.

Var

The var declares a mutable variable that can be reassigned:

# Declares a mutable variable
var counter = 0
# Reassigns the value
counter = counter + 1
# counter is now 1

Variables declared with var can be modified using the assignment operator (=):

var total = 100
| total = total - 25
# total is now 75

var message = "Hello"
| message = message + " World"
# message is now "Hello World"

Destructuring Assignment

Both let and var support destructuring patterns on the left-hand side.

Array Destructuring

let [a, b] = [1, 2] | a
# => 1

let [head, ..tail] = [1, 2, 3] | tail
# => [2, 3]

Dict Destructuring

let {name, age} = {"name": "Alice", "age": 30} | name
# => "Alice"

Mutable Destructuring

Using var allows reassigning destructured variables:

var [a, b] = [1, 2] | a = 99 | a
# => 99

Choosing Between Let and Var

  • Use let when you want to create an immutable binding (most cases)
  • Use var when you need to modify the value after declaration (counters, accumulators, etc.)

Self

The current value being processed can be referenced as self or . (dot). Both self and . behave identically. When there are insufficient arguments provided in a method call, the current value (self) is automatically passed as the first argument.

Examples

# These expressions are equivalent
"hello" | upcase()
"hello" | upcase(self)
"hello" | upcase(.)

String Interpolation

String Interpolation allow embedding expressions directly inside string literals. In mq, an interpolated string is prefixed with s" and variables can be embedded using ${} syntax.

Syntax

s"text ${ident} more text"

Escaping

You can escape the $ character in a string interpolation by using $$. This allows you to include literal $ symbols in your interpolated strings.

let price = 25
| s"The price is $$${price}"
# => Output: "The price is $25"

Examples

let name = "Alice"
| let age = 30
| s"Hello, my name is ${name} and I am ${age} years old."
# => Output: "Hello, my name is Alice and I am 30 years old."

Selectors

Selectors in mq allow you to select specific markdown nodes from a document. You can also access attributes of selected nodes using dot notation.

Basic Selector Usage

Selectors use the . prefix to select markdown nodes. For example:

.h       # Selects all heading nodes
.code    # Selects all code blocks
.link    # Selects all link nodes

Selector Aliases

Many selectors have shorter or alternative names that you can use interchangeably:

Canonical SelectorAliasesDescription
.text.p, .paragraphParagraph / text nodes
.list.liList items
.code.code_blockFenced code blocks
.code_inline.inline_codeInline code spans
.math_inline.inline_mathInline math spans
.horizontal_rule.hr, .---, .***, .___Horizontal rules
.break.brLine breaks

Example:

.p          # Same as .text — selects all paragraph nodes
.li         # Same as .list — selects all list items
.code_block # Same as .code — selects all fenced code blocks
.hr         # Same as .horizontal_rule

Selector Calls (Filtered Matching)

Selectors can accept arguments to filter nodes by specific properties, using a function-call syntax:

.h(1)           # Selects only h1 headings
.h(2, 3)        # Selects h2 and h3 headings
.h(1..3)        # Selects h1, h2, and h3 headings (range)
.code("rust")   # Selects only Rust code blocks

Heading Depth Filtering

Pass one or more numeric arguments to match headings at specific depths:

# Select only top-level headings
.h(1)

# Select h2 and h3 headings
.h(2, 3)

# Select h1 through h3 using a range
.h(1..3)

Code Language Filtering

Pass a string argument to match code blocks with a specific language:

# Select only Rust code blocks
.code("rust")

# Select Python or JavaScript code blocks
.code("python") | to_array | concat(.code("javascript"))

Pass one or more string arguments to match links or images by their exact URL:

# Select only links pointing to a specific URL
.link("https://example.com")

# Select links pointing to either of two URLs
.link("https://a.com", "https://b.com")

# Select only images with a specific URL
.image("photo.png")

Reference Identifier Filtering

Pass one or more string arguments to match reference-style nodes by their identifier:

# Select the link reference with identifier "id"
.link_ref("id")

# Select the image reference with identifier "id"
.image_ref("id")

# Select the footnote reference with identifier "1"
.footnote_ref("1")

# Select the footnote definition with identifier "1"
.footnote("1")

# Select the link/image definition with identifier "id"
.definition("id")

Combining with Other Operations

Selector calls can be combined with pipes and functions just like plain selectors:

# Extract content of all h2 headings
.h(2) | .value

# Count Rust code blocks
.code("rust") | len

# Replace language of all TypeScript blocks
.code("typescript").lang |= "ts"

Descendant Selectors

Chaining two or more selectors with a space selects nodes of the second type that are nested anywhere underneath a node of the first type — similar to a descendant combinator in CSS:

# Code blocks nested inside a blockquote (not top-level code blocks)
.blockquote .code

# Chains can go arbitrarily deep
.blockquote .list .code

# Each step can use a selector call to filter further
.blockquote .code("rust")

# A trailing attribute access still works after the chain
.blockquote .code.lang

This is sugar for recursing into each match and then filtering by the next selector, i.e. .blockquote .code is equivalent to .blockquote | .. | .code.

Note this only matches descendants (nodes nested at any depth), not direct children specifically. To get only the immediate children of a match, use the .children attribute: .blockquote.children | .code.

Attribute Access

Once you’ve selected a node, you can access its attributes using dot notation. The available attributes depend on the node type.

Common Attributes

value

Most nodes support value to get the text content:

.code.value    # Gets the code content

Heading Attributes

Heading nodes support the following attributes:

AttributeTypeDescriptionExample
depth, levelIntegerThe heading level (1-6).h.level
valueStringThe value of the heading.h.value

Example:

# Input: # Hello World

.h.level # Returns: 1
.h.value # Returns: "Hello World"

Code Block Attributes

Code block nodes support the following attributes:

AttributeTypeDescriptionExample
lang, languageStringThe language of the code block.code.lang
valueStringThe code content.code.value
metaStringMetadata associated with the code block.code.meta
fenceBooleanWhether the code block is fenced.code.fence

Example:

# Input: ```rust
# fn main() {}
# ```

.code.lang      # Returns: "rust"
.code.value     # Returns: "fn main() {}"

Link nodes support the following attributes:

AttributeTypeDescriptionExample
urlStringThe URL of the link.link.url
titleStringThe title of the link.link.title
valueStringThe link value.link.value

Example:

# Input: [Example](https://example.com "Example Site")

.link.url       # Returns: "https://example.com"
.link.title     # Returns: "Example Site"
.link.value     # Returns: "Example"

Image Attributes

Image nodes support the following attributes:

AttributeTypeDescriptionExample
urlStringThe URL of the image.image.url
altStringThe alt text of the image.image.alt
titleStringThe title of the image.image.title

Example:

# Input: ![Alt text](image.png "Image Title")

.image.url      # Returns: "image.png"
.image.alt      # Returns: "Alt text"
.image.title    # Returns: "Image Title"

List Attributes

List nodes support the following attributes:

AttributeTypeDescriptionExample
indexIntegerThe index of the list item.list.index
levelIntegerThe nesting level of the list item.list.level
orderedBooleanWhether the list is ordered.list.ordered
checkedBooleanThe checked state (for task lists).list.checked
valueStringThe text content of the list item.list.value

Table Cell Attributes

Table cell nodes support the following attributes:

AttributeTypeDescriptionExample
rowIntegerThe row number of the cell.[0][0].row
columnIntegerThe column number of the cell.[0][0].column
last_cell_in_rowBooleanWhether this is the last cell in the row.[0][0].last_cell_in_row
last_cell_of_in_tableBooleanWhether this is the last cell in the table.[0][0].last_cell_of_in_table
valueStringThe text content of the cell.[0][0].value

Reference Nodes Attributes

Reference nodes (link references, image references, footnotes) support:

Node TypeAttributesDescription
.link_refident, labelIdentifier and label of link reference
.image_refident, label, altIdentifier, label, and alt text
.footnote_refident, labelIdentifier and label of footnote
.footnoteident, textIdentifier and content of footnote
.definitionident, url, title, labelLink/image definition attributes

MDX Attributes

MDX nodes support the following attributes:

AttributeTypeDescriptionExample
nameStringThe name of the MDX element.mdx_jsx_flow_element.name
valueStringThe content of the MDX node.mdx_flow_expression.value

Text Nodes Attributes

Text, HTML, YAML, TOML, Math nodes support:

AttributeTypeDescriptionExample
valueStringThe text content.text.value

Property Selector (Dict Key Access)

Property selectors access values from dict (object) values using quoted dot notation (."key"). They work on both single dicts and arrays of dicts.

Use ."key" to access a dict key by name:

# Input dict: {"name": "Alice", "age": 30}

."name"   # Returns: "Alice"
."age"    # Returns: 30

Keys with spaces, special characters, or names that match reserved selector names are also supported:

# Input dict: {"h1": "title", "my key": "value"}

."h1"       # Returns: "title"
."my key"   # Returns: "value"
."url"      # Returns: the "url" key value

Escape sequences inside quoted keys: \" for a literal " and \\ for a literal \.

Arrays of Dicts

When applied to an array of dicts, the property selector maps over each element:

# Input: [{"name": "Alice"}, {"name": "Bob"}, {"name": "Charlie"}]

."name"   # Returns: ["Alice", "Bob", "Charlie"]

Non-dict elements in the array return none.

Missing Keys

Accessing a key that doesn’t exist returns none:

# Input dict: {"name": "Alice"}

."age"    # Returns: none

Combining Selectors with Functions

You can combine selectors with functions like select(), map(), and filter() for powerful transformations:

Using select()

The select() function filters elements based on a condition:

# Select only code blocks (exclude non-code nodes)
select(.code)

# Select nodes that are not code blocks
select(!.code)

Using map()

Transform each selected node:

# Get all heading levels
.h | map(fn(h): h.level;)

# Get all code block languages
.code | map(fn(c): c.lang;)

Using filter()

Filter nodes based on attribute values:

# Get only level 2 headings
.h | filter(fn(h): h.level == 2;)

# Get only rust code blocks
.code | filter(fn(c): c.lang == "rust";)

The selector call syntax provides a more concise alternative for common cases:

.h(2)           # equivalent to: .h | filter(fn(h): h.level == 2;)
.code("rust")   # equivalent to: .code | filter(fn(c): c.lang == "rust";)

Extract Code Languages

.code.lang
.link.url

Filter High-Level Headings

# Using attribute comparison
select(.h.level <= 2)

# Using selector call for exact levels
.h(1, 2)

Setting Attributes

You can modify node attributes using the update operator (|=):

# Change code block language
.code.lang |= "rust"

# Update link URL
.link.url |= "https://new-url.com"

# Update heading level
.h.depth |= 2

The set_attr() function is an alternative that takes the attribute name as a string:

.code | set_attr("lang", "rust")
.link | set_attr("url", "https://new-url.com")
.h | set_attr("level", 2)

See Also

  • mq help <selector> - Signature, matched/produced type, description, and examples for any selector, e.g. mq help .h1 (the leading . is optional: mq help h1 also works).
  • Builtin selectors and functions - Complete list of available selectors and functions
  • Nodes - Details about markdown node types

Nodes

The nodes in mq allows you to access and manipulate all Markdown nodes as a single flat array.

Basic Usage

The nodes filter returns an array of all nodes in a Markdown document:

nodes

Examples

Finding all headings

nodes | select(.h)

Converting all text to uppercase

nodes | map(upcase)

Counting nodes by type

nodes | len

Builtin selectors and functions

mq - Function Reference