General optimizations, simplifications and improvements
[project/luci.git] / libs / core / luasrc / util.lua
index 1e03489..6cefb8e 100644 (file)
@@ -24,18 +24,45 @@ limitations under the License.
 
 ]]--
 
--- LuCI utility functions.
-module("luci.util", package.seeall)
+local io = require "io"
+local table = require "table"
+local debug = require "debug"
+local string = require "string"
+local coroutine = require "coroutine"
+
+local getmetatable, setmetatable = getmetatable, setmetatable
+local getfenv, setfenv = getfenv, setfenv
+local rawget, rawset, unpack = rawget, rawset, unpack
+local tostring, type, assert = tostring, type, assert 
+local ipairs, pairs, loadstring = ipairs, pairs, loadstring
+local require, pcall, xpcall = require, pcall, xpcall
+
+--- LuCI utility functions.
+module "luci.util"
+
+--
+-- Pythonic string formatting extension
+--
+getmetatable("").__mod = function(a, b)
+       if not b then
+               return a
+       elseif type(b) == "table" then
+               return a:format(unpack(b))
+       else
+               return a:format(b)
+       end
+end
+
 
 --
 -- Class helper routines
 --
 
---- Creates a Class object (Python-style object model)
--- Creates a new class object which can be instantiated by calling itself.
+--- Create a Class object (Python-style object model).
+-- The class object can be instantiated by calling itself.
 -- Any class functions or shared parameters can be attached to this object.
 -- Attaching a table to the class object makes this table shared between
--- all instances of this class. For object paramters use the __init__ function.
+-- all instances of this class. For object parameters use the __init__ function.
 -- Classes can inherit member functions and values from a base class.
 -- Class can be instantiated by calling them. All parameters will be passed
 -- to the __init__ function of this class - if such a function exists.
@@ -49,14 +76,10 @@ function class(base)
        local class = {}
 
        local create = function(class, ...)
-               local inst = {}
-               setmetatable(inst, {__index = class})
+               local inst = setmetatable({}, {__index = class})
 
                if inst.__init__ then
-                       local stat, err = copcall(inst.__init__, inst, ...)
-                       if not stat then
-                               error(err)
-                       end
+                       inst:__init__(...)
                end
 
                return inst
@@ -75,7 +98,7 @@ end
 --- Test whether the given object is an instance of the given class.
 -- @param object       Object instance
 -- @param class                Class object to test against
--- @return                     Boolean indicating wheather the object is an instance
+-- @return                     Boolean indicating whether the object is an instance
 -- @see                                class
 -- @see                                clone
 function instanceof(object, class)
@@ -94,24 +117,33 @@ end
 -- Scope manipulation routines
 --
 
---- Resets the scope of f doing a shallow copy of its scope into a new table
--- (ToDo: @param and @return)
+--- Replace a function scope with a shallow copy of itself.
+-- This is useful if you want to get rid of several unwanted side effects
+-- while changing the scope of a certain Lua function.
+-- @param f            Lua function
 function resfenv(f)
        setfenv(f, clone(getfenv(f)))
 end
 
---- Store given object associated with given key in the scope associated with
--- the given identifier.
--- @param f            Value containing the scope identifier
+--- Store given object associated with given key in the scope of a function.
+-- @param f            Lua function
 -- @param key  String value containg the key of the object to store
 -- @param obj  Object to store in the scope
 -- @return             Always nil
+-- @see updfenv
+-- @see resfenv
 function extfenv(f, key, obj)
        local scope = getfenv(f)
        scope[key] = obj
 end
 
---- Updates the scope of f with "extscope" (ToDo: docu)
+--- Extend the scope of a function with the contents of a table
+-- @param f            Lua function
+-- @param key  String value containg the key of the object to store
+-- @param obj  Object to store in the scope
+-- @return             Always nil
+-- @see extfenv
+-- @see resfenv
 function updfenv(f, extscope)
        update(getfenv(f), extscope)
 end
@@ -153,21 +185,28 @@ end
 
 --- Write given object to stderr.
 -- @param obj  Value to write to stderr
--- @return             Boolean indicating wheather the write operation was successful
+-- @return             Boolean indicating whether the write operation was successful
 function perror(obj)
