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

lunarmodules / Penlight / 11721790512

07 Nov 2024 11:09AM UTC coverage: 88.871% (-0.8%) from 89.677%
11721790512

push

github

web-flow
fix(callable): __call cannot be in a nested metatable (#489)

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

61 existing lines in 5 files now uncovered.

5454 of 6137 relevant lines covered (88.87%)

211.96 hits per line

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

84.96
/lua/pl/app.lua
1
--- Application support functions.
2
-- See @{01-introduction.md.Application_Support|the Guide}
3
--
4
-- Dependencies: `pl.utils`, `pl.path`
5
-- @module pl.app
6

7
local io,package,require = _G.io, _G.package, _G.require
30✔
8
local utils = require 'pl.utils'
30✔
9
local path = require 'pl.path'
30✔
10

11
local app = {}
30✔
12

13
--- return the name of the current script running.
14
-- The name will be the name as passed on the command line
15
-- @return string filename
16
function app.script_name()
30✔
17
    if _G.arg and _G.arg[0] then
20✔
18
        return _G.arg[0]
20✔
19
    end
20
    return utils.raise("No script name found")
×
21
end
22

23
--- prefixes the current script's path to the Lua module path.
24
-- Applies to both the source and the binary module paths. It makes it easy for
25
-- the main file of a multi-file program to access its modules in the same directory.
26
-- `base` allows these modules to be put in a specified subdirectory, to allow for
27
-- cleaner deployment and resolve potential conflicts between a script name and its
28
-- library directory.
29
--
30
-- Note: the path is prefixed, so it is searched first when requiring modules.
31
-- @string base optional base directory (absolute, or relative path).
32
-- @bool nofollow always use the invocation's directory, even if the invoked file is a symlink
33
-- @treturn string the current script's path with a trailing slash
34
function app.require_here (base, nofollow)
30✔
35
    local p = app.script_name()
15✔
36
    if not path.isabs(p) then
18✔
37
        p = path.join(path.currentdir(),p)
21✔
38
    end
39
    if not nofollow then
15✔
40
      local t = path.link_attrib(p)
15✔
41
      if t and t.mode == 'link' then
15✔
42
        t = t.target
×
43
        if not path.isabs(t) then
×
44
          t = path.join(path.dirname(p), t)
×
45
        end
46
        p = t
×
47
      end
48
    end
49
    p = path.normpath(path.dirname(p))
21✔
50
    if p:sub(-1,-1) ~= path.sep then
18✔
51
        p = p..path.sep
15✔
52
    end
53
    if base then
15✔
54
        if path.is_windows then
15✔
UNCOV
55
            base = base:gsub('/','\\')
×
56
        end
57
        if path.isabs(base) then
18✔
58
            p = base .. path.sep
×
59
        else
60
            p = p..base..path.sep
15✔
61
        end
62
    end
63
    local so_ext = path.is_windows and 'dll' or 'so'
15✔
64
    local lsep = package.path:find '^;' and '' or ';'
15✔
65
    local csep = package.cpath:find '^;' and '' or ';'
15✔
66
    package.path = ('%s?.lua;%s?%sinit.lua%s%s'):format(p,p,path.sep,lsep,package.path)
15✔
67
    package.cpath = ('%s?.%s%s%s'):format(p,so_ext,csep,package.cpath)
15✔
68
    return p
15✔
69
end
70

71
--- return a suitable path for files private to this application.
72
-- These will look like '~/.SNAME/file', with '~' as with expanduser and
73
-- SNAME is the name of the script without .lua extension.
74
-- If the directory does not exist, it will be created.
75
-- @string file a filename (w/out path)
76
-- @return a full pathname, or nil
77
-- @return cannot create directory error
78
-- @usage
79
-- -- when run from a script called 'testapp' (on Windows):
80
-- local app = require 'pl.app'
81
-- print(app.appfile 'test.txt')
82
-- -- C:\Documents and Settings\steve\.testapp\test.txt
83
function app.appfile(file)
30✔
84
    local sfullname, err = app.script_name()
×
85
    if not sfullname then return utils.raise(err) end
×
86
    local sname = path.basename(sfullname)
×
87
    local name = path.splitext(sname)
×
88
    local dir = path.join(path.expanduser('~'),'.'..name)
×
89
    if not path.isdir(dir) then
×
90
        local ret = path.mkdir(dir)
×
91
        if not ret then return utils.raise('cannot create '..dir) end
×
92
    end
93
    return path.join(dir,file)
×
94
end
95

96
--- return string indicating operating system.
97
-- @return 'Windows','OSX' or whatever uname returns (e.g. 'Linux')
98
function app.platform()
30✔
99
    if path.is_windows then
5✔
UNCOV
100
        return 'Windows'
×
101
    else
102
        local f = io.popen('uname')
5✔
103
        local res = f:read()
5✔
104
        if res == 'Darwin' then res = 'OSX' end
5✔
105
        f:close()
5✔
106
        return res
5✔
107
    end
108
end
109

110
--- return the full command-line used to invoke this script.
111
-- It will not include the scriptname itself, see `app.script_name`.
112
-- @return command-line
113
-- @return name of Lua program used
114
-- @usage
115
-- -- execute:  lua -lluacov -e 'print(_VERSION)' myscript.lua
116
--
117
-- -- myscript.lua
118
-- print(require("pl.app").lua())  --> "lua -lluacov -e 'print(_VERSION)'", "lua"
119
function app.lua()
30✔
120
    local args = _G.arg
20✔
121
    if not args then
20✔
122
        return utils.raise "not in a main program"
×
123
    end
124

125
    local cmd = {}
20✔
126
    local i = -1
20✔
127
    while true do
128
        table.insert(cmd, 1, args[i])
70✔
129
        if not args[i-1] then
70✔
130
            return utils.quote_arg(cmd), args[i]
24✔
131
        end
132
        i = i - 1
50✔
133
    end
134
end
135

136
--- parse command-line arguments into flags and parameters.
137
-- Understands GNU-style command-line flags; short (`-f`) and long (`--flag`).
138
--
139
-- These may be given a value with either '=' or ':' (`-k:2`,`--alpha=3.2`,`-n2`),
140
-- a number value can be given without a space. If the flag is marked
141
-- as having a value, then a space-separated value is also accepted (`-i hello`),
142
-- see the `flags_with_values` argument).
143
--
144
-- Multiple short args can be combined like so: ( `-abcd`).
145
--
146
-- When specifying the `flags_valid` parameter, its contents can also contain
147
-- aliases, to convert short/long flags to the same output name. See the
148
-- example below.
149
--
150
-- Note: if a flag is repeated, the last value wins.
151
-- @tparam {string} args an array of strings (default is the global `arg`)
152
-- @tab flags_with_values any flags that take values, either list or hash
153
-- table e.g. `{ out=true }` or `{ "out" }`.
154
-- @tab flags_valid (optional) flags that are valid, either list or hashtable.
155
-- If not given, everything
156
-- will be accepted(everything in `flags_with_values` will automatically be allowed)
157
-- @return a table of flags (flag=value pairs)
158
-- @return an array of parameters
159
-- @raise if args is nil, then the global `args` must be available!
160
-- @usage
161
-- -- Simple form:
162
-- local flags, params = app.parse_args(nil,
163
--      { "hello", "world" },  -- list of flags taking values
164
--      { "l", "a", "b"})      -- list of allowed flags (value ones will be added)
165
--
166
-- -- More complex example using aliases:
167
-- local valid = {
168
--     long = "l",           -- if 'l' is specified, it is reported as 'long'
169
--     new = { "n", "old" }, -- here both 'n' and 'old' will go into 'new'
170
-- }
171
-- local values = {
172
--     "value",   -- will automatically be added to the allowed set of flags
173
--     "new",     -- will mark 'n' and 'old' as requiring a value as well
174
-- }
175
-- local flags, params = app.parse_args(nil, values, valid)
176
--
177
-- -- command:  myapp.lua -l --old:hello --value world param1 param2
178
-- -- will yield:
179
-- flags = {
180
--     long = true,     -- input from 'l'
181
--     new = "hello",   -- input from 'old'
182
--     value = "world", -- allowed because it was in 'values', note: space separated!
183
-- }
184
-- params = {
185
--     [1] = "param1"
186
--     [2] = "param2"
187
-- }
188
function app.parse_args (args,flags_with_values, flags_valid)
30✔
189
    if not args then
