Skip to content
← Back to Examples

Capture Atoms

RustRubyconceptualintermediate

Extract named values from parsed input using capture atoms. Like named groups in regex, but integrated into the parsing grammar.

Key Concepts

  • named captures
  • zero-copy extraction
  • capture inspection API
  • cross-backend support

Why Parse This Format?

Captures allow extracting specific fields from parsed input without building a full AST. This is more efficient for simple extraction tasks.

Use Cases

  • Extracting email addresses from text
  • Parsing log files for specific fields (IP, status, timestamp)
  • Parsing CSV exports for data migration
  • Field extraction without full AST construction

Input Format

Any structured text with fields to extract.

user@example.com, 192.168.1.1 - - [timestamp] "GET /path" 200

Examples

user@example.comVALID

Email address to parse

192.168.1.1 - - [10/Oct/2000:13:55:36] "GET /apache_pb.gif HTTP/1.0" 200 2326VALID

Apache log line

Output Format

Named captures accessible via the capture inspection API.

Structure

capture_names

List of all capture names

get_capture

Get text of a named capture

captures

All captures as HashMap

How the Code Works

This section explains how the parsing rules map to the format semantics.

Basic Capture

let grammar = GrammarBuilder::new()
    .rule("greeting", capture("greeting", str("hello")))
    .build();

let result = parser.parse("hello world")?;
if let Some(text) = result.get_capture("greeting", input) {
    println!("Captured: {}", text);  // "hello"
}

Wrap any parslet with `capture()` to name the matched text: The captured text is extracted without copying the input string.

Nested Captures

let grammar = GrammarBuilder::new()
    .rule("email", capture("email",
        sequence([
            capture("local", re(r"[a-zA-Z0-9._%+-]+")),
            str("@"),
            capture("domain", re(r"[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")),
        ])
    ))
    .build();

Captures can be nested to capture both the whole and parts: This captures: - `email`: the full email address - `local`: the part before @ - `domain`: the part after @

Log Line Parsing

let grammar = GrammarBuilder::new()
    .rule("log_line", sequence([
        capture("ip", re(r"\d+\.\d+\.\d+\.\d+")),
        str(" - - ["),
        capture("timestamp", re(r"[^\]]+")),
        str("] \""),
        capture("method", re(r"[A-Z]+")),
        str(" "),
        capture("path", re(r"[^\s]+")),
        str("\" "),
        capture("status", re(r"\d+")),
    ]))
    .build();

Real-world example parsing Apache log format: Each field is captured by name for easy access.

Inspection API

// Get all capture names
let names: Vec<String> = result.capture_names();

// Get specific capture
let value: Option<&str> = result.get_capture("name", input);

// Get all captures as HashMap
let all: HashMap<&str, &str> = result.captures(input);

Raw Grammar API

let mut grammar = Grammar::new();
let name = grammar.add_atom(Atom::Regex { pattern: r"[a-zA-Z]+".into() });
let name_capture = grammar.add_atom(Atom::Capture {
    name: "name".into(),
    atom: name,
});

For more control, use the raw `Grammar` API:

Raw Grammar API

let mut grammar = Grammar::new();
let name = grammar.add_atom(Atom::Regex { pattern: r"[a-zA-Z]+".into() });
let name_capture = grammar.add_atom(Atom::Capture {
    name: "name".into(),
    atom: name,
});

For more control, use the raw `Grammar` API:

Source Code

Rust
//! Capture Atoms Example
//!
//! Demonstrates how to extract named values from parsed input using capture atoms.
//! Captures work like named groups in regular expressions, but are integrated
//! into the parsing grammar and work across all backends.
//!
//! Run with: cargo run --example captures --no-default-features

#![allow(clippy::print_literal)]

