Skip to content

Commit

Permalink
updated pl.app docs, adding examples and tests
Browse files Browse the repository at this point in the history
includes some refactoring with pl.utils. And a new backwards
compatible option for parse_args, to accept only valid flags
  • Loading branch information
Tieske committed Nov 27, 2018
1 parent 424bb8e commit ee381b7
Show file tree
Hide file tree
Showing 8 changed files with 464 additions and 98 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

### New features

- `utils.quote_arg` will now optionally take an array of arguments and escape
them all into a single string.
- `app.parse_args` now accepts a 3rd parameter with a list of valid flags and aliasses
- `app.script_name` returns the name of the current script (previously a private function)

### Changes

Expand All @@ -20,6 +24,7 @@
- `compat.execute` now handles the Windows exitcode -1 properly
- `types.is_empty` would return true on spaces always, indepedent of the parameter
- `types.to_bool` will now compare case-insensitive for the extra passed strings
- `app.require_here` will now properly handle an absolute base path

## 1.6.0 (2018-11-23)

Expand Down
5 changes: 0 additions & 5 deletions examples/testapp.lua

This file was deleted.

196 changes: 165 additions & 31 deletions lua/pl/app.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ local path = require 'pl.path'

local app = {}

local function check_script_name ()
if _G.arg == nil then error('no command line args available\nWas this run from a main script?') end
return _G.arg[0]
--- return the name of the current script running.
-- The name will be the name as passed on the command line
-- @return string filename
function app.script_name()
if _G.arg and _G.arg[0] then
return _G.arg[0]
end
return utils.raise("No script name found")
end

--- add the current script's path to the Lua module path.
Expand All @@ -24,15 +29,20 @@ end
-- @string base optional base directory.
-- @treturn string the current script's path with a trailing slash
function app.require_here (base)
local p = path.dirname(check_script_name())
local p = path.dirname(app.script_name())
if not path.isabs(p) then
p = path.join(path.currentdir(),p)
end
if p:sub(-1,-1) ~= path.sep then
p = p..path.sep
end
if base then
p = p..base..path.sep
base = path.normcase(base)
if path.isabs(base) then
p = base .. path.sep
else
p = p..base..path.sep
end
end
local so_ext = path.is_windows and 'dll' or 'so'
local lsep = package.path:find '^;' and '' or ';'
Expand All @@ -45,16 +55,24 @@ end
--- return a suitable path for files private to this application.
-- These will look like '~/.SNAME/file', with '~' as with expanduser and
-- SNAME is the name of the script without .lua extension.
-- If the directory does not exist, it will be created.
-- @string file a filename (w/out path)
-- @return a full pathname, or nil
-- @return 'cannot create' error
function app.appfile (file)
local sname = path.basename(check_script_name())
-- @return cannot create directory error
-- @usage
-- -- when run from a script called 'testapp' (on Windows):
-- local app = require 'pl.app'
-- print(app.appfile 'test.txt')
-- -- C:\Documents and Settings\steve\.testapp\test.txt
function app.appfile(file)
local sfullname, err = app.script_name()
if not sfullname then return utils.raise(err) end
local sname = path.basename(sfullname)
local name = path.splitext(sname)
local dir = path.join(path.expanduser('~'),'.'..name)
if not path.isdir(dir) then
local ret = path.mkdir(dir)
if not ret then return utils.raise ('cannot create '..dir) end
if not ret then return utils.raise('cannot create '..dir) end
end
return path.join(dir,file)
end
Expand All @@ -74,55 +92,162 @@ function app.platform()
end

--- return the full command-line used to invoke this script.
-- Any extra flags occupy slots, so that `lua -lpl` gives us `{[-2]='lua',[-1]='-lpl'}`
-- It will not include the scriptname itself, see `app.script_name`.
-- @return command-line
-- @return name of Lua program used
function app.lua ()
local args = _G.arg or error "not in a main program"
local imin = 0
for i in pairs(args) do
if i < imin then imin = i end
-- @usage
-- -- execute: lua -lluacov -e 'print(_VERSION)' myscript.lua
--
-- -- myscript.lua
-- print(require("pl.app").lua())) --> lua -lluacov -e 'print(_VERSION)'
function app.lua()
local args = _G.arg
if not args then
return utils.raise "not in a main program"
end
local cmd, append = {}, table.insert
for i = imin,-1 do
append(cmd, utils.quote_arg(args[i]))