55✔
190
        args = _G.arg
×
191
        if not args then utils.raise "Not in a main program: 'arg' not found" end
×
192
    end
193

194
    local with_values = {}
55✔
195
    for k,v in pairs(flags_with_values or {}) do
110✔
196
        if type(k) == "number" then
55✔
197
            k = v
30✔
198
        end
199
        with_values[k] = true
55✔
200
    end
201

202
    local valid
203
    if not flags_valid then
55✔
204
        -- if no allowed flags provided, we create a table that always returns
205
        -- the keyname, no matter what you look up
206
        valid = setmetatable({},{ __index = function(_, key) return key end })
285✔
207
    else
208
        valid = {}
20✔
209
        for k,aliases in pairs(flags_valid) do
70✔
210
            if type(k) == "number" then         -- array/list entry
50✔
211
                k = aliases
30✔
212
            end
213
            if type(aliases) == "string" then  -- single alias
50✔
214
                aliases = { aliases }
40✔
215
            end
216
            if type(aliases) == "table" then   -- list of aliases
50✔
217
                -- it's the alternate name, so add the proper mappings
218
                for i, alias in ipairs(aliases) do
95✔
219
                    valid[alias] = k
50✔
220
                end
221
            end
222
            valid[k] = k
50✔
223
        end
224
        do
