3 Commits

Author SHA1 Message Date
a18573c9f8 Memoize the glob statement
Because we were doing the same work many times :(
2025-12-02 16:50:39 +01:00
eacc92ce4b Rename GLob to glob 2025-12-02 16:36:11 +01:00
3bcc958dda Remove the error return value and instead just throw error 2025-11-15 18:10:24 +01:00
6 changed files with 122 additions and 112 deletions

View File

@@ -82,7 +82,7 @@ func TestGlobExpansion(t *testing.T) {
for _, pattern := range tc.patterns {
patternMap[pattern] = struct{}{}
}
files, err := utils.ExpandGLobs(patternMap)
files, err := utils.ExpandGlobs(patternMap)
if err != nil {
t.Fatalf("ExpandGLobs failed: %v", err)
}

View File

@@ -243,7 +243,7 @@ func runModifier(args []string, cmd *cobra.Command) {
// Resolve all the files for all the globs
mainLogger.Info("Found %d unique file patterns", len(globs))
mainLogger.Debug("Expanding glob patterns to files")
files, err := utils.ExpandGLobs(globs)
files, err := utils.ExpandGlobs(globs)
if err != nil {
mainLogger.Error("Failed to expand file patterns: %v", err)
return

View File

@@ -18,26 +18,23 @@ end
-- Test fromCSV option validation
test("fromCSV invalid option", function()
local csv = "a,b,c\n1,2,3"
local rows, err = fromCSV(csv, { invalidOption = true })
assert(rows ~= nil and #rows == 0, "Should return empty table on error")
assert(err ~= nil, "Should return error message")
assert(string.find(err, "unknown option"), "Error should mention unknown option")
local ok, errMsg = pcall(function() fromCSV(csv, { invalidOption = true }) end)
assert(ok == false, "Should raise error")
assert(string.find(errMsg, "unknown option"), "Error should mention unknown option")
end)
-- Test toCSV error handling
-- Test toCSV invalid delimiter
test("toCSV invalid delimiter", function()
local rows = { { "a", "b", "c" } }
local csv, err = toCSV(rows, { delimiter = 123 })
local csv = toCSV(rows, { delimiter = 123 })
-- toCSV converts delimiter to string, so 123 becomes "123"
assert(csv == "a123b123c", "Should convert delimiter to string")
assert(err == nil, "Should not return error")
end)
-- Test fromCSV basic parsing
test("fromCSV basic", function()
local csv = "a,b,c\n1,2,3\n4,5,6"
local rows, err = fromCSV(csv)
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv)
assert(#rows == 3, "Should have 3 rows")
assert(rows[1][1] == "a", "First row first field should be 'a'")
assert(rows[2][2] == "2", "Second row second field should be '2'")
@@ -46,8 +43,7 @@ end)
-- Test fromCSV with headers
test("fromCSV with headers", function()
local csv = "foo,bar,baz\n1,2,3\n4,5,6"
local rows, err = fromCSV(csv, { hasheader = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hasheader = true })
assert(#rows == 2, "Should have 2 data rows")
assert(rows[1][1] == "1", "First row first field should be '1'")
assert(rows[1].foo == "1", "First row foo should be '1'")
@@ -58,8 +54,7 @@ end)
-- Test fromCSV with custom delimiter
test("fromCSV with tab delimiter", function()
local csv = "a\tb\tc\n1\t2\t3"
local rows, err = fromCSV(csv, { delimiter = "\t" })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { delimiter = "\t" })
assert(#rows == 2, "Should have 2 rows")
assert(rows[1][1] == "a", "First row first field should be 'a'")
assert(rows[2][2] == "2", "Second row second field should be '2'")
@@ -68,8 +63,7 @@ end)
-- Test fromCSV with quoted fields
test("fromCSV with quoted fields", function()
local csv = '"hello,world","test"\n"foo","bar"'
local rows, err = fromCSV(csv)
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv)
assert(#rows == 2, "Should have 2 rows")
assert(rows[1][1] == "hello,world", "Quoted field with comma should be preserved")
assert(rows[1][2] == "test", "Second field should be 'test'")
@@ -78,44 +72,37 @@ end)
-- Test toCSV basic
test("toCSV basic", function()
local rows = { { "a", "b", "c" }, { "1", "2", "3" } }
local csv, err = toCSV(rows)
if err then error("toCSV error: " .. err) end
local csv = toCSV(rows)
assert(csv == "a,b,c\n1,2,3", "CSV output should match expected")
end)
-- Test toCSV with custom delimiter
test("toCSV with tab delimiter", function()
local rows = { { "a", "b", "c" }, { "1", "2", "3" } }
local csv, err = toCSV(rows, { delimiter = "\t" })
if err then error("toCSV error: " .. err) end
local csv = toCSV(rows, { delimiter = "\t" })
assert(csv == "a\tb\tc\n1\t2\t3", "TSV output should match expected")
end)
-- Test toCSV with fields needing quoting
test("toCSV with quoted fields", function()
local rows = { { "hello,world", "test" }, { "foo", "bar" } }
local csv, err = toCSV(rows)
if err then error("toCSV error: " .. err) end
local csv = toCSV(rows)
assert(csv == '"hello,world",test\nfoo,bar', "Fields with commas should be quoted")
end)
-- Test round trip
test("fromCSV toCSV round trip", function()
local original = "a,b,c\n1,2,3\n4,5,6"
local rows, err = fromCSV(original)
if err then error("fromCSV error: " .. err) end
local csv, err = toCSV(rows)
if err then error("toCSV error: " .. err) end
local rows = fromCSV(original)
local csv = toCSV(rows)
assert(csv == original, "Round trip should preserve original")
end)
-- Test round trip with headers
test("fromCSV toCSV round trip with headers", function()
local original = "foo,bar,baz\n1,2,3\n4,5,6"
local rows, err = fromCSV(original, { hasheader = true })
if err then error("fromCSV error: " .. err) end
local csv, err = toCSV(rows)
if err then error("toCSV error: " .. err) end
local rows = fromCSV(original, { hasheader = true })
local csv = toCSV(rows)
local expected = "1,2,3\n4,5,6"
assert(csv == expected, "Round trip with headers should preserve data rows")
end)
@@ -123,8 +110,7 @@ end)
-- Test fromCSV with comments
test("fromCSV with comments", function()
local csv = "# This is a comment\nfoo,bar,baz\n1,2,3\n# Another comment\n4,5,6"
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 3, "Should have 3 rows (comments filtered, header + 2 data rows)")
assert(rows[1][1] == "foo", "First row should be header row")
assert(rows[2][1] == "1", "Second row first field should be '1'")
@@ -134,8 +120,7 @@ end)
-- Test fromCSV with comments and headers
test("fromCSV with comments and headers", function()
local csv = "#mercenary_profiles\nId,Name,Value\n1,Test,100\n# End of data\n2,Test2,200"
local rows, err = fromCSV(csv, { hasheader = true, hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hasheader = true, hascomments = true })
assert(#rows == 2, "Should have 2 data rows")
assert(rows[1].Id == "1", "First row Id should be '1'")
assert(rows[1].Name == "Test", "First row Name should be 'Test'")
@@ -146,8 +131,7 @@ end)
-- Test fromCSV with comments disabled
test("fromCSV without comments", function()
local csv = "# This should not be filtered\nfoo,bar\n1,2"
local rows, err = fromCSV(csv, { hascomments = false })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = false })
assert(#rows == 3, "Should have 3 rows (including comment)")
assert(rows[1][1] == "# This should not be filtered", "Comment line should be preserved")
end)
@@ -155,8 +139,7 @@ end)
-- Test fromCSV with comment at start
test("fromCSV comment at start", function()
local csv = "# Header comment\nId,Name\n1,Test"
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 2, "Should have 2 rows (comment filtered)")
assert(rows[1][1] == "Id", "First row should be header")
end)
@@ -164,8 +147,7 @@ end)
-- Test fromCSV with comment with leading whitespace
test("fromCSV comment with whitespace", function()
local csv = " # Comment with spaces\nId,Name\n1,Test"
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 2, "Should have 2 rows (comment with spaces filtered)")
assert(rows[1][1] == "Id", "First row should be header")
end)
@@ -173,8 +155,7 @@ end)
-- Test fromCSV with comment with tabs
test("fromCSV comment with tabs", function()
local csv = "\t# Comment with tab\nId,Name\n1,Test"
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 2, "Should have 2 rows (comment with tab filtered)")
assert(rows[1][1] == "Id", "First row should be header")
end)
@@ -182,8 +163,7 @@ end)
-- Test fromCSV with multiple consecutive comments
test("fromCSV multiple consecutive comments", function()
local csv = "# First comment\n# Second comment\n# Third comment\nId,Name\n1,Test"
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 2, "Should have 2 rows (all comments filtered)")
assert(rows[1][1] == "Id", "First row should be header")
end)
@@ -191,8 +171,7 @@ end)
-- Test fromCSV with comment in middle of data
test("fromCSV comment in middle", function()
local csv = "Id,Name\n1,Test\n# Middle comment\n2,Test2"
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 3, "Should have 3 rows (comment filtered)")
assert(rows[1][1] == "Id", "First row should be header")
assert(rows[2][1] == "1", "Second row should be first data")
@@ -202,8 +181,7 @@ end)
-- Test fromCSV with comment at end
test("fromCSV comment at end", function()
local csv = "Id,Name\n1,Test\n# End comment"
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 2, "Should have 2 rows (end comment filtered)")
assert(rows[1][1] == "Id", "First row should be header")
assert(rows[2][1] == "1", "Second row should be data")
@@ -212,8 +190,7 @@ end)
-- Test fromCSV with empty comment line
test("fromCSV empty comment", function()
local csv = "#\nId,Name\n1,Test"
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 2, "Should have 2 rows (empty comment filtered)")
assert(rows[1][1] == "Id", "First row should be header")
end)
@@ -221,8 +198,7 @@ end)
-- Test fromCSV with comment and headers
test("fromCSV comment with headers enabled", function()
local csv = "#mercenary_profiles\nId,Name,Value\n1,Test,100\n2,Test2,200"
local rows, err = fromCSV(csv, { hasheader = true, hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hasheader = true, hascomments = true })
assert(#rows == 2, "Should have 2 data rows")
assert(rows[1].Id == "1", "First row Id should be '1'")
assert(rows[1].Name == "Test", "First row Name should be 'Test'")
@@ -232,8 +208,7 @@ end)
-- Test fromCSV with comment and TSV delimiter
test("fromCSV comment with tab delimiter", function()
local csv = "# Comment\nId\tName\n1\tTest"
local rows, err = fromCSV(csv, { delimiter = "\t", hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { delimiter = "\t", hascomments = true })
assert(#rows == 2, "Should have 2 rows")
assert(rows[1][1] == "Id", "First row should be header")
assert(rows[2][1] == "1", "Second row first field should be '1'")
@@ -242,8 +217,7 @@ end)
-- Test fromCSV with comment and headers and TSV
test("fromCSV comment with headers and TSV", function()
local csv = "#mercenary_profiles\nId\tName\tValue\n1\tTest\t100"
local rows, err = fromCSV(csv, { delimiter = "\t", hasheader = true, hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { delimiter = "\t", hasheader = true, hascomments = true })
assert(#rows == 1, "Should have 1 data row")
assert(rows[1].Id == "1", "Row Id should be '1'")
assert(rows[1].Name == "Test", "Row Name should be 'Test'")
@@ -253,8 +227,7 @@ end)
-- Test fromCSV with data field starting with # (not a comment)
test("fromCSV data field starting with hash", function()
local csv = "Id,Name\n1,#NotAComment\n2,Test"
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 3, "Should have 3 rows (data with # not filtered)")
assert(rows[1][1] == "Id", "First row should be header")
assert(rows[2][2] == "#NotAComment", "Second row should have #NotAComment as data")
@@ -263,8 +236,7 @@ end)
-- Test fromCSV with quoted field starting with #
test("fromCSV quoted field with hash", function()
local csv = 'Id,Name\n1,"#NotAComment"\n2,Test'
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 3, "Should have 3 rows (quoted # not filtered)")
assert(rows[2][2] == "#NotAComment", "Quoted field with # should be preserved")
end)
@@ -272,8 +244,7 @@ end)
-- Test fromCSV with comment after quoted field
test("fromCSV comment after quoted field", function()
local csv = 'Id,Name\n1,"Test"\n# This is a comment\n2,Test2'
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 3, "Should have 3 rows (comment filtered)")
assert(rows[2][2] == "Test", "Quoted field should be preserved")
assert(rows[3][1] == "2", "Third row should be second data row")
@@ -414,8 +385,7 @@ Id ModifyStartCost ModifyStep ModifyLevelLimit Health ResistSheet WoundSlots Mel
john_hawkwood_boss 20 0.1 140 blunt 0 pierce 0 lacer 0 fire 0 cold 0 poison 0 shock 0 beam 0 HumanHead HumanShoulder HumanArm HumanThigh HumanFeet HumanChest HumanBody HumanStomach HumanKnee blunt 8 16 crit 1.60 critchance 0.05 0.5 0.5 0.03 0.5 1.2 0.3 8 2200 16 2 talent_the_man_who_sold_the_world human_male 0 hair1 #633D08 player Human
francis_reid_daly 20 0.1 130 blunt 0 pierce 0 lacer 0 fire 0 cold 0 poison 0 shock 0 beam 0 HumanHead HumanShoulder HumanArm HumanThigh HumanFeet HumanChest HumanBody HumanStomach HumanKnee blunt 7 14 crit 1.70 critchance 0.05 0.5 0.4 0.04 0.9 1 0.3 8 2000 10 1 talent_weapon_durability human_male 0 player Human
]]
local rows, err = fromCSV(teststr, { delimiter = "\t", hasheader = true, hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(teststr, { delimiter = "\t", hasheader = true, hascomments = true })
assert(#rows == 2, "Should have 2 data rows")
-- Test first row
@@ -440,8 +410,7 @@ end)
test("fromCSV debug header assignment", function()
local csv = "Id Name Value\n1 Test 100\n2 Test2 200"
local rows, err = fromCSV(csv, { delimiter = "\t", hasheader = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { delimiter = "\t", hasheader = true })
assert(rows[1].Id == "1", "Id should be '1'")
assert(rows[1].Name == "Test", "Name should be 'Test'")
assert(rows[1].Value == "100", "Value should be '100'")
@@ -453,8 +422,7 @@ Id ModifyStartCost ModifyStep ModifyLevelLimit Health ResistSheet WoundSlots Mel
john_hawkwood_boss 20 0.1 140 blunt 0 pierce 0 lacer 0 fire 0 cold 0 poison 0 shock 0 beam 0 HumanHead HumanShoulder HumanArm HumanThigh HumanFeet HumanChest HumanBody HumanStomach HumanKnee blunt 8 16 crit 1.60 critchance 0.05 0.5 0.5 0.03 0.5 1.2 0.3 8 2200 16 2 talent_the_man_who_sold_the_world human_male 0 hair1 #633D08 player Human
francis_reid_daly 20 0.1 130 blunt 0 pierce 0 lacer 0 fire 0 cold 0 poison 0 shock 0 beam 0 HumanHead HumanShoulder HumanArm HumanThigh HumanFeet HumanChest HumanBody HumanStomach HumanKnee blunt 7 14 crit 1.70 critchance 0.05 0.5 0.4 0.04 0.9 1 0.3 8 2000 10 1 talent_weapon_durability human_male 0 player Human
]]
local rows, err = fromCSV(csv, { delimiter = "\t", hasheader = true, hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { delimiter = "\t", hasheader = true, hascomments = true })
assert(#rows == 2, "Should have 2 data rows")
assert(rows[1].Id == "john_hawkwood_boss", "First row Id should be 'john_hawkwood_boss'")
@@ -491,17 +459,14 @@ phoenix_brigade 30 0.1 shielding_basic battle_physicist_basic reinforced_battery
]]
-- Parse with headers and comments
local rows, err = fromCSV(original, { delimiter = "\t", hasheader = true, hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(original, { delimiter = "\t", hasheader = true, hascomments = true })
assert(#rows > 0, "Should have parsed rows")
-- Convert back to CSV with headers
local csv, err = toCSV(rows, { delimiter = "\t", hasheader = true })
if err then error("toCSV error: " .. err) end
local csv = toCSV(rows, { delimiter = "\t", hasheader = true })
-- Parse again
local rows2, err = fromCSV(csv, { delimiter = "\t", hasheader = true, hascomments = false })
if err then error("fromCSV error: " .. err) end
local rows2 = fromCSV(csv, { delimiter = "\t", hasheader = true, hascomments = false })
-- Verify identical - same number of rows
assert(#rows2 == #rows, "Round trip should have same number of rows")
@@ -523,8 +488,7 @@ end)
-- Test metatable: row[1] and row.foobar return same value
test("metatable row[1] equals row.header", function()
local csv = "Id Name Value\n1 Test 100"
local rows, err = fromCSV(csv, { delimiter = "\t", hasheader = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { delimiter = "\t", hasheader = true })
assert(rows[1][1] == rows[1].Id, "row[1] should equal row.Id")
assert(rows[1][2] == rows[1].Name, "row[2] should equal row.Name")
assert(rows[1][3] == rows[1].Value, "row[3] should equal row.Value")
@@ -535,8 +499,7 @@ end)
-- Test metatable: setting via header name updates numeric index
test("metatable set via header name", function()
local csv = "Id Name Value\n1 Test 100"
local rows, err = fromCSV(csv, { delimiter = "\t", hasheader = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { delimiter = "\t", hasheader = true })
rows[1].Id = "999"
assert(rows[1][1] == "999", "Setting row.Id should update row[1]")
assert(rows[1].Id == "999", "row.Id should be '999'")
@@ -545,11 +508,8 @@ end)
-- Test metatable: error on unknown header assignment
test("metatable error on unknown header", function()
local csv = "Id Name Value\n1 Test 100"
local rows, err = fromCSV(csv, { delimiter = "\t", hasheader = true })
if err then error("fromCSV error: " .. err) end
local ok, errMsg = pcall(function()
rows[1].UnknownHeader = "test"
end)
local rows = fromCSV(csv, { delimiter = "\t", hasheader = true })
local ok, errMsg = pcall(function() rows[1].UnknownHeader = "test" end)
assert(ok == false, "Should error on unknown header")
assert(string.find(errMsg, "unknown header"), "Error should mention unknown header")
end)
@@ -557,8 +517,7 @@ end)
-- Test metatable: numeric indices still work
test("metatable numeric indices work", function()
local csv = "Id Name Value\n1 Test 100"
local rows, err = fromCSV(csv, { delimiter = "\t", hasheader = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { delimiter = "\t", hasheader = true })
rows[1][1] = "999"
assert(rows[1].Id == "999", "Setting row[1] should update row.Id")
assert(rows[1][1] == "999", "row[1] should be '999'")
@@ -567,8 +526,7 @@ end)
-- Test metatable: numeric keys work normally
test("metatable numeric keys work", function()
local csv = "Id Name Value\n1 Test 100"
local rows, err = fromCSV(csv, { delimiter = "\t", hasheader = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { delimiter = "\t", hasheader = true })
rows[1][100] = "hundred"
assert(rows[1][100] == "hundred", "Numeric keys should work")
end)

View File

@@ -58,12 +58,10 @@ parserDefaultOptions = { delimiter = ",", hasheader = false, hascomments = false
--- Validates options against a set of valid option keys.
--- @param options ParserOptions? The options table to validate
--- @return boolean #True if options are valid
--- @return string? #Error message if invalid, nil if valid
function areOptionsValid(options)
if options == nil then return true, nil end
if options == nil then return end
if type(options) ~= "table" then return false, "options must be a table" end
if type(options) ~= "table" then error("options must be a table") end
-- Build valid options list from validOptions table
local validOptionsStr = ""
@@ -73,12 +71,11 @@ function areOptionsValid(options)
for k, _ in pairs(options) do
if parserDefaultOptions[k] == nil then
return false,
error(
"unknown option: " .. tostring(k) .. " (valid options: " .. validOptionsStr .. ")"
)
end
end
return true, nil
end
--- Parses CSV text into rows and fields using a minimal RFC 4180 state machine.
@@ -99,13 +96,11 @@ end
--- @param csv string The CSV text to parse.
--- @param options ParserOptions? Options for the parser
--- @return table #A table (array) of rows; each row is a table with numeric indices and optionally header-named keys.
--- @return string? #Error message if parsing fails.
function fromCSV(csv, options)
if options == nil then options = {} end
-- Validate options
local isValid, err = areOptionsValid(options)
if not isValid then return {}, err end
areOptionsValid(options)
local delimiter = options.delimiter or parserDefaultOptions.delimiter
local hasheader = options.hasheader or parserDefaultOptions.hasheader
@@ -237,7 +232,7 @@ function fromCSV(csv, options)
else
rawset(t, key, value)
end
end
end,
}
local rows = {}
@@ -251,10 +246,10 @@ function fromCSV(csv, options)
table.insert(rows, row)
end
rows.Headers = headers
return rows, nil
return rows
end
return allRows, nil
return allRows
end
--- Converts a table of rows back to CSV text format (RFC 4180 compliant).
@@ -269,14 +264,12 @@ end
---
--- @param rows table Array of rows, where each row is an array of field values.
--- @param options ParserOptions? Options for the parser
--- @return string? #CSV-formatted text, error string?
--- @return string? #Error message if conversion fails.
--- @return string #CSV-formatted text
function toCSV(rows, options)
if options == nil then options = {} end
-- Validate options
local isValid, err = areOptionsValid(options)
if not isValid then return nil, err end
areOptionsValid(options)
local delimiter = options.delimiter or parserDefaultOptions.delimiter
local includeHeaders = options.hasheader or parserDefaultOptions.hasheader
@@ -332,7 +325,7 @@ function toCSV(rows, options)
table.insert(rowStrings, table.concat(fieldStrings, delimiter))
end
return table.concat(rowStrings, "\n"), nil
return table.concat(rowStrings, "\n")
end
-- String to number conversion helper

View File

@@ -7,8 +7,8 @@ import (
"strings"
logger "git.site.quack-lab.dev/dave/cylogger"
"github.com/bmatcuk/doublestar/v4"
"github.com/BurntSushi/toml"
"github.com/bmatcuk/doublestar/v4"
"gopkg.in/yaml.v3"
)
@@ -62,6 +62,7 @@ func (c *ModifyCommand) Validate() error {
// Ehh.. Not much better... Guess this wasn't the big deal
var matchesMemoTable map[string]bool = make(map[string]bool)
var globMemoTable map[string][]string = make(map[string][]string)
func Matches(path string, glob string) (bool, error) {
matchesLogger := modifyCommandLogger.WithPrefix("Matches").WithField("path", path).WithField("glob", glob)
@@ -208,7 +209,7 @@ func AggregateGlobs(commands []ModifyCommand) map[string]struct{} {
return globs
}
func ExpandGLobs(patterns map[string]struct{}) ([]string, error) {
func ExpandGlobs(patterns map[string]struct{}) ([]string, error) {
expandGlobsLogger := modifyCommandLogger.WithPrefix("ExpandGLobs")
expandGlobsLogger.Debug("Expanding glob patterns to actual files")
expandGlobsLogger.Trace("Input patterns for expansion: %v", patterns)
@@ -225,10 +226,16 @@ func ExpandGLobs(patterns map[string]struct{}) ([]string, error) {
for pattern := range patterns {
expandGlobsLogger.Debug("Processing glob pattern: %q", pattern)
static, pattern := SplitPattern(pattern)
matches, err := doublestar.Glob(os.DirFS(static), pattern)
if err != nil {
expandGlobsLogger.Warning("Error expanding glob %q in %q: %v", pattern, static, err)
continue
key := static + "|" + pattern
matches, ok := globMemoTable[key]
if !ok {
var err error
matches, err = doublestar.Glob(os.DirFS(static), pattern)
if err != nil {
expandGlobsLogger.Warning("Error expanding glob %q in %q: %v", pattern, static, err)
continue
}
globMemoTable[key] = matches
}
expandGlobsLogger.Debug("Found %d matches for pattern %q", len(matches), pattern)
expandGlobsLogger.Trace("Raw matches for pattern %q: %v", pattern, matches)

View File

@@ -705,6 +705,58 @@ func TestLoadCommandsFromCookFilesNoYamlFiles(t *testing.T) {
// }
// }
func TestExpandGlobsMemoization(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "expand-globs-memo-test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
err = os.WriteFile(filepath.Join(tmpDir, "test1.go"), []byte("test"), 0644)
if err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
err = os.WriteFile(filepath.Join(tmpDir, "test2.go"), []byte("test"), 0644)
if err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
origDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(origDir)
cwd, _ := os.Getwd()
resolvedCwd := ResolvePath(cwd)
pattern1 := resolvedCwd + "/*.go"
patterns := map[string]struct{}{pattern1: {}}
globMemoTable = make(map[string][]string)
files1, err := ExpandGlobs(patterns)
if err != nil {
t.Fatalf("ExpandGlobs failed: %v", err)
}
if len(files1) != 2 {
t.Fatalf("Expected 2 files, got %d", len(files1))
}
if len(globMemoTable) != 1 {
t.Fatalf("Expected 1 entry in memo table, got %d", len(globMemoTable))
}
files2, err := ExpandGlobs(patterns)
if err != nil {
t.Fatalf("ExpandGlobs failed: %v", err)
}
if len(files2) != 2 {
t.Fatalf("Expected 2 files, got %d", len(files2))
}
if len(globMemoTable) != 1 {
t.Fatalf("Expected memo table to still have 1 entry, got %d", len(globMemoTable))
}
}
// LoadCommandsFromCookFile returns an error for a malformed YAML file
// func TestLoadCommandsFromCookFilesMalformedYAML(t *testing.T) {
// // Setup test directory with mock YAML files