Namespace Globbing #5

Open
opened 2026-01-10 10:53:41 +08:00 by panda · 0 comments
Owner

Namespace Globbing Implementation

Behavior Specification

Pattern Matching

  1. * (single-level wildcard)

    • Pattern: ctx:*
    • Matches: ctx:foo, ctx:bar, ctx:baz
    • Does NOT match: ctx:foo:sub, ctx, (empty segment)
  2. ** (multi-level wildcard)

    • Pattern: ctx:**
    • Matches: ctx:foo, ctx:foo:bar, ctx:foo:bar:baz
    • Does NOT match: ctx (the prefix itself)
  3. Combined patterns

    • Pattern: ctx:project:*
    • Matches: ctx:project:ideas, ctx:project:bugs
    • Does NOT match: ctx:project, ctx:project:ideas:archived

Output Format

When a pattern is detected (contains * or **), output is a list of namespace keys:

     1  ctx:foo
     2  ctx:bar
     3  ctx:foo:sub

This is the LISTING format (with numbers). For export:

vlist ctx:* export

Outputs (no numbers):

ctx:foo
ctx:bar
ctx:foo:sub

Implementation Algorithm

fn expand_glob(pattern: &str, prefix: &str) -> Result<Vec<String>, VkError> {
    // 1. Get all keys matching "prefix:*"
    let all_keys = Valkey::keys(&format!("{}:*", prefix))?;
    
    // 2. Strip prefix from keys to get list names
    let list_names: Vec<String> = all_keys
        .into_iter()
        .filter_map(|k| k.strip_prefix(&format!("{}:", prefix)).map(String::from))
        .collect();
    
    // 3. Match pattern
    if pattern.contains("**") {
        // Recursive: match all descendants
        match_recursive(pattern, &list_names)
    } else if pattern.contains('*') {
        // Single level: match one segment
        match_single_level(pattern, &list_names)
    } else {
        // No glob
        Ok(vec![pattern.to_string()])
    }
}

fn match_single_level(pattern: &str, names: &[String]) -> Result<Vec<String>, VkError> {
    let parts: Vec<&str> = pattern.split(':').collect();
    let depth = parts.len();
    
    names.iter()
        .filter(|name| {
            let name_parts: Vec<&str> = name.split(':').collect();
            if name_parts.len() != depth {
                return false;
            }
            
            parts.iter().zip(&name_parts).all(|(p, n)| {
                p == "*" || p == n
            })
        })
        .cloned()
        .collect()
}

fn match_recursive(pattern: &str, names: &[String]) -> Result<Vec<String>, VkError> {
    // "ctx:**" matches anything starting with "ctx:"
    let prefix = pattern.trim_end_matches(":**");
    
    names.iter()
        .filter(|name| {
            name.starts_with(prefix) && name.len() > prefix.len()
        })
        .cloned()
        .collect()
}

Command Behavior

# Without command: lists namespaces
vlist ctx:*                    # Shows: ctx:foo, ctx:bar

# With export: exports namespace keys (not contents)
vlist ctx:* export             # Outputs: ctx:foo\nctx:bar\n

# To export contents, pipe it:
vlist ctx:* export | parallel vlist {} export

# Or with xargs:
vlist ctx:* export | xargs -I{} sh -c 'echo "=== {} ==="; vlist {} export'

Integration Points

In vlist.rs

Modify parse_list_and_command:

fn parse_list_and_command(args: &[String]) -> (Option<String>, Option<String>, Vec<String>) {
    // ... existing logic ...
    
    // Check if list name contains glob
    if let Some(ref list) = list_opt {
        if list.contains('*') {
            // This is a glob query, not a normal list
            // If no command, this becomes a "list namespaces" operation
            // If command is "export", we export the namespace list itself
            // Other commands are errors (for now)
        }
    }
    
    // ... rest of logic ...
}

Add new function:

fn list_namespaces(cfg: &Config, pattern: &str) -> Result<(), VkError> {
    let matches = expand_glob(pattern, &cfg.prefix)?;
    for (i, name) in matches.iter().enumerate() {
        println!("{:>6}\t{}", i + 1, name);
    }
    Ok(())
}

Testing

# Setup
vlist ctx:project:ideas add "idea 1"
vlist ctx:project:bugs add "bug 1"
vlist ctx:notes add "note 1"

# Test single level
vlist ctx:*
# Expected:
#      1  ctx:notes
#      2  ctx:project

vlist ctx:project:*
# Expected:
#      1  ctx:project:bugs
#      2  ctx:project:ideas

# Test recursive
vlist ctx:**
# Expected:
#      1  ctx:notes
#      2  ctx:project
#      3  ctx:project:bugs
#      4  ctx:project:ideas

# Test export
vlist ctx:** export | wc -l
# Expected: 4

Error Handling

  1. Glob with mutation command:

    vlist ctx:* add "item"  # ERROR: cannot add to multiple lists
    
  2. No matches:

    vlist nonexistent:*  # No output (empty list)
    
  3. Mixed glob and literal:

    vlist ctx:*:ideas  # OK: matches ctx:X:ideas for any X
    

Future Extensions

  1. Glob in middle of path: ctx:*:ideas (already supported by design)
  2. Multiple globs: *:project:* (more complex)
  3. Exclusion: ctx:** but not ctx:archive:** (later)