use parsanol::portable::{
    parser_dsl::{capture, dynamic, re, seq, str, GrammarBuilder},
    AstArena, Atom, Grammar, PortableParser,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("Capture Atoms Example");
    println!("=====================\n");

    // =========================================================================
    // Example 1: Basic Capture
    // =========================================================================
    println!("--- Example 1: Basic Capture ---\n");

    let grammar = GrammarBuilder::new()
        .rule("greeting", capture("greeting", dynamic(str("hello"))))
        .build();

    let input = "hello world";
    let mut arena = AstArena::for_input(input.len());
    let mut parser = PortableParser::new(&grammar, input, &mut arena);
    let result = parser.parse_from_pos(0)?;

    // Extract the captured value
    if let Some(text) = result.get_capture("greeting", input) {
        println!("  Captured 'greeting': {:?}", text); // "hello"
    }

    // =========================================================================
    // Example 2: Email Parsing with Nested Captures
    // =========================================================================
    println!("\n--- Example 2: Email Parsing with Nested Captures ---\n");

    let grammar = GrammarBuilder::new()
        .rule(
            "email",
            capture(
                "email",
                seq(vec![
                    dynamic(capture("local", dynamic(re(r"[a-zA-Z0-9._%+-]+")))),
                    dynamic(str("@")),
                    dynamic(capture(
                        "domain",
                        dynamic(re(r"[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")),
                    )),
                ]),
            ),
        )
        .build();

    let input = "user@example.com";
    let mut arena = AstArena::for_input(input.len());
    let mut parser = PortableParser::new(&grammar, input, &mut arena);
    let result = parser.parse_from_pos(0)?;

    println!("  Full email: {:?}", result.get_capture("email", input));
    println!("  Local part: {:?}", result.get_capture("local", input));
    println!("  Domain:     {:?}", result.get_capture("domain", input));

    // =========================================================================
    // Example 3: Inspecting All Captures
    // =========================================================================
    println!("\n--- Example 3: Inspecting All Captures ---\n");

    // Get all capture names
    println!("  Capture names: {:?}", result.capture_names());

    // Get all captures as HashMap
    let all_captures = result.captures(input);
    println!("  All captures:");
    for (name, value) in &all_captures {
        println!("    {} = {:?}", name, value);
    }

    // =========================================================================
    // Example 4: Using Raw Grammar API
    // =========================================================================
    println!("\n--- Example 4: Using Raw Grammar API ---\n");

    let mut grammar = Grammar::new();

    let name = grammar.add_atom(Atom::Re {
        pattern: r"[a-zA-Z]+".into(),
    });
    let name_capture = grammar.add_atom(Atom::Capture {
        name: "name".into(),
        atom: name,
    });

    let comma = grammar.add_atom(Atom::Str {
        pattern: ", ".into(),
    });

    let age = grammar.add_atom(Atom::Re {
        pattern: r"\d+".into(),
    });
    let age_capture = grammar.add_atom(Atom::Capture {
        name: "age".into(),
        atom: age,
    });

    let person = grammar.add_atom(Atom::Sequence {
        atoms: vec![name_capture, comma, age_capture],
    });
    grammar.root = person;

    let input = "Alice, 30";
    let mut arena = AstArena::for_input(input.len());
    let mut parser = PortableParser::new(&grammar, input, &mut arena);
    let result = parser.parse_from_pos(0)?;

    println!("  Name: {:?}", result.get_capture("name", input));
    println!("  Age:  {:?}", result.get_capture("age", input));

    // =========================================================================
    // Example 5: Accessing Capture State Directly
    // =========================================================================
    println!("\n--- Example 5: Accessing Capture State Directly ---\n");

    let grammar = GrammarBuilder::new()
        .rule("item", capture("item", dynamic(re(r"[a-z]+"))))
        .build();

    let input = "apple";
    let mut arena = AstArena::for_input(input.len());
    let mut parser = PortableParser::new(&grammar, input, &mut arena);
    parser.parse()?;

    // Access capture state directly from parser
    let captures = parser.capture_state();
    let names: Vec<_> = captures.names().collect();
    println!("  Capture names from parser: {:?}", names);
    if let Some(value) = captures.get("item") {
        println!("  Item: {:?}", value.get_text(input));
    }

    // =========================================================================
    // Summary
    // =========================================================================
    println!("\n--- Benefits of Capture Atoms ---");
    println!("* Zero-copy: captures store offsets, not strings");
    println!("* Works across all backends (Packrat, Bytecode, Streaming)");
    println!("* Clean API: capture_names(), get_capture(), captures()");
    println!("* No AST construction needed for simple extraction");
    println!("* DSL helpers: capture(\"name\", parslet)");

    println!("\n--- Performance Notes ---");
    println!("* Captures add minimal overhead (~5%% for heavy use)");
    println!("* Capture lookup is O(n) where n = number of captures");
    println!("* Consider scope atoms for nested contexts");

    println!("\n--- API Summary ---");
    println!("  parser.parse_from_pos(0)  -> ParseResult with captures");
    println!("  parser.capture_state()    -> &CaptureState");
    println!("  result.get_capture(name, input) -> Option<&str>");

    Ok(())
}
Ruby
# frozen_string_literal: true