local cmd = {}
local i = -1
while true do
table.insert(cmd, 1, args[i])
if not args[i-1] then
return utils.quote_arg(cmd), args[i]
end
i = i - 1
end
return table.concat(cmd,' '),args[imin]
end

--- parse command-line arguments into flags and parameters.
-- Understands GNU-style command-line flags; short (`-f`) and long (`--flag`).
-- These may be given a value with either '=' or ':' (`-k:2`,`--alpha=3.2`,`-n2`);
-- note that a number value can be given without a space.
--
-- These may be given a value with either '=' or ':' (`-k:2`,`--alpha=3.2`,`-n2`),
-- a number value can be given without a space. If the flag is marked
-- as having a value, then a space-separated value is also accepted (`-i hello`),
-- see the `flags_with_values` argument).
--
-- Multiple short args can be combined like so: ( `-abcd`).
--
-- When specifying the `flags_valid` parameter, its contents can also contain
-- aliasses, to convert short/long flags to the same output name. See the
-- example below.
--
-- Note: if a flag is repeated, the last value wins.
-- @tparam {string} args an array of strings (default is the global `arg`)
-- @tab flags_with_values any flags that take values, e.g. `{out=true}`
-- @tab flags_with_values any flags that take values, either list or hash
-- table e.g. `{ out=true }` or `{ "out" }`.
-- @tab flags_valid (optional) flags that are valid, either list or hashtable.
-- If not given, everything
-- will be accepted(everything in `flags_with_values` will automatically be allowed)
-- @return a table of flags (flag=value pairs)
-- @return an array of parameters
-- @raise if args is nil, then the global `args` must be available!
function app.parse_args (args,flags_with_values)
-- @usage
-- -- Simple form:
-- local flags, params = app.parse_args(nil,
-- { "hello", "world" }, -- list of flags taking values
-- { "l", "a", "b"}) -- list of allowed flags (value ones will be added)
--
-- -- More complex example using aliasses:
-- local valid = {
-- long = "l", -- if 'l' is specified, it is reported as 'long'
-- new = { "n", "old" }, -- here both 'n' and 'old' will go into 'new'
-- }
-- local values = {
-- "value", -- will automatically be added to the allowed set of flags
-- "new", -- will mark 'n' and 'old' as requiring a value as well
-- }
-- local flags, params = app.parse_args(nil, values, valid)
--
-- -- command: myapp.lua -l --old:hello --value world param1 param2
-- -- will yield:
-- flags = {
-- long = true, -- input from 'l'
-- new = "hello", -- input from 'old'
-- value = "world", -- allowed because it was in 'values', note: space separated!
-- }
-- params = {
-- [1] = "param1"
-- [2] = "param2"
-- }
function app.parse_args (args,flags_with_values, flags_valid)
if not args then
args = _G.arg
if not args then error "Not in a main program: 'arg' not found" end
if not args then utils.raise "Not in a main program: 'arg' not found" end
end

local with_values = {}
for k,v in pairs(flags_with_values or {}) do
if type(k) == "number" then
k = v
end
with_values[k] = true
end

local valid
if not flags_valid then
-- if no allowed flags provided, we create a table that always returns
-- the keyname, no matter what you look up
valid = setmetatable({},{ __index = function(_, key) return key end })
else
valid = {}
for k,aliasses in pairs(flags_valid) do
if type(k) == "number" then -- array/list entry
k = aliasses
end
if type(aliasses) == "string" then -- single alias
aliasses = { aliasses }
end
if type(aliasses) == "table" then -- list of aliasses
-- it's the alternate name, so add the proper mappings
for i, alias in ipairs(aliasses) do
valid[alias] = k
end
end
valid[k] = k
end
do
local new_with_values = {} -- needed to prevent "invalid key to 'next'" error
for k,v in pairs(with_values) do
if not valid[k] then
valid[k] = k -- add the with_value entry as a valid one
new_with_values[k] = true
else
new_with_values[valid[k]] = true --set, but by its alias
end
end
with_values = new_with_values
end
end
flags_with_values = flags_with_values or {}

-- now check that all flags with values are reported as such under all
-- of their aliasses
for k, main_alias in pairs(valid) do
if with_values[main_alias] then
with_values[k] = true
end
end

