• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lunarmodules / Penlight / 3796977032

pending completion
3796977032

push

github

GitHub
enhance(func) Extend compose to suport N functions (#448)

5 of 5 new or added lines in 1 file covered. (100.0%)

5277 of 6020 relevant lines covered (87.66%)

45.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.81
/lua/pl/sip.lua
1
--- Simple Input Patterns (SIP).
2
-- SIP patterns start with '$', then a
3
-- one-letter type, and then an optional variable in curly braces.
4
--
5
--    sip.match('$v=$q','name="dolly"',res)
6
--    ==> res=={'name','dolly'}
7
--    sip.match('($q{first},$q{second})','("john","smith")',res)
8
--    ==> res=={second='smith',first='john'}
9
--
10
-- Type names:
11
--
12
--    v     identifier
13
--    i     integer
14
--    f     floating-point
15
--    q     quoted string
16
--    ([{<  match up to closing bracket
17
--
18
-- See @{08-additional.md.Simple_Input_Patterns|the Guide}
19
--
20
-- @module pl.sip
21

22
local loadstring = rawget(_G,'loadstring') or load
2✔
23
local unpack = rawget(_G,'unpack') or rawget(table,'unpack')
2✔
24

25
local append,concat = table.insert,table.concat
2✔
26
local ipairs,type = ipairs,type
2✔
27
local io,_G = io,_G
2✔
28
local print,rawget = print,rawget
2✔
29

30
local patterns = {
2✔
31
    FLOAT = '[%+%-%d]%d*%.?%d*[eE]?[%+%-]?%d*',
32
    INTEGER = '[+%-%d]%d*',
33
    IDEN = '[%a_][%w_]*',
34
    OPTION = '[%a_][%w_%-]*',
35
}
36

37
local function assert_arg(idx,val,tp)
38
    if type(val) ~= tp then
1,876✔
39
        error("argument "..idx.." must be "..tp, 2)
×
40
    end
41
end
42

43
local sip = {}
2✔
44

45
local brackets = {['<'] = '>', ['('] = ')', ['{'] = '}', ['['] = ']' }
2✔
46
local stdclasses = {a=1,c=0,d=1,l=1,p=0,u=1,w=1,x=1,s=0}
2✔
47

48
local function group(s)
49
    return '('..s..')'
8✔
50
end
51

52
-- escape all magic characters except $, which has special meaning
53
-- Also, un-escape any characters after $, so $( and $[ passes through as is.
54
local function escape (spec)
55
    return (spec:gsub('[%-%.%+%[%]%(%)%^%%%?%*]','%%%0'):gsub('%$%%(%S)','$%1'))
28✔
56
end
57

58
-- Most spaces within patterns can match zero or more spaces.
59
-- Spaces between alphanumeric characters or underscores or between
60
-- patterns that can match these characters, however, must match at least
61
-- one space. Otherwise '$v $v' would match 'abcd' as {'abc', 'd'}.
62
-- This function replaces continuous spaces within a pattern with either
63
-- '%s*' or '%s+' according to this rule. The pattern has already
64
-- been stripped of pattern names by now.
65
local function compress_spaces(patt)
66
    return (patt:gsub("()%s+()", function(i1, i2)
56✔
67
        local before = patt:sub(i1 - 2, i1 - 1)
32✔
68
        if before:match('%$[vifadxlu]') or before:match('^[^%$]?[%w_]$') then
32✔
69
            local after = patt:sub(i2, i2 + 1)
25✔
70
            if after:match('%$[vifadxlu]') or after:match('^[%w_]') then
25✔
71
                return '%s+'
16✔
72
            end
73
        end
74
        return '%s*'
16✔
75
    end))
76
end
77

78
local pattern_map = {
2✔
79
  v = group(patterns.IDEN),
4✔
80
  i = group(patterns.INTEGER),
4✔
81
  f = group(patterns.FLOAT),
4✔
82
  o = group(patterns.OPTION),
4✔
83
  r = '(%S.*)',
84
  p = '([%a]?[:]?[\\/%.%w_]+)'
×
85
}
86

87
function sip.custom_pattern(flag,patt)
2✔
88
    pattern_map[flag] = patt
1✔
89
end
90

91
--- convert a SIP pattern into the equivalent Lua string pattern.
92
-- @param spec a SIP pattern
93
-- @param options a table; only the <code>at_start</code> field is
94
-- currently meaningful and ensures that the pattern is anchored
95
-- at the start of the string.
96
-- @return a Lua string pattern.
97
function sip.create_pattern (spec,options)
2✔
98
    assert_arg(1,spec,'string')
28✔
99
    local fieldnames,fieldtypes = {},{}
28✔
100

101
    if type(spec) == 'string' then
28✔
102
        spec = escape(spec)
56✔
103
    else
104
        local res = {}
×
105
        for i,s in ipairs(spec) do
×
106
            res[i] = escape(s)
×
107
        end
108
        spec = concat(res,'.-')
×
109
    end
110

111
    local kount = 1
28✔
112

113
    local function addfield (name,type)
114
        name = name or kount
50✔
115
        append(fieldnames,name)
50✔
116
        fieldtypes[name] = type
50✔
117
        kount = kount + 1
50✔
118
    end
119

120
    local named_vars = spec:find('{%a+}')
28✔
121

122
    if options and options.at_start then
28✔
123
        spec = '^'..spec
10✔
124
    end
125
    if spec:sub(-1,-1) == '$' then
56✔
126
        spec = spec:sub(1,-2)..'$r'
16✔
127
        if named_vars then spec = spec..'{rest}' end
8✔
128
    end
129

130
    local names
131

132
    if named_vars then
28✔
133
        names = {}
16✔
134
        spec = spec:gsub('{(%a+)}',function(name)
32✔
135
            append(names,name)
33✔
136
            return ''
33✔
137
        end)
138
    end
139
    spec = compress_spaces(spec)
56✔
140

141
    local k = 1
28✔
142
    local err
143
    local r = (spec:gsub('%$%S',function(s)
56✔
144
        local type,name
145
        type = s:sub(2,2)
98✔
146
        if names then name = names[k]; k=k+1 end
49✔
147
        -- this kludge is necessary because %q generates two matches, and
148
        -- we want to ignore the first. Not a problem for named captures.
149
        if not names and type == 'q' then
49✔
150
            addfield(nil,'Q')
×
151
        else
152
            addfield(name,type)
49✔
153
        end
154
        local res
155
        if pattern_map[type] then
49✔
156
            res = pattern_map[type]
29✔
157
        elseif type == 'q' then
20✔
158
            -- some Lua pattern matching voodoo; we want to match '...' as
159
            -- well as "...", and can use the fact that %n will match a
160
            -- previous capture. Adding the extra field above comes from needing
161
            -- to accommodate the extra spurious match (which is either ' or ")
162
            addfield(name,type)
1✔
163
            res = '(["\'])(.-)%'..(kount-2)
1✔
164
        else
165
            local endbracket = brackets[type]
19✔
166
            if endbracket then
19✔
167
                res = '(%b'..type..endbracket..')'
3✔
168
            elseif stdclasses[type] or stdclasses[type:lower()] then
19✔
169
                res = '(%'..type..'+)'
16✔
170
            else
171
                err = "unknown format type or character class"
×
172
            end
173
        end
174
        return res
49✔
175
    end))
176

177
    if err then
28✔
178
        return nil,err
×
179
    else
180
        return r,fieldnames,fieldtypes
28✔
181
    end
182
end
183

184

185
local function tnumber (s)
186
    return s == 'd' or s == 'i' or s == 'f'
50✔
187
end
188

189
function sip.create_spec_fun(spec,options)
2✔
190
    local fieldtypes,fieldnames
191
    local ls = {}
28✔
192
    spec,fieldnames,fieldtypes = sip.create_pattern(spec,options)
56✔
193
    if not spec then return spec,fieldnames end
28✔
194
    local named_vars = type(fieldnames[1]) == 'string'
28✔
195
    for i = 1,#fieldnames do
78✔
196
        append(ls,'mm'..i)
50✔
197
    end
198
    ls[1] = ls[1] or "mm1" -- behave correctly if there are no patterns
28✔
199
    local fun = ('return (function(s,res)\n\tlocal %s = s:match(%q)\n'):format(concat(ls,','),spec)
28✔
200
    fun = fun..'\tif not mm1 then return false end\n'
28✔
201
    local k=1
28✔
202
    for i,f in ipairs(fieldnames) do
78✔
203
        if f ~= '_' then
50✔
204
            local var = 'mm'..i
50✔
205
            if tnumber(fieldtypes[f]) then
100✔
206
                var = 'tonumber('..var..')'
18✔
207
            elseif brackets[fieldtypes[f]] then
32✔
208
                var = var..':sub(2,-2)'
3✔
209
            end
210
            if named_vars then
50✔
211
                fun = ('%s\tres.%s = %s\n'):format(fun,f,var)
34✔
212
            else
213
                if fieldtypes[f] ~= 'Q' then -- we skip the string-delim capture
16✔
214
                    fun = ('%s\tres[%d] = %s\n'):format(fun,k,var)
16✔
215
                    k = k + 1
16✔
216
                end
217
            end
218
        end
219
    end
220
    return fun..'\treturn true\nend)\n', named_vars
28✔
221
end
222

223
--- convert a SIP pattern into a matching function.
224
-- The returned function takes two arguments, the line and an empty table.
225
-- If the line matched the pattern, then this function returns true
226
-- and the table is filled with field-value pairs.
227
-- @param spec a SIP pattern
228
-- @param options optional table; {at_start=true} ensures that the pattern
229
-- is anchored at the start of the string.
230
-- @return a function if successful, or nil,error
231
function sip.compile(spec,options)
2✔
232
    assert_arg(1,spec,'string')
28✔
233
    local fun,names = sip.create_spec_fun(spec,options)
28✔
234
    if not fun then return nil,names end
28✔
235
    if rawget(_G,'_DEBUG') then print(fun) end
28✔
236
    local chunk,err = loadstring(fun,'tmp')
28✔
237
    if err then return nil,err end
28✔
238
    return chunk(),names
56✔
239
end
240

241
local cache = {}
2✔
242

243
--- match a SIP pattern against a string.
244
-- @param spec a SIP pattern
245
-- @param line a string
246
-- @param res a table to receive values
247
-- @param options (optional) option table
248
-- @return true or false
249
function sip.match (spec,line,res,options)
2✔
250
    assert_arg(1,spec,'string')
606✔
251
    assert_arg(2,line,'string')
606✔
252
    assert_arg(3,res,'table')
606✔
253
    if not cache[spec] then
606✔
254
        cache[spec] = sip.compile(spec,options)
20✔
255
    end
256
    return cache[spec](line,res)
606✔
257
end
258

259
--- match a SIP pattern against the start of a string.
260
-- @param spec a SIP pattern
261
-- @param line a string
262
-- @param res a table to receive values
263
-- @return true or false
264
function sip.match_at_start (spec,line,res)
2✔
265
    return sip.match(spec,line,res,{at_start=true})
606✔
266
end
267

268
--- given a pattern and a file object, return an iterator over the results
269
-- @param spec a SIP pattern
270
-- @param f a file-like object.
271
function sip.fields (spec,f)
2✔
272
    assert_arg(1,spec,'string')
×
273
    if not f then return nil,"no file object" end
×
274
    local fun,err = sip.compile(spec)
×
275
    if not fun then return nil,err end
×
276
    local res = {}
×
277
    return function()
278
        while true do
279
            local line = f:read()
×
280
            if not line then return end
×
281
            if fun(line,res) then
×
282
                local values = res
×
283
                res = {}
×
284
                return unpack(values)
×
285
            end
286
        end
287
    end
288
end
289

290
local read_patterns = {}
2✔
291

292
--- register a match which will be used in the read function.
293
-- @string spec a SIP pattern
294
-- @func fun a function to be called with the results of the match
295
-- @see read
296
function sip.pattern (spec,fun)
2✔
297
    assert_arg(1,spec,'string')
2✔
298
    local pat,named = sip.compile(spec)
2✔
299
    append(read_patterns,{pat=pat,named=named,callback=fun})
2✔
300
end
301

302
--- enter a loop which applies all registered matches to the input file.
303
-- @param f a file-like object
304
-- @array matches optional list of `{spec,fun}` pairs, as for `pattern` above.
305
function sip.read (f,matches)
2✔
306
    local owned,err
307
    if not f then return nil,"no file object" end
1✔
308
    if type(f) == 'string' then
1✔
309
        f,err = io.open(f)
×
310
        if not f then return nil,err end
×
311
        owned = true
×
312
    end
313
    if matches then
1✔
314
        for _,p in ipairs(matches) do
3✔
315
            sip.pattern(p[1],p[2])
2✔
316
        end
317
    end
318
    local res = {}
1✔
319
    for line in f:lines() do
7✔
320
        for _,item in ipairs(read_patterns) do
3✔
321
            if item.pat(line,res) then
6✔
322
                if item.callback then
2✔
323
                    if item.named then
2✔
324
                        item.callback(res)
×
325
                    else
326
                        item.callback(unpack(res))
2✔
327
                    end
328
                end
329
                res = {}
2✔
330
                break
2✔
331
            end
332
        end
333
    end
334
    if owned then f:close() end
1✔
335
end
336

337
return sip
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc