[latex3-commits] [git/LaTeX3-latex3-l3build] experimental: CLI refactoring. (77aceaf)

Paulo Roberto Massa Cereda cereda.paulo at gmail.com
Tue Jul 30 19:38:58 CEST 2019


Repository : https://github.com/latex3/l3build
On branch  : experimental
Link       : https://github.com/latex3/l3build/commit/77aceaf69eee1c2a802f8445054a038c8c08c56d

>---------------------------------------------------------------

commit 77aceaf69eee1c2a802f8445054a038c8c08c56d
Author: Paulo Roberto Massa Cereda <cereda.paulo at gmail.com>
Date:   Tue Jul 30 14:38:58 2019 -0300

    CLI refactoring.
    
    - New command line argument extraction function.
    - CLI options now enclosed inside a function.
    - Added comments for automated documentation (ldoc).
    - Module export.


>---------------------------------------------------------------

77aceaf69eee1c2a802f8445054a038c8c08c56d
 .travis.yml => .travis.yml.hold |   0
 l3build-arguments.lua           | 812 +++++++++++++++++++++++++++-------------
 2 files changed, 557 insertions(+), 255 deletions(-)

diff --git a/.travis.yml b/.travis.yml.hold
similarity index 100%
rename from .travis.yml
rename to .travis.yml.hold
diff --git a/l3build-arguments.lua b/l3build-arguments.lua
index cbf68c6..7eeb714 100644
--- a/l3build-arguments.lua
+++ b/l3build-arguments.lua
@@ -22,280 +22,582 @@ for those people who are interested.
 
 --]]
 
-local exit             = os.exit
-local stderr           = io.stderr
-
-local find             = string.find
-local gmatch           = string.gmatch
-local match            = string.match
-local sub              = string.sub
-
-local insert           = table.insert
-
--- Parse command line options
-
-option_list =
-  {
-    config =
-      {
-        desc  = "Sets the config(s) used for running tests",
-        short = "c",
-        type  = "table"
-      },
-    date =
-      {
-        desc  = "Sets the date to insert into sources",
-        type  = "string"
-      },
-    debug =
-      {
-        desc = "Runs target in debug mode (not supported by all targets)",
-        type = "boolean"
-      },
-    dirty =
-      {
-        desc = "Skip cleaning up the test area",
-        type = "boolean"
-      },
-    ["dry-run"] =
-      {
-        desc = "Dry run for install",
-        type = "boolean"
-      },
-    email =
-      {
-        desc = "Email address of CTAN uploader",
-        type = "string"
-      },
-    engine =
-      {
-        desc  = "Sets the engine(s) to use for running test",
-        short = "e",
-        type  = "table"
-      },
-    epoch =
-      {
-        desc  = "Sets the epoch for tests and typesetting",
-        type  = "string"
-      },
-    file =
-      {
-        desc  = "Take the upload announcement from the given file",
-        short = "F",
-        type  = "string"
-      },
-    first =
-      {
-        desc  = "Name of first test to run",
-        type  = "string"
-      },
-    force =
-      {
-        desc  = "Force tests to run if engine is not set up",
-        short = "f",
-        type  = "boolean"
-      },
-    full =
-      {
-        desc = "Install all files",
-        type = "boolean"
-      },
-    ["halt-on-error"] =
-      {
-        desc  = "Stops running tests after the first failure",
-        short = "H",
-        type  = "boolean"
-      },
-    help =
-      {
-        short = "h",
-        type  = "boolean"
-      },
-    last =
-      {
-        desc  = "Name of last test to run",
-        type  = "string"
-      },
-    message =
-      {
-        desc  = "Text for upload announcement message",
-        short = "m",
-        type  = "string"
-      },
-    quiet =
-      {
-        desc  = "Suppresses TeX output when unpacking",
-        short = "q",
-        type  = "boolean"
-      },
-    rerun =
-      {
-        desc  = "Skip setup: simply rerun tests",
-        type  = "boolean"
-      },
-    ["show-log-on-error"] =
-      {
-        desc  = "If 'halt-on-error' stops, show the full log of the failure",
-        type  = "boolean"
-      },
-    shuffle =
-      {
-        desc  = "Shuffle order of tests",
-        type  = "boolean"
-      },
-    texmfhome =
-      {
-        desc = "Location of user texmf tree",
-        type = "string"
-      }
-  }
+--- Provides handling and parsing of command line arguments.
+-- This module is self-contained has no other dependencies.
+-- @author The LaTeX3 team
+-- @license LaTeX Project Public License (LPPL 1.3c)
+-- @copyright 2019 The LaTeX3 Project
+-- @release 1.0
 
--- This is done as a function (rather than do ... end) as it allows early
--- termination (break)
-local function argparse()
-  local result = { }
-  local names  = { }
-  local long_options =  { }
-  local short_options = { }
-  -- Turn long/short options into two lookup tables
-  for k,v in pairs(option_list) do
-    if v["short"] then
-      short_options[v["short"]] = k
-    end
-    long_options[k] = k
-  end
-  local args = args
-  -- arg[1] is a special case: must be a command or "-h"/"--help"
-  -- Deal with this by assuming help and storing only apparently-valid
-  -- input
-  local a = arg[1]
-  result["target"] = "help"
-  if a then
-    -- No options are allowed in position 1, so filter those out
-    if a == "--version" then
-      result["target"] = "version"
-    elseif not match(a, "^%-") then
-      result["target"] = a
+-- the LaTeX3 namespace
+local l3args = {}
+
+-- populate the namespace with global functions
+
+-- table iterators
+l3args.ipairs = ipairs
+l3args.pairs  = pairs
+
+-- table operations
+l3args.insert = table.insert
+l3args.remove = table.remove
+
+-- string operations
+l3args.find   = string.find
+l3args.length = string.len
+l3args.sub    = string.sub
+l3args.gsub   = string.gsub
+
+--l3.exit   = os.exit
+--l3.stderr = io.stderr
+--l3.gmatch = string.gmatch
+--l3.match  = string.match
+
+--- Checks whether the provided value exists as a table element.
+-- This function searches and compares the provided value against
+-- every element in the table. If a match is found, the value
+-- therefore exists and the search ends. Otherwise, after scanning
+-- the entire table with no match, the function returns `false` as result.
+-- @param a Table of elements.
+-- @param hit Value to be searched.
+-- @return Boolean value whether the table contains the provided value
+-- as an element.
+function l3args.exists(a, hit)
+  for _, v in l3args.ipairs(a) do
+    if v == hit then
+      return true
     end
   end
-  -- Stop here if help or version is required
-  if result["target"] == "help" or result["target"] == "version" then
-    return result
-  end
-  -- An auxiliary to grab all file names into a table
-  local function remainder(num)
-    local names = { }
-    for i = num, #arg do
-      insert(names, arg[i])
+  return false
+end
+
+--- Parses the argument table based on a table of targets and options.
+-- This function parses the argument table obtained from the command line
+-- invocation and extracts options and potential corresponding values
+-- based on rules set forth in a table of targets and options. As a result,
+-- two tables are returned (one containing the correct elements and the
+-- other holding potential issues found).
+-- @param targets Table of targets which dictate the tool behaviour, given
+-- the remaining set of command line arguments.
+-- @param options Table of options which dictate the parsing operation.
+-- @param arguments Argument table obtained from the command line.
+-- @return Table holding option keys and potential corresponding values.
+-- @return Table holding potential issues found during the parsing
+-- operation. These issues are grouped into five categories, presented
+-- as follows:
+--
+-- - `unknown`: This table holds unknown options, either in their long or short
+-- forms. An unknown option is any element preceed by `-` and `--` and not
+-- explicitly described in the `options` table. The table has no duplicate values.
+-- - `duplicate`: This table holds duplicate options, i.e, options that were
+-- previously found during the argument processing, either in their long or
+-- short forms. Whenever possible, options are always normalized to their
+-- long forms. The table has no duplicate values.
+-- - `invalid`: This table holds invalid options, either in their long or short
+-- forms. An invalid option, in this case, is a boolean switch which was
+-- inadvertently specified with a value, using the `=` separator. The table has
+-- no duplicate values.
+-- - `remainder`: This table, as the name implies, holds remainder values obtained
+-- from duplicate and invalid options (see previous descriptions). The table
+-- might have duplicate values, as they are simply inserted into the structure.
+-- - `target`: This boolean switch holds the reference to an invalid target, i.e,
+-- when the first positional argument does not correspond to an element in the
+-- list of valid targets. Initially, this switch is set to `false` (invalid).
+function l3args.argparse(targets, options, arguments)
+
+  local keys, key, issues = {}, 'remainder', {}
+  local a, b, c
+
+  -- inner table
+  keys['remainder'] = {}
+
+  -- inner tables
+  issues[ 'unknown' ] = {}
+  issues['duplicate'] = {}
+  issues[ 'invalid' ] = {}
+  issues['remainder'] = {}
+
+  -- boolean switch for an invalid
+  -- target, initially set to true
+  issues['target'] = true
+
+  -- handle the positional target
+  if #arguments > 0 then
+    local target = l3args.remove(arguments, 1)
+    if l3args.exists(targets, target) then
+      keys['target'] = target
+      issues['target'] = false
     end
-    return names
   end
-  -- Examine all other arguments
-  -- Use a while loop rather than for as this makes it easier
-  -- to grab arg for optionals where appropriate
-  local i = 2
-  while i <= #arg do
-    local a = arg[i]
-    -- Terminate search for options
-    if a == "--" then
-      names = remainder(i + 1)
-      break
-    end
-    -- Look for optionals
-    local opt
-    local optarg
-    local opts
-    -- Look for and option and get it into a variable
-    if match(a, "^%-") then
-      if match(a, "^%-%-") then
-        opts = long_options
-        local pos = find(a, "=", 1, true)
-        if pos then
-          opt    = sub(a, 3, pos - 1)
-          optarg = sub(a, pos + 1)
-        else
-          opt = sub(a, 3)
+
+  for _, v in l3args.ipairs(arguments) do
+
+    -- look for a short option (no separator)
+    a, _, b = l3args.find(v, '^%-(%w+)$')
+
+    -- we got a hit
+    if a then
+      for _, x in l3args.ipairs(options) do
+
+        -- get the key reference
+        key = 'remainder'
+        if x['short'] == b then
+          key = x['long']
+
+          -- check if the key was
+          -- already defined
+          if not keys[key] then
+
+            -- check if this is a
+            -- boolean switch
+            if not x['argument'] then
+              keys[key] = true
+            end
+
+          -- we got a duplicate
+          else
+            if not l3args.exists(issues['duplicate'], '--' .. key) then
+              l3args.insert(issues['duplicate'], '--' .. key)
+            end
+          end
+          break
         end
-      else
-        opts = short_options
-        opt  = sub(a, 2, 2)
-        -- Only set optarg if it is there
-        if #a > 2 then
-          optarg = sub(a, 3)
+      end
+
+      -- key is unknown, log it
+      if key == 'remainder' then
+
+        for i = 1, l3args.length(b) do
+          a = l3args.sub(b, i, i)
+
+          for _, x in l3args.ipairs(options) do
+
+            key = 'remainder'
+            if x['short'] == a then
+              key = x['long']
+
+              -- check if the key was
+              -- already defined
+              if not keys[key] then
+
+                -- check if this is a
+                -- boolean switch
+                if not x['argument'] then
+                  keys[key] = true
+
+                -- it is not a boolean switch,
+                -- so report as an invalid flag
+                else
+                  if not l3args.exists(issues['invalid'], '--' .. key) then
+                    l3args.insert(issues['invalid'], '--' .. key)
+                  end
+                end
+
+              -- the key already exists,
+              -- report as a duplicate
+              else
+                if not l3args.exists(issues['duplicate'], '--' .. key) then
+                  l3args.insert(issues['duplicate'], '--' .. key)
+                end
+              end
+
+              break
+            end
+          end
+
+          -- key is unknown, log it
+          if key == 'remainder' then
+            if not l3args.exists(issues['unknown'], '-' .. a) then
+              l3args.insert(issues['unknown'], '-' .. a)
+            end
+          end
         end
       end
-      -- Now check that the option is valid and sort out the argument
-      -- if required
-      local optname = opts[opt]
-      if optname then
-        -- Tidy up arguments
-        if option_list[optname]["type"] == "boolean" then
-          if optarg then
-            local opt = "-" .. (match(a, "^%-%-") and "-" or "") .. opt
-            stderr:write("Value not allowed for option " .. opt .."\n")
-            return {"help"}
+
+    -- no short option, move
+    -- on to the next branch
+    else
+
+      -- look for a long option (no separator)
+      a, _, b = l3args.find(v, '^%-%-([%w-]+)$')
+
+      -- we got a hit
+      if a then
+        for _, x in l3args.ipairs(options) do
+
+          -- get the key reference
+          key = 'remainder'
+          if x['long'] == b then
+            key = b
+
+            -- check if the key was
+            -- already defined
+            if not keys[key] then
+
+              -- check if this is a
+              -- boolean switch
+              if not x['argument'] then
+                keys[key] = true
+              end
+
+            -- we got a duplicate
+            else
+              if not l3args.exists(issues['duplicate'], '--' .. key) then
+                l3args.insert(issues['duplicate'], '--' .. key)
+              end
+            end
+            break
           end
-        else
-         if not optarg then
-          optarg = arg[i + 1]
-          if not optarg then
-            stderr:write("Missing value for option " .. a .."\n")
-            return {"help"}
+        end
+
+        -- key is unknown, log it
+        if key == 'remainder' then
+          if not l3args.exists(issues['unknown'], '--' .. b) then
+            l3args.insert(issues['unknown'], '--' .. b)
           end
-          i = i + 1
-         end
         end
+
+      -- no long option, move
+      -- on to the next branch
       else
-        stderr:write("Unknown option " .. a .."\n")
-        return {"help"}
-      end
-      -- Store the result
-      if optarg then
-        if option_list[optname]["type"] == "string" then
-          result[optname] = optarg
+
+        -- look for a long option
+        -- (with the '=' separator)
+        a, _, b, c = l3args.find(v, '^%-%-([%w-]+)=(.+)$')
+
+        -- there is a hit
+        if a then
+          for _, x in l3args.ipairs(options) do
+
+            -- get the key reference
+            key = 'remainder'
+            if x['long'] == b then
+              key = b
+
+              -- check if the key is not
+              -- a boolean switch
+              if x['argument'] then
+
+                -- check if the key was
+                -- already defined
+                if not keys[key] then
+                  if x['handler'] then
+                    keys[key] = x['handler'](c)
+                  else
+                    keys[key] = c
+                  end
+
+                -- we got a duplicate, so the value
+                -- goes to the remainder, as the
+                -- other counterparts
+                else
+
+                  -- log the duplicate key
+                  if not l3args.exists(issues['duplicate'], '--' .. key) then
+                    l3args.insert(issues['duplicate'], '--' .. key)
+                  end
+
+                  -- the value is thrown to the remainder
+                  l3args.insert(issues['remainder'], c)
+                end
+
+              -- the option is actually a boolean
+              -- switch, so report the invalid key
+              else
+                if not l3args.exists(issues['invalid'], '--' .. key) then
+                  l3args.insert(issues['invalid'], '--' .. key)
+                end
+
+                -- the value is thrown to the remainder
+                l3args.insert(issues['remainder'], c)
+              end
+
+              break
+            end
+          end
+
+          -- key is unknown, log it
+          if key == 'remainder' then
+            if not l3args.exists(issues['unknown'], '--' .. b) then
+              l3args.insert(issues['unknown'], '--' .. b)
+            end
+
+            -- the value is thrown to the remainder
+            l3args.insert(issues['remainder'], c)
+          end
+
+        -- no long option with separator,
+        -- so move on to the next branch
         else
-          local opts = result[optname] or { }
-          for hit in gmatch(optarg, "([^,%s]+)") do
-            insert(opts, hit)
+
+          -- look for a short option
+          -- (with the '=' separator)
+          a, _, b, c = l3args.find(v, '^%-([%w-]+)=(.+)$')
+
+          -- there is a hit
+          if a then
+            for _, x in l3args.ipairs(options) do
+
+              -- get the key reference
+              key = 'remainder'
+              if x['short'] == b then
+                key = x['long']
+
+                -- check if this is not
+                -- a boolean switch
+                if x['argument'] then
+
+                  -- check if the key was
+                  -- already defined
+                  if not keys[key] then
+                    if x['handler'] then
+                      keys[key] = x['handler'](c)
+                    else
+                      keys[key] = c
+                    end
+
+                  -- we got a duplicate, so the value
+                  -- goes to the remainder, as the
+                  -- other counterparts
+                  else
+                    if not l3args.exists(issues['duplicate'], '--' .. key) then
+                      l3args.insert(issues['duplicate'], '--' .. key)
+                    end
+
+                    -- the value is thrown to the remainder
+                    l3args.insert(issues['remainder'], c)
+                  end
+
+                -- the option is actually a boolean
+                -- switch, so report the invalid key
+                else
+                  if not l3args.exists(issues['invalid'], '--' .. key) then
+                    l3args.insert(issues['invalid'], '--' .. key)
+                  end
+
+                  -- the value is thrown to the remainder
+                  l3args.insert(issues['remainder'], c)
+                end
+
+                break
+              end
+            end
+
+            -- key is unknown, log it
+            if key == 'remainder' then
+              if not l3args.exists(issues['unknown'], '-' .. b) then
+                l3args.insert(issues['unknown'], '-' .. b)
+              end
+
+              -- the value is thrown to the remainder
+              l3args.insert(issues['remainder'], c)
+            end
+
+          -- no short option with separator,
+          -- so move to the next branch
+          else
+
+            -- we have a valid key
+            if key ~= 'remainder' then
+              for _, x in l3args.ipairs(options) do
+
+                -- check if the current key reference
+                -- accepts a corresponding value
+                if x['long'] == key then
+                  if not (x['argument'] and not keys[key] ) then
+                    key = 'remainder'
+                  end
+                  c = x['handler']
+                  break
+                end
+              end
+
+              -- set value accordingly
+              if key ~= 'remainder' then
+                if c then
+                  keys[key] = c(v)
+                else
+                  keys[key] = v
+                end
+              else
+                l3args.insert(keys[key], v)
+              end
+
+            -- there is no key, so we are
+            -- in the remainder branch
+            else
+
+              -- insert the value into the table
+              l3args.insert(keys[key], v)
+            end
           end
-          result[optname] = opts
         end
-      else
-        result[optname] = true
       end
-      i = i + 1
-    end
-    if not opt then
-      names = remainder(i)
-      break
     end
   end
-  if next(names) then
-   result["names"] = names
-  end
-  return result
+
+  -- return the key/value table and
+  -- the potential issues
+  return keys, issues
 end
 