-       io.stderr:write(tostring(obj) .. "\n")
+       return io.stderr:write(tostring(obj) .. "\n")
 end
 
 --- Recursively dumps a table to stdout, useful for testing and debugging.
 -- @param t    Table value to dump
--- @param i    Number of tabs to prepend to each line
+-- @param maxdepth     Maximum depth
 -- @return     Always nil
-function dumptable(t, i)
+function dumptable(t, maxdepth, i, seen)
        i = i or 0
+       seen = seen or setmetatable({}, {__mode="k"})
+       
        for k,v in pairs(t) do
-               print(string.rep("\t", i) .. tostring(k), tostring(v))
-               if type(v) == "table" then
-                       dumptable(v, i+1)
+               perror(string.rep("\t", i) .. tostring(k) .. "\t" .. tostring(v))
+               if type(v) == "table" and (not maxdepth or i < maxdepth) then
+                       if not seen[v] then
+                               seen[v] = true
+                               dumptable(v, maxdepth, i+1, seen)
+                       else
+                               perror(string.rep("\t", i) .. "*** RECURSION ***")
+                       end
                end
        end
 end
@@ -177,10 +216,10 @@ end
 -- String and data manipulation routines
 --
 
---- Escapes all occurences of the given character in given string.
+--- Escapes all occurrences of the given character in given string.
 -- @param s    String value containing unescaped characters
 -- @param c    String value with character to escape (optional, defaults to "\")
--- @return     String value with each occurence of character escaped with "\"
+-- @return     String value with each occurrence of character escaped with "\"
 function escape(s, c)
        c = c or "\\"
        return s:gsub(c, "\\" .. c)
@@ -190,6 +229,8 @@ end
 -- @param value        String value containing the data to escape
 -- @return             String value containing the escaped data
 function pcdata(value)
+       if not value then return end
+       value = tostring(value)
        value = value:gsub("&", "&amp;")
        value = value:gsub('"', "&quot;")
        value = value:gsub("'", "&apos;")
@@ -197,15 +238,22 @@ function pcdata(value)
        return value:gsub(">", "&gt;")
 end
 
---- Splits given string on a defined seperator sequence and return a table
+--- Strip HTML tags from given string.
+-- @param value        String containing the HTML text
+-- @return     String with HTML tags stripped of
+function striptags(s)
+       return pcdata(s:gsub("</?[A-Za-z][A-Za-z0-9:_%-]*[^>]*>", " "):gsub("%s+", " "))
+end
+
+--- Splits given string on a defined separator sequence and return a table
 -- containing the resulting substrings. The optional max parameter specifies
 -- the number of bytes to process, regardless of the actual length of the given
--- string. The optional last parameter, regex, sepcifies wheather the separator
+-- string. The optional last parameter, regex, specifies whether the separator
 -- sequence is interpreted as regular expression.
 -- @param str          String value containing the data to split up
 -- @param pat          String with separator pattern (optional, defaults to "\n")
--- @param max          Num of bytes to process (optional, default is string length)
--- @param regexp       Boolean indicating wheather to interprete the separator
+-- @param max          Maximum times to split (optional)
+-- @param regex        Boolean indicating whether to interpret the separator
 --                                     pattern as regular expression (optional, default is false)
 -- @return                     Table containing the resulting substrings
 function split(str, pat, max, regex)
@@ -245,12 +293,11 @@ end
 -- @param str  String value containing whitespace padded data
 -- @return             String value with leading and trailing space removed
 function trim(str)
-       local s = str:gsub("^%s*(.-)%s*$", "%1")
-       return s
+       return (str:gsub("^%s*(.-)%s*$", "%1"))
 end
 
 --- Parse certain units from the given string and return the canonical integer
--- value or 0 if the unit is unknown. Upper- or lowercase is irrelevant.
+-- value or 0 if the unit is unknown. Upper- or lower case is irrelevant.
 -- Recognized units are:
 --     o "y"   - one year   (60*60*24*366)
 --  o "m"      - one month  (60*60*24*31)
@@ -311,7 +358,7 @@ end
 --- Combines two or more numerically indexed tables into one.
 -- @param tbl1 Table value to combine
 -- @param tbl2 Table value to combine
--- @param tblN More values to combine
+-- @param ...  More tables to combine
 -- @return             Table value containing all values of given tables
 function combine(...)
        local result = {}
@@ -326,7 +373,7 @@ end
 --- Checks whether the given table contains the given value.
 -- @param table        Table value
 -- @param value        Value to search within the given table
--- @return             Boolean indicating wheather the given value occurs within table
+-- @return             Boolean indicating whether the given value occurs within table
 function contains(table, value)
        for k, v in pairs(table) do
                if value == v then
@@ -347,9 +394,22 @@ function update(t, updates)
        end
 end
 
+--- Retrieve all keys of given associative table.
+-- @param t    Table to extract keys from
+-- @return     Sorted table containing the keys
+function keys(t)
+       local keys = { }
+       if t then
+               for k, _ in kspairs(t) do
+                       table.insert( keys, k )
+               end
+       end
+       return keys
+end
+
 --- Clones the given object and return it's copy.
 -- @param object       Table value to clone
--- @param deep         Boolean indicating wheather to do recursive cloning
+-- @param deep         Boolean indicating whether to do recursive cloning
 -- @return                     Cloned table value
 function clone(object, deep)
        local copy = {}
@@ -361,9 +421,83 @@ function clone(object, deep)
                copy[k] = v
        end
 
-       setmetatable(copy, getmetatable(object))
+       return setmetatable(copy, getmetatable(object))
+end
+
+
+--- Create a dynamic table which automatically creates subtables.
+-- @return     Dynamic Table
+function dtable()
+        return setmetatable({}, { __index =
+                function(tbl, key)
+                        return rawget(tbl, key)
+                         or rawget(rawset(tbl, key, dtable()), key)
+                end
+        })
+end
+
+
+-- Serialize the contents of a table value.
+function _serialize_table(t, seen)
+       assert(not seen[t], "Recursion detected.")
+       seen[t] = true
+       
+       local data  = ""
+       local idata = ""
+       local ilen  = 0
+
+       for k, v in pairs(t) do
+               if type(k) ~= "number" or k < 1 or math.floor(k) ~= k or ( k - #t ) > 3 then
+                       k = serialize_data(k, seen)
+                       v = serialize_data(v, seen)
+                       data = data .. ( #data > 0 and ", " or "" ) ..
+                               '[' .. k .. '] = ' .. v
+               elseif k > ilen then
+                       ilen = k
+               end
+       end
+
+       for i = 1, ilen do
+               local v = serialize_data(t[i], seen)
+               idata = idata .. ( #idata > 0 and ", " or "" ) .. v
+       end             
+
+       return idata .. ( #data > 0 and #idata > 0 and ", " or "" ) .. data
+end
+
+--- Recursively serialize given data to lua code, suitable for restoring
+-- with loadstring().
+-- @param val  Value containing the data to serialize
+-- @return             String value containing the serialized code
+-- @see                        restore_data
+-- @see                        get_bytecode
+function serialize_data(val, seen)
+       seen = seen or setmetatable({}, {__mode="k"})
+       
+       if val == nil then
+               return "nil"
+       elseif type(val) == "number" then
+               return val
+       elseif type(val) == "string" then
+               return "%q" % val
+       elseif type(val) == "boolean" then
+               return val and "true" or "false"
+       elseif type(val) == "function" then
+               return "loadstring(%q)" % get_bytecode(val)
+       elseif type(val) == "table" then
+               return "{ " .. _serialize_table(val, seen) .. " }"
+       else
+               return '"[unhandled data type:' .. type(val) .. ']"'
+       end
+end
 
-       return copy
+--- Restore data previously serialized with serialize_data().
+-- @param str  String containing the data to restore
+-- @return             Value containing the restored data structure
+-- @see                        serialize_data
+-- @see                        get_bytecode
+function restore_data(str)
+       return loadstring("return " .. str)()
 end
 
 
@@ -371,13 +505,20 @@ end
 -- Byte code manipulation routines
 --
 
---- Return the current runtime bytecode of the given function. The byte code
+--- Return the current runtime bytecode of the given data. The byte code
 -- will be stripped before it is returned.
--- @param f    Function value to return as bytecode
--- @return     String value containing the bytecode of the given function
-function get_bytecode(f)
-       local d = string.dump(f)
-       return d and strip_bytecode(d)
+-- @param val  Value to return as bytecode
+-- @return             String value containing the bytecode of the given data
+function get_bytecode(val)
+       local code
+
+       if type(val) == "function" then
+               code = string.dump(val)
+       else
+               code = string.dump( loadstring( "return " .. serialize_data(val) ) )
+       end
+
+       return code and strip_bytecode(code)
 end
 
 --- Strips unnescessary lua bytecode from given string. Information like line
@@ -500,17 +641,106 @@ end
 
 
 --
+-- System utility functions
+--
+
+--- Test whether the current system is operating in big endian mode.
+-- @return     Boolean value indicating whether system is big endian
+function bigendian()
+       return string.byte(string.dump(function() end), 7) == 0
+end
+
+--- Execute given commandline and gather stdout.
+-- @param command      String containing command to execute
+-- @return                     String containing the command's stdout
+function exec(command)
+       local pp   = io.popen(command)
+       local data = pp:read("*a")
+       pp:close()
+
+       return data
+end
+
+--- Return a line-buffered iterator over the output of given command.
+-- @param command      String containing the command to execute
+-- @return                     Iterator
+function execi(command)
+       local pp = io.popen(command)
+
+       return pp and function()
+               local line = pp:read()
+               
+               if not line then
+                       pp:close()
+               end
+               
+               return line
+       end
+end
+
+-- Deprecated
+function execl(command)
+       local pp   = io.popen(command)
+       local line = ""
+       local data = {}
+
+       while true do
+               line = pp:read()
+               if (line == nil) then break end
+               table.insert(data, line)
+       end
+       pp:close()
+
+       return data
+end
+
+--- Returns the absolute path to LuCI base directory.
+-- @return             String containing the directory path
+function libpath()
+       return require "luci.fs".dirname(require "luci.debug".__file__)
+end
+
+
+--
 -- Coroutine safe xpcall and pcall versions modified for Luci
 -- original version:
 -- coxpcall 1.13 - Copyright 2005 - Kepler Project (www.keplerproject.org)
 --
+-- Copyright © 2005 Kepler Project.
+-- 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 performResume, handleReturnValue
 local oldpcall, oldxpcall = pcall, xpcall
 coxpt = {}
 setmetatable(coxpt, {__mode = "kv"})
 
---- (ToDo: docu)
+-- Identity function for copcall
+local function copcall_id(trace, ...)
+  return ...
+end
+
+--- This is a coroutine-safe drop-in replacement for Lua's "xpcall"-function
+-- @param f            Lua function to be called protected
+-- @param err  Custom error handler
+-- @param ...  Parameters passed to the function
+-- @return             A boolean whether the function call succeeded and the return
+--                             values of either the function or the error handler
 function coxpcall(f, err, ...)
        local res, co = oldpcall(coroutine.create, f)
        if not res then
@@ -524,17 +754,16 @@ function coxpcall(f, err, ...)
        return performResume(err, co, ...)
 end
 
---- (ToDo: docu)
+--- This is a coroutine-safe drop-in replacement for Lua's "pcall"-function
+-- @param f            Lua function to be called protected
+-- @param ...  Parameters passed to the function
+-- @return             A boolean whether the function call succeeded and the returns
+--                             values of the function or the error object
 function copcall(f, ...)
-       return coxpcall(f, id, ...)
-end
-
---- (ToDo: docu)
-local function id(trace, ...)
-  return ...
+       return coxpcall(f, copcall_id, ...)
 end
 
---- (ToDo: docu)
+-- Handle return value of protected call
 function handleReturnValue(err, co, status, ...)
        if not status then
                return false, err(debug.traceback(co, (...)), ...)
@@ -546,7 +775,7 @@ function handleReturnValue(err, co, status, ...)
        end
 end
 
---- (ToDo: docu)
+-- Resume execution of protected function call
 function performResume(err, co, ...)
        return handleReturnValue(err, co, coroutine.resume(co, ...))
 end