aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoronefang2026-02-05 03:43:04 +1000
committeronefang2026-02-05 03:43:04 +1000
commitaba1131c59e54a7b2baf048b3f7d372e14268e6b (patch)
tree6ae939aaecfec3a4a18815372d6c92f7a2160473
parentAdd the README.md link. (diff)
downloadpolygLua-aba1131c59e54a7b2baf048b3f7d372e14268e6b.zip
polygLua-aba1131c59e54a7b2baf048b3f7d372e14268e6b.tar.gz
polygLua-aba1131c59e54a7b2baf048b3f7d372e14268e6b.tar.bz2
polygLua-aba1131c59e54a7b2baf048b3f7d372e14268e6b.tar.xz
The actual script.
-rw-r--r--LICENCE31
-rwxr-xr-xpolygLua.lua518
2 files changed, 549 insertions, 0 deletions
diff --git a/LICENCE b/LICENCE
new file mode 100644
index 0000000..d4cba67
--- /dev/null
+++ b/LICENCE
@@ -0,0 +1,31 @@
1Copyright notice for polygLua
2
3Copyright (C) 2025 David Walter Seikel AKA onefang
4
5All rights reserved.
6
7Redistribution and use in source and binary forms, with or without
8modification, are permitted provided that the following conditions are
9met:
10
111. Redistributions of source code must retain the above copyright
12notice, this list of conditions and the following disclaimer.
13
142. Redistributions in binary form must reproduce the above copyright
15notice, this list of conditions and the following disclaimer in the
16documentation and/or other materials provided with the distribution.
17
18THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
19INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
20AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
21THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
22INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
23NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
24USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29Freedom -1: the author specifically grants themselves the freedom to not
30be infected by the viral licence clauses of any code this source code
31"links" to. It's my code, I choose my licence terms, no one else does.
diff --git a/polygLua.lua b/polygLua.lua
new file mode 100755
index 0000000..008011e
--- /dev/null
+++ b/polygLua.lua
@@ -0,0 +1,518 @@
1#!/usr/bin/env luajit
2
3--- polygLua, gluing things onto Lua, making it a polyglot language.
4-- I can write Lua in any language. B-)
5-- Also includes some other little useful bits I commonly use, most of which is used in support of its main function.
6-- @module polygLua
7-- @alias _
8
9local _ = {}
10--- version number and string
11_.version = '0.0 crap'
12
13
14_.verbosity = 4
15local log = function( v, -- verbosity
16 t, -- level
17 s -- message
18 )
19 if nil == s then s = 'nil' end
20 if v <= _.verbosity then
21 if 3 <= _.verbosity then t = os.date('!%F %T') .. ' ' .. t end
22 if 4 == v then t = '' else t = t .. ': ' end
23 if 3 <= v then
24 io.stdout:write(t .. s .. '\n')
25 io.stdout:flush()
26 else
27 io.stderr:write(t .. s .. '\n')
28 io.stderr:flush()
29 end
30 end
31end
32-- This sets the global values, here and in the caller.
33--- log DEBUG level string
34D = function(s) log(5, 'DEBUG ', s) end
35--- log PRINT level string
36P = function(s) log(4, 'PRINT ', s) end
37--- log INFO level string
38I = function(s) log(3, 'INFO ', s) end
39--- log TIMEOUT level string
40T = function(s) log(2, 'TIMEOUT ', s) end
41--- log WARNING level string
42W = function(s) log(1, 'WARNING ', s) end
43--- log ERROR level string
44E = function(s) log(0, 'ERROR ', s) end
45--- log CRITICAL level string
46C = function(s) log(-1, 'CRITICAL ', s) end
47
48--- common options
49local optionsCommon =
50{
51 help = {help = 'Print the help text.',
52 func = function(self, options, a, args, i, name)
53 for i,v in ipairs{'/usr/share/doc/' .. name, '/usr/local/share/doc/' .. name, './'} do
54 local p = v .. 'README.md'
55 local h = io.open(p, 'r')
56 if nil ~= h then
57 D('Found README file '.. p)
58 Help = h:read('*a') -- NOTE Lua 5.3 doesn't use the *, but ignores it if it's there, earlier versions need it.
59 h:close()
60 end
61 end
62
63 P(Help)
64 _.usage(args, options, true)
65 os.exit(0)
66 end
67 },
68 version = {help = 'Print the version details.',
69 func = function(self, options, a, args, i)
70 P('This is version ' ..Version .. ' of ' .. args[0])
71 os.exit(0)
72 end
73 },
74 ['-q'] = {help = 'Decrease verbosity level.',
75 func = function(self, options, a, args, i)
76 if _.verbosity > -1 then _.verbosity = _.verbosity - 1 end
77 I('New verbosity level is ' .. _.verbosity)
78 end
79 },
80 ['-v'] = {help = 'Increase verbosity level.',
81 func = function(self, options, a, args, i)
82 if _.verbosity < 4 then _.verbosity = _.verbosity + 1 end
83 I('New verbosity level is ' .. _.verbosity)
84 end
85 },
86 ['--maximum-verbosity'] = {help = 'Increase verbosity level to maximum.',
87 func = function(self, options, a, args, i)
88 _.verbosity = 4
89 I('New verbosity level is ' .. _.verbosity)
90 end
91 },
92}
93
94--- print usage info
95_.usage = function( args, -- command line arguments that where passed to the script, including the name
96 options, -- describes all the command line options
97 all -- print the lot
98 )
99 local h = ''
100 for k, v in pairs(options) do
101 if 'table' == type(v) then h = h .. k .. ' | ' end
102 end
103 for k, v in pairs(optionsCommon) do
104 if 'table' == type(v) then h = h .. k .. ' | ' end
105 end
106 P('Usage: ' .. args[0] .. ' {' .. h:sub(1, -2) .. '}')
107 if true == all then
108 for k, v in pairs(options) do
109 if 'table' == type(v) then
110 if nil ~= v.help then P(k .. '\t\t' .. v.help) end
111 end
112 end
113 for k, v in pairs(optionsCommon) do
114 if 'table' == type(v) then
115 if nil ~= v.help then P(k .. '\t\t' .. v.help) end
116 end
117 end
118 end
119end
120
121--- parse command line options
122_.parse = function( args, -- command line arguments that where passed to the script
123 options, -- describes all the command line options
124 name -- name to use for finding config file
125 )
126 local o = nil
127
128 local doIt = function(name, val, a, args, i)
129 local o = options[name]
130 if nil == o then o = optionsCommon[name] end
131 if nil ~= o then
132 if nil ~= val then o.value = val; D(name .. ' = ' .. tostring(val)) end
133 if nil ~= o.func then o:func(options, a, args, i, name) end
134 end
135 return o
136 end
137
138 if nil ~= name then
139 for i,v in ipairs{'/etc/', '~/.', './.'} do
140 local p = v .. name .. '.conf.lua'
141 local h = io.open(p, 'r')
142 if nil ~= h then
143 D('Found configuration file '.. p)
144 h:close()
145 local ar = dofile(p)
146 for k, w in pairs(ar) do
147 if nil == doIt(k, w, k .. '=' .. tostring(w), args, i) then W('config variable not found ' .. k .. ' = ' .. tostring(w)) end
148 end
149 end
150 end
151 end
152
153 if (0 == #args) and (nil ~= options['']) then table.insert(args, '') end
154 if 0 ~= #args then
155 for i,a in ipairs(args) do
156 D('Argument ' .. i .. ' = ' .. a)
157 local ds = 0
158 if ('-' == a:sub(1, 1)) and ('-' ~= a:sub(2, 2)) then ds = 1 end
159 if '--' == a:sub(1, 2) then ds = 2; a = a:sub(3, -1) end
160 local s, e = a:find('=', 1, true)
161 local k , v
162 if not s then
163 e = 0
164 v = nil
165 else
166 v = a:sub(e + 1, -1)
167 end
168 k = a:sub(1, e - 1)
169 if 1 == ds then
170 for j = 2, #k do
171 o = doIt('-' .. k:sub(j, j), v, a, args, i)
172 end
173 else
174 o = doIt(k, v, a, args, i)
175 end
176 end
177 end
178
179 if nil == o then
180 _.usage(args, options)
181 os.exit(0)
182 end
183end
184
185
186--- run a shell command, return the output in a table
187_.readCmd = function(cmd)
188 local result = {}
189 --[[
190 io.popen gives us a read file handle for 'r' and a write one for 'w'
191 os.execute gives us some sort of status code, plus other things in other Lua versions, but no in or out.
192 ]]
193 local output = io.popen(cmd, 'r')
194 if nil ~= output then
195 for l in output:lines() do
196 table.insert(result, l)
197 end
198 end
199 output:close()
200 return result
201end
202
203
204
205--- funky executable wrapper, might even call it a class
206-- @alias exe
207__ = function(c -- main command, or commands in a multiline string, or list of commands in a table, or a #! script
208 )
209 local exe = {status = 0, lines = {}, logging = false, showing = false, cmd = '', command = c, isScript = false, script = ''}
210 local n = 0
211
212 exe.cmd = '{ '
213 if 'table' == type(c) then
214 for i, l in ipairs(c) do
215 n = n + 1
216 exe.cmd = exe.cmd .. l .. ' ; '
217 end
218 elseif 'string' == type(c) then
219 exe.isScript = (n == 0) and ('#!' == c:sub(1,2))
220 for l in c:gmatch('\n*([^\n]+)\n*') do
221 if '' ~= l then
222 if exe.isScript then
223 if '' == exe.script then
224 exe.scriptFile = os.tmpname()
225 D('Creating temporary script file at ' .. exe.scriptFile)
226 exe.cmd = exe.cmd .. l:sub(3) .. ' ' .. exe.scriptFile .. ' ; '
227 -- PHP wants this to be executable.
228 __('chmod u+x ' .. exe.scriptFile)
229 end
230 exe.script = exe.script .. l .. '\n'
231 else
232 n = n + 1
233 exe.cmd = exe.cmd .. l .. ' ; '
234 end
235 end
236 end
237 end
238 if exe.isScript then
239 local a, e = io.open(exe.scriptFile, 'w')
240 if nil == a then E('Could not open ' .. exe.scriptFile .. ' - ' .. e) else
241 a:write(exe.script)
242 a:close()
243 end
244-- exe.cmd = exe.cmd .. 'rm ' .. exe.scriptFile .. ' ; '
245 end
246 exe.cmd = exe.cmd .. ' } '
247 if 1 == n then exe.cmd = c end
248
249
250 --- run this command under ionice and nice
251 function exe:Nice(c)
252 if nil == c then
253 self.cmd = 'ionice -c3 nice -n 19 ' .. self.cmd
254 else
255 self.cmd = self.cmd .. ' ionice -c3 nice -n 19 ' .. c .. ' '
256 end
257 return self
258 end
259
260 --- run this command under timeout
261 function exe:timeout(c)
262 if nil == c then
263 -- timeout returns a status of - command status if --preserve-status; "128+9" (actually 137) if --kill-after ends up being done; 124 if it had to TERM; command status if all went well.
264 -- --kill-after means "send KILL after TERM fails".
265 self.cmd = 'timeout --kill-after=10.0 --foreground 42.0s ' .. self.cmd
266 else
267 self.cmd = 'timeout --kill-after=10.0 --foreground ' .. c .. ' ' .. self.cmd
268 end
269 return self
270 end
271
272 --- enable logging the command line
273 function exe:log() self.logging = true return self end
274 --- enable showing the command output
275 function exe:show() self.showing = true return self end
276 --- run this command after the last one runs
277 function exe:Then(c) if nil == c then c = '' else c = ' ' .. c end self.cmd = self.cmd .. '; ' .. c .. ' ' return self end
278 --- run this command after the last one runs, if it worked
279 function exe:And(c) if nil == c then c = '' else c = ' ' .. c end self.cmd = self.cmd .. ' && ' .. c .. ' ' return self end
280 --- run this command after the last one runs, if it failed
281 function exe:Or(c) if nil == c then c = '' else c = ' ' .. c end self.cmd = self.cmd .. ' || ' .. c .. ' ' return self end
282 --- discard stderr
283 function exe:noErr() self.cmd = self.cmd .. ' 2>/dev/null ' return self end
284 --- discard stdout
285 function exe:noOut() self.cmd = self.cmd .. ' 1>/dev/null ' return self end
286 --- if the command worked, touch the w file, which is being waited on elsewhere, maybe
287 function exe:wait(w) self.cmd = self.cmd .. ' && touch ' .. w .. ' ' return self end
288
289 --- actually run the command.
290 function exe:Do()
291 --[[ "The condition expression of a control structure can return any
292 value. Both false and nil are considered false. All values different
293 from nil and false are considered true (in particular, the number 0
294 and the empty string are also true)."
295 says the docs, I beg to differ.]]
296 if true == self.logging then D(' executing - ' .. self.cmd) end
297 self.lines = _.readCmd(self.cmd .. '; echo "$?"', 'r')
298 -- The last line will be the command's returned status, fish that out and collect everything else in lines.
299 self.status = tonumber(self.lines[#self.lines])
300 self.lines[#self.lines] = nil
301 if true == self.showing then for i, l in ipairs(self.lines) do I(l) end end
302
303 if (nil == self.status) then D('STATUS |' .. 'NIL' .. '| ' .. self.command)
304 elseif (137 == self.status) or (124 == self.status) then T('timeout killed ' .. self.status .. ' ' .. self.command)
305 elseif (0 ~= self.status) then D('STATUS |' .. self.status .. '| ' .. self.command)
306 end
307
308 if nil ~= exe.scriptFile then os.execute('rm ' .. exe.scriptFile) end
309
310 return self
311 end
312
313-- TODO - currently after is a string that is run after the command. Could be a Lua function to call.
314
315 --- fork the command
316 function exe:fork(after, host)
317 if nil == after then after = '' end
318 if '' ~= after then after = ' ; ' .. after end
319-- The host part is from apt-panopticon, likely needed there, but makes no sense here.
320-- if nil ~= host then self.cmd = self.cmd .. '; r=$?; if [ $r -ge 124 ]; then echo "$r ' .. host .. ' failed forked command ' .. self.cmd:gsub(, '"', "'") .. '"; fi' end
321 self.cmd = '{ ' .. self.cmd .. after .. ' ; } & '
322 if true == self.logging then D(' forking - ' .. self.cmd) end
323 os.execute(self.cmd)
324 return self
325 end
326
327 --- fork the command, unless it's already running
328 function exe:forkOnce()
329 if _.running(self.command) then
330 D('Already running ' .. self.command)
331 else
332 self:fork()
333 end
334 end
335
336 return exe
337end
338
339
340
341--- dereference an array
342-- A simple table.subtable = subtable wont work, you end up with a reference so that changes to the later get applied to the former.
343-- On the other hand, this isn't going deep, only the top layer if there is sub tables.
344_.derefiTable = function(t)
345 local argh = {}
346 for l, y in ipairs(t) do if (l ~= y.name) then table.insert(argh, y) end end
347 return argh
348end
349--- dereference a table
350_.derefTable = function(t)
351 local argh = {}
352 for l, y in pairs(t) do argh[l] = y end
353 return argh
354end
355
356--- Does this file exist?
357_.exists = function(f)
358 local h = io.open(f, 'r')
359 if nil == h then return false else h:close(); return true end
360end
361--- Is this command runnable?
362_.runnable = function(c) return ( 0 == __('which ' .. c):Do().status ) end
363--- Is this command running?
364_.running = function(c) return ( 1 ~= tonumber(__('pgrep -u $USER -cf ' .. c):Do().lines[1]) ) end
365--- pkill all
366_.killEmAll = function(all -- table of command names to pkill
367 )
368 for i,l in ipairs(all) do
369 local c = 0
370 while 0 ~= tonumber(__('pgrep -u $USER -xc ' .. l):Do().lines[1]) do
371 local s = 'TERM'
372 if c > 1 then s = 'KILL'; __('sleep ' .. c):Do() end
373 __('pkill -' .. s .. ' -u $USER -x ' .. l):log():Do()
374 c = c + 1
375 end
376 end
377end
378
379--- execute whoami and return the result
380_.who = __[[whoami]]:noErr():Do().lines[1]
381--- execute pwd and return the result
382_.dir = __[[pwd]]:noErr():Do().lines[1]
383
384--- write a string to a file
385_.string2file = function(s, f)
386 local a, e = io.open(f, 'w')
387 if nil == a then E('Could not open ' .. f .. ' - ' .. e) else
388 a:write(s)
389 a:close()
390 end
391end
392
393--- dump a table to a pretty string
394_.table2string = function (table, -- table to dump
395 name, -- name of table
396 space -- Optional, used internally for sub tables.
397 )
398 if nil == space then space = '' end
399 local r = space
400 if '' == space then r = r .. name .. ' =\n' else r = r .. '[' .. name .. '] =\n' end
401 r = r .. space .. '{\n' .. _.table2stringSub(table, space .. ' ') .. space .. '}'
402 if '' ~= space then r = r .. ',' end
403 return r .. '\n'
404end
405_.table2stringSub = function (table, space)
406 local r = ''
407 for k, v in pairs(table) do
408 if type(k) == 'string' then k = "'" .. k .. "'" end
409 if type(v) == 'table' then r = r .. _.table2string(v, k, space)
410 elseif type(v) == 'string' then
411 local bq, eq = "'", "'"
412 if nil ~= v:find(bq, 1, true) then
413 bq, eq = '[=[', ']=]'
414 end
415 if nil ~= v:find(bq, 1, true) then
416 bq, eq = '[==[', ']==]'
417 if nil ~= v:find(bq, 1, true) then
418 bq, eq = '[===[', ']===]'
419 mbq, meq = '%[%[', '%]%]'
420 while (nil ~= v:match(mbq)) or (nil ~= v:match(meq)) do
421 bq = '[' .. '=' .. bq:sub(2, -1)
422 eq = ']' .. '=' .. eq:sub(2, -1)
423 mbq = '%[' .. '=' .. bq:sub(3, -1)
424 meq = '%]' .. '=' .. eq:sub(3, -1)
425 end
426 end
427 end
428 r = r .. space .. '[' .. k .. '] = ' .. bq .. v .. eq .. ';\n'
429 elseif type(v) == 'function' then r = r .. space .. '[' .. k .. '] = function ();\n'
430 elseif type(v) == 'userdata' then r = r .. space .. 'userdata ' .. '[' .. k .. '];\n'
431 elseif type(v) == 'boolean' then
432 if (v) then r = r .. space .. '[' .. k .. '] = true;\n'
433 else r = r .. space .. '[' .. k .. '] = false;\n'
434 end
435 else r = r .. space .. '[' .. k .. '] = ' .. v .. ';\n'
436 end
437 end
438 return r
439end
440
441
442
443-- Deal with being called directly.
444if (arg[0] == './polygLua.lua') or (arg[0] == 'polygLua.lua') then
445 local function goAway(txt)
446 local luas = __'ls -d1 /usr/share/lua/*':noErr():Do()
447 for i,l in ipairs(luas.lines) do
448 local lua = '/usr/local/share/lua/' .. l:sub(16) .. '/polygLua.lua'
449 if _.exists(lua) then
450 if 'root' == _.who then
451 I(txt .. ' ' .. lua)
452 __('rm ' .. lua):Do()
453 end
454 end
455 end
456 end
457
458 local options =
459 {
460 install =
461 {
462 help = 'Command to install polygLua.lua',
463 func = function(self, options, a, args, i)
464 if 'root' ~= _.who then
465 E'Need to be root user to install.'
466 else
467 I'INSTALLING polygLua.lua!!!'
468
469 local luas = __'ls -d1 /usr/share/lua/*':noErr():Do()
470 for i,l in ipairs(luas.lines) do
471 local lua = '/usr/local/share/lua/' .. l:sub(16) .. '/polygLua.lua'
472 if _.exists(lua) then
473 P(lua .. ' installed')
474 else
475 if 'root' == _.who then
476 P('Installing ' .. lua)
477 __('mkdir -p /usr/local/share/lua/' .. l:sub(16) .. ' ; ln -s ' .. _.dir .. '/polygLua.lua ' .. lua):Do()
478 else
479 P(lua .. ' NOT installed')
480 end
481 end
482 end
483
484 end
485 end
486 },
487 uninstall =
488 {
489 help = 'Command to uninstall polygLua.lua',
490 func = function(self, options, a, args, i)
491 if 'root' ~= _.who then
492 E'Need to be root user to uninstall.'
493 else
494 P'UNINSTALLING polygLua.lua!!!'
495 goAway('Uninstalling')
496 end
497 end
498 },
499 purge =
500 {
501 help = 'Command to purge polygLua.lua',
502 func = function(self, options, a, args, i)
503 if 'root' ~= _.who then
504 E'Need to be root user to purge.'
505 else
506 P'PURGING polygLua.lua!!!'
507 goAway('Purging')
508 end
509 end
510 },
511 }
512
513 _.parse(arg, options)
514end
515
516
517
518return _