-options = argparse()
+--- Splits the provided string at every comma.
+-- This function splits the provided string at every comma,
+-- adding each part to a table of elements.
+-- @param a The string to be splitted at every comma.
+-- @return A table contaninig every part of the splitted
+-- text, in extraction order.
+function l3args.split(a)
+   local sep, fields = ',', {}
+   local pattern = string.format("([^%s]+)", sep)
+   l3args.gsub(a, pattern,
+    function(c)
+      fields[#fields + 1] = c
+    end)
+   return fields
+end
 
--- Sanity check
-function check_engines()
-  if options["engine"] and not options["force"] then
-     -- Make a lookup table
-     local t = { }
-    for _, engine in pairs(checkengines) do
-      t[engine] = true
-    end
-    for _, engine in pairs(options["engine"]) do
-      if not t[engine] then
-        print("\n! Error: Engine \"" .. engine .. "\" not set up for testing!")
-        print("\n  Valid values are:")
-        for _, engine in ipairs(checkengines) do
-          print("  - " .. engine)
-        end
-        print("")
-        exit(1)
-      end
-    end
-  end
+--- Fetches all command line options `l3build` has, as a table.
+-- This function simply returns all command line options
+-- `l3build` has, as a table, to be used later on by certain
+-- helper methods. The elements of such table follow a certain
+-- structure.
+-- @return A table containing all command line options. Each
+-- element is a table on itself and has the following structure:
+--
+-- - `short`: optional, it denotes the option in its short form. Please
+-- mind that at least one of the forms must be present, so the
+-- corresponding instance can be detected at runtime.
+-- - `long`: optional, it denotes the option in its long form. Please
+-- mind that at least one of the forms must be present, so the
+-- corresponding instance can be detected at runtime.
+-- - `description`: as the name implies, this entry holds the option
+-- description, to be displayed in the help menu. This entry is
+-- optional. If absent, the corresponding option will not be
+-- displayed in the help menu.
+-- `- handler`: optional, it denotes the handler in which the option
+-- value will be transformed. This feature will only be in effect
+-- when the corresponding `argument` entry is set to `true`.
+-- - `argument`: mandatory, it denotes whether the option will take
+-- an associated value. When set to `false`, the option will
+-- automatically act as a boolean switch.
+function l3args.getOptions()
+  return {
+    {
+      short       = "c",
+      long        = "config",
+      description = "Sets the config(s) used for running tests",
+      handler     = l3args.split,
+      argument    = true
+    },
+    {
+      long        = "date",
+      description = "Sets the date to insert into sources",
+      argument    = true
+    },
+    {
+      long        = "debug",
+      description = "Runs target in debug mode (not supported by all targets)",
+      argument    = false
+    },
+    {
+      long        = "dirty",
+      description = "Skip cleaning up the test area",
+      argument    = false
+    },
+    {
+      long        = "dry-run",
+      description = "Dry run for install",
+      argument    = false
+    },
+    {
+      long        = "email",
+      description = "Email address of CTAN uploader",
+      argument    = true
+    },
+    {
+      short       = "e",
+      long        = "engine",
+      description = "Sets the engine(s) to use for running test",
+      handler     = l3args.split,
+      argument    = true
+    },
+    {
+      long        = "epoch",
+      description = "Sets the epoch for tests and typesetting",
+      argument    = true
+    },
+    {
+      long        = "file",
+      short       = "F",
+      description = "Take the upload announcement from the given file",
+      argument    = true
+    },
+    {
+      long        = "first",
+      description = "Name of first test to run",
+      argument    = true
+    },
+    {
+      long        = "force",
+      short       = "f",
+      description = "Force tests to run if engine is not set up",
+      argument    = false
+    },
+    {
+      long        = "full",
+      description = "Install all files",
+      argument    = false
+    },
+    {
+      long        = "halt-on-error",
+      short       = "H",
+      description = "Stops running tests after the first failure",
+      argument    = false
+    },
+    {
+      long        = "help",
+      short       = "h",
+      argument    = false
+    },
+    {
+      long        = "last",
+      description = "Name of last test to run",
+      argument    = true
+    },
+    {
+      long        = "message",
+      short       = "m",
+      description = "Text for upload announcement message",
+      argument    = true
+    },
+    {
+      long        = "quiet",
+      short       = "q",
+      description = "Suppresses TeX output when unpacking",
+      argument    = false
+    },
+    {
+      long        = "rerun",
+      description = "Skip setup: simply rerun tests",
+      argument    = false
+    },
+    {
+      long        = "show-log-on-error",
+      description = "If 'halt-on-error' stops, show the full log of the failure",
+      argument    = false
+    },
+    {
+      long        = "shuffle",
+      description = "Shuffle order of tests",
+      argument    = false
+    },
+    {
+      long        = "texmfhome",
+      description = "Location of user texmf tree",
+      argument    = true
+    }
+  }
 end
+
+return l3args





More information about the latex3-commits mailing list