summaryrefslogtreecommitdiff
path: root/macros/luatex/generic/penlight/pl/app.lua
blob: a06305da4fefdc7539cd652f8ce76d9e8b8ae004 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
--- Application support functions.
-- See @{01-introduction.md.Application_Support|the Guide}
--
-- Dependencies: `pl.utils`, `pl.path`
-- @module pl.app

local io,package,require = _G.io, _G.package, _G.require
local utils = require 'pl.utils'
local path = require 'pl.path'

local app = {}

--- return the name of the current script running.
-- The name will be the name as passed on the command line
-- @return string filename
function app.script_name()
    if _G.arg and _G.arg[0] then
        return _G.arg[0]
    end
    return utils.raise("No script name found")
end

--- prefixes the current script's path to the Lua module path.
-- Applies to both the source and the binary module paths. It makes it easy for
-- the main file of a multi-file program to access its modules in the same directory.
-- `base` allows these modules to be put in a specified subdirectory, to allow for
-- cleaner deployment and resolve potential conflicts between a script name and its
-- library directory.
--
-- Note: the path is prefixed, so it is searched first when requiring modules.
-- @string base optional base directory (absolute, or relative path).
-- @treturn string the current script's path with a trailing slash
function app.require_here (base)
    local p = path.dirname(app.script_name())
    if not path.isabs(p) then
        p = path.join(path.currentdir(),p)
    end
    if p:sub(-1,-1) ~= path.sep then
        p = p..path.sep
    end
    if base then
        if path.is_windows then
            base = base:gsub('/','\\')
        end
        if path.isabs(base) then
            p = base .. path.sep
        else
            p = p..base..path.sep
        end
    end
    local so_ext = path.is_windows and 'dll' or 'so'
    local lsep = package.path:find '^;' and '' or ';'
    local csep = package.cpath:find '^;' and '' or ';'
    package.path = ('%s?.lua;%s?%sinit.lua%s%s'):format(p,p,path.sep,lsep,package.path)
    package.cpath = ('%s?.%s%s%s'):format(p,so_ext,csep,package.cpath)
    return p
end

--- return a suitable path for files private to this application.
-- These will look like '~/.SNAME/file', with '~' as with expanduser and
-- SNAME is the name of the script without .lua extension.
-- If the directory does not exist, it will be created.
-- @string file a filename (w/out path)
-- @return a full pathname, or nil
-- @return cannot create directory error
-- @usage
-- -- when run from a script called 'testapp' (on Windows):
-- local app = require 'pl.app'
-- print(app.appfile 'test.txt')
-- -- C:\Documents and Settings\steve\.testapp\test.txt
function app.appfile(file)
    local sfullname, err = app.script_name()
    if not sfullname then return utils.raise(err) end
    local sname = path.basename(sfullname)
    local name = path.splitext(sname)
    local dir = path.join(path.expanduser('~'),'.'..name)
    if not path.isdir(dir) then
        local ret = path.mkdir(dir)
        if not ret then return utils.raise('cannot create '..dir) end
    end
    return path.join(dir,file)
end

--- return string indicating operating system.
-- @return 'Windows','OSX' or whatever uname returns (e.g. 'Linux')
function app.platform()
    if path.is_windows then
        return 'Windows'
    else
        local f = io.popen('uname')
        local res = f:read()
        if res == 'Darwin' then res = 'OSX' end
        f:close()
        return res
    end
end

--- return the full command-line used to invoke this script.
-- It will not include the scriptname itself, see `app.script_name`.
-- @return command-line
-- @return name of Lua program used
-- @usage
-- -- execute:  lua -lluacov -e 'print(_VERSION)' myscript.lua
--
-- -- myscript.lua
-- print(require("pl.app").lua())  --> "lua -lluacov -e 'print(_VERSION)'", "lua"
function app.lua()
    local args = _G.arg
    if not args then
        return utils.raise "not in a main program"
    end

    local cmd = {}
    local i = -1
    while true do
        table.insert(cmd, 1, args[i])
        if not args[i-1] then
            return utils.quote_arg(cmd), args[i]
        end
        i = i - 1
    end
end

--- parse command-line arguments into flags and parameters.
-- Understands GNU-style command-line flags; short (`-f`) and long (`--flag`).
--
-- These may be given a value with either '=' or ':' (`-k:2`,`--alpha=3.2`,`-n2`),
-- a number value can be given without a space. If the flag is marked
-- as having a value, then a space-separated value is also accepted (`-i hello`),
-- see the `flags_with_values` argument).
--
-- Multiple short args can be combined like so: ( `-abcd`).
--
-- When specifying the `flags_valid` parameter, its contents can also contain
-- aliasses, to convert short/long flags to the same output name. See the
-- example below.
--
-- Note: if a flag is repeated, the last value wins.
-- @tparam {string} args an array of strings (default is the global `arg`)
-- @tab flags_with_values any flags that take values, either list or hash
-- table e.g. `{ out=true }` or `{ "out" }`.
-- @tab flags_valid (optional) flags that are valid, either list or hashtable.
-- If not given, everything
-- will be accepted(everything in `flags_with_values` will automatically be allowed)
-- @return a table of flags (flag=value pairs)
-- @return an array of parameters
-- @raise if args is nil, then the global `args` must be available!
-- @usage
-- -- Simple form:
-- local flags, params = app.parse_args(nil,
--      { "hello", "world" },  -- list of flags taking values
--      { "l", "a", "b"})      -- list of allowed flags (value ones will be added)
--
-- -- More complex example using aliasses:
-- local valid = {
--     long = "l",           -- if 'l' is specified, it is reported as 'long'
--     new = { "n", "old" }, -- here both 'n' and 'old' will go into 'new'
-- }
-- local values = {
--     "value",   -- will automatically be added to the allowed set of flags
--     "new",     -- will mark 'n' and 'old' as requiring a value as well
-- }
-- local flags, params = app.parse_args(nil, values, valid)
--
-- -- command:  myapp.lua -l --old:hello --value world param1 param2
-- -- will yield:
-- flags = {
--     long = true,     -- input from 'l'
--     new = "hello",   -- input from 'old'
--     value = "world", -- allowed because it was in 'values', note: space separated!
-- }
-- params = {
--     [1] = "param1"
--     [2] = "param2"
-- }
function app.parse_args (args,flags_with_values, flags_valid)
    if not args then
        args = _G.arg
        if not args then utils.raise "Not in a main program: 'arg' not found" end
    end

    local with_values = {}
    for k,v in pairs(flags_with_values or {}) do
        if type(k) == "number" then
            k = v
        end
        with_values[k] = true
    end

    local valid
    if not flags_valid then
        -- if no allowed flags provided, we create a table that always returns
        -- the keyname, no matter what you look up
        valid = setmetatable({},{ __index = function(_, key) return key end })
    else
        valid = {}
        for k,aliasses in pairs(flags_valid) do
            if type(k) == "number" then         -- array/list entry
                k = aliasses
            end
            if type(aliasses) == "string" then  -- single alias
                aliasses = { aliasses }
            end
            if type(aliasses) == "table" then   -- list of aliasses
                -- it's the alternate name, so add the proper mappings
                for i, alias in ipairs(aliasses) do
                    valid[alias] = k
                end
            end
            valid[k] = k
        end
        do
            local new_with_values = {}  -- needed to prevent "invalid key to 'next'" error
            for k,v in pairs(with_values) do
                if not valid[k] then
                    valid[k] = k   -- add the with_value entry as a valid one
                    new_with_values[k] = true
                else
                    new_with_values[valid[k]] = true  --set, but by its alias
                end
            end
            with_values = new_with_values
        end
    end

    -- now check that all flags with values are reported as such under all
    -- of their aliasses
    for k, main_alias in pairs(valid) do
        if with_values[main_alias] then
            with_values[k] = true
        end
    end

    local _args = {}
    local flags = {}
    local i = 1
    while i <= #args do
        local a = args[i]
        local v = a:match('^-(.+)')
        local is_long
        if not v then
            -- we have a parameter
            _args[#_args+1] = a
        else
            -- it's a flag
            if v:find '^-' then
                is_long = true
                v = v:sub(2)
            end
            if with_values[v] then
                if i == #args or args[i+1]:find '^-' then
                    return utils.raise ("no value for '"..v.."'")
                end
                flags[valid[v]] = args[i+1]
                i = i + 1
            else
                -- a value can also be indicated with = or :
                local var,val =  utils.splitv (v,'[=:]', false, 2)
                var = var or v
                val = val or true
                if not is_long then
                    if #var > 1 then
                        if var:find '.%d+' then -- short flag, number value
                            val = var:sub(2)
                            var = var:sub(1,1)
                        else -- multiple short flags
                            for i = 1,#var do
                                local f = var:sub(i,i)
                                if not valid[f] then
                                    return utils.raise("unknown flag '"..f.."'")
                                else
                                    f = valid[f]
                                end
                                flags[f] = true
                            end
                            val = nil -- prevents use of var as a flag below
                        end
                    else  -- single short flag (can have value, defaults to true)
                        val = val or true
                    end
                end
                if val then
                    if not valid[var] then
                        return utils.raise("unknown flag '"..var.."'")
                    else
                        var = valid[var]
                    end
                    flags[var] = val
                end
            end
        end
        i = i + 1
    end
    return flags,_args
end

return app