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

lunarmodules / Penlight / 477

12 Feb 2024 09:01AM UTC coverage: 89.675% (+0.7%) from 88.938%
477

push

appveyor

web-flow
fix(doc): typo in example (#463)

5489 of 6121 relevant lines covered (89.67%)

346.11 hits per line

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

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

11
local app = {}
48✔
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()
48✔
17
    if _G.arg and _G.arg[0] then
32✔
18
        return _G.arg[0]
32✔
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)
48✔
35
    local p = app.script_name()
24✔
36
    if not path.isabs(p) then
36✔
37
        p = path.join(path.currentdir(),p)
48✔
38
    end
39
    if not nofollow then
24✔
40
      local t = path.link_attrib(p)
24✔
41
      if t and t.mode == 'link' then
24✔
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))
48✔
50
    if p:sub(-1,-1) ~= path.sep then
36✔
51
        p = p..path.sep
24✔
52
    end
53
    if base then
24✔
54
        if path.is_windows then
24✔
55
            base = base:gsub('/','\\')
24✔
56
        end
57
        if path.isabs(base) then
36✔
58
            p = base .. path.sep
×
59
        else
60
            p = p..base..path.sep
24✔
61
        end
62
    end
63
    local so_ext = path.is_windows and 'dll' or 'so'
24✔
64
    local lsep = package.path:find '^;' and '' or ';'
24✔
65
    local csep = package.cpath:find '^;' and '' or ';'
24✔
66
    package.path = ('%s?.lua;%s?%sinit.lua%s%s'):format(p,p,path.sep,lsep,package.path)
24✔
67
    package.cpath = ('%s?.%s%s%s'):format(p,so_ext,csep,package.cpath)
24✔
68
    return p
24✔
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)
48✔
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()
48✔
99
    if path.is_windows then
8✔
100
        return 'Windows'
8✔
101
    else
102
        local f = io.popen('uname')
×
103
        local res = f:read()
×
104
        if res == 'Darwin' then res = 'OSX' end
×
105
        f:close()
×
106
        return res
×
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()
48✔
120
    local args = _G.arg
32✔
121
    if not args then
32✔
122
        return utils.raise "not in a main program"
×
123
    end
124

125
    local cmd = {}
32✔
126
    local i = -1
32✔
127
    while true do
128
        table.insert(cmd, 1, args[i])
112✔
129
        if not args[i-1] then
112✔
130
            return utils.quote_arg(cmd), args[i]
48✔
131
        end
132
        i = i - 1
80✔
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)
48✔
189
    if not args then
88✔
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 = {}
88✔
195
    for k,v in pairs(flags_with_values or {}) do
176✔
196
        if type(k) == "number" then
88✔
197
            k = v
48✔
198
        end
199
        with_values[k] = true
88✔
200
    end
201

202
    local valid
203
    if not flags_valid then
88✔
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 })
456✔
207
    else
208
        valid = {}
32✔
209
        for k,aliases in pairs(flags_valid) do
112✔
210
            if type(k) == "number" then         -- array/list entry
80✔
211
                k = aliases
48✔
212
            end
213
            if type(aliases) == "string" then  -- single alias
80✔
214
                aliases = { aliases }
64✔
215
            end
216
            if type(aliases) == "table" then   -- list of aliases
80✔
217
                -- it's the alternate name, so add the proper mappings
218
                for i, alias in ipairs(aliases) do
152✔
219
                    valid[alias] = k
80✔
220
                end
221
            end
222
            valid[k] = k
80✔
223
        end
224
        do
225
            local new_with_values = {}  -- needed to prevent "invalid key to 'next'" error
32✔
226
            for k,v in pairs(with_values) do
64✔
227
                if not valid[k] then
32✔
228
                    valid[k] = k   -- add the with_value entry as a valid one
8✔
229
                    new_with_values[k] = true
8✔
230
                else
231
                    new_with_values[valid[k]] = true  --set, but by its alias
24✔
232
                end
233
            end
234
            with_values = new_with_values
32✔
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
208✔
241
        if with_values[main_alias] then
120✔
242
            with_values[k] = true
40✔
243
        end
244
    end
245

246
    local _args = {}
88✔
247
    local flags = {}
88✔
248
    local i = 1
88✔
249
    while i <= #args do
360✔
250
        local a = args[i]
304✔
251
        local v = a:match('^-(.+)')
304✔
252
        local is_long
253
        if not v then
304✔
254
            -- we have a parameter
255
            _args[#_args+1] = a
24✔
256
        else
257
            -- it's a flag
258
            if v:find '^-' then
280✔
259
                is_long = true
24✔
260
                v = v:sub(2)
36✔
261
            end
262
            if with_values[v] then
280✔
263
                if i == #args or args[i+1]:find '^-' then
64✔
264
                    return utils.raise ("no value for '"..v.."'")
16✔
265
                end
266
                flags[valid[v]] = args[i+1]
64✔
267
                i = i + 1
48✔
268
            else
269
                -- a value can also be indicated with = or :
270
                local var,val =  utils.splitv (v,'[=:]', false, 2)
216✔
271
                var = var or v
216✔
272
                val = val or true
216✔
273
                if not is_long then
216✔
274
                    if #var > 1 then
192✔
275
                        if var:find '.%d+' then -- short flag, number value
48✔
276
                            val = var:sub(2)
36✔
277
                            var = var:sub(1,1)
36✔
278
                        else -- multiple short flags
279
                            for i = 1,#var do
80✔
280
                                local f = var:sub(i,i)
64✔
281
                                if not valid[f] then
76✔
282
                                    return utils.raise("unknown flag '"..f.."'")
8✔
283
                                else
284
                                    f = valid[f]
56✔
285
                                end
286
                                flags[f] = true
56✔
287
                            end
288
                            val = nil -- prevents use of var as a flag below
16✔
289
                        end
290
                    else  -- single short flag (can have value, defaults to true)
291
                        val = val or true
144✔
292
                    end
293
                end
294
                if val then
208✔
295
                    if not valid[var] then
272✔
296
                        return utils.raise("unknown flag '"..var.."'")
8✔
297
                    else
298
                        var = valid[var]
184✔
299
                    end
300
                    flags[var] = val
184✔
301
                end
302
            end
303
        end
304
        i = i + 1
272✔
305
    end
306
    return flags,_args
56✔
307
end
308

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