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

lunarmodules / Penlight / 8703486185

16 Apr 2024 09:46AM UTC coverage: 88.885% (+0.03%) from 88.86%
8703486185

Pull #473

github

Tieske
add tests
Pull Request #473: feat(utils/app): allow for global env var defaults

15 of 17 new or added lines in 3 files covered. (88.24%)

8 existing lines in 1 file now uncovered.

5462 of 6145 relevant lines covered (88.89%)

260.37 hits per line

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

85.07
/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
36✔
8
local utils = require 'pl.utils'
36✔
9
local path = require 'pl.path'
36✔
10

11
local app = {}
36✔
12

13

14
--- Sets/clears an environment variable default, to use with `utils.getenv`.
15
-- This links to `utils.setenv_default`.
16
-- @function setenv_default
17
-- @usage -- override the global with this implementation to control
18
-- -- environment variables with defaults on application level.
19
-- os.getenv = require("pl.utils").getenv
20
--
21
-- utils.setenv_default("MY_APP_CONFIG", "~/.my_app")
22
app.setenv_default = utils.setenv_default
36✔
23

24

25
--- return the name of the current script running.
26
-- The name will be the name as passed on the command line
27
-- @return string filename
28
function app.script_name()
36✔
29
    if _G.arg and _G.arg[0] then
24✔
30
        return _G.arg[0]
24✔
31
    end
32
    return utils.raise("No script name found")
×
33
end
34

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

83
--- return a suitable path for files private to this application.
84
-- These will look like `'~/.SNAME/file'`, with '~' expanded through `path.expanduser` and
85
-- `SNAME` is the filename of the script without `'.lua'` extension (see `script_name`).
86
-- If the directory does not exist, it will be created.
87
-- @string file a filename (w/out path)
88
-- @return a full pathname, or nil
89
-- @return cannot create directory error
90
-- @usage
91
-- -- when run from a script called 'testapp' (on Windows):
92
-- local app = require 'pl.app'
93
-- print(app.appfile 'some\test.txt')
94
-- -- C:\Documents and Settings\steve\.testapp\some\test.txt
95
function app.appfile(file)
36✔
UNCOV
96
    local sfullname, err = app.script_name()
×
UNCOV
97
    if not sfullname then return utils.raise(err) end
×
UNCOV
98
    local sname = path.basename(sfullname)
×
UNCOV
99
    local name = path.splitext(sname)
×
100
    local dir = path.join(path.expanduser('~'),'.'..name)
×
101
    if not path.isdir(dir) then
×
102
        local ret = path.mkdir(dir)
×
103
        if not ret then return utils.raise('cannot create '..dir) end
×
104
    end
105
    return path.join(dir,file)
×
106
end
107

108
--- return string indicating operating system.
109
-- @treturn[1] string 'Windows' on Windows platforms
110
-- @treturn[2] string 'OSX' on Apple platforms
111
-- @treturn[3] string whatever `uname` returns on other platforms (e.g. 'Linux')
112
function app.platform()
36✔
113
    if path.is_windows then
6✔
UNCOV
114
        return 'Windows'
×
115
    else
116
        local f = io.popen('uname')
6✔
117
        local res = f:read()
6✔
118
        if res == 'Darwin' then res = 'OSX' end
6✔
119
        f:close()
6✔
120
        return res
6✔
121
    end
122
end
123

124
--- return the full command-line used to invoke this script.
125
-- It will not include the scriptname itself, see `app.script_name`.
126
-- @return command-line
127
-- @return name of Lua program used
128
-- @usage
129
-- -- execute:  lua -lluacov -e 'print(_VERSION)' myscript.lua
130
--
131
-- -- myscript.lua
132
-- print(require("pl.app").lua())  --> "lua -lluacov -e 'print(_VERSION)'", "lua"
133
function app.lua()
36✔
134
    local args = _G.arg
24✔
135
    if not args then
24✔
UNCOV
136
        return utils.raise "not in a main program"
×
137
    end
138

139
    local cmd = {}
24✔
140
    local i = -1
24✔
141
    while true do
142
        table.insert(cmd, 1, args[i])
84✔
143
        if not args[i-1] then
84✔
144
            return utils.quote_arg(cmd), args[i]
32✔
145
        end
146
        i = i - 1
