# LPeg Minimal Guide: Finding Partial Strings

LPeg is NOT regex. Use these patterns instead:

- <https://www.inf.puc-rio.br/~roberto/lpeg/>

## Basic String Finding

```lua
local peg = require "peg"

-- Simple string finding
peg.found(text, "error")        -- finds "error" anywhere
peg.found(text, "warning")      -- finds "warning" anywhere

-- Pattern matching with peg module (for complex patterns)
local errorPattern = peg.pattern("error") + peg.pattern("warning") -- error OR warning
-- error FOLLOWED BY warning (concatenation)
local errorThenWarning = peg.pattern("error") * peg.pattern("warning")
peg.match(text, errorPattern)
```

## Pattern Matching Numbers

```lua
-- Ranges: peg.range("09") = [0-9], peg.range("az") = [a-z], peg.range("AZ") = [A-Z]
local digit = peg.range("09")  -- single digit 0-9
local letter = peg.range("az") + peg.range("AZ")  -- Any letter
-- Match "1 error", "2 errors", "10 warnings", etc.
local nonZeroDigit = peg.range("19")  -- [1-9]
local singularError = peg.pattern("1") * peg.pattern(" error")      -- "1 error"
local pluralErrors = nonZeroDigit * digit * peg.pattern(" errors") -- "2-99 errors"
```

## Common Patterns

```lua
-- Match "not found"
peg.found(text, "not found")
-- Match "command not found"
peg.found(text, "command not found")
-- Match file not found patterns
peg.found(text, "No such file or directory")
-- Match I/O error
peg.found(text, "I/O error")
-- Match luacheck specific patterns
peg.found(text, "unfinished string")
peg.found(text, "syntax error")
```

## Combining Patterns

```lua
-- Multiple alternatives with OR (use peg for complex patterns)
local errorPatterns = peg.pattern("error") + peg.pattern("warning") + peg.pattern("I/O error")
peg.match(text, errorPatterns)

-- NOT found pattern (negation) - use peg.other()
local goodPattern = peg.pattern("0 errors") + peg.pattern("0 warnings")
local hasErrors = peg.other(goodPattern) * peg.pattern("error")  -- error that's not "0 errors"

-- Search pattern anywhere in string (not just from start)
-- This is a more advanced pattern using recursion - see peg source for details
local function anywherePattern(patt)
  -- Implementation requires peg grammar table syntax
  return patt -- Simplified for documentation - use peg.found() instead
end
```

## Key Differences from Regex

- No `[a-z]` character classes - use `peg.range("az")`
- No `+` quantifier - use `peg.least(pattern, 1)` for one or more
- No `*` quantifier - use `peg.least(pattern, 0)` for zero or more
- No `|` OR - use `peg.pattern(p1) + peg.pattern(p2)` for OR
- No `^` start - use `peg.pattern("^")` for literal ^
- No `$` end - use `-1` for end of string

## Important Notes

- `peg.toPattern(pattern)` takes **only one parameter**, not multiple
- Use `peg.found(text, "string-to-find")` for simple string finding
- Use `peg.pattern()`, `peg.range()`, `peg.match()` for complex patterns
- `*` = concatenation (A followed by B) - works with peg patterns
- `+` = OR (A or B) - works with peg patterns
- `peg.least(pattern, 0)` = zero or more repetitions
- `peg.least(pattern, 1)` = one or more repetitions
- `peg.most(pattern, n)` = at most n repetitions

## For fix.lua

Instead of regex `[1-9]%d* errors?` use:

```lua
-- Create proper range patterns using peg
local nonZeroDigit = peg.range("19")  -- digits 1-9 (non-zero)
local digit = peg.range("09")         -- digits 0-9
local singularError = peg.pattern("1") * peg.pattern(" error")      -- "1 error"
local pluralErrors = nonZeroDigit * digit * peg.pattern(" errors") -- "2 errors" to "99 errors"
local luacheckErrorPattern = singularError + pluralErrors + peg.pattern("1 warning") + peg.pattern("I/O error")

-- Usage with peg.found
if peg.found(output, luacheckErrorPattern) then
  -- Found errors like "2 errors", "10 errors", "1 error", etc.
end

```

## patternAntiPattern

```lua
-- Match content between quotes using anti-pattern
local quotePattern, notQuotePattern = peg.patternAntiPattern('"')
local quotedContent = quotePattern * notQuotePattern * quotePattern

-- Remove DOCTYPE declarations: "<!DOCTYPE" + (anything except ">") + ">"
local gtPattern, notGtPattern = peg.patternAntiPattern(">")
local doctypePattern = peg.pattern("<!DOCTYPE") * notGtPattern * gtPattern
```

### Pattern Caching

For performance, cache patterns outside functions:

```lua
local quote, notQuote, entity, notEntity, entityPattern

function extractVersion(dtdContent)
    if entityPattern == nil then
        quote, notQuote = peg.patternAntiPattern('"')
        entity, notEntity = peg.patternAntiPattern("<!ENTITY cxml.version ")
        -- notEntity allows anything before "<!ENTITY"
        -- entity matches "<!ENTITY cxml.version " exactly
        entityPattern = notEntity * entity * quote * peg.capture(notQuote) * quote
    end
    return peg.match(dtdContent, entityPattern)
end
```

### WHY patternAntiPattern is Essential

**Problem**: When searching for a pattern like `version=` in XML documents, you don't want to match the XML declaration's `version="1.0"` - you want the cXML element's `version="1.2.066"`.

**Solution**: `peg.patternAntiPattern("version=")` returns two patterns:

- `version` - matches "version=" exactly
- `notVersion` - matches anything EXCEPT "version="

**Usage**: `notVersion * version` ensures:

1. The pattern can appear ANYWHERE in the document (not at start)
2. It won't match false positives like XML declaration version
3. It correctly handles multiple version attributes

**CRITICAL**: This pattern is almost ALWAYS needed when matching text that does NOT start at the beginning. Without `notVersion * version`, the pattern would only match if it appears at the very start of the text.

**Example for method attribute**:

```lua
-- Find method="..." anywhere in XSLT
method, notMethod = peg.patternAntiPattern('method=')
quote, notQuote = peg.patternAntiPattern('"')
methodPattern = notMethod * method * quote * peg.capture(notQuote) * quote
-- This matches: anything, then method=", then capture value until next "
```

### Critical: Why peg.least(peg.define.anychar, n) FAILS

**Common mistake**: Using `peg.least(peg.define.anychar, 1)` to match characters before a pattern.

**Problem**: `peg.least()` is GREEDY and will consume ANY characters, including the text you're looking for!

**Example**:

```xml
<cXML payloadID="..." timestamp="..." version="1.2.066">
```

**Wrong approach**:

```lua
-- This FAILS because peg.least consumes "version="!
local badPattern = notCxml * cxml * peg.least(peg.define.anychar, 1) * peg.pattern("version=") * quote * peg.capture(notQuote) * quote
```

What happens:

1. `peg.least(peg.define.anychar, 1)` consumes: `payloadID="..." timestamp="..." version="1.2.066">`
2. Then `peg.pattern("version=")` looks for ANOTHER "version="
3. Pattern fails because "version=" was already eaten!

**Correct approach**:

```lua
local version, notVersion = peg.patternAntiPattern("version=")
local goodPattern = notVersion * version * quote * peg.capture(notQuote) * quote
```

What happens:

1. `notVersion` matches: ` payloadID="..." timestamp="..." `
2. **Stops BEFORE "version="** (doesn't consume it)
3. `version` matches "version="
4. Success!

**Key Rule**:

- Use `peg.least()` for minimum repetitions (like regex `*` or `+`)
- Use `peg.patternAntiPattern()` to match up to BUT NOT INCLUDING specific text

### patternAntiPattern Parameters: When and How to Use

The four parameters control repetition behavior:

```lua
peg.patternAntiPattern(pattern, countLeast, countMost, countAntiLeast)
```

#### 1. Basic Usage (default parameters)

```lua
local gt, notGt = peg.patternAntiPattern(">")
-- gt matches ">"
-- notGt matches anything except ">"
```

#### 2. Optional Patterns (countLeast = 0)

```lua
local quote, notQuote = peg.patternAntiPattern('"', 0)
-- quote matches '"' zero or more times
-- notQuote matches anything except '"'
```

**Use when**: The pattern might not appear at all.

#### 3. Range of Repetitions (countLeast, countMost)

```lua
local digit, notDigit = peg.patternAntiPattern(peg.range("09"), 2, 4)
-- digit matches 2-4 digits
-- notDigit matches anything except digits
```

**Use when**: You need a specific number of repetitions.

#### 4. Minimum Anti-Pattern Length (countAntiLeast)

```lua
local word, notWord = peg.patternAntiPattern(peg.define.separator, 0, nil, 1)
-- word matches zero or more separators
-- notWord matches anything except separators (needs at least 1 char)
```

**Use when**: You want to ensure the anti-pattern matches at least some content.

#### 5. Real-World Examples

**Extract email domain**:

```lua
local at, notAt = peg.patternAntiPattern("@")
local domain = notAt * at * peg.least(peg.define.alpha + peg.range("09"), 1)
```

**Parse CSV values**:

```lua
local comma, notComma = peg.patternAntiPattern(",", 0)
local value = notComma * comma
```

**Find nth occurrence**:

```lua
local word, notWord = peg.patternAntiPattern("error", 2, 2) -- Exactly 2 "error"s
local beforeSecondError = notWord
```

**Parameter combinations**:

```lua
-- Match "tag" that appears 1-3 times, with anti-pattern matching at least 5 chars
local tag, notTag = peg.patternAntiPattern("tag", 1, 3, 5)
```

## Essential PEG Functions

### Pattern Creation

```lua
peg.pattern("string")     -- Literal string pattern
peg.set("abc")           -- Character set: any of a, b, or c
peg.range("az")          -- Character range: a-z
peg.range("09")          -- Character range: 0-9
peg.toPattern("string")  -- Convert string to pattern (single parameter)
peg.patternAntiPattern(patt, countLeast, countMost, countAntiLeast)
                        -- Returns two patterns: pattern, antiPattern
                        -- pattern: matches 'patt' with optional repetition counts
                        -- antiPattern: matches anything EXCEPT 'patt'

-- Parameters:
-- patt: The pattern to match (string or pattern)
-- countLeast: Minimum repetitions of patt (default 1)
-- countMost: Maximum repetitions of patt (default 1)
-- countAntiLeast: Minimum repetitions of anti-pattern (default 1)

-- Examples:
peg.patternAntiPattern('">')           -- Match '">" exactly once
peg.patternAntiPattern('">"', 0)        -- Match '">' zero or more times
peg.patternAntiPattern('">"', 1, 3)     -- Match '">' 1 to 3 times
peg.patternAntiPattern('">"', 0, nil, 2) -- Anti-pattern needs at least 2 chars
```

### Predefined Patterns (peg.define)

The `peg.define` table contains useful predefined patterns:

```lua
peg.define.locale           -- Lua locale patterns (alpha, alnum, etc.)
peg.define.digit           -- Digits 0-9 (same as peg.range("09"))
peg.define.space           -- Space character " "
peg.define.whiteSpace      -- Space or tab " \t"
peg.define.anyWhiteSpace   -- Space, tab, return, newline, formfeed " \t\r\n\f"
peg.define.notSpace        -- Any character except space
peg.define.endOfLine       -- Any end-of-line pattern: "\n\r", "\r\n", "\n", "\r"
peg.define.separator       -- Non-alphanumeric, non-underscore characters (used for word boundaries)
peg.define.emptyLineEnd    -- End of string (-1)
peg.define.anychar         -- Any character except null
peg.define.anytext         -- Zero or more any characters

-- String patterns
peg.define.singlequoted    -- Single-quoted string: 'content'
peg.define.doublequoted    -- Double-quoted string: "content"
peg.define.quoted          -- Either single or double quoted string

-- Long string patterns (Lua style)
peg.define.longStringStart    -- Start of long string: [[ or [=[...=[
peg.define.longCommentStart   -- Start of long comment: --[[
peg.define.longStringEnd      -- End of long string: ]] or ]=...=]
```

### Pattern Operations

```lua
peg.least(pattern, 0)    -- Zero or more repetitions (like regex *)
peg.least(pattern, 1)    -- One or more repetitions (like regex +)
peg.most(pattern, n)     -- At most n repetitions
peg.pattern1 + peg.pattern2  -- OR (pattern1 OR pattern2)
peg.pattern1 * peg.pattern2  -- Concatenation (pattern1 FOLLOWED BY pattern2)
```

### String Finding & Matching

```lua
peg.found(text, pattern)      -- Find pattern anywhere in text (returns boolean)
peg.match(text, pattern)      -- Match pattern from start (returns position)
peg.find(text, pattern)       -- Find pattern position (returns number)
peg.startsWith(text, pattern) -- Check if text starts with pattern
peg.endsWith(text, pattern)   -- Check if text ends with pattern
```

### String Manipulation

```lua
peg.replace(text, old, new)        -- Replace all occurrences
peg.replaceOnce(text, old, new)     -- Replace first occurrence
peg.parseBefore(text, divider)      -- Get text before divider
peg.parseAfter(text, divider)       -- Get text after divider
peg.parseLast(text, divider)        -- Get last part after divider
peg.splitToArray(text, separator)   -- Split text to array
peg.trimFunction(separator)         -- Returns trim function, faster to create pattern once
peg.trim(text)                      -- Trim whitespace from both ends of string
```

### Examples

```lua
-- Find digits in text
local digitPattern = peg.range("09")
if peg.found("file123", digitPattern) then print("Found digits") end

-- Replace words
peg.replace("hello world", "world", "universe") -- "hello universe"

-- Parse file extension
peg.parseAfter("script.lua", ".") -- "lua"

-- Split CSV
local arr = peg.splitToArray("a,b,c", ",") -- {"a", "b", "c"}

-- Trim whitespace
local trimmed = peg.trim("  hello world  ") -- "hello world"
```