225
            local new_with_values = {}  -- needed to prevent "invalid key to 'next'" error
20✔
226
            for k,v in pairs(with_values) do
40✔
227
                if not valid[k] then
20✔
228
                    valid[k] = k   -- add the with_value entry as a valid one
5✔
229
                    new_with_values[k] = true
5✔
230
                else
231
                    new_with_values[valid[k]] = true  --set, but by its alias
15✔
232
                end
233
            end
234
            with_values = new_with_values
20✔
235
        end
236
    end
237

238
    -- now check that all flags with values are reported as such under all
239
    -- of their aliases
240
    for k, main_alias in pairs(valid) do
130✔
241
        if with_values[main_alias] then
75✔
242
            with_values[k] = true
25✔
243
        end
244
    end
245

246
    local _args = {}
55✔
247
    local flags = {}
55✔
248
    local i = 1
55✔
249
    while i <= #args do
225✔
250
        local a = args[i]
190✔
251
        local v = a:match('^-(.+)')
190✔
252
        local is_long
253
        if not v then
190✔
254
            -- we have a parameter
255
            _args[#_args+1] = a
15✔
256
        else
257
            -- it's a flag
258
            if v:find '^-' then
175✔
259
                is_long = true
15✔
260
                v = v:sub(2)
18✔
261
            end
262
            if with_values[v] then
175✔
263
                if i == #args or args[i+1]:find '^-' then
40✔
264
                    return utils.raise ("no value for '"..v.."'")
10✔
265
                end
266
                flags[valid[v]] = args[i+1]
34✔
267
                i = i + 1
30✔
268
            else
269
                -- a value can also be indicated with = or :
270
                local var,val =  utils.splitv (v,'[=:]', false, 2)
135✔
271
                var = var or v
135✔
272
                val = val or true
135✔
273
                if not is_long then
135✔
274
                    if #var > 1 then
120✔
275
                        if var:find '.%d+' then -- short flag, number value
30✔
276
                            val = var:sub(2)
18✔
277
                            var = var:sub(1,1)
18✔
278
                        else -- multiple short flags
279
                            for i = 1,#var do
50✔
280
                                local f = var:sub(i,i)
40✔
281
                                if not valid[f] then
43✔
282
                                    return utils.raise("unknown flag '"..f.."'")
5✔
283
                                else
284
                                    f = valid[f]
35✔
285
                                end
286
                                flags[f] = true
35✔
287
                            end
288
                            val = nil -- prevents use of var as a flag below
10✔
289
                        end
290
                    else  -- single short flag (can have value, defaults to true)
291
                        val = val or true
90✔
292
                    end
293
                end
294
                if val then
130✔
295
                    if not valid[var] then
140✔
296
                        return utils.raise("unknown flag '"..var.."'")
5✔
297
                    else
298
                        var = valid[var]
115✔
299
                    end
300
                    flags[var] = val
115✔
301
                end
302
            end
303
        end
304
        i = i + 1
170✔
305
    end
306
    return flags,_args
35✔
307
end
308

309
return app
30✔
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