#
# Capture Atoms Example
#
# Demonstrates how to extract named values from parsed input using capture atoms.
# Captures work like named groups in regular expressions, but are integrated
# into the parsing grammar.

# NOTE: This example focuses on demonstrating the capture API.
# For production use, please refer to the existing capture example
# and the FFI documentation for more advanced patterns.

$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
require "parsanol/parslet"
require "pp"

include Parsanol::Parslet

puts "Capture Atoms Example"
puts "=====================\n"

# ===========================================================================
# Example 1: Basic Capture
# ===========================================================================
puts "--- Example 1: Basic Capture ---\n"

# Simple capture: match 'hello' and capture it
parser = str("hello").capture(:greeting)

input = "hello"
result = parser.parse(input)
puts "  Input: #{input.inspect}"
puts "  Result: #{result.inspect} (a Slice)"
puts "  Result.to_s: #{result.to_s.inspect}"
# ===========================================================================
# Example 2: Multiple Captures in Sequence
# ===========================================================================
puts "\n--- Example 2: Multiple Captures in Sequence ---\n"

# Parse key=value pairs
kv_parser = match("[a-z]+").capture(:key) >>
  str("=") >>
  match("[a-zA-Z0-9]+").capture(:value)
input = "name=Alice"
result = kv_parser.parse(input)
puts "  Input: #{input.inspect}"
puts "  Result: #{result.inspect}"
puts "  Key: #{result[:key]}"
puts "  Value: #{result[:value]}"
# ===========================================================================
# Example 3: Captures with Dynamic
# ===========================================================================
puts "\n--- Example 3: Captures with Dynamic ---\n"

# Use captured value in dynamic block
class TypeParser < Parsanol::Parser
  include Parsanol::Parslet

  root :declaration
  rule(:type) { match("[a-z]+").capture(:type) }
  rule(:value) do
    dynamic do |ctx|
      # Get captured type to determine value parser
      type_val = ctx[:type].to_s
      case type_val
      when "int" then match('\d+')
      when "str" then match("[a-z]+")
      when "bool" then str("true") | str("false")
      else match("[a-z]+") # fallback
      end.capture(:value)
    end
  end
  rule(:declaration) do
    type >> str(":") >> match("[a-z]+").capture(:name) >> str("=") >> value
  end
end
test_cases = [
  ["int:count=42", "int"],
  ["str:message=hello", "str"],
  ["bool:enabled=true", "bool"],
]
puts "\nTesting type-driven parsing:"
test_cases.each_key do |input|
  puts "  Input: #{input.inspect}"
  parser = TypeParser.new
  result = parser.parse(input)
  puts "  ✓ Parsed successfully"
  puts "    type: #{result[:type]}"
  puts "    name: #{result[:name]}"
  puts "    value: #{result[:value]}"
end
# ===========================================================================
# Summary
# ===========================================================================
puts "\n--- Benefits of Capture Atoms ---"
puts "* Zero-copy: captures store offsets, not strings"
puts "* Works across all backends (Packrat, Streaming)"
puts "* Clean API: capture(name) method on atoms"
puts "* No AST construction needed for simple extraction"
puts "\n--- Performance Notes ---"
puts "* Captures add minimal overhead (~5% for heavy use)"
puts "* Capture lookup is O(n) where n = number of captures"
puts "\n--- API Summary ---"
puts "  atom.capture(:name)     -> captures match result"
puts "  result[:name]           -> retrieves captured value (Slice or Hash)"
puts "  result[:name].to_s      -> converts Slice to String"
puts "  context.captures[:name] -> in dynamic blocks"