60✔
147
    end
148
end
149

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

208
    local with_values = {}
66✔
209
    for k,v in pairs(flags_with_values or {}) do
132✔
210
        if type(k) == "number" then
66✔
211
            k = v
36✔
212
        end
213
        with_values[k] = true
66✔
214
    end
215

216
    local valid
217
    if not flags_valid then
66✔
218
        -- if no allowed flags provided, we create a table that always returns
219
        -- the keyname, no matter what you look up
220
        valid = setmetatable({},{ __index = function(_, key) return key end })
342✔
221
    else
222
        valid = {}
24✔
223
        for k,aliases in pairs(flags_valid) do
84✔
224
            if type(k) == "number" then         -- array/list entry
60✔
225
                k = aliases
36✔
226
            end
227
            if type(aliases) == "string" then  -- single alias
60✔
228
                aliases = { aliases }
48✔
229
            end
230
            if type(aliases) == "table" then   -- list of aliases
60✔
231
                -- it's the alternate name, so add the proper mappings
232
                for i, alias in ipairs(aliases) do
114✔
233
                    valid[alias] = k
60✔
234
                end
235
            end
236
            valid[k] = k
60✔
237
        end
238
        do
239
            local new_with_values = {}  -- needed to prevent "invalid key to 'next'" error
24✔
240
            for k,v in pairs(with_values) do
48✔
241
                if not valid[k] then
24✔
242
                    valid[k] = k   -- add the with_value entry as a valid one
6✔
243
                    new_with_values[k] = true
6✔
244
                else
245
                    new_with_values[valid[k]] = true  --set, but by its alias
18✔
246
                end
247
            end
248
            with_values = new_with_values
24✔
249
        end
250
    end
251

252
    -- now check that all flags with values are reported as such under all
253
    -- of their aliases
254
    for k, main_alias in pairs(valid) do
156✔
255
        if with_values[main_alias] then
90✔
256
            with_values[k] = true
30✔
257
        end
258
    end
259

260
    local _args = {}
66✔
261
    local flags = {}
66✔
262
    local i = 1
66✔
263
    while i <= #args do
270✔
264
        local a = args[i]
228✔
265
        local v = a:match('^-(.+)')
228✔
266
        local is_long
267
        if not v then
228✔
268
            -- we have a parameter
269
            _args[#_args+1] = a
18✔
270
        else
271
            -- it's a flag
272
            if v:find '^-' then
210✔
273
                is_long = true
18✔
274
                v = v:sub(2)
24✔
275
            end
276
            if with_values[v] then
210✔
277
                if i == #args or args[i+1]:find '^-' then
48✔
278
                    return utils.raise ("no value for '"..v.."'")
12✔
279
                end
280
                flags[valid[v]] = args[i+1]
44✔
281
                i = i + 1
36✔
282
            else
283
                -- a value can also be indicated with = or :
284
                local var,val =  utils.splitv (v,'[=:]', false, 2)
162✔
285
                var = var or v
162✔
286
                val = val or true
162✔
287
                if not is_long then
162✔
288
                    if #var > 1 then
144✔
289
                        if var:find '.%d+' then -- short flag, number value
36✔
290
                            val = var:sub(2)
24✔
291
                            var = var:sub(1,1)
24✔
292
                        else -- multiple short flags
293
                            for i = 1,#var do
60✔
294
                                local f = var:sub(i,i)
48✔
295
                                if not valid[f] then
54✔
296
                                    return utils.raise("unknown flag '"..f.."'")
6✔
297
                                else
298
                                    f = valid[f]
42✔
299
                                end
300
                                flags[f] = true
42✔
301
                            end
302
                            val = nil -- prevents use of var as a flag below
12✔
303
                        end
304
                    else  -- single short flag (can have value, defaults to true)
305
                        val = val or true
108✔
306
                    end
307
                end
308
                if val then
156✔
309
                    if not valid[var] then
184✔
310
                        return utils.raise("unknown flag '"..var.."'")
6✔
311
                    else
312
                        var = valid[var]
138✔
313
                    end
314
                    flags[var] = val
138✔
315
                end
316
            end
317
        end
318
        i = i + 1
204✔
319
    end
320
    return flags,_args
42✔
321
end
322

323
return app
36✔
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

© 2026 Coveralls, Inc