local _args = {}
local flags = {}
local i = 1
while i <= #args do
local a = args[i]
local v = a:match('^-(.+)')
local is_long
if v then -- we have a flag
if not v then
-- we have a parameter
_args[#_args+1] = a
else
-- it's a flag
if v:find '^-' then
is_long = true
v = v:sub(2)
end
if flags_with_values[v] then
if with_values[v] then
if i == #args or args[i+1]:find '^-' then
return utils.raise ("no value for '"..v.."'")
end
flags[v] = args[i+1]
flags[valid[v]] = args[i+1]
i = i + 1
else
-- a value can also be indicated with = or :
Expand All @@ -136,7 +261,13 @@ function app.parse_args (args,flags_with_values)
var = var:sub(1,1)
else -- multiple short flags
for i = 1,#var do
flags[var:sub(i,i)] = true
local f = var:sub(i,i)
if not valid[f] then
return utils.raise("unknown flag '"..f.."'")
else
f = valid[f]
end
flags[f] = true
end
val = nil -- prevents use of var as a flag below
end
Expand All @@ -145,11 +276,14 @@ function app.parse_args (args,flags_with_values)
end
end
if val then
if not valid[var] then
return utils.raise("unknown flag '"..var.."'")
else
var = valid[var]
end
flags[var] = val
end
end
else
_args[#_args+1] = a
end
i = i + 1
end
Expand Down
5 changes: 3 additions & 2 deletions lua/pl/import_into.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ return function(env)
comprehension=true,xml=true,types=true,
test = true, app = true, file = true, class = true,
luabalanced = true, permute = true, template = true,
url = true, compat = true, List = true, Map = true, Set = true,
OrderedMap = true, MultiMap = true, Date = true,
url = true, compat = true,
-- classes --
List = true, Map = true, Set = true,
OrderedMap = true, MultiMap = true, Date = true,
}
rawset(env,'utils',require 'pl.utils')

Expand Down
12 changes: 5 additions & 7 deletions lua/pl/text.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ local bind1,usplit,assert_arg = utils.bind1,utils.split,utils.assert_arg
local is_callable = require 'pl.types'.is_callable
local unpack = utils.unpack

local text = {}


local function makelist(l)
return setmetatable(l, require('pl.List'))
end
Expand All @@ -39,12 +42,6 @@ local function imap(f,t,...)
return res
end

--[[
module ('pl.text',utils._module)
]]

local text = {}

local function _indent (s,sp)
local sl = split(s,'\n')
return concat(imap(bind1('..',sp),sl),'\n')..'\n'
Expand Down Expand Up @@ -78,7 +75,8 @@ end
-- to that extent.
-- @param s the string
-- @param width the margin width, default 70
-- @return a list of lines
-- @return a list of lines (List object)
-- @see pl.List
function text.wrap (s,width)
assert_arg(1,s,'string')
width = width or 70
Expand Down
28 changes: 23 additions & 5 deletions lua/pl/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -382,12 +382,30 @@ function utils.executeex(cmd, bin)
return success, retcode, (outcontent or ""), (errcontent or "")
end

--- Quote an argument of a command.
-- Quotes a single argument of a command to be passed
--- Quote and escape an argument of a command.
-- Quotes a single (or list of) argument(s) of a command to be passed
-- to `os.execute`, `pl.utils.execute` or `pl.utils.executeex`.
-- @string argument the argument.
-- @return quoted argument.
-- @param argument (string or table/list) the argument to quote. If a list then
-- all arguments in the list will be returned as a single string quoted.
-- @return quoted and escaped argument.
-- @usage
-- local options = utils.quote_arg {
-- "-lluacov",
-- "-e",
-- "utils = print(require('pl.utils')._VERSION",
-- }
-- -- returns: -lluacov -e 'utils = print(require('\''pl.utils'\'')._VERSION'
function utils.quote_arg(argument)
if type(argument) == "table" then
-- encode an entire table
local r = {}
for i, arg in ipairs(argument) do
r[i] = utils.quote_arg(arg)
end

return table.concat(r, " ")
end
-- only a single argument
if is_windows then
if argument == "" or argument:find('[ \f\t\v]') then
-- Need to quote the argument.
Expand Down Expand Up @@ -437,7 +455,7 @@ end
--- String functions
-- @section string-functions

--- escape any 'magic' characters in a string
--- escape any Lua 'magic' characters in a string
-- @param s The input string
function utils.escape(s)
utils.assert_string(1,s)
Expand Down
Loading

0 comments on commit ee381b7

Please sign in to comment.