Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a49998c2c | |||
| 590f19603e | |||
| ee8c4b9aa5 | |||
| e8d6613ac8 | |||
| 91ad9006fa | |||
| 60ba3ad417 | |||
| b74e4724d4 | |||
| 30246fd626 |
@@ -84,7 +84,7 @@ END`
|
|||||||
assert.Len(t, association.Commands, 0, "Expected 0 regular commands")
|
assert.Len(t, association.Commands, 0, "Expected 0 regular commands")
|
||||||
|
|
||||||
// Run the isolate commands
|
// Run the isolate commands
|
||||||
result, err := RunIsolateCommands(association, "test.txt", testContent)
|
result, err := RunIsolateCommands(association, "test.txt", testContent, false)
|
||||||
if err != nil && err != NothingToDo {
|
if err != nil && err != NothingToDo {
|
||||||
t.Fatalf("Failed to run isolate commands: %v", err)
|
t.Fatalf("Failed to run isolate commands: %v", err)
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@ END_SECTION2`
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run the isolate commands
|
// Run the isolate commands
|
||||||
result, err := RunIsolateCommands(associations["test.txt"], "test.txt", testContent)
|
result, err := RunIsolateCommands(associations["test.txt"], "test.txt", testContent, false)
|
||||||
if err != nil && err != NothingToDo {
|
if err != nil && err != NothingToDo {
|
||||||
t.Fatalf("Failed to run isolate commands: %v", err)
|
t.Fatalf("Failed to run isolate commands: %v", err)
|
||||||
}
|
}
|
||||||
@@ -234,7 +234,7 @@ func TestIsolateCommandsWithJSONMode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run the isolate commands
|
// Run the isolate commands
|
||||||
result, err := RunIsolateCommands(associations["test.json"], "test.json", testContent)
|
result, err := RunIsolateCommands(associations["test.json"], "test.json", testContent, false)
|
||||||
if err != nil && err != NothingToDo {
|
if err != nil && err != NothingToDo {
|
||||||
t.Fatalf("Failed to run isolate commands: %v", err)
|
t.Fatalf("Failed to run isolate commands: %v", err)
|
||||||
}
|
}
|
||||||
@@ -309,7 +309,7 @@ END_REGULAR`
|
|||||||
assert.Len(t, association.Commands, 1, "Expected 1 regular command")
|
assert.Len(t, association.Commands, 1, "Expected 1 regular command")
|
||||||
|
|
||||||
// First run isolate commands
|
// First run isolate commands
|
||||||
isolateResult, err := RunIsolateCommands(association, "test.txt", testContent)
|
isolateResult, err := RunIsolateCommands(association, "test.txt", testContent, false)
|
||||||
if err != nil && err != NothingToDo {
|
if err != nil && err != NothingToDo {
|
||||||
t.Fatalf("Failed to run isolate commands: %v", err)
|
t.Fatalf("Failed to run isolate commands: %v", err)
|
||||||
}
|
}
|
||||||
@@ -320,7 +320,7 @@ END_REGULAR`
|
|||||||
|
|
||||||
// Then run regular commands
|
// Then run regular commands
|
||||||
commandLoggers := make(map[string]*logger.Logger)
|
commandLoggers := make(map[string]*logger.Logger)
|
||||||
finalResult, err := RunOtherCommands("test.txt", isolateResult, association, commandLoggers)
|
finalResult, err := RunOtherCommands("test.txt", isolateResult, association, commandLoggers, false)
|
||||||
if err != nil && err != NothingToDo {
|
if err != nil && err != NothingToDo {
|
||||||
t.Fatalf("Failed to run regular commands: %v", err)
|
t.Fatalf("Failed to run regular commands: %v", err)
|
||||||
}
|
}
|
||||||
@@ -397,7 +397,7 @@ irons_spellbooks:chain_lightning
|
|||||||
assert.Len(t, association.Commands, 0, "Expected 0 regular commands")
|
assert.Len(t, association.Commands, 0, "Expected 0 regular commands")
|
||||||
|
|
||||||
// Run the isolate commands
|
// Run the isolate commands
|
||||||
result, err := RunIsolateCommands(association, "irons_spellbooks-server.toml", testContent)
|
result, err := RunIsolateCommands(association, "irons_spellbooks-server.toml", testContent, false)
|
||||||
if err != nil && err != NothingToDo {
|
if err != nil && err != NothingToDo {
|
||||||
t.Fatalf("Failed to run isolate commands: %v", err)
|
t.Fatalf("Failed to run isolate commands: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
9
main.go
9
main.go
@@ -12,8 +12,8 @@ import (
|
|||||||
"cook/processor"
|
"cook/processor"
|
||||||
"cook/utils"
|
"cook/utils"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
logger "git.site.quack-lab.dev/dave/cylogger"
|
logger "git.site.quack-lab.dev/dave/cylogger"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed example_cook.toml
|
//go:embed example_cook.toml
|
||||||
@@ -54,12 +54,16 @@ Features:
|
|||||||
- Parallel file processing
|
- Parallel file processing
|
||||||
- Command filtering and organization`,
|
- Command filtering and organization`,
|
||||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
CreateExampleConfig()
|
|
||||||
logger.InitFlag()
|
logger.InitFlag()
|
||||||
mainLogger.Info("Initializing with log level: %s", logger.GetLevel().String())
|
mainLogger.Info("Initializing with log level: %s", logger.GetLevel().String())
|
||||||
mainLogger.Trace("Full argv: %v", os.Args)
|
mainLogger.Trace("Full argv: %v", os.Args)
|
||||||
},
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
exampleFlag, _ := cmd.Flags().GetBool("example")
|
||||||
|
if exampleFlag {
|
||||||
|
CreateExampleConfig()
|
||||||
|
return
|
||||||
|
}
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
cmd.Usage()
|
cmd.Usage()
|
||||||
return
|
return
|
||||||
@@ -76,6 +80,7 @@ Features:
|
|||||||
rootCmd.Flags().StringP("filter", "f", "", "Filter commands before running them")
|
rootCmd.Flags().StringP("filter", "f", "", "Filter commands before running them")
|
||||||
rootCmd.Flags().Bool("json", false, "Enable JSON mode for processing JSON files")
|
rootCmd.Flags().Bool("json", false, "Enable JSON mode for processing JSON files")
|
||||||
rootCmd.Flags().BoolP("conv", "c", false, "Convert YAML files to TOML format")
|
rootCmd.Flags().BoolP("conv", "c", false, "Convert YAML files to TOML format")
|
||||||
|
rootCmd.Flags().BoolP("example", "e", false, "Generate example_cook.toml and exit")
|
||||||
|
|
||||||
// Set up examples in the help text
|
// Set up examples in the help text
|
||||||
rootCmd.SetUsageTemplate(`Usage:{{if .Runnable}}
|
rootCmd.SetUsageTemplate(`Usage:{{if .Runnable}}
|
||||||
|
|||||||
@@ -189,24 +189,161 @@ end
|
|||||||
|
|
||||||
---@param table table
|
---@param table table
|
||||||
---@param depth number?
|
---@param depth number?
|
||||||
function DumpTable(table, depth)
|
function dump(table, depth)
|
||||||
if depth == nil then
|
if depth == nil then
|
||||||
depth = 0
|
depth = 0
|
||||||
end
|
end
|
||||||
if (depth > 200) then
|
if (depth > 200) then
|
||||||
print("Error: Depth > 200 in dumpTable()")
|
print("Error: Depth > 200 in dump()")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
for k, v in pairs(table) do
|
for k, v in pairs(table) do
|
||||||
if (type(v) == "table") then
|
if (type(v) == "table") then
|
||||||
print(string.rep(" ", depth) .. k .. ":")
|
print(string.rep(" ", depth) .. k .. ":")
|
||||||
DumpTable(v, depth + 1)
|
dump(v, depth + 1)
|
||||||
else
|
else
|
||||||
print(string.rep(" ", depth) .. k .. ": ", v)
|
print(string.rep(" ", depth) .. k .. ": ", v)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Parses CSV text into rows and fields using a minimal RFC 4180 state machine.
|
||||||
|
---
|
||||||
|
--- Requirements/assumptions:
|
||||||
|
--- - Input is a single string containing the entire CSV content.
|
||||||
|
--- - Field separators are commas (,).
|
||||||
|
--- - Newlines between rows may be "\n" or "\r\n". "\r\n" is treated as one line break.
|
||||||
|
--- - Fields may be quoted with double quotes (").
|
||||||
|
--- - Inside quoted fields, doubled quotes ("") represent a literal quote character.
|
||||||
|
--- - No backslash escaping is supported (not part of RFC 4180).
|
||||||
|
--- - Newlines inside quoted fields are preserved as part of the field.
|
||||||
|
--- - Leading/trailing spaces are preserved; no trimming is performed.
|
||||||
|
--- - Empty fields and empty rows are preserved.
|
||||||
|
--- - The final row is emitted even if the text does not end with a newline.
|
||||||
|
---
|
||||||
|
--- Returns:
|
||||||
|
--- - A table (array) of rows; each row is a table (array) of string fields.
|
||||||
|
function fromCSV(csv)
|
||||||
|
local rows = {}
|
||||||
|
local fields = {}
|
||||||
|
local field = {}
|
||||||
|
|
||||||
|
local STATE_DEFAULT = 1
|
||||||
|
local STATE_IN_QUOTES = 2
|
||||||
|
local STATE_QUOTE_IN_QUOTES = 3
|
||||||
|
local state = STATE_DEFAULT
|
||||||
|
|
||||||
|
local i = 1
|
||||||
|
local len = #csv
|
||||||
|
|
||||||
|
while i <= len do
|
||||||
|
local c = csv:sub(i, i)
|
||||||
|
|
||||||
|
if state == STATE_DEFAULT then
|
||||||
|
if c == '"' then
|
||||||
|
state = STATE_IN_QUOTES
|
||||||
|
i = i + 1
|
||||||
|
elseif c == ',' then
|
||||||
|
table.insert(fields, table.concat(field))
|
||||||
|
field = {}
|
||||||
|
i = i + 1
|
||||||
|
elseif c == '\r' or c == '\n' then
|
||||||
|
table.insert(fields, table.concat(field))
|
||||||
|
field = {}
|
||||||
|
table.insert(rows, fields)
|
||||||
|
fields = {}
|
||||||
|
if c == '\r' and i < len and csv:sub(i + 1, i + 1) == '\n' then
|
||||||
|
i = i + 2
|
||||||
|
else
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.insert(field, c)
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
elseif state == STATE_IN_QUOTES then
|
||||||
|
if c == '"' then
|
||||||
|
state = STATE_QUOTE_IN_QUOTES
|
||||||
|
i = i + 1
|
||||||
|
else
|
||||||
|
table.insert(field, c)
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
else -- STATE_QUOTE_IN_QUOTES
|
||||||
|
if c == '"' then
|
||||||
|
table.insert(field, '"')
|
||||||
|
state = STATE_IN_QUOTES
|
||||||
|
i = i + 1
|
||||||
|
elseif c == ',' then
|
||||||
|
table.insert(fields, table.concat(field))
|
||||||
|
field = {}
|
||||||
|
state = STATE_DEFAULT
|
||||||
|
i = i + 1
|
||||||
|
elseif c == '\r' or c == '\n' then
|
||||||
|
table.insert(fields, table.concat(field))
|
||||||
|
field = {}
|
||||||
|
table.insert(rows, fields)
|
||||||
|
fields = {}
|
||||||
|
state = STATE_DEFAULT
|
||||||
|
if c == '\r' and i < len and csv:sub(i + 1, i + 1) == '\n' then
|
||||||
|
i = i + 2
|
||||||
|
else
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
state = STATE_DEFAULT
|
||||||
|
-- Don't increment i, reprocess character in DEFAULT state
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #field > 0 or #fields > 0 then
|
||||||
|
table.insert(fields, table.concat(field))
|
||||||
|
table.insert(rows, fields)
|
||||||
|
end
|
||||||
|
|
||||||
|
return rows
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Converts a table of rows back to CSV text format (RFC 4180 compliant).
|
||||||
|
---
|
||||||
|
--- Requirements:
|
||||||
|
--- - Input is a table (array) of rows, where each row is a table (array) of field values.
|
||||||
|
--- - Field values are converted to strings using tostring().
|
||||||
|
--- - Fields are quoted if they contain commas, newlines, or double quotes.
|
||||||
|
--- - Double quotes inside quoted fields are doubled ("").
|
||||||
|
--- - Fields are joined with commas; rows are joined with newlines.
|
||||||
|
---
|
||||||
|
--- @param rows table Array of rows, where each row is an array of field values.
|
||||||
|
--- @return string CSV-formatted text.
|
||||||
|
function toCSV(rows)
|
||||||
|
local rowStrings = {}
|
||||||
|
|
||||||
|
for _, row in ipairs(rows) do
|
||||||
|
local fieldStrings = {}
|
||||||
|
|
||||||
|
for _, field in ipairs(row) do
|
||||||
|
local fieldStr = tostring(field)
|
||||||
|
local needsQuoting = false
|
||||||
|
|
||||||
|
if fieldStr:find(',') or fieldStr:find('\n') or fieldStr:find('\r') or fieldStr:find('"') then
|
||||||
|
needsQuoting = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if needsQuoting then
|
||||||
|
fieldStr = fieldStr:gsub('"', '""')
|
||||||
|
fieldStr = '"' .. fieldStr .. '"'
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(fieldStrings, fieldStr)
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(rowStrings, table.concat(fieldStrings, ','))
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(rowStrings, '\n')
|
||||||
|
end
|
||||||
|
|
||||||
-- String to number conversion helper
|
-- String to number conversion helper
|
||||||
function num(str)
|
function num(str)
|
||||||
return tonumber(str) or 0
|
return tonumber(str) or 0
|
||||||
@@ -501,8 +638,8 @@ func EvalRegex(L *lua.LState) int {
|
|||||||
if len(matches) > 0 {
|
if len(matches) > 0 {
|
||||||
matchesTable := L.NewTable()
|
matchesTable := L.NewTable()
|
||||||
for i, match := range matches {
|
for i, match := range matches {
|
||||||
matchesTable.RawSetString(fmt.Sprintf("%d", i), lua.LString(match))
|
matchesTable.RawSetInt(i+1, lua.LString(match))
|
||||||
evalRegexLogger.Debug("Set table[%d] = %q", i, match)
|
evalRegexLogger.Debug("Set table[%d] = %q", i+1, match)
|
||||||
}
|
}
|
||||||
L.Push(matchesTable)
|
L.Push(matchesTable)
|
||||||
} else {
|
} else {
|
||||||
@@ -519,25 +656,27 @@ func GetLuaFunctionsHelp() string {
|
|||||||
return `Lua Functions Available in Global Environment:
|
return `Lua Functions Available in Global Environment:
|
||||||
|
|
||||||
MATH FUNCTIONS:
|
MATH FUNCTIONS:
|
||||||
min(a, b) - Returns the minimum of two numbers
|
min(a, b) - Returns the minimum of two numbers
|
||||||
max(a, b) - Returns the maximum of two numbers
|
max(a, b) - Returns the maximum of two numbers
|
||||||
round(x, n) - Rounds x to n decimal places (default 0)
|
round(x, n) - Rounds x to n decimal places (default 0)
|
||||||
floor(x) - Returns the floor of x
|
floor(x) - Returns the floor of x
|
||||||
ceil(x) - Returns the ceiling of x
|
ceil(x) - Returns the ceiling of x
|
||||||
|
|
||||||
STRING FUNCTIONS:
|
STRING FUNCTIONS:
|
||||||
upper(s) - Converts string to uppercase
|
upper(s) - Converts string to uppercase
|
||||||
lower(s) - Converts string to lowercase
|
lower(s) - Converts string to lowercase
|
||||||
format(s, ...) - Formats string using Lua string.format
|
format(s, ...) - Formats string using Lua string.format
|
||||||
trim(s) - Removes leading/trailing whitespace
|
trim(s) - Removes leading/trailing whitespace
|
||||||
strsplit(inputstr, sep) - Splits string by separator (default: whitespace)
|
strsplit(inputstr, sep) - Splits string by separator (default: whitespace)
|
||||||
num(str) - Converts string to number (returns 0 if invalid)
|
fromCSV(csv) - Parses CSV text into rows of fields
|
||||||
str(num) - Converts number to string
|
toCSV(rows) - Converts table of rows to CSV text format
|
||||||
is_number(str) - Returns true if string is numeric
|
num(str) - Converts string to number (returns 0 if invalid)
|
||||||
|
str(num) - Converts number to string
|
||||||
|
is_number(str) - Returns true if string is numeric
|
||||||
|
|
||||||
TABLE FUNCTIONS:
|
TABLE FUNCTIONS:
|
||||||
DumpTable(table, depth) - Prints table structure recursively
|
dump(table, depth) - Prints table structure recursively
|
||||||
isArray(t) - Returns true if table is a sequential array
|
isArray(t) - Returns true if table is a sequential array
|
||||||
|
|
||||||
HTTP FUNCTIONS:
|
HTTP FUNCTIONS:
|
||||||
fetch(url, options) - Makes HTTP request, returns response table
|
fetch(url, options) - Makes HTTP request, returns response table
|
||||||
@@ -552,12 +691,12 @@ UTILITY FUNCTIONS:
|
|||||||
print(...) - Prints arguments to Go logger
|
print(...) - Prints arguments to Go logger
|
||||||
|
|
||||||
EXAMPLES:
|
EXAMPLES:
|
||||||
round(3.14159, 2) -> 3.14
|
round(3.14159, 2) -> 3.14
|
||||||
strsplit("a,b,c", ",") -> {"a", "b", "c"}
|
strsplit("a,b,c", ",") -> {"a", "b", "c"}
|
||||||
upper("hello") -> "HELLO"
|
upper("hello") -> "HELLO"
|
||||||
min(5, 3) -> 3
|
min(5, 3) -> 3
|
||||||
num("123") -> 123
|
num("123") -> 123
|
||||||
is_number("abc") -> false
|
is_number("abc") -> false
|
||||||
fetch("https://api.example.com/data")
|
fetch("https://api.example.com/data")
|
||||||
re("(\\w+)@(\\w+)", "user@domain.com") -> {"user@domain.com", "user", "domain.com"}`
|
re("(\\w+)@(\\w+)", "user@domain.com") -> {"user@domain.com", "user", "domain.com"}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package processor_test
|
package processor_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -30,8 +29,8 @@ func TestEvalRegex_CaptureGroupsReturned(t *testing.T) {
|
|||||||
}
|
}
|
||||||
expected := []string{"test-42", "test", "42"}
|
expected := []string{"test-42", "test", "42"}
|
||||||
for i, v := range expected {
|
for i, v := range expected {
|
||||||
val := tbl.RawGetString(fmt.Sprintf("%d", i))
|
val := tbl.RawGetInt(i + 1)
|
||||||
assert.Equal(t, lua.LString(v), val, "Expected index %d to be %q", i, v)
|
assert.Equal(t, lua.LString(v), val, "Expected index %d to be %q", i+1, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,9 +66,9 @@ func TestEvalRegex_NoCaptureGroups(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("Expected Lua table, got %T", out)
|
t.Fatalf("Expected Lua table, got %T", out)
|
||||||
}
|
}
|
||||||
fullMatch := tbl.RawGetString("0")
|
fullMatch := tbl.RawGetInt(1)
|
||||||
assert.Equal(t, lua.LString("foo123"), fullMatch)
|
assert.Equal(t, lua.LString("foo123"), fullMatch)
|
||||||
// There should be only the full match (index 0)
|
// There should be only the full match (index 1)
|
||||||
count := 0
|
count := 0
|
||||||
tbl.ForEach(func(k, v lua.LValue) {
|
tbl.ForEach(func(k, v lua.LValue) {
|
||||||
count++
|
count++
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func ProcessRegex(content string, command utils.ModifyCommand, filename string)
|
|||||||
processRegexLogger.Error("Error compiling pattern %q: %v", pattern, err)
|
processRegexLogger.Error("Error compiling pattern %q: %v", pattern, err)
|
||||||
return commands, fmt.Errorf("error compiling pattern: %v", err)
|
return commands, fmt.Errorf("error compiling pattern: %v", err)
|
||||||
}
|
}
|
||||||
processRegexLogger.Debug("Compiled pattern successfully in %v", time.Since(patternCompileStart))
|
processRegexLogger.Debug("Compiled pattern successfully in %v. Pattern: %s", time.Since(patternCompileStart), pattern)
|
||||||
|
|
||||||
// Same here, it's just string concatenation, it won't kill us
|
// Same here, it's just string concatenation, it won't kill us
|
||||||
// More important is that we don't fuck up the command
|
// More important is that we don't fuck up the command
|
||||||
@@ -77,7 +77,7 @@ func ProcessRegex(content string, command utils.ModifyCommand, filename string)
|
|||||||
processRegexLogger.Debug("Pattern complexity estimate: %d", patternComplexity)
|
processRegexLogger.Debug("Pattern complexity estimate: %d", patternComplexity)
|
||||||
|
|
||||||
if len(indices) == 0 {
|
if len(indices) == 0 {
|
||||||
processRegexLogger.Warning("No matches found for regex: %q", pattern)
|
processRegexLogger.Warning("No matches found for regex: %s", pattern)
|
||||||
processRegexLogger.Debug("Total regex processing time: %v", time.Since(startTime))
|
processRegexLogger.Debug("Total regex processing time: %v", time.Since(startTime))
|
||||||
return commands, nil
|
return commands, nil
|
||||||
}
|
}
|
||||||
@@ -335,6 +335,9 @@ func resolveRegexPlaceholders(pattern string) string {
|
|||||||
pattern = strings.ReplaceAll(pattern, "!any", `.*?`)
|
pattern = strings.ReplaceAll(pattern, "!any", `.*?`)
|
||||||
resolveLogger.Debug("Replaced !any with non-greedy wildcard")
|
resolveLogger.Debug("Replaced !any with non-greedy wildcard")
|
||||||
|
|
||||||
|
pattern = strings.ReplaceAll(pattern, "\n", "\r?\n")
|
||||||
|
resolveLogger.Debug("Added optional carriage return support for Windows line endings")
|
||||||
|
|
||||||
repPattern := regexp.MustCompile(`!rep\(([^,]+),\s*(\d+)\)`)
|
repPattern := regexp.MustCompile(`!rep\(([^,]+),\s*(\d+)\)`)
|
||||||
// !rep(pattern, count) repeats the pattern n times
|
// !rep(pattern, count) repeats the pattern n times
|
||||||
// Inserting !any between each repetition
|
// Inserting !any between each repetition
|
||||||
|
|||||||
Reference in New Issue
Block a user