diff --git a/busted/.package b/busted/.package new file mode 100644 index 0000000..3bfdfce --- /dev/null +++ b/busted/.package @@ -0,0 +1,9 @@ +{ + title = 'busted', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/moonscript', + description = [[]], + license = 'MIT', + required = { + 'penlight', + }, +} diff --git a/busted/depend/system.lua b/busted/depend/system.lua new file mode 100644 index 0000000..b181771 --- /dev/null +++ b/busted/depend/system.lua @@ -0,0 +1,16 @@ +return { + -- Returns the monotonic time the system has been up, in secconds. + monotime = function() + return os.clock() + end, + + -- Sleep for n seconds. + sleep = function(n) + os.sleep(n) + end, + + -- Returns the current system time, 1970 (UTC), in secconds. + gettime = function() + return os.epoch('utc') / 1000 + end, +} diff --git a/busted/depend/term.lua b/busted/depend/term.lua new file mode 100644 index 0000000..01278d4 --- /dev/null +++ b/busted/depend/term.lua @@ -0,0 +1,5 @@ +return { + isatty = function() + return false + end, +} diff --git a/busted/etc/fstab b/busted/etc/fstab new file mode 100644 index 0000000..c1141b8 --- /dev/null +++ b/busted/etc/fstab @@ -0,0 +1,8 @@ +packages/busted/busted urlfs https://raw.githubusercontent.com/Olivine-Labs/busted/master/bin/busted +rom/modules/main/busted gitfs Olivine-Labs/busted/master/busted +rom/modules/main/mediator.lua urlfs https://raw.githubusercontent.com/Olivine-Labs/mediator_lua/master/src/mediator.lua +rom/modules/main/cliargs gitfs amireh/lua_cliargs/master/src/cliargs +rom/modules/main/luassert gitfs Olivine-Labs/luassert/master/src +rom/modules/main/say.lua urlfs https://raw.githubusercontent.com/Olivine-Labs/say/master/src/init.lua +rom/modules/main/term.lua linkfs packages/busted/depend/term.lua +rom/modules/main/system.lua linkfs packages/busted/depend/system.lua diff --git a/debugger/apis/init.lua b/debugger/apis/init.lua index 1e860b8..6f19958 100644 --- a/debugger/apis/init.lua +++ b/debugger/apis/init.lua @@ -9,21 +9,25 @@ local dbg = { breakpoints = nil, } -local function breakpointHook(info) +local function breakpointHook(depth, lineNo) if dbg.breakpoints then - local src = info.short_src - for _,v in pairs(dbg.breakpoints) do - if v.line == info.currentline - and (v.file == src or v.bfile == src) then - return not v.disabled + local info + for _,v in ipairs(dbg.breakpoints) do + if v.line == lineNo then + if not info then + info = debug.getinfo(depth) + end + if (v.file == info.short_src or v.bfile == info.short_src) then + return not v.disabled + end end end end end local function functionHook(fn) - return function(info) - return info.func == fn + return function() + return debug.getinfo(3).func == fn end end @@ -42,9 +46,9 @@ local function stackSizeHook(n) end i = i + 1 end - return function(info) + return function(depth, lineNo) return not debug.getinfo(i - n) - or breakpointHook(info) + or breakpointHook(depth + 1, lineNo) end end @@ -133,12 +137,10 @@ local function get_trace(offset, stack_inspect_offset) return t end -local function hook() - local info = debug.getinfo(2) - +local function hook(_, lineNo) local h = dbg.hooks[coroutine.running()] - if h and h.eval(info) then + if h and h.eval(3, lineNo) then local inspectOffset = 0 repeat diff --git a/debugger/debug.lua b/debugger/debug.lua index c0f57d2..2a3ac39 100644 --- a/debugger/debug.lua +++ b/debugger/debug.lua @@ -1,3 +1,4 @@ +local class = require('opus.class') local Config = require('opus.config') local Event = require('opus.event') local UI = require('opus.ui') @@ -128,7 +129,7 @@ local function message(...) client:resume('debugger', ...) end -UI.InverseButton = require('opus.class')(UI.Button) +UI.InverseButton = class(UI.Button) UI.InverseButton.defaults = { UIElement = 'InverseButton', backgroundColor = 'primary', diff --git a/moonscript/README.txt b/moonscript/README.txt index d306970..d0b8bfa 100644 --- a/moonscript/README.txt +++ b/moonscript/README.txt @@ -1,5 +1,7 @@ -running the compiler works fine... -moonc T.moon <-- OK +moonscript must be run in compatibility mode: +> compat moon T.moon +> compat moonc T.moon -working on getting the moon command to work properly -moon T.moon <-- NOPE +moon and moonc were modified to allow relative paths: +> cd /packages/moonscript +> compat moonc T.moon diff --git a/moonscript/T.lua b/moonscript/T.lua index 8825f17..cd0fa6d 100644 --- a/moonscript/T.lua +++ b/moonscript/T.lua @@ -1,11 +1,13 @@ local Event = require('opus.event') local UI = require('opus.ui') -local kernel = _G.kernel -local multishell = _ENV.multishell +local kernel +kernel = _G.kernel +local multishell +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({ + UI.MenuBar({ buttons = { { text = 'Activate', diff --git a/moonscript/T.moon b/moonscript/T.moon index 6cbf5f4..2fbbc27 100644 --- a/moonscript/T.moon +++ b/moonscript/T.moon @@ -1,14 +1,15 @@ 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 +import kernel from _G +import multishell from _ENV + +tasks = multishell and multishell.getTabs and multishell.getTabs! or kernel.routines UI\configure 'Tasks', ... page = UI.Page { - menuBar: UI.MenuBar { + UI.MenuBar { buttons: { { text: 'Activate', event: 'activate' }, { text: 'Terminate', event: 'terminate' }, @@ -43,7 +44,7 @@ page = UI.Page { t: 'terminate', }, eventHandler: (event) => - t = self.grid\getSelected! + t = @grid\getSelected! switch event.type when 'activate', 'grid_select' multishell.setFocus t.uid if t diff --git a/moonscript/argparse.lua b/moonscript/argparse.lua deleted file mode 100644 index de89237..0000000 --- a/moonscript/argparse.lua +++ /dev/null @@ -1,1527 +0,0 @@ --- 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 index 539787f..4f45064 100644 --- a/moonscript/etc/fstab +++ b/moonscript/etc/fstab @@ -1,4 +1,5 @@ -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 +packages/moonscript/repo gitfs leafo/moonscript/master +rom/modules/main/moonscript linkfs packages/moonscript/repo/moonscript +rom/modules/main/moon linkfs packages/moonscript/repo/moon +rom/modules/main/moonutil gitfs natnat-mc/moonutil/master/moonutil +rom/modules/main/argparse urlfs https://raw.githubusercontent.com/mpeterv/argparse/master/src/argparse.lua diff --git a/moonscript/moon b/moonscript/moon new file mode 100644 index 0000000..bd17e3c --- /dev/null +++ b/moonscript/moon @@ -0,0 +1,116 @@ +#!/usr/bin/env lua +local argparse = require("argparse") +local moonscript = require("moonscript.base") +local util = require("moonscript.util") +local errors = require("moonscript.errors") +local unpack = util.unpack +local argparser = argparse()({ + name = "moon" +}) +argparser:argument("script") +argparser:argument("args"):args("*") +argparser:option("-c --coverage", "Collect and print code coverage") +argparser:option("-d", "Disable stack trace rewriting") +argparser:option("-v --version", "Print version information") +local base = 0 +local _list_0 = arg +for _index_0 = 1, #_list_0 do + local flag = _list_0[_index_0] + base = base + 1 + if flag:sub(1, 1) ~= "-" then + break + end +end +local args = { + unpack(arg, 1, base) +} +local opts = argparser:parse(args) +local print_err +print_err = function(...) + local msg = table.concat((function(...) + local _accum_0 = { } + local _len_0 = 1 + local _list_1 = { + ... + } + for _index_0 = 1, #_list_1 do + local v = _list_1[_index_0] + _accum_0[_len_0] = tostring(v) + _len_0 = _len_0 + 1 + end + return _accum_0 + end)(...), "\t") + return io.stderr:write(msg .. "\n") +end +local run +run = function() + if opts.version then + require("moonscript.version").print_version() + os.exit() + end + local script_fname = shell.resolve(opts.script) + args = { + unpack(arg, base + 1) + } + args[-1] = arg[0] + args[0] = opts.script + local moonscript_chunk, lua_parse_error + local passed, err = pcall(function() + moonscript_chunk, lua_parse_error = moonscript.loadfile(script_fname, { + implicitly_return_root = false + }) + end) + if not (passed) then + print_err(err) + os.exit(1) + end + if not (moonscript_chunk) then + if lua_parse_error then + print_err(lua_parse_error) + else + print_err("Can't file file: " .. tostring(script_fname)) + end + os.exit(1) + end + util.getfenv(moonscript_chunk).arg = args + local run_chunk + run_chunk = function() + moonscript.insert_loader() + moonscript_chunk(unpack(args)) + return moonscript.remove_loader() + end + if opts.d then + return run_chunk() + end + local err, trace, cov + if opts.coverage then + print("starting coverage") + local coverage = require("moonscript.cmd.coverage") + cov = coverage.CodeCoverage() + cov:start() + end + xpcall(run_chunk, function(_err) + err = _err + trace = debug.traceback("", 2) + end) + if err then + local truncated = errors.truncate_traceback(util.trim(trace)) + local rewritten = errors.rewrite_traceback(truncated, err) + if rewritten then + print_err(rewritten) + else + print_err(table.concat({ + err, + util.trim(trace) + }, "\n")) + end + return os.exit(1) + else + if cov then + cov:stop() + return cov:print_results() + end + end +end +return run() +-- vim: set filetype=lua: \ No newline at end of file diff --git a/moonscript/moonc b/moonscript/moonc new file mode 100644 index 0000000..9d9c2ef --- /dev/null +++ b/moonscript/moonc @@ -0,0 +1,235 @@ +#!/usr/bin/env lua + +local argparse = require "argparse" +local lfs = require "lfs" + +local parser = argparse() + +parser:flag("-l --lint", "Perform a lint on the file instead of compiling") +parser:flag("-v --version", "Print version") +parser:flag("-w --watch", "Watch file/directory for updates") +parser:option("--transform", "Transform syntax tree with module") +parser:mutex( + parser:option("-t --output-to", "Specify where to place compiled files"), + parser:option("-o", "Write output to file"), + parser:flag("-p", "Write output to standard output"), + parser:flag("-T", "Write parse tree instead of code (to stdout)"), + parser:flag("-b", "Write parse and compile time instead of code(to stdout)"), + parser:flag("-X", "Write line rewrite map instead of code (to stdout)") +) +parser:flag("-", + "Read from standard in, print to standard out (Must be only argument)") + +local read_stdin = arg[1] == "--" -- luacheck: ignore 113 + +if not read_stdin then + parser:argument("file/directory"):args("+") +end + +local opts = parser:parse() + +if opts.version then + local v = require "moonscript.version" + v.print_version() + os.exit() +end + +function log_msg(...) + if not opts.p then + io.stderr:write(table.concat({...}, " ") .. "\n") + end +end + +local moonc = require("moonscript.cmd.moonc") +local util = require "moonscript.util" +local normalize_dir = moonc.normalize_dir +local compile_and_write = moonc.compile_and_write +local path_to_target = moonc.path_to_target + +local function scan_directory(root, collected) + root = normalize_dir(root) + collected = collected or {} + + for fname in lfs.dir(root) do + if not fname:match("^%.") then + local full_path = root..fname + + if lfs.attributes(full_path, "mode") == "directory" then + scan_directory(full_path, collected) + elseif fname:match("%.moon$") then + table.insert(collected, full_path) + end + end + end + + return collected +end + +local function remove_dups(tbl, key_fn) + local hash = {} + local final = {} + + for _, v in ipairs(tbl) do + local dup_key = key_fn and key_fn(v) or v + if not hash[dup_key] then + table.insert(final, v) + hash[dup_key] = true + end + end + + return final +end + +-- creates tuples of input and target +local function get_files(fname, files) + files = files or {} + + if lfs.attributes(fname, "mode") == "directory" then + for _, sub_fname in ipairs(scan_directory(fname)) do + table.insert(files, { + sub_fname, + path_to_target(sub_fname, opts.output_to, fname) + }) + end + else + if fname:sub(1, 1) ~= '/' then + fname = lfs.currentdir() .. '/' .. fname + end + + table.insert(files, { + fname, + path_to_target(fname, opts.output_to) + }) + end + + return files +end + +if read_stdin then + local parse = require "moonscript.parse" + local compile = require "moonscript.compile" + + local text = io.stdin:read("*a") + local tree, err = parse.string(text) + + if not tree then error(err) end + local code, err, pos = compile.tree(tree) + + if not code then + error(compile.format_error(err, pos, text)) + end + + print(code) + os.exit() +end + +local inputs = opts["file/directory"] + +local files = {} +for _, input in ipairs(inputs) do + get_files(input, files) +end + +files = remove_dups(files, function(f) + return f[2] +end) + +-- returns an iterator that returns files that have been updated +local function create_watcher(files) + local watchers = require("moonscript.cmd.watchers") + + if watchers.InotifyWacher:available() then + return watchers.InotifyWacher(files):each_update() + end + + return watchers.SleepWatcher(files):each_update() +end + +if opts.watch then + -- build function to check for lint or compile in watch + local handle_file + if opts.lint then + local lint = require "moonscript.cmd.lint" + handle_file = lint.lint_file + else + handle_file = compile_and_write + end + + local watcher = create_watcher(files) + -- catches interrupt error for ctl-c + local protected = function() + local status, file = true, watcher() + if status then + return file + elseif file ~= "interrupted!" then + error(file) + end + end + + for fname in protected do + local target = path_to_target(fname, opts.t) + + if opts.o then + target = opts.o + end + + local success, err = handle_file(fname, target) + if opts.lint then + if success then + io.stderr:write(success .. "\n\n") + elseif err then + io.stderr:write(fname .. "\n" .. err .. "\n\n") + end + elseif not success then + io.stderr:write(table.concat({ + "", + "Error: " .. fname, + err, + "\n", + }, "\n")) + elseif success == "build" then + log_msg("Built", fname, "->", target) + end + end + + io.stderr:write("\nQuitting...\n") +elseif opts.lint then + local has_linted_with_error; + local lint = require "moonscript.cmd.lint" + for _, tuple in pairs(files) do + local fname = tuple[1] + local res, err = lint.lint_file(fname) + if res then + has_linted_with_error = true + io.stderr:write(res .. "\n\n") + elseif err then + has_linted_with_error = true + io.stderr:write(fname .. "\n" .. err.. "\n\n") + end + end + if has_linted_with_error then + os.exit(1) + end +else + for _, tuple in ipairs(files) do + local fname, target = util.unpack(tuple) + if opts.o then + target = opts.o + end + + local success, err = compile_and_write(fname, target, { + print = opts.p, + fname = fname, + benchmark = opts.b, + show_posmap = opts.X, + show_parse_tree = opts.T, + transform_module = opts.transform + }) + + if not success then + io.stderr:write(fname .. "\t" .. err .. "\n") + os.exit(1) + end + end +end + diff --git a/penlight/etc/fstab b/penlight/etc/fstab deleted file mode 100644 index 2a1632b..0000000 --- a/penlight/etc/fstab +++ /dev/null @@ -1 +0,0 @@ -#rom/modules/main/pl gitfs Tieske/Penlight/master/lua/pl \ No newline at end of file diff --git a/penlight/.package b/pl/.package similarity index 81% rename from penlight/.package rename to pl/.package index 4543cf2..58081b7 100644 --- a/penlight/.package +++ b/pl/.package @@ -1,6 +1,6 @@ { - title = 'Penlight apis', - repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/penlight', + title = 'Penlight', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/pl', description = [[See: https://github.com/Tieske/Penlight Penlight brings together a set of generally useful pure Lua modules, focusing on input data handling (such as reading configuration files), functional programming (such as map, reduce, placeholder expressions, etc), and OS path management. Much of the functionality is inspired by the Python standard libraries.]], diff --git a/pl/apis/compat.lua b/pl/apis/compat.lua new file mode 100644 index 0000000..46b84f4 --- /dev/null +++ b/pl/apis/compat.lua @@ -0,0 +1,188 @@ +---------------- +--- Lua 5.1/5.2/5.3 compatibility. +-- Injects `table.pack`, `table.unpack`, and `package.searchpath` in the global +-- environment, to make sure they are available for Lua 5.1 and LuaJIT. +-- +-- All other functions are exported as usual in the returned module table. +-- +-- NOTE: everything in this module is also available in `pl.utils`. +-- @module pl.compat +local compat = {} + +--- boolean flag this is Lua 5.1 (or LuaJIT). +-- @field lua51 +compat.lua51 = _VERSION == 'Lua 5.1' + +--- boolean flag this is LuaJIT. +-- @field jit +compat.jit = (tostring(assert):match('builtin') ~= nil) + +--- boolean flag this is LuaJIT with 5.2 compatibility compiled in. +-- @field jit52 +if compat.jit then + -- 'goto' is a keyword when 52 compatibility is enabled in LuaJit + compat.jit52 = not loadstring("local goto = 1") +end + +--- the directory separator character for the current platform. +-- @field dir_separator +compat.dir_separator = _ENV.package.config:sub(1,1) + +--- boolean flag this is a Windows platform. +-- @field is_windows +compat.is_windows = compat.dir_separator == '\\' + +--- execute a shell command, in a compatible and platform independent way. +-- This is a compatibility function that returns the same for Lua 5.1 and +-- Lua 5.2+. +-- +-- NOTE: Windows systems can use signed 32bit integer exitcodes. Posix systems +-- only use exitcodes 0-255, anything else is undefined. +-- @param cmd a shell command +-- @return true if successful +-- @return actual return code +function compat.execute(cmd) + local res1,res2,res3 = os.execute(cmd) + if res2 == "No error" and res3 == 0 and compat.is_windows then + -- os.execute bug in Lua 5.2+ not reporting -1 properly on Windows + res3 = -1 + end + if compat.lua51 and not compat.jit52 then + if compat.is_windows then + return res1==0,res1 + else + res1 = res1 > 255 and res1 / 256 or res1 + return res1==0,res1 + end + else + if compat.is_windows then + return res3==0,res3 + else + return not not res1,res3 + end + end +end + +---------------- +-- Load Lua code as a text or binary chunk (in a Lua 5.2 compatible way). +-- @param ld code string or loader +-- @param[opt] source name of chunk for errors +-- @param[opt] mode 'b', 't' or 'bt' +-- @param[opt] env environment to load the chunk in +-- @function compat.load + +--------------- +-- Get environment of a function (in a Lua 5.1 compatible way). +-- Not 100% compatible, so with Lua 5.2 it may return nil for a function with no +-- global references! +-- Based on code by [Sergey Rozhenko](http://lua-users.org/lists/lua-l/2010-06/msg00313.html) +-- @param f a function or a call stack reference +-- @function compat.getfenv + +--------------- +-- Set environment of a function (in a Lua 5.1 compatible way). +-- @param f a function or a call stack reference +-- @param env a table that becomes the new environment of `f` +-- @function compat.setfenv + +if compat.lua51 then -- define Lua 5.2 style load() + if not compat.jit then -- but LuaJIT's load _is_ compatible + local lua51_load = load + function compat.load(str,src,mode,env) + local chunk,err + if type(str) == 'string' then + if str:byte(1) == 27 and not (mode or 'bt'):find 'b' then + return nil,"attempt to load a binary chunk" + end + chunk,err = loadstring(str,src) + else + chunk,err = lua51_load(str,src) + end + if chunk and env then setfenv(chunk,env) end + return chunk,err + end + else + compat.load = load + end + compat.setfenv, compat.getfenv = setfenv, getfenv +else + compat.load = load + -- setfenv/getfenv replacements for Lua 5.2 + -- by Sergey Rozhenko + -- http://lua-users.org/lists/lua-l/2010-06/msg00313.html + -- Roberto Ierusalimschy notes that it is possible for getfenv to return nil + -- in the case of a function with no globals: + -- http://lua-users.org/lists/lua-l/2010-06/msg00315.html + function compat.setfenv(f, t) + f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func) + local name + local up = 0 + repeat + up = up + 1 + name = debug.getupvalue(f, up) + until name == '_ENV' or name == nil + if name then + debug.upvaluejoin(f, up, function() return name end, 1) -- use unique upvalue + debug.setupvalue(f, up, t) + end + if f ~= 0 then return f end + end + + function compat.getfenv(f) + local f = f or 0 + f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func) + local name, val + local up = 0 + repeat + up = up + 1 + name, val = debug.getupvalue(f, up) + until name == '_ENV' or name == nil + return val + end +end + +--- Global exported functions (for Lua 5.1 & LuaJIT) +-- @section lua52 + +--- pack an argument list into a table. +-- @param ... any arguments +-- @return a table with field n set to the length +-- @function table.pack +if not table.pack then + function table.pack (...) -- luacheck: ignore + return {n=select('#',...); ...} + end +end + +--- unpack a table and return the elements. +-- +-- NOTE: this version does NOT honor the n field, and hence it is not nil-safe. +-- See `utils.unpack` for a version that is nil-safe. +-- @param t table to unpack +-- @param[opt] i index from which to start unpacking, defaults to 1 +-- @param[opt] t index of the last element to unpack, defaults to #t +-- @return multiple return values from the table +-- @function table.unpack +-- @see utils.unpack +if not table.unpack then + table.unpack = unpack -- luacheck: ignore +end + +--- return the full path where a Lua module name would be matched. +-- @param mod module name, possibly dotted +-- @param path a path in the same form as package.path or package.cpath +-- @see path.package_path +-- @function package.searchpath +if not package.searchpath then + local sep = package.config:sub(1,1) + function package.searchpath (mod,path) -- luacheck: ignore + mod = mod:gsub('%.',sep) + for m in path:gmatch('[^;]+') do + local nm = m:gsub('?',mod) + local f = io.open(nm,'r') + if f then f:close(); return nm end + end + end +end + +return compat \ No newline at end of file diff --git a/pl/apis/path.lua b/pl/apis/path.lua new file mode 100644 index 0000000..835fdfc --- /dev/null +++ b/pl/apis/path.lua @@ -0,0 +1,445 @@ +--- Path manipulation and file queries. +-- +-- This is modelled after Python's os.path library (10.1); see @{04-paths.md|the Guide}. +-- +-- Dependencies: `pl.utils`, `lfs` +-- @module pl.path + +-- imports and locals +local _G = _G +local sub = string.sub +local getenv = os.getenv +local tmpnam = os.tmpname +local attributes, currentdir, link_attrib +local package = package +local append, concat, remove = table.insert, table.concat, table.remove +local utils = require 'pl.utils' +local assert_string,raise = utils.assert_string,utils.raise + +local attrib +local path = {} + +local lfs = require('lfs') + attributes = lfs.attributes + currentdir = lfs.currentdir + link_attrib = lfs.symlinkattributes + + +attrib = attributes +path.attrib = attrib +path.link_attrib = link_attrib + +--- Lua iterator over the entries of a given directory. +-- Behaves like `lfs.dir` +path.dir = lfs.dir + +--- Creates a directory. +path.mkdir = lfs.mkdir + +--- Removes a directory. +path.rmdir = lfs.rmdir + +---- Get the working directory. +path.currentdir = currentdir + +--- Changes the working directory. +path.chdir = lfs.chdir + + +--- is this a directory? +-- @string P A file path +function path.isdir(P) + assert_string(1,P) + if P:match("\\$") then + P = P:sub(1,-2) + end + return attrib(P,'mode') == 'directory' +end + +--- is this a file?. +-- @string P A file path +function path.isfile(P) + assert_string(1,P) + return attrib(P,'mode') == 'file' +end + +-- is this a symbolic link? +-- @string P A file path +function path.islink(P) + assert_string(1,P) + if link_attrib then + return link_attrib(P,'mode')=='link' + else + return false + end +end + +--- return size of a file. +-- @string P A file path +function path.getsize(P) + assert_string(1,P) + return attrib(P,'size') +end + +--- does a path exist?. +-- @string P A file path +-- @return the file path if it exists, nil otherwise +function path.exists(P) + assert_string(1,P) + return attrib(P,'mode') ~= nil and P +end + +--- Return the time of last access as the number of seconds since the epoch. +-- @string P A file path +function path.getatime(P) + assert_string(1,P) + return attrib(P,'access') +end + +--- Return the time of last modification +-- @string P A file path +function path.getmtime(P) + assert_string(1,P) + return attrib(P,'modification') +end + +---Return the system's ctime. +-- @string P A file path +function path.getctime(P) + assert_string(1,P) + return path.attrib(P,'change') +end + + +local function at(s,i) + return sub(s,i,i) +end + +path.is_windows = utils.is_windows + +local other_sep +-- !constant sep is the directory separator for this platform. +if path.is_windows then + path.sep = '\\'; other_sep = '/' + path.dirsep = ';' +else + path.sep = '/' + path.dirsep = ':' +end +local sep = path.sep + +--- are we running Windows? +-- @class field +-- @name path.is_windows + +--- path separator for this platform. +-- @class field +-- @name path.sep + +--- separator for PATH for this platform +-- @class field +-- @name path.dirsep + +--- given a path, return the directory part and a file part. +-- if there's no directory part, the first value will be empty +-- @string P A file path +function path.splitpath(P) + assert_string(1,P) + local i = #P + local ch = at(P,i) + while i > 0 and ch ~= sep and ch ~= other_sep do + i = i - 1 + ch = at(P,i) + end + if i == 0 then + return '',P + else + return sub(P,1,i-1), sub(P,i+1) + end +end + +--- return an absolute path. +-- @string P A file path +-- @string[opt] pwd optional start path to use (default is current dir) +function path.abspath(P,pwd) + assert_string(1,P) + if pwd then assert_string(2,pwd) end + local use_pwd = pwd ~= nil + if not use_pwd and not currentdir then return P end + P = P:gsub('[\\/]$','') + pwd = pwd or currentdir() + if not path.isabs(P) then + P = path.join(pwd,P) + elseif path.is_windows and not use_pwd and at(P,2) ~= ':' and at(P,2) ~= '\\' then + P = pwd:sub(1,2)..P -- attach current drive to path like '\\fred.txt' + end + return path.normpath(P) +end + +--- given a path, return the root part and the extension part. +-- if there's no extension part, the second value will be empty +-- @string P A file path +-- @treturn string root part +-- @treturn string extension part (maybe empty) +function path.splitext(P) + assert_string(1,P) + local i = #P + local ch = at(P,i) + while i > 0 and ch ~= '.' do + if ch == sep or ch == other_sep then + return P,'' + end + i = i - 1 + ch = at(P,i) + end + if i == 0 then + return P,'' + else + return sub(P,1,i-1),sub(P,i) + end +end + +--- return the directory part of a path +-- @string P A file path +function path.dirname(P) + assert_string(1,P) + local p1 = path.splitpath(P) + return p1 +end + +--- return the file part of a path +-- @string P A file path +function path.basename(P) + assert_string(1,P) + local _,p2 = path.splitpath(P) + return p2 +end + +--- get the extension part of a path. +-- @string P A file path +function path.extension(P) + assert_string(1,P) + local _,p2 = path.splitext(P) + return p2 +end + +--- is this an absolute path?. +-- @string P A file path +function path.isabs(P) + assert_string(1,P) + if path.is_windows then + return at(P,1) == '/' or at(P,1)=='\\' or at(P,2)==':' + else + return at(P,1) == '/' + end +end + +--- return the path resulting from combining the individual paths. +-- if the second (or later) path is absolute, we return the last absolute path (joined with any non-absolute paths following). +-- empty elements (except the last) will be ignored. +-- @string p1 A file path +-- @string p2 A file path +-- @string ... more file paths +function path.join(p1,p2,...) + assert_string(1,p1) + assert_string(2,p2) + if select('#',...) > 0 then + local p = path.join(p1,p2) + local args = {...} + for i = 1,#args do + assert_string(i,args[i]) + p = path.join(p,args[i]) + end + return p + end + if path.isabs(p2) then return p2 end + local endc = at(p1,#p1) + if endc ~= path.sep and endc ~= other_sep and endc ~= "" then + p1 = p1..path.sep + end + return p1..p2 +end + +--- normalize the case of a pathname. On Unix, this returns the path unchanged; +-- for Windows, it converts the path to lowercase, and it also converts forward slashes +-- to backward slashes. +-- @string P A file path +function path.normcase(P) + assert_string(1,P) + if path.is_windows then + return (P:lower():gsub('/','\\')) + else + return P + end +end + +--- normalize a path name. +-- `A//B`, `A/./B`, and `A/foo/../B` all become `A/B`. +-- @string P a file path +function path.normpath(P) + assert_string(1,P) + -- Split path into anchor and relative path. + local anchor = '' + if path.is_windows then + if P:match '^\\\\' then -- UNC + anchor = '\\\\' + P = P:sub(3) + elseif at(P, 1) == '/' or at(P, 1) == '\\' then + anchor = '\\' + P = P:sub(2) + elseif at(P, 2) == ':' then + anchor = P:sub(1, 2) + P = P:sub(3) + if at(P, 1) == '/' or at(P, 1) == '\\' then + anchor = anchor..'\\' + P = P:sub(2) + end + end + P = P:gsub('/','\\') + else + -- According to POSIX, in path start '//' and '/' are distinct, + -- but '///+' is equivalent to '/'. + if P:match '^//' and at(P, 3) ~= '/' then + anchor = '//' + P = P:sub(3) + elseif at(P, 1) == '/' then + anchor = '/' + P = P:match '^/*(.*)$' + end + end + local parts = {} + for part in P:gmatch('[^'..sep..']+') do + if part == '..' then + if #parts ~= 0 and parts[#parts] ~= '..' then + remove(parts) + else + append(parts, part) + end + elseif part ~= '.' then + append(parts, part) + end + end + P = anchor..concat(parts, sep) + if P == '' then P = '.' end + return P +end + +--- relative path from current directory or optional start point +-- @string P a path +-- @string[opt] start optional start point (default current directory) +function path.relpath (P,start) + assert_string(1,P) + if start then assert_string(2,start) end + local split,min,append = utils.split, math.min, table.insert + P = path.abspath(P,start) + start = start or currentdir() + local compare + if path.is_windows then + P = P:gsub("/","\\") + start = start:gsub("/","\\") + compare = function(v) return v:lower() end + else + compare = function(v) return v end + end + local startl, Pl = split(start,sep), split(P,sep) + local n = min(#startl,#Pl) + if path.is_windows and n > 0 and at(Pl[1],2) == ':' and Pl[1] ~= startl[1] then + return P + end + local k = n+1 -- default value if this loop doesn't bail out! + for i = 1,n do + if compare(startl[i]) ~= compare(Pl[i]) then + k = i + break + end + end + local rell = {} + for i = 1, #startl-k+1 do rell[i] = '..' end + if k <= #Pl then + for i = k,#Pl do append(rell,Pl[i]) end + end + return table.concat(rell,sep) +end + + +--- Replace a starting '~' with the user's home directory. +-- In windows, if HOME isn't set, then USERPROFILE is used in preference to +-- HOMEDRIVE HOMEPATH. This is guaranteed to be writeable on all versions of Windows. +-- @string P A file path +function path.expanduser(P) + assert_string(1,P) + if at(P,1) == '~' then + local home = getenv('HOME') + if not home then -- has to be Windows + home = getenv 'USERPROFILE' or (getenv 'HOMEDRIVE' .. getenv 'HOMEPATH') + end + return home..sub(P,2) + else + return P + end +end + + +---Return a suitable full path to a new temporary file name. +-- unlike os.tmpname(), it always gives you a writeable path (uses TEMP environment variable on Windows) +function path.tmpname () + local res = tmpnam() + -- On Windows if Lua is compiled using MSVC14 os.tmpname + -- already returns an absolute path within TEMP env variable directory, + -- no need to prepend it. + if path.is_windows and not res:find(':') then + res = getenv('TEMP')..res + end + return res +end + +--- return the largest common prefix path of two paths. +-- @string path1 a file path +-- @string path2 a file path +function path.common_prefix (path1,path2) + assert_string(1,path1) + assert_string(2,path2) + -- get them in order! + if #path1 > #path2 then path2,path1 = path1,path2 end + local compare + if path.is_windows then + path1 = path1:gsub("/", "\\") + path2 = path2:gsub("/", "\\") + compare = function(v) return v:lower() end + else + compare = function(v) return v end + end + for i = 1,#path1 do + if compare(at(path1,i)) ~= compare(at(path2,i)) then + local cp = path1:sub(1,i-1) + if at(path1,i-1) ~= sep then + cp = path.dirname(cp) + end + return cp + end + end + if at(path2,#path1+1) ~= sep then path1 = path.dirname(path1) end + return path1 + --return '' +end + +--- return the full path where a particular Lua module would be found. +-- Both package.path and package.cpath is searched, so the result may +-- either be a Lua file or a shared library. +-- @string mod name of the module +-- @return on success: path of module, lua or binary +-- @return on error: nil,error string +function path.package_path(mod) + assert_string(1,mod) + local res + mod = mod:gsub('%.',sep) + res = package.searchpath(mod,package.path) + if res then return res,true end + res = package.searchpath(mod,package.cpath) + if res then return res,false end + return raise 'cannot find module on path' +end + + +---- finis ----- +return path \ No newline at end of file diff --git a/pl/etc/fstab b/pl/etc/fstab new file mode 100644 index 0000000..02aa462 --- /dev/null +++ b/pl/etc/fstab @@ -0,0 +1 @@ +packages/pl/apis gitfs Tieske/Penlight/master/lua/pl \ No newline at end of file diff --git a/penlight/init/6.penlight.lua b/pl/init/6.penlight.lua similarity index 100% rename from penlight/init/6.penlight.lua rename to pl/init/6.penlight.lua