# Namespace Globbing Implementation ## Behavior Specification ### Pattern Matching 1. **`*` (single-level wildcard)** - Pattern: `ctx:*` - Matches: `ctx:foo`, `ctx:bar`, `ctx:baz` - Does NOT match: `ctx:foo:sub`, `ctx`, (empty segment) 2. **`**` (multi-level wildcard)** - Pattern: `ctx:**` - Matches: `ctx:foo`, `ctx:foo:bar`, `ctx:foo:bar:baz` - Does NOT match: `ctx` (the prefix itself) 3. **Combined patterns** - Pattern: `ctx:project:*` - Matches: `ctx:project:ideas`, `ctx:project:bugs` - Does NOT match: `ctx:project`, `ctx:project:ideas:archived` ### Output Format When a pattern is detected (contains `*` or `**`), output is a list of namespace keys: ``` 1 ctx:foo 2 ctx:bar 3 ctx:foo:sub ``` This is the LISTING format (with numbers). For export: ```sh vlist ctx:* export ``` Outputs (no numbers): ``` ctx:foo ctx:bar ctx:foo:sub ``` ### Implementation Algorithm ```rust fn expand_glob(pattern: &str, prefix: &str) -> Result<Vec<String>, VkError> { // 1. Get all keys matching "prefix:*" let all_keys = Valkey::keys(&format!("{}:*", prefix))?; // 2. Strip prefix from keys to get list names let list_names: Vec<String> = all_keys .into_iter() .filter_map(|k| k.strip_prefix(&format!("{}:", prefix)).map(String::from)) .collect(); // 3. Match pattern if pattern.contains("**") { // Recursive: match all descendants match_recursive(pattern, &list_names) } else if pattern.contains('*') { // Single level: match one segment match_single_level(pattern, &list_names) } else { // No glob Ok(vec![pattern.to_string()]) } } fn match_single_level(pattern: &str, names: &[String]) -> Result<Vec<String>, VkError> { let parts: Vec<&str> = pattern.split(':').collect(); let depth = parts.len(); names.iter() .filter(|name| { let name_parts: Vec<&str> = name.split(':').collect(); if name_parts.len() != depth { return false; } parts.iter().zip(&name_parts).all(|(p, n)| { p == "*" || p == n }) }) .cloned() .collect() } fn match_recursive(pattern: &str, names: &[String]) -> Result<Vec<String>, VkError> { // "ctx:**" matches anything starting with "ctx:" let prefix = pattern.trim_end_matches(":**"); names.iter() .filter(|name| { name.starts_with(prefix) && name.len() > prefix.len() }) .cloned() .collect() } ``` ### Command Behavior ```sh # Without command: lists namespaces vlist ctx:* # Shows: ctx:foo, ctx:bar # With export: exports namespace keys (not contents) vlist ctx:* export # Outputs: ctx:foo\nctx:bar\n # To export contents, pipe it: vlist ctx:* export | parallel vlist {} export # Or with xargs: vlist ctx:* export | xargs -I{} sh -c 'echo "=== {} ==="; vlist {} export' ``` ## Integration Points ### In `vlist.rs` Modify `parse_list_and_command`: ```rust fn parse_list_and_command(args: &[String]) -> (Option<String>, Option<String>, Vec<String>) { // ... existing logic ... // Check if list name contains glob if let Some(ref list) = list_opt { if list.contains('*') { // This is a glob query, not a normal list // If no command, this becomes a "list namespaces" operation // If command is "export", we export the namespace list itself // Other commands are errors (for now) } } // ... rest of logic ... } ``` Add new function: ```rust fn list_namespaces(cfg: &Config, pattern: &str) -> Result<(), VkError> { let matches = expand_glob(pattern, &cfg.prefix)?; for (i, name) in matches.iter().enumerate() { println!("{:>6}\t{}", i + 1, name); } Ok(()) } ``` ### Testing ```sh # Setup vlist ctx:project:ideas add "idea 1" vlist ctx:project:bugs add "bug 1" vlist ctx:notes add "note 1" # Test single level vlist ctx:* # Expected: # 1 ctx:notes # 2 ctx:project vlist ctx:project:* # Expected: # 1 ctx:project:bugs # 2 ctx:project:ideas # Test recursive vlist ctx:** # Expected: # 1 ctx:notes # 2 ctx:project # 3 ctx:project:bugs # 4 ctx:project:ideas # Test export vlist ctx:** export | wc -l # Expected: 4 ``` ## Error Handling 1. **Glob with mutation command**: ```sh vlist ctx:* add "item" # ERROR: cannot add to multiple lists ``` 2. **No matches**: ```sh vlist nonexistent:* # No output (empty list) ``` 3. **Mixed glob and literal**: ```sh vlist ctx:*:ideas # OK: matches ctx:X:ideas for any X ``` ## Future Extensions 1. **Glob in middle of path**: `ctx:*:ideas` (already supported by design) 2. **Multiple globs**: `*:project:*` (more complex) 3. **Exclusion**: `ctx:**` but not `ctx:archive:**` (later)
panda added reference rustic 2026-01-10 10:59:21 +08:00
Sign in to join this conversation.
No labels
CHORE
DOCS
IDEA
ISSUE
UX
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
panda/vlist#5
No description provided.