diff --git a/common/.package b/common/.package index 4ffb026..b70d53a 100644 --- a/common/.package +++ b/common/.package @@ -13,7 +13,7 @@ * and more... ]], license = 'MIT', - required = { + required = { 'core', }, } diff --git a/compress/apis/lzw.lua b/compress/apis/lzw.lua deleted file mode 100644 index 3d4c4b5..0000000 --- a/compress/apis/lzw.lua +++ /dev/null @@ -1,142 +0,0 @@ --- see: https://github.com/Rochet2/lualzw --- MIT License - Copyright (c) 2016 Rochet2 - -local char = string.char -local type = type -local sub = string.sub -local tconcat = table.concat - -local SIGC = 'LZWC' - -local basedictcompress = {} -local basedictdecompress = {} -for i = 0, 255 do - local ic, iic = char(i), char(i, 0) - basedictcompress[ic] = iic - basedictdecompress[iic] = ic -end - -local function dictAddA(str, dict, a, b) - if a >= 256 then - a, b = 0, b+1 - if b >= 256 then - dict = {} - b = 1 - end - end - dict[str] = char(a,b) - a = a+1 - return dict, a, b -end - -local function compress(input) - if type(input) ~= "string" then - error ("string expected, got "..type(input)) - end - local len = #input - if len <= 1 then - return input - end - - local dict = {} - local a, b = 0, 1 - - local result = { SIGC } - local resultlen = 1 - local n = 2 - local word = "" - for i = 1, len do - local c = sub(input, i, i) - local wc = word..c - if not (basedictcompress[wc] or dict[wc]) then - local write = basedictcompress[word] or dict[word] - if not write then - error "algorithm error, could not fetch word" - end - result[n] = write - resultlen = resultlen + #write - n = n+1 - if len <= resultlen then - return input - end - dict, a, b = dictAddA(wc, dict, a, b) - word = c - else - word = wc - end - end - result[n] = basedictcompress[word] or dict[word] - resultlen = resultlen+#result[n] - if len <= resultlen then - return input - end - return tconcat(result) -end - -local function dictAddB(str, dict, a, b) - if a >= 256 then - a, b = 0, b+1 - if b >= 256 then - dict = {} - b = 1 - end - end - dict[char(a,b)] = str - a = a+1 - return dict, a, b -end - -local function decompress(input) - if type(input) ~= "string" then - error( "string expected, got "..type(input)) - end - - if #input <= 1 then - return input - end - - local control = sub(input, 1, 4) - if control ~= SIGC then - return input - end - input = sub(input, 5) - local len = #input - - if len < 2 then - error("invalid input - not a compressed string") - end - - local dict = {} - local a, b = 0, 1 - - local result = {} - local n = 1 - local last = sub(input, 1, 2) - result[n] = basedictdecompress[last] or dict[last] - n = n+1 - for i = 3, len, 2 do - local code = sub(input, i, i+1) - local lastStr = basedictdecompress[last] or dict[last] - if not lastStr then - error( "could not find last from dict. Invalid input?") - end - local toAdd = basedictdecompress[code] or dict[code] - if toAdd then - result[n] = toAdd - n = n+1 - dict, a, b = dictAddB(lastStr..sub(toAdd, 1, 1), dict, a, b) - else - local tmp = lastStr..sub(lastStr, 1, 1) - result[n] = tmp - n = n+1 - dict, a, b = dictAddB(tmp, dict, a, b) - end - last = code - end - return tconcat(result) -end - -return { - compress = compress, - decompress = decompress, -} diff --git a/compress/apis/tar.lua b/compress/apis/tar.lua deleted file mode 100644 index 83188b1..0000000 --- a/compress/apis/tar.lua +++ /dev/null @@ -1,212 +0,0 @@ - --- see: https://github.com/luarocks/luarocks/blob/master/src/luarocks/tools/tar.lua --- A pure-Lua implementation of untar (unpacking .tar archives) -local Util = require('opus.util') - -local fs = _G.fs - -local blocksize = 512 - -local function get_typeflag(flag) - if flag == "0" or flag == "\0" then return "file" - elseif flag == "1" then return "link" - elseif flag == "2" then return "symlink" -- "reserved" in POSIX, "symlink" in GNU - elseif flag == "3" then return "character" - elseif flag == "4" then return "block" - elseif flag == "5" then return "directory" - elseif flag == "6" then return "fifo" - elseif flag == "7" then return "contiguous" -- "reserved" in POSIX, "contiguous" in GNU - elseif flag == "x" then return "next file" - elseif flag == "g" then return "global extended header" - elseif flag == "L" then return "long name" - elseif flag == "K" then return "long link name" - end - return "unknown" -end - -local function octal_to_number(octal) - local exp = 0 - local number = 0 - octal = octal:gsub("%s", "") - for i = #octal,1,-1 do - local digit = tonumber(octal:sub(i,i)) - if not digit then - break - end - number = number + (digit * 8^exp) - exp = exp + 1 - end - return number -end - -local function checksum_header(block) - local sum = 256 - for i = 1,148 do - local b = block:byte(i) or 0 - sum = sum + b - end - for i = 157,500 do - local b = block:byte(i) or 0 - sum = sum + b - end - return sum -end - -local function nullterm(s) - return s:match("^[^%z]*") -end - -local function read_header_block(block) - local header = {} - header.name = nullterm(block:sub(1,100)) - header.mode = nullterm(block:sub(101,108)):gsub(" ", "") - header.uid = octal_to_number(nullterm(block:sub(109,116))) - header.gid = octal_to_number(nullterm(block:sub(117,124))) - header.size = octal_to_number(nullterm(block:sub(125,136))) - header.mtime = octal_to_number(nullterm(block:sub(137,148))) - header.chksum = octal_to_number(nullterm(block:sub(149,156))) - header.typeflag = get_typeflag(block:sub(157,157)) - header.linkname = nullterm(block:sub(158,257)) - header.magic = block:sub(258,263) - header.version = block:sub(264,265) - header.uname = nullterm(block:sub(266,297)) - header.gname = nullterm(block:sub(298,329)) - header.devmajor = octal_to_number(nullterm(block:sub(330,337))) - header.devminor = octal_to_number(nullterm(block:sub(338,345))) - header.prefix = block:sub(346,500) - if not checksum_header(block) == header.chksum then - return false, "Failed header checksum" - end - return header -end - -local function untar(filename, destdir, verbose) - assert(type(filename) == "string") - assert(type(destdir) == "string") - - local tar_handle = io.open(filename, "rb") - if not tar_handle then return nil, "Error opening file "..filename end - - local long_name, long_link_name - local ok, err - - local make_dir = function(a) - if not fs.exists(a) then - fs.makeDir(a) - end - return true - end - - while true do - local block - repeat - block = tar_handle:read(blocksize) - until (not block) or checksum_header(block) > 256 - if not block then break end - if #block < blocksize then - ok, err = nil, "Invalid block size -- corrupted file?" - break - end - local header - header, err = read_header_block(block) - if not header then - ok = false - break - end - - local file_data = tar_handle:read(math.ceil(header.size / blocksize) * blocksize):sub(1,header.size) - - if header.typeflag == "long name" then - long_name = nullterm(file_data) - elseif header.typeflag == "long link name" then - long_link_name = nullterm(file_data) - else - if long_name then - header.name = long_name - long_name = nil - end - if long_link_name then - header.name = long_link_name - long_link_name = nil - end - end - local pathname = fs.combine(destdir, header.name) - - if header.typeflag == "directory" then - ok, err = make_dir(pathname) - if not ok then - break - end - elseif header.typeflag == "file" then - local dirname = fs.getDir(pathname) - if dirname ~= "" then - ok, err = make_dir(dirname) - if not ok then - break - end - end - local file_handle - if verbose then - print(pathname) - end - file_handle, err = io.open(pathname, "wb") - if not file_handle then - ok = nil - break - end - file_handle:write(file_data) - file_handle:close() - end - end - tar_handle:close() - return ok, err -end - -local function create_header_block(filename, abspath) - local block = ('\0'):rep(blocksize) - - local function number_to_octal(n) - return ('%o'):format(n) - end - - local function ins(pos, istr) - block = block:sub(1, pos - 1) .. istr .. block:sub(pos + #istr) - end - - ins(1, filename) -- header - ins(125, number_to_octal(fs.getSize(abspath))) - ins(157, '0') -- typeflag - - ins(149, number_to_octal(checksum_header(block))) - - return block -end - --- the bare minimum for this program to untar -local function tar(filename, root, files) - assert(type(filename) == "string") - assert(type(root) == "string") - assert(type(files) == "table") - - local tar_handle = io.open(filename, "wb") - if not tar_handle then return nil, "Error opening file "..filename end - - for _, file in pairs(files) do - local abs = fs.combine(root, file) - local block = create_header_block(file, abs) - tar_handle:write(block) - local f = Util.readFile(abs, 'rb') - tar_handle:write(f) - local padding = #f % blocksize - if padding > 0 then - tar_handle:write(('\0'):rep(blocksize - padding)) - end - end - tar_handle:close() - return true -end - -return { - tar = tar, - untar = untar, -} diff --git a/compress/compress.lua b/compress/compress.lua index f98d294..4ca69a1 100644 --- a/compress/compress.lua +++ b/compress/compress.lua @@ -1,14 +1,10 @@ -local LZW = require('compress.lzw') -local Tar = require('compress.tar') +local LZW = require('opus.compress.lzw') +local Tar = require('opus.compress.tar') local Util = require('opus.util') -local fs = _G.fs local shell = _ENV.shell -local TMP_FILE = '.out.tar' - local args = { ... } -local files = { } if not args[2] then error('Syntax: tar OUTFILE DIR') @@ -24,30 +20,10 @@ elseif file:match('(.+)%.lzw$') then filetype = 'lzw' end -local function recurse(rel) - local abs = fs.combine(dir, rel) - for _,f in ipairs(fs.list(abs)) do - local fullName = fs.combine(abs, f) - if fs.native.isDir(fullName) then -- skip virtual dirs - recurse(fs.combine(rel, f)) - else - table.insert(files, fs.combine(rel, f)) - end - end -end -recurse('') - if filetype == 'tar' then - Tar.tar(file, dir, files) + Tar.tar(file, dir) elseif filetype == 'lzw' then - fs.mount(TMP_FILE, 'ramfs', 'file') - Tar.tar(TMP_FILE, dir, files) - - local c = Util.readFile(TMP_FILE) - fs.delete(TMP_FILE) - - c = LZW.compress(c) - Util.writeFile(file, c) + local c = Tar.tar_string(dir) + Util.writeFile(file, LZW.compress(c), 'wb') end - diff --git a/compress/uncompress.lua b/compress/uncompress.lua index 0d64203..abed47a 100644 --- a/compress/uncompress.lua +++ b/compress/uncompress.lua @@ -1,14 +1,11 @@ local DEFLATE = require('compress.deflatelua') -local LZW = require('compress.lzw') -local Tar = require('compress.tar') +local LZW = require('opus.compress.lzw') +local Tar = require('opus.compress.tar') local Util = require('opus.util') -local fs = _G.fs local io = _G.io local shell = _ENV.shell -local TMP_FILE = '.out.tar' - local args = { ... } if not args[2] then @@ -18,49 +15,40 @@ end local inFile = shell.resolve(args[1]) local outDir = shell.resolve(args[2]) -local s, m = pcall(function() - if inFile:match('(.+)%.[gG][zZ]$') then - local fh = io.open(inFile, 'rb') or error('Error opening ' .. inFile) +if inFile:match('(.+)%.[gG][zZ]$') then + -- uncompress a file created with: tar czf ... + local fh = io.open(inFile, 'rb') or error('Error opening ' .. inFile) - fs.mount(TMP_FILE, 'ramfs', 'file') - local ofh = io.open(TMP_FILE, 'wb') - - DEFLATE.gunzip {input=fh, output=ofh, disable_crc=true} - - fh:close() - ofh:close() - - local s, m = Tar.untar(TMP_FILE, outDir, true) - - if not s then - error(m) - end - - elseif inFile:match('(.+)%.lzw$') then - local c = Util.readFile(inFile) - if not c then - error('Unable to open ' .. inFile) - end - - fs.mount(TMP_FILE, 'ramfs', 'file') - Util.writeFile(TMP_FILE, LZW.decompress(c)) - - local s, m = Tar.untar(TMP_FILE, outDir, true) - - if not s then - error(m) - end - - else - local s, m = Tar.untar(inFile, outDir) - if not s then - error(m) - end + local t = { } + local function writer(b) + table.insert(t, b) end -end) -fs.delete(TMP_FILE) + DEFLATE.gunzip {input=fh, output=writer, disable_crc=true} -if not s then - error(m) + fh:close() + + local s, m = Tar.untar_string(string.char(table.unpack(t)), outDir, true) + + if not s then + error(m) + end + +elseif inFile:match('(.+)%.tar%.lzw$') then + local c = Util.readFile(inFile, 'rb') + if not c then + error('Unable to open ' .. inFile) + end + + local s, m = Tar.untar_string(LZW.decompress(c), outDir, true) + + if not s then + error(m) + end + +else + local s, m = Tar.untar(inFile, outDir, true) + if not s then + error(m) + end end diff --git a/debugger/apis/init.lua b/debugger/apis/init.lua index a7b97d3..1e860b8 100644 --- a/debugger/apis/init.lua +++ b/debugger/apis/init.lua @@ -9,33 +9,12 @@ local dbg = { breakpoints = nil, } -local romFiles = { - load = function(self) - local function recurse(dir) - local files = fs.list(dir) - for _,f in ipairs(files) do - local fullName = fs.combine(dir, f) - if fs.isDir(fullName) then - recurse(fullName) - else - self.files[f] = fullName - end - end - end - recurse('rom/apis') - end, - get = function(self, file) - return self.files[file] - end, - files = { }, -} -romFiles:load() - local function breakpointHook(info) if dbg.breakpoints then - local src = romFiles:get(info.short_src) or info.short_src + local src = info.short_src for _,v in pairs(dbg.breakpoints) do - if v.line == info.currentline and v.file == src then + if v.line == info.currentline + and (v.file == src or v.bfile == src) then return not v.disabled end end diff --git a/debugger/debug.lua b/debugger/debug.lua index d362227..c0f57d2 100644 --- a/debugger/debug.lua +++ b/debugger/debug.lua @@ -50,7 +50,7 @@ local function startClient() os.sleep(0) -- not sure why, but we need a sleep before :resume -- directly resuming debugger routine to prevent -- serialization of the snapshot - dbg.debugger:resume('debuggerX', dbg.debugger.uid, snapshot) + dbg.debugger:resume('debuggerX', 'break', snapshot) local e, cmd, param repeat e, cmd, param = os.pullEvent('debugger') @@ -65,15 +65,27 @@ local function startClient() dbg.debugger = debugger dbg.stopIn(fn) local s, m = dbg.call(fn, table.unpack(args)) + + dbg.debugger:resume('debuggerX', 'disconnect') + if not s then error(m, -1) end + print('Process ended normally') + print('Press enter to exit') + while true do + local e, code = os.pullEventRaw('key') + if e == 'terminate' or e == 'key' and code == _G.keys.enter then + break + end + end end, }) client = kernel.find(clientId) end local romFiles = { + files = { }, load = function(self) local function recurse(dir) local files = fs.list(dir) @@ -87,11 +99,14 @@ local romFiles = { end end recurse('rom/apis') + self.reversed = Util.transpose(self.files) end, get = function(self, file) return self.files[file] end, - files = { }, + lookup = function(self, file) + return self.reversed[file] + end, } romFiles:load() @@ -137,7 +152,7 @@ local page = UI.Page { disableHeader = true, unfocusedBackgroundSelectedColor = 'black', columns = { - { heading = 'Key', key = 'name' }, + { heading = 'localname', key = 'name' }, { heading = 'Value', key = 'value', textColor = 'yellow' }, }, autospace = true, @@ -214,6 +229,13 @@ local page = UI.Page { { heading = 'Name', key = 'short' }, { heading = 'Path', key = 'path', textColor = 'lightGray' }, }, + getDisplayValues = function(_, row) + return { + line = row.line, + short = fs.getName(row.file), + path = fs.getDir(row.file), + } + end, getRowTextColor = function(self, row, selected) return row.disabled and 'lightGray' or UI.Grid.getRowTextColor(self, row, selected) @@ -438,8 +460,7 @@ local page = UI.Page { table.insert(breakpoints, { file = event.file, line = event.line, - short = fs.getName(event.file), - path = fs.getDir(event.file), + bfile = romFiles:lookup(event.file), }) self:emit({ type = 'update_breakpoints' }) @@ -487,8 +508,12 @@ local page = UI.Page { end, } -Event.on('debuggerX', function(_, uid, data) - if uid == debugger.uid then +Event.on('debuggerX', function(_, cmd, data) + if cmd == 'disconnect' then + page.statusBar:setStatus('Finished') + page:sync() + + elseif cmd == 'break' then kernel.raise(debugger.uid) -- local tab diff --git a/lpeg/.package b/lpeg/.package new file mode 100644 index 0000000..d134966 --- /dev/null +++ b/lpeg/.package @@ -0,0 +1,9 @@ +{ + title = 'LPeg', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/lpeg', + description = [[A pure Lua port of LPeg, Roberto Ierusalimschy's Parsing Expression Grammars library. + +This version of LuLPeg emulates LPeg v0.12. +see: https://github.com/pygy/LuLPeg/]], + license = 'MIT', +} \ No newline at end of file diff --git a/lpeg/etc/fstab b/lpeg/etc/fstab new file mode 100644 index 0000000..b4336b2 --- /dev/null +++ b/lpeg/etc/fstab @@ -0,0 +1 @@ +rom/modules/main/lpeg.lua urlfs https://raw.githubusercontent.com/pygy/LuLPeg/master/lulpeg.lua diff --git a/moonscript/.package b/moonscript/.package new file mode 100644 index 0000000..e347a82 --- /dev/null +++ b/moonscript/.package @@ -0,0 +1,11 @@ +{ + title = 'moonscript', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/moonscript', + description = [[MoonScript is a programmer friendly language that compiles into Lua. It gives you the power of the fastest scripting language combined with a rich set of features. It runs on Lua 5.1 and above, including alternative runtimes like LuaJIT. + +See https://moonscript.org.]], + license = 'MIT', + required = { + 'lpeg', + }, +} diff --git a/moonscript/README.txt b/moonscript/README.txt new file mode 100644 index 0000000..d306970 --- /dev/null +++ b/moonscript/README.txt @@ -0,0 +1,5 @@ +running the compiler works fine... +moonc T.moon <-- OK + +working on getting the moon command to work properly +moon T.moon <-- NOPE diff --git a/moonscript/T.lua b/moonscript/T.lua new file mode 100644 index 0000000..8825f17 --- /dev/null +++ b/moonscript/T.lua @@ -0,0 +1,97 @@ +local Event = require('opus.event') +local UI = require('opus.ui') +local kernel = _G.kernel +local multishell = _ENV.multishell +local tasks = multishell and multishell.getTabs and multishell.getTabs() or kernel.routines +UI:configure('Tasks', ...) +local page = UI.Page({ + menuBar = UI.MenuBar({ + buttons = { + { + text = 'Activate', + event = 'activate' + }, + { + text = 'Terminate', + event = 'terminate' + }, + { + text = 'Inspect', + event = 'inspect' + } + } + }), + grid = UI.ScrollingGrid({ + y = 2, + columns = { + { + heading = 'ID', + key = 'uid', + width = 3 + }, + { + heading = 'Title', + key = 'title' + }, + { + heading = 'Status', + key = 'status' + }, + { + heading = 'Time', + key = 'timestamp' + } + }, + values = tasks, + sortColumn = 'uid', + autospace = true, + getDisplayValues = function(self, row) + local elapsed = os.clock() - row.timestamp + return { + uid = row.uid, + title = row.title, + status = row.isDead and 'error' or coroutine.status(row.co), + timestamp = elapsed < 60 and string.format("%ds", math.floor(elapsed)) or string.format("%sm", math.floor(elapsed / 6) / 10) + } + end + }), + accelerators = { + ['control-q'] = 'quit', + [' '] = 'activate', + t = 'terminate' + }, + eventHandler = function(self, event) + local t = self.grid:getSelected() + local _exp_0 = event.type + if 'activate' == _exp_0 or 'grid_select' == _exp_0 then + if t then + return multishell.setFocus(t.uid) + end + elseif 'terminate' == _exp_0 then + if t then + return multishell.terminate(t.uid) + end + elseif 'inspect' == _exp_0 then + if t then + return multishell.openTab(_ENV, { + path = 'sys/apps/Lua.lua', + args = { + t + }, + focused = true + }) + end + elseif 'quit' == _exp_0 then + return UI:quit() + else + return UI.Page.eventHandler(self, event) + end + end +}) +Event.onInterval(1, function() + page.grid:update() + page.grid:draw() + return page:sync() +end) +UI:setPage(page) +return UI:start() diff --git a/moonscript/T.moon b/moonscript/T.moon new file mode 100644 index 0000000..6cbf5f4 --- /dev/null +++ b/moonscript/T.moon @@ -0,0 +1,70 @@ +Event = require('opus.event') +UI = require('opus.ui') + +kernel = _G.kernel +multishell = _ENV.multishell +tasks = multishell and multishell.getTabs and multishell.getTabs() or kernel.routines + +UI\configure 'Tasks', ... + +page = UI.Page { + menuBar: UI.MenuBar { + buttons: { + { text: 'Activate', event: 'activate' }, + { text: 'Terminate', event: 'terminate' }, + { text: 'Inspect', event: 'inspect' }, + }, + }, + grid: UI.ScrollingGrid { + y: 2, + columns: { + { heading: 'ID', key: 'uid', width: 3 }, + { heading: 'Title', key: 'title' }, + { heading: 'Status', key: 'status' }, + { heading: 'Time', key: 'timestamp' }, + }, + values: tasks, + sortColumn: 'uid', + autospace: true, + getDisplayValues: (row) => + elapsed = os.clock! - row.timestamp + { + uid: row.uid, + title: row.title, + status: row.isDead and 'error' or coroutine.status(row.co), + timestamp: elapsed < 60 and + string.format("%ds", math.floor(elapsed)) or + string.format("%sm", math.floor(elapsed/6)/10), + } + }, + accelerators: { + [ 'control-q' ]: 'quit', + [ ' ' ]: 'activate', + t: 'terminate', + }, + eventHandler: (event) => + t = self.grid\getSelected! + switch event.type + when 'activate', 'grid_select' + multishell.setFocus t.uid if t + when 'terminate' + multishell.terminate t.uid if t + when 'inspect' + multishell.openTab _ENV, { + path: 'sys/apps/Lua.lua', + args: { t }, + focused: true, + } if t + when 'quit' + UI\quit! + else + UI.Page.eventHandler(@, event) +} + +Event.onInterval 1, () -> + page.grid\update! + page.grid\draw! + page\sync! + +UI\setPage page +UI\start! diff --git a/moonscript/argparse.lua b/moonscript/argparse.lua new file mode 100644 index 0000000..de89237 --- /dev/null +++ b/moonscript/argparse.lua @@ -0,0 +1,1527 @@ +-- The MIT License (MIT) + +-- Copyright (c) 2013 - 2018 Peter Melnichenko + +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +-- the Software, and to permit persons to whom the Software is furnished to do so, +-- subject to the following conditions: + +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. + +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +local function deep_update(t1, t2) + for k, v in pairs(t2) do + if type(v) == "table" then + v = deep_update({}, v) + end + + t1[k] = v + end + + return t1 +end + +-- A property is a tuple {name, callback}. +-- properties.args is number of properties that can be set as arguments +-- when calling an object. +local function class(prototype, properties, parent) + -- Class is the metatable of its instances. + local cl = {} + cl.__index = cl + + if parent then + cl.__prototype = deep_update(deep_update({}, parent.__prototype), prototype) + else + cl.__prototype = prototype + end + + if properties then + local names = {} + + -- Create setter methods and fill set of property names. + for _, property in ipairs(properties) do + local name, callback = property[1], property[2] + + cl[name] = function(self, value) + if not callback(self, value) then + self["_" .. name] = value + end + + return self + end + + names[name] = true + end + + function cl.__call(self, ...) + -- When calling an object, if the first argument is a table, + -- interpret keys as property names, else delegate arguments + -- to corresponding setters in order. + if type((...)) == "table" then + for name, value in pairs((...)) do + if names[name] then + self[name](self, value) + end + end + else + local nargs = select("#", ...) + + for i, property in ipairs(properties) do + if i > nargs or i > properties.args then + break + end + + local arg = select(i, ...) + + if arg ~= nil then + self[property[1]](self, arg) + end + end + end + + return self + end + end + + -- If indexing class fails, fallback to its parent. + local class_metatable = {} + class_metatable.__index = parent + + function class_metatable.__call(self, ...) + -- Calling a class returns its instance. + -- Arguments are delegated to the instance. + local object = deep_update({}, self.__prototype) + setmetatable(object, self) + return object(...) + end + + return setmetatable(cl, class_metatable) +end + +local function typecheck(name, types, value) + for _, type_ in ipairs(types) do + if type(value) == type_ then + return true + end + end + + error(("bad property '%s' (%s expected, got %s)"):format(name, table.concat(types, " or "), type(value))) +end + +local function typechecked(name, ...) + local types = {...} + return {name, function(_, value) typecheck(name, types, value) end} +end + +local multiname = {"name", function(self, value) + typecheck("name", {"string"}, value) + + for alias in value:gmatch("%S+") do + self._name = self._name or alias + table.insert(self._aliases, alias) + end + + -- Do not set _name as with other properties. + return true +end} + +local function parse_boundaries(str) + if tonumber(str) then + return tonumber(str), tonumber(str) + end + + if str == "*" then + return 0, math.huge + end + + if str == "+" then + return 1, math.huge + end + + if str == "?" then + return 0, 1 + end + + if str:match "^%d+%-%d+$" then + local min, max = str:match "^(%d+)%-(%d+)$" + return tonumber(min), tonumber(max) + end + + if str:match "^%d+%+$" then + local min = str:match "^(%d+)%+$" + return tonumber(min), math.huge + end +end + +local function boundaries(name) + return {name, function(self, value) + typecheck(name, {"number", "string"}, value) + + local min, max = parse_boundaries(value) + + if not min then + error(("bad property '%s'"):format(name)) + end + + self["_min" .. name], self["_max" .. name] = min, max + end} +end + +local actions = {} + +local option_action = {"action", function(_, value) + typecheck("action", {"function", "string"}, value) + + if type(value) == "string" and not actions[value] then + error(("unknown action '%s'"):format(value)) + end +end} + +local option_init = {"init", function(self) + self._has_init = true +end} + +local option_default = {"default", function(self, value) + if type(value) ~= "string" then + self._init = value + self._has_init = true + return true + end +end} + +local add_help = {"add_help", function(self, value) + typecheck("add_help", {"boolean", "string", "table"}, value) + + if self._has_help then + table.remove(self._options) + self._has_help = false + end + + if value then + local help = self:flag() + :description "Show this help message and exit." + :action(function() + print(self:get_help()) + os.exit(0) + end) + + if value ~= true then + help = help(value) + end + + if not help._name then + help "-h" "--help" + end + + self._has_help = true + end +end} + +local Parser = class({ + _arguments = {}, + _options = {}, + _commands = {}, + _mutexes = {}, + _groups = {}, + _require_command = true, + _handle_options = true +}, { + args = 3, + typechecked("name", "string"), + typechecked("description", "string"), + typechecked("epilog", "string"), + typechecked("usage", "string"), + typechecked("help", "string"), + typechecked("require_command", "boolean"), + typechecked("handle_options", "boolean"), + typechecked("action", "function"), + typechecked("command_target", "string"), + typechecked("help_vertical_space", "number"), + typechecked("usage_margin", "number"), + typechecked("usage_max_width", "number"), + typechecked("help_usage_margin", "number"), + typechecked("help_description_margin", "number"), + typechecked("help_max_width", "number"), + add_help +}) + +local Command = class({ + _aliases = {} +}, { + args = 3, + multiname, + typechecked("description", "string"), + typechecked("epilog", "string"), + typechecked("target", "string"), + typechecked("usage", "string"), + typechecked("help", "string"), + typechecked("require_command", "boolean"), + typechecked("handle_options", "boolean"), + typechecked("action", "function"), + typechecked("command_target", "string"), + typechecked("help_vertical_space", "number"), + typechecked("usage_margin", "number"), + typechecked("usage_max_width", "number"), + typechecked("help_usage_margin", "number"), + typechecked("help_description_margin", "number"), + typechecked("help_max_width", "number"), + typechecked("hidden", "boolean"), + add_help +}, Parser) + +local Argument = class({ + _minargs = 1, + _maxargs = 1, + _mincount = 1, + _maxcount = 1, + _defmode = "unused", + _show_default = true +}, { + args = 5, + typechecked("name", "string"), + typechecked("description", "string"), + option_default, + typechecked("convert", "function", "table"), + boundaries("args"), + typechecked("target", "string"), + typechecked("defmode", "string"), + typechecked("show_default", "boolean"), + typechecked("argname", "string", "table"), + typechecked("hidden", "boolean"), + option_action, + option_init +}) + +local Option = class({ + _aliases = {}, + _mincount = 0, + _overwrite = true +}, { + args = 6, + multiname, + typechecked("description", "string"), + option_default, + typechecked("convert", "function", "table"), + boundaries("args"), + boundaries("count"), + typechecked("target", "string"), + typechecked("defmode", "string"), + typechecked("show_default", "boolean"), + typechecked("overwrite", "boolean"), + typechecked("argname", "string", "table"), + typechecked("hidden", "boolean"), + option_action, + option_init +}, Argument) + +function Parser:_inherit_property(name, default) + local element = self + + while true do + local value = element["_" .. name] + + if value ~= nil then + return value + end + + if not element._parent then + return default + end + + element = element._parent + end +end + +function Argument:_get_argument_list() + local buf = {} + local i = 1 + + while i <= math.min(self._minargs, 3) do + local argname = self:_get_argname(i) + + if self._default and self._defmode:find "a" then + argname = "[" .. argname .. "]" + end + + table.insert(buf, argname) + i = i+1 + end + + while i <= math.min(self._maxargs, 3) do + table.insert(buf, "[" .. self:_get_argname(i) .. "]") + i = i+1 + + if self._maxargs == math.huge then + break + end + end + + if i < self._maxargs then + table.insert(buf, "...") + end + + return buf +end + +function Argument:_get_usage() + local usage = table.concat(self:_get_argument_list(), " ") + + if self._default and self._defmode:find "u" then + if self._maxargs > 1 or (self._minargs == 1 and not self._defmode:find "a") then + usage = "[" .. usage .. "]" + end + end + + return usage +end + +function actions.store_true(result, target) + result[target] = true +end + +function actions.store_false(result, target) + result[target] = false +end + +function actions.store(result, target, argument) + result[target] = argument +end + +function actions.count(result, target, _, overwrite) + if not overwrite then + result[target] = result[target] + 1 + end +end + +function actions.append(result, target, argument, overwrite) + result[target] = result[target] or {} + table.insert(result[target], argument) + + if overwrite then + table.remove(result[target], 1) + end +end + +function actions.concat(result, target, arguments, overwrite) + if overwrite then + error("'concat' action can't handle too many invocations") + end + + result[target] = result[target] or {} + + for _, argument in ipairs(arguments) do + table.insert(result[target], argument) + end +end + +function Argument:_get_action() + local action, init + + if self._maxcount == 1 then + if self._maxargs == 0 then + action, init = "store_true", nil + else + action, init = "store", nil + end + else + if self._maxargs == 0 then + action, init = "count", 0 + else + action, init = "append", {} + end + end + + if self._action then + action = self._action + end + + if self._has_init then + init = self._init + end + + if type(action) == "string" then + action = actions[action] + end + + return action, init +end + +-- Returns placeholder for `narg`-th argument. +function Argument:_get_argname(narg) + local argname = self._argname or self:_get_default_argname() + + if type(argname) == "table" then + return argname[narg] + else + return argname + end +end + +function Argument:_get_default_argname() + return "<" .. self._name .. ">" +end + +function Option:_get_default_argname() + return "<" .. self:_get_default_target() .. ">" +end + +-- Returns labels to be shown in the help message. +function Argument:_get_label_lines() + return {self._name} +end + +function Option:_get_label_lines() + local argument_list = self:_get_argument_list() + + if #argument_list == 0 then + -- Don't put aliases for simple flags like `-h` on different lines. + return {table.concat(self._aliases, ", ")} + end + + local longest_alias_length = -1 + + for _, alias in ipairs(self._aliases) do + longest_alias_length = math.max(longest_alias_length, #alias) + end + + local argument_list_repr = table.concat(argument_list, " ") + local lines = {} + + for i, alias in ipairs(self._aliases) do + local line = (" "):rep(longest_alias_length - #alias) .. alias .. " " .. argument_list_repr + + if i ~= #self._aliases then + line = line .. "," + end + + table.insert(lines, line) + end + + return lines +end + +function Command:_get_label_lines() + return {table.concat(self._aliases, ", ")} +end + +function Argument:_get_description() + if self._default and self._show_default then + if self._description then + return ("%s (default: %s)"):format(self._description, self._default) + else + return ("default: %s"):format(self._default) + end + else + return self._description or "" + end +end + +function Command:_get_description() + return self._description or "" +end + +function Option:_get_usage() + local usage = self:_get_argument_list() + table.insert(usage, 1, self._name) + usage = table.concat(usage, " ") + + if self._mincount == 0 or self._default then + usage = "[" .. usage .. "]" + end + + return usage +end + +function Argument:_get_default_target() + return self._name +end + +function Option:_get_default_target() + local res + + for _, alias in ipairs(self._aliases) do + if alias:sub(1, 1) == alias:sub(2, 2) then + res = alias:sub(3) + break + end + end + + res = res or self._name:sub(2) + return (res:gsub("-", "_")) +end + +function Option:_is_vararg() + return self._maxargs ~= self._minargs +end + +function Parser:_get_fullname() + local parent = self._parent + local buf = {self._name} + + while parent do + table.insert(buf, 1, parent._name) + parent = parent._parent + end + + return table.concat(buf, " ") +end + +function Parser:_update_charset(charset) + charset = charset or {} + + for _, command in ipairs(self._commands) do + command:_update_charset(charset) + end + + for _, option in ipairs(self._options) do + for _, alias in ipairs(option._aliases) do + charset[alias:sub(1, 1)] = true + end + end + + return charset +end + +function Parser:argument(...) + local argument = Argument(...) + table.insert(self._arguments, argument) + return argument +end + +function Parser:option(...) + local option = Option(...) + + if self._has_help then + table.insert(self._options, #self._options, option) + else + table.insert(self._options, option) + end + + return option +end + +function Parser:flag(...) + return self:option():args(0)(...) +end + +function Parser:command(...) + local command = Command():add_help(true)(...) + command._parent = self + table.insert(self._commands, command) + return command +end + +function Parser:mutex(...) + local elements = {...} + + for i, element in ipairs(elements) do + local mt = getmetatable(element) + assert(mt == Option or mt == Argument, ("bad argument #%d to 'mutex' (Option or Argument expected)"):format(i)) + end + + table.insert(self._mutexes, elements) + return self +end + +function Parser:group(name, ...) + assert(type(name) == "string", ("bad argument #1 to 'group' (string expected, got %s)"):format(type(name))) + + local group = {name = name, ...} + + for i, element in ipairs(group) do + local mt = getmetatable(element) + assert(mt == Option or mt == Argument or mt == Command, + ("bad argument #%d to 'group' (Option or Argument or Command expected)"):format(i + 1)) + end + + table.insert(self._groups, group) + return self +end + +local usage_welcome = "Usage: " + +function Parser:get_usage() + if self._usage then + return self._usage + end + + local usage_margin = self:_inherit_property("usage_margin", #usage_welcome) + local max_usage_width = self:_inherit_property("usage_max_width", 70) + local lines = {usage_welcome .. self:_get_fullname()} + + local function add(s) + if #lines[#lines]+1+#s <= max_usage_width then + lines[#lines] = lines[#lines] .. " " .. s + else + lines[#lines+1] = (" "):rep(usage_margin) .. s + end + end + + -- Normally options are before positional arguments in usage messages. + -- However, vararg options should be after, because they can't be reliable used + -- before a positional argument. + -- Mutexes come into play, too, and are shown as soon as possible. + -- Overall, output usages in the following order: + -- 1. Mutexes that don't have positional arguments or vararg options. + -- 2. Options that are not in any mutexes and are not vararg. + -- 3. Positional arguments - on their own or as a part of a mutex. + -- 4. Remaining mutexes. + -- 5. Remaining options. + + local elements_in_mutexes = {} + local added_elements = {} + local added_mutexes = {} + local argument_to_mutexes = {} + + local function add_mutex(mutex, main_argument) + if added_mutexes[mutex] then + return + end + + added_mutexes[mutex] = true + local buf = {} + + for _, element in ipairs(mutex) do + if not element._hidden and not added_elements[element] then + if getmetatable(element) == Option or element == main_argument then + table.insert(buf, element:_get_usage()) + added_elements[element] = true + end + end + end + + if #buf == 1 then + add(buf[1]) + elseif #buf > 1 then + add("(" .. table.concat(buf, " | ") .. ")") + end + end + + local function add_element(element) + if not element._hidden and not added_elements[element] then + add(element:_get_usage()) + added_elements[element] = true + end + end + + for _, mutex in ipairs(self._mutexes) do + local is_vararg = false + local has_argument = false + + for _, element in ipairs(mutex) do + if getmetatable(element) == Option then + if element:_is_vararg() then + is_vararg = true + end + else + has_argument = true + argument_to_mutexes[element] = argument_to_mutexes[element] or {} + table.insert(argument_to_mutexes[element], mutex) + end + + elements_in_mutexes[element] = true + end + + if not is_vararg and not has_argument then + add_mutex(mutex) + end + end + + for _, option in ipairs(self._options) do + if not elements_in_mutexes[option] and not option:_is_vararg() then + add_element(option) + end + end + + -- Add usages for positional arguments, together with one mutex containing them, if they are in a mutex. + for _, argument in ipairs(self._arguments) do + -- Pick a mutex as a part of which to show this argument, take the first one that's still available. + local mutex + + if elements_in_mutexes[argument] then + for _, argument_mutex in ipairs(argument_to_mutexes[argument]) do + if not added_mutexes[argument_mutex] then + mutex = argument_mutex + end + end + end + + if mutex then + add_mutex(mutex, argument) + else + add_element(argument) + end + end + + for _, mutex in ipairs(self._mutexes) do + add_mutex(mutex) + end + + for _, option in ipairs(self._options) do + add_element(option) + end + + if #self._commands > 0 then + if self._require_command then + add("") + else + add("[]") + end + + add("...") + end + + return table.concat(lines, "\n") +end + +local function split_lines(s) + if s == "" then + return {} + end + + local lines = {} + + if s:sub(-1) ~= "\n" then + s = s .. "\n" + end + + for line in s:gmatch("([^\n]*)\n") do + table.insert(lines, line) + end + + return lines +end + +local function autowrap_line(line, max_length) + -- Algorithm for splitting lines is simple and greedy. + local result_lines = {} + + -- Preserve original indentation of the line, put this at the beginning of each result line. + -- If the first word looks like a list marker ('*', '+', or '-'), add spaces so that starts + -- of the second and the following lines vertically align with the start of the second word. + local indentation = line:match("^ *") + + if line:find("^ *[%*%+%-]") then + indentation = indentation .. " " .. line:match("^ *[%*%+%-]( *)") + end + + -- Parts of the last line being assembled. + local line_parts = {} + + -- Length of the current line. + local line_length = 0 + + -- Index of the next character to consider. + local index = 1 + + while true do + local word_start, word_finish, word = line:find("([^ ]+)", index) + + if not word_start then + -- Ignore trailing spaces, if any. + break + end + + local preceding_spaces = line:sub(index, word_start - 1) + index = word_finish + 1 + + if (#line_parts == 0) or (line_length + #preceding_spaces + #word <= max_length) then + -- Either this is the very first word or it fits as an addition to the current line, add it. + table.insert(line_parts, preceding_spaces) -- For the very first word this adds the indentation. + table.insert(line_parts, word) + line_length = line_length + #preceding_spaces + #word + else + -- Does not fit, finish current line and put the word into a new one. + table.insert(result_lines, table.concat(line_parts)) + line_parts = {indentation, word} + line_length = #indentation + #word + end + end + + if #line_parts > 0 then + table.insert(result_lines, table.concat(line_parts)) + end + + if #result_lines == 0 then + -- Preserve empty lines. + result_lines[1] = "" + end + + return result_lines +end + +-- Automatically wraps lines within given array, +-- attempting to limit line length to `max_length`. +-- Existing line splits are preserved. +local function autowrap(lines, max_length) + local result_lines = {} + + for _, line in ipairs(lines) do + local autowrapped_lines = autowrap_line(line, max_length) + + for _, autowrapped_line in ipairs(autowrapped_lines) do + table.insert(result_lines, autowrapped_line) + end + end + + return result_lines +end + +function Parser:_get_element_help(element) + local label_lines = element:_get_label_lines() + local description_lines = split_lines(element:_get_description()) + + local result_lines = {} + + -- All label lines should have the same length (except the last one, it has no comma). + -- If too long, start description after all the label lines. + -- Otherwise, combine label and description lines. + + local usage_margin_len = self:_inherit_property("help_usage_margin", 3) + local usage_margin = (" "):rep(usage_margin_len) + local description_margin_len = self:_inherit_property("help_description_margin", 25) + local description_margin = (" "):rep(description_margin_len) + + local help_max_width = self:_inherit_property("help_max_width") + + if help_max_width then + local description_max_width = math.max(help_max_width - description_margin_len, 10) + description_lines = autowrap(description_lines, description_max_width) + end + + if #label_lines[1] >= (description_margin_len - usage_margin_len) then + for _, label_line in ipairs(label_lines) do + table.insert(result_lines, usage_margin .. label_line) + end + + for _, description_line in ipairs(description_lines) do + table.insert(result_lines, description_margin .. description_line) + end + else + for i = 1, math.max(#label_lines, #description_lines) do + local label_line = label_lines[i] + local description_line = description_lines[i] + + local line = "" + + if label_line then + line = usage_margin .. label_line + end + + if description_line and description_line ~= "" then + line = line .. (" "):rep(description_margin_len - #line) .. description_line + end + + table.insert(result_lines, line) + end + end + + return table.concat(result_lines, "\n") +end + +local function get_group_types(group) + local types = {} + + for _, element in ipairs(group) do + types[getmetatable(element)] = true + end + + return types +end + +function Parser:_add_group_help(blocks, added_elements, label, elements) + local buf = {label} + + for _, element in ipairs(elements) do + if not element._hidden and not added_elements[element] then + added_elements[element] = true + table.insert(buf, self:_get_element_help(element)) + end + end + + if #buf > 1 then + table.insert(blocks, table.concat(buf, ("\n"):rep(self:_inherit_property("help_vertical_space", 0) + 1))) + end +end + +function Parser:get_help() + if self._help then + return self._help + end + + local blocks = {self:get_usage()} + + local help_max_width = self:_inherit_property("help_max_width") + + if self._description then + local description = self._description + + if help_max_width then + description = table.concat(autowrap(split_lines(description), help_max_width), "\n") + end + + table.insert(blocks, description) + end + + -- 1. Put groups containing arguments first, then other arguments. + -- 2. Put remaining groups containing options, then other options. + -- 3. Put remaining groups containing commands, then other commands. + -- Assume that an element can't be in several groups. + local groups_by_type = { + [Argument] = {}, + [Option] = {}, + [Command] = {} + } + + for _, group in ipairs(self._groups) do + local group_types = get_group_types(group) + + for _, mt in ipairs({Argument, Option, Command}) do + if group_types[mt] then + table.insert(groups_by_type[mt], group) + break + end + end + end + + local default_groups = { + {name = "Arguments", type = Argument, elements = self._arguments}, + {name = "Options", type = Option, elements = self._options}, + {name = "Commands", type = Command, elements = self._commands} + } + + local added_elements = {} + + for _, default_group in ipairs(default_groups) do + local type_groups = groups_by_type[default_group.type] + + for _, group in ipairs(type_groups) do + self:_add_group_help(blocks, added_elements, group.name .. ":", group) + end + + local default_label = default_group.name .. ":" + + if #type_groups > 0 then + default_label = "Other " .. default_label:gsub("^.", string.lower) + end + + self:_add_group_help(blocks, added_elements, default_label, default_group.elements) + end + + if self._epilog then + local epilog = self._epilog + + if help_max_width then + epilog = table.concat(autowrap(split_lines(epilog), help_max_width), "\n") + end + + table.insert(blocks, epilog) + end + + return table.concat(blocks, "\n\n") +end + +local function get_tip(context, wrong_name) + local context_pool = {} + local possible_name + local possible_names = {} + + for name in pairs(context) do + if type(name) == "string" then + for i = 1, #name do + possible_name = name:sub(1, i - 1) .. name:sub(i + 1) + + if not context_pool[possible_name] then + context_pool[possible_name] = {} + end + + table.insert(context_pool[possible_name], name) + end + end + end + + for i = 1, #wrong_name + 1 do + possible_name = wrong_name:sub(1, i - 1) .. wrong_name:sub(i + 1) + + if context[possible_name] then + possible_names[possible_name] = true + elseif context_pool[possible_name] then + for _, name in ipairs(context_pool[possible_name]) do + possible_names[name] = true + end + end + end + + local first = next(possible_names) + + if first then + if next(possible_names, first) then + local possible_names_arr = {} + + for name in pairs(possible_names) do + table.insert(possible_names_arr, "'" .. name .. "'") + end + + table.sort(possible_names_arr) + return "\nDid you mean one of these: " .. table.concat(possible_names_arr, " ") .. "?" + else + return "\nDid you mean '" .. first .. "'?" + end + else + return "" + end +end + +local ElementState = class({ + invocations = 0 +}) + +function ElementState:__call(state, element) + self.state = state + self.result = state.result + self.element = element + self.target = element._target or element:_get_default_target() + self.action, self.result[self.target] = element:_get_action() + return self +end + +function ElementState:error(fmt, ...) + self.state:error(fmt, ...) +end + +function ElementState:convert(argument, index) + local converter = self.element._convert + + if converter then + local ok, err + + if type(converter) == "function" then + ok, err = converter(argument) + elseif type(converter[index]) == "function" then + ok, err = converter[index](argument) + else + ok = converter[argument] + end + + if ok == nil then + self:error(err and "%s" or "malformed argument '%s'", err or argument) + end + + argument = ok + end + + return argument +end + +function ElementState:default(mode) + return self.element._defmode:find(mode) and self.element._default +end + +local function bound(noun, min, max, is_max) + local res = "" + + if min ~= max then + res = "at " .. (is_max and "most" or "least") .. " " + end + + local number = is_max and max or min + return res .. tostring(number) .. " " .. noun .. (number == 1 and "" or "s") +end + +function ElementState:set_name(alias) + self.name = ("%s '%s'"):format(alias and "option" or "argument", alias or self.element._name) +end + +function ElementState:invoke() + self.open = true + self.overwrite = false + + if self.invocations >= self.element._maxcount then + if self.element._overwrite then + self.overwrite = true + else + local num_times_repr = bound("time", self.element._mincount, self.element._maxcount, true) + self:error("%s must be used %s", self.name, num_times_repr) + end + else + self.invocations = self.invocations + 1 + end + + self.args = {} + + if self.element._maxargs <= 0 then + self:close() + end + + return self.open +end + +function ElementState:pass(argument) + argument = self:convert(argument, #self.args + 1) + table.insert(self.args, argument) + + if #self.args >= self.element._maxargs then + self:close() + end + + return self.open +end + +function ElementState:complete_invocation() + while #self.args < self.element._minargs do + self:pass(self.element._default) + end +end + +function ElementState:close() + if self.open then + self.open = false + + if #self.args < self.element._minargs then + if self:default("a") then + self:complete_invocation() + else + if #self.args == 0 then + if getmetatable(self.element) == Argument then + self:error("missing %s", self.name) + elseif self.element._maxargs == 1 then + self:error("%s requires an argument", self.name) + end + end + + self:error("%s requires %s", self.name, bound("argument", self.element._minargs, self.element._maxargs)) + end + end + + local args + + if self.element._maxargs == 0 then + args = self.args[1] + elseif self.element._maxargs == 1 then + if self.element._minargs == 0 and self.element._mincount ~= self.element._maxcount then + args = self.args + else + args = self.args[1] + end + else + args = self.args + end + + self.action(self.result, self.target, args, self.overwrite) + end +end + +local ParseState = class({ + result = {}, + options = {}, + arguments = {}, + argument_i = 1, + element_to_mutexes = {}, + mutex_to_element_state = {}, + command_actions = {} +}) + +function ParseState:__call(parser, error_handler) + self.parser = parser + self.error_handler = error_handler + self.charset = parser:_update_charset() + self:switch(parser) + return self +end + +function ParseState:error(fmt, ...) + self.error_handler(self.parser, fmt:format(...)) +end + +function ParseState:switch(parser) + self.parser = parser + + if parser._action then + table.insert(self.command_actions, {action = parser._action, name = parser._name}) + end + + for _, option in ipairs(parser._options) do + option = ElementState(self, option) + table.insert(self.options, option) + + for _, alias in ipairs(option.element._aliases) do + self.options[alias] = option + end + end + + for _, mutex in ipairs(parser._mutexes) do + for _, element in ipairs(mutex) do + if not self.element_to_mutexes[element] then + self.element_to_mutexes[element] = {} + end + + table.insert(self.element_to_mutexes[element], mutex) + end + end + + for _, argument in ipairs(parser._arguments) do + argument = ElementState(self, argument) + table.insert(self.arguments, argument) + argument:set_name() + argument:invoke() + end + + self.handle_options = parser._handle_options + self.argument = self.arguments[self.argument_i] + self.commands = parser._commands + + for _, command in ipairs(self.commands) do + for _, alias in ipairs(command._aliases) do + self.commands[alias] = command + end + end +end + +function ParseState:get_option(name) + local option = self.options[name] + + if not option then + self:error("unknown option '%s'%s", name, get_tip(self.options, name)) + else + return option + end +end + +function ParseState:get_command(name) + local command = self.commands[name] + + if not command then + if #self.commands > 0 then + self:error("unknown command '%s'%s", name, get_tip(self.commands, name)) + else + self:error("too many arguments") + end + else + return command + end +end + +function ParseState:check_mutexes(element_state) + if self.element_to_mutexes[element_state.element] then + for _, mutex in ipairs(self.element_to_mutexes[element_state.element]) do + local used_element_state = self.mutex_to_element_state[mutex] + + if used_element_state and used_element_state ~= element_state then + self:error("%s can not be used together with %s", element_state.name, used_element_state.name) + else + self.mutex_to_element_state[mutex] = element_state + end + end + end +end + +function ParseState:invoke(option, name) + self:close() + option:set_name(name) + self:check_mutexes(option, name) + + if option:invoke() then + self.option = option + end +end + +function ParseState:pass(arg) + if self.option then + if not self.option:pass(arg) then + self.option = nil + end + elseif self.argument then + self:check_mutexes(self.argument) + + if not self.argument:pass(arg) then + self.argument_i = self.argument_i + 1 + self.argument = self.arguments[self.argument_i] + end + else + local command = self:get_command(arg) + self.result[command._target or command._name] = true + + if self.parser._command_target then + self.result[self.parser._command_target] = command._name + end + + self:switch(command) + end +end + +function ParseState:close() + if self.option then + self.option:close() + self.option = nil + end +end + +function ParseState:finalize() + self:close() + + for i = self.argument_i, #self.arguments do + local argument = self.arguments[i] + if #argument.args == 0 and argument:default("u") then + argument:complete_invocation() + else + argument:close() + end + end + + if self.parser._require_command and #self.commands > 0 then + self:error("a command is required") + end + + for _, option in ipairs(self.options) do + option.name = option.name or ("option '%s'"):format(option.element._name) + + if option.invocations == 0 then + if option:default("u") then + option:invoke() + option:complete_invocation() + option:close() + end + end + + local mincount = option.element._mincount + + if option.invocations < mincount then + if option:default("a") then + while option.invocations < mincount do + option:invoke() + option:close() + end + elseif option.invocations == 0 then + self:error("missing %s", option.name) + else + self:error("%s must be used %s", option.name, bound("time", mincount, option.element._maxcount)) + end + end + end + + for i = #self.command_actions, 1, -1 do + self.command_actions[i].action(self.result, self.command_actions[i].name) + end +end + +function ParseState:parse(args) + for _, arg in ipairs(args) do + local plain = true + + if self.handle_options then + local first = arg:sub(1, 1) + + if self.charset[first] then + if #arg > 1 then + plain = false + + if arg:sub(2, 2) == first then + if #arg == 2 then + if self.options[arg] then + local option = self:get_option(arg) + self:invoke(option, arg) + else + self:close() + end + + self.handle_options = false + else + local equals = arg:find "=" + if equals then + local name = arg:sub(1, equals - 1) + local option = self:get_option(name) + + if option.element._maxargs <= 0 then + self:error("option '%s' does not take arguments", name) + end + + self:invoke(option, name) + self:pass(arg:sub(equals + 1)) + else + local option = self:get_option(arg) + self:invoke(option, arg) + end + end + else + for i = 2, #arg do + local name = first .. arg:sub(i, i) + local option = self:get_option(name) + self:invoke(option, name) + + if i ~= #arg and option.element._maxargs > 0 then + self:pass(arg:sub(i + 1)) + break + end + end + end + end + end + end + + if plain then + self:pass(arg) + end + end + + self:finalize() + return self.result +end + +function Parser:error(msg) + io.stderr:write(("%s\n\nError: %s\n"):format(self:get_usage(), msg)) + os.exit(1) +end + +-- Compatibility with strict.lua and other checkers: +local default_cmdline = rawget(_ENV, "arg") or {} + +function Parser:_parse(args, error_handler) + return ParseState(self, error_handler):parse(args or default_cmdline) +end + +function Parser:parse(args) + return self:_parse(args, self.error) +end + +local function xpcall_error_handler(err) + return tostring(err) .. "\noriginal " .. debug.traceback("", 2):sub(2) +end + +function Parser:pparse(args) + local parse_error + + local ok, result = xpcall(function() + return self:_parse(args, function(_, err) + parse_error = err + error(err, 0) + end) + end, xpcall_error_handler) + + if ok then + return true, result + elseif not parse_error then + error(result, 0) + else + return false, parse_error + end +end + +local argparse = {} + +argparse.version = "0.6.0" + +setmetatable(argparse, {__call = function(_, ...) + return Parser(default_cmdline[0]):add_help(true)(...) +end}) + +return argparse \ No newline at end of file diff --git a/moonscript/etc/fstab b/moonscript/etc/fstab new file mode 100644 index 0000000..539787f --- /dev/null +++ b/moonscript/etc/fstab @@ -0,0 +1,4 @@ +packages/moonscript gitfs leafo/moonscript/master/bin +rom/modules/main/moonscript gitfs leafo/moonscript/master/moonscript +rom/modules/main/moon gitfs leafo/moonscript/master/moon +rom/modules/main/argparse.lua linkfs packages/moonscript/argparse.lua