From 052f9a67d02ac166e6d34d8c1edf9c76e67cf7f3 Mon Sep 17 00:00:00 2001 From: XeroOl Date: Mon, 1 Aug 2022 18:21:27 -0500 Subject: [PATCH] reduce the prevelance of penlight --- .gitignore | 2 +- Makefile | 2 +- README.md | 4 +- src/fennel-ls/json-rpc.fnl | 5 +- src/fennel-ls/mod.fnl | 38 +- src/fennel-ls/the-actual-code.fnl | 1 - src/fennel-ls/util.fnl | 4 +- src/pl/CHANGELOG.md | 669 ----------------- src/pl/CONTRIBUTING.md | 60 -- src/pl/Date.lua | 675 ----------------- src/pl/List.lua | 566 -------------- src/pl/Map.lua | 120 --- src/pl/MultiMap.lua | 54 -- src/pl/OrderedMap.lua | 167 ----- src/pl/README.md | 127 ---- src/pl/Set.lua | 222 ------ src/pl/app.lua | 297 -------- src/pl/array2d.lua | 585 --------------- src/pl/class.lua | 265 ------- src/pl/compat.lua | 250 ------- src/pl/comprehension.lua | 285 ------- src/pl/config.lua | 207 ----- src/pl/data.lua | 654 ---------------- src/pl/dir.lua | 527 ------------- src/pl/file.lua | 55 -- src/pl/func.lua | 393 ---------- src/pl/import_into.lua | 91 --- src/pl/init.lua | 11 - src/pl/input.lua | 171 ----- src/pl/lapp.lua | 451 ----------- src/pl/lexer.lua | 515 ------------- src/pl/luabalanced.lua | 264 ------- src/pl/operator.lua | 209 ------ src/pl/path.lua | 575 -------------- src/pl/permute.lua | 196 ----- src/pl/pretty.lua | 437 ----------- src/pl/seq.lua | 544 -------------- src/pl/sip.lua | 337 --------- src/pl/strict.lua | 138 ---- src/pl/stringx.lua | 917 ----------------------- src/pl/tablex.lua | 999 ------------------------- src/pl/template.lua | 202 ----- src/pl/test.lua | 164 ---- src/pl/text.lua | 26 - src/pl/types.lua | 183 ----- src/pl/url.lua | 51 -- src/pl/utils.lua | 841 --------------------- src/pl/xml.lua | 1164 ----------------------------- test/init.fnl | 2 +- test/json-rpc-test.fnl | 3 +- test/lsp-test.fnl | 9 +- {src => test}/pl/LICENSE.md | 0 {src => test}/pl/stringio.lua | 0 test/string-processing-test.fnl | 2 - test/util.fnl | 3 +- 55 files changed, 42 insertions(+), 14697 deletions(-) delete mode 100644 src/pl/CHANGELOG.md delete mode 100644 src/pl/CONTRIBUTING.md delete mode 100644 src/pl/Date.lua delete mode 100644 src/pl/List.lua delete mode 100644 src/pl/Map.lua delete mode 100644 src/pl/MultiMap.lua delete mode 100644 src/pl/OrderedMap.lua delete mode 100644 src/pl/README.md delete mode 100644 src/pl/Set.lua delete mode 100644 src/pl/app.lua delete mode 100644 src/pl/array2d.lua delete mode 100644 src/pl/class.lua delete mode 100644 src/pl/compat.lua delete mode 100644 src/pl/comprehension.lua delete mode 100644 src/pl/config.lua delete mode 100644 src/pl/data.lua delete mode 100644 src/pl/dir.lua delete mode 100644 src/pl/file.lua delete mode 100644 src/pl/func.lua delete mode 100644 src/pl/import_into.lua delete mode 100644 src/pl/init.lua delete mode 100644 src/pl/input.lua delete mode 100644 src/pl/lapp.lua delete mode 100644 src/pl/lexer.lua delete mode 100644 src/pl/luabalanced.lua delete mode 100644 src/pl/operator.lua delete mode 100644 src/pl/path.lua delete mode 100644 src/pl/permute.lua delete mode 100644 src/pl/pretty.lua delete mode 100644 src/pl/seq.lua delete mode 100644 src/pl/sip.lua delete mode 100644 src/pl/strict.lua delete mode 100644 src/pl/stringx.lua delete mode 100644 src/pl/tablex.lua delete mode 100644 src/pl/template.lua delete mode 100644 src/pl/test.lua delete mode 100644 src/pl/text.lua delete mode 100644 src/pl/types.lua delete mode 100644 src/pl/url.lua delete mode 100644 src/pl/utils.lua delete mode 100644 src/pl/xml.lua rename {src => test}/pl/LICENSE.md (100%) rename {src => test}/pl/stringio.lua (100%) diff --git a/.gitignore b/.gitignore index ca7d968..9226e05 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -^fennel-ls$ +/fennel-ls diff --git a/Makefile b/Makefile index efe5641..90ec671 100644 --- a/Makefile +++ b/Makefile @@ -10,4 +10,4 @@ fennel-ls: $(SOURCES) clean: rm -f fennel-ls test: - ./fennel --correlate test/init.fnl + FENNEL_PATH="./src/?.fnl;./src/?/init.fnl" ./fennel --correlate test/init.fnl diff --git a/README.md b/README.md index 72ebc1e..954026e 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,8 @@ and `./src/fennel-ls/json-rpc.fnl` is where messages are being converted to json ## License fennel-ls is licensed under the MIT license. See LICENSE for more info. However, this project contains files from other projects: -* src/pl comes from [Penlight](https://github.com/lunarmodules/Penlight) +* test/pl comes from [Penlight](https://github.com/lunarmodules/Penlight) * src/json comes from [json.lua](https://github.com/rxi/json.lua) * fennel and src/fennel.lua are built from [fennel](https://git.sr.ht/~technomancy/fennel) -fennel-ls's MIT license may not apply to these files; check those projects for their respective code licenses. +fennel-ls's license does not apply to these files; check those projects for their respective code licenses. diff --git a/src/fennel-ls/json-rpc.fnl b/src/fennel-ls/json-rpc.fnl index 3d7881e..dadcb73 100644 --- a/src/fennel-ls/json-rpc.fnl +++ b/src/fennel-ls/json-rpc.fnl @@ -12,7 +12,6 @@ Luckily, I'm testing with Neovim, so I can pretend these problems don't exist fo ;; TODO find json library that doesn't conflate missing fields with null (local {: encode : decode} (require :json.json)) -(local {: split} (require :pl.stringx)) (λ read-header [in ?header] "Reads the header of a JSON-RPC message" @@ -22,7 +21,9 @@ Luckily, I'm testing with Neovim, so I can pretend these problems don't exist fo nil nil ;; hit end of stream, return nil ;; reading an actual line header-line - (let [[k v] (split header-line ": " 2)] + (let [sep (string.find header-line ": ") + k (string.sub header-line 1 (- sep 1)) + v (string.sub header-line (+ sep 2))] (tset header k (string.sub v 1 -2)) (read-header in header))))) diff --git a/src/fennel-ls/mod.fnl b/src/fennel-ls/mod.fnl index e988fc2..0223519 100644 --- a/src/fennel-ls/mod.fnl +++ b/src/fennel-ls/mod.fnl @@ -3,25 +3,43 @@ This file has all the logic needed to take the name of a module and find the cor I suspect this file is going to be gone after a bit of refactoring." (local fennel (require :fennel)) -(local stringx (require :pl.stringx)) -(local plpath (require :pl.path)) (local util (require :fennel-ls.util)) "works on my machine >:)" (local luapath "?.lua;src/?.lua") (local fennelpath "?.fnl;src/?.fnl") -(fn add-workspaces-to-path [path ?workspaces] - (let [paths (stringx.split path ";") - result []] - (each [_ path (ipairs paths)] - (if (plpath.isabs path) +(local sep (package.config:sub 1 1)) + +(λ is_absolute [path] + (or + ;; windows + (-> path + (: :sub 2 3) + (: :match ":\\")) + ;; modern society + (= (path:sub 1 1) "/"))) + +(λ join [path suffix] + (-> (.. path sep suffix) + ;; delete duplicate + ;; windows + (: :gsub "\\+" "\\") + ;; modern society + (: :gsub "/+" "/") + (->> (pick-values 1)))) + +(λ add-workspaces-to-path [path ?workspaces] + "Make every relative path be relative to every workspace." + (let [result []] + (each [path (path:gmatch "[^;]+")] + (if (is_absolute path) (table.insert result path) - (each [_ space (ipairs (or ?workspaces []))] - (table.insert result (plpath.normpath (plpath.join (util.uri->path space) path)))))) + (each [_ workspace (ipairs (or ?workspaces []))] + (table.insert result (join (util.uri->path workspace) path))))) (table.concat result ";"))) -(fn lookup [{: root-uri} mod] +(λ lookup [{: root-uri} mod] (match (or (fennel.searchModule mod (add-workspaces-to-path luapath [root-uri])) (fennel.searchModule mod (add-workspaces-to-path fennelpath [root-uri]))) modname (util.path->uri modname) diff --git a/src/fennel-ls/the-actual-code.fnl b/src/fennel-ls/the-actual-code.fnl index caec16c..7d3f26d 100644 --- a/src/fennel-ls/the-actual-code.fnl +++ b/src/fennel-ls/the-actual-code.fnl @@ -5,7 +5,6 @@ Every time the client sends a message, it gets handled by a function in the corr (ie, a textDocument/didChange notification will call notifications.textDocument/didChange and a textDocument/defintion request will call requests.textDocument/didChange)" (local fennel (require :fennel)) -(local dir (require :pl.dir)) (local parser (require :fennel-ls.parser)) (local util (require :fennel-ls.util)) diff --git a/src/fennel-ls/util.fnl b/src/fennel-ls/util.fnl index c8b1f54..60175a6 100644 --- a/src/fennel-ls/util.fnl +++ b/src/fennel-ls/util.fnl @@ -3,7 +3,9 @@ A collection of utility functions. Many of these convert data between a Language-Server-Protocol representation and a Lua representation. These functions are all pure functions, which makes me happy." -(local {: startswith} (require :pl.stringx)) +(λ startswith [str pre] + (let [len (length pre)] + (= (str:sub 1 len) pre))) (λ uri->path [uri] (local prefix "file://") diff --git a/src/pl/CHANGELOG.md b/src/pl/CHANGELOG.md deleted file mode 100644 index 968e94b..0000000 --- a/src/pl/CHANGELOG.md +++ /dev/null @@ -1,669 +0,0 @@ -# Changelog - -Versioning is strictly according to [Semantic Versioning](https://semver.org/), -see the [README.md](README.md#versioning) for details on version scoping and -deprecation policy. - -see [CONTRIBUTING.md](CONTRIBUTING.md#release-instructions-for-a-new-version) for release instructions - -## 1.13.1 (2022-Jul-22) - - fix: `warn` unquoted argument - [#439](https://github.com/lunarmodules/Penlight/pull/439) - -## 1.13.0 (2022-Jul-22) - - fix: `xml.parse` returned nonsense when given a file name - [#431](https://github.com/lunarmodules/Penlight/pull/431) - - feat: `app.require_here` now follows symlink'd main modules to their directory - [#423](https://github.com/lunarmodules/Penlight/pull/423) - - fix: `pretty.write` invalid order function for sorting - [#430](https://github.com/lunarmodules/Penlight/pull/430) - - fix: `compat.warn` raised write guard warning in OpenResty - [#414](https://github.com/lunarmodules/Penlight/pull/414) - - feat: `utils.enum` now accepts hash tables, to enable better error handling - [#413](https://github.com/lunarmodules/Penlight/pull/413) - - feat: `utils.kpairs` new iterator over all non-integer keys - [#413](https://github.com/lunarmodules/Penlight/pull/413) - - fix: `warn` use rawget to not trigger strict-checkers - [#437](https://github.com/lunarmodules/Penlight/pull/437) - - fix: `lapp` provides the file name when using the default argument - [#427](https://github.com/lunarmodules/Penlight/pull/427) - - fix: `lapp` positional arguments now allow digits after the first character - [#428](https://github.com/lunarmodules/Penlight/pull/428) - - fix: `path.isdir` windows root directories (including drive letter) were not considered valid - [#436](https://github.com/lunarmodules/Penlight/pull/436) - - -## 1.12.0 (2022-Jan-10) - - deprecate: module `pl.text` the contents have moved to `pl.stringx` (removal later) - [#407](https://github.com/lunarmodules/Penlight/pull/407) - - deprecate: module `pl.xml`, please switch to a more specialized library (removal later) - [#409](https://github.com/lunarmodules/Penlight/pull/409) - - feat: `utils.npairs` added. An iterator with a range that honours the `n` field - [#387](https://github.com/lunarmodules/Penlight/pull/387) - - fix: `xml.maptags` would hang if it encountered text-nodes - [#396](https://github.com/lunarmodules/Penlight/pull/396) - - fix: `text.dedent` didn't handle declining indents nor empty lines - [#402](https://github.com/lunarmodules/Penlight/pull/402) - - fix: `dir.getfiles`, `dir.getdirectories`, and `dir.getallfiles` now have the - directory optional, as was already documented - [#405](https://github.com/lunarmodules/Penlight/pull/405) - - feat: `array2d.default_range` now also takes a spreadsheet range, which means - also other functions now take a range. [#404](https://github.com/lunarmodules/Penlight/pull/404) - - fix: `lapp` enums allow [patterns magic characters](https://www.lua.org/pil/20.2.html) - [#393](https://github.com/lunarmodules/Penlight/pull/393) - - fix: `text.wrap` and `text.fill` numerous fixes for handling whitespace, - accented characters, honouring width, etc. - [#400](https://github.com/lunarmodules/Penlight/pull/400) - - feat: `text.wrap` and `text.fill` have a new parameter to forcefully break words - longer than the width given. - [#400](https://github.com/lunarmodules/Penlight/pull/400) - - fix: `stringx.expandtabs` could error out on Lua 5.3+ - [#406](https://github.com/lunarmodules/Penlight/pull/406) - - fix: `pl` the module would not properly forward the `newindex` metamethod - on the global table. - [#395](https://github.com/lunarmodules/Penlight/pull/395) - - feat: `utils.enum` added to create enums and prevent magic strings - [#408](https://github.com/lunarmodules/Penlight/pull/408) - - change: `xml.new` added some sanity checks on input - [#397](https://github.com/lunarmodules/Penlight/pull/397) - - added: `xml.xml_escape` and `xml.xml_unescape` functions (previously private) - [#397](https://github.com/lunarmodules/Penlight/pull/397) - - feat: `xml.tostring` now also takes numeric indents (previously only strings) - [#397](https://github.com/lunarmodules/Penlight/pull/397) - - fix: `xml.walk` now detects recursion (errors out) - [#397](https://github.com/lunarmodules/Penlight/pull/397) - - fix: `xml.clone` now detects recursion (errors out) - [#397](https://github.com/lunarmodules/Penlight/pull/397) - - fix: `xml.compare` now detects recursion (errors out) - [#397](https://github.com/lunarmodules/Penlight/pull/397) - - fix: `xml.compare` text compares now work - [#397](https://github.com/lunarmodules/Penlight/pull/397) - - fix: `xml.compare` attribute order compares now only compare if both inputs provide an order - [#397](https://github.com/lunarmodules/Penlight/pull/397) - - fix: `xml.compare` child comparisons failing now report proper error - [#397](https://github.com/lunarmodules/Penlight/pull/397) - - -## 1.11.0 (2021-Aug-18) - - - fix: `stringx.strip` behaved badly with string lengths > 200 - [#382](https://github.com/lunarmodules/Penlight/pull/382) - - fix: `path.currentdir` now takes no arguments and calls `lfs.currentdir` without argument - [#383](https://github.com/lunarmodules/Penlight/pull/383) - - feat: `utils.raise_deprecation` now has an option to NOT include a - stack-trace [#385](https://github.com/lunarmodules/Penlight/pull/385) - - -## 1.10.0 (2021-Apr-27) - - - deprecate: `permute.iter`, renamed to `permute.order_iter` (removal later) - [#360](https://github.com/lunarmodules/Penlight/pull/360) - - deprecate: `permute.table`, renamed to `permute.order_table` (removal later) - [#360](https://github.com/lunarmodules/Penlight/pull/360) - - deprecate: `Date` module (removal later) - [#367](https://github.com/lunarmodules/Penlight/pull/367) - - feat: `permute.list_iter` to iterate over different sets of values - [#360](https://github.com/lunarmodules/Penlight/pull/360) - - feat: `permute.list_table` generate table with different sets of values - [#360](https://github.com/lunarmodules/Penlight/pull/360) - - feat: Lua 5.4 'warn' compatibility function - [#366](https://github.com/lunarmodules/Penlight/pull/366) - - feat: deprecation functionality `utils.raise_deprecation` - [#361](https://github.com/lunarmodules/Penlight/pull/361) - - feat: `utils.splitv` now takes same args as `split` - [#373](https://github.com/lunarmodules/Penlight/pull/373) - - fix: `dir.rmtree` failed to remove symlinks to directories - [#365](https://github.com/lunarmodules/Penlight/pull/365) - - fix: `pretty.write` could error out on failing metamethods (Lua 5.3+) - [#368](https://github.com/lunarmodules/Penlight/pull/368) - - fix: `app.parse` now correctly parses values containing '=' or ':' - [#373](https://github.com/lunarmodules/Penlight/pull/373) - - fix: `dir.makepath` failed to create top-level directories - [#372](https://github.com/lunarmodules/Penlight/pull/372) - - overhaul: `array2d` module was updated, got additional tests and several - documentation updates - [#377](https://github.com/lunarmodules/Penlight/pull/377) - - feat: `aray2d` now accepts negative indices - - feat: `array2d.row` added to align with `column` - - fix: bad error message in `array2d.map` - - fix: `array2d.flatten` now ensures to deliver a 'square' result if `nil` is - encountered - - feat: `array2d.transpose` added - - feat: `array2d.swap_rows` and `array2d.swap_cols` now return the array - - fix: `aray2d.range` correctly recognizes `R` column in spreadsheet format, was - mistaken for `R1C1` format. - - fix: `aray2d.range` correctly recognizes 2 char column in spreadsheet format - - feat: `array2d.default_range` added (previously private) - - feat: `array2d.set` if used with a function now passes `i,j` to the function - in line with the `new` implementation. - - fix: `array2d.iter` didn't properly iterate the indices - [#376](https://github.com/lunarmodules/Penlight/issues/376) - - feat: `array2d.columns` now returns a second value; the column index - - feat: `array2d.rows` added to be in line with `columns` - - -## 1.9.2 (2020-Sep-27) - - - fix: dir.walk [#350](https://github.com/lunarmodules/Penlight/pull/350) - - -## 1.9.1 (2020-Sep-24) - - - released to superseed the 1.9.0 version which was retagged in git after some - distro's already had picked it up. This version is identical to 1.8.1. - -## 1.8.1 (2020-Sep-24) (replacing a briefly released but broken 1.9.0 version) - -## Fixes - - - In `pl.class`, `_init` can now be inherited from grandparent (or older ancestor) classes. [#289](https://github.com/lunarmodules/Penlight/pull/289) - - Fixes `dir`, `lexer`, and `permute` to no longer use coroutines. [#344](https://github.com/lunarmodules/Penlight/pull/344) - -## 1.8.0 (2020-Aug-05) - -### New features - - - `pretty.debug` quickly dumps a set of values to stdout for debug purposes - -### Changes - - - `pretty.write`: now also sorts non-string keys [#319](https://github.com/lunarmodules/Penlight/pull/319) - - `stringx.count` has an extra option to allow overlapping matches - [#326](https://github.com/lunarmodules/Penlight/pull/326) - - added an extra changelog entry for `types.is_empty` on the 1.6.0 changelog, due - to additional fixed behaviour not called out appropriately [#313](https://github.com/lunarmodules/Penlight/pull/313) - - `path.packagepath` now returns a proper error message with names tried if - it fails - -### Fixes - - - Fix: `stringx.rfind` now properly works with overlapping matches - [#314](https://github.com/lunarmodules/Penlight/pull/314) - - Fix: `package.searchpath` (in module `pl.compat`) - [#328](https://github.com/lunarmodules/Penlight/pull/328) - - Fix: `path.isabs` now reports drive + relative-path as `false`, eg. "c:some/path" (Windows only) - - Fix: OpenResty coroutines, used by `dir.dirtree`, `pl.lexer`, `pl.permute`. If - available the original coroutine functions are now used [#329](https://github.com/lunarmodules/Penlight/pull/329) - - Fix: in `pl.strict` also predefine global `_PROMPT2` - - Fix: in `pl.strict` apply `tostring` to the given name, in case it is not a string. - - Fix: the lexer would not recognize numbers without leading zero; "-.123". - See [#315](https://github.com/lunarmodules/Penlight/issues/315) - -## 1.7.0 (2019-Oct-14) - -### 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 - - - Documentation updates - - `utils.quit`: exit message is no longer required, and closes the Lua state (on 5.2+). - - `utils.assert_arg` and `utils.assert_string`: now return the validated value - - `pl.compat`: now exports the `jit` and `jit52` flags - - `pretty.write`: now sorts the output for easier diffs [#293](https://github.com/lunarmodules/Penlight/pull/293) - -### Fixes - - - `utils.raise` changed the global `on_error`-level when passing in bad arguments - - `utils.writefile` now checks and returns errors when writing - - `compat.execute` now handles the Windows exitcode -1 properly - - `types.is_empty` would return true on spaces always, independent 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 - - `stringx.split` will no longer append an empty match if the number of requested - elements has already been reached [#295](https://github.com/lunarmodules/Penlight/pull/295) - - `path.common_prefix` and `path.relpath` return the result in the original casing - (only impacted Windows) [#297](https://github.com/lunarmodules/Penlight/pull/297) - - `dir.copyfile`, `dir.movefile`, and `dir.makepath` create the new file/path with - the requested casing, and no longer force lowercase (only impacted Windows) - [#297](https://github.com/lunarmodules/Penlight/pull/297) - - added a missing assertion on `path.getmtime` [#291](https://github.com/lunarmodules/Penlight/pull/291) - - `stringx.rpartition` returned bad results on a not-found [#299](https://github.com/lunarmodules/Penlight/pull/299) - -## 1.6.0 (2018-Nov-23) - -### New features - - - `pl.compat` now provides `unpack` as `table.unpack` on Lua 5.1 - -### Changes - - - `utils.unpack` is now documented and respects `.n` field of its argument. - - `tablex.deepcopy` and `tablex.deepcompare` are now cycle aware (#262) - - Installing through LuaRocks will now include the full rendered documentation - -### Fixes - - - Fixed `seq.last` returning `nil` instead of an empty list when given an empty iterator (#253). - - `pl.template` now applies `tostring` when substituting values in templates, avoiding errors when they are not strings or numbers (#256). - - Fixed `pl.import_into` not importing some Penlight modules (#268). - - Fixed version number stuck at 1.5.2 (#260). - - Fixed `types.is_empty` returning `true` on tables containing `false` key (#267). - - Fixed `types.is_empty` returning `false` if not a nil/table/string - - Fixed `test.assertraise` throwing an error when passed an array with a function to call plus its arguments (#272). - - Fixed `test.assertraise` not throwing an error when given function does not error but instead returns a string matching given error pattern. - - Fixed placeholder expressions being evaluated with wrong precedence of binary and unary negation. - - Fixed placeholder expressions being evaluated assuming wrong binary operator associativity (e.g. `_1-(_2+_3)` was evaluated as `(_1-_2)+_3`. - - Fixed placeholder expressions being evaluated as if unary operators take precedence over power operator (e.g. `(-_1)^_2`) was evaluated as `-(_1^2)`). - - Fixed vulnerable backtracking pattern in `pl.stringx.strip` (#275) - -## 1.5.4 (2017-07-17) - -### Fixes - - - Fixed `compat.execute` behaving differently on Lua 5.1 and 5.1+. - - Fixed `lapp.process_options_string` setting global `success` variable. - -## 1.5.3 (2017-07-16) - -### Changes - - - Added `template.compile` function that allows caching compiled template and rendering it multiple times. - - Added special `_debug` field to environment table argument in `template.substitute` for printing generated template code upon render error. - -### Fixes - - - Fixed error (`attempt to concatenate a nil value (local 'vtype')`) in `lapp.process_options_string`. - -## 1.5.2 (2017-04-08) - -### Fixes - - - Removed leftover debug pring in `lapp.process_options_string`. - -## 1.5.1 (2017-04-02) - -### Fixes - - - Fixed `dir.getfiles` matching given pattern against full paths from base directory instead of file names. - -## 1.5.0 (2017-04-01) - -### Changes - - - `stringx.splitlines` considers `\r\n` a single line ending. - - `stringx.splitlines` returns an empty list for an empty string. - -### Fixes - - - `tablex.count_map` no longer raises an error. - - `strict.module` correctly handles existing `__index` metamethod returning `false`. - - `app.parse_args` accepts colon as a separator between option name and value, as advertised. - - `pretty.load` handles case where a C hook is present. - ' `os.execute` had issue with LuaJIT in 5.2 compat mode. - -### Features - - - `template` supports customizing inline escape character and chunk name. - - `seq` constructor supports iterators with a state object as the second argument. - - `stringx.splitlines` has `keep_ends` argument. - - `tablex.reduce` can take an optional initial value. - -## 1.4.1 (2016-08-16) - -### Changes - - - All functions that return instances of `pl.List`, `pl.Map` and `pl.Set` now require corresponding modules, - so that their methods always work right away. - -### Fixes - - - Fixed `dir.getallfiles` returning an empty array when called without `pattern` argument. - -### Features - -## 1.4.0 (2016-08-14) - -### Changes - -### Fixes - - - `pl.path` covers edge cases better (e.g `path.normpath` was broken) - - `p.dir` shell patterns fixed - - `os.tmpname` broken on modern Windows/MSVC14 - - (likewise for `utils.executeex` which depends on it) - - `pretty.write` more robust and does not lose floating-point precision; - saves and restores debug hooks when loading. - - `pl.lexer` fixes: `cpp` lexer now filters space by default - - `tablex.sortv` no longer assumes that the values are all unique - - `stringx.center` is now consistent with Python; `stringx.rfind` and - `string.quote_string` fixed. - - `data.write` had a problem with default delimiter, properly returns error now. - - `pl.Set` `+` and `-` now have correct semantics - -### Features - - - `pl.tablex` has `union` and `merge` convenience functions - - `pl.lapp` understands '--' meaning end of parsed arguments - - `utils.quote_arg` quotes command arguments for `os.execute`, - correctly handling all special characters. - - `utils.writefile` has optional `is_bin` argument - - 'pl.lexer' supports line numbers with string argument - - `stringx.endswith` may be passed an array of possible suffixes. - - `data.read` - in CSV mode, assume empty fields are numerical zero - - -## 1.3.2 (2015-05-10) - -### Changes - - - now works and passes tests with Lua 5.3 - - utils.import will NOT override global symbols (import 'math' caused global type() to be clobbered) - - Updated pl.dir.file_op to return true on success and false on failure... - - workaround for issues with pl.lapp with amalg.lua - will look at global LAPP_SCRIPT if arg[0] is nil - -### Fixes - - - func was broken: do NOT use ipairs to iterate if __index is overriden! - - issue #133 pretty.read (naively) confused by unbalanced brackets - - xml attribute underscore fix for simple parser - - Fix path.normpath - - lexer: fix parsing block comments/string. fix hang on empty string. - - Fixed utils.execute returning different values for Lua 5.1 and Lua 5.2 - - Issue #97; fixed attempt to put a month into a day - - problem with tablex.count_map with custom comparison - - tablex.pairmap overwrites result if key already exists; instead, upon detection that key already exists - for a returned value, we modify the key's value to be a table and insert values into that table - -### Features - - - Add Python style url module for quote and unquote. - - stringx.quote_string, which scans for embedded long-string quote matches and escapes them by creating a long-string quote. - - issue #117: tablex.range now works with decreasing numbers, consistent with numerical for loop - - utils.import will NOT override global symbols (import 'math' caused global type() to be clobbered) - - issue #125: DOCTYPE ignored in xml documents as well - - Allow XML tostring() function to customize the default prefacing with `` - - More Robust Quoted Strings - - lapp: improved detection of unsupported short flags - -## 1.3.1 (2013-09-24) - -## 1.3.0 (2013-09-14) - -### Changes - - - class: RIP base method - not possible to implement correctly - - lapp: short flags can now always be followed directly by their value, for instance, -`-I/usr/include/lua/5.1` - - Date: new explicit `Date.Interval` class; `toUTC/toLocal` return new object; `Date.__tostring` -always returns ISO 8601 times for exact serialization. `+/-` explicit operators. Date objects -are explicitly flagged as being UTC or not. - -### Fixes - - - class: super method fixed. - - Date: DST is now accounted for properly. - - Date: weekday calculation borked. - -### Features - - - All tests pass with no-5.1-compatible Lua 5.2; now always uses `utils.load` and -`utils.unpack` is always available. - - types: new module containing `utils.is_xxx` methods plus new `to_bool`. - - class: can be passed methods in a table (see `test=klass.lua`). This is -particularly convenient for using from Moonscript. - - general documentation improvements, e.g `class` - -## 1.2.1 (2013-06-21) - -### Changes - - - utils.set(get)fenv always defined (_not_ set as globals for 5.2 anymore!). - These are defined in new module pl.compat, but still available through utils. - - class.Frodo now puts 'Frodo' in _current environment_ - -### Fixes - - - lapp.add_type was broken (Pete Kazmier) - - class broke with classes that redefined __newindex - - Set.isdisjoint was broken because of misspelling; default ctor Set() now works as expected - - tablex.transform was broken; result now has same keys as original (CoolistheName007) - - xml match not handling empty matches (royalbee) - - pl.strict: assigning nil to global declares it, as God intended. (Pierre Chapuis) - - tests all work with pl.strict - - 5.2 compatible load now respects mode - - tablex.difference thought that a value of `false` meant 'not present' (Andrew Starke) - -### Features - - - tablex.sort(t) iterates over sorted keys, tablex.sortv(t) iterates over sorted values (Pete Kazmier) - - tablex.readonly(t) creates a read-only proxy for a table (John Schember) - - utils.is_empty(o) true if o==nil, o is an empty table, or o is an empty string (John Schember) - - utils.executeex(cmd,bin) returns true if successful, return code, plus stdout and stderr output as strings. (tieske) - - class method base for calling inherited methods (theypsilon) - - class supports pre-constructor _create for making a custom self (used in pl.List) - - xml HTML mode improvements - can parse non-trivial well-formed HTML documents. - xml.parsehtml is a parse function, no longer a flag - - if a LOM document has ordered attributes, use these when stringifying - - xml.tostring has yet another extra parm to force prefacing with `` - - lapp boolean flags may have `true` default - - lapp slack mode where 'short' flags can be multi-char - - test.asserteq etc take extra arg, which is extra level where error must be reported at - - path.currentdir,chdir,rmdir,mkdir and dir as alias to lfs are exported; no dependencies on luafilesystem outside pl.path, making it easier to plug in different implementations. - -## 1.2.0 (2013-05-28) - -## 1.1.1 (2013-05-14) - -## 1.1.0 (2013-03-18) - -## 1.0.3 (2012-12-07) - -## 1.0.2 (2012-05-12) - -## 1.0.1 (2012-05-26) - -## 1.0.0 (2012-04-26) - -## 0.9.8 (2011-11-27) - -## 0.9.7 (2011-11-27) - -### Lua 5.2 compatibility - -(These are all now defined in pl.utils) - -- setfenv, getfenv defined for Lua 5.2 (by Sergey Rozhenko) - -### Changes - -- array2d.flatten is new -- OrderedMap:insert is new - -### Fixes - -- seq.reduce re-implemented to give correct order (Carl Ådahl) -- seq.unique was broken: new test -- tablex.icopy broken for last argument; new test -- utils.function_arg last parm 'msg' was missing -- array2d.product was broken; more sensible implementation -- array2d.range, .slice, .write were broken -- text optional operator % overload broken for 'fmt % fun'; new tests -- a few occurances of non-existent function utils.error removed - - -## 0.9.6 (2011-09-11) - -### Lua 5.2 compatibility - -- Bad string escape in tests fixed - -### Changes - -- LuaJIT FFI used on Windows for Copy/MoveFile functionality - -### Fixes - -- Issue 13 seq.sort now calls seq.copy -- issue 14 bad pattern to escape trailing separators in path.abspath -- lexer: string tokens broken with some combinations -- lexer: long comments broken for Lua and C -- stringx.split behaves according to Python spec; extra parm meaning 'max splits' -- stringx.title behaves according to Python spec -- stringx.endswith broken for 2nd arg being table of postfixes -- OrderedMap.set broken when value was nil and key did not exist in map; ctor throws - error if unhappy - -## 0.9.5 (2011-07-05) - -### Lua 5.2 compatibility - - - defines Lua 5.2 beta compatible load() - - defines table.pack() - -### New functions - - - stringx.title(): translates "a dog's day" to "A Dog's Day" - - path.normpath(): translates 'A//B','A/./B' and 'A/C/../B' to 'A/B' - - utils.execute(): returns ok,return-code: compatible with 5.1 and 5.2 - -### Fixes - - - pretty.write() _always_ returns a string, but will return also an error string -if the argument is not a table. Non-integer indices between 1 and #t are no longer falsely considered part of the array - - stringx.expandtabs() now works like the Python string method; it will expand each field up to the next tab stop - - path.normcase() was broken, because of a misguided attempt to normalize the path. - - UNC specific fix to path.abspath() - - UNC paths recognized as absolute; dir.makedir() works here - - utils.quit() varargs broken, e.g. utils.quit("answer was %d",42) - - some stray globals caused trouble with 'strict' - -## 0.9.4 (2011-04-08) - -## 0.9.3 (2011-03-05) - -## 0.9.2 (2011-02-16) - -## 0.9.1 (2011-02-12) - -## 0.9.0 (2010-12-20) - -## 0.8.5 (2010-12-16) - -### What's new with 0.8b ? - -#### Features: - -pl.app provides useful stuff like simple command-line argument parsing and require_here(), which -makes subsequent require() calls look in the local directory by preference. - -p.file provides useful functions like copy(),move(), read() and write(). (These are aliases to -dir.copyfile(),movefile(),utils.readfile(),writefile()) - -Custom error trace will only show the functions in user code. - -More robust argument checking. - -In function arguments, now supports 'string lambdas', e.g. `'|x| 2*x'` - -utils.readfile,writefile now insist on being given filenames. This will cause less confusion. - -tablex.search() is new: will look recursively in an arbitrary table; can specify tables not to follow. -tablex.move() will work with source and destination tables the same, with overlapping ranges. - -#### Bug Fixes: - -dir.copyfile() now works fine without Alien on Windows - -dir.makepath() and rmtree() had problems. - -tablex.compare_no_order() is now O(NlogN), as expected. -tablex.move() had a problem with source size - -### What's New with 0.7.0b? - -#### Features: - -utils.is_type(v,tp) can say is_type(s,'string') and is_type(l,List). -utils.is_callable(v) either a function, or has a `__call` metamethod. - -Sequence wrappers: can write things like this: - -seq(s):last():filter('<'):copy() - -seq:mapmethod(s,name) - map using a named method over a sequence. - -seq:enum(s) If s is a simple sequence, then - for i,v in seq.enum(s) do print(i,v) end - -seq:take(s,n) Grab the next n values from a (possibly infinite) -sequence. - -In a related change suggested by Flemming Madsden, the in-place List -methods like reverse() and sort() return the list, allowing for -method chaining. - -list.join() explicitly converts using tostring first. - -tablex.count_map() like seq.count_map(), but takes an equality function. - -tablex.difference() set difference -tablex.set() explicit set generator given a list of values - -Template.indent_substitute() is a new Template method which adjusts -for indentation and can also substitute templates themselves. - -pretty.read(). This reads a Lua table (as dumped by pretty.write) -and attempts to be paranoid about its contents. - -sip.match_at_start(). Convenience function for anchored SIP matches. - -#### Bug Fixes: - -tablex.deepcompare() was confused by false boolean values, which -it thought were synonymous with being nil. - -pretty.write() did not handle cycles, and could not display tables -with 'holes' properly (Flemming Madsden) - -The SIP pattern '$(' was not escaped properly. -sip.match() did not pass on options table. - -seq.map() was broken for double-valued sequences. -seq.copy_tuples() did not use default_iter(), so did not e.g. like -table arguments. - -dir.copyfile() returns the wrong result for \*nix operations. -dir.makepath() was broken for non-Windows paths. - -### What's New with 0.6.3? - -The map and reduce functions now take the function first, as Nature intended. - -The Python-like overloading of '\*' for strings has been dropped, since it -is silly. Also, strings are no longer callable; use 's:at(1)' instead of -'s(1)' - this tended to cause Obscure Error messages. - -Wherever a function argument is expected, you can use the operator strings -like '+','==',etc as well as pl.operator.add, pl.operator.eq, etc. -(see end of pl/operator.lua for the full list.) - -tablex now has compare() and compare_no_order(). An explicit set() -function has been added which constructs a table with the specified -keys, all set to a value of true. - -List has reduce() and partition() (This is a cool function which -separates out elements of a list depending on a classifier function.) - -There is a new array module which generalizes tablex operations like -map and reduce for two-dimensional arrays. - -The famous iterator over permutations from PiL 9.3 has been included. - -David Manura's list comprehension library has been included. - -Also, utils now contains his memoize function, plus a useful function -args which captures the case where varargs contains nils. - -There was a bug with dir.copyfile() where the flag was the wrong way round. - -config.lines() had a problem with continued lines. - -Some operators were missing in pl.operator; have renamed them to be -consistent with the Lua metamethod names. - - diff --git a/src/pl/CONTRIBUTING.md b/src/pl/CONTRIBUTING.md deleted file mode 100644 index 74cde11..0000000 --- a/src/pl/CONTRIBUTING.md +++ /dev/null @@ -1,60 +0,0 @@ -Contributing to Penlight -======================== - -So you want to contribute to Penlight? Fantastic! Here's a brief overview on -how best to do so. - -## What to change - -Here's some examples of things you might want to make a pull request for: - -* New features -* Bugfixes -* Inefficient blocks of code - -If you have a more deeply-rooted problem with how the library is built or some -of the stylistic decisions made in the code, it's best to -[create an issue](https://github.com/lunarmodules/Penlight/issues) before putting -the effort into a pull request. The same goes for new features - it might be -best to check the project's direction, existing pull requests, and currently open -and closed issues first. - -## Using Git appropriately - -Here's how to go about contributing to Penlight: - -1. [Fork the repository](https://github.com/lunarmodules/Penlight/fork) to -your Github account. -2. Create a *topical branch* - a branch whose name is succint but explains what -you're doing, such as _"added-klingon-cloacking-device"_ - from `master` branch. -3. Make your changes, committing at logical breaks. -4. Push your branch to your personal account -5. [Create a pull request](https://help.github.com/articles/using-pull-requests) -6. Watch for comments or acceptance - -If you wanna be a rockstar; - -1. Update the [CHANGELOG.md](https://github.com/lunarmodules/Penlight/blob/master/CHANGELOG.md) file -2. [Add tests](https://github.com/lunarmodules/Penlight/tree/master/tests) that show the defect your fix repairs, or that tests your new feature - -Please note - if you want to change multiple things that don't depend on each -other, make sure you check out the `master` branch again and create a different topical branch -before making more changes - that way we can take in each change separately. - -## Release instructions for a new version - - - create a new release branch - - update `./lua/pl/utils.lua` (the `_VERSION` constant) - - update `./config.ld` with the new version number - - create a new rockspec file for the version in `./rockspecs` - - check the `./CHANGELOG.md` files for completeness - - commit the release related changes with `release x.y.z` - - render the documentation using `ldoc .` - - commit the documentation as a separate commit with `release x.y.z docs` - - push the release branch and create a PR - - merge the PR - - tag the release as `x.y.z` and push the tag to the github repo - - upload the rockspec, and source rock files to LuaRocks - - test installing through LuaRocks - - announce the release on the Lua mailing list - diff --git a/src/pl/Date.lua b/src/pl/Date.lua deleted file mode 100644 index 039dbaf..0000000 --- a/src/pl/Date.lua +++ /dev/null @@ -1,675 +0,0 @@ ---- Date and Date Format classes. --- See @{05-dates.md|the Guide}. --- --- NOTE: the date module is deprecated! see --- https://github.com/lunarmodules/Penlight/issues/285 --- --- Dependencies: `pl.class`, `pl.stringx`, `pl.utils` --- @classmod pl.Date --- @pragma nostrip - -local class = require 'pl.class' -local os_time, os_date = os.time, os.date -local stringx = require 'pl.stringx' -local utils = require 'pl.utils' -local assert_arg,assert_string = utils.assert_arg,utils.assert_string - - -utils.raise_deprecation { - source = "Penlight " .. utils._VERSION, - message = "the 'Date' module is deprecated, see https://github.com/lunarmodules/Penlight/issues/285", - version_removed = "2.0.0", - version_deprecated = "1.9.2", -} - - -local Date = class() -Date.Format = class() - ---- Date constructor. --- @param t this can be either --- --- * `nil` or empty - use current date and time --- * number - seconds since epoch (as returned by `os.time`). Resulting time is UTC --- * `Date` - make a copy of this date --- * table - table containing year, month, etc as for `os.time`. You may leave out year, month or day, --- in which case current values will be used. --- * year (will be followed by month, day etc) --- --- @param ... true if Universal Coordinated Time, or two to five numbers: month,day,hour,min,sec --- @function Date -function Date:_init(t,...) - local time - local nargs = select('#',...) - if nargs > 2 then - local extra = {...} - local year = t - t = { - year = year, - month = extra[1], - day = extra[2], - hour = extra[3], - min = extra[4], - sec = extra[5] - } - end - if nargs == 1 then - self.utc = select(1,...) == true - end - if t == nil or t == 'utc' then - time = os_time() - self.utc = t == 'utc' - elseif type(t) == 'number' then - time = t - if self.utc == nil then self.utc = true end - elseif type(t) == 'table' then - if getmetatable(t) == Date then -- copy ctor - time = t.time - self.utc = t.utc - else - if not (t.year and t.month) then - local lt = os_date('*t') - if not t.year and not t.month and not t.day then - t.year = lt.year - t.month = lt.month - t.day = lt.day - else - t.year = t.year or lt.year - t.month = t.month or (t.day and lt.month or 1) - t.day = t.day or 1 - end - end - t.day = t.day or 1 - time = os_time(t) - end - else - error("bad type for Date constructor: "..type(t),2) - end - self:set(time) -end - ---- set the current time of this Date object. --- @int t seconds since epoch -function Date:set(t) - self.time = t - if self.utc then - self.tab = os_date('!*t',t) - else - self.tab = os_date('*t',t) - end -end - ---- get the time zone offset from UTC. --- @int ts seconds ahead of UTC -function Date.tzone (ts) - if ts == nil then - ts = os_time() - elseif type(ts) == "table" then - if getmetatable(ts) == Date then - ts = ts.time - else - ts = Date(ts).time - end - end - local utc = os_date('!*t',ts) - local lcl = os_date('*t',ts) - lcl.isdst = false - return os.difftime(os_time(lcl), os_time(utc)) -end - ---- convert this date to UTC. -function Date:toUTC () - local ndate = Date(self) - if not self.utc then - ndate.utc = true - ndate:set(ndate.time) - end - return ndate -end - ---- convert this UTC date to local. -function Date:toLocal () - local ndate = Date(self) - if self.utc then - ndate.utc = false - ndate:set(ndate.time) ---~ ndate:add { sec = Date.tzone(self) } - end - return ndate -end - ---- set the year. --- @int y Four-digit year --- @class function --- @name Date:year - ---- set the month. --- @int m month --- @class function --- @name Date:month - ---- set the day. --- @int d day --- @class function --- @name Date:day - ---- set the hour. --- @int h hour --- @class function --- @name Date:hour - ---- set the minutes. --- @int min minutes --- @class function --- @name Date:min - ---- set the seconds. --- @int sec seconds --- @class function --- @name Date:sec - ---- set the day of year. --- @class function --- @int yday day of year --- @name Date:yday - ---- get the year. --- @int y Four-digit year --- @class function --- @name Date:year - ---- get the month. --- @class function --- @name Date:month - ---- get the day. --- @class function --- @name Date:day - ---- get the hour. --- @class function --- @name Date:hour - ---- get the minutes. --- @class function --- @name Date:min - ---- get the seconds. --- @class function --- @name Date:sec - ---- get the day of year. --- @class function --- @name Date:yday - - -for _,c in ipairs{'year','month','day','hour','min','sec','yday'} do - Date[c] = function(self,val) - if val then - assert_arg(1,val,"number") - self.tab[c] = val - self:set(os_time(self.tab)) - return self - else - return self.tab[c] - end - end -end - ---- name of day of week. --- @bool full abbreviated if true, full otherwise. --- @ret string name -function Date:weekday_name(full) - return os_date(full and '%A' or '%a',self.time) -end - ---- name of month. --- @int full abbreviated if true, full otherwise. --- @ret string name -function Date:month_name(full) - return os_date(full and '%B' or '%b',self.time) -end - ---- is this day on a weekend?. -function Date:is_weekend() - return self.tab.wday == 1 or self.tab.wday == 7 -end - ---- add to a date object. --- @param t a table containing one of the following keys and a value: --- one of `year`,`month`,`day`,`hour`,`min`,`sec` --- @return this date -function Date:add(t) - local old_dst = self.tab.isdst - local key,val = next(t) - self.tab[key] = self.tab[key] + val - self:set(os_time(self.tab)) - if old_dst ~= self.tab.isdst then - self.tab.hour = self.tab.hour - (old_dst and 1 or -1) - self:set(os_time(self.tab)) - end - return self -end - ---- last day of the month. --- @return int day -function Date:last_day() - local d = 28 - local m = self.tab.month - while self.tab.month == m do - d = d + 1 - self:add{day=1} - end - self:add{day=-1} - return self -end - ---- difference between two Date objects. --- @tparam Date other Date object --- @treturn Date.Interval object -function Date:diff(other) - local dt = self.time - other.time - if dt < 0 then error("date difference is negative!",2) end - return Date.Interval(dt) -end - ---- long numerical ISO data format version of this date. -function Date:__tostring() - local fmt = '%Y-%m-%dT%H:%M:%S' - if self.utc then - fmt = "!"..fmt - end - local t = os_date(fmt,self.time) - if self.utc then - return t .. 'Z' - else - local offs = self:tzone() - if offs == 0 then - return t .. 'Z' - end - local sign = offs > 0 and '+' or '-' - local h = math.ceil(offs/3600) - local m = (offs % 3600)/60 - if m == 0 then - return t .. ('%s%02d'):format(sign,h) - else - return t .. ('%s%02d:%02d'):format(sign,h,m) - end - end -end - ---- equality between Date objects. -function Date:__eq(other) - return self.time == other.time -end - ---- ordering between Date objects. -function Date:__lt(other) - return self.time < other.time -end - ---- difference between Date objects. --- @function Date:__sub -Date.__sub = Date.diff - ---- add a date and an interval. --- @param other either a `Date.Interval` object or a table such as --- passed to `Date:add` -function Date:__add(other) - local nd = Date(self) - if Date.Interval:class_of(other) then - other = {sec=other.time} - end - nd:add(other) - return nd -end - -Date.Interval = class(Date) - ----- Date.Interval constructor --- @int t an interval in seconds --- @function Date.Interval -function Date.Interval:_init(t) - self:set(t) -end - -function Date.Interval:set(t) - self.time = t - self.tab = os_date('!*t',self.time) -end - -local function ess(n) - if n > 1 then return 's ' - else return ' ' - end -end - ---- If it's an interval then the format is '2 hours 29 sec' etc. -function Date.Interval:__tostring() - local t, res = self.tab, '' - local y,m,d = t.year - 1970, t.month - 1, t.day - 1 - if y > 0 then res = res .. y .. ' year'..ess(y) end - if m > 0 then res = res .. m .. ' month'..ess(m) end - if d > 0 then res = res .. d .. ' day'..ess(d) end - if y == 0 and m == 0 then - local h = t.hour - if h > 0 then res = res .. h .. ' hour'..ess(h) end - if t.min > 0 then res = res .. t.min .. ' min ' end - if t.sec > 0 then res = res .. t.sec .. ' sec ' end - end - if res == '' then res = 'zero' end - return res -end - ------------- Date.Format class: parsing and renderinig dates ------------ - --- short field names, explicit os.date names, and a mask for allowed field repeats -local formats = { - d = {'day',{true,true}}, - y = {'year',{false,true,false,true}}, - m = {'month',{true,true}}, - H = {'hour',{true,true}}, - M = {'min',{true,true}}, - S = {'sec',{true,true}}, -} - ---- Date.Format constructor. --- @string fmt. A string where the following fields are significant: --- --- * d day (either d or dd) --- * y year (either yy or yyy) --- * m month (either m or mm) --- * H hour (either H or HH) --- * M minute (either M or MM) --- * S second (either S or SS) --- --- Alternatively, if fmt is nil then this returns a flexible date parser --- that tries various date/time schemes in turn: --- --- * [ISO 8601](http://en.wikipedia.org/wiki/ISO_8601), like `2010-05-10 12:35:23Z` or `2008-10-03T14:30+02` --- * times like 15:30 or 8.05pm (assumed to be today's date) --- * dates like 28/10/02 (European order!) or 5 Feb 2012 --- * month name like march or Mar (case-insensitive, first 3 letters); here the --- day will be 1 and the year this current year --- --- A date in format 3 can be optionally followed by a time in format 2. --- Please see test-date.lua in the tests folder for more examples. --- @usage df = Date.Format("yyyy-mm-dd HH:MM:SS") --- @class function --- @name Date.Format -function Date.Format:_init(fmt) - if not fmt then - self.fmt = '%Y-%m-%d %H:%M:%S' - self.outf = self.fmt - self.plain = true - return - end - local append = table.insert - local D,PLUS,OPENP,CLOSEP = '\001','\002','\003','\004' - local vars,used = {},{} - local patt,outf = {},{} - local i = 1 - while i < #fmt do - local ch = fmt:sub(i,i) - local df = formats[ch] - if df then - if used[ch] then error("field appeared twice: "..ch,4) end - used[ch] = true - -- this field may be repeated - local _,inext = fmt:find(ch..'+',i+1) - local cnt = not _ and 1 or inext-i+1 - if not df[2][cnt] then error("wrong number of fields: "..ch,4) end - -- single chars mean 'accept more than one digit' - local p = cnt==1 and (D..PLUS) or (D):rep(cnt) - append(patt,OPENP..p..CLOSEP) - append(vars,ch) - if ch == 'y' then - append(outf,cnt==2 and '%y' or '%Y') - else - append(outf,'%'..ch) - end - i = i + cnt - else - append(patt,ch) - append(outf,ch) - i = i + 1 - end - end - -- escape any magic characters - fmt = utils.escape(table.concat(patt)) - -- fmt = table.concat(patt):gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1') - -- replace markers with their magic equivalents - fmt = fmt:gsub(D,'%%d'):gsub(PLUS,'+'):gsub(OPENP,'('):gsub(CLOSEP,')') - self.fmt = fmt - self.outf = table.concat(outf) - self.vars = vars -end - -local parse_date - ---- parse a string into a Date object. --- @string str a date string --- @return date object -function Date.Format:parse(str) - assert_string(1,str) - if self.plain then - return parse_date(str,self.us) - end - local res = {str:match(self.fmt)} - if #res==0 then return nil, 'cannot parse '..str end - local tab = {} - for i,v in ipairs(self.vars) do - local name = formats[v][1] -- e.g. 'y' becomes 'year' - tab[name] = tonumber(res[i]) - end - -- os.date() requires these fields; if not present, we assume - -- that the time set is for the current day. - if not (tab.year and tab.month and tab.day) then - local today = Date() - tab.year = tab.year or today:year() - tab.month = tab.month or today:month() - tab.day = tab.day or today:day() - end - local Y = tab.year - if Y < 100 then -- classic Y2K pivot - tab.year = Y + (Y < 35 and 2000 or 1999) - elseif not Y then - tab.year = 1970 - end - return Date(tab) -end - ---- convert a Date object into a string. --- @param d a date object, or a time value as returned by @{os.time} --- @return string -function Date.Format:tostring(d) - local tm - local fmt = self.outf - if type(d) == 'number' then - tm = d - else - tm = d.time - if d.utc then - fmt = '!'..fmt - end - end - return os_date(fmt,tm) -end - ---- force US order in dates like 9/11/2001 -function Date.Format:US_order(yesno) - self.us = yesno -end - ---local months = {jan=1,feb=2,mar=3,apr=4,may=5,jun=6,jul=7,aug=8,sep=9,oct=10,nov=11,dec=12} -local months -local parse_date_unsafe -local function create_months() - local ld, day1 = parse_date_unsafe '2000-12-31', {day=1} - months = {} - for i = 1,12 do - ld = ld:last_day() - ld:add(day1) - local mon = ld:month_name():lower() - months [mon] = i - end -end - ---[[ -Allowed patterns: -- [day] [monthname] [year] [time] -- [day]/[month][/year] [time] - -]] - -local function looks_like_a_month(w) - return w:match '^%a+,*$' ~= nil -end -local is_number = stringx.isdigit -local function tonum(s,l1,l2,kind) - kind = kind or '' - local n = tonumber(s) - if not n then error(("%snot a number: '%s'"):format(kind,s)) end - if n < l1 or n > l2 then - error(("%s out of range: %s is not between %d and %d"):format(kind,s,l1,l2)) - end - return n -end - -local function parse_iso_end(p,ns,sec) - -- may be fractional part of seconds - local _,nfrac,secfrac = p:find('^%.%d+',ns+1) - if secfrac then - sec = sec .. secfrac - p = p:sub(nfrac+1) - else - p = p:sub(ns+1) - end - -- ISO 8601 dates may end in Z (for UTC) or [+-][isotime] - -- (we're working with the date as lower case, hence 'z') - if p:match 'z$' then -- we're UTC! - return sec, {h=0,m=0} - end - p = p:gsub(':','') -- turn 00:30 to 0030 - local _,_,sign,offs = p:find('^([%+%-])(%d+)') - if not sign then return sec, nil end -- not UTC - - if #offs == 2 then offs = offs .. '00' end -- 01 to 0100 - local tz = { h = tonumber(offs:sub(1,2)), m = tonumber(offs:sub(3,4)) } - if sign == '-' then tz.h = -tz.h; tz.m = -tz.m end - return sec, tz -end - -function parse_date_unsafe (s,US) - s = s:gsub('T',' ') -- ISO 8601 - local parts = stringx.split(s:lower()) - local i,p = 1,parts[1] - local function nextp() i = i + 1; p = parts[i] end - local year,min,hour,sec,apm - local tz - local _,nxt,day, month = p:find '^(%d+)/(%d+)' - if day then - -- swop for US case - if US then - day, month = month, day - end - _,_,year = p:find('^/(%d+)',nxt+1) - nextp() - else -- ISO - year,month,day = p:match('^(%d+)%-(%d+)%-(%d+)') - if year then - nextp() - end - end - if p and not year and is_number(p) then -- has to be date - if #p < 4 then - day = p - nextp() - else -- unless it looks like a 24-hour time - year = true - end - end - if p and looks_like_a_month(p) then -- date followed by month - p = p:sub(1,3) - if not months then - create_months() - end - local mon = months[p] - if mon then - month = mon - else error("not a month: " .. p) end - nextp() - end - if p and not year and is_number(p) then - year = p - nextp() - end - - if p then -- time is hh:mm[:ss], hhmm[ss] or H.M[am|pm] - _,nxt,hour,min = p:find '^(%d+):(%d+)' - local ns - if nxt then -- are there seconds? - _,ns,sec = p:find ('^:(%d+)',nxt+1) - --if ns then - sec,tz = parse_iso_end(p,ns or nxt,sec) - --end - else -- might be h.m - _,ns,hour,min = p:find '^(%d+)%.(%d+)' - if ns then - apm = p:match '[ap]m$' - else -- or hhmm[ss] - local hourmin - _,nxt,hourmin = p:find ('^(%d+)') - if nxt then - hour = hourmin:sub(1,2) - min = hourmin:sub(3,4) - sec = hourmin:sub(5,6) - if #sec == 0 then sec = nil end - sec,tz = parse_iso_end(p,nxt,sec) - end - end - end - end - local today - if year == true then year = nil end - if not (year and month and day) then - today = Date() - end - day = day and tonum(day,1,31,'day') or (month and 1 or today:day()) - month = month and tonum(month,1,12,'month') or today:month() - year = year and tonumber(year) or today:year() - if year < 100 then -- two-digit year pivot around year < 2035 - year = year + (year < 35 and 2000 or 1900) - end - hour = hour and tonum(hour,0,apm and 12 or 24,'hour') or 12 - if apm == 'pm' then - hour = hour + 12 - end - min = min and tonum(min,0,59) or 0 - sec = sec and tonum(sec,0,60) or 0 --60 used to indicate leap second - local res = Date {year = year, month = month, day = day, hour = hour, min = min, sec = sec} - if tz then -- ISO 8601 UTC time - local corrected = false - if tz.h ~= 0 then res:add {hour = -tz.h}; corrected = true end - if tz.m ~= 0 then res:add {min = -tz.m}; corrected = true end - res.utc = true - -- we're in UTC, so let's go local... - if corrected then - res = res:toLocal() - end-- we're UTC! - end - return res -end - -function parse_date (s) - local ok, d = pcall(parse_date_unsafe,s) - if not ok then -- error - d = d:gsub('.-:%d+: ','') - return nil, d - else - return d - end -end - -return Date - diff --git a/src/pl/List.lua b/src/pl/List.lua deleted file mode 100644 index b66c251..0000000 --- a/src/pl/List.lua +++ /dev/null @@ -1,566 +0,0 @@ ---- Python-style list class. --- --- **Please Note**: methods that change the list will return the list. --- This is to allow for method chaining, but please note that `ls = ls:sort()` --- does not mean that a new copy of the list is made. In-place (mutable) methods --- are marked as returning 'the list' in this documentation. --- --- See the Guide for further @{02-arrays.md.Python_style_Lists|discussion} --- --- See http://www.python.org/doc/current/tut/tut.html, section 5.1 --- --- **Note**: The comments before some of the functions are from the Python docs --- and contain Python code. --- --- Written for Lua version Nick Trout 4.0; Redone for Lua 5.1, Steve Donovan. --- --- Dependencies: `pl.utils`, `pl.tablex`, `pl.class` --- @classmod pl.List --- @pragma nostrip - -local tinsert,tremove,concat,tsort = table.insert,table.remove,table.concat,table.sort -local setmetatable, getmetatable,type,tostring,string = setmetatable,getmetatable,type,tostring,string -local tablex = require 'pl.tablex' -local filter,imap,imap2,reduce,transform,tremovevalues = tablex.filter,tablex.imap,tablex.imap2,tablex.reduce,tablex.transform,tablex.removevalues -local tsub = tablex.sub -local utils = require 'pl.utils' -local class = require 'pl.class' - -local array_tostring,split,assert_arg,function_arg = utils.array_tostring,utils.split,utils.assert_arg,utils.function_arg -local normalize_slice = tablex._normalize_slice - --- metatable for our list and map objects has already been defined.. -local Multimap = utils.stdmt.MultiMap -local List = utils.stdmt.List - -local iter - -class(nil,nil,List) - --- we want the result to be _covariant_, i.e. t must have type of obj if possible -local function makelist (t,obj) - local klass = List - if obj then - klass = getmetatable(obj) - end - return setmetatable(t,klass) -end - -local function simple_table(t) - return type(t) == 'table' and not getmetatable(t) and #t > 0 -end - -function List._create (src) - if simple_table(src) then return src end -end - -function List:_init (src) - if self == src then return end -- existing table used as self! - if src then - for v in iter(src) do - tinsert(self,v) - end - end -end - ---- Create a new list. Can optionally pass a table; --- passing another instance of List will cause a copy to be created; --- this will return a plain table with an appropriate metatable. --- we pass anything which isn't a simple table to iterate() to work out --- an appropriate iterator --- @see List.iterate --- @param[opt] t An optional list-like table --- @return a new List --- @usage ls = List(); ls = List {1,2,3,4} --- @function List.new - -List.new = List - ---- Make a copy of an existing list. --- The difference from a plain 'copy constructor' is that this returns --- the actual List subtype. -function List:clone() - local ls = makelist({},self) - ls:extend(self) - return ls -end - ---- Add an item to the end of the list. --- @param i An item --- @return the list -function List:append(i) - tinsert(self,i) - return self -end - -List.push = tinsert - ---- Extend the list by appending all the items in the given list. --- equivalent to 'a[len(a):] = L'. --- @tparam List L Another List --- @return the list -function List:extend(L) - assert_arg(1,L,'table') - for i = 1,#L do tinsert(self,L[i]) end - return self -end - ---- Insert an item at a given position. i is the index of the --- element before which to insert. --- @int i index of element before whichh to insert --- @param x A data item --- @return the list -function List:insert(i, x) - assert_arg(1,i,'number') - tinsert(self,i,x) - return self -end - ---- Insert an item at the begining of the list. --- @param x a data item --- @return the list -function List:put (x) - return self:insert(1,x) -end - ---- Remove an element given its index. --- (equivalent of Python's del s[i]) --- @int i the index --- @return the list -function List:remove (i) - assert_arg(1,i,'number') - tremove(self,i) - return self -end - ---- Remove the first item from the list whose value is given. --- (This is called 'remove' in Python; renamed to avoid confusion --- with table.remove) --- Return nil if there is no such item. --- @param x A data value --- @return the list -function List:remove_value(x) - for i=1,#self do - if self[i]==x then tremove(self,i) return self end - end - return self - end - ---- Remove the item at the given position in the list, and return it. --- If no index is specified, a:pop() returns the last item in the list. --- The item is also removed from the list. --- @int[opt] i An index --- @return the item -function List:pop(i) - if not i then i = #self end - assert_arg(1,i,'number') - return tremove(self,i) -end - -List.get = List.pop - ---- Return the index in the list of the first item whose value is given. --- Return nil if there is no such item. --- @function List:index --- @param x A data value --- @int[opt=1] idx where to start search --- @return the index, or nil if not found. - -local tfind = tablex.find -List.index = tfind - ---- Does this list contain the value? --- @param x A data value --- @return true or false -function List:contains(x) - return tfind(self,x) and true or false -end - ---- Return the number of times value appears in the list. --- @param x A data value --- @return number of times x appears -function List:count(x) - local cnt=0 - for i=1,#self do - if self[i]==x then cnt=cnt+1 end - end - return cnt -end - ---- Sort the items of the list, in place. --- @func[opt='<'] cmp an optional comparison function --- @return the list -function List:sort(cmp) - if cmp then cmp = function_arg(1,cmp) end - tsort(self,cmp) - return self -end - ---- Return a sorted copy of this list. --- @func[opt='<'] cmp an optional comparison function --- @return a new list -function List:sorted(cmp) - return List(self):sort(cmp) -end - ---- Reverse the elements of the list, in place. --- @return the list -function List:reverse() - local t = self - local n = #t - for i = 1,n/2 do - t[i],t[n] = t[n],t[i] - n = n - 1 - end - return self -end - ---- Return the minimum and the maximum value of the list. --- @return minimum value --- @return maximum value -function List:minmax() - local vmin,vmax = 1e70,-1e70 - for i = 1,#self do - local v = self[i] - if v < vmin then vmin = v end - if v > vmax then vmax = v end - end - return vmin,vmax -end - ---- Emulate list slicing. like 'list[first:last]' in Python. --- If first or last are negative then they are relative to the end of the list --- eg. slice(-2) gives last 2 entries in a list, and --- slice(-4,-2) gives from -4th to -2nd --- @param first An index --- @param last An index --- @return a new List -function List:slice(first,last) - return tsub(self,first,last) -end - ---- Empty the list. --- @return the list -function List:clear() - for i=1,#self do tremove(self) end - return self -end - -local eps = 1.0e-10 - ---- Emulate Python's range(x) function. --- Include it in List table for tidiness --- @int start A number --- @int[opt] finish A number greater than start; if absent, --- then start is 1 and finish is start --- @int[opt=1] incr an increment (may be less than 1) --- @return a List from start .. finish --- @usage List.range(0,3) == List{0,1,2,3} --- @usage List.range(4) = List{1,2,3,4} --- @usage List.range(5,1,-1) == List{5,4,3,2,1} -function List.range(start,finish,incr) - if not finish then - finish = start - start = 1 - end - if incr then - assert_arg(3,incr,'number') - if math.ceil(incr) ~= incr then finish = finish + eps end - else - incr = 1 - end - assert_arg(1,start,'number') - assert_arg(2,finish,'number') - local t = List() - for i=start,finish,incr do tinsert(t,i) end - return t -end - ---- list:len() is the same as #list. -function List:len() - return #self -end - --- Extended operations -- - ---- Remove a subrange of elements. --- equivalent to 'del s[i1:i2]' in Python. --- @int i1 start of range --- @int i2 end of range --- @return the list -function List:chop(i1,i2) - return tremovevalues(self,i1,i2) -end - ---- Insert a sublist into a list --- equivalent to 's[idx:idx] = list' in Python --- @int idx index --- @tparam List list list to insert --- @return the list --- @usage l = List{10,20}; l:splice(2,{21,22}); assert(l == List{10,21,22,20}) -function List:splice(idx,list) - assert_arg(1,idx,'number') - idx = idx - 1 - local i = 1 - for v in iter(list) do - tinsert(self,i+idx,v) - i = i + 1 - end - return self -end - ---- General slice assignment s[i1:i2] = seq. --- @int i1 start index --- @int i2 end index --- @tparam List seq a list --- @return the list -function List:slice_assign(i1,i2,seq) - assert_arg(1,i1,'number') - assert_arg(1,i2,'number') - i1,i2 = normalize_slice(self,i1,i2) - if i2 >= i1 then self:chop(i1,i2) end - self:splice(i1,seq) - return self -end - ---- Concatenation operator. --- @within metamethods --- @tparam List L another List --- @return a new list consisting of the list with the elements of the new list appended -function List:__concat(L) - assert_arg(1,L,'table') - local ls = self:clone() - ls:extend(L) - return ls -end - ---- Equality operator ==. True iff all elements of two lists are equal. --- @within metamethods --- @tparam List L another List --- @return true or false -function List:__eq(L) - if #self ~= #L then return false end - for i = 1,#self do - if self[i] ~= L[i] then return false end - end - return true -end - ---- Join the elements of a list using a delimiter. --- This method uses tostring on all elements. --- @string[opt=''] delim a delimiter string, can be empty. --- @return a string -function List:join (delim) - delim = delim or '' - assert_arg(1,delim,'string') - return concat(array_tostring(self),delim) -end - ---- Join a list of strings.
--- Uses `table.concat` directly. --- @function List:concat --- @string[opt=''] delim a delimiter --- @return a string -List.concat = concat - -local function tostring_q(val) - local s = tostring(val) - if type(val) == 'string' then - s = '"'..s..'"' - end - return s -end - ---- How our list should be rendered as a string. Uses join(). --- @within metamethods --- @see List:join -function List:__tostring() - return '{'..self:join(',',tostring_q)..'}' -end - ---- Call the function on each element of the list. --- @func fun a function or callable object --- @param ... optional values to pass to function -function List:foreach (fun,...) - fun = function_arg(1,fun) - for i = 1,#self do - fun(self[i],...) - end -end - -local function lookup_fun (obj,name) - local f = obj[name] - if not f then error(type(obj).." does not have method "..name,3) end - return f -end - ---- Call the named method on each element of the list. --- @string name the method name --- @param ... optional values to pass to function -function List:foreachm (name,...) - for i = 1,#self do - local obj = self[i] - local f = lookup_fun(obj,name) - f(obj,...) - end -end - ---- Create a list of all elements which match a function. --- @func fun a boolean function --- @param[opt] arg optional argument to be passed as second argument of the predicate --- @return a new filtered list. -function List:filter (fun,arg) - return makelist(filter(self,fun,arg),self) -end - ---- Split a string using a delimiter. --- @string s the string --- @string[opt] delim the delimiter (default spaces) --- @return a List of strings --- @see pl.utils.split -function List.split (s,delim) - assert_arg(1,s,'string') - return makelist(split(s,delim)) -end - ---- Apply a function to all elements. --- Any extra arguments will be passed to the function. --- @func fun a function of at least one argument --- @param ... arbitrary extra arguments. --- @return a new list: {f(x) for x in self} --- @usage List{'one','two'}:map(string.upper) == {'ONE','TWO'} --- @see pl.tablex.imap -function List:map (fun,...) - return makelist(imap(fun,self,...),self) -end - ---- Apply a function to all elements, in-place. --- Any extra arguments are passed to the function. --- @func fun A function that takes at least one argument --- @param ... arbitrary extra arguments. --- @return the list. -function List:transform (fun,...) - transform(fun,self,...) - return self -end - ---- Apply a function to elements of two lists. --- Any extra arguments will be passed to the function --- @func fun a function of at least two arguments --- @tparam List ls another list --- @param ... arbitrary extra arguments. --- @return a new list: {f(x,y) for x in self, for x in arg1} --- @see pl.tablex.imap2 -function List:map2 (fun,ls,...) - return makelist(imap2(fun,self,ls,...),self) -end - ---- apply a named method to all elements. --- Any extra arguments will be passed to the method. --- @string name name of method --- @param ... extra arguments --- @return a new list of the results --- @see pl.seq.mapmethod -function List:mapm (name,...) - local res = {} - for i = 1,#self do - local val = self[i] - local fn = lookup_fun(val,name) - res[i] = fn(val,...) - end - return makelist(res,self) -end - -local function composite_call (method,f) - return function(self,...) - return self[method](self,f,...) - end -end - -function List.default_map_with(T) - return function(self,name) - local m - if T then - local f = lookup_fun(T,name) - m = composite_call('map',f) - else - m = composite_call('mapn',name) - end - getmetatable(self)[name] = m -- and cache.. - return m - end -end - -List.default_map = List.default_map_with - ---- 'reduce' a list using a binary function. --- @func fun a function of two arguments --- @return result of the function --- @see pl.tablex.reduce -function List:reduce (fun) - return reduce(fun,self) -end - ---- Partition a list using a classifier function. --- The function may return nil, but this will be converted to the string key ''. --- @func fun a function of at least one argument --- @param ... will also be passed to the function --- @treturn MultiMap a table where the keys are the returned values, and the values are Lists --- of values where the function returned that key. --- @see pl.MultiMap -function List:partition (fun,...) - fun = function_arg(1,fun) - local res = {} - for i = 1,#self do - local val = self[i] - local klass = fun(val,...) - if klass == nil then klass = '' end - if not res[klass] then res[klass] = List() end - res[klass]:append(val) - end - return setmetatable(res,Multimap) -end - ---- return an iterator over all values. -function List:iter () - return iter(self) -end - ---- Create an iterator over a seqence. --- This captures the Python concept of 'sequence'. --- For tables, iterates over all values with integer indices. --- @param seq a sequence; a string (over characters), a table, a file object (over lines) or an iterator function --- @usage for x in iterate {1,10,22,55} do io.write(x,',') end ==> 1,10,22,55 --- @usage for ch in iterate 'help' do do io.write(ch,' ') end ==> h e l p -function List.iterate(seq) - if type(seq) == 'string' then - local idx = 0 - local n = #seq - local sub = string.sub - return function () - idx = idx + 1 - if idx > n then return nil - else - return sub(seq,idx,idx) - end - end - elseif type(seq) == 'table' then - local idx = 0 - local n = #seq - return function() - idx = idx + 1 - if idx > n then return nil - else - return seq[idx] - end - end - elseif type(seq) == 'function' then - return seq - elseif type(seq) == 'userdata' and io.type(seq) == 'file' then - return seq:lines() - end -end -iter = List.iterate - -return List - diff --git a/src/pl/Map.lua b/src/pl/Map.lua deleted file mode 100644 index 572b22a..0000000 --- a/src/pl/Map.lua +++ /dev/null @@ -1,120 +0,0 @@ ---- A Map class. --- --- > Map = require 'pl.Map' --- > m = Map{one=1,two=2} --- > m:update {three=3,four=4,two=20} --- > = m == M{one=1,two=20,three=3,four=4} --- true --- --- Dependencies: `pl.utils`, `pl.class`, `pl.tablex`, `pl.pretty` --- @classmod pl.Map - -local tablex = require 'pl.tablex' -local utils = require 'pl.utils' -local stdmt = utils.stdmt -local deepcompare = tablex.deepcompare - -local pretty_write = require 'pl.pretty' . write -local Map = stdmt.Map -local Set = stdmt.Set - -local class = require 'pl.class' - --- the Map class --------------------- -class(nil,nil,Map) - -function Map:_init (t) - local mt = getmetatable(t) - if mt == Set or mt == Map then - self:update(t) - else - return t -- otherwise assumed to be a map-like table - end -end - - -local function makelist(t) - return setmetatable(t, require('pl.List')) -end - ---- list of keys. -Map.keys = tablex.keys - ---- list of values. -Map.values = tablex.values - ---- return an iterator over all key-value pairs. -function Map:iter () - return pairs(self) -end - ---- return a List of all key-value pairs, sorted by the keys. -function Map:items() - local ls = makelist(tablex.pairmap (function (k,v) return makelist {k,v} end, self)) - ls:sort(function(t1,t2) return t1[1] < t2[1] end) - return ls -end - ---- set a value in the map if it doesn't exist yet. --- @param key the key --- @param default value to set --- @return the value stored in the map (existing value, or the new value) -function Map:setdefault(key, default) - local val = self[key] - if val ~= nil then - return val - end - self:set(key,default) - return default -end - ---- size of map. --- note: this is a relatively expensive operation! --- @class function --- @name Map:len -Map.len = tablex.size - ---- put a value into the map. --- This will remove the key if the value is `nil` --- @param key the key --- @param val the value -function Map:set (key,val) - self[key] = val -end - ---- get a value from the map. --- @param key the key --- @return the value, or nil if not found. -function Map:get (key) - return rawget(self,key) -end - -local index_by = tablex.index_by - ---- get a list of values indexed by a list of keys. --- @param keys a list-like table of keys --- @return a new list -function Map:getvalues (keys) - return makelist(index_by(self,keys)) -end - ---- update the map using key/value pairs from another table. --- @tab table --- @function Map:update -Map.update = tablex.update - ---- equality between maps. --- @within metamethods --- @tparam Map m another map. -function Map:__eq (m) - -- note we explicitly ask deepcompare _not_ to use __eq! - return deepcompare(self,m,true) -end - ---- string representation of a map. --- @within metamethods -function Map:__tostring () - return pretty_write(self,'') -end - -return Map diff --git a/src/pl/MultiMap.lua b/src/pl/MultiMap.lua deleted file mode 100644 index 0abe84d..0000000 --- a/src/pl/MultiMap.lua +++ /dev/null @@ -1,54 +0,0 @@ ---- MultiMap, a Map which has multiple values per key. --- --- Dependencies: `pl.utils`, `pl.class`, `pl.List`, `pl.Map` --- @classmod pl.MultiMap - -local utils = require 'pl.utils' -local class = require 'pl.class' -local List = require 'pl.List' -local Map = require 'pl.Map' - --- MultiMap is a standard MT -local MultiMap = utils.stdmt.MultiMap - -class(Map,nil,MultiMap) -MultiMap._name = 'MultiMap' - -function MultiMap:_init (t) - if not t then return end - self:update(t) -end - ---- update a MultiMap using a table. --- @param t either a Multimap or a map-like table. --- @return the map -function MultiMap:update (t) - utils.assert_arg(1,t,'table') - if Map:class_of(t) then - for k,v in pairs(t) do - self[k] = List() - self[k]:append(v) - end - else - for k,v in pairs(t) do - self[k] = List(v) - end - end -end - ---- add a new value to a key. Setting a nil value removes the key. --- @param key the key --- @param val the value --- @return the map -function MultiMap:set (key,val) - if val == nil then - self[key] = nil - else - if not self[key] then - self[key] = List() - end - self[key]:append(val) - end -end - -return MultiMap diff --git a/src/pl/OrderedMap.lua b/src/pl/OrderedMap.lua deleted file mode 100644 index 379c44f..0000000 --- a/src/pl/OrderedMap.lua +++ /dev/null @@ -1,167 +0,0 @@ ---- OrderedMap, a map which preserves ordering. --- --- Derived from `pl.Map`. --- --- Dependencies: `pl.utils`, `pl.tablex`, `pl.class`, `pl.List`, `pl.Map` --- @classmod pl.OrderedMap - -local tablex = require 'pl.tablex' -local utils = require 'pl.utils' -local List = require 'pl.List' -local index_by,tsort,concat = tablex.index_by,table.sort,table.concat - -local class = require 'pl.class' -local Map = require 'pl.Map' - -local OrderedMap = class(Map) -OrderedMap._name = 'OrderedMap' - -local rawset = rawset - ---- construct an OrderedMap. --- Will throw an error if the argument is bad. --- @param t optional initialization table, same as for @{OrderedMap:update} -function OrderedMap:_init (t) - rawset(self,'_keys',List()) - if t then - local map,err = self:update(t) - if not map then error(err,2) end - end -end - -local assert_arg,raise = utils.assert_arg,utils.raise - ---- update an OrderedMap using a table. --- If the table is itself an OrderedMap, then its entries will be appended. --- if it s a table of the form `{{key1=val1},{key2=val2},...}` these will be appended. --- --- Otherwise, it is assumed to be a map-like table, and order of extra entries is arbitrary. --- @tab t a table. --- @return the map, or nil in case of error --- @return the error message -function OrderedMap:update (t) - assert_arg(1,t,'table') - if OrderedMap:class_of(t) then - for k,v in t:iter() do - self:set(k,v) - end - elseif #t > 0 then -- an array must contain {key=val} tables - if type(t[1]) == 'table' then - for _,pair in ipairs(t) do - local key,value = next(pair) - if not key then return raise 'empty pair initialization table' end - self:set(key,value) - end - else - return raise 'cannot use an array to initialize an OrderedMap' - end - else - for k,v in pairs(t) do - self:set(k,v) - end - end - return self -end - ---- set the key's value. This key will be appended at the end of the map. --- --- If the value is nil, then the key is removed. --- @param key the key --- @param val the value --- @return the map -function OrderedMap:set (key,val) - if rawget(self, key) == nil and val ~= nil then -- new key - self._keys:append(key) -- we keep in order - rawset(self,key,val) -- don't want to provoke __newindex! - else -- existing key-value pair - if val == nil then - self._keys:remove_value(key) - rawset(self,key,nil) - else - self[key] = val - end - end - return self -end - -OrderedMap.__newindex = OrderedMap.set - ---- insert a key/value pair before a given position. --- Note: if the map already contains the key, then this effectively --- moves the item to the new position by first removing at the old position. --- Has no effect if the key does not exist and val is nil --- @int pos a position starting at 1 --- @param key the key --- @param val the value; if nil use the old value -function OrderedMap:insert (pos,key,val) - local oldval = self[key] - val = val or oldval - if oldval then - self._keys:remove_value(key) - end - if val then - self._keys:insert(pos,key) - rawset(self,key,val) - end - return self -end - ---- return the keys in order. --- (Not a copy!) --- @return List -function OrderedMap:keys () - return self._keys -end - ---- return the values in order. --- this is relatively expensive. --- @return List -function OrderedMap:values () - return List(index_by(self,self._keys)) -end - ---- sort the keys. --- @func cmp a comparison function as for @{table.sort} --- @return the map -function OrderedMap:sort (cmp) - tsort(self._keys,cmp) - return self -end - ---- iterate over key-value pairs in order. -function OrderedMap:iter () - local i = 0 - local keys = self._keys - local idx - return function() - i = i + 1 - if i > #keys then return nil end - idx = keys[i] - return idx,self[idx] - end -end - ---- iterate over an ordered map (5.2). --- @within metamethods --- @function OrderedMap:__pairs -OrderedMap.__pairs = OrderedMap.iter - ---- string representation of an ordered map. --- @within metamethods -function OrderedMap:__tostring () - local res = {} - for i,v in ipairs(self._keys) do - local val = self[v] - local vs = tostring(val) - if type(val) ~= 'number' then - vs = '"'..vs..'"' - end - res[i] = tostring(v)..'='..vs - end - return '{'..concat(res,',')..'}' -end - -return OrderedMap - - - diff --git a/src/pl/README.md b/src/pl/README.md deleted file mode 100644 index 909b3c2..0000000 --- a/src/pl/README.md +++ /dev/null @@ -1,127 +0,0 @@ -# Penlight Lua Libraries - -[![Unix build](https://img.shields.io/github/workflow/status/lunarmodules/penlight/Unix%20build?label=Unix%20build&logo=linux)](https://github.com/lunarmodules/Penlight/actions) -[![AppVeyor build status](https://img.shields.io/appveyor/build/Tieske/penlight-ta1gi/master?label=Windows%20build&logo=windows)](https://ci.appveyor.com/project/Tieske/penlight-ta1gi/branch/master) -[![Coveralls code coverage](https://img.shields.io/coveralls/github/lunarmodules/Penlight?logo=coveralls)](https://coveralls.io/github/lunarmodules/Penlight) -[![Luacheck](https://github.com/lunarmodules/Penlight/workflows/Luacheck/badge.svg)](https://github.com/lunarmodules/Penlight/actions) -[![SemVer](https://img.shields.io/github/v/tag/lunarmodules/Penlight?color=brightgreen&label=SemVer&logo=semver&sort=semver)](CHANGELOG.md) - -## Why a new set of libraries? - -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. - -## Module Overview - -### Paths, Files and Directories - - * `path`: queries like `isdir`,`isfile`,`exists`, splitting paths like `dirname` and `basename` - * `dir`: listing files in directories (`getfiles`,`getallfiles`) and creating/removing directory paths - * `file`: `copy`,`move`; read/write contents with `read` and `write` - -### Application Support - - * `app`: `require_here` to rebase `require` to work with main script path; simple argument parsing `parse_args` - * `lapp`: sophisticated usage-text-driven argument parsing for applications - * `config`: flexibly read Unix config files and Windows INI files - * `strict`: check for undefined global variables - can use `strict.module` for modules - * `utils`,`compat`: Penlight support for unified Lua 5.1/5.2 codebases - * `types`: predicates like `is_callable` and `is_integer`; extended `type` function. - -### Extra String Operations - - * `utils`: can split a string with a delimiter using `utils.split` - * `stringx`: extended string functions covering the Python `string` type - * `stringio`: open strings for reading, and creating strings using standard Lua IO methods - * `lexer`: lexical scanner for splitting text into tokens; special cases for Lua and C - * `text`: indenting and dedenting text, wrapping paragraphs; optionally make `%` work as in Python - * `template`: small but powerful template expansion engine - * `sip`: Simple Input Patterns - higher-level string patterns for parsing text - -### Extra Table Operations - - * `tablex`: copying, comparing and mapping over - * `pretty`: pretty-printing Lua tables, and various safe ways to load Lua as data - * `List`: implementation of Python 'list' type - slices, concatenation and partitioning - * `Map`, `Set`, `OrderedMap`: classes for specialized kinds of tables - * `data`: reading tabular data into 2D arrays and efficient queries - * `array2d`: operations on 2D arrays - * `permute`: generate permutations - -### Iterators, OOP and Functional - - * `seq`: working with iterator pipelines; collecting iterators as tables - * `class`: a simple reusable class framework - * `func`: symbolic manipulation of expressions and lambda expressions - * `utils`: `utils.string_lambda` converts short strings like '|x| x^2' into functions - * `comprehension`: list comprehensions: `C'x for x=1,4'()=={1,2,3,4}` - -## Versioning - -Penlight is strictly versioned according to [Semantic Versioning](https://semver.org/). - -In scope of the version: - * functionality provided by Penlight modules/classes - * based on stock Lua PuC-Rio or LuaJIT - -Not in scope of the version: - * Documentation - * Error messages (textual changes) - * Deprecation warnings (by default to `stderr`) - -### Deprecating functionality - -Any version may deprecate functionality. So new deprecation notices may appear -in major, minor, and patch releases. Final removal of functionality (assuming it -is a breaking change) will only be done in a major version. - -It is strongly suggested to use the deprecation warning mechanism to test usage -of deprecated functionalities when upgrading. This is done by enabling the -warning system (in Lua 5.4, or the Penlight compatibility function for earlier -versions): - -```lua -require "pl.compat" -warn "@on" -``` - -See `pl.utils.raise_deprecation` for more info. - -## License - -Penlight is distributed under the [MIT license](LICENSE.md). - -## Installation - -Using [LuaRocks](https://luarocks.org): simply run `luarocks install penlight`. - -Manually: copy `lua/pl` directory into your Lua module path. It's typically -`/usr/local/share/lua/5.x` on a Linux system and `C:\Program Files\Lua\5.x\lua` -for Lua for Windows. - -## Dependencies - -The file and directory functions depend on [LuaFileSystem](https://keplerproject.github.io/luafilesystem/), -which is installed automatically if you are using LuaRocks. Additionally, if you want `dir.copyfile` to work -elegantly on Windows, then you need [Alien](http://mascarenhas.github.io/alien/). Both libraries are present -in Lua for Windows. - -## Building the Documentation - -Requires [ldoc](https://github.com/stevedonovan/LDoc), which is available -through LuaRocks. Then it's a simple matter of running `ldoc .` from the repo. - -## Contributing - -Contributions are most welcome, please check the [contribution guidelines](CONTRIBUTING.md). - -## Running tests - -Execute `lua run.lua tests` to run the tests. Execute `lua run.lua examples` to run examples. - -## History - -For a complete history of the development of Penlight, please check the [changelog](CHANGELOG.md). diff --git a/src/pl/Set.lua b/src/pl/Set.lua deleted file mode 100644 index ce428f0..0000000 --- a/src/pl/Set.lua +++ /dev/null @@ -1,222 +0,0 @@ ---- A Set class. --- --- > Set = require 'pl.Set' --- > = Set{'one','two'} == Set{'two','one'} --- true --- > fruit = Set{'apple','banana','orange'} --- > = fruit['banana'] --- true --- > = fruit['hazelnut'] --- nil --- > colours = Set{'red','orange','green','blue'} --- > = fruit,colours --- [apple,orange,banana] [blue,green,orange,red] --- > = fruit+colours --- [blue,green,apple,red,orange,banana] --- [orange] --- > more_fruits = fruit + 'apricot' --- > = fruit*colours --- > = more_fruits, fruit --- [banana,apricot,apple,orange] [banana,apple,orange] --- --- Dependencies: `pl.utils`, `pl.tablex`, `pl.class`, `pl.Map`, (`pl.List` if __tostring is used) --- @classmod pl.Set - -local tablex = require 'pl.tablex' -local utils = require 'pl.utils' -local array_tostring, concat = utils.array_tostring, table.concat -local merge,difference = tablex.merge,tablex.difference -local Map = require 'pl.Map' -local class = require 'pl.class' -local stdmt = utils.stdmt -local Set = stdmt.Set - --- the Set class -------------------- -class(Map,nil,Set) - --- note: Set has _no_ methods! -Set.__index = nil - -local function makeset (t) - return setmetatable(t,Set) -end - ---- create a set.
--- @param t may be a Set, Map or list-like table. --- @class function --- @name Set -function Set:_init (t) - t = t or {} - local mt = getmetatable(t) - if mt == Set or mt == Map then - for k in pairs(t) do self[k] = true end - else - for _,v in ipairs(t) do self[v] = true end - end -end - ---- string representation of a set. --- @within metamethods -function Set:__tostring () - return '['..concat(array_tostring(Set.values(self)),',')..']' -end - ---- get a list of the values in a set. --- @param self a Set --- @function Set.values --- @return a list -Set.values = Map.keys - ---- map a function over the values of a set. --- @param self a Set --- @param fn a function --- @param ... extra arguments to pass to the function. --- @return a new set -function Set.map (self,fn,...) - fn = utils.function_arg(1,fn) - local res = {} - for k in pairs(self) do - res[fn(k,...)] = true - end - return makeset(res) -end - ---- union of two sets (also +). --- @param self a Set --- @param set another set --- @return a new set -function Set.union (self,set) - return merge(self,set,true) -end - ---- modifies '+' operator to allow addition of non-Set elements ---- Preserves +/- semantics - does not modify first argument. -local function setadd(self,other) - local mt = getmetatable(other) - if mt == Set or mt == Map then - return Set.union(self,other) - else - local new = Set(self) - new[other] = true - return new - end -end - ---- union of sets. --- @within metamethods --- @function Set.__add - -Set.__add = setadd - ---- intersection of two sets (also *). --- @param self a Set --- @param set another set --- @return a new set --- @usage --- > s = Set{10,20,30} --- > t = Set{20,30,40} --- > = t --- [20,30,40] --- > = Set.intersection(s,t) --- [30,20] --- > = s*t --- [30,20] - -function Set.intersection (self,set) - return merge(self,set,false) -end - ---- intersection of sets. --- @within metamethods --- @function Set.__mul -Set.__mul = Set.intersection - ---- new set with elements in the set that are not in the other (also -). --- @param self a Set --- @param set another set --- @return a new set -function Set.difference (self,set) - return difference(self,set,false) -end - ---- modifies "-" operator to remove non-Set values from set. ---- Preserves +/- semantics - does not modify first argument. -local function setminus (self,other) - local mt = getmetatable(other) - if mt == Set or mt == Map then - return Set.difference(self,other) - else - local new = Set(self) - new[other] = nil - return new - end -end - ---- difference of sets. --- @within metamethods --- @function Set.__sub -Set.__sub = setminus - --- a new set with elements in _either_ the set _or_ other but not both (also ^). --- @param self a Set --- @param set another set --- @return a new set -function Set.symmetric_difference (self,set) - return difference(self,set,true) -end - ---- symmetric difference of sets. --- @within metamethods --- @function Set.__pow -Set.__pow = Set.symmetric_difference - ---- is the first set a subset of the second (also <)?. --- @param self a Set --- @param set another set --- @return true or false -function Set.issubset (self,set) - for k in pairs(self) do - if not set[k] then return false end - end - return true -end - ---- first set subset of second? --- @within metamethods --- @function Set.__lt -Set.__lt = Set.issubset - ---- is the set empty?. --- @param self a Set --- @return true or false -function Set.isempty (self) - return next(self) == nil -end - ---- are the sets disjoint? (no elements in common). --- Uses naive definition, i.e. that intersection is empty --- @param s1 a Set --- @param s2 another set --- @return true or false -function Set.isdisjoint (s1,s2) - return Set.isempty(Set.intersection(s1,s2)) -end - ---- size of this set (also # for 5.2). --- @param s a Set --- @return size --- @function Set.len -Set.len = tablex.size - ---- cardinality of set (5.2). --- @within metamethods --- @function Set.__len -Set.__len = Set.len - ---- equality between sets. --- @within metamethods -function Set.__eq (s1,s2) - return Set.issubset(s1,s2) and Set.issubset(s2,s1) -end - -return Set diff --git a/src/pl/app.lua b/src/pl/app.lua deleted file mode 100644 index a06305d..0000000 --- a/src/pl/app.lua +++ /dev/null @@ -1,297 +0,0 @@ ---- Application support functions. --- See @{01-introduction.md.Application_Support|the Guide} --- --- Dependencies: `pl.utils`, `pl.path` --- @module pl.app - -local io,package,require = _G.io, _G.package, _G.require -local utils = require 'pl.utils' -local path = require 'pl.path' - -local app = {} - ---- 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 - ---- prefixes the current script's path to the Lua module path. --- Applies to both the source and the binary module paths. It makes it easy for --- the main file of a multi-file program to access its modules in the same directory. --- `base` allows these modules to be put in a specified subdirectory, to allow for --- cleaner deployment and resolve potential conflicts between a script name and its --- library directory. --- --- Note: the path is prefixed, so it is searched first when requiring modules. --- @string base optional base directory (absolute, or relative path). --- @treturn string the current script's path with a trailing slash -function app.require_here (base) - 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 - if path.is_windows then - base = base:gsub('/','\\') - end - 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 ';' - local csep = package.cpath:find '^;' and '' or ';' - package.path = ('%s?.lua;%s?%sinit.lua%s%s'):format(p,p,path.sep,lsep,package.path) - package.cpath = ('%s?.%s%s%s'):format(p,so_ext,csep,package.cpath) - return p -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 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 - end - return path.join(dir,file) -end - ---- return string indicating operating system. --- @return 'Windows','OSX' or whatever uname returns (e.g. 'Linux') -function app.platform() - if path.is_windows then - return 'Windows' - else - local f = io.popen('uname') - local res = f:read() - if res == 'Darwin' then res = 'OSX' end - f:close() - return res - end -end - ---- return the full command-line used to invoke this script. --- It will not include the scriptname itself, see `app.script_name`. --- @return command-line --- @return name of Lua program used --- @usage --- -- execute: lua -lluacov -e 'print(_VERSION)' myscript.lua --- --- -- myscript.lua --- print(require("pl.app").lua()) --> "lua -lluacov -e 'print(_VERSION)'", "lua" -function app.lua() - local args = _G.arg - if not args then - return utils.raise "not in a main program" - end - - 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 -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`), --- 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, 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! --- @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 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 - - -- 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 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 with_values[v] then - if i == #args or args[i+1]:find '^-' then - return utils.raise ("no value for '"..v.."'") - end - flags[valid[v]] = args[i+1] - i = i + 1 - else - -- a value can also be indicated with = or : - local var,val = utils.splitv (v,'[=:]', false, 2) - var = var or v - val = val or true - if not is_long then - if #var > 1 then - if var:find '.%d+' then -- short flag, number value - val = var:sub(2) - var = var:sub(1,1) - else -- multiple short flags - for i = 1,#var do - 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 - else -- single short flag (can have value, defaults to true) - val = val or true - 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 - end - i = i + 1 - end - return flags,_args -end - -return app diff --git a/src/pl/array2d.lua b/src/pl/array2d.lua deleted file mode 100644 index 6e884d1..0000000 --- a/src/pl/array2d.lua +++ /dev/null @@ -1,585 +0,0 @@ ---- Operations on two-dimensional arrays. --- See @{02-arrays.md.Operations_on_two_dimensional_tables|The Guide} --- --- The size of the arrays is determined by using the length operator `#` hence --- the module is not `nil` safe, and the usual precautions apply. --- --- Note: all functions taking `i1,j1,i2,j2` as arguments will normalize the --- arguments using `default_range`. --- --- Dependencies: `pl.utils`, `pl.tablex`, `pl.types` --- @module pl.array2d - -local tonumber,tostring,io,ipairs,string,table = - _G.tonumber,_G.tostring,_G.io,_G.ipairs,_G.string,_G.table -local setmetatable,getmetatable = setmetatable,getmetatable - -local tablex = require 'pl.tablex' -local utils = require 'pl.utils' -local types = require 'pl.types' -local imap,tmap,reduce,keys,tmap2,tset,index_by = tablex.imap,tablex.map,tablex.reduce,tablex.keys,tablex.map2,tablex.set,tablex.index_by -local remove = table.remove -local splitv,fprintf,assert_arg = utils.splitv,utils.fprintf,utils.assert_arg -local byte = string.byte -local stdout = io.stdout -local min = math.min - - -local array2d = {} - -local function obj (int,out) - local mt = getmetatable(int) - if mt then - setmetatable(out,mt) - end - return out -end - -local function makelist (res) - return setmetatable(res, require('pl.List')) -end - ---- return the row and column size. --- Size is calculated using the Lua length operator #, so usual precautions --- regarding `nil` values apply. --- @array2d a a 2d array --- @treturn int number of rows (`#a`) --- @treturn int number of cols (`#a[1]`) -function array2d.size (a) - assert_arg(1,a,'table') - return #a,#a[1] -end - -do - local function index (t,k) - return t[k] - end - - --- extract a column from the 2D array. - -- @array2d a 2d array - -- @param j column index - -- @return 1d array - function array2d.column (a,j) - assert_arg(1,a,'table') - return makelist(imap(index,a,j)) - end -end -local column = array2d.column - ---- extract a row from the 2D array. --- Added in line with `column`, for read-only purposes directly --- accessing a[i] is more performant. --- @array2d a 2d array --- @param i row index --- @return 1d array (copy of the row) -function array2d.row(a,i) - assert_arg(1,a,'table') - local row = a[i] - local r = {} - for n,v in ipairs(row) do - r[n] = v - end - return makelist(r) -end - ---- map a function over a 2D array --- @func f a function of at least one argument --- @array2d a 2d array --- @param arg an optional extra argument to be passed to the function. --- @return 2d array -function array2d.map (f,a,arg) - assert_arg(2,a,'table') - f = utils.function_arg(1,f) - return obj(a,imap(function(row) return imap(f,row,arg) end, a)) -end - ---- reduce the rows using a function. --- @func f a binary function --- @array2d a 2d array --- @return 1d array --- @see pl.tablex.reduce -function array2d.reduce_rows (f,a) - assert_arg(1,a,'table') - return tmap(function(row) return reduce(f,row) end, a) -end - ---- reduce the columns using a function. --- @func f a binary function --- @array2d a 2d array --- @return 1d array --- @see pl.tablex.reduce -function array2d.reduce_cols (f,a) - assert_arg(1,a,'table') - return tmap(function(c) return reduce(f,column(a,c)) end, keys(a[1])) -end - ---- reduce a 2D array into a scalar, using two operations. --- @func opc operation to reduce the final result --- @func opr operation to reduce the rows --- @param a 2D array -function array2d.reduce2 (opc,opr,a) - assert_arg(3,a,'table') - local tmp = array2d.reduce_rows(opr,a) - return reduce(opc,tmp) -end - ---- map a function over two arrays. --- They can be both or either 2D arrays --- @func f function of at least two arguments --- @int ad order of first array (`1` if `a` is a list/array, `2` if it is a 2d array) --- @int bd order of second array (`1` if `b` is a list/array, `2` if it is a 2d array) --- @tab a 1d or 2d array --- @tab b 1d or 2d array --- @param arg optional extra argument to pass to function --- @return 2D array, unless both arrays are 1D -function array2d.map2 (f,ad,bd,a,b,arg) - assert_arg(1,a,'table') - assert_arg(2,b,'table') - f = utils.function_arg(1,f) - if ad == 1 and bd == 2 then - return imap(function(row) - return tmap2(f,a,row,arg) - end, b) - elseif ad == 2 and bd == 1 then - return imap(function(row) - return tmap2(f,row,b,arg) - end, a) - elseif ad == 1 and bd == 1 then - return tmap2(f,a,b) - elseif ad == 2 and bd == 2 then - return tmap2(function(rowa,rowb) - return tmap2(f,rowa,rowb,arg) - end, a,b) - end -end - ---- cartesian product of two 1d arrays. --- @func f a function of 2 arguments --- @array t1 a 1d table --- @array t2 a 1d table --- @return 2d table --- @usage product('..',{1,2},{'a','b'}) == {{'1a','2a'},{'1b','2b'}} -function array2d.product (f,t1,t2) - f = utils.function_arg(1,f) - assert_arg(2,t1,'table') - assert_arg(3,t2,'table') - local res = {} - for i,v in ipairs(t2) do - res[i] = tmap(f,t1,v) - end - return res -end - ---- flatten a 2D array. --- (this goes over columns first.) --- @array2d t 2d table --- @return a 1d table --- @usage flatten {{1,2},{3,4},{5,6}} == {1,2,3,4,5,6} -function array2d.flatten (t) - local res = {} - local k = 1 - local rows, cols = array2d.size(t) - for r = 1, rows do - local row = t[r] - for c = 1, cols do - res[k] = row[c] - k = k + 1 - end - end - return makelist(res) -end - ---- reshape a 2D array. Reshape the aray by specifying a new nr of rows. --- @array2d t 2d array --- @int nrows new number of rows --- @bool co use column-order (Fortran-style) (default false) --- @return a new 2d array -function array2d.reshape (t,nrows,co) - local nr,nc = array2d.size(t) - local ncols = nr*nc / nrows - local res = {} - local ir,ic = 1,1 - for i = 1,nrows do - local row = {} - for j = 1,ncols do - row[j] = t[ir][ic] - if not co then - ic = ic + 1 - if ic > nc then - ir = ir + 1 - ic = 1 - end - else - ir = ir + 1 - if ir > nr then - ic = ic + 1 - ir = 1 - end - end - end - res[i] = row - end - return obj(t,res) -end - ---- transpose a 2D array. --- @array2d t 2d array --- @return a new 2d array -function array2d.transpose(t) - assert_arg(1,t,'table') - local _, c = array2d.size(t) - return array2d.reshape(t,c,true) -end - ---- swap two rows of an array. --- @array2d t a 2d array --- @int i1 a row index --- @int i2 a row index --- @return t (same, modified 2d array) -function array2d.swap_rows (t,i1,i2) - assert_arg(1,t,'table') - t[i1],t[i2] = t[i2],t[i1] - return t -end - ---- swap two columns of an array. --- @array2d t a 2d array --- @int j1 a column index --- @int j2 a column index --- @return t (same, modified 2d array) -function array2d.swap_cols (t,j1,j2) - assert_arg(1,t,'table') - for _, row in ipairs(t) do - row[j1],row[j2] = row[j2],row[j1] - end - return t -end - ---- extract the specified rows. --- @array2d t 2d array --- @tparam {int} ridx a table of row indices --- @return a new 2d array with the extracted rows -function array2d.extract_rows (t,ridx) - return obj(t,index_by(t,ridx)) -end - ---- extract the specified columns. --- @array2d t 2d array --- @tparam {int} cidx a table of column indices --- @return a new 2d array with the extracted colums -function array2d.extract_cols (t,cidx) - assert_arg(1,t,'table') - local res = {} - for i = 1,#t do - res[i] = index_by(t[i],cidx) - end - return obj(t,res) -end - ---- remove a row from an array. --- @function array2d.remove_row --- @array2d t a 2d array --- @int i a row index -array2d.remove_row = remove - ---- remove a column from an array. --- @array2d t a 2d array --- @int j a column index -function array2d.remove_col (t,j) - assert_arg(1,t,'table') - for i = 1,#t do - remove(t[i],j) - end -end - -do - local function _parse (s) - local r, c = s:match 'R(%d+)C(%d+)' - if r then - r,c = tonumber(r),tonumber(c) - return r,c - end - c,r = s:match '(%a+)(%d+)' - if c then - local cv = 0 - for i = 1, #c do - cv = cv * 26 + byte(c:sub(i,i)) - byte 'A' + 1 - end - return tonumber(r), cv - end - error('bad cell specifier: '..s) - end - - --- parse a spreadsheet range or cell. - -- The range/cell can be specified either as 'A1:B2' or 'R1C1:R2C2' or for - -- single cells as 'A1' or 'R1C1'. - -- @string s a range (case insensitive). - -- @treturn int start row - -- @treturn int start col - -- @treturn int end row (or `nil` if the range was a single cell) - -- @treturn int end col (or `nil` if the range was a single cell) - function array2d.parse_range (s) - assert_arg(1,s,'string') - s = s:upper() - if s:find ':' then - local start,finish = splitv(s,':') - local i1,j1 = _parse(start) - local i2,j2 = _parse(finish) - return i1,j1,i2,j2 - else -- single value - local i,j = _parse(s) - return i,j - end - end -end - ---- get a slice of a 2D array. --- Same as `slice`. --- @see slice -function array2d.range (...) - return array2d.slice(...) -end - -local default_range do - local function norm_value(v, max) - if not v then return v end - if v < 0 then - v = max + v + 1 - end - if v < 1 then v = 1 end - if v > max then v = max end - return v - end - - --- normalizes coordinates to valid positive entries and defaults. - -- Negative indices will be counted from the end, too low, or too high - -- will be limited by the array sizes. - -- @array2d t a 2D array - -- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range` - -- @tparam[opt=1] int j1 start col - -- @tparam[opt=N] int i2 end row - -- @tparam[opt=M] int j2 end col - -- @see parse_range - -- @return i1, j1, i2, j2 - function array2d.default_range (t,i1,j1,i2,j2) - if (type(i1) == 'string') and not (j1 or i2 or j2) then - i1, j1, i2, j2 = array2d.parse_range(i1) - end - local nr, nc = array2d.size(t) - i1 = norm_value(i1 or 1, nr) - j1 = norm_value(j1 or 1, nc) - i2 = norm_value(i2 or nr, nr) - j2 = norm_value(j2 or nc, nc) - return i1,j1,i2,j2 - end - default_range = array2d.default_range -end - ---- get a slice of a 2D array. Note that if the specified range has --- a 1D result, the rank of the result will be 1. --- @array2d t a 2D array --- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range` --- @tparam[opt=1] int j1 start col --- @tparam[opt=N] int i2 end row --- @tparam[opt=M] int j2 end col --- @see parse_range --- @return an array, 2D in general but 1D in special cases. -function array2d.slice (t,i1,j1,i2,j2) - assert_arg(1,t,'table') - i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2) - local res = {} - for i = i1,i2 do - local val - local row = t[i] - if j1 == j2 then - val = row[j1] - else - val = {} - for j = j1,j2 do - val[#val+1] = row[j] - end - end - res[#res+1] = val - end - if i1 == i2 then res = res[1] end - return obj(t,res) -end - ---- set a specified range of an array to a value. --- @array2d t a 2D array --- @param value the value (may be a function, called as `val(i,j)`) --- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range` --- @tparam[opt=1] int j1 start col --- @tparam[opt=N] int i2 end row --- @tparam[opt=M] int j2 end col --- @see parse_range --- @see tablex.set -function array2d.set (t,value,i1,j1,i2,j2) - i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2) - local i = i1 - if types.is_callable(value) then - local old_f = value - value = function(j) - return old_f(i,j) - end - end - while i <= i2 do - tset(t[i],value,j1,j2) - i = i + 1 - end -end - ---- write a 2D array to a file. --- @array2d t a 2D array --- @param f a file object (default stdout) --- @string fmt a format string (default is just to use tostring) --- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range` --- @tparam[opt=1] int j1 start col --- @tparam[opt=N] int i2 end row --- @tparam[opt=M] int j2 end col --- @see parse_range -function array2d.write (t,f,fmt,i1,j1,i2,j2) - assert_arg(1,t,'table') - f = f or stdout - local rowop - if fmt then - rowop = function(row,j) fprintf(f,fmt,row[j]) end - else - rowop = function(row,j) f:write(tostring(row[j]),' ') end - end - local function newline() - f:write '\n' - end - array2d.forall(t,rowop,newline,i1,j1,i2,j2) -end - ---- perform an operation for all values in a 2D array. --- @array2d t 2D array --- @func row_op function to call on each value; `row_op(row,j)` --- @func end_row_op function to call at end of each row; `end_row_op(i)` --- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range` --- @tparam[opt=1] int j1 start col --- @tparam[opt=N] int i2 end row --- @tparam[opt=M] int j2 end col --- @see parse_range -function array2d.forall (t,row_op,end_row_op,i1,j1,i2,j2) - assert_arg(1,t,'table') - i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2) - for i = i1,i2 do - local row = t[i] - for j = j1,j2 do - row_op(row,j) - end - if end_row_op then end_row_op(i) end - end -end - ----- move a block from the destination to the source. --- @array2d dest a 2D array --- @int di start row in dest --- @int dj start col in dest --- @array2d src a 2D array --- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range` --- @tparam[opt=1] int j1 start col --- @tparam[opt=N] int i2 end row --- @tparam[opt=M] int j2 end col --- @see parse_range -function array2d.move (dest,di,dj,src,i1,j1,i2,j2) - assert_arg(1,dest,'table') - assert_arg(4,src,'table') - i1,j1,i2,j2 = default_range(src,i1,j1,i2,j2) - local nr,nc = array2d.size(dest) - i2, j2 = min(nr,i2), min(nc,j2) - --i1, j1 = max(1,i1), max(1,j1) - dj = dj - 1 - for i = i1,i2 do - local drow, srow = dest[i+di-1], src[i] - for j = j1,j2 do - drow[j+dj] = srow[j] - end - end -end - ---- iterate over all elements in a 2D array, with optional indices. --- @array2d a 2D array --- @bool indices with indices (default false) --- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range` --- @tparam[opt=1] int j1 start col --- @tparam[opt=N] int i2 end row --- @tparam[opt=M] int j2 end col --- @see parse_range --- @return either `value` or `i,j,value` depending on the value of `indices` -function array2d.iter(a,indices,i1,j1,i2,j2) - assert_arg(1,a,'table') - i1,j1,i2,j2 = default_range(a,i1,j1,i2,j2) - local i,j = i1,j1-1 - local row = a[i] - return function() - j = j + 1 - if j > j2 then - j = j1 - i = i + 1 - row = a[i] - if i > i2 then - return nil - end - end - if indices then - return i,j,row[j] - else - return row[j] - end - end -end - ---- iterate over all columns. --- @array2d a a 2D array --- @return column, column-index -function array2d.columns(a) - assert_arg(1,a,'table') - local n = #a[1] - local i = 0 - return function() - i = i + 1 - if i > n then return nil end - return column(a,i), i - end -end - ---- iterate over all rows. --- Returns a copy of the row, for read-only purposes directly iterating --- is more performant; `ipairs(a)` --- @array2d a a 2D array --- @return row, row-index -function array2d.rows(a) - assert_arg(1,a,'table') - local n = #a - local i = 0 - return function() - i = i + 1 - if i > n then return nil end - return array2d.row(a,i), i - end -end - ---- new array of specified dimensions --- @int rows number of rows --- @int cols number of cols --- @param val initial value; if it's a function then use `val(i,j)` --- @return new 2d array -function array2d.new(rows,cols,val) - local res = {} - local fun = types.is_callable(val) - for i = 1,rows do - local row = {} - if fun then - for j = 1,cols do row[j] = val(i,j) end - else - for j = 1,cols do row[j] = val end - end - res[i] = row - end - return res -end - -return array2d diff --git a/src/pl/class.lua b/src/pl/class.lua deleted file mode 100644 index 49246ee..0000000 --- a/src/pl/class.lua +++ /dev/null @@ -1,265 +0,0 @@ ---- Provides a reuseable and convenient framework for creating classes in Lua. --- Two possible notations: --- --- B = class(A) --- class.B(A) --- --- The latter form creates a named class within the current environment. Note --- that this implicitly brings in `pl.utils` as a dependency. --- --- See the Guide for further @{01-introduction.md.Simplifying_Object_Oriented_Programming_in_Lua|discussion} --- @module pl.class - -local error, getmetatable, io, pairs, rawget, rawset, setmetatable, tostring, type = - _G.error, _G.getmetatable, _G.io, _G.pairs, _G.rawget, _G.rawset, _G.setmetatable, _G.tostring, _G.type -local compat - --- this trickery is necessary to prevent the inheritance of 'super' and --- the resulting recursive call problems. -local function call_ctor (c,obj,...) - local init = rawget(c,'_init') - local parent_with_init = rawget(c,'_parent_with_init') - - if parent_with_init then - if not init then -- inheriting an init - init = rawget(parent_with_init, '_init') - parent_with_init = rawget(parent_with_init, '_parent_with_init') - end - if parent_with_init then -- super() points to one above whereever _init came from - rawset(obj,'super',function(obj,...) - call_ctor(parent_with_init,obj,...) - end) - end - else - -- Without this, calling super() where none exists will sometimes loop and stack overflow - rawset(obj,'super',nil) - end - - local res = init(obj,...) - if parent_with_init then -- If this execution of call_ctor set a super, unset it - rawset(obj,'super',nil) - end - - return res -end - ---- initializes an __instance__ upon creation. --- @function class:_init --- @param ... parameters passed to the constructor --- @usage local Cat = class() --- function Cat:_init(name) --- --self:super(name) -- call the ancestor initializer if needed --- self.name = name --- end --- --- local pussycat = Cat("pussycat") --- print(pussycat.name) --> pussycat - ---- checks whether an __instance__ is derived from some class. --- Works the other way around as `class_of`. It has two ways of using; --- 1) call with a class to check against, 2) call without params. --- @function instance:is_a --- @param some_class class to check against, or `nil` to return the class --- @return `true` if `instance` is derived from `some_class`, or if `some_class == nil` then --- it returns the class table of the instance --- @usage local pussycat = Lion() -- assuming Lion derives from Cat --- if pussycat:is_a(Cat) then --- -- it's true, it is a Lion, but also a Cat --- end --- --- if pussycat:is_a() == Lion then --- -- It's true --- end -local function is_a(self,klass) - if klass == nil then - -- no class provided, so return the class this instance is derived from - return getmetatable(self) - end - local m = getmetatable(self) - if not m then return false end --*can't be an object! - while m do - if m == klass then return true end - m = rawget(m,'_base') - end - return false -end - ---- checks whether an __instance__ is derived from some class. --- Works the other way around as `is_a`. --- @function some_class:class_of --- @param some_instance instance to check against --- @return `true` if `some_instance` is derived from `some_class` --- @usage local pussycat = Lion() -- assuming Lion derives from Cat --- if Cat:class_of(pussycat) then --- -- it's true --- end -local function class_of(klass,obj) - if type(klass) ~= 'table' or not rawget(klass,'is_a') then return false end - return klass.is_a(obj,klass) -end - ---- cast an object to another class. --- It is not clever (or safe!) so use carefully. --- @param some_instance the object to be changed --- @function some_class:cast -local function cast (klass, obj) - return setmetatable(obj,klass) -end - - -local function _class_tostring (obj) - local mt = obj._class - local name = rawget(mt,'_name') - setmetatable(obj,nil) - local str = tostring(obj) - setmetatable(obj,mt) - if name then str = name ..str:gsub('table','') end - return str -end - -local function tupdate(td,ts,dont_override) - for k,v in pairs(ts) do - if not dont_override or td[k] == nil then - td[k] = v - end - end -end - -local function _class(base,c_arg,c) - -- the class `c` will be the metatable for all its objects, - -- and they will look up their methods in it. - local mt = {} -- a metatable for the class to support __call and _handler - -- can define class by passing it a plain table of methods - local plain = type(base) == 'table' and not getmetatable(base) - if plain then - c = base - base = c._base - else - c = c or {} - end - - if type(base) == 'table' then - -- our new class is a shallow copy of the base class! - -- but be careful not to wipe out any methods we have been given at this point! - tupdate(c,base,plain) - c._base = base - -- inherit the 'not found' handler, if present - if rawget(c,'_handler') then mt.__index = c._handler end - elseif base ~= nil then - error("must derive from a table type",3) - end - - c.__index = c - setmetatable(c,mt) - if not plain then - if base and rawget(base,'_init') then c._parent_with_init = base end -- For super and inherited init - c._init = nil - end - - if base and rawget(base,'_class_init') then - base._class_init(c,c_arg) - end - - -- expose a ctor which can be called by () - mt.__call = function(class_tbl,...) - local obj - if rawget(c,'_create') then obj = c._create(...) end - if not obj then obj = {} end - setmetatable(obj,c) - - if rawget(c,'_init') or rawget(c,'_parent_with_init') then -- constructor exists - local res = call_ctor(c,obj,...) - if res then -- _if_ a ctor returns a value, it becomes the object... - obj = res - setmetatable(obj,c) - end - end - - if base and rawget(base,'_post_init') then - base._post_init(obj) - end - - return obj - end - -- Call Class.catch to set a handler for methods/properties not found in the class! - c.catch = function(self, handler) - if type(self) == "function" then - -- called using . instead of : - handler = self - end - c._handler = handler - mt.__index = handler - end - c.is_a = is_a - c.class_of = class_of - c.cast = cast - c._class = c - - if not rawget(c,'__tostring') then - c.__tostring = _class_tostring - end - - return c -end - ---- create a new class, derived from a given base class. --- Supporting two class creation syntaxes: --- either `Name = class(base)` or `class.Name(base)`. --- The first form returns the class directly and does not set its `_name`. --- The second form creates a variable `Name` in the current environment set --- to the class, and also sets `_name`. --- @function class --- @param base optional base class --- @param c_arg optional parameter to class constructor --- @param c optional table to be used as class -local class -class = setmetatable({},{ - __call = function(fun,...) - return _class(...) - end, - __index = function(tbl,key) - if key == 'class' then - io.stderr:write('require("pl.class").class is deprecated. Use require("pl.class")\n') - return class - end - compat = compat or require 'pl.compat' - local env = compat.getfenv(2) - return function(...) - local c = _class(...) - c._name = key - rawset(env,key,c) - return c - end - end -}) - -class.properties = class() - -function class.properties._class_init(klass) - klass.__index = function(t,key) - -- normal class lookup! - local v = klass[key] - if v then return v end - -- is it a getter? - v = rawget(klass,'get_'..key) - if v then - return v(t) - end - -- is it a field? - return rawget(t,'_'..key) - end - klass.__newindex = function (t,key,value) - -- if there's a setter, use that, otherwise directly set table - local p = 'set_'..key - local setter = klass[p] - if setter then - setter(t,value) - else - rawset(t,key,value) - end - end -end - - -return class - diff --git a/src/pl/compat.lua b/src/pl/compat.lua deleted file mode 100644 index a4c9841..0000000 --- a/src/pl/compat.lua +++ /dev/null @@ -1,250 +0,0 @@ ----------------- ---- 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 = _G.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. --- --- NOTE2: In Lua 5.2 and 5.3 a Windows exitcode of -1 would not properly be --- returned, this function will return it properly for all versions. --- @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/5.3 not reporting -1 properly on Windows - -- this was fixed in 5.4 - 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] j 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 file name would be matched. --- This function was introduced in Lua 5.2, so this compatibility version --- will be injected in Lua 5.1 engines. --- @string name file name, possibly dotted --- @string path a path-template in the same form as package.path or package.cpath --- @string[opt] sep template separate character to be replaced by path separator. Default: "." --- @string[opt] rep the path separator to use, defaults to system separator. Default; "/" on Unixes, "\" on Windows. --- @see path.package_path --- @function package.searchpath --- @return on success: path of the file --- @return on failure: nil, error string listing paths tried -if not package.searchpath then - function package.searchpath (name,path,sep,rep) -- luacheck: ignore - if type(name) ~= "string" then - error(("bad argument #1 to 'searchpath' (string expected, got %s)"):format(type(path)), 2) - end - if type(path) ~= "string" then - error(("bad argument #2 to 'searchpath' (string expected, got %s)"):format(type(path)), 2) - end - if sep ~= nil and type(sep) ~= "string" then - error(("bad argument #3 to 'searchpath' (string expected, got %s)"):format(type(path)), 2) - end - if rep ~= nil and type(rep) ~= "string" then - error(("bad argument #4 to 'searchpath' (string expected, got %s)"):format(type(path)), 2) - end - sep = sep or "." - rep = rep or compat.dir_separator - do - local s, e = name:find(sep, nil, true) - while s do - name = name:sub(1, s-1) .. rep .. name:sub(e+1, -1) - s, e = name:find(sep, s + #rep + 1, true) - end - end - local tried = {} - for m in path:gmatch('[^;]+') do - local nm = m:gsub('?', name) - tried[#tried+1] = nm - local f = io.open(nm,'r') - if f then f:close(); return nm end - end - return nil, "\tno file '" .. table.concat(tried, "'\n\tno file '") .. "'" - end -end - ---- Global exported functions (for Lua < 5.4) --- @section lua54 - ---- raise a warning message. --- This functions mimics the `warn` function added in Lua 5.4. --- @function warn --- @param ... any arguments -if not warn then -- luacheck: ignore - local enabled = false - function warn(arg1, ...) -- luacheck: ignore - if type(arg1) == "string" and arg1:sub(1, 1) == "@" then - -- control message - if arg1 == "@on" then - enabled = true - return - end - if arg1 == "@off" then - enabled = false - return - end - return -- ignore unknown control messages - end - if enabled then - io.stderr:write("Lua warning: ", arg1, ...) - io.stderr:write("\n") - end - end -end - -return compat diff --git a/src/pl/comprehension.lua b/src/pl/comprehension.lua deleted file mode 100644 index 39be7c5..0000000 --- a/src/pl/comprehension.lua +++ /dev/null @@ -1,285 +0,0 @@ ---- List comprehensions implemented in Lua. --- --- See the [wiki page](http://lua-users.org/wiki/ListComprehensions) --- --- local C= require 'pl.comprehension' . new() --- --- C ('x for x=1,10') () --- ==> {1,2,3,4,5,6,7,8,9,10} --- C 'x^2 for x=1,4' () --- ==> {1,4,9,16} --- C '{x,x^2} for x=1,4' () --- ==> {{1,1},{2,4},{3,9},{4,16}} --- C '2*x for x' {1,2,3} --- ==> {2,4,6} --- dbl = C '2*x for x' --- dbl {10,20,30} --- ==> {20,40,60} --- C 'x for x if x % 2 == 0' {1,2,3,4,5} --- ==> {2,4} --- C '{x,y} for x = 1,2 for y = 1,2' () --- ==> {{1,1},{1,2},{2,1},{2,2}} --- C '{x,y} for x for y' ({1,2},{10,20}) --- ==> {{1,10},{1,20},{2,10},{2,20}} --- assert(C 'sum(x^2 for x)' {2,3,4} == 2^2+3^2+4^2) --- --- (c) 2008 David Manura. Licensed under the same terms as Lua (MIT license). --- --- Dependencies: `pl.utils`, `pl.luabalanced` --- --- See @{07-functional.md.List_Comprehensions|the Guide} --- @module pl.comprehension - -local utils = require 'pl.utils' - -local status,lb = pcall(require, "pl.luabalanced") -if not status then - lb = require 'luabalanced' -end - -local math_max = math.max -local table_concat = table.concat - --- fold operations --- http://en.wikipedia.org/wiki/Fold_(higher-order_function) -local ops = { - list = {init=' {} ', accum=' __result[#__result+1] = (%s) '}, - table = {init=' {} ', accum=' local __k, __v = %s __result[__k] = __v '}, - sum = {init=' 0 ', accum=' __result = __result + (%s) '}, - min = {init=' nil ', accum=' local __tmp = %s ' .. - ' if __result then if __tmp < __result then ' .. - '__result = __tmp end else __result = __tmp end '}, - max = {init=' nil ', accum=' local __tmp = %s ' .. - ' if __result then if __tmp > __result then ' .. - '__result = __tmp end else __result = __tmp end '}, -} - - --- Parses comprehension string expr. --- Returns output expression list string, array of for types --- ('=', 'in' or nil) , array of input variable name --- strings , array of input variable value strings --- , array of predicate expression strings , --- operation name string , and number of placeholder --- parameters . --- --- The is equivalent to the mathematical set-builder notation: --- --- { | in , } --- --- @usage "x^2 for x" -- array values --- @usage "x^2 for x=1,10,2" -- numeric for --- @usage "k^v for k,v in pairs(_1)" -- iterator for --- @usage "(x+y)^2 for x for y if x > y" -- nested --- -local function parse_comprehension(expr) - local pos = 1 - - -- extract opname (if exists) - local opname - local tok, post = expr:match('^%s*([%a_][%w_]*)%s*%(()', pos) - local pose = #expr + 1 - if tok then - local tok2, posb = lb.match_bracketed(expr, post-1) - assert(tok2, 'syntax error') - if expr:match('^%s*$', posb) then - opname = tok - pose = posb - 1 - pos = post - end - end - opname = opname or "list" - - -- extract out expression list - local out; out, pos = lb.match_explist(expr, pos) - assert(out, "syntax error: missing expression list") - out = table_concat(out, ', ') - - -- extract "for" clauses - local fortypes = {} - local invarlists = {} - local invallists = {} - while 1 do - local post = expr:match('^%s*for%s+()', pos) - if not post then break end - pos = post - - -- extract input vars - local iv; iv, pos = lb.match_namelist(expr, pos) - assert(#iv > 0, 'syntax error: zero variables') - for _,ident in ipairs(iv) do - assert(not ident:match'^__', - "identifier " .. ident .. " may not contain __ prefix") - end - invarlists[#invarlists+1] = iv - - -- extract '=' or 'in' (optional) - local fortype, post = expr:match('^(=)%s*()', pos) - if not fortype then fortype, post = expr:match('^(in)%s+()', pos) end - if fortype then - pos = post - -- extract input value range - local il; il, pos = lb.match_explist(expr, pos) - assert(#il > 0, 'syntax error: zero expressions') - assert(fortype ~= '=' or #il == 2 or #il == 3, - 'syntax error: numeric for requires 2 or three expressions') - fortypes[#invarlists] = fortype - invallists[#invarlists] = il - else - fortypes[#invarlists] = false - invallists[#invarlists] = false - end - end - assert(#invarlists > 0, 'syntax error: missing "for" clause') - - -- extract "if" clauses - local preds = {} - while 1 do - local post = expr:match('^%s*if%s+()', pos) - if not post then break end - pos = post - local pred; pred, pos = lb.match_expression(expr, pos) - assert(pred, 'syntax error: predicated expression not found') - preds[#preds+1] = pred - end - - -- extract number of parameter variables (name matching "_%d+") - local stmp = ''; lb.gsub(expr, function(u, sin) -- strip comments/strings - if u == 'e' then stmp = stmp .. ' ' .. sin .. ' ' end - end) - local max_param = 0; stmp:gsub('[%a_][%w_]*', function(s) - local s = s:match('^_(%d+)$') - if s then max_param = math_max(max_param, tonumber(s)) end - end) - - if pos ~= pose then - assert(false, "syntax error: unrecognized " .. expr:sub(pos)) - end - - --DEBUG: - --print('----\n', string.format("%q", expr), string.format("%q", out), opname) - --for k,v in ipairs(invarlists) do print(k,v, invallists[k]) end - --for k,v in ipairs(preds) do print(k,v) end - - return out, fortypes, invarlists, invallists, preds, opname, max_param -end - - --- Create Lua code string representing comprehension. --- Arguments are in the form returned by parse_comprehension. -local function code_comprehension( - out, fortypes, invarlists, invallists, preds, opname, max_param -) - local op = assert(ops[opname]) - local code = op.accum:gsub('%%s', out) - - for i=#preds,1,-1 do local pred = preds[i] - code = ' if ' .. pred .. ' then ' .. code .. ' end ' - end - for i=#invarlists,1,-1 do - if not fortypes[i] then - local arrayname = '__in' .. i - local idx = '__idx' .. i - code = - ' for ' .. idx .. ' = 1, #' .. arrayname .. ' do ' .. - ' local ' .. invarlists[i][1] .. ' = ' .. arrayname .. '['..idx..'] ' .. - code .. ' end ' - else - code = - ' for ' .. - table_concat(invarlists[i], ', ') .. - ' ' .. fortypes[i] .. ' ' .. - table_concat(invallists[i], ', ') .. - ' do ' .. code .. ' end ' - end - end - code = ' local __result = ( ' .. op.init .. ' ) ' .. code - return code -end - - --- Convert code string represented by code_comprehension --- into Lua function. Also must pass ninputs = #invarlists, --- max_param, and invallists (from parse_comprehension). --- Uses environment env. -local function wrap_comprehension(code, ninputs, max_param, invallists, env) - assert(ninputs > 0) - local ts = {} - for i=1,max_param do - ts[#ts+1] = '_' .. i - end - for i=1,ninputs do - if not invallists[i] then - local name = '__in' .. i - ts[#ts+1] = name - end - end - if #ts > 0 then - code = ' local ' .. table_concat(ts, ', ') .. ' = ... ' .. code - end - code = code .. ' return __result ' - --print('DEBUG:', code) - local f, err = utils.load(code,'tmp','t',env) - if not f then assert(false, err .. ' with generated code ' .. code) end - return f -end - - --- Build Lua function from comprehension string. --- Uses environment env. -local function build_comprehension(expr, env) - local out, fortypes, invarlists, invallists, preds, opname, max_param - = parse_comprehension(expr) - local code = code_comprehension( - out, fortypes, invarlists, invallists, preds, opname, max_param) - local f = wrap_comprehension(code, #invarlists, max_param, invallists, env) - return f -end - - --- Creates new comprehension cache. --- Any list comprehension function created are set to the environment --- env (defaults to caller of new). -local function new(env) - -- Note: using a single global comprehension cache would have had - -- security implications (e.g. retrieving cached functions created - -- in other environments). - -- The cache lookup function could have instead been written to retrieve - -- the caller's environment, lookup up the cache private to that - -- environment, and then looked up the function in that cache. - -- That would avoid the need for this call to - -- explicitly manage caches; however, that might also have an undue - -- performance penalty. - - if not env then - env = utils.getfenv(2) - end - - local mt = {} - local cache = setmetatable({}, mt) - - -- Index operator builds, caches, and returns Lua function - -- corresponding to comprehension expression string. - -- - -- Example: f = comprehension['x^2 for x'] - -- - function mt:__index(expr) - local f = build_comprehension(expr, env) - self[expr] = f -- cache - return f - end - - -- Convenience syntax. - -- Allows comprehension 'x^2 for x' instead of comprehension['x^2 for x']. - mt.__call = mt.__index - - cache.new = new - - return cache -end - - -local comprehension = {} -comprehension.new = new - -return comprehension diff --git a/src/pl/config.lua b/src/pl/config.lua deleted file mode 100644 index 2e6db0c..0000000 --- a/src/pl/config.lua +++ /dev/null @@ -1,207 +0,0 @@ ---- Reads configuration files into a Lua table. --- Understands INI files, classic Unix config files, and simple --- delimited columns of values. See @{06-data.md.Reading_Configuration_Files|the Guide} --- --- # test.config --- # Read timeout in seconds --- read.timeout=10 --- # Write timeout in seconds --- write.timeout=5 --- #acceptable ports --- ports = 1002,1003,1004 --- --- -- readconfig.lua --- local config = require 'config' --- local t = config.read 'test.config' --- print(pretty.write(t)) --- --- ### output ##### --- { --- ports = { --- 1002, --- 1003, --- 1004 --- }, --- write_timeout = 5, --- read_timeout = 10 --- } --- --- @module pl.config - -local type,tonumber,ipairs,io, table = _G.type,_G.tonumber,_G.ipairs,_G.io,_G.table - -local function split(s,re) - local res = {} - local t_insert = table.insert - re = '[^'..re..']+' - for k in s:gmatch(re) do t_insert(res,k) end - return res -end - -local function strip(s) - return s:gsub('^%s+',''):gsub('%s+$','') -end - -local function strip_quotes (s) - return s:gsub("['\"](.*)['\"]",'%1') -end - -local config = {} - ---- like io.lines(), but allows for lines to be continued with '\'. --- @param file a file-like object (anything where read() returns the next line) or a filename. --- Defaults to stardard input. --- @return an iterator over the lines, or nil --- @return error 'not a file-like object' or 'file is nil' -function config.lines(file) - local f,openf,err - local line = '' - if type(file) == 'string' then - f,err = io.open(file,'r') - if not f then return nil,err end - openf = true - else - f = file or io.stdin - if not file.read then return nil, 'not a file-like object' end - end - if not f then return nil, 'file is nil' end - return function() - local l = f:read() - while l do - -- only for non-blank lines that don't begin with either ';' or '#' - if l:match '%S' and not l:match '^%s*[;#]' then - -- does the line end with '\'? - local i = l:find '\\%s*$' - if i then -- if so, - line = line..l:sub(1,i-1) - elseif line == '' then - return l - else - l = line..l - line = '' - return l - end - end - l = f:read() - end - if openf then f:close() end - end -end - ---- read a configuration file into a table --- @param file either a file-like object or a string, which must be a filename --- @tab[opt] cnfg a configuration table that may contain these fields: --- --- * `smart` try to deduce what kind of config file we have (default false) --- * `variabilize` make names into valid Lua identifiers (default true) --- * `convert_numbers` try to convert values into numbers (default true) --- * `trim_space` ensure that there is no starting or trailing whitespace with values (default true) --- * `trim_quotes` remove quotes from strings (default false) --- * `list_delim` delimiter to use when separating columns (default ',') --- * `keysep` separator between key and value pairs (default '=') --- --- @return a table containing items, or `nil` --- @return error message (same as @{config.lines} -function config.read(file,cnfg) - local auto - - local iter,err = config.lines(file) - if not iter then return nil,err end - local line = iter() - cnfg = cnfg or {} - if cnfg.smart then - auto = true - if line:match '^[^=]+=' then - cnfg.keysep = '=' - elseif line:match '^[^:]+:' then - cnfg.keysep = ':' - cnfg.list_delim = ':' - elseif line:match '^%S+%s+' then - cnfg.keysep = ' ' - -- more than two columns assume that it's a space-delimited list - -- cf /etc/fstab with /etc/ssh/ssh_config - if line:match '^%S+%s+%S+%s+%S+' then - cnfg.list_delim = ' ' - end - cnfg.variabilize = false - end - end - - - local function check_cnfg (var,def) - local val = cnfg[var] - if val == nil then return def else return val end - end - - local initial_digits = '^[%d%+%-]' - local t = {} - local top_t = t - local variabilize = check_cnfg ('variabilize',true) - local list_delim = check_cnfg('list_delim',',') - local convert_numbers = check_cnfg('convert_numbers',true) - local convert_boolean = check_cnfg('convert_boolean',false) - local trim_space = check_cnfg('trim_space',true) - local trim_quotes = check_cnfg('trim_quotes',false) - local ignore_assign = check_cnfg('ignore_assign',false) - local keysep = check_cnfg('keysep','=') - local keypat = keysep == ' ' and '%s+' or '%s*'..keysep..'%s*' - if list_delim == ' ' then list_delim = '%s+' end - - local function process_name(key) - if variabilize then - key = key:gsub('[^%w]','_') - end - return key - end - - local function process_value(value) - if list_delim and value:find(list_delim) then - value = split(value,list_delim) - for i,v in ipairs(value) do - value[i] = process_value(v) - end - elseif convert_numbers and value:find(initial_digits) then - local val = tonumber(value) - if not val and value:match ' kB$' then - value = value:gsub(' kB','') - val = tonumber(value) - end - if val then value = val end - elseif convert_boolean and value == 'true' then - return true - elseif convert_boolean and value == 'false' then - return false - end - if type(value) == 'string' then - if trim_space then value = strip(value) end - if not trim_quotes and auto and value:match '^"' then - trim_quotes = true - end - if trim_quotes then value = strip_quotes(value) end - end - return value - end - - while line do - if line:find('^%[') then -- section! - local section = process_name(line:match('%[([^%]]+)%]')) - t = top_t - t[section] = {} - t = t[section] - else - line = line:gsub('^%s*','') - local i1,i2 = line:find(keypat) - if i1 and not ignore_assign then -- key,value assignment - local key = process_name(line:sub(1,i1-1)) - local value = process_value(line:sub(i2+1)) - t[key] = value - else -- a plain list of values... - t[#t+1] = process_value(line) - end - end - line = iter() - end - return top_t -end - -return config diff --git a/src/pl/data.lua b/src/pl/data.lua deleted file mode 100644 index a565ebc..0000000 --- a/src/pl/data.lua +++ /dev/null @@ -1,654 +0,0 @@ ---- Reading and querying simple tabular data. --- --- data.read 'test.txt' --- ==> {{10,20},{2,5},{40,50},fieldnames={'x','y'},delim=','} --- --- Provides a way of creating basic SQL-like queries. --- --- require 'pl' --- local d = data.read('xyz.txt') --- local q = d:select('x,y,z where x > 3 and z < 2 sort by y') --- for x,y,z in q do --- print(x,y,z) --- end --- --- See @{06-data.md.Reading_Columnar_Data|the Guide} --- --- Dependencies: `pl.utils`, `pl.array2d` (fallback methods) --- @module pl.data - -local utils = require 'pl.utils' -local _DEBUG = rawget(_G,'_DEBUG') - -local patterns,function_arg,usplit,array_tostring = utils.patterns,utils.function_arg,utils.split,utils.array_tostring -local append,concat = table.insert,table.concat -local gsub = string.gsub -local io = io -local _G,print,type,tonumber,ipairs,setmetatable = _G,print,type,tonumber,ipairs,setmetatable - - -local data = {} - -local parse_select - -local function rstrip(s) - return (s:gsub('%s+$','')) -end - -local function strip (s) - return (rstrip(s):gsub('^%s*','')) -end - --- This gives `l` the standard List metatable, --- pulling in the List module. -local function makelist(l) - return setmetatable(l, require('pl.List')) -end - -local function map(fun,t) - local res = {} - for i = 1,#t do - res[i] = fun(t[i]) - end - return res -end - -local function split(line,delim,csv,n) - local massage - -- CSV fields may be double-quoted and may contain commas! - if csv and line:match '"' then - line = line:gsub('"([^"]+)"',function(str) - local s,cnt = str:gsub(',','\001') - if cnt > 0 then massage = true end - return s - end) - if massage then - massage = function(s) return (s:gsub('\001',',')) end - end - end - local res = (usplit(line,delim,false,n)) - if csv then - -- restore CSV commas-in-fields - if massage then res = map(massage,res) end - -- in CSV mode trailiing commas are significant! - if line:match ',$' then append(res,'') end - end - return makelist(res) -end - -local function find(t,v) - for i = 1,#t do - if v == t[i] then return i end - end -end - -local DataMT = { - column_by_name = function(self,name) - if type(name) == 'number' then - name = '$'..name - end - local arr = {} - for res in data.query(self,name) do - append(arr,res) - end - return makelist(arr) - end, - - copy_select = function(self,condn) - condn = parse_select(condn,self) - local iter = data.query(self,condn) - local res = {} - local row = makelist{iter()} - while #row > 0 do - append(res,row) - row = makelist{iter()} - end - res.delim = self.delim - return data.new(res,split(condn.fields,',')) - end, - - column_names = function(self) - return self.fieldnames - end, -} - -local array2d - -DataMT.__index = function(self,name) - local f = DataMT[name] - if f then return f end - if not array2d then - array2d = require 'pl.array2d' - end - return array2d[name] -end - ---- return a particular column as a list of values (method). --- @param name either name of column, or numerical index. --- @function Data.column_by_name - ---- return a query iterator on this data (method). --- @string condn the query expression --- @function Data.select --- @see data.query - ---- return a row iterator on this data (method). --- @string condn the query expression --- @function Data.select_row - ---- return a new data object based on this query (method). --- @string condn the query expression --- @function Data.copy_select - ---- return the field names of this data object (method). --- @function Data.column_names - ---- write out a row (method). --- @param f file-like object --- @function Data.write_row - ---- write data out to file (method). --- @param f file-like object --- @function Data.write - - --- [guessing delimiter] We check for comma, tab and spaces in that order. --- [issue] any other delimiters to be checked? -local delims = {',', '\t', ' ', ';'} - -local function guess_delim (line) - if line=='' then return ' ' end - for _,delim in ipairs(delims) do - if line:find(delim) then - return delim == ' ' and '%s+' or delim - end - end - return ' ' -end - --- [file parameter] If it's a string, we try open as a filename. If nil, then --- either stdin or stdout depending on the mode. Otherwise, check if this is --- a file-like object (implements read or write depending) -local function open_file (f,mode) - local opened, err - local reading = mode == 'r' - if type(f) == 'string' then - if f == 'stdin' then - f = io.stdin - elseif f == 'stdout' then - f = io.stdout - else - f,err = io.open(f,mode) - if not f then return nil,err end - opened = true - end - end - if f and ((reading and not f.read) or (not reading and not f.write)) then - return nil, "not a file-like object" - end - return f,nil,opened -end - ---- read a delimited file in a Lua table. --- By default, attempts to treat first line as separated list of fieldnames. --- @param file a filename or a file-like object --- @tab cnfg parsing options --- @string cnfg.delim a string pattern to split fields --- @array cnfg.fieldnames (i.e. don't read from first line) --- @bool cnfg.no_convert (default is to try conversion on first data line) --- @tab cnfg.convert table of custom conversion functions with column keys --- @int cnfg.numfields indices of columns known to be numbers --- @bool cnfg.last_field_collect only split as many fields as fieldnames. --- @int cnfg.thousands_dot thousands separator in Excel CSV is '.' --- @bool cnfg.csv fields may be double-quoted and contain commas; --- Also, empty fields are considered to be equivalent to zero. --- @return `data` object, or `nil` --- @return error message. May be a file error, 'not a file-like object' --- or a conversion error -function data.read(file,cnfg) - local count,line - local D = {} - if not cnfg then cnfg = {} end - local f,err,opened = open_file(file,'r') - if not f then return nil, err end - local thousands_dot = cnfg.thousands_dot - local csv = cnfg.csv - if csv then cnfg.delim = ',' end - - -- note that using dot as the thousands separator (@thousands_dot) - -- requires a special conversion function! For CSV, _empty fields_ are - -- considered to default to numerial zeroes. - local tonumber = tonumber - local function try_number(x) - if thousands_dot then x = x:gsub('%.(...)','%1') end - if csv and x == '' then x = '0' end - local v = tonumber(x) - if v == nil then return nil,"not a number" end - return v - end - - count = 1 - line = f:read() - if not line then return nil, "empty file" end - - -- first question: what is the delimiter? - D.delim = cnfg.delim and cnfg.delim or guess_delim(line) - local delim = D.delim - - local conversion - local numfields = {} - local function append_conversion (idx,conv) - conversion = conversion or {} - append(numfields,idx) - append(conversion,conv) - end - if cnfg.numfields then - for _,n in ipairs(cnfg.numfields) do append_conversion(n,try_number) end - end - - -- some space-delimited data starts with a space. This should not be a column, - -- although it certainly would be for comma-separated, etc. - local stripper - if delim == '%s+' and line:find(delim) == 1 then - stripper = function(s) return s:gsub('^%s+','') end - line = stripper(line) - end - -- first line will usually be field names. Unless fieldnames are specified, - -- we check if it contains purely numerical values for the case of reading - -- plain data files. - if not cnfg.fieldnames then - local fields,nums - fields = split(line,delim,csv) - if not cnfg.convert then - nums = map(tonumber,fields) - if #nums == #fields then -- they're ALL numbers! - append(D,nums) -- add the first converted row - -- and specify conversions for subsequent rows - for i = 1,#nums do append_conversion(i,try_number) end - else -- we'll try to check numbers just now.. - nums = nil - end - else -- [explicit column conversions] (any deduced number conversions will be added) - for idx,conv in pairs(cnfg.convert) do append_conversion(idx,conv) end - end - if nums == nil then - cnfg.fieldnames = fields - end - line = f:read() - count = count + 1 - if stripper then line = stripper(line) end - elseif type(cnfg.fieldnames) == 'string' then - cnfg.fieldnames = split(cnfg.fieldnames,delim,csv) - end - local nfields - -- at this point, the column headers have been read in. If the first - -- row consisted of numbers, it has already been added to the dataset. - if cnfg.fieldnames then - D.fieldnames = cnfg.fieldnames - -- [collecting end field] If @last_field_collect then we'll - -- only split as many fields as there are fieldnames - if cnfg.last_field_collect then - nfields = #D.fieldnames - end - -- [implicit column conversion] unless @no_convert, we need the numerical field indices - -- of the first data row. These can also be specified explicitly by @numfields. - if not cnfg.no_convert then - local fields = split(line,D.delim,csv,nfields) - for i = 1,#fields do - if not find(numfields,i) and try_number(fields[i]) then - append_conversion(i,try_number) - end - end - end - end - -- keep going until finished - while line do - if not line:find ('^%s*$') then -- [blank lines] ignore them! - if stripper then line = stripper(line) end - local fields = split(line,delim,csv,nfields) - if conversion then -- there were field conversions... - for k = 1,#numfields do - local i,conv = numfields[k],conversion[k] - local val,err = conv(fields[i]) - if val == nil then - return nil, err..": "..fields[i].." at line "..count - else - fields[i] = val - end - end - end - append(D,fields) - end - line = f:read() - count = count + 1 - end - if opened then f:close() end - if delim == '%s+' then D.delim = ' ' end - if not D.fieldnames then D.fieldnames = {} end - return data.new(D) -end - -local function write_row (data,f,row,delim) - data.temp = array_tostring(row,data.temp) - f:write(concat(data.temp,delim),'\n') -end - -function DataMT:write_row(f,row) - write_row(self,f,row,self.delim) -end - ---- write 2D data to a file. --- Does not assume that the data has actually been --- generated with `new` or `read`. --- @param data 2D array --- @param file filename or file-like object --- @tparam[opt] {string} fieldnames list of fields (optional) --- @string[opt='\t'] delim delimiter (default tab) --- @return true or nil, error -function data.write (data,file,fieldnames,delim) - local f,err,opened = open_file(file,'w') - if not f then return nil, err end - if not fieldnames then - fieldnames = data.fieldnames - end - delim = delim or '\t' - if fieldnames and #fieldnames > 0 then - f:write(concat(fieldnames,delim),'\n') - end - for i = 1,#data do - write_row(data,f,data[i],delim) - end - if opened then f:close() end - return true -end - - -function DataMT:write(file) - data.write(self,file,self.fieldnames,self.delim) -end - -local function massage_fieldnames (fields,copy) - -- fieldnames must be valid Lua identifiers; ignore any surrounding padding - -- but keep the original fieldnames... - for i = 1,#fields do - local f = strip(fields[i]) - copy[i] = f - fields[i] = f:gsub('%W','_') - end -end - ---- create a new dataset from a table of rows. --- Can specify the fieldnames, else the table must have a field called --- 'fieldnames', which is either a string of delimiter-separated names, --- or a table of names.
--- If the table does not have a field called 'delim', then an attempt will be --- made to guess it from the fieldnames string, defaults otherwise to tab. --- @param d the table. --- @tparam[opt] {string} fieldnames optional fieldnames --- @return the table. -function data.new (d,fieldnames) - d.fieldnames = d.fieldnames or fieldnames or '' - if not d.delim and type(d.fieldnames) == 'string' then - d.delim = guess_delim(d.fieldnames) - d.fieldnames = split(d.fieldnames,d.delim) - end - d.fieldnames = makelist(d.fieldnames) - d.original_fieldnames = {} - massage_fieldnames(d.fieldnames,d.original_fieldnames) - setmetatable(d,DataMT) - -- a query with just the fieldname will return a sequence - -- of values, which seq.copy turns into a table. - return d -end - -local sorted_query = [[ -return function (t) - local i = 0 - local v - local ls = {} - for i,v in ipairs(t) do - if CONDITION then - ls[#ls+1] = v - end - end - table.sort(ls,function(v1,v2) - return SORT_EXPR - end) - local n = #ls - return function() - i = i + 1 - v = ls[i] - if i > n then return end - return FIELDLIST - end -end -]] - --- question: is this optimized case actually worth the extra code? -local simple_query = [[ -return function (t) - local n = #t - local i = 0 - local v - return function() - repeat - i = i + 1 - v = t[i] - until i > n or CONDITION - if i > n then return end - return FIELDLIST - end -end -]] - -local function is_string (s) - return type(s) == 'string' -end - -local field_error - -local function fieldnames_as_string (data) - return concat(data.fieldnames,',') -end - -local function massage_fields(data,f) - local idx - if f:find '^%d+$' then - idx = tonumber(f) - else - idx = find(data.fieldnames,f) - end - if idx then - return 'v['..idx..']' - else - field_error = f..' not found in '..fieldnames_as_string(data) - return f - end -end - - -local function process_select (data,parms) - --- preparing fields ---- - field_error = nil - local fields = parms.fields - local numfields = fields:find '%$' or #data.fieldnames == 0 - if fields:find '^%s*%*%s*' then - if not numfields then - fields = fieldnames_as_string(data) - else - local ncol = #data[1] - fields = {} - for i = 1,ncol do append(fields,'$'..i) end - fields = concat(fields,',') - end - end - local idpat = patterns.IDEN - if numfields then - idpat = '%$(%d+)' - else - -- massage field names to replace non-identifier chars - fields = rstrip(fields):gsub('[^,%w]','_') - end - local massage_fields = utils.bind1(massage_fields,data) - local ret = gsub(fields,idpat,massage_fields) - if field_error then return nil,field_error end - parms.fields = fields - parms.proc_fields = ret - parms.where = parms.where or 'true' - if is_string(parms.where) then - parms.where = gsub(parms.where,idpat,massage_fields) - field_error = nil - end - return true -end - - -parse_select = function(s,data) - local endp - local parms = {} - local w1,w2 = s:find('where ') - local s1,s2 = s:find('sort by ') - if w1 then -- where clause! - endp = (s1 or 0)-1 - parms.where = s:sub(w2+1,endp) - end - if s1 then -- sort by clause (must be last!) - parms.sort_by = s:sub(s2+1) - end - endp = (w1 or s1 or 0)-1 - parms.fields = s:sub(1,endp) - local status,err = process_select(data,parms) - if not status then return nil,err - else return parms end -end - ---- create a query iterator from a select string. --- Select string has this format:
--- FIELDLIST [ where LUA-CONDN [ sort by FIELD] ]
--- FIELDLIST is a comma-separated list of valid fields, or '*'.

--- The condition can also be a table, with fields 'fields' (comma-sep string or --- table), 'sort_by' (string) and 'where' (Lua expression string or function) --- @param data table produced by read --- @param condn select string or table --- @param context a list of tables to be searched when resolving functions --- @param return_row if true, wrap the results in a row table --- @return an iterator over the specified fields, or nil --- @return an error message -function data.query(data,condn,context,return_row) - local err - if is_string(condn) then - condn,err = parse_select(condn,data) - if not condn then return nil,err end - elseif type(condn) == 'table' then - if type(condn.fields) == 'table' then - condn.fields = concat(condn.fields,',') - end - if not condn.proc_fields then - local status,err = process_select(data,condn) - if not status then return nil,err end - end - else - return nil, "condition must be a string or a table" - end - local query - if condn.sort_by then -- use sorted_query - query = sorted_query - else - query = simple_query - end - local fields = condn.proc_fields or condn.fields - if return_row then - fields = '{'..fields..'}' - end - query = query:gsub('FIELDLIST',fields) - if is_string(condn.where) then - query = query:gsub('CONDITION',condn.where) - condn.where = nil - else - query = query:gsub('CONDITION','_condn(v)') - condn.where = function_arg(0,condn.where,'condition.where must be callable') - end - if condn.sort_by then - local expr,sort_var,sort_dir - local sort_by = condn.sort_by - local i1,i2 = sort_by:find('%s+') - if i1 then - sort_var,sort_dir = sort_by:sub(1,i1-1),sort_by:sub(i2+1) - else - sort_var = sort_by - sort_dir = 'asc' - end - if sort_var:match '^%$' then sort_var = sort_var:sub(2) end - sort_var = massage_fields(data,sort_var) - if field_error then return nil,field_error end - if sort_dir == 'asc' then - sort_dir = '<' - else - sort_dir = '>' - end - expr = ('%s %s %s'):format(sort_var:gsub('v','v1'),sort_dir,sort_var:gsub('v','v2')) - query = query:gsub('SORT_EXPR',expr) - end - if condn.where then - query = 'return function(_condn) '..query..' end' - end - if _DEBUG then print(query) end - - local fn,err = utils.load(query,'tmp') - if not fn then return nil,err end - fn = fn() -- get the function - if condn.where then - fn = fn(condn.where) - end - local qfun = fn(data) - if context then - -- [specifying context for condition] @context is a list of tables which are - -- 'injected'into the condition's custom context - append(context,_G) - local lookup = {} - utils.setfenv(qfun,lookup) - setmetatable(lookup,{ - __index = function(tbl,key) - -- _G.print(tbl,key) - for k,t in ipairs(context) do - if t[key] then return t[key] end - end - end - }) - end - return qfun -end - - -DataMT.select = data.query -DataMT.select_row = function(d,condn,context) - return data.query(d,condn,context,true) -end - ---- Filter input using a query. --- @string Q a query string --- @param infile filename or file-like object --- @param outfile filename or file-like object --- @bool dont_fail true if you want to return an error, not just fail -function data.filter (Q,infile,outfile,dont_fail) - local d = data.read(infile or 'stdin') - local out = open_file(outfile or 'stdout') - local iter,err = d:select(Q) - local delim = d.delim - if not iter then - err = 'error: '..err - if dont_fail then - return nil,err - else - utils.quit(1,err) - end - end - while true do - local res = {iter()} - if #res == 0 then break end - out:write(concat(res,delim),'\n') - end -end - -return data - diff --git a/src/pl/dir.lua b/src/pl/dir.lua deleted file mode 100644 index cde16dc..0000000 --- a/src/pl/dir.lua +++ /dev/null @@ -1,527 +0,0 @@ ---- Listing files in directories and creating/removing directory paths. --- --- Dependencies: `pl.utils`, `pl.path` --- --- Soft Dependencies: `alien`, `ffi` (either are used on Windows for copying/moving files) --- @module pl.dir - -local utils = require 'pl.utils' -local path = require 'pl.path' -local is_windows = path.is_windows -local ldir = path.dir -local mkdir = path.mkdir -local rmdir = path.rmdir -local sub = string.sub -local os,pcall,ipairs,pairs,require,setmetatable = os,pcall,ipairs,pairs,require,setmetatable -local remove = os.remove -local append = table.insert -local assert_arg,assert_string,raise = utils.assert_arg,utils.assert_string,utils.raise - -local exists, isdir = path.exists, path.isdir -local sep = path.sep - -local dir = {} - -local function makelist(l) - return setmetatable(l, require('pl.List')) -end - -local function assert_dir (n,val) - assert_arg(n,val,'string',path.isdir,'not a directory',4) -end - -local function filemask(mask) - mask = utils.escape(path.normcase(mask)) - return '^'..mask:gsub('%%%*','.*'):gsub('%%%?','.')..'$' -end - ---- Test whether a file name matches a shell pattern. --- Both parameters are case-normalized if operating system is --- case-insensitive. --- @string filename A file name. --- @string pattern A shell pattern. The only special characters are --- `'*'` and `'?'`: `'*'` matches any sequence of characters and --- `'?'` matches any single character. --- @treturn bool --- @raise dir and mask must be strings -function dir.fnmatch(filename,pattern) - assert_string(1,filename) - assert_string(2,pattern) - return path.normcase(filename):find(filemask(pattern)) ~= nil -end - ---- Return a list of all file names within an array which match a pattern. --- @tab filenames An array containing file names. --- @string pattern A shell pattern (see `fnmatch`). --- @treturn List(string) List of matching file names. --- @raise dir and mask must be strings -function dir.filter(filenames,pattern) - assert_arg(1,filenames,'table') - assert_string(2,pattern) - local res = {} - local mask = filemask(pattern) - for i,f in ipairs(filenames) do - if path.normcase(f):find(mask) then append(res,f) end - end - return makelist(res) -end - -local function _listfiles(dirname,filemode,match) - local res = {} - local check = utils.choose(filemode,path.isfile,path.isdir) - if not dirname then dirname = '.' end - for f in ldir(dirname) do - if f ~= '.' and f ~= '..' then - local p = path.join(dirname,f) - if check(p) and (not match or match(f)) then - append(res,p) - end - end - end - return makelist(res) -end - ---- return a list of all files in a directory which match a shell pattern. --- @string[opt='.'] dirname A directory. --- @string[opt] mask A shell pattern (see `fnmatch`). If not given, all files are returned. --- @treturn {string} list of files --- @raise dirname and mask must be strings -function dir.getfiles(dirname,mask) - dirname = dirname or '.' - assert_dir(1,dirname) - if mask then assert_string(2,mask) end - local match - if mask then - mask = filemask(mask) - match = function(f) - return path.normcase(f):find(mask) - end - end - return _listfiles(dirname,true,match) -end - ---- return a list of all subdirectories of the directory. --- @string[opt='.'] dirname A directory. --- @treturn {string} a list of directories --- @raise dir must be a valid directory -function dir.getdirectories(dirname) - dirname = dirname or '.' - assert_dir(1,dirname) - return _listfiles(dirname,false) -end - -local alien,ffi,ffi_checked,CopyFile,MoveFile,GetLastError,win32_errors,cmd_tmpfile - -local function execute_command(cmd,parms) - if not cmd_tmpfile then cmd_tmpfile = path.tmpname () end - local err = path.is_windows and ' > ' or ' 2> ' - cmd = cmd..' '..parms..err..utils.quote_arg(cmd_tmpfile) - local ret = utils.execute(cmd) - if not ret then - local err = (utils.readfile(cmd_tmpfile):gsub('\n(.*)','')) - remove(cmd_tmpfile) - return false,err - else - remove(cmd_tmpfile) - return true - end -end - -local function find_ffi_copyfile () - if not ffi_checked then - ffi_checked = true - local res - res,alien = pcall(require,'alien') - if not res then - alien = nil - res, ffi = pcall(require,'ffi') - end - if not res then - ffi = nil - return - end - else - return - end - if alien then - -- register the Win32 CopyFile and MoveFile functions - local kernel = alien.load('kernel32.dll') - CopyFile = kernel.CopyFileA - CopyFile:types{'string','string','int',ret='int',abi='stdcall'} - MoveFile = kernel.MoveFileA - MoveFile:types{'string','string',ret='int',abi='stdcall'} - GetLastError = kernel.GetLastError - GetLastError:types{ret ='int', abi='stdcall'} - elseif ffi then - ffi.cdef [[ - int CopyFileA(const char *src, const char *dest, int iovr); - int MoveFileA(const char *src, const char *dest); - int GetLastError(); - ]] - CopyFile = ffi.C.CopyFileA - MoveFile = ffi.C.MoveFileA - GetLastError = ffi.C.GetLastError - end - win32_errors = { - ERROR_FILE_NOT_FOUND = 2, - ERROR_PATH_NOT_FOUND = 3, - ERROR_ACCESS_DENIED = 5, - ERROR_WRITE_PROTECT = 19, - ERROR_BAD_UNIT = 20, - ERROR_NOT_READY = 21, - ERROR_WRITE_FAULT = 29, - ERROR_READ_FAULT = 30, - ERROR_SHARING_VIOLATION = 32, - ERROR_LOCK_VIOLATION = 33, - ERROR_HANDLE_DISK_FULL = 39, - ERROR_BAD_NETPATH = 53, - ERROR_NETWORK_BUSY = 54, - ERROR_DEV_NOT_EXIST = 55, - ERROR_FILE_EXISTS = 80, - ERROR_OPEN_FAILED = 110, - ERROR_INVALID_NAME = 123, - ERROR_BAD_PATHNAME = 161, - ERROR_ALREADY_EXISTS = 183, - } -end - -local function two_arguments (f1,f2) - return utils.quote_arg(f1)..' '..utils.quote_arg(f2) -end - -local function file_op (is_copy,src,dest,flag) - if flag == 1 and path.exists(dest) then - return false,"cannot overwrite destination" - end - if is_windows then - -- if we haven't tried to load Alien/LuaJIT FFI before, then do so - find_ffi_copyfile() - -- fallback if there's no Alien, just use DOS commands *shudder* - -- 'rename' involves a copy and then deleting the source. - if not CopyFile then - if path.is_windows then - src = src:gsub("/","\\") - dest = dest:gsub("/","\\") - end - local res, err = execute_command('copy',two_arguments(src,dest)) - if not res then return false,err end - if not is_copy then - return execute_command('del',utils.quote_arg(src)) - end - return true - else - if path.isdir(dest) then - dest = path.join(dest,path.basename(src)) - end - local ret - if is_copy then ret = CopyFile(src,dest,flag) - else ret = MoveFile(src,dest) end - if ret == 0 then - local err = GetLastError() - for name,value in pairs(win32_errors) do - if value == err then return false,name end - end - return false,"Error #"..err - else return true - end - end - else -- for Unix, just use cp for now - return execute_command(is_copy and 'cp' or 'mv', - two_arguments(src,dest)) - end -end - ---- copy a file. --- @string src source file --- @string dest destination file or directory --- @bool flag true if you want to force the copy (default) --- @treturn bool operation succeeded --- @raise src and dest must be strings -function dir.copyfile (src,dest,flag) - assert_string(1,src) - assert_string(2,dest) - flag = flag==nil or flag - return file_op(true,src,dest,flag and 0 or 1) -end - ---- move a file. --- @string src source file --- @string dest destination file or directory --- @treturn bool operation succeeded --- @raise src and dest must be strings -function dir.movefile (src,dest) - assert_string(1,src) - assert_string(2,dest) - return file_op(false,src,dest,0) -end - -local function _dirfiles(dirname,attrib) - local dirs = {} - local files = {} - for f in ldir(dirname) do - if f ~= '.' and f ~= '..' then - local p = path.join(dirname,f) - local mode = attrib(p,'mode') - if mode=='directory' then - append(dirs,f) - else - append(files,f) - end - end - end - return makelist(dirs), makelist(files) -end - - ---- return an iterator which walks through a directory tree starting at root. --- The iterator returns (root,dirs,files) --- Note that dirs and files are lists of names (i.e. you must say path.join(root,d) --- to get the actual full path) --- If bottom_up is false (or not present), then the entries at the current level are returned --- before we go deeper. This means that you can modify the returned list of directories before --- continuing. --- This is a clone of os.walk from the Python libraries. --- @string root A starting directory --- @bool bottom_up False if we start listing entries immediately. --- @bool follow_links follow symbolic links --- @return an iterator returning root,dirs,files --- @raise root must be a directory -function dir.walk(root,bottom_up,follow_links) - assert_dir(1,root) - local attrib - if path.is_windows or not follow_links then - attrib = path.attrib - else - attrib = path.link_attrib - end - - local to_scan = { root } - local to_return = {} - local iter = function() - while #to_scan > 0 do - local current_root = table.remove(to_scan) - local dirs,files = _dirfiles(current_root, attrib) - for _, d in ipairs(dirs) do - table.insert(to_scan, current_root..path.sep..d) - end - if not bottom_up then - return current_root, dirs, files - else - table.insert(to_return, { current_root, dirs, files }) - end - end - if #to_return > 0 then - return utils.unpack(table.remove(to_return)) - end - end - - return iter -end - ---- remove a whole directory tree. --- Symlinks in the tree will be deleted without following them. --- @string fullpath A directory path (must be an actual directory, not a symlink) --- @return true or nil --- @return error if failed --- @raise fullpath must be a string -function dir.rmtree(fullpath) - assert_dir(1,fullpath) - if path.islink(fullpath) then return false,'will not follow symlink' end - for root,dirs,files in dir.walk(fullpath,true) do - if path.islink(root) then - -- sub dir is a link, remove link, do not follow - if is_windows then - -- Windows requires using "rmdir". Deleting the link like a file - -- will instead delete all files from the target directory!! - local res, err = rmdir(root) - if not res then return nil,err .. ": " .. root end - else - local res, err = remove(root) - if not res then return nil,err .. ": " .. root end - end - else - for i,f in ipairs(files) do - local res, err = remove(path.join(root,f)) - if not res then return nil,err .. ": " .. path.join(root,f) end - end - local res, err = rmdir(root) - if not res then return nil,err .. ": " .. root end - end - end - return true -end - - -do - local dirpat - if path.is_windows then - dirpat = '(.+)\\[^\\]+$' - else - dirpat = '(.+)/[^/]+$' - end - - local _makepath - function _makepath(p) - -- windows root drive case - if p:find '^%a:[\\]*$' then - return true - end - if not path.isdir(p) then - local subp = p:match(dirpat) - if subp then - local ok, err = _makepath(subp) - if not ok then return nil, err end - end - return mkdir(p) - else - return true - end - end - - --- create a directory path. - -- This will create subdirectories as necessary! - -- @string p A directory path - -- @return true on success, nil + errormsg on failure - -- @raise failure to create - function dir.makepath (p) - assert_string(1,p) - if path.is_windows then - p = p:gsub("/", "\\") - end - return _makepath(path.abspath(p)) - end -end - ---- clone a directory tree. Will always try to create a new directory structure --- if necessary. --- @string path1 the base path of the source tree --- @string path2 the new base path for the destination --- @func file_fun an optional function to apply on all files --- @bool verbose an optional boolean to control the verbosity of the output. --- It can also be a logging function that behaves like print() --- @return true, or nil --- @return error message, or list of failed directory creations --- @return list of failed file operations --- @raise path1 and path2 must be strings --- @usage clonetree('.','../backup',copyfile) -function dir.clonetree (path1,path2,file_fun,verbose) - assert_string(1,path1) - assert_string(2,path2) - if verbose == true then verbose = print end - local abspath,normcase,isdir,join = path.abspath,path.normcase,path.isdir,path.join - local faildirs,failfiles = {},{} - if not isdir(path1) then return raise 'source is not a valid directory' end - path1 = abspath(normcase(path1)) - path2 = abspath(normcase(path2)) - if verbose then verbose('normalized:',path1,path2) end - -- particularly NB that the new path isn't fully contained in the old path - if path1 == path2 then return raise "paths are the same" end - local _,i2 = path2:find(path1,1,true) - if i2 == #path1 and path2:sub(i2+1,i2+1) == path.sep then - return raise 'destination is a subdirectory of the source' - end - local cp = path.common_prefix (path1,path2) - local idx = #cp - if idx == 0 then -- no common path, but watch out for Windows paths! - if path1:sub(2,2) == ':' then idx = 3 end - end - for root,dirs,files in dir.walk(path1) do - local opath = path2..root:sub(idx) - if verbose then verbose('paths:',opath,root) end - if not isdir(opath) then - local ret = dir.makepath(opath) - if not ret then append(faildirs,opath) end - if verbose then verbose('creating:',opath,ret) end - end - if file_fun then - for i,f in ipairs(files) do - local p1 = join(root,f) - local p2 = join(opath,f) - local ret = file_fun(p1,p2) - if not ret then append(failfiles,p2) end - if verbose then - verbose('files:',p1,p2,ret) - end - end - end - end - return true,faildirs,failfiles -end - - --- each entry of the stack is an array with three items: --- 1. the name of the directory --- 2. the lfs iterator function --- 3. the lfs iterator userdata -local function treeiter(iterstack) - local diriter = iterstack[#iterstack] - if not diriter then - return -- done - end - - local dirname = diriter[1] - local entry = diriter[2](diriter[3]) - if not entry then - table.remove(iterstack) - return treeiter(iterstack) -- tail-call to try next - end - - if entry ~= "." and entry ~= ".." then - entry = dirname .. sep .. entry - if exists(entry) then -- Just in case a symlink is broken. - local is_dir = isdir(entry) - if is_dir then - table.insert(iterstack, { entry, ldir(entry) }) - end - return entry, is_dir - end - end - - return treeiter(iterstack) -- tail-call to try next -end - - ---- return an iterator over all entries in a directory tree --- @string d a directory --- @return an iterator giving pathname and mode (true for dir, false otherwise) --- @raise d must be a non-empty string -function dir.dirtree( d ) - assert( d and d ~= "", "directory parameter is missing or empty" ) - - local last = sub ( d, -1 ) - if last == sep or last == '/' then - d = sub( d, 1, -2 ) - end - - local iterstack = { {d, ldir(d)} } - - return treeiter, iterstack -end - - ---- Recursively returns all the file starting at 'path'. It can optionally take a shell pattern and --- only returns files that match 'shell_pattern'. If a pattern is given it will do a case insensitive search. --- @string[opt='.'] start_path A directory. --- @string[opt='*'] shell_pattern A shell pattern (see `fnmatch`). --- @treturn List(string) containing all the files found recursively starting at 'path' and filtered by 'shell_pattern'. --- @raise start_path must be a directory -function dir.getallfiles( start_path, shell_pattern ) - start_path = start_path or '.' - assert_dir(1,start_path) - shell_pattern = shell_pattern or "*" - - local files = {} - local normcase = path.normcase - for filename, mode in dir.dirtree( start_path ) do - if not mode then - local mask = filemask( shell_pattern ) - if normcase(filename):find( mask ) then - files[#files + 1] = filename - end - end - end - - return makelist(files) -end - -return dir diff --git a/src/pl/file.lua b/src/pl/file.lua deleted file mode 100644 index b8058c4..0000000 --- a/src/pl/file.lua +++ /dev/null @@ -1,55 +0,0 @@ ---- File manipulation functions: reading, writing, moving and copying. --- --- This module wraps a number of functions from other modules into a --- file related module for convenience. --- --- Dependencies: `pl.utils`, `pl.dir`, `pl.path` --- @module pl.file -local os = os -local utils = require 'pl.utils' -local dir = require 'pl.dir' -local path = require 'pl.path' - -local file = {} - ---- return the contents of a file as a string. --- This function is a copy of `utils.readfile`. --- @function file.read -file.read = utils.readfile - ---- write a string to a file. --- This function is a copy of `utils.writefile`. --- @function file.write -file.write = utils.writefile - ---- copy a file. --- This function is a copy of `dir.copyfile`. --- @function file.copy -file.copy = dir.copyfile - ---- move a file. --- This function is a copy of `dir.movefile`. --- @function file.move -file.move = dir.movefile - ---- Return the time of last access as the number of seconds since the epoch. --- This function is a copy of `path.getatime`. --- @function file.access_time -file.access_time = path.getatime - ----Return when the file was created. --- This function is a copy of `path.getctime`. --- @function file.creation_time -file.creation_time = path.getctime - ---- Return the time of last modification. --- This function is a copy of `path.getmtime`. --- @function file.modified_time -file.modified_time = path.getmtime - ---- Delete a file. --- This function is a copy of `os.remove`. --- @function file.delete -file.delete = os.remove - -return file diff --git a/src/pl/func.lua b/src/pl/func.lua deleted file mode 100644 index daac0fc..0000000 --- a/src/pl/func.lua +++ /dev/null @@ -1,393 +0,0 @@ ---- Functional helpers like composition, binding and placeholder expressions. --- Placeholder expressions are useful for short anonymous functions, and were --- inspired by the Boost Lambda library. --- --- > utils.import 'pl.func' --- > ls = List{10,20,30} --- > = ls:map(_1+1) --- {11,21,31} --- --- They can also be used to _bind_ particular arguments of a function. --- --- > p = bind(print,'start>',_0) --- > p(10,20,30) --- > start> 10 20 30 --- --- See @{07-functional.md.Creating_Functions_from_Functions|the Guide} --- --- Dependencies: `pl.utils`, `pl.tablex` --- @module pl.func -local type,setmetatable,getmetatable,rawset = type,setmetatable,getmetatable,rawset -local concat,append = table.concat,table.insert -local tostring = tostring -local utils = require 'pl.utils' -local pairs,rawget,unpack,pack = pairs,rawget,utils.unpack,utils.pack -local tablex = require 'pl.tablex' -local map = tablex.map -local _DEBUG = rawget(_G,'_DEBUG') -local assert_arg = utils.assert_arg - -local func = {} - --- metatable for Placeholder Expressions (PE) -local _PEMT = {} - -local function P (t) - setmetatable(t,_PEMT) - return t -end - -func.PE = P - -local function isPE (obj) - return getmetatable(obj) == _PEMT -end - -func.isPE = isPE - --- construct a placeholder variable (e.g _1 and _2) -local function PH (idx) - return P {op='X',repr='_'..idx, index=idx} -end - --- construct a constant placeholder variable (e.g _C1 and _C2) -local function CPH (idx) - return P {op='X',repr='_C'..idx, index=idx} -end - -func._1,func._2,func._3,func._4,func._5 = PH(1),PH(2),PH(3),PH(4),PH(5) -func._0 = P{op='X',repr='...',index=0} - -function func.Var (name) - local ls = utils.split(name,'[%s,]+') - local res = {} - for i = 1, #ls do - append(res,P{op='X',repr=ls[i],index=0}) - end - return unpack(res) -end - -function func._ (value) - return P{op='X',repr=value,index='wrap'} -end - -local repr - -func.Nil = func.Var 'nil' - -function _PEMT.__index(obj,key) - return P{op='[]',obj,key} -end - -function _PEMT.__call(fun,...) - return P{op='()',fun,...} -end - -function _PEMT.__tostring (e) - return repr(e) -end - -function _PEMT.__unm(arg) - return P{op='unm',arg} -end - -function func.Not (arg) - return P{op='not',arg} -end - -function func.Len (arg) - return P{op='#',arg} -end - - -local function binreg(context,t) - for name,op in pairs(t) do - rawset(context,name,function(x,y) - return P{op=op,x,y} - end) - end -end - -local function import_name (name,fun,context) - rawset(context,name,function(...) - return P{op='()',fun,...} - end) -end - -local imported_functions = {} - -local function is_global_table (n) - return type(_G[n]) == 'table' -end - ---- wrap a table of functions. This makes them available for use in --- placeholder expressions. --- @string tname a table name --- @tab context context to put results, defaults to environment of caller -function func.import(tname,context) - assert_arg(1,tname,'string',is_global_table,'arg# 1: not a name of a global table') - local t = _G[tname] - context = context or _G - for name,fun in pairs(t) do - import_name(name,fun,context) - imported_functions[fun] = name - end -end - ---- register a function for use in placeholder expressions. --- @func fun a function --- @string[opt] name an optional name --- @return a placeholder functiond -function func.register (fun,name) - assert_arg(1,fun,'function') - if name then - assert_arg(2,name,'string') - imported_functions[fun] = name - end - return function(...) - return P{op='()',fun,...} - end -end - -function func.lookup_imported_name (fun) - return imported_functions[fun] -end - -local function _arg(...) return ... end - -function func.Args (...) - return P{op='()',_arg,...} -end - --- binary operators with their precedences (see Lua manual) --- precedences might be incremented by one before use depending on --- left- or right-associativity, space them out -local binary_operators = { - ['or'] = 0, - ['and'] = 2, - ['=='] = 4, ['~='] = 4, ['<'] = 4, ['>'] = 4, ['<='] = 4, ['>='] = 4, - ['..'] = 6, - ['+'] = 8, ['-'] = 8, - ['*'] = 10, ['/'] = 10, ['%'] = 10, - ['^'] = 14 -} - --- unary operators with their precedences -local unary_operators = { - ['not'] = 12, ['#'] = 12, ['unm'] = 12 -} - --- comparisons (as prefix functions) -binreg (func,{And='and',Or='or',Eq='==',Lt='<',Gt='>',Le='<=',Ge='>='}) - --- standard binary operators (as metamethods) -binreg (_PEMT,{__add='+',__sub='-',__mul='*',__div='/',__mod='%',__pow='^',__concat='..'}) - -binreg (_PEMT,{__eq='=='}) - ---- all elements of a table except the first. --- @tab ls a list-like table. -function func.tail (ls) - assert_arg(1,ls,'table') - local res = {} - for i = 2,#ls do - append(res,ls[i]) - end - return res -end - ---- create a string representation of a placeholder expression. --- @param e a placeholder expression --- @param lastpred not used -function repr (e,lastpred) - local tail = func.tail - if isPE(e) then - local pred = binary_operators[e.op] or unary_operators[e.op] - if pred then - -- binary or unary operator - local s - if binary_operators[e.op] then - local left_pred = pred - local right_pred = pred - if e.op == '..' or e.op == '^' then - left_pred = left_pred + 1 - else - right_pred = right_pred + 1 - end - local left_arg = repr(e[1], left_pred) - local right_arg = repr(e[2], right_pred) - s = left_arg..' '..e.op..' '..right_arg - else - local op = e.op == 'unm' and '-' or e.op - s = op..' '..repr(e[1], pred) - end - if lastpred and lastpred > pred then - s = '('..s..')' - end - return s - else -- either postfix, or a placeholder - local ls = map(repr,e) - if e.op == '[]' then - return ls[1]..'['..ls[2]..']' - elseif e.op == '()' then - local fn - if ls[1] ~= nil then -- was _args, undeclared! - fn = ls[1] - else - fn = '' - end - return fn..'('..concat(tail(ls),',')..')' - else - return e.repr - end - end - elseif type(e) == 'string' then - return '"'..e..'"' - elseif type(e) == 'function' then - local name = func.lookup_imported_name(e) - if name then return name else return tostring(e) end - else - return tostring(e) --should not really get here! - end -end -func.repr = repr - --- collect all the non-PE values in this PE into vlist, and replace each occurence --- with a constant PH (_C1, etc). Return the maximum placeholder index found. -local collect_values -function collect_values (e,vlist) - if isPE(e) then - if e.op ~= 'X' then - local m = 0 - for i = 1,#e do - local subx = e[i] - local pe = isPE(subx) - if pe then - if subx.op == 'X' and subx.index == 'wrap' then - subx = subx.repr - pe = false - else - m = math.max(m,collect_values(subx,vlist)) - end - end - if not pe then - append(vlist,subx) - e[i] = CPH(#vlist) - end - end - return m - else -- was a placeholder, it has an index... - return e.index - end - else -- plain value has no placeholder dependence - return 0 - end -end -func.collect_values = collect_values - ---- instantiate a PE into an actual function. First we find the largest placeholder used, --- e.g. _2; from this a list of the formal parameters can be build. Then we collect and replace --- any non-PE values from the PE, and build up a constant binding list. --- Finally, the expression can be compiled, and e.__PE_function is set. --- @param e a placeholder expression --- @return a function -function func.instantiate (e) - local consts,values,parms = {},{},{} - local rep, err, fun - local n = func.collect_values(e,values) - for i = 1,#values do - append(consts,'_C'..i) - if _DEBUG then print(i,values[i]) end - end - for i =1,n do - append(parms,'_'..i) - end - consts = concat(consts,',') - parms = concat(parms,',') - rep = repr(e) - local fstr = ('return function(%s) return function(%s) return %s end end'):format(consts,parms,rep) - if _DEBUG then print(fstr) end - fun,err = utils.load(fstr,'fun') - if not fun then return nil,err end - fun = fun() -- get wrapper - fun = fun(unpack(values)) -- call wrapper (values could be empty) - e.__PE_function = fun - return fun -end - ---- instantiate a PE unless it has already been done. --- @param e a placeholder expression --- @return the function -function func.I(e) - if rawget(e,'__PE_function') then - return e.__PE_function - else return func.instantiate(e) - end -end - -utils.add_function_factory(_PEMT,func.I) - ---- bind the first parameter of the function to a value. --- @function func.bind1 --- @func fn a function of one or more arguments --- @param p a value --- @return a function of one less argument --- @usage (bind1(math.max,10))(20) == math.max(10,20) -func.bind1 = utils.bind1 -func.curry = func.bind1 - ---- create a function which chains two functions. --- @func f a function of at least one argument --- @func g a function of at least one argument --- @return a function --- @usage printf = compose(io.write,string.format) -function func.compose (f,g) - return function(...) return f(g(...)) end -end - ---- bind the arguments of a function to given values. --- `bind(fn,v,_2)` is equivalent to `bind1(fn,v)`. --- @func fn a function of at least one argument --- @param ... values or placeholder variables --- @return a function --- @usage (bind(f,_1,a))(b) == f(a,b) --- @usage (bind(f,_2,_1))(a,b) == f(b,a) -function func.bind(fn,...) - local args = pack(...) - local holders,parms,bvalues,values = {},{},{'fn'},{} - local nv,maxplace,varargs = 1,0,false - for i = 1,args.n do - local a = args[i] - if isPE(a) and a.op == 'X' then - append(holders,a.repr) - maxplace = math.max(maxplace,a.index) - if a.index == 0 then varargs = true end - else - local v = '_v'..nv - append(bvalues,v) - append(holders,v) - append(values,a) - nv = nv + 1 - end - end - for np = 1,maxplace do - append(parms,'_'..np) - end - if varargs then append(parms,'...') end - bvalues = concat(bvalues,',') - parms = concat(parms,',') - holders = concat(holders,',') - local fstr = ([[ -return function (%s) - return function(%s) return fn(%s) end -end -]]):format(bvalues,parms,holders) - if _DEBUG then print(fstr) end - local res = utils.load(fstr) - res = res() - return res(fn,unpack(values)) -end - -return func - - diff --git a/src/pl/import_into.lua b/src/pl/import_into.lua deleted file mode 100644 index 46aaa3e..0000000 --- a/src/pl/import_into.lua +++ /dev/null @@ -1,91 +0,0 @@ --------------- --- PL loader, for loading all PL libraries, only on demand. --- Whenever a module is implicitly accesssed, the table will have the module automatically injected. --- (e.g. `_ENV.tablex`) --- then that module is dynamically loaded. The submodules are all brought into --- the table that is provided as the argument, or returned in a new table. --- If a table is provided, that table's metatable is clobbered, but the values are not. --- This module returns a single function, which is passed the environment. --- If this is `true`, then return a 'shadow table' as the module --- See @{01-introduction.md.To_Inject_or_not_to_Inject_|the Guide} - --- @module pl.import_into - -return function(env) - local mod - if env == true then - mod = {} - env = {} - end - local env = env or {} - - local modules = { - utils = true,path=true,dir=true,tablex=true,stringio=true,sip=true, - input=true,seq=true,lexer=true,stringx=true, - config=true,pretty=true,data=true,func=true,text=true, - operator=true,lapp=true,array2d=true, - comprehension=true,xml=true,types=true, - test = true, app = true, file = true, class = true, - luabalanced = true, permute = true, template = true, - url = true, compat = true, - -- classes -- - List = true, Map = true, Set = true, - OrderedMap = true, MultiMap = true, Date = true, - } - rawset(env,'utils',require 'pl.utils') - - for name,klass in pairs(env.utils.stdmt) do - klass.__index = function(t,key) - return require ('pl.'..name)[key] - end; - end - - -- ensure that we play nice with libraries that also attach a metatable - -- to the global table; always forward to a custom __index if we don't - -- match - - local _hook,_prev_index - local gmt = {} - local prevenvmt = getmetatable(env) - if prevenvmt then - _prev_index = prevenvmt.__index - if prevenvmt.__newindex then - gmt.__newindex = prevenvmt.__newindex - end - end - - function gmt.hook(handler) - _hook = handler - end - - function gmt.__index(t,name) - local found = modules[name] - -- either true, or the name of the module containing this class. - -- either way, we load the required module and make it globally available. - if found then - -- e..g pretty.dump causes pl.pretty to become available as 'pretty' - rawset(env,name,require('pl.'..name)) - return env[name] - else - local res - if _hook then - res = _hook(t,name) - if res then return res end - end - if _prev_index then - return _prev_index(t,name) - end - end - end - - if mod then - function gmt.__newindex(t,name,value) - mod[name] = value - rawset(t,name,value) - end - end - - setmetatable(env,gmt) - - return env,mod or env -end diff --git a/src/pl/init.lua b/src/pl/init.lua deleted file mode 100644 index c27a890..0000000 --- a/src/pl/init.lua +++ /dev/null @@ -1,11 +0,0 @@ --------------- --- Entry point for loading all PL libraries only on demand, into the global space. --- Requiring 'pl' means that whenever a module is implicitly accesssed --- (e.g. `utils.split`) --- then that module is dynamically loaded. The submodules are all brought into --- the global space. ---Updated to use @{pl.import_into} --- @module pl -require'pl.import_into'(_G) - -if rawget(_G,'PENLIGHT_STRICT') then require 'pl.strict' end diff --git a/src/pl/input.lua b/src/pl/input.lua deleted file mode 100644 index f81de87..0000000 --- a/src/pl/input.lua +++ /dev/null @@ -1,171 +0,0 @@ ---- Iterators for extracting words or numbers from an input source. --- --- require 'pl' --- local total,n = seq.sum(input.numbers()) --- print('average',total/n) --- --- _source_ is defined as a string or a file-like object (i.e. has a read() method which returns the next line) --- --- See @{06-data.md.Reading_Unstructured_Text_Data|here} --- --- Dependencies: `pl.utils` --- @module pl.input -local strfind = string.find -local strsub = string.sub -local strmatch = string.match -local utils = require 'pl.utils' -local unpack = utils.unpack -local pairs,type,tonumber = pairs,type,tonumber -local patterns = utils.patterns -local io = io - -local input = {} - ---- create an iterator over all tokens. --- based on allwords from PiL, 7.1 --- @func getter any function that returns a line of text --- @string pattern --- @string[opt] fn Optionally can pass a function to process each token as it's found. --- @return an iterator -function input.alltokens (getter,pattern,fn) - local line = getter() -- current line - local pos = 1 -- current position in the line - return function () -- iterator function - while line do -- repeat while there are lines - local s, e = strfind(line, pattern, pos) - if s then -- found a word? - pos = e + 1 -- next position is after this token - local res = strsub(line, s, e) -- return the token - if fn then res = fn(res) end - return res - else - line = getter() -- token not found; try next line - pos = 1 -- restart from first position - end - end - return nil -- no more lines: end of traversal - end -end -local alltokens = input.alltokens - --- question: shd this _split_ a string containing line feeds? - ---- create a function which grabs the next value from a source. If the source is a string, then the getter --- will return the string and thereafter return nil. If not specified then the source is assumed to be stdin. --- @param f a string or a file-like object (i.e. has a read() method which returns the next line) --- @return a getter function -function input.create_getter(f) - if f then - if type(f) == 'string' then - local ls = utils.split(f,'\n') - local i,n = 0,#ls - return function() - i = i + 1 - if i > n then return nil end - return ls[i] - end - else - -- anything that supports the read() method! - if not f.read then error('not a file-like object') end - return function() return f:read() end - end - else - return io.read -- i.e. just read from stdin - end -end - ---- generate a sequence of numbers from a source. --- @param f A source --- @return An iterator -function input.numbers(f) - return alltokens(input.create_getter(f), - '('..patterns.FLOAT..')',tonumber) -end - ---- generate a sequence of words from a source. --- @param f A source --- @return An iterator -function input.words(f) - return alltokens(input.create_getter(f),"%w+") -end - -local function apply_tonumber (no_fail,...) - local args = {...} - for i = 1,#args do - local n = tonumber(args[i]) - if n == nil then - if not no_fail then return nil,args[i] end - else - args[i] = n - end - end - return args -end - ---- parse an input source into fields. --- By default, will fail if it cannot convert a field to a number. --- @param ids a list of field indices, or a maximum field index --- @string delim delimiter to parse fields (default space) --- @param f a source @see create_getter --- @tab opts option table, `{no_fail=true}` --- @return an iterator with the field values --- @usage for x,y in fields {2,3} do print(x,y) end -- 2nd and 3rd fields from stdin -function input.fields (ids,delim,f,opts) - local sep - local s - local getter = input.create_getter(f) - local no_fail = opts and opts.no_fail - local no_convert = opts and opts.no_convert - if not delim or delim == ' ' then - delim = '%s' - sep = '%s+' - s = '%s*' - else - sep = delim - s = '' - end - local max_id = 0 - if type(ids) == 'table' then - for i,id in pairs(ids) do - if id > max_id then max_id = id end - end - else - max_id = ids - ids = {} - for i = 1,max_id do ids[#ids+1] = i end - end - local pat = '[^'..delim..']*' - local k = 1 - for i = 1,max_id do - if ids[k] == i then - k = k + 1 - s = s..'('..pat..')' - else - s = s..pat - end - if i < max_id then - s = s..sep - end - end - local linecount = 1 - return function() - local line,results,err - repeat - line = getter() - linecount = linecount + 1 - if not line then return nil end - if no_convert then - results = {strmatch(line,s)} - else - results,err = apply_tonumber(no_fail,strmatch(line,s)) - if not results then - utils.quit("line "..(linecount-1)..": cannot convert '"..err.."' to number") - end - end - until #results > 0 - return unpack(results) - end -end - -return input - diff --git a/src/pl/lapp.lua b/src/pl/lapp.lua deleted file mode 100644 index 6c54592..0000000 --- a/src/pl/lapp.lua +++ /dev/null @@ -1,451 +0,0 @@ ---- Simple command-line parsing using human-readable specification. --- Supports GNU-style parameters. --- --- lapp = require 'pl.lapp' --- local args = lapp [[ --- Does some calculations --- -o,--offset (default 0.0) Offset to add to scaled number --- -s,--scale (number) Scaling factor --- (number) Number to be scaled --- ]] --- --- print(args.offset + args.scale * args.number) --- --- Lines beginning with `'-'` are flags; there may be a short and a long name; --- lines beginning with `''` are arguments. Anything in parens after --- the flag/argument is either a default, a type name or a range constraint. --- --- See @{08-additional.md.Command_line_Programs_with_Lapp|the Guide} --- --- Dependencies: `pl.sip` --- @module pl.lapp - -local status,sip = pcall(require,'pl.sip') -if not status then - sip = require 'sip' -end -local match = sip.match_at_start -local append,tinsert = table.insert,table.insert - -sip.custom_pattern('X','(%a[%w_%-]*)') - -local function lines(s) return s:gmatch('([^\n]*)\n') end -local function lstrip(str) return str:gsub('^%s+','') end -local function strip(str) return lstrip(str):gsub('%s+$','') end -local function at(s,k) return s:sub(k,k) end - -local lapp = {} - -local open_files,parms,aliases,parmlist,usage,script - -lapp.callback = false -- keep Strict happy - -local filetypes = { - stdin = {io.stdin,'file-in'}, stdout = {io.stdout,'file-out'}, - stderr = {io.stderr,'file-out'} -} - ---- controls whether to dump usage on error. --- Defaults to true -lapp.show_usage_error = true - ---- quit this script immediately. --- @string msg optional message --- @bool no_usage suppress 'usage' display -function lapp.quit(msg,no_usage) - if no_usage == 'throw' then - error(msg) - end - if msg then - io.stderr:write(msg..'\n\n') - end - if not no_usage then - io.stderr:write(usage) - end - os.exit(1) -end - ---- print an error to stderr and quit. --- @string msg a message --- @bool no_usage suppress 'usage' display -function lapp.error(msg,no_usage) - if not lapp.show_usage_error then - no_usage = true - elseif lapp.show_usage_error == 'throw' then - no_usage = 'throw' - end - lapp.quit(script..': '..msg,no_usage) -end - ---- open a file. --- This will quit on error, and keep a list of file objects for later cleanup. --- @string file filename --- @string[opt] opt same as second parameter of `io.open` -function lapp.open (file,opt) - local val,err = io.open(file,opt) - if not val then lapp.error(err,true) end - append(open_files,val) - return val -end - ---- quit if the condition is false. --- @bool condn a condition --- @string msg message text -function lapp.assert(condn,msg) - if not condn then - lapp.error(msg) - end -end - -local function range_check(x,min,max,parm) - lapp.assert(min <= x and max >= x,parm..' out of range') -end - -local function xtonumber(s) - local val = tonumber(s) - if not val then lapp.error("unable to convert to number: "..s) end - return val -end - -local types = {} - -local builtin_types = {string=true,number=true,['file-in']='file',['file-out']='file',boolean=true} - -local function convert_parameter(ps,val) - if ps.converter then - val = ps.converter(val) - end - if ps.type == 'number' then - val = xtonumber(val) - elseif builtin_types[ps.type] == 'file' then - val = lapp.open(val,(ps.type == 'file-in' and 'r') or 'w' ) - elseif ps.type == 'boolean' then - return val - end - if ps.constraint then - ps.constraint(val) - end - return val -end - ---- add a new type to Lapp. These appear in parens after the value like --- a range constraint, e.g. ' (integer) Process PID' --- @string name name of type --- @param converter either a function to convert values, or a Lua type name. --- @func[opt] constraint optional function to verify values, should use lapp.error --- if failed. -function lapp.add_type (name,converter,constraint) - types[name] = {converter=converter,constraint=constraint} -end - -local function force_short(short) - lapp.assert(#short==1,short..": short parameters should be one character") -end - --- deducing type of variable from default value; -local function process_default (sval,vtype) - local val, success - if not vtype or vtype == 'number' then - val = tonumber(sval) - end - if val then -- we have a number! - return val,'number' - elseif filetypes[sval] then - local ft = filetypes[sval] - return ft[1],ft[2] - else - if sval == 'true' and not vtype then - return true, 'boolean' - end - if sval:match '^["\']' then sval = sval:sub(2,-2) end - - local ps = types[vtype] or {} - ps.type = vtype - - local show_usage_error = lapp.show_usage_error - lapp.show_usage_error = "throw" - success, val = pcall(convert_parameter, ps, sval) - lapp.show_usage_error = show_usage_error - if success then - return val, vtype or 'string' - end - - return sval,vtype or 'string' - end -end - ---- process a Lapp options string. --- Usually called as `lapp()`. --- @string str the options text --- @tparam {string} args a table of arguments (default is `_G.arg`) --- @return a table with parameter-value pairs -function lapp.process_options_string(str,args) - local results = {} - local varargs - local arg = args or _G.arg - open_files = {} - parms = {} - aliases = {} - parmlist = {} - - local function check_varargs(s) - local res,cnt = s:gsub('^%.%.%.%s*','') - return res, (cnt > 0) - end - - local function set_result(ps,parm,val) - parm = type(parm) == "string" and parm:gsub("%W", "_") or parm -- so foo-bar becomes foo_bar in Lua - if not ps.varargs then - results[parm] = val - else - if not results[parm] then - results[parm] = { val } - else - append(results[parm],val) - end - end - end - - usage = str - - for _,a in ipairs(arg) do - if a == "-h" or a == "--help" then - return lapp.quit() - end - end - - - for line in lines(str) do - local res = {} - local optparm,defval,vtype,constraint,rest - line = lstrip(line) - local function check(str) - return match(str,line,res) - end - - -- flags: either '-', '-,--' or '--' - if check '-$v{short}, --$o{long} $' or check '-$v{short} $' or check '--$o{long} $' then - if res.long then - optparm = res.long:gsub('[^%w%-]','_') -- I'm not sure the $o pattern will let anything else through? - if #res.rest == 1 then optparm = optparm .. res.rest end - if res.short then aliases[res.short] = optparm end - else - optparm = res.short - end - if res.short and not lapp.slack then force_short(res.short) end - res.rest, varargs = check_varargs(res.rest) - elseif check '$<{name} $' then -- is it ? - -- so becomes input_file ... - optparm,rest = res.name:match '([^%.]+)(.*)' - optparm = optparm:gsub('%A','_') - varargs = rest == '...' - append(parmlist,optparm) - end - -- this is not a pure doc line and specifies the flag/parameter type - if res.rest then - line = res.rest - res = {} - local optional - -- do we have ([optional] [] [default ])? - if match('$({def} $',line,res) or match('$({def}',line,res) then - local typespec = strip(res.def) - local ftype, rest = typespec:match('^(%S+)(.*)$') - rest = strip(rest) - if ftype == 'optional' then - ftype, rest = rest:match('^(%S+)(.*)$') - rest = strip(rest) - optional = true - end - local default - if ftype == 'default' then - default = true - if rest == '' then lapp.error("value must follow default") end - else -- a type specification - if match('$f{min}..$f{max}',ftype,res) then - -- a numerical range like 1..10 - local min,max = res.min,res.max - vtype = 'number' - constraint = function(x) - range_check(x,min,max,optparm) - end - elseif not ftype:match '|' then -- plain type - vtype = ftype - else - -- 'enum' type is a string which must belong to - -- one of several distinct values - local enums = ftype - local enump = '|' .. enums .. '|' - vtype = 'string' - constraint = function(s) - lapp.assert(enump:find('|'..s..'|', 1, true), - "value '"..s.."' not in "..enums - ) - end - end - end - res.rest = rest - typespec = res.rest - -- optional 'default value' clause. Type is inferred as - -- 'string' or 'number' if there's no explicit type - if default or match('default $r{rest}',typespec,res) then - defval,vtype = process_default(res.rest,vtype) - end - else -- must be a plain flag, no extra parameter required - defval = false - vtype = 'boolean' - end - local ps = { - type = vtype, - defval = defval, - required = defval == nil and not optional, - comment = res.rest or optparm, - constraint = constraint, - varargs = varargs - } - varargs = nil - if types[vtype] then - local converter = types[vtype].converter - if type(converter) == 'string' then - ps.type = converter - else - ps.converter = converter - end - ps.constraint = types[vtype].constraint - elseif not builtin_types[vtype] and vtype then - lapp.error(vtype.." is unknown type") - end - parms[optparm] = ps - end - end - -- cool, we have our parms, let's parse the command line args - local iparm = 1 - local iextra = 1 - local i = 1 - local parm,ps,val - local end_of_flags = false - - local function check_parm (parm) - local eqi = parm:find '[=:]' - if eqi then - tinsert(arg,i+1,parm:sub(eqi+1)) - parm = parm:sub(1,eqi-1) - end - return parm,eqi - end - - local function is_flag (parm) - return parms[aliases[parm] or parm] - end - - while i <= #arg do - local theArg = arg[i] - local res = {} - -- after '--' we don't parse args and they end up in - -- the array part of the result (args[1] etc) - if theArg == '--' then - end_of_flags = true - iparm = #parmlist + 1 - i = i + 1 - theArg = arg[i] - if not theArg then - break - end - end - -- look for a flag, - or -- - if not end_of_flags and (match('--$S{long}',theArg,res) or match('-$S{short}',theArg,res)) then - if res.long then -- long option - parm = check_parm(res.long) - elseif #res.short == 1 or is_flag(res.short) then - parm = res.short - else - local parmstr,eq = check_parm(res.short) - if not eq then - parm = at(parmstr,1) - local flag = is_flag(parm) - if flag and flag.type ~= 'boolean' then - --if isdigit(at(parmstr,2)) then - -- a short option followed by a digit is an exception (for AW;)) - -- push ahead into the arg array - tinsert(arg,i+1,parmstr:sub(2)) - else - -- push multiple flags into the arg array! - for k = 2,#parmstr do - tinsert(arg,i+k-1,'-'..at(parmstr,k)) - end - end - else - parm = parmstr - end - end - if aliases[parm] then parm = aliases[parm] end - if not parms[parm] and (parm == 'h' or parm == 'help') then - lapp.quit() - end - else -- a parameter - parm = parmlist[iparm] - if not parm then - -- extra unnamed parameters are indexed starting at 1 - parm = iextra - ps = { type = 'string' } - parms[parm] = ps - iextra = iextra + 1 - else - ps = parms[parm] - end - if not ps.varargs then - iparm = iparm + 1 - end - val = theArg - end - ps = parms[parm] - if not ps then lapp.error("unrecognized parameter: "..parm) end - if ps.type ~= 'boolean' then -- we need a value! This should follow - if not val then - i = i + 1 - val = arg[i] - theArg = val - end - lapp.assert(val,parm.." was expecting a value") - else -- toggle boolean flags (usually false -> true) - val = not ps.defval - end - ps.used = true - val = convert_parameter(ps,val) - set_result(ps,parm,val) - if builtin_types[ps.type] == 'file' then - set_result(ps,parm..'_name',theArg) - end - if lapp.callback then - lapp.callback(parm,theArg,res) - end - i = i + 1 - val = nil - end - -- check unused parms, set defaults and check if any required parameters were missed - for parm,ps in pairs(parms) do - if not ps.used then - if ps.required then lapp.error("missing required parameter: "..parm) end - set_result(ps,parm,ps.defval) - end - end - return results -end - -if arg then - script = arg[0] - script = script or rawget(_G,"LAPP_SCRIPT") or "unknown" - -- strip dir and extension to get current script name - script = script:gsub('.+[\\/]',''):gsub('%.%a+$','') -else - script = "inter" -end - - -setmetatable(lapp, { - __call = function(tbl,str,args) return lapp.process_options_string(str,args) end, -}) - - -return lapp - - diff --git a/src/pl/lexer.lua b/src/pl/lexer.lua deleted file mode 100644 index 9219716..0000000 --- a/src/pl/lexer.lua +++ /dev/null @@ -1,515 +0,0 @@ ---- Lexical scanner for creating a sequence of tokens from text. --- `lexer.scan(s)` returns an iterator over all tokens found in the --- string `s`. This iterator returns two values, a token type string --- (such as 'string' for quoted string, 'iden' for identifier) and the value of the --- token. --- --- Versions specialized for Lua and C are available; these also handle block comments --- and classify keywords as 'keyword' tokens. For example: --- --- > s = 'for i=1,n do' --- > for t,v in lexer.lua(s) do print(t,v) end --- keyword for --- iden i --- = = --- number 1 --- , , --- iden n --- keyword do --- --- See the Guide for further @{06-data.md.Lexical_Scanning|discussion} --- @module pl.lexer - -local strfind = string.find -local strsub = string.sub -local append = table.insert - - -local function assert_arg(idx,val,tp) - if type(val) ~= tp then - error("argument "..idx.." must be "..tp, 2) - end -end - -local lexer = {} - -local NUMBER1 = '^[%+%-]?%d+%.?%d*[eE][%+%-]?%d+' -local NUMBER1a = '^[%+%-]?%d*%.%d+[eE][%+%-]?%d+' -local NUMBER2 = '^[%+%-]?%d+%.?%d*' -local NUMBER2a = '^[%+%-]?%d*%.%d+' -local NUMBER3 = '^0x[%da-fA-F]+' -local NUMBER4 = '^%d+%.?%d*[eE][%+%-]?%d+' -local NUMBER4a = '^%d*%.%d+[eE][%+%-]?%d+' -local NUMBER5 = '^%d+%.?%d*' -local NUMBER5a = '^%d*%.%d+' -local IDEN = '^[%a_][%w_]*' -local WSPACE = '^%s+' -local STRING1 = "^(['\"])%1" -- empty string -local STRING2 = [[^(['"])(\*)%2%1]] -local STRING3 = [[^(['"]).-[^\](\*)%2%1]] -local CHAR1 = "^''" -local CHAR2 = [[^'(\*)%1']] -local CHAR3 = [[^'.-[^\](\*)%1']] -local PREPRO = '^#.-[^\\]\n' - -local plain_matches,lua_matches,cpp_matches,lua_keyword,cpp_keyword - -local function tdump(tok) - return tok,tok -end - -local function ndump(tok,options) - if options and options.number then - tok = tonumber(tok) - end - return "number",tok -end - --- regular strings, single or double quotes; usually we want them --- without the quotes -local function sdump(tok,options) - if options and options.string then - tok = tok:sub(2,-2) - end - return "string",tok -end - --- long Lua strings need extra work to get rid of the quotes -local function sdump_l(tok,options,findres) - if options and options.string then - local quotelen = 3 - if findres[3] then - quotelen = quotelen + findres[3]:len() - end - tok = tok:sub(quotelen, -quotelen) - if tok:sub(1, 1) == "\n" then - tok = tok:sub(2) - end - end - return "string",tok -end - -local function chdump(tok,options) - if options and options.string then - tok = tok:sub(2,-2) - end - return "char",tok -end - -local function cdump(tok) - return "comment",tok -end - -local function wsdump (tok) - return "space",tok -end - -local function pdump (tok) - return "prepro",tok -end - -local function plain_vdump(tok) - return "iden",tok -end - -local function lua_vdump(tok) - if lua_keyword[tok] then - return "keyword",tok - else - return "iden",tok - end -end - -local function cpp_vdump(tok) - if cpp_keyword[tok] then - return "keyword",tok - else - return "iden",tok - end -end - ---- create a plain token iterator from a string or file-like object. --- @tparam string|file s a string or a file-like object with `:read()` method returning lines. --- @tab matches an optional match table - array of token descriptions. --- A token is described by a `{pattern, action}` pair, where `pattern` should match --- token body and `action` is a function called when a token of described type is found. --- @tab[opt] filter a table of token types to exclude, by default `{space=true}` --- @tab[opt] options a table of options; by default, `{number=true,string=true}`, --- which means convert numbers and strip string quotes. -function lexer.scan(s,matches,filter,options) - local file = type(s) ~= 'string' and s - filter = filter or {space=true} - options = options or {number=true,string=true} - if filter then - if filter.space then filter[wsdump] = true end - if filter.comments then - filter[cdump] = true - end - end - if not matches then - if not plain_matches then - plain_matches = { - {WSPACE,wsdump}, - {NUMBER3,ndump}, - {IDEN,plain_vdump}, - {NUMBER1,ndump}, - {NUMBER1a,ndump}, - {NUMBER2,ndump}, - {NUMBER2a,ndump}, - {STRING1,sdump}, - {STRING2,sdump}, - {STRING3,sdump}, - {'^.',tdump} - } - end - matches = plain_matches - end - - local line_nr = 0 - local next_line = file and file:read() - local sz = file and 0 or #s - local idx = 1 - - local tlist_i - local tlist - - local first_hit = true - - local function iter(res) - local tp = type(res) - - if tlist then -- returning the inserted token list - local cur = tlist[tlist_i] - if cur then - tlist_i = tlist_i + 1 - return cur[1], cur[2] - else - tlist = nil - end - end - - if tp == 'string' then -- search up to some special pattern - local i1,i2 = strfind(s,res,idx) - if i1 then - local tok = strsub(s,i1,i2) - idx = i2 + 1 - return '', tok - else - idx = sz + 1 - return '', '' - end - - elseif tp == 'table' then -- insert a token list - tlist_i = 1 - tlist = res - return '', '' - - elseif tp ~= 'nil' then -- return position - return line_nr, idx - - else -- look for next token - if first_hit then - if not file then line_nr = 1 end - first_hit = false - end - - if idx > sz then - if file then - if not next_line then - return -- past the end of file, done - end - s = next_line - line_nr = line_nr + 1 - next_line = file:read() - if next_line then - s = s .. '\n' - end - idx, sz = 1, #s - else - return -- past the end of input, done - end - end - - for _,m in ipairs(matches) do - local pat = m[1] - local fun = m[2] - local findres = {strfind(s,pat,idx)} - local i1, i2 = findres[1], findres[2] - if i1 then - local tok = strsub(s,i1,i2) - idx = i2 + 1 - local ret1, ret2 - if not (filter and filter[fun]) then - lexer.finished = idx > sz - ret1, ret2 = fun(tok, options, findres) - end - if not file and tok:find("\n") then - -- Update line number. - local _, newlines = tok:gsub("\n", {}) - line_nr = line_nr + newlines - end - if ret1 then - return ret1, ret2 -- found a match - else - return iter() -- tail-call to try again - end - end - end - end - end - - return iter -end - -local function isstring (s) - return type(s) == 'string' -end - ---- insert tokens into a stream. --- @param tok a token stream --- @param a1 a string is the type, a table is a token list and --- a function is assumed to be a token-like iterator (returns type & value) --- @string a2 a string is the value -function lexer.insert (tok,a1,a2) - if not a1 then return end - local ts - if isstring(a1) and isstring(a2) then - ts = {{a1,a2}} - elseif type(a1) == 'function' then - ts = {} - for t,v in a1() do - append(ts,{t,v}) - end - else - ts = a1 - end - tok(ts) -end - ---- get everything in a stream upto a newline. --- @param tok a token stream --- @return a string -function lexer.getline (tok) - local _,v = tok('.-\n') - return v -end - ---- get current line number. --- @param tok a token stream --- @return the line number. --- if the input source is a file-like object, --- also return the column. -function lexer.lineno (tok) - return tok(0) -end - ---- get the rest of the stream. --- @param tok a token stream --- @return a string -function lexer.getrest (tok) - local _,v = tok('.+') - return v -end - ---- get the Lua keywords as a set-like table. --- So `res["and"]` etc would be `true`. --- @return a table -function lexer.get_keywords () - if not lua_keyword then - lua_keyword = { - ["and"] = true, ["break"] = true, ["do"] = true, - ["else"] = true, ["elseif"] = true, ["end"] = true, - ["false"] = true, ["for"] = true, ["function"] = true, - ["if"] = true, ["in"] = true, ["local"] = true, ["nil"] = true, - ["not"] = true, ["or"] = true, ["repeat"] = true, - ["return"] = true, ["then"] = true, ["true"] = true, - ["until"] = true, ["while"] = true - } - end - return lua_keyword -end - ---- create a Lua token iterator from a string or file-like object. --- Will return the token type and value. --- @string s the string --- @tab[opt] filter a table of token types to exclude, by default `{space=true,comments=true}` --- @tab[opt] options a table of options; by default, `{number=true,string=true}`, --- which means convert numbers and strip string quotes. -function lexer.lua(s,filter,options) - filter = filter or {space=true,comments=true} - lexer.get_keywords() - if not lua_matches then - lua_matches = { - {WSPACE,wsdump}, - {NUMBER3,ndump}, - {IDEN,lua_vdump}, - {NUMBER4,ndump}, - {NUMBER4a,ndump}, - {NUMBER5,ndump}, - {NUMBER5a,ndump}, - {STRING1,sdump}, - {STRING2,sdump}, - {STRING3,sdump}, - {'^%-%-%[(=*)%[.-%]%1%]',cdump}, - {'^%-%-.-\n',cdump}, - {'^%[(=*)%[.-%]%1%]',sdump_l}, - {'^==',tdump}, - {'^~=',tdump}, - {'^<=',tdump}, - {'^>=',tdump}, - {'^%.%.%.',tdump}, - {'^%.%.',tdump}, - {'^.',tdump} - } - end - return lexer.scan(s,lua_matches,filter,options) -end - ---- create a C/C++ token iterator from a string or file-like object. --- Will return the token type type and value. --- @string s the string --- @tab[opt] filter a table of token types to exclude, by default `{space=true,comments=true}` --- @tab[opt] options a table of options; by default, `{number=true,string=true}`, --- which means convert numbers and strip string quotes. -function lexer.cpp(s,filter,options) - filter = filter or {space=true,comments=true} - if not cpp_keyword then - cpp_keyword = { - ["class"] = true, ["break"] = true, ["do"] = true, ["sizeof"] = true, - ["else"] = true, ["continue"] = true, ["struct"] = true, - ["false"] = true, ["for"] = true, ["public"] = true, ["void"] = true, - ["private"] = true, ["protected"] = true, ["goto"] = true, - ["if"] = true, ["static"] = true, ["const"] = true, ["typedef"] = true, - ["enum"] = true, ["char"] = true, ["int"] = true, ["bool"] = true, - ["long"] = true, ["float"] = true, ["true"] = true, ["delete"] = true, - ["double"] = true, ["while"] = true, ["new"] = true, - ["namespace"] = true, ["try"] = true, ["catch"] = true, - ["switch"] = true, ["case"] = true, ["extern"] = true, - ["return"] = true,["default"] = true,['unsigned'] = true,['signed'] = true, - ["union"] = true, ["volatile"] = true, ["register"] = true,["short"] = true, - } - end - if not cpp_matches then - cpp_matches = { - {WSPACE,wsdump}, - {PREPRO,pdump}, - {NUMBER3,ndump}, - {IDEN,cpp_vdump}, - {NUMBER4,ndump}, - {NUMBER4a,ndump}, - {NUMBER5,ndump}, - {NUMBER5a,ndump}, - {CHAR1,chdump}, - {CHAR2,chdump}, - {CHAR3,chdump}, - {STRING1,sdump}, - {STRING2,sdump}, - {STRING3,sdump}, - {'^//.-\n',cdump}, - {'^/%*.-%*/',cdump}, - {'^==',tdump}, - {'^!=',tdump}, - {'^<=',tdump}, - {'^>=',tdump}, - {'^->',tdump}, - {'^&&',tdump}, - {'^||',tdump}, - {'^%+%+',tdump}, - {'^%-%-',tdump}, - {'^%+=',tdump}, - {'^%-=',tdump}, - {'^%*=',tdump}, - {'^/=',tdump}, - {'^|=',tdump}, - {'^%^=',tdump}, - {'^::',tdump}, - {'^.',tdump} - } - end - return lexer.scan(s,cpp_matches,filter,options) -end - ---- get a list of parameters separated by a delimiter from a stream. --- @param tok the token stream --- @string[opt=')'] endtoken end of list. Can be '\n' --- @string[opt=','] delim separator --- @return a list of token lists. -function lexer.get_separated_list(tok,endtoken,delim) - endtoken = endtoken or ')' - delim = delim or ',' - local parm_values = {} - local level = 1 -- used to count ( and ) - local tl = {} - local function tappend (tl,t,val) - val = val or t - append(tl,{t,val}) - end - local is_end - if endtoken == '\n' then - is_end = function(t,val) - return t == 'space' and val:find '\n' - end - else - is_end = function (t) - return t == endtoken - end - end - local token,value - while true do - token,value=tok() - if not token then return nil,'EOS' end -- end of stream is an error! - if is_end(token,value) and level == 1 then - append(parm_values,tl) - break - elseif token == '(' then - level = level + 1 - tappend(tl,'(') - elseif token == ')' then - level = level - 1 - if level == 0 then -- finished with parm list - append(parm_values,tl) - break - else - tappend(tl,')') - end - elseif token == delim and level == 1 then - append(parm_values,tl) -- a new parm - tl = {} - else - tappend(tl,token,value) - end - end - return parm_values,{token,value} -end - ---- get the next non-space token from the stream. --- @param tok the token stream. -function lexer.skipws (tok) - local t,v = tok() - while t == 'space' do - t,v = tok() - end - return t,v -end - -local skipws = lexer.skipws - ---- get the next token, which must be of the expected type. --- Throws an error if this type does not match! --- @param tok the token stream --- @string expected_type the token type --- @bool no_skip_ws whether we should skip whitespace -function lexer.expecting (tok,expected_type,no_skip_ws) - assert_arg(1,tok,'function') - assert_arg(2,expected_type,'string') - local t,v - if no_skip_ws then - t,v = tok() - else - t,v = skipws(tok) - end - if t ~= expected_type then error ("expecting "..expected_type,2) end - return v -end - -return lexer diff --git a/src/pl/luabalanced.lua b/src/pl/luabalanced.lua deleted file mode 100644 index a1f7dc6..0000000 --- a/src/pl/luabalanced.lua +++ /dev/null @@ -1,264 +0,0 @@ ---- Extract delimited Lua sequences from strings. --- Inspired by Damian Conway's Text::Balanced in Perl.
---
    ---
  • [1] Lua Wiki Page
  • ---
  • [2] http://search.cpan.org/dist/Text-Balanced/lib/Text/Balanced.pm
  • ---

---
--- local lb = require "pl.luabalanced"
--- --Extract Lua expression starting at position 4.
---  print(lb.match_expression("if x^2 + x > 5 then print(x) end", 4))
---  --> x^2 + x > 5     16
--- --Extract Lua string starting at (default) position 1.
--- print(lb.match_string([["test\"123" .. "more"]]))
--- --> "test\"123"     12
--- 
--- (c) 2008, David Manura, Licensed under the same terms as Lua (MIT license). --- @class module --- @name pl.luabalanced - -local M = {} - -local assert = assert - --- map opening brace <-> closing brace. -local ends = { ['('] = ')', ['{'] = '}', ['['] = ']' } -local begins = {}; for k,v in pairs(ends) do begins[v] = k end - - --- Match Lua string in string starting at position . --- Returns , , where is the matched --- string (or nil on no match) and is the character --- following the match (or on no match). --- Supports all Lua string syntax: "...", '...', [[...]], [=[...]=], etc. -local function match_string(s, pos) - pos = pos or 1 - local posa = pos - local c = s:sub(pos,pos) - if c == '"' or c == "'" then - pos = pos + 1 - while 1 do - pos = assert(s:find("[" .. c .. "\\]", pos), 'syntax error') - if s:sub(pos,pos) == c then - local part = s:sub(posa, pos) - return part, pos + 1 - else - pos = pos + 2 - end - end - else - local sc = s:match("^%[(=*)%[", pos) - if sc then - local _; _, pos = s:find("%]" .. sc .. "%]", pos) - assert(pos) - local part = s:sub(posa, pos) - return part, pos + 1 - else - return nil, pos - end - end -end -M.match_string = match_string - - --- Match bracketed Lua expression, e.g. "(...)", "{...}", "[...]", "[[...]]", --- [=[...]=], etc. --- Function interface is similar to match_string. -local function match_bracketed(s, pos) - pos = pos or 1 - local posa = pos - local ca = s:sub(pos,pos) - if not ends[ca] then - return nil, pos - end - local stack = {} - while 1 do - pos = s:find('[%(%{%[%)%}%]\"\']', pos) - assert(pos, 'syntax error: unbalanced') - local c = s:sub(pos,pos) - if c == '"' or c == "'" then - local part; part, pos = match_string(s, pos) - assert(part) - elseif ends[c] then -- open - local mid, posb - if c == '[' then mid, posb = s:match('^%[(=*)%[()', pos) end - if mid then - pos = s:match('%]' .. mid .. '%]()', posb) - assert(pos, 'syntax error: long string not terminated') - if #stack == 0 then - local part = s:sub(posa, pos-1) - return part, pos - end - else - stack[#stack+1] = c - pos = pos + 1 - end - else -- close - assert(stack[#stack] == assert(begins[c]), 'syntax error: unbalanced') - stack[#stack] = nil - if #stack == 0 then - local part = s:sub(posa, pos) - return part, pos+1 - end - pos = pos + 1 - end - end -end -M.match_bracketed = match_bracketed - - --- Match Lua comment, e.g. "--...\n", "--[[...]]", "--[=[...]=]", etc. --- Function interface is similar to match_string. -local function match_comment(s, pos) - pos = pos or 1 - if s:sub(pos, pos+1) ~= '--' then - return nil, pos - end - pos = pos + 2 - local partt, post = match_string(s, pos) - if partt then - return '--' .. partt, post - end - local part; part, pos = s:match('^([^\n]*\n?)()', pos) - return '--' .. part, pos -end - - --- Match Lua expression, e.g. "a + b * c[e]". --- Function interface is similar to match_string. -local wordop = {['and']=true, ['or']=true, ['not']=true} -local is_compare = {['>']=true, ['<']=true, ['~']=true} -local function match_expression(s, pos) - pos = pos or 1 - local _ - local posa = pos - local lastident - local poscs, posce - while pos do - local c = s:sub(pos,pos) - if c == '"' or c == "'" or c == '[' and s:find('^[=%[]', pos+1) then - local part; part, pos = match_string(s, pos) - assert(part, 'syntax error') - elseif c == '-' and s:sub(pos+1,pos+1) == '-' then - -- note: handle adjacent comments in loop to properly support - -- backtracing (poscs/posce). - poscs = pos - while s:sub(pos,pos+1) == '--' do - local part; part, pos = match_comment(s, pos) - assert(part) - pos = s:match('^%s*()', pos) - posce = pos - end - elseif c == '(' or c == '{' or c == '[' then - _, pos = match_bracketed(s, pos) - elseif c == '=' and s:sub(pos+1,pos+1) == '=' then - pos = pos + 2 -- skip over two-char op containing '=' - elseif c == '=' and is_compare[s:sub(pos-1,pos-1)] then - pos = pos + 1 -- skip over two-char op containing '=' - elseif c:match'^[%)%}%];,=]' then - local part = s:sub(posa, pos-1) - return part, pos - elseif c:match'^[%w_]' then - local newident,newpos = s:match('^([%w_]+)()', pos) - if pos ~= posa and not wordop[newident] then -- non-first ident - local pose = ((posce == pos) and poscs or pos) - 1 - while s:match('^%s', pose) do pose = pose - 1 end - local ce = s:sub(pose,pose) - if ce:match'[%)%}\'\"%]]' or - ce:match'[%w_]' and not wordop[lastident] - then - local part = s:sub(posa, pos-1) - return part, pos - end - end - lastident, pos = newident, newpos - else - pos = pos + 1 - end - pos = s:find('[%(%{%[%)%}%]\"\';,=%w_%-]', pos) - end - local part = s:sub(posa, #s) - return part, #s+1 -end -M.match_expression = match_expression - - --- Match name list (zero or more names). E.g. "a,b,c" --- Function interface is similar to match_string, --- but returns array as match. -local function match_namelist(s, pos) - pos = pos or 1 - local list = {} - while 1 do - local c = #list == 0 and '^' or '^%s*,%s*' - local item, post = s:match(c .. '([%a_][%w_]*)%s*()', pos) - if item then pos = post else break end - list[#list+1] = item - end - return list, pos -end -M.match_namelist = match_namelist - - --- Match expression list (zero or more expressions). E.g. "a+b,b*c". --- Function interface is similar to match_string, --- but returns array as match. -local function match_explist(s, pos) - pos = pos or 1 - local list = {} - while 1 do - if #list ~= 0 then - local post = s:match('^%s*,%s*()', pos) - if post then pos = post else break end - end - local item; item, pos = match_expression(s, pos) - assert(item, 'syntax error') - list[#list+1] = item - end - return list, pos -end -M.match_explist = match_explist - - --- Replace snippets of code in Lua code string --- using replacement function f(u,sin) --> sout. --- is the type of snippet ('c' = comment, 's' = string, --- 'e' = any other code). --- Snippet is replaced with (unless is nil or false, in --- which case the original snippet is kept) --- This is somewhat analogous to string.gsub . -local function gsub(s, f) - local pos = 1 - local posa = 1 - local sret = '' - while 1 do - pos = s:find('[%-\'\"%[]', pos) - if not pos then break end - if s:match('^%-%-', pos) then - local exp = s:sub(posa, pos-1) - if #exp > 0 then sret = sret .. (f('e', exp) or exp) end - local comment; comment, pos = match_comment(s, pos) - sret = sret .. (f('c', assert(comment)) or comment) - posa = pos - else - local posb = s:find('^[\'\"%[]', pos) - local str - if posb then str, pos = match_string(s, posb) end - if str then - local exp = s:sub(posa, posb-1) - if #exp > 0 then sret = sret .. (f('e', exp) or exp) end - sret = sret .. (f('s', str) or str) - posa = pos - else - pos = pos + 1 - end - end - end - local exp = s:sub(posa) - if #exp > 0 then sret = sret .. (f('e', exp) or exp) end - return sret -end -M.gsub = gsub - - -return M diff --git a/src/pl/operator.lua b/src/pl/operator.lua deleted file mode 100644 index 60eaffd..0000000 --- a/src/pl/operator.lua +++ /dev/null @@ -1,209 +0,0 @@ ---- Lua operators available as functions. --- --- (similar to the Python module of the same name) --- --- There is a module field `optable` which maps the operator strings --- onto these functions, e.g. `operator.optable['()']==operator.call` --- --- Operator strings like '>' and '{}' can be passed to most Penlight functions --- expecting a function argument. --- --- @module pl.operator - -local strfind = string.find - -local operator = {} - ---- apply function to some arguments **()** --- @param fn a function or callable object --- @param ... arguments -function operator.call(fn,...) - return fn(...) -end - ---- get the indexed value from a table **[]** --- @param t a table or any indexable object --- @param k the key -function operator.index(t,k) - return t[k] -end - ---- returns true if arguments are equal **==** --- @param a value --- @param b value -function operator.eq(a,b) - return a==b -end - ---- returns true if arguments are not equal **~=** - -- @param a value --- @param b value -function operator.neq(a,b) - return a~=b -end - ---- returns true if a is less than b **<** --- @param a value --- @param b value -function operator.lt(a,b) - return a < b -end - ---- returns true if a is less or equal to b **<=** --- @param a value --- @param b value -function operator.le(a,b) - return a <= b -end - ---- returns true if a is greater than b **>** --- @param a value --- @param b value -function operator.gt(a,b) - return a > b -end - ---- returns true if a is greater or equal to b **>=** --- @param a value --- @param b value -function operator.ge(a,b) - return a >= b -end - ---- returns length of string or table **#** --- @param a a string or a table -function operator.len(a) - return #a -end - ---- add two values **+** --- @param a value --- @param b value -function operator.add(a,b) - return a+b -end - ---- subtract b from a **-** --- @param a value --- @param b value -function operator.sub(a,b) - return a-b -end - ---- multiply two values __*__ --- @param a value --- @param b value -function operator.mul(a,b) - return a*b -end - ---- divide first value by second **/** --- @param a value --- @param b value -function operator.div(a,b) - return a/b -end - ---- raise first to the power of second **^** --- @param a value --- @param b value -function operator.pow(a,b) - return a^b -end - ---- modulo; remainder of a divided by b **%** --- @param a value --- @param b value -function operator.mod(a,b) - return a%b -end - ---- concatenate two values (either strings or `__concat` defined) **..** --- @param a value --- @param b value -function operator.concat(a,b) - return a..b -end - ---- return the negative of a value **-** --- @param a value -function operator.unm(a) - return -a -end - ---- false if value evaluates as true **not** --- @param a value -function operator.lnot(a) - return not a -end - ---- true if both values evaluate as true **and** --- @param a value --- @param b value -function operator.land(a,b) - return a and b -end - ---- true if either value evaluate as true **or** --- @param a value --- @param b value -function operator.lor(a,b) - return a or b -end - ---- make a table from the arguments **{}** --- @param ... non-nil arguments --- @return a table -function operator.table (...) - return {...} -end - ---- match two strings **~**. --- uses @{string.find} -function operator.match (a,b) - return strfind(a,b)~=nil -end - ---- the null operation. --- @param ... arguments --- @return the arguments -function operator.nop (...) - return ... -end - ----- Map from operator symbol to function. --- Most of these map directly from operators; --- But note these extras --- --- * __'()'__ `call` --- * __'[]'__ `index` --- * __'{}'__ `table` --- * __'~'__ `match` --- --- @table optable --- @field operator - operator.optable = { - ['+']=operator.add, - ['-']=operator.sub, - ['*']=operator.mul, - ['/']=operator.div, - ['%']=operator.mod, - ['^']=operator.pow, - ['..']=operator.concat, - ['()']=operator.call, - ['[]']=operator.index, - ['<']=operator.lt, - ['<=']=operator.le, - ['>']=operator.gt, - ['>=']=operator.ge, - ['==']=operator.eq, - ['~=']=operator.neq, - ['#']=operator.len, - ['and']=operator.land, - ['or']=operator.lor, - ['{}']=operator.table, - ['~']=operator.match, - ['']=operator.nop, -} - -return operator diff --git a/src/pl/path.lua b/src/pl/path.lua deleted file mode 100644 index a438a9d..0000000 --- a/src/pl/path.lua +++ /dev/null @@ -1,575 +0,0 @@ ---- Path manipulation and file queries. --- --- This is modelled after Python's os.path library (10.1); see @{04-paths.md|the Guide}. --- --- NOTE: the functions assume the paths being dealt with to originate --- from the OS the application is running on. Windows drive letters are not --- to be used when running on a Unix system for example. The one exception --- is Windows paths to allow both forward and backward slashes (since Lua --- also accepts those) --- --- 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 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 res,lfs = _G.pcall(_G.require,'lfs') -if not res then - error("pl.path requires LuaFileSystem") -end - -local attrib = lfs.attributes -local currentdir = lfs.currentdir -local link_attrib = lfs.symlinkattributes - -local path = {} - -local function err_func(name, param, err, code) - local ret = ("%s failed"):format(tostring(name)) - if param ~= nil then - ret = ret .. (" for '%s'"):format(tostring(param)) - end - ret = ret .. (": %s"):format(tostring(err)) - if code ~= nil then - ret = ret .. (" (code %s)"):format(tostring(code)) - end - return ret -end - ---- Lua iterator over the entries of a given directory. --- Implicit link to [`luafilesystem.dir`](https://keplerproject.github.io/luafilesystem/manual.html#reference) --- @function dir -path.dir = lfs.dir - ---- Creates a directory. --- Implicit link to [`luafilesystem.mkdir`](https://keplerproject.github.io/luafilesystem/manual.html#reference) --- @function mkdir -path.mkdir = function(d) - local ok, err, code = lfs.mkdir(d) - if not ok then - return ok, err_func("mkdir", d, err, code), code - end - return ok, err, code -end - ---- Removes a directory. --- Implicit link to [`luafilesystem.rmdir`](https://keplerproject.github.io/luafilesystem/manual.html#reference) --- @function rmdir -path.rmdir = function(d) - local ok, err, code = lfs.rmdir(d) - if not ok then - return ok, err_func("rmdir", d, err, code), code - end - return ok, err, code -end - ---- Gets attributes. --- Implicit link to [`luafilesystem.attributes`](https://keplerproject.github.io/luafilesystem/manual.html#reference) --- @function attrib -path.attrib = function(d, r) - local ok, err, code = attrib(d, r) - if not ok then - return ok, err_func("attrib", d, err, code), code - end - return ok, err, code -end - ---- Get the working directory. --- Implicit link to [`luafilesystem.currentdir`](https://keplerproject.github.io/luafilesystem/manual.html#reference) --- @function currentdir -path.currentdir = function() - local ok, err, code = currentdir() - if not ok then - return ok, err_func("currentdir", nil, err, code), code - end - return ok, err, code -end - ---- Gets symlink attributes. --- Implicit link to [`luafilesystem.symlinkattributes`](https://keplerproject.github.io/luafilesystem/manual.html#reference) --- @function link_attrib -path.link_attrib = function(d, r) - local ok, err, code = link_attrib(d, r) - if not ok then - return ok, err_func("link_attrib", d, err, code), code - end - return ok, err, code -end - ---- Changes the working directory. --- On Windows, if a drive is specified, it also changes the current drive. If --- only specifying the drive, it will only switch drive, but not modify the path. --- Implicit link to [`luafilesystem.chdir`](https://keplerproject.github.io/luafilesystem/manual.html#reference) --- @function chdir -path.chdir = function(d) - local ok, err, code = lfs.chdir(d) - if not ok then - return ok, err_func("chdir", d, err, code), code - end - return ok, err, code -end - ---- 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 (either as file, directory, socket, etc), 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 as the number of seconds since the epoch. --- @string P A file path -function path.getmtime(P) - assert_string(1,P) - return attrib(P,'modification') -end - ----Return the system's ctime as the number of seconds since the epoch. --- @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 sep, other_sep, seps --- constant sep is the directory separator for this platform. --- constant dirsep is the separator in the PATH environment variable -if path.is_windows then - path.sep = '\\'; other_sep = '/' - path.dirsep = ';' - seps = { ['/'] = true, ['\\'] = true } -else - path.sep = '/' - path.dirsep = ':' - seps = { ['/'] = true } -end -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 --- @return directory part --- @return file part --- @usage --- local dir, file = path.splitpath("some/dir/myfile.txt") --- assert(dir == "some/dir") --- assert(file == "myfile.txt") --- --- local dir, file = path.splitpath("some/dir/") --- assert(dir == "some/dir") --- assert(file == "") --- --- local dir, file = path.splitpath("some_dir") --- assert(dir == "") --- assert(file == "some_dir") -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 (everything upto the "."", maybe empty) --- @treturn string extension part (including the ".", maybe empty) --- @usage --- local file_path, ext = path.splitext("/bonzo/dog_stuff/cat.txt") --- assert(file_path == "/bonzo/dog_stuff/cat") --- assert(ext == ".txt") --- --- local file_path, ext = path.splitext("") --- assert(file_path == "") --- assert(ext == "") -function path.splitext(P) - assert_string(1,P) - local i = #P - local ch = at(P,i) - while i > 0 and ch ~= '.' do - if seps[ch] 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 --- @treturn string everything before the last dir-separator --- @see splitpath --- @usage --- path.dirname("/some/path/file.txt") -- "/some/path" --- path.dirname("file.txt") -- "" (empty string) -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 --- @treturn string --- @see splitpath --- @usage --- path.basename("/some/path/file.txt") -- "file.txt" --- path.basename("/some/path/file/") -- "" (empty string) -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 --- @treturn string --- @see splitext --- @usage --- path.extension("/some/path/file.txt") -- ".txt" --- path.extension("/some/path/file_txt") -- "" (empty string) -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 --- @usage --- path.isabs("hello/path") -- false --- path.isabs("/hello/path") -- true --- -- Windows; --- path.isabs("hello\path") -- false --- path.isabs("\hello\path") -- true --- path.isabs("C:\hello\path") -- true --- path.isabs("C:hello\path") -- false -function path.isabs(P) - assert_string(1,P) - if path.is_windows and at(P,2) == ":" then - return seps[at(P,3)] ~= nil - end - return seps[at(P,1)] ~= nil -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 --- @treturn string the combined path --- @usage --- path.join("/first","second","third") -- "/first/second/third" --- path.join("first","second/third") -- "first/second/third" --- path.join("/first","/second","third") -- "/second/third" -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 --- * forward slashes to backward slashes --- @string P A file path --- @usage path.normcase("/Some/Path/File.txt") --- -- Windows: "\some\path\file.txt" --- -- Others : "/Some/Path/File.txt" -function path.normcase(P) - assert_string(1,P) - if path.is_windows then - return P:gsub('/','\\'):lower() - else - return P - end -end - ---- normalize a path name. --- `A//B`, `A/./B`, and `A/foo/../B` all become `A/B`. --- --- An empty path results in '.'. --- @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 seps[at(P, 1)] then - anchor = '\\' - P = P:sub(2) - elseif at(P, 2) == ':' then - anchor = P:sub(1, 2) - P = P:sub(3) - if seps[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 --- @return the common prefix (Windows: separators will be normalized, casing will be original) -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 listing paths tried -function path.package_path(mod) - assert_string(1,mod) - local res, err1, err2 - res, err1 = package.searchpath(mod,package.path) - if res then return res,true end - res, err2 = package.searchpath(mod,package.cpath) - if res then return res,false end - return raise ('cannot find module on path\n' .. err1 .. "\n" .. err2) -end - - ----- finis ----- -return path diff --git a/src/pl/permute.lua b/src/pl/permute.lua deleted file mode 100644 index ce646f4..0000000 --- a/src/pl/permute.lua +++ /dev/null @@ -1,196 +0,0 @@ ---- Permutation operations. --- --- Dependencies: `pl.utils`, `pl.tablex` --- @module pl.permute -local tablex = require 'pl.tablex' -local utils = require 'pl.utils' -local copy = tablex.deepcopy -local append = table.insert -local assert_arg = utils.assert_arg - - -local permute = {} - - ---- an iterator over all order-permutations of the elements of a list. --- Please note that the same list is returned each time, so do not keep references! --- @param a list-like table --- @return an iterator which provides the next permutation as a list -function permute.order_iter(a) - assert_arg(1,a,'table') - - local t = #a - local stack = { 1 } - local function iter() - local h = #stack - local n = t - h + 1 - - local i = stack[h] - if i > t then - return - end - - if n == 0 then - table.remove(stack) - h = h - 1 - - stack[h] = stack[h] + 1 - return a - - elseif i <= n then - - -- put i-th element as the last one - a[n], a[i] = a[i], a[n] - - -- generate all permutations of the other elements - table.insert(stack, 1) - - else - - table.remove(stack) - h = h - 1 - - n = n + 1 - i = stack[h] - - -- restore i-th element - a[n], a[i] = a[i], a[n] - - stack[h] = stack[h] + 1 - end - return iter() -- tail-call - end - - return iter -end - - ---- construct a table containing all the order-permutations of a list. --- @param a list-like table --- @return a table of tables --- @usage permute.order_table {1,2,3} --> {{2,3,1},{3,2,1},{3,1,2},{1,3,2},{2,1,3},{1,2,3}} -function permute.order_table (a) - assert_arg(1,a,'table') - local res = {} - for t in permute.iter(a) do - append(res,copy(t)) - end - return res -end - - - ---- an iterator over all permutations of the elements of the given lists. --- @param ... list-like tables, they are nil-safe if a length-field `n` is provided (see `utils.pack`) --- @return an iterator which provides the next permutation as return values in the same order as the provided lists, preceeded by an index --- @usage --- local strs = utils.pack("one", nil, "three") -- adds an 'n' field for nil-safety --- local bools = utils.pack(true, false) --- local iter = permute.list_iter(strs, bools) --- --- print(iter()) --> 1, one, true --- print(iter()) --> 2, nil, true --- print(iter()) --> 3, three, true --- print(iter()) --> 4, one, false --- print(iter()) --> 5, nil, false --- print(iter()) --> 6, three, false -function permute.list_iter(...) - local elements = {...} - local pointers = {} - local sizes = {} - local size = #elements - for i, list in ipairs(elements) do - assert_arg(i,list,'table') - pointers[i] = 1 - sizes[i] = list.n or #list - end - local count = 0 - - return function() - if pointers[size] > sizes[size] then return end -- we're done - count = count + 1 - local r = { n = #elements } - local cascade_up = true - for i = 1, size do - r[i] = elements[i][pointers[i]] - if cascade_up then - pointers[i] = pointers[i] + 1 - if pointers[i] <= sizes[i] then - -- this list is not done yet, stop cascade - cascade_up = false - else - -- this list is done - if i ~= size then - -- reset pointer - pointers[i] = 1 - end - end - end - end - return count, utils.unpack(r) - end -end - - - ---- construct a table containing all the permutations of a set of lists. --- @param ... list-like tables, they are nil-safe if a length-field `n` is provided --- @return a list of lists, the sub-lists have an 'n' field for nil-safety --- @usage --- local strs = utils.pack("one", nil, "three") -- adds an 'n' field for nil-safety --- local bools = utils.pack(true, false) --- local results = permute.list_table(strs, bools) --- -- results = { --- -- { "one, true, n = 2 } --- -- { nil, true, n = 2 }, --- -- { "three, true, n = 2 }, --- -- { "one, false, n = 2 }, --- -- { nil, false, n = 2 }, --- -- { "three", false, n = 2 }, --- -- } -function permute.list_table(...) - local iter = permute.list_iter(...) - local results = {} - local i = 1 - while true do - local values = utils.pack(iter()) - if values[1] == nil then return results end - for i = 1, values.n do values[i] = values[i+1] end - values.n = values.n - 1 - results[i] = values - i = i + 1 - end -end - - --- backward compat, to be deprecated - ---- deprecated. --- @param ... --- @see permute.order_iter -function permute.iter(...) - utils.raise_deprecation { - source = "Penlight " .. utils._VERSION, - message = "function 'iter' was renamed to 'order_iter'", - version_removed = "2.0.0", - deprecated_after = "1.9.2", - } - - return permute.order_iter(...) -end - ---- deprecated. --- @param ... --- @see permute.order_iter -function permute.table(...) - utils.raise_deprecation { - source = "Penlight " .. utils._VERSION, - message = "function 'table' was renamed to 'order_table'", - version_removed = "2.0.0", - deprecated_after = "1.9.2", - } - - return permute.order_table(...) -end - -return permute diff --git a/src/pl/pretty.lua b/src/pl/pretty.lua deleted file mode 100644 index 09add30..0000000 --- a/src/pl/pretty.lua +++ /dev/null @@ -1,437 +0,0 @@ ---- Pretty-printing Lua tables. --- Also provides a sandboxed Lua table reader and --- a function to present large numbers in human-friendly format. --- --- Dependencies: `pl.utils`, `pl.lexer`, `pl.stringx`, `debug` --- @module pl.pretty - -local append = table.insert -local concat = table.concat -local mfloor, mhuge = math.floor, math.huge -local mtype = math.type -local utils = require 'pl.utils' -local lexer = require 'pl.lexer' -local debug = require 'debug' -local quote_string = require'pl.stringx'.quote_string -local assert_arg = utils.assert_arg - -local original_tostring = tostring - --- Patch tostring to format numbers with better precision --- and to produce cross-platform results for --- infinite values and NaN. -local function tostring(value) - if type(value) ~= "number" then - return original_tostring(value) - elseif value ~= value then - return "NaN" - elseif value == mhuge then - return "Inf" - elseif value == -mhuge then - return "-Inf" - elseif (_VERSION ~= "Lua 5.3" or mtype(value) == "integer") and mfloor(value) == value then - return ("%d"):format(value) - else - local res = ("%.14g"):format(value) - if _VERSION == "Lua 5.3" and mtype(value) == "float" and not res:find("%.") then - -- Number is internally a float but looks like an integer. - -- Insert ".0" after first run of digits. - res = res:gsub("%d+", "%0.0", 1) - end - return res - end -end - -local pretty = {} - -local function save_global_env() - local env = {} - env.hook, env.mask, env.count = debug.gethook() - - -- env.hook is "external hook" if is a C hook function - if env.hook~="external hook" then - debug.sethook() - end - - env.string_mt = getmetatable("") - debug.setmetatable("", nil) - return env -end - -local function restore_global_env(env) - if env then - debug.setmetatable("", env.string_mt) - if env.hook~="external hook" then - debug.sethook(env.hook, env.mask, env.count) - end - end -end - ---- Read a string representation of a Lua table. --- This function loads and runs the string as Lua code, but bails out --- if it contains a function definition. --- Loaded string is executed in an empty environment. --- @string s string to read in `{...}` format, possibly with some whitespace --- before or after the curly braces. A single line comment may be present --- at the beginning. --- @return a table in case of success. --- If loading the string failed, return `nil` and error message. --- If executing loaded string failed, return `nil` and the error it raised. -function pretty.read(s) - assert_arg(1,s,'string') - if s:find '^%s*%-%-' then -- may start with a comment.. - s = s:gsub('%-%-.-\n','') - end - if not s:find '^%s*{' then return nil,"not a Lua table" end - if s:find '[^\'"%w_]function[^\'"%w_]' then - local tok = lexer.lua(s) - for t,v in tok do - if t == 'keyword' and v == 'function' then - return nil,"cannot have functions in table definition" - end - end - end - s = 'return '..s - local chunk,err = utils.load(s,'tbl','t',{}) - if not chunk then return nil,err end - local global_env = save_global_env() - local ok,ret = pcall(chunk) - restore_global_env(global_env) - if ok then return ret - else - return nil,ret - end -end - ---- Read a Lua chunk. --- @string s Lua code. --- @tab[opt] env environment used to run the code, empty by default. --- @bool[opt] paranoid abort loading if any looping constructs a found in the code --- and disable string methods. --- @return the environment in case of success or `nil` and syntax or runtime error --- if something went wrong. -function pretty.load (s, env, paranoid) - env = env or {} - if paranoid then - local tok = lexer.lua(s) - for t,v in tok do - if t == 'keyword' - and (v == 'for' or v == 'repeat' or v == 'function' or v == 'goto') - then - return nil,"looping not allowed" - end - end - end - local chunk,err = utils.load(s,'tbl','t',env) - if not chunk then return nil,err end - local global_env = paranoid and save_global_env() - local ok,err = pcall(chunk) - restore_global_env(global_env) - if not ok then return nil,err end - return env -end - -local function quote_if_necessary (v) - if not v then return '' - else - --AAS - if v:find ' ' then v = quote_string(v) end - end - return v -end - -local keywords - -local function is_identifier (s) - return type(s) == 'string' and s:find('^[%a_][%w_]*$') and not keywords[s] -end - -local function quote (s) - if type(s) == 'table' then - return pretty.write(s,'') - else - --AAS - return quote_string(s)-- ('%q'):format(tostring(s)) - end -end - -local function index (numkey,key) - --AAS - if not numkey then - key = quote(key) - key = key:find("^%[") and (" " .. key .. " ") or key - end - return '['..key..']' -end - - ---- Create a string representation of a Lua table. --- This function never fails, but may complain by returning an --- extra value. Normally puts out one item per line, using --- the provided indent; set the second parameter to an empty string --- if you want output on one line. --- --- *NOTE:* this is NOT a serialization function, not a full blown --- debug function. Checkout out respectively the --- [serpent](https://github.com/pkulchenko/serpent) --- or [inspect](https://github.com/kikito/inspect.lua) --- Lua modules for that if you need them. --- @tab tbl Table to serialize to a string. --- @string[opt] space The indent to use. --- Defaults to two spaces; pass an empty string for no indentation. --- @bool[opt] not_clever Pass `true` for plain output, e.g `{['key']=1}`. --- Defaults to `false`. --- @return a string --- @return an optional error message -function pretty.write (tbl,space,not_clever) - if type(tbl) ~= 'table' then - local res = tostring(tbl) - if type(tbl) == 'string' then return quote(tbl) end - return res, 'not a table' - end - if not keywords then - keywords = lexer.get_keywords() - end - local set = ' = ' - if space == '' then set = '=' end - space = space or ' ' - local lines = {} - local line = '' - local tables = {} - - - local function put(s) - if #s > 0 then - line = line..s - end - end - - local function putln (s) - if #line > 0 then - line = line..s - append(lines,line) - line = '' - else - append(lines,s) - end - end - - local function eat_last_comma () - local n = #lines - local lastch = lines[n]:sub(-1,-1) - if lastch == ',' then - lines[n] = lines[n]:sub(1,-2) - end - end - - - -- safe versions for iterators since 5.3+ honors metamethods that can throw - -- errors - local ipairs = function(t) - local i = 0 - local ok, v - local getter = function() return t[i] end - return function() - i = i + 1 - ok, v = pcall(getter) - if v == nil or not ok then return end - return i, t[i] - end - end - local pairs = function(t) - local k, v, ok - local getter = function() return next(t, k) end - return function() - ok, k, v = pcall(getter) - if not ok then return end - return k, v - end - end - - local writeit - writeit = function (t,oldindent,indent) - local tp = type(t) - if tp ~= 'string' and tp ~= 'table' then - putln(quote_if_necessary(tostring(t))..',') - elseif tp == 'string' then - -- if t:find('\n') then - -- putln('[[\n'..t..']],') - -- else - -- putln(quote(t)..',') - -- end - --AAS - putln(quote_string(t) ..",") - elseif tp == 'table' then - if tables[t] then - putln(',') - return - end - tables[t] = true - local newindent = indent..space - putln('{') - local used = {} - if not not_clever then - for i,val in ipairs(t) do - put(indent) - writeit(val,indent,newindent) - used[i] = true - end - end - local ordered_keys = {} - for k,v in pairs(t) do - if type(k) ~= 'number' then - ordered_keys[#ordered_keys + 1] = k - end - end - table.sort(ordered_keys, function (a, b) - if type(a) == type(b) and type(a) == 'string' then - return a < b - end - return type(a) == 'boolean' or (type(b) ~= 'boolean' and type(a) == 'table') - end) - local function write_entry (key, val) - local tkey = type(key) - local numkey = tkey == 'number' - if not_clever then - key = tostring(key) - put(indent..index(numkey,key)..set) - writeit(val,indent,newindent) - else - if not numkey or not used[key] then -- non-array indices - if tkey ~= 'string' then - key = tostring(key) - end - if numkey or not is_identifier(key) then - key = index(numkey,key) - end - put(indent..key..set) - writeit(val,indent,newindent) - end - end - end - for i = 1, #ordered_keys do - local key = ordered_keys[i] - local val = t[key] - write_entry(key, val) - end - for key,val in pairs(t) do - if type(key) == 'number' then - write_entry(key, val) - end - end - tables[t] = nil - eat_last_comma() - putln(oldindent..'},') - else - putln(tostring(t)..',') - end - end - writeit(tbl,'',space) - eat_last_comma() - return concat(lines,#space > 0 and '\n' or '') -end - ---- Dump a Lua table out to a file or stdout. --- @tab t The table to write to a file or stdout. --- @string[opt] filename File name to write too. Defaults to writing --- to stdout. -function pretty.dump (t, filename) - if not filename then - print(pretty.write(t)) - return true - else - return utils.writefile(filename, pretty.write(t)) - end -end - ---- Dump a series of arguments to stdout for debug purposes. --- This function is attached to the module table `__call` method, to make it --- extra easy to access. So the full: --- --- print(require("pl.pretty").write({...})) --- --- Can be shortened to: --- --- require"pl.pretty" (...) --- --- Any `nil` entries will be printed as `""` to make them explicit. --- @param ... the parameters to dump to stdout. --- @usage --- -- example debug output --- require"pl.pretty" ("hello", nil, "world", { bye = "world", true} ) --- --- -- output: --- { --- ["arg 1"] = "hello", --- ["arg 2"] = "", --- ["arg 3"] = "world", --- ["arg 4"] = { --- true, --- bye = "world" --- } --- } -function pretty.debug(...) - local n = select("#", ...) - local t = { ... } - for i = 1, n do - local value = t[i] - if value == nil then - value = "" - end - t[i] = nil - t["arg " .. i] = value - end - - print(pretty.write(t)) - return true -end - - -local memp,nump = {'B','KiB','MiB','GiB'},{'','K','M','B'} - -local function comma (val) - local thou = math.floor(val/1000) - if thou > 0 then return comma(thou)..','.. tostring(val % 1000) - else return tostring(val) end -end - ---- Format large numbers nicely for human consumption. --- @number num a number. --- @string[opt] kind one of `'M'` (memory in `KiB`, `MiB`, etc.), --- `'N'` (postfixes are `'K'`, `'M'` and `'B'`), --- or `'T'` (use commas as thousands separator), `'N'` by default. --- @int[opt] prec number of digits to use for `'M'` and `'N'`, `1` by default. -function pretty.number (num,kind,prec) - local fmt = '%.'..(prec or 1)..'f%s' - if kind == 'T' then - return comma(num) - else - local postfixes, fact - if kind == 'M' then - fact = 1024 - postfixes = memp - else - fact = 1000 - postfixes = nump - end - local div = fact - local k = 1 - while num >= div and k <= #postfixes do - div = div * fact - k = k + 1 - end - div = div / fact - if k > #postfixes then k = k - 1; div = div/fact end - if k > 1 then - return fmt:format(num/div,postfixes[k] or 'duh') - else - return num..postfixes[1] - end - end -end - -return setmetatable(pretty, { - __call = function(self, ...) - return self.debug(...) - end -}) diff --git a/src/pl/seq.lua b/src/pl/seq.lua deleted file mode 100644 index 1c08d20..0000000 --- a/src/pl/seq.lua +++ /dev/null @@ -1,544 +0,0 @@ ---- Manipulating iterators as sequences. --- See @{07-functional.md.Sequences|The Guide} --- --- Dependencies: `pl.utils`, `pl.types`, `debug` --- @module pl.seq - -local next,assert,pairs,tonumber,type,setmetatable = next,assert,pairs,tonumber,type,setmetatable -local strfind,format = string.find,string.format -local mrandom = math.random -local tsort,tappend = table.sort,table.insert -local io = io -local utils = require 'pl.utils' -local callable = require 'pl.types'.is_callable -local function_arg = utils.function_arg -local assert_arg = utils.assert_arg -local debug = require 'debug' - -local seq = {} - --- given a number, return a function(y) which returns true if y > x --- @param x a number -function seq.greater_than(x) - return function(v) - return tonumber(v) > x - end -end - --- given a number, returns a function(y) which returns true if y < x --- @param x a number -function seq.less_than(x) - return function(v) - return tonumber(v) < x - end -end - --- given any value, return a function(y) which returns true if y == x --- @param x a value -function seq.equal_to(x) - if type(x) == "number" then - return function(v) - return tonumber(v) == x - end - else - return function(v) - return v == x - end - end -end - ---- given a string, return a function(y) which matches y against the string. --- @param s a string -function seq.matching(s) - return function(v) - return strfind(v,s) - end -end - -local nexti - ---- sequence adaptor for a table. Note that if any generic function is --- passed a table, it will automatically use seq.list() --- @param t a list-like table --- @usage sum(list(t)) is the sum of all elements of t --- @usage for x in list(t) do...end -function seq.list(t) - assert_arg(1,t,'table') - if not nexti then - nexti = ipairs{} - end - local key,value = 0 - return function() - key,value = nexti(t,key) - return value - end -end - ---- return the keys of the table. --- @param t an arbitrary table --- @return iterator over keys -function seq.keys(t) - assert_arg(1,t,'table') - local key - return function() - key = next(t,key) - return key - end -end - -local list = seq.list -local function default_iter(iter) - if type(iter) == 'table' then return list(iter) - else return iter end -end - -seq.iter = default_iter - ---- create an iterator over a numerical range. Like the standard Python function xrange. --- @param start a number --- @param finish a number greater than start -function seq.range(start,finish) - local i = start - 1 - return function() - i = i + 1 - if i > finish then return nil - else return i end - end -end - --- count the number of elements in the sequence which satisfy the predicate --- @param iter a sequence --- @param condn a predicate function (must return either true or false) --- @param optional argument to be passed to predicate as second argument. --- @return count -function seq.count(iter,condn,arg) - local i = 0 - seq.foreach(iter,function(val) - if condn(val,arg) then i = i + 1 end - end) - return i -end - ---- return the minimum and the maximum value of the sequence. --- @param iter a sequence --- @return minimum value --- @return maximum value -function seq.minmax(iter) - local vmin,vmax = 1e70,-1e70 - for v in default_iter(iter) do - v = tonumber(v) - if v < vmin then vmin = v end - if v > vmax then vmax = v end - end - return vmin,vmax -end - ---- return the sum and element count of the sequence. --- @param iter a sequence --- @param fn an optional function to apply to the values -function seq.sum(iter,fn) - local s = 0 - local i = 0 - for v in default_iter(iter) do - if fn then v = fn(v) end - s = s + v - i = i + 1 - end - return s,i -end - ---- create a table from the sequence. (This will make the result a List.) --- @param iter a sequence --- @return a List --- @usage copy(list(ls)) is equal to ls --- @usage copy(list {1,2,3}) == List{1,2,3} -function seq.copy(iter) - local res,k = {},1 - for v in default_iter(iter) do - res[k] = v - k = k + 1 - end - setmetatable(res, require('pl.List')) - return res -end - ---- create a table of pairs from the double-valued sequence. --- @param iter a double-valued sequence --- @param i1 used to capture extra iterator values --- @param i2 as with pairs & ipairs --- @usage copy2(ipairs{10,20,30}) == {{1,10},{2,20},{3,30}} --- @return a list-like table -function seq.copy2 (iter,i1,i2) - local res,k = {},1 - for v1,v2 in iter,i1,i2 do - res[k] = {v1,v2} - k = k + 1 - end - return res -end - ---- create a table of 'tuples' from a multi-valued sequence. --- A generalization of copy2 above --- @param iter a multiple-valued sequence --- @return a list-like table -function seq.copy_tuples (iter) - iter = default_iter(iter) - local res = {} - local row = {iter()} - while #row > 0 do - tappend(res,row) - row = {iter()} - end - return res -end - ---- return an iterator of random numbers. --- @param n the length of the sequence --- @param l same as the first optional argument to math.random --- @param u same as the second optional argument to math.random --- @return a sequence -function seq.random(n,l,u) - local rand - assert(type(n) == 'number') - if u then - rand = function() return mrandom(l,u) end - elseif l then - rand = function() return mrandom(l) end - else - rand = mrandom - end - - return function() - if n == 0 then return nil - else - n = n - 1 - return rand() - end - end -end - ---- return an iterator to the sorted elements of a sequence. --- @param iter a sequence --- @param comp an optional comparison function (comp(x,y) is true if x < y) -function seq.sort(iter,comp) - local t = seq.copy(iter) - tsort(t,comp) - return list(t) -end - ---- return an iterator which returns elements of two sequences. --- @param iter1 a sequence --- @param iter2 a sequence --- @usage for x,y in seq.zip(ls1,ls2) do....end -function seq.zip(iter1,iter2) - iter1 = default_iter(iter1) - iter2 = default_iter(iter2) - return function() - return iter1(),iter2() - end -end - ---- Makes a table where the key/values are the values and value counts of the sequence. --- This version works with 'hashable' values like strings and numbers. --- `pl.tablex.count_map` is more general. --- @param iter a sequence --- @return a map-like table --- @return a table --- @see pl.tablex.count_map -function seq.count_map(iter) - local t = {} - local v - for s in default_iter(iter) do - v = t[s] - if v then t[s] = v + 1 - else t[s] = 1 end - end - return setmetatable(t, require('pl.Map')) -end - --- given a sequence, return all the unique values in that sequence. --- @param iter a sequence --- @param returns_table true if we return a table, not a sequence --- @return a sequence or a table; defaults to a sequence. -function seq.unique(iter,returns_table) - local t = seq.count_map(iter) - local res,k = {},1 - for key in pairs(t) do res[k] = key; k = k + 1 end - table.sort(res) - if returns_table then - return res - else - return list(res) - end -end - ---- print out a sequence iter with a separator. --- @param iter a sequence --- @param sep the separator (default space) --- @param nfields maximum number of values per line (default 7) --- @param fmt optional format function for each value -function seq.printall(iter,sep,nfields,fmt) - local write = io.write - if not sep then sep = ' ' end - if not nfields then - if sep == '\n' then nfields = 1e30 - else nfields = 7 end - end - if fmt then - local fstr = fmt - fmt = function(v) return format(fstr,v) end - end - local k = 1 - for v in default_iter(iter) do - if fmt then v = fmt(v) end - if k < nfields then - write(v,sep) - k = k + 1 - else - write(v,'\n') - k = 1 - end - end - write '\n' -end - --- return an iterator running over every element of two sequences (concatenation). --- @param iter1 a sequence --- @param iter2 a sequence -function seq.splice(iter1,iter2) - iter1 = default_iter(iter1) - iter2 = default_iter(iter2) - local iter = iter1 - return function() - local ret = iter() - if ret == nil then - if iter == iter1 then - iter = iter2 - return iter() - else return nil end - else - return ret - end - end -end - ---- return a sequence where every element of a sequence has been transformed --- by a function. If you don't supply an argument, then the function will --- receive both values of a double-valued sequence, otherwise behaves rather like --- tablex.map. --- @param fn a function to apply to elements; may take two arguments --- @param iter a sequence of one or two values --- @param arg optional argument to pass to function. -function seq.map(fn,iter,arg) - fn = function_arg(1,fn) - iter = default_iter(iter) - return function() - local v1,v2 = iter() - if v1 == nil then return nil end - return fn(v1,arg or v2) or false - end -end - ---- filter a sequence using a predicate function. --- @param iter a sequence of one or two values --- @param pred a boolean function; may take two arguments --- @param arg optional argument to pass to function. -function seq.filter (iter,pred,arg) - pred = function_arg(2,pred) - return function () - local v1,v2 - while true do - v1,v2 = iter() - if v1 == nil then return nil end - if pred(v1,arg or v2) then return v1,v2 end - end - end -end - ---- 'reduce' a sequence using a binary function. --- @func fn a function of two arguments --- @param iter a sequence --- @param initval optional initial value --- @usage seq.reduce(operator.add,seq.list{1,2,3,4}) == 10 --- @usage seq.reduce('-',{1,2,3,4,5}) == -13 -function seq.reduce (fn,iter,initval) - fn = function_arg(1,fn) - iter = default_iter(iter) - local val = initval or iter() - if val == nil then return nil end - for v in iter do - val = fn(val,v) - end - return val -end - ---- take the first n values from the sequence. --- @param iter a sequence of one or two values --- @param n number of items to take --- @return a sequence of at most n items -function seq.take (iter,n) - iter = default_iter(iter) - return function() - if n < 1 then return end - local val1,val2 = iter() - if not val1 then return end - n = n - 1 - return val1,val2 - end -end - ---- skip the first n values of a sequence --- @param iter a sequence of one or more values --- @param n number of items to skip -function seq.skip (iter,n) - n = n or 1 - for i = 1,n do - if iter() == nil then return list{} end - end - return iter -end - ---- a sequence with a sequence count and the original value. --- enum(copy(ls)) is a roundabout way of saying ipairs(ls). --- @param iter a single or double valued sequence --- @return sequence of (i,v), i = 1..n and v is from iter. -function seq.enum (iter) - local i = 0 - iter = default_iter(iter) - return function () - local val1,val2 = iter() - if not val1 then return end - i = i + 1 - return i,val1,val2 - end -end - ---- map using a named method over a sequence. --- @param iter a sequence --- @param name the method name --- @param arg1 optional first extra argument --- @param arg2 optional second extra argument -function seq.mapmethod (iter,name,arg1,arg2) - iter = default_iter(iter) - return function() - local val = iter() - if not val then return end - local fn = val[name] - if not fn then error(type(val).." does not have method "..name) end - return fn(val,arg1,arg2) - end -end - ---- a sequence of (last,current) values from another sequence. --- This will return S(i-1),S(i) if given S(i) --- @param iter a sequence -function seq.last (iter) - iter = default_iter(iter) - local val, l = iter(), nil - if val == nil then return list{} end - return function () - val,l = iter(),val - if val == nil then return nil end - return val,l - end -end - ---- call the function on each element of the sequence. --- @param iter a sequence with up to 3 values --- @param fn a function -function seq.foreach(iter,fn) - fn = function_arg(2,fn) - for i1,i2,i3 in default_iter(iter) do fn(i1,i2,i3) end -end - ----------------------- Sequence Adapters --------------------- - -local SMT - -local function SW (iter,...) - if callable(iter) then - return setmetatable({iter=iter},SMT) - else - return iter,... - end -end - - --- can't directly look these up in seq because of the wrong argument order... -local map,reduce,mapmethod = seq.map, seq.reduce, seq.mapmethod -local overrides = { - map = function(self,fun,arg) - return map(fun,self,arg) - end, - reduce = function(self,fun,initval) - return reduce(fun,self,initval) - end -} - -SMT = { - __index = function (tbl,key) - local fn = overrides[key] or seq[key] - if fn then - return function(sw,...) return SW(fn(sw.iter,...)) end - else - return function(sw,...) return SW(mapmethod(sw.iter,key,...)) end - end - end, - __call = function (sw) - return sw.iter() - end, -} - -setmetatable(seq,{ - __call = function(tbl,iter,extra) - if not callable(iter) then - if type(iter) == 'table' then iter = seq.list(iter) - else return iter - end - end - if extra then - return setmetatable({iter=function() - return iter(extra) - end},SMT) - else - return setmetatable({iter=iter},SMT) - end - end -}) - ---- create a wrapped iterator over all lines in the file. --- @param f either a filename, file-like object, or 'STDIN' (for standard input) --- @param ... for Lua 5.2 only, optional format specifiers, as in `io.read`. --- @return a sequence wrapper -function seq.lines (f,...) - local iter,obj - if f == 'STDIN' then - f = io.stdin - elseif type(f) == 'string' then - iter,obj = io.lines(f,...) - elseif not f.read then - error("Pass either a string or a file-like object",2) - end - if not iter then - iter,obj = f:lines(...) - end - if obj then -- LuaJIT version returns a function operating on a file - local lines,file = iter,obj - iter = function() return lines(file) end - end - return SW(iter) -end - -function seq.import () - debug.setmetatable(function() end,{ - __index = function(tbl,key) - local s = overrides[key] or seq[key] - if s then return s - else - return function(s,...) return seq.mapmethod(s,key,...) end - end - end - }) -end - -return seq diff --git a/src/pl/sip.lua b/src/pl/sip.lua deleted file mode 100644 index 5d1f2aa..0000000 --- a/src/pl/sip.lua +++ /dev/null @@ -1,337 +0,0 @@ ---- Simple Input Patterns (SIP). --- SIP patterns start with '$', then a --- one-letter type, and then an optional variable in curly braces. --- --- sip.match('$v=$q','name="dolly"',res) --- ==> res=={'name','dolly'} --- sip.match('($q{first},$q{second})','("john","smith")',res) --- ==> res=={second='smith',first='john'} --- --- Type names: --- --- v identifier --- i integer --- f floating-point --- q quoted string --- ([{< match up to closing bracket --- --- See @{08-additional.md.Simple_Input_Patterns|the Guide} --- --- @module pl.sip - -local loadstring = rawget(_G,'loadstring') or load -local unpack = rawget(_G,'unpack') or rawget(table,'unpack') - -local append,concat = table.insert,table.concat -local ipairs,type = ipairs,type -local io,_G = io,_G -local print,rawget = print,rawget - -local patterns = { - FLOAT = '[%+%-%d]%d*%.?%d*[eE]?[%+%-]?%d*', - INTEGER = '[+%-%d]%d*', - IDEN = '[%a_][%w_]*', - OPTION = '[%a_][%w_%-]*', -} - -local function assert_arg(idx,val,tp) - if type(val) ~= tp then - error("argument "..idx.." must be "..tp, 2) - end -end - -local sip = {} - -local brackets = {['<'] = '>', ['('] = ')', ['{'] = '}', ['['] = ']' } -local stdclasses = {a=1,c=0,d=1,l=1,p=0,u=1,w=1,x=1,s=0} - -local function group(s) - return '('..s..')' -end - --- escape all magic characters except $, which has special meaning --- Also, un-escape any characters after $, so $( and $[ passes through as is. -local function escape (spec) - return (spec:gsub('[%-%.%+%[%]%(%)%^%%%?%*]','%%%0'):gsub('%$%%(%S)','$%1')) -end - --- Most spaces within patterns can match zero or more spaces. --- Spaces between alphanumeric characters or underscores or between --- patterns that can match these characters, however, must match at least --- one space. Otherwise '$v $v' would match 'abcd' as {'abc', 'd'}. --- This function replaces continuous spaces within a pattern with either --- '%s*' or '%s+' according to this rule. The pattern has already --- been stripped of pattern names by now. -local function compress_spaces(patt) - return (patt:gsub("()%s+()", function(i1, i2) - local before = patt:sub(i1 - 2, i1 - 1) - if before:match('%$[vifadxlu]') or before:match('^[^%$]?[%w_]$') then - local after = patt:sub(i2, i2 + 1) - if after:match('%$[vifadxlu]') or after:match('^[%w_]') then - return '%s+' - end - end - return '%s*' - end)) -end - -local pattern_map = { - v = group(patterns.IDEN), - i = group(patterns.INTEGER), - f = group(patterns.FLOAT), - o = group(patterns.OPTION), - r = '(%S.*)', - p = '([%a]?[:]?[\\/%.%w_]+)' -} - -function sip.custom_pattern(flag,patt) - pattern_map[flag] = patt -end - ---- convert a SIP pattern into the equivalent Lua string pattern. --- @param spec a SIP pattern --- @param options a table; only the at_start field is --- currently meaningful and ensures that the pattern is anchored --- at the start of the string. --- @return a Lua string pattern. -function sip.create_pattern (spec,options) - assert_arg(1,spec,'string') - local fieldnames,fieldtypes = {},{} - - if type(spec) == 'string' then - spec = escape(spec) - else - local res = {} - for i,s in ipairs(spec) do - res[i] = escape(s) - end - spec = concat(res,'.-') - end - - local kount = 1 - - local function addfield (name,type) - name = name or kount - append(fieldnames,name) - fieldtypes[name] = type - kount = kount + 1 - end - - local named_vars = spec:find('{%a+}') - - if options and options.at_start then - spec = '^'..spec - end - if spec:sub(-1,-1) == '$' then - spec = spec:sub(1,-2)..'$r' - if named_vars then spec = spec..'{rest}' end - end - - local names - - if named_vars then - names = {} - spec = spec:gsub('{(%a+)}',function(name) - append(names,name) - return '' - end) - end - spec = compress_spaces(spec) - - local k = 1 - local err - local r = (spec:gsub('%$%S',function(s) - local type,name - type = s:sub(2,2) - if names then name = names[k]; k=k+1 end - -- this kludge is necessary because %q generates two matches, and - -- we want to ignore the first. Not a problem for named captures. - if not names and type == 'q' then - addfield(nil,'Q') - else - addfield(name,type) - end - local res - if pattern_map[type] then - res = pattern_map[type] - elseif type == 'q' then - -- some Lua pattern matching voodoo; we want to match '...' as - -- well as "...", and can use the fact that %n will match a - -- previous capture. Adding the extra field above comes from needing - -- to accommodate the extra spurious match (which is either ' or ") - addfield(name,type) - res = '(["\'])(.-)%'..(kount-2) - else - local endbracket = brackets[type] - if endbracket then - res = '(%b'..type..endbracket..')' - elseif stdclasses[type] or stdclasses[type:lower()] then - res = '(%'..type..'+)' - else - err = "unknown format type or character class" - end - end - return res - end)) - - if err then - return nil,err - else - return r,fieldnames,fieldtypes - end -end - - -local function tnumber (s) - return s == 'd' or s == 'i' or s == 'f' -end - -function sip.create_spec_fun(spec,options) - local fieldtypes,fieldnames - local ls = {} - spec,fieldnames,fieldtypes = sip.create_pattern(spec,options) - if not spec then return spec,fieldnames end - local named_vars = type(fieldnames[1]) == 'string' - for i = 1,#fieldnames do - append(ls,'mm'..i) - end - ls[1] = ls[1] or "mm1" -- behave correctly if there are no patterns - local fun = ('return (function(s,res)\n\tlocal %s = s:match(%q)\n'):format(concat(ls,','),spec) - fun = fun..'\tif not mm1 then return false end\n' - local k=1 - for i,f in ipairs(fieldnames) do - if f ~= '_' then - local var = 'mm'..i - if tnumber(fieldtypes[f]) then - var = 'tonumber('..var..')' - elseif brackets[fieldtypes[f]] then - var = var..':sub(2,-2)' - end - if named_vars then - fun = ('%s\tres.%s = %s\n'):format(fun,f,var) - else - if fieldtypes[f] ~= 'Q' then -- we skip the string-delim capture - fun = ('%s\tres[%d] = %s\n'):format(fun,k,var) - k = k + 1 - end - end - end - end - return fun..'\treturn true\nend)\n', named_vars -end - ---- convert a SIP pattern into a matching function. --- The returned function takes two arguments, the line and an empty table. --- If the line matched the pattern, then this function returns true --- and the table is filled with field-value pairs. --- @param spec a SIP pattern --- @param options optional table; {at_start=true} ensures that the pattern --- is anchored at the start of the string. --- @return a function if successful, or nil,error -function sip.compile(spec,options) - assert_arg(1,spec,'string') - local fun,names = sip.create_spec_fun(spec,options) - if not fun then return nil,names end - if rawget(_G,'_DEBUG') then print(fun) end - local chunk,err = loadstring(fun,'tmp') - if err then return nil,err end - return chunk(),names -end - -local cache = {} - ---- match a SIP pattern against a string. --- @param spec a SIP pattern --- @param line a string --- @param res a table to receive values --- @param options (optional) option table --- @return true or false -function sip.match (spec,line,res,options) - assert_arg(1,spec,'string') - assert_arg(2,line,'string') - assert_arg(3,res,'table') - if not cache[spec] then - cache[spec] = sip.compile(spec,options) - end - return cache[spec](line,res) -end - ---- match a SIP pattern against the start of a string. --- @param spec a SIP pattern --- @param line a string --- @param res a table to receive values --- @return true or false -function sip.match_at_start (spec,line,res) - return sip.match(spec,line,res,{at_start=true}) -end - ---- given a pattern and a file object, return an iterator over the results --- @param spec a SIP pattern --- @param f a file-like object. -function sip.fields (spec,f) - assert_arg(1,spec,'string') - if not f then return nil,"no file object" end - local fun,err = sip.compile(spec) - if not fun then return nil,err end - local res = {} - return function() - while true do - local line = f:read() - if not line then return end - if fun(line,res) then - local values = res - res = {} - return unpack(values) - end - end - end -end - -local read_patterns = {} - ---- register a match which will be used in the read function. --- @string spec a SIP pattern --- @func fun a function to be called with the results of the match --- @see read -function sip.pattern (spec,fun) - assert_arg(1,spec,'string') - local pat,named = sip.compile(spec) - append(read_patterns,{pat=pat,named=named,callback=fun}) -end - ---- enter a loop which applies all registered matches to the input file. --- @param f a file-like object --- @array matches optional list of `{spec,fun}` pairs, as for `pattern` above. -function sip.read (f,matches) - local owned,err - if not f then return nil,"no file object" end - if type(f) == 'string' then - f,err = io.open(f) - if not f then return nil,err end - owned = true - end - if matches then - for _,p in ipairs(matches) do - sip.pattern(p[1],p[2]) - end - end - local res = {} - for line in f:lines() do - for _,item in ipairs(read_patterns) do - if item.pat(line,res) then - if item.callback then - if item.named then - item.callback(res) - else - item.callback(unpack(res)) - end - end - res = {} - break - end - end - end - if owned then f:close() end -end - -return sip diff --git a/src/pl/strict.lua b/src/pl/strict.lua deleted file mode 100644 index 67cbf5b..0000000 --- a/src/pl/strict.lua +++ /dev/null @@ -1,138 +0,0 @@ ---- Checks uses of undeclared global variables. --- All global variables must be 'declared' through a regular assignment --- (even assigning `nil` will do) in a main chunk before being used --- anywhere or assigned to inside a function. Existing metatables `__newindex` and `__index` --- metamethods are respected. --- --- You can set any table to have strict behaviour using `strict.module`. Creating a new --- module with `strict.closed_module` makes the module immune to monkey-patching, if --- you don't wish to encourage monkey business. --- --- If the global `PENLIGHT_NO_GLOBAL_STRICT` is defined, then this module won't make the --- global environment strict - if you just want to explicitly set table strictness. --- --- @module pl.strict - -require 'debug' -- for Lua 5.2 -local getinfo, error, rawset, rawget = debug.getinfo, error, rawset, rawget -local strict = {} - -local function what () - local d = getinfo(3, "S") - return d and d.what or "C" -end - ---- make an existing table strict. --- @string[opt] name name of table --- @tab[opt] mod the table to protect - if `nil` then we'll return a new table --- @tab[opt] predeclared - table of variables that are to be considered predeclared. --- @return the given table, or a new table --- @usage --- local M = { hello = "world" } --- strict.module ("Awesome_Module", M, { --- Lua = true, -- defines allowed keys --- }) --- --- assert(M.hello == "world") --- assert(M.Lua == nil) -- access allowed, but has no value yet --- M.Lua = "Rocks" --- assert(M.Lua == "Rocks") --- M.not_allowed = "bad boy" -- throws an error -function strict.module (name,mod,predeclared) - local mt, old_newindex, old_index, old_index_type, global - if predeclared then - global = predeclared.__global - end - if type(mod) == 'table' then - mt = getmetatable(mod) - if mt and rawget(mt,'__declared') then return end -- already patched... - else - mod = {} - end - if mt == nil then - mt = {} - setmetatable(mod, mt) - else - old_newindex = mt.__newindex - old_index = mt.__index - old_index_type = type(old_index) - end - mt.__declared = predeclared or {} - mt.__newindex = function(t, n, v) - if old_newindex then - old_newindex(t, n, v) - if rawget(t,n)~=nil then return end - end - if not mt.__declared[n] then - if global then - local w = what() - if w ~= "main" and w ~= "C" then - error("assign to undeclared global '"..n.."'", 2) - end - end - mt.__declared[n] = true - end - rawset(t, n, v) - end - mt.__index = function(t,n) - if not mt.__declared[n] and what() ~= "C" then - if old_index then - if old_index_type == "table" then - local fallback = old_index[n] - if fallback ~= nil then - return fallback - end - else - local res = old_index(t, n) - if res ~= nil then - return res - end - end - end - local msg = "variable '"..n.."' is not declared" - if name then - msg = msg .. " in '"..tostring(name).."'" - end - error(msg, 2) - end - return rawget(t, n) - end - return mod -end - ---- make all tables in a table strict. --- So `strict.make_all_strict(_G)` prevents monkey-patching --- of any global table --- @tab T the table containing the tables to protect. Table `T` itself will NOT be protected. -function strict.make_all_strict (T) - for k,v in pairs(T) do - if type(v) == 'table' and v ~= T then - strict.module(k,v) - end - end -end - ---- make a new module table which is closed to further changes. --- @tab mod module table --- @string name module name -function strict.closed_module (mod,name) - -- No clue to what this is useful for? see tests - -- Deprecate this and remove??? - local M = {} - mod = mod or {} - local mt = getmetatable(mod) - if not mt then - mt = {} - setmetatable(mod,mt) - end - mt.__newindex = function(t,k,v) - M[k] = v - end - return strict.module(name,M) -end - -if not rawget(_G,'PENLIGHT_NO_GLOBAL_STRICT') then - strict.module(nil,_G,{_PROMPT=true,_PROMPT2=true,__global=true}) -end - -return strict diff --git a/src/pl/stringx.lua b/src/pl/stringx.lua deleted file mode 100644 index 75c61c7..0000000 --- a/src/pl/stringx.lua +++ /dev/null @@ -1,917 +0,0 @@ ---- Python-style extended string library. --- --- see 3.6.1 of the Python reference. --- If you want to make these available as string methods, then say --- `stringx.import()` to bring them into the standard `string` table. --- --- See @{03-strings.md|the Guide} --- --- Dependencies: `pl.utils`, `pl.types` --- @module pl.stringx -local utils = require 'pl.utils' -local is_callable = require 'pl.types'.is_callable -local string = string -local find = string.find -local type,setmetatable,ipairs = type,setmetatable,ipairs -local error = error -local gsub = string.gsub -local rep = string.rep -local sub = string.sub -local reverse = string.reverse -local concat = table.concat -local append = table.insert -local remove = table.remove -local escape = utils.escape -local ceil, max = math.ceil, math.max -local assert_arg,usplit = utils.assert_arg,utils.split -local lstrip -local unpack = utils.unpack -local pack = utils.pack - -local function assert_string (n,s) - assert_arg(n,s,'string') -end - -local function non_empty(s) - return #s > 0 -end - -local function assert_nonempty_string(n,s) - assert_arg(n,s,'string',non_empty,'must be a non-empty string') -end - -local function makelist(l) - return setmetatable(l, require('pl.List')) -end - -local stringx = {} - ------------------- --- String Predicates --- @section predicates - ---- does s only contain alphabetic characters? --- @string s a string -function stringx.isalpha(s) - assert_string(1,s) - return find(s,'^%a+$') == 1 -end - ---- does s only contain digits? --- @string s a string -function stringx.isdigit(s) - assert_string(1,s) - return find(s,'^%d+$') == 1 -end - ---- does s only contain alphanumeric characters? --- @string s a string -function stringx.isalnum(s) - assert_string(1,s) - return find(s,'^%w+$') == 1 -end - ---- does s only contain whitespace? --- Matches on pattern '%s' so matches space, newline, tabs, etc. --- @string s a string -function stringx.isspace(s) - assert_string(1,s) - return find(s,'^%s+$') == 1 -end - ---- does s only contain lower case characters? --- @string s a string -function stringx.islower(s) - assert_string(1,s) - return find(s,'^[%l%s]+$') == 1 -end - ---- does s only contain upper case characters? --- @string s a string -function stringx.isupper(s) - assert_string(1,s) - return find(s,'^[%u%s]+$') == 1 -end - -local function raw_startswith(s, prefix) - return find(s,prefix,1,true) == 1 -end - -local function raw_endswith(s, suffix) - return #s >= #suffix and find(s, suffix, #s-#suffix+1, true) and true or false -end - -local function test_affixes(s, affixes, fn) - if type(affixes) == 'string' then - return fn(s,affixes) - elseif type(affixes) == 'table' then - for _,affix in ipairs(affixes) do - if fn(s,affix) then return true end - end - return false - else - error(("argument #2 expected a 'string' or a 'table', got a '%s'"):format(type(affixes))) - end -end - ---- does s start with prefix or one of prefixes? --- @string s a string --- @param prefix a string or an array of strings -function stringx.startswith(s,prefix) - assert_string(1,s) - return test_affixes(s,prefix,raw_startswith) -end - ---- does s end with suffix or one of suffixes? --- @string s a string --- @param suffix a string or an array of strings -function stringx.endswith(s,suffix) - assert_string(1,s) - return test_affixes(s,suffix,raw_endswith) -end - ---- Strings and Lists --- @section lists - ---- concatenate the strings using this string as a delimiter. --- Note that the arguments are reversed from `string.concat`. --- @string s the string --- @param seq a table of strings or numbers --- @usage stringx.join(' ', {1,2,3}) == '1 2 3' -function stringx.join(s,seq) - assert_string(1,s) - return concat(seq,s) -end - ---- Split a string into a list of lines. --- `"\r"`, `"\n"`, and `"\r\n"` are considered line ends. --- They are not included in the lines unless `keepends` is passed. --- Terminal line end does not produce an extra line. --- Splitting an empty string results in an empty list. --- @string s the string. --- @bool[opt] keep_ends include line ends. --- @return List of lines -function stringx.splitlines(s, keep_ends) - assert_string(1, s) - local res = {} - local pos = 1 - while true do - local line_end_pos = find(s, '[\r\n]', pos) - if not line_end_pos then - break - end - - local line_end = sub(s, line_end_pos, line_end_pos) - if line_end == '\r' and sub(s, line_end_pos + 1, line_end_pos + 1) == '\n' then - line_end = '\r\n' - end - - local line = sub(s, pos, line_end_pos - 1) - if keep_ends then - line = line .. line_end - end - append(res, line) - - pos = line_end_pos + #line_end - end - - if pos <= #s then - append(res, sub(s, pos)) - end - return makelist(res) -end - ---- split a string into a list of strings using a delimiter. --- @function split --- @string s the string --- @string[opt] re a delimiter (defaults to whitespace) --- @int[opt] n maximum number of results --- @return List --- @usage #(stringx.split('one two')) == 2 --- @usage stringx.split('one,two,three', ',') == List{'one','two','three'} --- @usage stringx.split('one,two,three', ',', 2) == List{'one','two,three'} -function stringx.split(s,re,n) - assert_string(1,s) - local plain = true - if not re then -- default spaces - s = lstrip(s) - plain = false - end - local res = usplit(s,re,plain,n) - if re and re ~= '' and - find(s,re,-#re,true) and - (n or math.huge) > #res then - res[#res+1] = "" - end - return makelist(res) -end - ---- replace all tabs in s with tabsize spaces. If not specified, tabsize defaults to 8. --- Tab stops will be honored. --- @string s the string --- @int tabsize[opt=8] number of spaces to expand each tab --- @return expanded string --- @usage stringx.expandtabs('\tone,two,three', 4) == ' one,two,three' --- @usage stringx.expandtabs(' \tone,two,three', 4) == ' one,two,three' -function stringx.expandtabs(s,tabsize) - assert_string(1,s) - tabsize = tabsize or 8 - return (s:gsub("([^\t\r\n]*)\t", function(before_tab) - if tabsize == 0 then - return before_tab - else - return before_tab .. (" "):rep(tabsize - #before_tab % tabsize) - end - end)) -end - ---- Finding and Replacing --- @section find - -local function _find_all(s,sub,first,last,allow_overlap) - first = first or 1 - last = last or #s - if sub == '' then return last+1,last-first+1 end - local i1,i2 = find(s,sub,first,true) - local res - local k = 0 - while i1 do - if last and i2 > last then break end - res = i1 - k = k + 1 - if allow_overlap then - i1,i2 = find(s,sub,i1+1,true) - else - i1,i2 = find(s,sub,i2+1,true) - end - end - return res,k -end - ---- find index of first instance of sub in s from the left. --- @string s the string --- @string sub substring --- @int[opt] first first index --- @int[opt] last last index --- @return start index, or nil if not found -function stringx.lfind(s,sub,first,last) - assert_string(1,s) - assert_string(2,sub) - local i1, i2 = find(s,sub,first,true) - - if i1 and (not last or i2 <= last) then - return i1 - else - return nil - end -end - ---- find index of first instance of sub in s from the right. --- @string s the string --- @string sub substring --- @int[opt] first first index --- @int[opt] last last index --- @return start index, or nil if not found -function stringx.rfind(s,sub,first,last) - assert_string(1,s) - assert_string(2,sub) - return (_find_all(s,sub,first,last,true)) -end - ---- replace up to n instances of old by new in the string s. --- If n is not present, replace all instances. --- @string s the string --- @string old the target substring --- @string new the substitution --- @int[opt] n optional maximum number of substitutions --- @return result string -function stringx.replace(s,old,new,n) - assert_string(1,s) - assert_string(2,old) - assert_string(3,new) - return (gsub(s,escape(old),new:gsub('%%','%%%%'),n)) -end - ---- count all instances of substring in string. --- @string s the string --- @string sub substring --- @bool[opt] allow_overlap allow matches to overlap --- @usage --- assert(stringx.count('banana', 'ana') == 1) --- assert(stringx.count('banana', 'ana', true) == 2) -function stringx.count(s,sub,allow_overlap) - assert_string(1,s) - local _,k = _find_all(s,sub,1,false,allow_overlap) - return k -end - ---- Stripping and Justifying --- @section strip - -local function _just(s,w,ch,left,right) - local n = #s - if w > n then - if not ch then ch = ' ' end - local f1,f2 - if left and right then - local rn = ceil((w-n)/2) - local ln = w - n - rn - f1 = rep(ch,ln) - f2 = rep(ch,rn) - elseif right then - f1 = rep(ch,w-n) - f2 = '' - else - f2 = rep(ch,w-n) - f1 = '' - end - return f1..s..f2 - else - return s - end -end - ---- left-justify s with width w. --- @string s the string --- @int w width of justification --- @string[opt=' '] ch padding character --- @usage stringx.ljust('hello', 10, '*') == '*****hello' -function stringx.ljust(s,w,ch) - assert_string(1,s) - assert_arg(2,w,'number') - return _just(s,w,ch,true,false) -end - ---- right-justify s with width w. --- @string s the string --- @int w width of justification --- @string[opt=' '] ch padding character --- @usage stringx.rjust('hello', 10, '*') == 'hello*****' -function stringx.rjust(s,w,ch) - assert_string(1,s) - assert_arg(2,w,'number') - return _just(s,w,ch,false,true) -end - ---- center-justify s with width w. --- @string s the string --- @int w width of justification --- @string[opt=' '] ch padding character --- @usage stringx.center('hello', 10, '*') == '**hello***' -function stringx.center(s,w,ch) - assert_string(1,s) - assert_arg(2,w,'number') - return _just(s,w,ch,true,true) -end - -local function _strip(s,left,right,chrs) - if not chrs then - chrs = '%s' - else - chrs = '['..escape(chrs)..']' - end - local f = 1 - local t - if left then - local i1,i2 = find(s,'^'..chrs..'*') - if i2 >= i1 then - f = i2+1 - end - end - if right then - if #s < 200 then - local i1,i2 = find(s,chrs..'*$',f) - if i2 >= i1 then - t = i1-1 - end - else - local rs = reverse(s) - local i1,i2 = find(rs, '^'..chrs..'*') - if i2 >= i1 then - t = -i2-1 - end - end - end - return sub(s,f,t) -end - ---- trim any characters on the left of s. --- @string s the string --- @string[opt='%s'] chrs default any whitespace character, --- but can be a string of characters to be trimmed -function stringx.lstrip(s,chrs) - assert_string(1,s) - return _strip(s,true,false,chrs) -end -lstrip = stringx.lstrip - ---- trim any characters on the right of s. --- @string s the string --- @string[opt='%s'] chrs default any whitespace character, --- but can be a string of characters to be trimmed -function stringx.rstrip(s,chrs) - assert_string(1,s) - return _strip(s,false,true,chrs) -end - ---- trim any characters on both left and right of s. --- @string s the string --- @string[opt='%s'] chrs default any whitespace character, --- but can be a string of characters to be trimmed --- @usage stringx.strip(' --== Hello ==-- ', "- =") --> 'Hello' -function stringx.strip(s,chrs) - assert_string(1,s) - return _strip(s,true,true,chrs) -end - ---- Partitioning Strings --- @section partitioning - ---- split a string using a pattern. Note that at least one value will be returned! --- @string s the string --- @string[opt='%s'] re a Lua string pattern (defaults to whitespace) --- @return the parts of the string --- @usage a,b = line:splitv('=') --- @see utils.splitv -function stringx.splitv(s,re) - assert_string(1,s) - return utils.splitv(s,re) -end - --- The partition functions split a string using a delimiter into three parts: --- the part before, the delimiter itself, and the part afterwards -local function _partition(p,delim,fn) - local i1,i2 = fn(p,delim) - if not i1 or i1 == -1 then - return p,'','' - else - if not i2 then i2 = i1 end - return sub(p,1,i1-1),sub(p,i1,i2),sub(p,i2+1) - end -end - ---- partition the string using first occurance of a delimiter --- @string s the string --- @string ch delimiter (match as plain string, no patterns) --- @return part before ch --- @return ch --- @return part after ch --- @usage {stringx.partition('a,b,c', ','))} == {'a', ',', 'b,c'} --- @usage {stringx.partition('abc', 'x'))} == {'abc', '', ''} -function stringx.partition(s,ch) - assert_string(1,s) - assert_nonempty_string(2,ch) - return _partition(s,ch,stringx.lfind) -end - ---- partition the string p using last occurance of a delimiter --- @string s the string --- @string ch delimiter (match as plain string, no patterns) --- @return part before ch --- @return ch --- @return part after ch --- @usage {stringx.rpartition('a,b,c', ','))} == {'a,b', ',', 'c'} --- @usage {stringx.rpartition('abc', 'x'))} == {'', '', 'abc'} -function stringx.rpartition(s,ch) - assert_string(1,s) - assert_nonempty_string(2,ch) - local a,b,c = _partition(s,ch,stringx.rfind) - if a == s then -- no match found - return c,b,a - end - return a,b,c -end - ---- return the 'character' at the index. --- @string s the string --- @int idx an index (can be negative) --- @return a substring of length 1 if successful, empty string otherwise. -function stringx.at(s,idx) - assert_string(1,s) - assert_arg(2,idx,'number') - return sub(s,idx,idx) -end - - ---- Text handling --- @section text - - ---- indent a multiline string. --- @tparam string s the (multiline) string --- @tparam integer n the size of the indent --- @tparam[opt=' '] string ch the character to use when indenting --- @return indented string -function stringx.indent (s,n,ch) - assert_arg(1,s,'string') - assert_arg(2,n,'number') - local lines = usplit(s ,'\n') - local prefix = string.rep(ch or ' ',n) - for i, line in ipairs(lines) do - lines[i] = prefix..line - end - return concat(lines,'\n')..'\n' -end - - ---- dedent a multiline string by removing any initial indent. --- useful when working with [[..]] strings. --- Empty lines are ignored. --- @tparam string s the (multiline) string --- @return a string with initial indent zero. --- @usage --- local s = dedent [[ --- One --- --- Two --- --- Three --- ]] --- assert(s == [[ --- One --- --- Two --- --- Three --- ]]) -function stringx.dedent (s) - assert_arg(1,s,'string') - local lst = usplit(s,'\n') - if #lst>0 then - local ind_size = math.huge - for i, line in ipairs(lst) do - local i1, i2 = lst[i]:find('^%s*[^%s]') - if i1 and i2 < ind_size then - ind_size = i2 - end - end - for i, line in ipairs(lst) do - lst[i] = lst[i]:sub(ind_size, -1) - end - end - return concat(lst,'\n')..'\n' -end - - - -do - local buildline = function(words, size, breaklong) - -- if overflow is set, a word longer than size, will overflow the size - -- otherwise it will be chopped in line-length pieces - local line = {} - if #words[1] > size then - -- word longer than line - if not breaklong then - line[1] = words[1] - remove(words, 1) - else - line[1] = words[1]:sub(1, size) - words[1] = words[1]:sub(size + 1, -1) - end - else - local len = 0 - while words[1] and (len + #words[1] <= size) or - (len == 0 and #words[1] == size) do - if words[1] ~= "" then - line[#line+1] = words[1] - len = len + #words[1] + 1 - end - remove(words, 1) - end - end - return stringx.strip(concat(line, " ")), words - end - - --- format a paragraph into lines so that they fit into a line width. - -- It will not break long words by default, so lines can be over the length - -- to that extent. - -- @tparam string s the string to format - -- @tparam[opt=70] integer width the margin width - -- @tparam[opt=false] boolean breaklong if truthy, words longer than the width given will be forced split. - -- @return a list of lines (List object), use `fill` to return a string instead of a `List`. - -- @see pl.List - -- @see fill - stringx.wrap = function(s, width, breaklong) - s = s:gsub('\n',' ') -- remove line breaks - s = stringx.strip(s) -- remove leading/trailing whitespace - if s == "" then - return { "" } - end - width = width or 70 - local out = {} - local words = usplit(s, "%s") - while words[1] do - out[#out+1], words = buildline(words, width, breaklong) - end - return makelist(out) - end -end - ---- format a paragraph so that it fits into a line width. --- @tparam string s the string to format --- @tparam[opt=70] integer width the margin width --- @tparam[opt=false] boolean breaklong if truthy, words longer than the width given will be forced split. --- @return a string, use `wrap` to return a list of lines instead of a string. --- @see wrap -function stringx.fill (s,width,breaklong) - return concat(stringx.wrap(s,width,breaklong),'\n') .. '\n' -end - ---- Template --- @section Template - - -local function _substitute(s,tbl,safe) - local subst - if is_callable(tbl) then - subst = tbl - else - function subst(f) - local s = tbl[f] - if not s then - if safe then - return f - else - error("not present in table "..f) - end - else - return s - end - end - end - local res = gsub(s,'%${([%w_]+)}',subst) - return (gsub(res,'%$([%w_]+)',subst)) -end - - - -local Template = {} -stringx.Template = Template -Template.__index = Template -setmetatable(Template, { - __call = function(obj,tmpl) - return Template.new(tmpl) - end -}) - ---- Creates a new Template class. --- This is a shortcut to `Template.new(tmpl)`. --- @tparam string tmpl the template string --- @function Template --- @treturn Template -function Template.new(tmpl) - assert_arg(1,tmpl,'string') - local res = {} - res.tmpl = tmpl - setmetatable(res,Template) - return res -end - ---- substitute values into a template, throwing an error. --- This will throw an error if no name is found. --- @tparam table tbl a table of name-value pairs. --- @return string with place holders substituted -function Template:substitute(tbl) - assert_arg(1,tbl,'table') - return _substitute(self.tmpl,tbl,false) -end - ---- substitute values into a template. --- This version just passes unknown names through. --- @tparam table tbl a table of name-value pairs. --- @return string with place holders substituted -function Template:safe_substitute(tbl) - assert_arg(1,tbl,'table') - return _substitute(self.tmpl,tbl,true) -end - ---- substitute values into a template, preserving indentation.
--- If the value is a multiline string _or_ a template, it will insert --- the lines at the correct indentation.
--- Furthermore, if a template, then that template will be substituted --- using the same table. --- @tparam table tbl a table of name-value pairs. --- @return string with place holders substituted -function Template:indent_substitute(tbl) - assert_arg(1,tbl,'table') - if not self.strings then - self.strings = usplit(self.tmpl,'\n') - end - - -- the idea is to substitute line by line, grabbing any spaces as - -- well as the $var. If the value to be substituted contains newlines, - -- then we split that into lines and adjust the indent before inserting. - local function subst(line) - return line:gsub('(%s*)%$([%w_]+)',function(sp,f) - local subtmpl - local s = tbl[f] - if not s then error("not present in table "..f) end - if getmetatable(s) == Template then - subtmpl = s - s = s.tmpl - else - s = tostring(s) - end - if s:find '\n' then - local lines = usplit(s, '\n') - for i, line in ipairs(lines) do - lines[i] = sp..line - end - s = concat(lines, '\n') .. '\n' - end - if subtmpl then - return _substitute(s, tbl) - else - return s - end - end) - end - - local lines = {} - for i, line in ipairs(self.strings) do - lines[i] = subst(line) - end - return concat(lines,'\n')..'\n' -end - - - ---- Miscelaneous --- @section misc - ---- return an iterator over all lines in a string --- @string s the string --- @return an iterator --- @usage --- local line_no = 1 --- for line in stringx.lines(some_text) do --- print(line_no, line) --- line_no = line_no + 1 --- end -function stringx.lines(s) - assert_string(1,s) - if not s:find '\n$' then s = s..'\n' end - return s:gmatch('([^\n]*)\n') -end - ---- inital word letters uppercase ('title case'). --- Here 'words' mean chunks of non-space characters. --- @string s the string --- @return a string with each word's first letter uppercase --- @usage stringx.title("hello world") == "Hello World") -function stringx.title(s) - assert_string(1,s) - return (s:gsub('(%S)(%S*)',function(f,r) - return f:upper()..r:lower() - end)) -end - -stringx.capitalize = stringx.title - -do - local ellipsis = '...' - local n_ellipsis = #ellipsis - - --- Return a shortened version of a string. - -- Fits string within w characters. Removed characters are marked with ellipsis. - -- @string s the string - -- @int w the maxinum size allowed - -- @bool tail true if we want to show the end of the string (head otherwise) - -- @usage ('1234567890'):shorten(8) == '12345...' - -- @usage ('1234567890'):shorten(8, true) == '...67890' - -- @usage ('1234567890'):shorten(20) == '1234567890' - function stringx.shorten(s,w,tail) - assert_string(1,s) - if #s > w then - if w < n_ellipsis then return ellipsis:sub(1,w) end - if tail then - local i = #s - w + 1 + n_ellipsis - return ellipsis .. s:sub(i) - else - return s:sub(1,w-n_ellipsis) .. ellipsis - end - end - return s - end -end - - -do - -- Utility function that finds any patterns that match a long string's an open or close. - -- Note that having this function use the least number of equal signs that is possible is a harder algorithm to come up with. - -- Right now, it simply returns the greatest number of them found. - -- @param s The string - -- @return 'nil' if not found. If found, the maximum number of equal signs found within all matches. - local function has_lquote(s) - local lstring_pat = '([%[%]])(=*)%1' - local equals, new_equals, _ - local finish = 1 - repeat - _, finish, _, new_equals = s:find(lstring_pat, finish) - if new_equals then - equals = max(equals or 0, #new_equals) - end - until not new_equals - - return equals - end - - --- Quote the given string and preserve any control or escape characters, such that reloading the string in Lua returns the same result. - -- @param s The string to be quoted. - -- @return The quoted string. - function stringx.quote_string(s) - assert_string(1,s) - -- Find out if there are any embedded long-quote sequences that may cause issues. - -- This is important when strings are embedded within strings, like when serializing. - -- Append a closing bracket to catch unfinished long-quote sequences at the end of the string. - local equal_signs = has_lquote(s .. "]") - - -- Note that strings containing "\r" can't be quoted using long brackets - -- as Lua lexer converts all newlines to "\n" within long strings. - if (s:find("\n") or equal_signs) and not s:find("\r") then - -- If there is an embedded sequence that matches a long quote, then - -- find the one with the maximum number of = signs and add one to that number. - equal_signs = ("="):rep((equal_signs or -1) + 1) - -- Long strings strip out leading newline. We want to retain that, when quoting. - if s:find("^\n") then s = "\n" .. s end - local lbracket, rbracket = - "[" .. equal_signs .. "[", - "]" .. equal_signs .. "]" - s = lbracket .. s .. rbracket - else - -- Escape funny stuff. Lua 5.1 does not handle "\r" correctly. - s = ("%q"):format(s):gsub("\r", "\\r") - end - return s - end -end - - ---- Python-style formatting operator. --- Calling `text.format_operator()` overloads the % operator for strings to give --- Python/Ruby style formated output. --- This is extended to also do template-like substitution for map-like data. --- --- Note this goes further than the original, and will allow these cases: --- --- 1. a single value --- 2. a list of values --- 3. a map of var=value pairs --- 4. a function, as in gsub --- --- For the second two cases, it uses $-variable substituion. --- --- When called, this function will monkey-patch the global `string` metatable by --- adding a `__mod` method. --- --- See the lua-users wiki --- --- @usage --- require 'pl.text'.format_operator() --- local out1 = '%s = %5.3f' % {'PI',math.pi} --> 'PI = 3.142' --- local out2 = '$name = $value' % {name='dog',value='Pluto'} --> 'dog = Pluto' -function stringx.format_operator() - - local format = string.format - - -- a more forgiving version of string.format, which applies - -- tostring() to any value with a %s format. - local function formatx (fmt,...) - local args = pack(...) - local i = 1 - for p in fmt:gmatch('%%.') do - if p == '%s' and type(args[i]) ~= 'string' then - args[i] = tostring(args[i]) - end - i = i + 1 - end - return format(fmt,unpack(args)) - end - - local function basic_subst(s,t) - return (s:gsub('%$([%w_]+)',t)) - end - - getmetatable("").__mod = function(a, b) - if b == nil then - return a - elseif type(b) == "table" and getmetatable(b) == nil then - if #b == 0 then -- assume a map-like table - return _substitute(a,b,true) - else - return formatx(a,unpack(b)) - end - elseif type(b) == 'function' then - return basic_subst(a,b) - else - return formatx(a,b) - end - end -end - ---- import the stringx functions into the global string (meta)table -function stringx.import() - utils.import(stringx,string) -end - -return stringx diff --git a/src/pl/tablex.lua b/src/pl/tablex.lua deleted file mode 100644 index 81f7395..0000000 --- a/src/pl/tablex.lua +++ /dev/null @@ -1,999 +0,0 @@ ---- Extended operations on Lua tables. --- --- See @{02-arrays.md.Useful_Operations_on_Tables|the Guide} --- --- Dependencies: `pl.utils`, `pl.types` --- @module pl.tablex -local utils = require ('pl.utils') -local types = require ('pl.types') -local getmetatable,setmetatable,require = getmetatable,setmetatable,require -local tsort,append,remove = table.sort,table.insert,table.remove -local min = math.min -local pairs,type,unpack,select,tostring = pairs,type,utils.unpack,select,tostring -local function_arg = utils.function_arg -local assert_arg = utils.assert_arg - -local tablex = {} - --- generally, functions that make copies of tables try to preserve the metatable. --- However, when the source has no obvious type, then we attach appropriate metatables --- like List, Map, etc to the result. -local function setmeta (res,tbl,pl_class) - local mt = getmetatable(tbl) or pl_class and require('pl.' .. pl_class) - return mt and setmetatable(res, mt) or res -end - -local function makelist(l) - return setmetatable(l, require('pl.List')) -end - -local function makemap(m) - return setmetatable(m, require('pl.Map')) -end - -local function complain (idx,msg) - error(('argument %d is not %s'):format(idx,msg),3) -end - -local function assert_arg_indexable (idx,val) - if not types.is_indexable(val) then - complain(idx,"indexable") - end -end - -local function assert_arg_iterable (idx,val) - if not types.is_iterable(val) then - complain(idx,"iterable") - end -end - -local function assert_arg_writeable (idx,val) - if not types.is_writeable(val) then - complain(idx,"writeable") - end -end - ---- copy a table into another, in-place. --- @within Copying --- @tab t1 destination table --- @tab t2 source (actually any iterable object) --- @return first table -function tablex.update (t1,t2) - assert_arg_writeable(1,t1) - assert_arg_iterable(2,t2) - for k,v in pairs(t2) do - t1[k] = v - end - return t1 -end - ---- total number of elements in this table. --- Note that this is distinct from `#t`, which is the number --- of values in the array part; this value will always --- be greater or equal. The difference gives the size of --- the hash part, for practical purposes. Works for any --- object with a __pairs metamethod. --- @tab t a table --- @return the size -function tablex.size (t) - assert_arg_iterable(1,t) - local i = 0 - for k in pairs(t) do i = i + 1 end - return i -end - ---- make a shallow copy of a table --- @within Copying --- @tab t an iterable source --- @return new table -function tablex.copy (t) - assert_arg_iterable(1,t) - local res = {} - for k,v in pairs(t) do - res[k] = v - end - return res -end - -local function cycle_aware_copy(t, cache) - if type(t) ~= 'table' then return t end - if cache[t] then return cache[t] end - assert_arg_iterable(1,t) - local res = {} - cache[t] = res - local mt = getmetatable(t) - for k,v in pairs(t) do - k = cycle_aware_copy(k, cache) - v = cycle_aware_copy(v, cache) - res[k] = v - end - setmetatable(res,mt) - return res -end - ---- make a deep copy of a table, recursively copying all the keys and fields. --- This supports cycles in tables; cycles will be reproduced in the copy. --- This will also set the copied table's metatable to that of the original. --- @within Copying --- @tab t A table --- @return new table -function tablex.deepcopy(t) - return cycle_aware_copy(t,{}) -end - -local abs = math.abs - -local function cycle_aware_compare(t1,t2,ignore_mt,eps,cache) - if cache[t1] and cache[t1][t2] then return true end - local ty1 = type(t1) - local ty2 = type(t2) - if ty1 ~= ty2 then return false end - -- non-table types can be directly compared - if ty1 ~= 'table' then - if ty1 == 'number' and eps then return abs(t1-t2) < eps end - return t1 == t2 - end - -- as well as tables which have the metamethod __eq - local mt = getmetatable(t1) - if not ignore_mt and mt and mt.__eq then return t1 == t2 end - for k1 in pairs(t1) do - if t2[k1]==nil then return false end - end - for k2 in pairs(t2) do - if t1[k2]==nil then return false end - end - cache[t1] = cache[t1] or {} - cache[t1][t2] = true - for k1,v1 in pairs(t1) do - local v2 = t2[k1] - if not cycle_aware_compare(v1,v2,ignore_mt,eps,cache) then return false end - end - return true -end - ---- compare two values. --- if they are tables, then compare their keys and fields recursively. --- @within Comparing --- @param t1 A value --- @param t2 A value --- @bool[opt] ignore_mt if true, ignore __eq metamethod (default false) --- @number[opt] eps if defined, then used for any number comparisons --- @return true or false -function tablex.deepcompare(t1,t2,ignore_mt,eps) - return cycle_aware_compare(t1,t2,ignore_mt,eps,{}) -end - ---- compare two arrays using a predicate. --- @within Comparing --- @array t1 an array --- @array t2 an array --- @func cmp A comparison function; `bool = cmp(t1_value, t2_value)` --- @return true or false --- @usage --- assert(tablex.compare({ 1, 2, 3 }, { 1, 2, 3 }, "==")) --- --- assert(tablex.compare( --- {1,2,3, hello = "world"}, -- fields are not compared! --- {1,2,3}, function(v1, v2) return v1 == v2 end) -function tablex.compare (t1,t2,cmp) - assert_arg_indexable(1,t1) - assert_arg_indexable(2,t2) - if #t1 ~= #t2 then return false end - cmp = function_arg(3,cmp) - for k = 1,#t1 do - if not cmp(t1[k],t2[k]) then return false end - end - return true -end - ---- compare two list-like tables using an optional predicate, without regard for element order. --- @within Comparing --- @array t1 a list-like table --- @array t2 a list-like table --- @param cmp A comparison function (may be nil) -function tablex.compare_no_order (t1,t2,cmp) - assert_arg_indexable(1,t1) - assert_arg_indexable(2,t2) - if cmp then cmp = function_arg(3,cmp) end - if #t1 ~= #t2 then return false end - local visited = {} - for i = 1,#t1 do - local val = t1[i] - local gotcha - for j = 1,#t2 do - if not visited[j] then - local match - if cmp then match = cmp(val,t2[j]) else match = val == t2[j] end - if match then - gotcha = j - break - end - end - end - if not gotcha then return false end - visited[gotcha] = true - end - return true -end - - ---- return the index of a value in a list. --- Like string.find, there is an optional index to start searching, --- which can be negative. --- @within Finding --- @array t A list-like table --- @param val A value --- @int idx index to start; -1 means last element,etc (default 1) --- @return index of value or nil if not found --- @usage find({10,20,30},20) == 2 --- @usage find({'a','b','a','c'},'a',2) == 3 -function tablex.find(t,val,idx) - assert_arg_indexable(1,t) - idx = idx or 1 - if idx < 0 then idx = #t + idx + 1 end - for i = idx,#t do - if t[i] == val then return i end - end - return nil -end - ---- return the index of a value in a list, searching from the end. --- Like string.find, there is an optional index to start searching, --- which can be negative. --- @within Finding --- @array t A list-like table --- @param val A value --- @param idx index to start; -1 means last element,etc (default `#t`) --- @return index of value or nil if not found --- @usage rfind({10,10,10},10) == 3 -function tablex.rfind(t,val,idx) - assert_arg_indexable(1,t) - idx = idx or #t - if idx < 0 then idx = #t + idx + 1 end - for i = idx,1,-1 do - if t[i] == val then return i end - end - return nil -end - - ---- return the index (or key) of a value in a table using a comparison function. --- --- *NOTE*: the 2nd return value of this function, the value returned --- by the comparison function, has a limitation that it cannot be `false`. --- Because if it is, then it indicates the comparison failed, and the --- function will continue the search. See examples. --- @within Finding --- @tab t A table --- @func cmp A comparison function --- @param arg an optional second argument to the function --- @return index of value, or nil if not found --- @return value returned by comparison function (cannot be `false`!) --- @usage --- -- using an operator --- local lst = { "Rudolph", true, false, 15 } --- local idx, cmp_result = tablex.rfind(lst, "==", "Rudolph") --- assert(idx == 1) --- assert(cmp_result == true) --- --- local idx, cmp_result = tablex.rfind(lst, "==", false) --- assert(idx == 3) --- assert(cmp_result == true) -- looking up 'false' works! --- --- -- using a function returning the value looked up --- local cmp = function(v1, v2) return v1 == v2 and v2 end --- local idx, cmp_result = tablex.rfind(lst, cmp, "Rudolph") --- assert(idx == 1) --- assert(cmp_result == "Rudolph") -- the value is returned --- --- -- NOTE: this fails, since 'false' cannot be returned! --- local idx, cmp_result = tablex.rfind(lst, cmp, false) --- assert(idx == nil) -- looking up 'false' failed! --- assert(cmp_result == nil) -function tablex.find_if(t,cmp,arg) - assert_arg_iterable(1,t) - cmp = function_arg(2,cmp) - for k,v in pairs(t) do - local c = cmp(v,arg) - if c then return k,c end - end - return nil -end - ---- return a list of all values in a table indexed by another list. --- @tab tbl a table --- @array idx an index table (a list of keys) --- @return a list-like table --- @usage index_by({10,20,30,40},{2,4}) == {20,40} --- @usage index_by({one=1,two=2,three=3},{'one','three'}) == {1,3} -function tablex.index_by(tbl,idx) - assert_arg_indexable(1,tbl) - assert_arg_indexable(2,idx) - local res = {} - for i = 1,#idx do - res[i] = tbl[idx[i]] - end - return setmeta(res,tbl,'List') -end - ---- apply a function to all values of a table. --- This returns a table of the results. --- Any extra arguments are passed to the function. --- @within MappingAndFiltering --- @func fun A function that takes at least one argument --- @tab t A table --- @param ... optional arguments --- @usage map(function(v) return v*v end, {10,20,30,fred=2}) is {100,400,900,fred=4} -function tablex.map(fun,t,...) - assert_arg_iterable(1,t) - fun = function_arg(1,fun) - local res = {} - for k,v in pairs(t) do - res[k] = fun(v,...) - end - return setmeta(res,t) -end - ---- apply a function to all values of a list. --- This returns a table of the results. --- Any extra arguments are passed to the function. --- @within MappingAndFiltering --- @func fun A function that takes at least one argument --- @array t a table (applies to array part) --- @param ... optional arguments --- @return a list-like table --- @usage imap(function(v) return v*v end, {10,20,30,fred=2}) is {100,400,900} -function tablex.imap(fun,t,...) - assert_arg_indexable(1,t) - fun = function_arg(1,fun) - local res = {} - for i = 1,#t do - res[i] = fun(t[i],...) or false - end - return setmeta(res,t,'List') -end - ---- apply a named method to values from a table. --- @within MappingAndFiltering --- @string name the method name --- @array t a list-like table --- @param ... any extra arguments to the method --- @return a `List` with the results of the method (1st result only) --- @usage --- local Car = {} --- Car.__index = Car --- function Car.new(car) --- return setmetatable(car or {}, Car) --- end --- Car.speed = 0 --- function Car:faster(increase) --- self.speed = self.speed + increase --- return self.speed --- end --- --- local ferrari = Car.new{ name = "Ferrari" } --- local lamborghini = Car.new{ name = "Lamborghini", speed = 50 } --- local cars = { ferrari, lamborghini } --- --- assert(ferrari.speed == 0) --- assert(lamborghini.speed == 50) --- tablex.map_named_method("faster", cars, 10) --- assert(ferrari.speed == 10) --- assert(lamborghini.speed == 60) -function tablex.map_named_method (name,t,...) - utils.assert_string(1,name) - assert_arg_indexable(2,t) - local res = {} - for i = 1,#t do - local val = t[i] - local fun = val[name] - res[i] = fun(val,...) - end - return setmeta(res,t,'List') -end - ---- apply a function to all values of a table, in-place. --- Any extra arguments are passed to the function. --- @func fun A function that takes at least one argument --- @tab t a table --- @param ... extra arguments passed to `fun` --- @see tablex.foreach -function tablex.transform (fun,t,...) - assert_arg_iterable(1,t) - fun = function_arg(1,fun) - for k,v in pairs(t) do - t[k] = fun(v,...) - end -end - ---- generate a table of all numbers in a range. --- This is consistent with a numerical for loop. --- @int start number --- @int finish number --- @int[opt=1] step make this negative for start < finish -function tablex.range (start,finish,step) - local res - step = step or 1 - if start == finish then - res = {start} - elseif (start > finish and step > 0) or (finish > start and step < 0) then - res = {} - else - local k = 1 - res = {} - for i=start,finish,step do res[k]=i; k=k+1 end - end - return makelist(res) -end - ---- apply a function to values from two tables. --- @within MappingAndFiltering --- @func fun a function of at least two arguments --- @tab t1 a table --- @tab t2 a table --- @param ... extra arguments --- @return a table --- @usage map2('+',{1,2,3,m=4},{10,20,30,m=40}) is {11,22,23,m=44} -function tablex.map2 (fun,t1,t2,...) - assert_arg_iterable(1,t1) - assert_arg_iterable(2,t2) - fun = function_arg(1,fun) - local res = {} - for k,v in pairs(t1) do - res[k] = fun(v,t2[k],...) - end - return setmeta(res,t1,'List') -end - ---- apply a function to values from two arrays. --- The result will be the length of the shortest array. --- @within MappingAndFiltering --- @func fun a function of at least two arguments --- @array t1 a list-like table --- @array t2 a list-like table --- @param ... extra arguments --- @usage imap2('+',{1,2,3,m=4},{10,20,30,m=40}) is {11,22,23} -function tablex.imap2 (fun,t1,t2,...) - assert_arg_indexable(2,t1) - assert_arg_indexable(3,t2) - fun = function_arg(1,fun) - local res,n = {},math.min(#t1,#t2) - for i = 1,n do - res[i] = fun(t1[i],t2[i],...) - end - return res -end - ---- 'reduce' a list using a binary function. --- @func fun a function of two arguments --- @array t a list-like table --- @array memo optional initial memo value. Defaults to first value in table. --- @return the result of the function --- @usage reduce('+',{1,2,3,4}) == 10 -function tablex.reduce (fun,t,memo) - assert_arg_indexable(2,t) - fun = function_arg(1,fun) - local n = #t - if n == 0 then - return memo - end - local res = memo and fun(memo, t[1]) or t[1] - for i = 2,n do - res = fun(res,t[i]) - end - return res -end - ---- apply a function to all elements of a table. --- The arguments to the function will be the value, --- the key and _finally_ any extra arguments passed to this function. --- Note that the Lua 5.0 function table.foreach passed the _key_ first. --- @within Iterating --- @tab t a table --- @func fun a function on the elements; `function(value, key, ...)` --- @param ... extra arguments passed to `fun` --- @see tablex.transform -function tablex.foreach(t,fun,...) - assert_arg_iterable(1,t) - fun = function_arg(2,fun) - for k,v in pairs(t) do - fun(v,k,...) - end -end - ---- apply a function to all elements of a list-like table in order. --- The arguments to the function will be the value, --- the index and _finally_ any extra arguments passed to this function --- @within Iterating --- @array t a table --- @func fun a function with at least one argument --- @param ... optional arguments -function tablex.foreachi(t,fun,...) - assert_arg_indexable(1,t) - fun = function_arg(2,fun) - for i = 1,#t do - fun(t[i],i,...) - end -end - ---- Apply a function to a number of tables. --- A more general version of map --- The result is a table containing the result of applying that function to the --- ith value of each table. Length of output list is the minimum length of all the lists --- @within MappingAndFiltering --- @func fun a function of n arguments --- @tab ... n tables --- @usage mapn(function(x,y,z) return x+y+z end, {1,2,3},{10,20,30},{100,200,300}) is {111,222,333} --- @usage mapn(math.max, {1,20,300},{10,2,3},{100,200,100}) is {100,200,300} --- @param fun A function that takes as many arguments as there are tables -function tablex.mapn(fun,...) - fun = function_arg(1,fun) - local res = {} - local lists = {...} - local minn = 1e40 - for i = 1,#lists do - minn = min(minn,#(lists[i])) - end - for i = 1,minn do - local args,k = {},1 - for j = 1,#lists do - args[k] = lists[j][i] - k = k + 1 - end - res[#res+1] = fun(unpack(args)) - end - return res -end - ---- call the function with the key and value pairs from a table. --- The function can return a value and a key (note the order!). If both --- are not nil, then this pair is inserted into the result: if the key already exists, we convert the value for that --- key into a table and append into it. If only value is not nil, then it is appended to the result. --- @within MappingAndFiltering --- @func fun A function which will be passed each key and value as arguments, plus any extra arguments to pairmap. --- @tab t A table --- @param ... optional arguments --- @usage pairmap(function(k,v) return v end,{fred=10,bonzo=20}) is {10,20} _or_ {20,10} --- @usage pairmap(function(k,v) return {k,v},k end,{one=1,two=2}) is {one={'one',1},two={'two',2}} -function tablex.pairmap(fun,t,...) - assert_arg_iterable(1,t) - fun = function_arg(1,fun) - local res = {} - for k,v in pairs(t) do - local rv,rk = fun(k,v,...) - if rk then - if res[rk] then - if type(res[rk]) == 'table' then - table.insert(res[rk],rv) - else - res[rk] = {res[rk], rv} - end - else - res[rk] = rv - end - else - res[#res+1] = rv - end - end - return res -end - -local function keys_op(i,v) return i end - ---- return all the keys of a table in arbitrary order. --- @within Extraction --- @tab t A table -function tablex.keys(t) - assert_arg_iterable(1,t) - return makelist(tablex.pairmap(keys_op,t)) -end - -local function values_op(i,v) return v end - ---- return all the values of the table in arbitrary order --- @within Extraction --- @tab t A table -function tablex.values(t) - assert_arg_iterable(1,t) - return makelist(tablex.pairmap(values_op,t)) -end - -local function index_map_op (i,v) return i,v end - ---- create an index map from a list-like table. The original values become keys, --- and the associated values are the indices into the original list. --- @array t a list-like table --- @return a map-like table -function tablex.index_map (t) - assert_arg_indexable(1,t) - return makemap(tablex.pairmap(index_map_op,t)) -end - -local function set_op(i,v) return true,v end - ---- create a set from a list-like table. A set is a table where the original values --- become keys, and the associated values are all true. --- @array t a list-like table --- @return a set (a map-like table) -function tablex.makeset (t) - assert_arg_indexable(1,t) - return setmetatable(tablex.pairmap(set_op,t),require('pl.Set')) -end - ---- combine two tables, either as union or intersection. Corresponds to --- set operations for sets () but more general. Not particularly --- useful for list-like tables. --- @within Merging --- @tab t1 a table --- @tab t2 a table --- @bool dup true for a union, false for an intersection. --- @usage merge({alice=23,fred=34},{bob=25,fred=34}) is {fred=34} --- @usage merge({alice=23,fred=34},{bob=25,fred=34},true) is {bob=25,fred=34,alice=23} --- @see tablex.index_map -function tablex.merge (t1,t2,dup) - assert_arg_iterable(1,t1) - assert_arg_iterable(2,t2) - local res = {} - for k,v in pairs(t1) do - if dup or t2[k] then res[k] = v end - end - if dup then - for k,v in pairs(t2) do - res[k] = v - end - end - return setmeta(res,t1,'Map') -end - ---- the union of two map-like tables. --- If there are duplicate keys, the second table wins. --- @tab t1 a table --- @tab t2 a table --- @treturn tab --- @see tablex.merge -function tablex.union(t1, t2) - return tablex.merge(t1, t2, true) -end - ---- the intersection of two map-like tables. --- @tab t1 a table --- @tab t2 a table --- @treturn tab --- @see tablex.merge -function tablex.intersection(t1, t2) - return tablex.merge(t1, t2, false) -end - ---- a new table which is the difference of two tables. --- With sets (where the values are all true) this is set difference and --- symmetric difference depending on the third parameter. --- @within Merging --- @tab s1 a map-like table or set --- @tab s2 a map-like table or set --- @bool symm symmetric difference (default false) --- @return a map-like table or set -function tablex.difference (s1,s2,symm) - assert_arg_iterable(1,s1) - assert_arg_iterable(2,s2) - local res = {} - for k,v in pairs(s1) do - if s2[k] == nil then res[k] = v end - end - if symm then - for k,v in pairs(s2) do - if s1[k] == nil then res[k] = v end - end - end - return setmeta(res,s1,'Map') -end - ---- A table where the key/values are the values and value counts of the table. --- @array t a list-like table --- @func cmp a function that defines equality (otherwise uses ==) --- @return a map-like table --- @see seq.count_map -function tablex.count_map (t,cmp) - assert_arg_indexable(1,t) - local res,mask = {},{} - cmp = function_arg(2,cmp or '==') - local n = #t - for i = 1,#t do - local v = t[i] - if not mask[v] then - mask[v] = true - -- check this value against all other values - res[v] = 1 -- there's at least one instance - for j = i+1,n do - local w = t[j] - local ok = cmp(v,w) - if ok then - res[v] = res[v] + 1 - mask[w] = true - end - end - end - end - return makemap(res) -end - ---- filter an array's values using a predicate function --- @within MappingAndFiltering --- @array t a list-like table --- @func pred a boolean function --- @param arg optional argument to be passed as second argument of the predicate -function tablex.filter (t,pred,arg) - assert_arg_indexable(1,t) - pred = function_arg(2,pred) - local res,k = {},1 - for i = 1,#t do - local v = t[i] - if pred(v,arg) then - res[k] = v - k = k + 1 - end - end - return setmeta(res,t,'List') -end - ---- return a table where each element is a table of the ith values of an arbitrary --- number of tables. It is equivalent to a matrix transpose. --- @within Merging --- @usage zip({10,20,30},{100,200,300}) is {{10,100},{20,200},{30,300}} --- @array ... arrays to be zipped -function tablex.zip(...) - return tablex.mapn(function(...) return {...} end,...) -end - -local _copy -function _copy (dest,src,idest,isrc,nsrc,clean_tail) - idest = idest or 1 - isrc = isrc or 1 - local iend - if not nsrc then - nsrc = #src - iend = #src - else - iend = isrc + min(nsrc-1,#src-isrc) - end - if dest == src then -- special case - if idest > isrc and iend >= idest then -- overlapping ranges - src = tablex.sub(src,isrc,nsrc) - isrc = 1; iend = #src - end - end - for i = isrc,iend do - dest[idest] = src[i] - idest = idest + 1 - end - if clean_tail then - tablex.clear(dest,idest) - end - return dest -end - ---- copy an array into another one, clearing `dest` after `idest+nsrc`, if necessary. --- @within Copying --- @array dest a list-like table --- @array src a list-like table --- @int[opt=1] idest where to start copying values into destination --- @int[opt=1] isrc where to start copying values from source --- @int[opt=#src] nsrc number of elements to copy from source -function tablex.icopy (dest,src,idest,isrc,nsrc) - assert_arg_indexable(1,dest) - assert_arg_indexable(2,src) - return _copy(dest,src,idest,isrc,nsrc,true) -end - ---- copy an array into another one. --- @within Copying --- @array dest a list-like table --- @array src a list-like table --- @int[opt=1] idest where to start copying values into destination --- @int[opt=1] isrc where to start copying values from source --- @int[opt=#src] nsrc number of elements to copy from source -function tablex.move (dest,src,idest,isrc,nsrc) - assert_arg_indexable(1,dest) - assert_arg_indexable(2,src) - return _copy(dest,src,idest,isrc,nsrc,false) -end - -function tablex._normalize_slice(self,first,last) - local sz = #self - if not first then first=1 end - if first<0 then first=sz+first+1 end - -- make the range _inclusive_! - if not last then last=sz end - if last < 0 then last=sz+1+last end - return first,last -end - ---- Extract a range from a table, like 'string.sub'. --- If first or last are negative then they are relative to the end of the list --- eg. sub(t,-2) gives last 2 entries in a list, and --- sub(t,-4,-2) gives from -4th to -2nd --- @within Extraction --- @array t a list-like table --- @int first An index --- @int last An index --- @return a new List -function tablex.sub(t,first,last) - assert_arg_indexable(1,t) - first,last = tablex._normalize_slice(t,first,last) - local res={} - for i=first,last do append(res,t[i]) end - return setmeta(res,t,'List') -end - ---- set an array range to a value. If it's a function we use the result --- of applying it to the indices. --- @array t a list-like table --- @param val a value --- @int[opt=1] i1 start range --- @int[opt=#t] i2 end range -function tablex.set (t,val,i1,i2) - assert_arg_indexable(1,t) - i1,i2 = i1 or 1,i2 or #t - if types.is_callable(val) then - for i = i1,i2 do - t[i] = val(i) - end - else - for i = i1,i2 do - t[i] = val - end - end -end - ---- create a new array of specified size with initial value. --- @int n size --- @param val initial value (can be `nil`, but don't expect `#` to work!) --- @return the table -function tablex.new (n,val) - local res = {} - tablex.set(res,val,1,n) - return res -end - ---- clear out the contents of a table. --- @array t a list --- @param istart optional start position -function tablex.clear(t,istart) - istart = istart or 1 - for i = istart,#t do remove(t) end -end - ---- insert values into a table. --- similar to `table.insert` but inserts values from given table `values`, --- not the object itself, into table `t` at position `pos`. --- @within Copying --- @array t the list --- @int[opt] position (default is at end) --- @array values -function tablex.insertvalues(t, ...) - assert_arg(1,t,'table') - local pos, values - if select('#', ...) == 1 then - pos,values = #t+1, ... - else - pos,values = ... - end - if #values > 0 then - for i=#t,pos,-1 do - t[i+#values] = t[i] - end - local offset = 1 - pos - for i=pos,pos+#values-1 do - t[i] = values[i + offset] - end - end - return t -end - ---- remove a range of values from a table. --- End of range may be negative. --- @array t a list-like table --- @int i1 start index --- @int i2 end index --- @return the table -function tablex.removevalues (t,i1,i2) - assert_arg(1,t,'table') - i1,i2 = tablex._normalize_slice(t,i1,i2) - for i = i1,i2 do - remove(t,i1) - end - return t -end - -local _find -_find = function (t,value,tables) - for k,v in pairs(t) do - if v == value then return k end - end - for k,v in pairs(t) do - if not tables[v] and type(v) == 'table' then - tables[v] = true - local res = _find(v,value,tables) - if res then - res = tostring(res) - if type(k) ~= 'string' then - return '['..k..']'..res - else - return k..'.'..res - end - end - end - end -end - ---- find a value in a table by recursive search. --- @within Finding --- @tab t the table --- @param value the value --- @array[opt] exclude any tables to avoid searching --- @return a fieldspec, e.g. 'a.b' or 'math.sin' --- @usage search(_G,math.sin,{package.path}) == 'math.sin' -function tablex.search (t,value,exclude) - assert_arg_iterable(1,t) - local tables = {[t]=true} - if exclude then - for _,v in pairs(exclude) do tables[v] = true end - end - return _find(t,value,tables) -end - ---- return an iterator to a table sorted by its keys --- @within Iterating --- @tab t the table --- @func f an optional comparison function (f(x,y) is true if x < y) --- @usage for k,v in tablex.sort(t) do print(k,v) end --- @return an iterator to traverse elements sorted by the keys -function tablex.sort(t,f) - local keys = {} - for k in pairs(t) do keys[#keys + 1] = k end - tsort(keys,f) - local i = 0 - return function() - i = i + 1 - return keys[i], t[keys[i]] - end -end - ---- return an iterator to a table sorted by its values --- @within Iterating --- @tab t the table --- @func f an optional comparison function (f(x,y) is true if x < y) --- @usage for k,v in tablex.sortv(t) do print(k,v) end --- @return an iterator to traverse elements sorted by the values -function tablex.sortv(t,f) - f = function_arg(2, f or '<') - local keys = {} - for k in pairs(t) do keys[#keys + 1] = k end - tsort(keys,function(x, y) return f(t[x], t[y]) end) - local i = 0 - return function() - i = i + 1 - return keys[i], t[keys[i]] - end -end - ---- modifies a table to be read only. --- This only offers weak protection. Tables can still be modified with --- `table.insert` and `rawset`. --- --- *NOTE*: for Lua 5.1 length, pairs and ipairs will not work, since the --- equivalent metamethods are only available in Lua 5.2 and newer. --- @tab t the table --- @return the table read only (a proxy). -function tablex.readonly(t) - local mt = { - __index=t, - __newindex=function(t, k, v) error("Attempt to modify read-only table", 2) end, - __pairs=function() return pairs(t) end, - __ipairs=function() return ipairs(t) end, - __len=function() return #t end, - __metatable=false - } - return setmetatable({}, mt) -end - -return tablex diff --git a/src/pl/template.lua b/src/pl/template.lua deleted file mode 100644 index 9961a1b..0000000 --- a/src/pl/template.lua +++ /dev/null @@ -1,202 +0,0 @@ ---- A template preprocessor. --- Originally by [Ricki Lake](http://lua-users.org/wiki/SlightlyLessSimpleLuaPreprocessor) --- --- There are two rules: --- --- * lines starting with # are Lua --- * otherwise, `$(expr)` is the result of evaluating `expr` --- --- Example: --- --- # for i = 1,3 do --- $(i) Hello, Word! --- # end --- ===> --- 1 Hello, Word! --- 2 Hello, Word! --- 3 Hello, Word! --- --- Other escape characters can be used, when the defaults conflict --- with the output language. --- --- > for _,n in pairs{'one','two','three'} do --- static int l_${n} (luaState *state); --- > end --- --- See @{03-strings.md.Another_Style_of_Template|the Guide}. --- --- Dependencies: `pl.utils` --- @module pl.template - -local utils = require 'pl.utils' - -local append,format,strsub,strfind,strgsub = table.insert,string.format,string.sub,string.find,string.gsub - -local APPENDER = "\n__R_size = __R_size + 1; __R_table[__R_size] = " - -local function parseDollarParen(pieces, chunk, exec_pat, newline) - local s = 1 - for term, executed, e in chunk:gmatch(exec_pat) do - executed = '('..strsub(executed,2,-2)..')' - append(pieces, APPENDER..format("%q", strsub(chunk,s, term - 1))) - append(pieces, APPENDER..format("__tostring(%s or '')", executed)) - s = e - end - local r - if newline then - r = format("%q", strgsub(strsub(chunk,s),"\n","")) - else - r = format("%q", strsub(chunk,s)) - end - if r ~= '""' then - append(pieces, APPENDER..r) - end -end - -local function parseHashLines(chunk,inline_escape,brackets,esc,newline) - local exec_pat = "()"..inline_escape.."(%b"..brackets..")()" - - local esc_pat = esc.."+([^\n]*\n?)" - local esc_pat1, esc_pat2 = "^"..esc_pat, "\n"..esc_pat - local pieces, s = {"return function()\nlocal __R_size, __R_table, __tostring = 0, {}, __tostring", n = 1}, 1 - while true do - local _, e, lua = strfind(chunk,esc_pat1, s) - if not e then - local ss - ss, e, lua = strfind(chunk,esc_pat2, s) - parseDollarParen(pieces, strsub(chunk,s, ss), exec_pat, newline) - if not e then break end - end - if strsub(lua, -1, -1) == "\n" then lua = strsub(lua, 1, -2) end - append(pieces, "\n"..lua) - s = e + 1 - end - append(pieces, "\nreturn __R_table\nend") - - -- let's check for a special case where there is nothing to template, but it's - -- just a single static string - local short = false - if (#pieces == 3) and (pieces[2]:find(APPENDER, 1, true) == 1) then - pieces = { "return " .. pieces[2]:sub(#APPENDER+1,-1) } - short = true - end - -- if short == true, the generated function will not return a table of strings, - -- but a single string - return table.concat(pieces), short -end - -local template = {} - ---- expand the template using the specified environment. --- This function will compile and render the template. For more performant --- recurring usage use the two step approach by using `compile` and `ct:render`. --- There are six special fields in the environment table `env` --- --- * `_parent`: continue looking up in this table (e.g. `_parent=_G`). --- * `_brackets`: bracket pair that wraps inline Lua expressions, default is '()'. --- * `_escape`: character marking Lua lines, default is '#' --- * `_inline_escape`: character marking inline Lua expression, default is '$'. --- * `_chunk_name`: chunk name for loaded templates, used if there --- is an error in Lua code. Default is 'TMP'. --- * `_debug`: if truthy, the generated code will be printed upon a render error --- --- @string str the template string --- @tab[opt] env the environment --- @return `rendered template + nil + source_code`, or `nil + error + source_code`. The last --- return value (`source_code`) is only returned if the debug option is used. -function template.substitute(str,env) - env = env or {} - local t, err = template.compile(str, { - chunk_name = rawget(env,"_chunk_name"), - escape = rawget(env,"_escape"), - inline_escape = rawget(env,"_inline_escape"), - inline_brackets = rawget(env,"_brackets"), - newline = nil, - debug = rawget(env,"_debug") - }) - if not t then return t, err end - - return t:render(env, rawget(env,"_parent"), rawget(env,"_debug")) -end - ---- executes the previously compiled template and renders it. --- @function ct:render --- @tab[opt] env the environment. --- @tab[opt] parent continue looking up in this table (e.g. `parent=_G`). --- @bool[opt] db if thruthy, it will print the code upon a render error --- (provided the template was compiled with the debug option). --- @return `rendered template + nil + source_code`, or `nil + error + source_code`. The last return value --- (`source_code`) is only returned if the template was compiled with the debug option. --- @usage --- local ct, err = template.compile(my_template) --- local rendered , err = ct:render(my_env, parent) -local render = function(self, env, parent, db) - env = env or {} - if parent then -- parent is a bit silly, but for backward compatibility retained - setmetatable(env, {__index = parent}) - end - setmetatable(self.env, {__index = env}) - - local res, out = xpcall(self.fn, debug.traceback) - if not res then - if self.code and db then print(self.code) end - return nil, out, self.code - end - return table.concat(out), nil, self.code -end - ---- compiles the template. --- Returns an object that can repeatedly be rendered without parsing/compiling --- the template again. --- The options passed in the `opts` table support the following options: --- --- * `chunk_name`: chunk name for loaded templates, used if there --- is an error in Lua code. Default is 'TMP'. --- * `escape`: character marking Lua lines, default is '#' --- * `inline_escape`: character marking inline Lua expression, default is '$'. --- * `inline_brackets`: bracket pair that wraps inline Lua expressions, default is '()'. --- * `newline`: string to replace newline characters, default is `nil` (not replacing newlines). --- * `debug`: if truthy, the generated source code will be retained within the compiled template object, default is `nil`. --- --- @string str the template string --- @tab[opt] opts the compilation options to use --- @return template object, or `nil + error + source_code` --- @usage --- local ct, err = template.compile(my_template) --- local rendered , err = ct:render(my_env, parent) -function template.compile(str, opts) - opts = opts or {} - local chunk_name = opts.chunk_name or 'TMP' - local escape = opts.escape or '#' - local inline_escape = opts.inline_escape or '$' - local inline_brackets = opts.inline_brackets or '()' - - local code, short = parseHashLines(str,inline_escape,inline_brackets,escape,opts.newline) - local env = { __tostring = tostring } - local fn, err = utils.load(code, chunk_name,'t',env) - if not fn then return nil, err, code end - - if short then - -- the template returns a single constant string, let's optimize for that - local constant_string = fn() - return { - fn = fn(), - env = env, - render = function(self) -- additional params can be ignored - -- skip the metatable magic and error handling in the render - -- function above for this special case - return constant_string, nil, self.code - end, - code = opts.debug and code or nil, - } - end - - return { - fn = fn(), - env = env, - render = render, - code = opts.debug and code or nil, - } -end - -return template diff --git a/src/pl/test.lua b/src/pl/test.lua deleted file mode 100644 index 694bbc5..0000000 --- a/src/pl/test.lua +++ /dev/null @@ -1,164 +0,0 @@ ---- Useful test utilities. --- --- test.asserteq({1,2},{1,2}) -- can compare tables --- test.asserteq(1.2,1.19,0.02) -- compare FP numbers within precision --- T = test.tuple -- used for comparing multiple results --- test.asserteq(T(string.find(" me","me")),T(2,3)) --- --- Dependencies: `pl.utils`, `pl.tablex`, `pl.pretty`, `pl.path`, `debug` --- @module pl.test - -local tablex = require 'pl.tablex' -local utils = require 'pl.utils' -local pretty = require 'pl.pretty' -local path = require 'pl.path' -local type,unpack,pack = type,utils.unpack,utils.pack -local clock = os.clock -local debug = require 'debug' -local io = io - -local function dump(x) - if type(x) == 'table' and not (getmetatable(x) and getmetatable(x).__tostring) then - return pretty.write(x,' ',true) - elseif type(x) == 'string' then - return '"'..x..'"' - else - return tostring(x) - end -end - -local test = {} - ----- error handling for test results. --- By default, this writes to stderr and exits the program. --- Re-define this function to raise an error and/or redirect output -function test.error_handler(file,line,got_text, needed_text,msg) - local err = io.stderr - err:write(path.basename(file)..':'..line..': assertion failed\n') - err:write("got:\t",got_text,'\n') - err:write("needed:\t",needed_text,'\n') - utils.quit(1,msg or "these values were not equal") -end - -local function complain (x,y,msg,where) - local i = debug.getinfo(3 + (where or 0)) - test.error_handler(i.short_src,i.currentline,dump(x),dump(y),msg) -end - ---- general test complain message. --- Useful for composing new test functions (see tests/tablex.lua for an example) --- @param x a value --- @param y value to compare first value against --- @param msg message --- @param where extra level offset for errors --- @function complain -test.complain = complain - ---- like assert, except takes two arguments that must be equal and can be tables. --- If they are plain tables, it will use tablex.deepcompare. --- @param x any value --- @param y a value equal to x --- @param eps an optional tolerance for numerical comparisons --- @param where extra level offset -function test.asserteq (x,y,eps,where) - local res = x == y - if not res then - res = tablex.deepcompare(x,y,true,eps) - end - if not res then - complain(x,y,nil,where) - end -end - ---- assert that the first string matches the second. --- @param s1 a string --- @param s2 a string --- @param where extra level offset -function test.assertmatch (s1,s2,where) - if not s1:match(s2) then - complain (s1,s2,"these strings did not match",where) - end -end - ---- assert that the function raises a particular error. --- @param fn a function or a table of the form {function,arg1,...} --- @param e a string to match the error against --- @param where extra level offset -function test.assertraise(fn,e,where) - local ok, err - if type(fn) == 'table' then - ok, err = pcall(unpack(fn)) - else - ok, err = pcall(fn) - end - if ok or err:match(e)==nil then - complain (err,e,"these errors did not match",where) - end -end - ---- a version of asserteq that takes two pairs of values. --- x1==y1 and x2==y2 must be true. Useful for functions that naturally --- return two values. --- @param x1 any value --- @param x2 any value --- @param y1 any value --- @param y2 any value --- @param where extra level offset -function test.asserteq2 (x1,x2,y1,y2,where) - if x1 ~= y1 then complain(x1,y1,nil,where) end - if x2 ~= y2 then complain(x2,y2,nil,where) end -end - --- tuple type -- - -local tuple_mt = { - unpack = unpack -} -tuple_mt.__index = tuple_mt - -function tuple_mt.__tostring(self) - local ts = {} - for i=1, self.n do - local s = self[i] - ts[i] = type(s) == 'string' and ('%q'):format(s) or tostring(s) - end - return 'tuple(' .. table.concat(ts, ', ') .. ')' -end - -function tuple_mt.__eq(a, b) - if a.n ~= b.n then return false end - for i=1, a.n do - if a[i] ~= b[i] then return false end - end - return true -end - -function tuple_mt.__len(self) - return self.n -end - ---- encode an arbitrary argument list as a tuple. --- This can be used to compare to other argument lists, which is --- very useful for testing functions which return a number of values. --- Unlike regular array-like tables ('sequences') they may contain nils. --- Tuples understand equality and know how to print themselves out. --- The # operator is defined to be the size, irrespecive of any nils, --- and there is an `unpack` method. --- @usage asserteq(tuple( ('ab'):find 'a'), tuple(1,1)) -function test.tuple(...) - return setmetatable(pack(...), tuple_mt) -end - ---- Time a function. Call the function a given number of times, and report the number of seconds taken, --- together with a message. Any extra arguments will be passed to the function. --- @string msg a descriptive message --- @int n number of times to call the function --- @func fun the function --- @param ... optional arguments to fun -function test.timer(msg,n,fun,...) - local start = clock() - for i = 1,n do fun(...) end - utils.printf("%s: took %7.2f sec\n",msg,clock()-start) -end - -return test diff --git a/src/pl/text.lua b/src/pl/text.lua deleted file mode 100644 index 8b6a53f..0000000 --- a/src/pl/text.lua +++ /dev/null @@ -1,26 +0,0 @@ ---- Text processing utilities. --- --- This provides a Template class (modeled after the same from the Python --- libraries, see string.Template). It also provides similar functions to those --- found in the textwrap module. --- --- IMPORTANT: this module has been deprecated and will be removed in a future --- version (2.0). The contents of this module have moved to the `pl.stringx` --- module. --- --- See @{03-strings.md.String_Templates|the Guide}. --- --- Dependencies: `pl.stringx`, `pl.utils` --- @module pl.text - -local utils = require("pl.utils") - -utils.raise_deprecation { - source = "Penlight " .. utils._VERSION, - message = "the contents of module 'pl.text' has moved into 'pl.stringx'", - version_removed = "2.0.0", - deprecated_after = "1.11.0", - no_trace = true, -} - -return require "pl.stringx" diff --git a/src/pl/types.lua b/src/pl/types.lua deleted file mode 100644 index 35b0ccb..0000000 --- a/src/pl/types.lua +++ /dev/null @@ -1,183 +0,0 @@ ----- Dealing with Detailed Type Information - --- Dependencies `pl.utils` --- @module pl.types - -local utils = require 'pl.utils' -local math_ceil = math.ceil -local assert_arg = utils.assert_arg -local types = {} - ---- is the object either a function or a callable object?. --- @param obj Object to check. -function types.is_callable (obj) - return type(obj) == 'function' or getmetatable(obj) and getmetatable(obj).__call and true -end - ---- is the object of the specified type?. --- If the type is a string, then use type, otherwise compare with metatable. --- --- NOTE: this function is imported from `utils.is_type`. --- @param obj An object to check --- @param tp The expected type --- @function is_type --- @see utils.is_type -types.is_type = utils.is_type - -local fileMT = getmetatable(io.stdout) - ---- a string representation of a type. --- For tables and userdata with metatables, we assume that the metatable has a `_name` --- field. If the field is not present it will return 'unknown table' or --- 'unknown userdata'. --- Lua file objects return the type 'file'. --- @param obj an object --- @return a string like 'number', 'table', 'file' or 'List' -function types.type (obj) - local t = type(obj) - if t == 'table' or t == 'userdata' then - local mt = getmetatable(obj) - if mt == fileMT then - return 'file' - elseif mt == nil then - return t - else - -- TODO: the "unknown" is weird, it should just return the type - return mt._name or "unknown "..t - end - else - return t - end -end - ---- is this number an integer? --- @param x a number --- @raise error if x is not a number --- @return boolean -function types.is_integer (x) - return math_ceil(x)==x -end - ---- Check if the object is "empty". --- An object is considered empty if it is: --- --- - `nil` --- - a table without any items (key-value pairs or indexes) --- - a string with no content ("") --- - not a nil/table/string --- @param o The object to check if it is empty. --- @param ignore_spaces If the object is a string and this is true the string is --- considered empty if it only contains spaces. --- @return `true` if the object is empty, otherwise a falsy value. -function types.is_empty(o, ignore_spaces) - if o == nil then - return true - elseif type(o) == "table" then - return next(o) == nil - elseif type(o) == "string" then - return o == "" or (not not ignore_spaces and (not not o:find("^%s+$"))) - else - return true - end -end - -local function check_meta (val) - if type(val) == 'table' then return true end - return getmetatable(val) -end - ---- is an object 'array-like'? --- An object is array like if: --- --- - it is a table, or --- - it has a metatable with `__len` and `__index` methods --- --- NOTE: since `__len` is 5.2+, on 5.1 is usually returns `false` for userdata --- @param val any value. --- @return `true` if the object is array-like, otherwise a falsy value. -function types.is_indexable (val) - local mt = check_meta(val) - if mt == true then return true end - return mt and mt.__len and mt.__index and true -end - ---- can an object be iterated over with `pairs`? --- An object is iterable if: --- --- - it is a table, or --- - it has a metatable with a `__pairs` meta method --- --- NOTE: since `__pairs` is 5.2+, on 5.1 is usually returns `false` for userdata --- @param val any value. --- @return `true` if the object is iterable, otherwise a falsy value. -function types.is_iterable (val) - local mt = check_meta(val) - if mt == true then return true end - return mt and mt.__pairs and true -end - ---- can an object accept new key/pair values? --- An object is iterable if: --- --- - it is a table, or --- - it has a metatable with a `__newindex` meta method --- --- @param val any value. --- @return `true` if the object is writeable, otherwise a falsy value. -function types.is_writeable (val) - local mt = check_meta(val) - if mt == true then return true end - return mt and mt.__newindex and true -end - --- Strings that should evaluate to true. -- TODO: add on/off ??? -local trues = { yes=true, y=true, ["true"]=true, t=true, ["1"]=true } --- Conditions types should evaluate to true. -local true_types = { - boolean=function(o, true_strs, check_objs) return o end, - string=function(o, true_strs, check_objs) - o = o:lower() - if trues[o] then - return true - end - -- Check alternative user provided strings. - for _,v in ipairs(true_strs or {}) do - if type(v) == "string" and o == v:lower() then - return true - end - end - return false - end, - number=function(o, true_strs, check_objs) return o ~= 0 end, - table=function(o, true_strs, check_objs) if check_objs and next(o) ~= nil then return true end return false end -} ---- Convert to a boolean value. --- True values are: --- --- * boolean: true. --- * string: 'yes', 'y', 'true', 't', '1' or additional strings specified by `true_strs`. --- * number: Any non-zero value. --- * table: Is not empty and `check_objs` is true. --- * everything else: Is not `nil` and `check_objs` is true. --- --- @param o The object to evaluate. --- @param[opt] true_strs optional Additional strings that when matched should evaluate to true. Comparison is case insensitive. --- This should be a List of strings. E.g. "ja" to support German. --- @param[opt] check_objs True if objects should be evaluated. --- @return true if the input evaluates to true, otherwise false. -function types.to_bool(o, true_strs, check_objs) - local true_func - if true_strs then - assert_arg(2, true_strs, "table") - end - true_func = true_types[type(o)] - if true_func then - return true_func(o, true_strs, check_objs) - elseif check_objs and o ~= nil then - return true - end - return false -end - - -return types diff --git a/src/pl/url.lua b/src/pl/url.lua deleted file mode 100644 index 8c7cfeb..0000000 --- a/src/pl/url.lua +++ /dev/null @@ -1,51 +0,0 @@ ---- Python-style URL quoting library. --- --- @module pl.url - -local url = {} - -local function quote_char(c) - return string.format("%%%02X", string.byte(c)) -end - ---- Quote the url, replacing special characters using the '%xx' escape. --- @string s the string --- @bool quote_plus Also escape slashes and replace spaces by plus signs. --- @return The quoted string, or if `s` wasn't a string, just plain unaltered `s`. -function url.quote(s, quote_plus) - if type(s) ~= "string" then - return s - end - - s = s:gsub("\n", "\r\n") - s = s:gsub("([^A-Za-z0-9 %-_%./])", quote_char) - if quote_plus then - s = s:gsub(" ", "+") - s = s:gsub("/", quote_char) - else - s = s:gsub(" ", "%%20") - end - - return s -end - -local function unquote_char(h) - return string.char(tonumber(h, 16)) -end - ---- Unquote the url, replacing '%xx' escapes and plus signs. --- @string s the string --- @return The unquoted string, or if `s` wasn't a string, just plain unaltered `s`. -function url.unquote(s) - if type(s) ~= "string" then - return s - end - - s = s:gsub("+", " ") - s = s:gsub("%%(%x%x)", unquote_char) - s = s:gsub("\r\n", "\n") - - return s -end - -return url diff --git a/src/pl/utils.lua b/src/pl/utils.lua deleted file mode 100644 index 1cb8a64..0000000 --- a/src/pl/utils.lua +++ /dev/null @@ -1,841 +0,0 @@ ---- Generally useful routines. --- See @{01-introduction.md.Generally_useful_functions|the Guide}. --- --- Dependencies: `pl.compat`, all exported fields and functions from --- `pl.compat` are also available in this module. --- --- @module pl.utils -local format = string.format -local compat = require 'pl.compat' -local stdout = io.stdout -local append = table.insert -local concat = table.concat -local _unpack = table.unpack -- always injected by 'compat' -local find = string.find -local sub = string.sub - -local is_windows = compat.is_windows -local err_mode = 'default' -local raise -local operators -local _function_factories = {} - - -local utils = { _VERSION = "1.12.0" } -for k, v in pairs(compat) do utils[k] = v end - ---- Some standard patterns --- @table patterns -utils.patterns = { - FLOAT = '[%+%-%d]%d*%.?%d*[eE]?[%+%-]?%d*', -- floating point number - INTEGER = '[+%-%d]%d*', -- integer number - IDEN = '[%a_][%w_]*', -- identifier - FILE = '[%a%.\\][:%][%w%._%-\\]*', -- file -} - - ---- Standard meta-tables as used by other Penlight modules --- @table stdmt --- @field List the List metatable --- @field Map the Map metatable --- @field Set the Set metatable --- @field MultiMap the MultiMap metatable -utils.stdmt = { - List = {_name='List'}, - Map = {_name='Map'}, - Set = {_name='Set'}, - MultiMap = {_name='MultiMap'}, -} - - ---- pack an argument list into a table. --- @param ... any arguments --- @return a table with field `n` set to the length --- @function utils.pack --- @see compat.pack --- @see utils.npairs --- @see utils.unpack -utils.pack = table.pack -- added here to be symmetrical with unpack - ---- unpack a table and return its contents. --- --- NOTE: this implementation differs from the Lua implementation in the way --- that this one DOES honor the `n` field in the table `t`, such that it is 'nil-safe'. --- @param t table to unpack --- @param[opt] i index from which to start unpacking, defaults to 1 --- @param[opt] j index of the last element to unpack, defaults to `t.n` or else `#t` --- @return multiple return values from the table --- @function utils.unpack --- @see compat.unpack --- @see utils.pack --- @see utils.npairs --- @usage --- local t = table.pack(nil, nil, nil, 4) --- local a, b, c, d = table.unpack(t) -- this `unpack` is NOT nil-safe, so d == nil --- --- local a, b, c, d = utils.unpack(t) -- this is nil-safe, so d == 4 -function utils.unpack(t, i, j) - return _unpack(t, i or 1, j or t.n or #t) -end - ---- print an arbitrary number of arguments using a format. --- Output will be sent to `stdout`. --- @param fmt The format (see `string.format`) --- @param ... Extra arguments for format -function utils.printf(fmt, ...) - utils.assert_string(1, fmt) - utils.fprintf(stdout, fmt, ...) -end - ---- write an arbitrary number of arguments to a file using a format. --- @param f File handle to write to. --- @param fmt The format (see `string.format`). --- @param ... Extra arguments for format -function utils.fprintf(f,fmt,...) - utils.assert_string(2,fmt) - f:write(format(fmt,...)) -end - -do - local function import_symbol(T,k,v,libname) - local key = rawget(T,k) - -- warn about collisions! - if key and k ~= '_M' and k ~= '_NAME' and k ~= '_PACKAGE' and k ~= '_VERSION' then - utils.fprintf(io.stderr,"warning: '%s.%s' will not override existing symbol\n",libname,k) - return - end - rawset(T,k,v) - end - - local function lookup_lib(T,t) - for k,v in pairs(T) do - if v == t then return k end - end - return '?' - end - - local already_imported = {} - - --- take a table and 'inject' it into the local namespace. - -- @param t The table (table), or module name (string), defaults to this `utils` module table - -- @param T An optional destination table (defaults to callers environment) - function utils.import(t,T) - T = T or _G - t = t or utils - if type(t) == 'string' then - t = require (t) - end - local libname = lookup_lib(T,t) - if already_imported[t] then return end - already_imported[t] = libname - for k,v in pairs(t) do - import_symbol(T,k,v,libname) - end - end -end - ---- return either of two values, depending on a condition. --- @param cond A condition --- @param value1 Value returned if cond is truthy --- @param value2 Value returned if cond is falsy -function utils.choose(cond, value1, value2) - return cond and value1 or value2 -end - ---- convert an array of values to strings. --- @param t a list-like table --- @param[opt] temp (table) buffer to use, otherwise allocate --- @param[opt] tostr custom tostring function, called with (value,index). Defaults to `tostring`. --- @return the converted buffer -function utils.array_tostring (t,temp,tostr) - temp, tostr = temp or {}, tostr or tostring - for i = 1,#t do - temp[i] = tostr(t[i],i) - end - return temp -end - - - ---- is the object of the specified type? --- If the type is a string, then use type, otherwise compare with metatable --- @param obj An object to check --- @param tp String of what type it should be --- @return boolean --- @usage utils.is_type("hello world", "string") --> true --- -- or check metatable --- local my_mt = {} --- local my_obj = setmetatable(my_obj, my_mt) --- utils.is_type(my_obj, my_mt) --> true -function utils.is_type (obj,tp) - if type(tp) == 'string' then return type(obj) == tp end - local mt = getmetatable(obj) - return tp == mt -end - - - ---- an iterator with indices, similar to `ipairs`, but with a range. --- This is a nil-safe index based iterator that will return `nil` when there --- is a hole in a list. To be safe ensure that table `t.n` contains the length. --- @tparam table t the table to iterate over --- @tparam[opt=1] integer i_start start index --- @tparam[opt=t.n or #t] integer i_end end index --- @tparam[opt=1] integer step step size --- @treturn integer index --- @treturn any value at index (which can be `nil`!) --- @see utils.pack --- @see utils.unpack --- @usage --- local t = utils.pack(nil, 123, nil) -- adds an `n` field when packing --- --- for i, v in utils.npairs(t, 2) do -- start at index 2 --- t[i] = tostring(t[i]) --- end --- --- -- t = { n = 3, [2] = "123", [3] = "nil" } -function utils.npairs(t, i_start, i_end, step) - step = step or 1 - if step == 0 then - error("iterator step-size cannot be 0", 2) - end - local i = (i_start or 1) - step - i_end = i_end or t.n or #t - if step < 0 then - return function() - i = i + step - if i < i_end then - return nil - end - return i, t[i] - end - - else - return function() - i = i + step - if i > i_end then - return nil - end - return i, t[i] - end - end -end - - - ---- Error handling --- @section Error-handling - ---- assert that the given argument is in fact of the correct type. --- @param n argument index --- @param val the value --- @param tp the type --- @param verify an optional verification function --- @param msg an optional custom message --- @param lev optional stack position for trace, default 2 --- @return the validated value --- @raise if `val` is not the correct type --- @usage --- local param1 = assert_arg(1,"hello",'table') --> error: argument 1 expected a 'table', got a 'string' --- local param4 = assert_arg(4,'!@#$%^&*','string',path.isdir,'not a directory') --- --> error: argument 4: '!@#$%^&*' not a directory -function utils.assert_arg (n,val,tp,verify,msg,lev) - if type(val) ~= tp then - error(("argument %d expected a '%s', got a '%s'"):format(n,tp,type(val)),lev or 2) - end - if verify and not verify(val) then - error(("argument %d: '%s' %s"):format(n,val,msg),lev or 2) - end - return val -end - ---- creates an Enum table. --- This helps prevent magic strings in code by throwing errors for accessing --- non-existing values. --- --- Calling on the object does the same, but returns a soft error; `nil + err`. --- --- The values are equal to the keys. The enum object is --- read-only. --- @param ... strings that make up the enumeration. --- @return Enum object --- @usage -- accessing at runtime --- local obj = {} --- obj.MOVEMENT = utils.enum("FORWARD", "REVERSE", "LEFT", "RIGHT") --- --- if current_movement == obj.MOVEMENT.FORWARD then --- -- do something --- --- elseif current_movement == obj.MOVEMENT.REVERES then --- -- throws error due to typo 'REVERES', so a silent mistake becomes a hard error --- -- "'REVERES' is not a valid value (expected one of: 'FORWARD', 'REVERSE', 'LEFT', 'RIGHT')" --- --- end --- @usage -- validating user-input --- local parameter = "...some user provided option..." --- local ok, err = obj.MOVEMENT(parameter) -- calling on the object --- if not ok then --- print("bad 'parameter', " .. err) --- os.exit(1) --- end -function utils.enum(...) - local lst = utils.pack(...) - utils.assert_arg(1, lst[1], "string") -- at least 1 string - - local enum = {} - for i, value in ipairs(lst) do - utils.assert_arg(i, value, "string") - enum[value] = value - end - - local valid = "(expected one of: '" .. concat(lst, "', '") .. "')" - setmetatable(enum, { - __index = function(self, key) - error(("'%s' is not a valid value %s"):format(tostring(key), valid), 2) - end, - __newindex = function(self, key, value) - error("the Enum object is read-only", 2) - end, - __call = function(self, key) - if type(key) == "string" then - local v = rawget(self, key) - if v then - return v - end - end - return nil, ("'%s' is not a valid value %s"):format(tostring(key), valid) - end - }) - - return enum -end - - ---- process a function argument. --- This is used throughout Penlight and defines what is meant by a function: --- Something that is callable, or an operator string as defined by pl.operator, --- such as '>' or '#'. If a function factory has been registered for the type, it will --- be called to get the function. --- @param idx argument index --- @param f a function, operator string, or callable object --- @param msg optional error message --- @return a callable --- @raise if idx is not a number or if f is not callable -function utils.function_arg (idx,f,msg) - utils.assert_arg(1,idx,'number') - local tp = type(f) - if tp == 'function' then return f end -- no worries! - -- ok, a string can correspond to an operator (like '==') - if tp == 'string' then - if not operators then operators = require 'pl.operator'.optable end - local fn = operators[f] - if fn then return fn end - local fn, err = utils.string_lambda(f) - if not fn then error(err..': '..f) end - return fn - elseif tp == 'table' or tp == 'userdata' then - local mt = getmetatable(f) - if not mt then error('not a callable object',2) end - local ff = _function_factories[mt] - if not ff then - if not mt.__call then error('not a callable object',2) end - return f - else - return ff(f) -- we have a function factory for this type! - end - end - if not msg then msg = " must be callable" end - if idx > 0 then - error("argument "..idx..": "..msg,2) - else - error(msg,2) - end -end - - ---- assert the common case that the argument is a string. --- @param n argument index --- @param val a value that must be a string --- @return the validated value --- @raise val must be a string --- @usage --- local val = 42 --- local param2 = utils.assert_string(2, val) --> error: argument 2 expected a 'string', got a 'number' -function utils.assert_string (n, val) - return utils.assert_arg(n,val,'string',nil,nil,3) -end - ---- control the error strategy used by Penlight. --- This is a global setting that controls how `utils.raise` behaves: --- --- - 'default': return `nil + error` (this is the default) --- - 'error': throw a Lua error --- - 'quit': exit the program --- --- @param mode either 'default', 'quit' or 'error' --- @see utils.raise -function utils.on_error (mode) - mode = tostring(mode) - if ({['default'] = 1, ['quit'] = 2, ['error'] = 3})[mode] then - err_mode = mode - else - -- fail loudly - local err = "Bad argument expected string; 'default', 'quit', or 'error'. Got '"..tostring(mode).."'" - if err_mode == 'default' then - error(err, 2) -- even in 'default' mode fail loud in this case - end - raise(err) - end -end - ---- used by Penlight functions to return errors. Its global behaviour is controlled --- by `utils.on_error`. --- To use this function you MUST use it in conjunction with `return`, since it might --- return `nil + error`. --- @param err the error string. --- @see utils.on_error --- @usage --- if some_condition then --- return utils.raise("some condition was not met") -- MUST use 'return'! --- end -function utils.raise (err) - if err_mode == 'default' then - return nil, err - elseif err_mode == 'quit' then - return utils.quit(err) - else - error(err, 2) - end -end -raise = utils.raise - - - ---- File handling --- @section files - ---- return the contents of a file as a string --- @param filename The file path --- @param is_bin open in binary mode --- @return file contents -function utils.readfile(filename,is_bin) - local mode = is_bin and 'b' or '' - utils.assert_string(1,filename) - local f,open_err = io.open(filename,'r'..mode) - if not f then return raise (open_err) end - local res,read_err = f:read('*a') - f:close() - if not res then - -- Errors in io.open have "filename: " prefix, - -- error in file:read don't, add it. - return raise (filename..": "..read_err) - end - return res -end - ---- write a string to a file --- @param filename The file path --- @param str The string --- @param is_bin open in binary mode --- @return true or nil --- @return error message --- @raise error if filename or str aren't strings -function utils.writefile(filename,str,is_bin) - local mode = is_bin and 'b' or '' - utils.assert_string(1,filename) - utils.assert_string(2,str) - local f,err = io.open(filename,'w'..mode) - if not f then return raise(err) end - local ok, write_err = f:write(str) - f:close() - if not ok then - -- Errors in io.open have "filename: " prefix, - -- error in file:write don't, add it. - return raise (filename..": "..write_err) - end - return true -end - ---- return the contents of a file as a list of lines --- @param filename The file path --- @return file contents as a table --- @raise error if filename is not a string -function utils.readlines(filename) - utils.assert_string(1,filename) - local f,err = io.open(filename,'r') - if not f then return raise(err) end - local res = {} - for line in f:lines() do - append(res,line) - end - f:close() - return res -end - ---- OS functions --- @section OS-functions - ---- execute a shell command and return the output. --- This function redirects the output to tempfiles and returns the content of those files. --- @param cmd a shell command --- @param bin boolean, if true, read output as binary file --- @return true if successful --- @return actual return code --- @return stdout output (string) --- @return errout output (string) -function utils.executeex(cmd, bin) - local outfile = os.tmpname() - local errfile = os.tmpname() - - if is_windows and not outfile:find(':') then - outfile = os.getenv('TEMP')..outfile - errfile = os.getenv('TEMP')..errfile - end - cmd = cmd .. " > " .. utils.quote_arg(outfile) .. " 2> " .. utils.quote_arg(errfile) - - local success, retcode = utils.execute(cmd) - local outcontent = utils.readfile(outfile, bin) - local errcontent = utils.readfile(errfile, bin) - os.remove(outfile) - os.remove(errfile) - return success, retcode, (outcontent or ""), (errcontent or "") -end - ---- 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`. --- @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 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. - -- Quotes need to be escaped with backslashes; - -- additionally, backslashes before a quote, escaped or not, - -- need to be doubled. - -- See documentation for CommandLineToArgvW Windows function. - argument = '"' .. argument:gsub([[(\*)"]], [[%1%1\"]]):gsub([[\+$]], "%0%0") .. '"' - end - - -- os.execute() uses system() C function, which on Windows passes command - -- to cmd.exe. Escape its special characters. - return (argument:gsub('["^<>!|&%%]', "^%0")) - else - if argument == "" or argument:find('[^a-zA-Z0-9_@%+=:,./-]') then - -- To quote arguments on posix-like systems use single quotes. - -- To represent an embedded single quote close quoted string ('), - -- add escaped quote (\'), open quoted string again ('). - argument = "'" .. argument:gsub("'", [['\'']]) .. "'" - end - - return argument - end -end - ---- error out of this program gracefully. --- @param[opt] code The exit code, defaults to -`1` if omitted --- @param msg The exit message will be sent to `stderr` (will be formatted with the extra parameters) --- @param ... extra arguments for message's format' --- @see utils.fprintf --- @usage utils.quit(-1, "Error '%s' happened", "42") --- -- is equivalent to --- utils.quit("Error '%s' happened", "42") --> Error '42' happened -function utils.quit(code, msg, ...) - if type(code) == 'string' then - utils.fprintf(io.stderr, code, msg, ...) - io.stderr:write('\n') - code = -1 -- TODO: this is odd, see the test. Which returns 255 as exit code - elseif msg then - utils.fprintf(io.stderr, msg, ...) - io.stderr:write('\n') - end - os.exit(code, true) -end - - ---- String functions --- @section string-functions - ---- escape any Lua 'magic' characters in a string --- @param s The input string -function utils.escape(s) - utils.assert_string(1,s) - return (s:gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1')) -end - ---- split a string into a list of strings separated by a delimiter. --- @param s The input string --- @param re optional A Lua string pattern; defaults to '%s+' --- @param plain optional If truthy don't use Lua patterns --- @param n optional maximum number of elements (if there are more, the last will remian un-split) --- @return a list-like table --- @raise error if s is not a string --- @see splitv -function utils.split(s,re,plain,n) - utils.assert_string(1,s) - local i1,ls = 1,{} - if not re then re = '%s+' end - if re == '' then return {s} end - while true do - local i2,i3 = find(s,re,i1,plain) - if not i2 then - local last = sub(s,i1) - if last ~= '' then append(ls,last) end - if #ls == 1 and ls[1] == '' then - return {} - else - return ls - end - end - append(ls,sub(s,i1,i2-1)) - if n and #ls == n then - ls[#ls] = sub(s,i1) - return ls - end - i1 = i3+1 - end -end - ---- split a string into a number of return values. --- Identical to `split` but returns multiple sub-strings instead of --- a single list of sub-strings. --- @param s the string --- @param re A Lua string pattern; defaults to '%s+' --- @param plain don't use Lua patterns --- @param n optional maximum number of splits --- @return n values --- @usage first,next = splitv('user=jane=doe','=', false, 2) --- assert(first == "user") --- assert(next == "jane=doe") --- @see split -function utils.splitv (s,re, plain, n) - return _unpack(utils.split(s,re, plain, n)) -end - - ---- Functional --- @section functional - - ---- 'memoize' a function (cache returned value for next call). --- This is useful if you have a function which is relatively expensive, --- but you don't know in advance what values will be required, so --- building a table upfront is wasteful/impossible. --- @param func a function of at least one argument --- @return a function with at least one argument, which is used as the key. -function utils.memoize(func) - local cache = {} - return function(k) - local res = cache[k] - if res == nil then - res = func(k) - cache[k] = res - end - return res - end -end - - ---- associate a function factory with a type. --- A function factory takes an object of the given type and --- returns a function for evaluating it --- @tab mt metatable --- @func fun a callable that returns a function -function utils.add_function_factory (mt,fun) - _function_factories[mt] = fun -end - -local function _string_lambda(f) - if f:find '^|' or f:find '_' then - local args,body = f:match '|([^|]*)|(.+)' - if f:find '_' then - args = '_' - body = f - else - if not args then return raise 'bad string lambda' end - end - local fstr = 'return function('..args..') return '..body..' end' - local fn,err = utils.load(fstr) - if not fn then return raise(err) end - fn = fn() - return fn - else - return raise 'not a string lambda' - end -end - - ---- an anonymous function as a string. This string is either of the form --- '|args| expression' or is a function of one argument, '_' --- @param lf function as a string --- @return a function --- @function utils.string_lambda --- @usage --- string_lambda '|x|x+1' (2) == 3 --- string_lambda '_+1' (2) == 3 -utils.string_lambda = utils.memoize(_string_lambda) - - ---- bind the first argument of the function to a value. --- @param fn a function of at least two values (may be an operator string) --- @param p a value --- @return a function such that f(x) is fn(p,x) --- @raise same as @{function_arg} --- @see func.bind1 --- @usage local function f(msg, name) --- print(msg .. " " .. name) --- end --- --- local hello = utils.bind1(f, "Hello") --- --- print(hello("world")) --> "Hello world" --- print(hello("sunshine")) --> "Hello sunshine" -function utils.bind1 (fn,p) - fn = utils.function_arg(1,fn) - return function(...) return fn(p,...) end -end - - ---- bind the second argument of the function to a value. --- @param fn a function of at least two values (may be an operator string) --- @param p a value --- @return a function such that f(x) is fn(x,p) --- @raise same as @{function_arg} --- @usage local function f(a, b, c) --- print(a .. " " .. b .. " " .. c) --- end --- --- local hello = utils.bind1(f, "world") --- --- print(hello("Hello", "!")) --> "Hello world !" --- print(hello("Bye", "?")) --> "Bye world ?" -function utils.bind2 (fn,p) - fn = utils.function_arg(1,fn) - return function(x,...) return fn(x,p,...) end -end - - - - ---- Deprecation --- @section deprecation - -do - -- the default implementation - local deprecation_func = function(msg, trace) - if trace then - warn(msg, "\n", trace) -- luacheck: ignore - else - warn(msg) -- luacheck: ignore - end - end - - --- Sets a deprecation warning function. - -- An application can override this function to support proper output of - -- deprecation warnings. The warnings can be generated from libraries or - -- functions by calling `utils.raise_deprecation`. The default function - -- will write to the 'warn' system (introduced in Lua 5.4, or the compatibility - -- function from the `compat` module for earlier versions). - -- - -- Note: only applications should set/change this function, libraries should not. - -- @param func a callback with signature: `function(msg, trace)` both arguments are strings, the latter being optional. - -- @see utils.raise_deprecation - -- @usage - -- -- write to the Nginx logs with OpenResty - -- utils.set_deprecation_func(function(msg, trace) - -- ngx.log(ngx.WARN, msg, (trace and (" " .. trace) or nil)) - -- end) - -- - -- -- disable deprecation warnings - -- utils.set_deprecation_func() - function utils.set_deprecation_func(func) - if func == nil then - deprecation_func = function() end - else - utils.assert_arg(1, func, "function") - deprecation_func = func - end - end - - --- raises a deprecation warning. - -- For options see the usage example below. - -- - -- Note: the `opts.deprecated_after` field is the last version in which - -- a feature or option was NOT YET deprecated! Because when writing the code it - -- is quite often not known in what version the code will land. But the last - -- released version is usually known. - -- @param opts options table - -- @see utils.set_deprecation_func - -- @usage - -- warn("@on") -- enable Lua warnings, they are usually off by default - -- - -- function stringx.islower(str) - -- raise_deprecation { - -- source = "Penlight " .. utils._VERSION, -- optional - -- message = "function 'islower' was renamed to 'is_lower'", -- required - -- version_removed = "2.0.0", -- optional - -- deprecated_after = "1.2.3", -- optional - -- no_trace = true, -- optional - -- } - -- return stringx.is_lower(str) - -- end - -- -- output: "[Penlight 1.9.2] function 'islower' was renamed to 'is_lower' (deprecated after 1.2.3, scheduled for removal in 2.0.0)" - function utils.raise_deprecation(opts) - utils.assert_arg(1, opts, "table") - if type(opts.message) ~= "string" then - error("field 'message' of the options table must be a string", 2) - end - local trace - if not opts.no_trace then - trace = debug.traceback("", 2):match("[\n%s]*(.-)$") - end - local msg - if opts.deprecated_after and opts.version_removed then - msg = (" (deprecated after %s, scheduled for removal in %s)"):format( - tostring(opts.deprecated_after), tostring(opts.version_removed)) - elseif opts.deprecated_after then - msg = (" (deprecated after %s)"):format(tostring(opts.deprecated_after)) - elseif opts.version_removed then - msg = (" (scheduled for removal in %s)"):format(tostring(opts.version_removed)) - else - msg = "" - end - - msg = opts.message .. msg - - if opts.source then - msg = "[" .. opts.source .."] " .. msg - else - if msg:sub(1,1) == "@" then - -- in Lua 5.4 "@" prefixed messages are control messages to the warn system - error("message cannot start with '@'", 2) - end - end - - deprecation_func(msg, trace) - end - -end - - -return utils - - diff --git a/src/pl/xml.lua b/src/pl/xml.lua deleted file mode 100644 index 7956de9..0000000 --- a/src/pl/xml.lua +++ /dev/null @@ -1,1164 +0,0 @@ ---- XML LOM Utilities. --- --- This implements some useful things on [LOM](http://matthewwild.co.uk/projects/luaexpat/lom.html) documents, such as returned by `lxp.lom.parse`. --- In particular, it can convert LOM back into XML text, with optional pretty-printing control. --- It is based on stanza.lua from [Prosody](http://hg.prosody.im/trunk/file/4621c92d2368/util/stanza.lua) --- --- > d = xml.parse "alice" --- > = d --- alice --- > = xml.tostring(d,'',' ') --- --- alice --- --- --- Can be used as a lightweight one-stop-shop for simple XML processing; a simple XML parser is included --- but the default is to use `lxp.lom` if it can be found. ---
--- Prosody IM
--- Copyright (C) 2008-2010 Matthew Wild
--- Copyright (C) 2008-2010 Waqas Hussain--
--- classic Lua XML parser by Roberto Ierusalimschy.
--- modified to output LOM format.
--- http://lua-users.org/wiki/LuaXml
--- 
--- See @{06-data.md.XML|the Guide} --- --- Dependencies: `pl.utils` --- --- Soft Dependencies: `lxp.lom` (fallback is to use basic Lua parser) --- @module pl.xml - -local utils = require 'pl.utils' -local split = utils.split -local t_insert = table.insert -local t_concat = table.concat -local t_remove = table.remove -local s_match = string.match -local tostring = tostring -local setmetatable = setmetatable -local getmetatable = getmetatable -local pairs = pairs -local ipairs = ipairs -local type = type -local next = next -local print = print -local unpack = utils.unpack -local s_gsub = string.gsub -local s_sub = string.sub -local s_find = string.find -local pcall = pcall -local require = require - - -utils.raise_deprecation { - source = "Penlight " .. utils._VERSION, - message = "the contents of module 'pl.xml' has been deprecated, please use a more specialized library instead", - version_removed = "2.0.0", - deprecated_after = "1.11.0", - no_trace = true, -} - - - -local _M = {} -local Doc = { __type = "doc" }; -Doc.__index = Doc; - - -local function is_text(s) return type(s) == 'string' end -local function is_tag(d) return type(d) == 'table' and is_text(d.tag) end - - - ---- create a new document node. --- @tparam string tag the tag name --- @tparam[opt={}] table attr attributes (table of name-value pairs) --- @return the Node object --- @see xml.elem --- @usage --- local doc = xml.new("main", { hello = "world", answer = "42" }) --- print(doc) -->
-function _M.new(tag, attr) - if type(tag) ~= "string" then - error("expected 'tag' to be a string value, got: " .. type(tag), 2) - end - attr = attr or {} - if type(attr) ~= "table" then - error("expected 'attr' to be a table value, got: " .. type(attr), 2) - end - - local doc = { tag = tag, attr = attr, last_add = {}}; - return setmetatable(doc, Doc); -end - - ---- parse an XML document. By default, this uses lxp.lom.parse, but --- falls back to basic_parse, or if `use_basic` is truthy --- @param text_or_filename file or string representation --- @param is_file whether text_or_file is a file name or not --- @param use_basic do a basic parse --- @return a parsed LOM document with the document metatatables set --- @return nil, error the error can either be a file error or a parse error -function _M.parse(text_or_filename, is_file, use_basic) - local parser,status,lom - if use_basic then - parser = _M.basic_parse - else - status,lom = pcall(require,'lxp.lom') - if not status then - parser = _M.basic_parse - else - parser = lom.parse - end - end - - if is_file then - local text_or_filename, err = utils.readfile(text_or_filename) - if not text_or_filename then - return nil, err - end - end - - local doc, err = parser(text_or_filename) - if not doc then - return nil, err - end - - if lom then - _M.walk(doc, false, function(_, d) - setmetatable(d, Doc) - end) - end - return doc -end - - ---- Create a Node with a set of children (text or Nodes) and attributes. --- @tparam string tag a tag name --- @tparam table|string items either a single child (text or Node), or a table where the hash --- part is the attributes and the list part is the children (text or Nodes). --- @return the new Node --- @see xml.new --- @see xml.tags --- @usage --- local doc = xml.elem("top", "hello world") -- hello world --- local doc = xml.elem("main", xml.new("child")) --
--- local doc = xml.elem("main", { "this ", "is ", "nice" }) --
this is nice
--- local doc = xml.elem("main", { xml.new "this", --- xml.new "is", --- xml.new "nice" }) --
--- local doc = xml.elem("main", { hello = "world" }) --
--- local doc = xml.elem("main", { --- "prefix", --- xml.elem("child", { "this ", "is ", "nice"}), --- "postfix", --- attrib = "value" --- }) --
prefixthis is nicepostfix
" -function _M.elem(tag, items) - local s = _M.new(tag) - if is_text(items) then items = {items} end - if is_tag(items) then - t_insert(s,items) - elseif type(items) == 'table' then - for k,v in pairs(items) do - if is_text(k) then - s.attr[k] = v - t_insert(s.attr,k) - else - s[k] = v - end - end - end - return s -end - - ---- given a list of names, return a number of element constructors. --- If passing a comma-separated string, then whitespace surrounding the values --- will be stripped. --- --- The returned constructor functions are a shortcut to `xml.elem` where you --- no longer provide the tag-name, but only the `items` table. --- @tparam string|table list a list of names, or a comma-separated string. --- @return (multiple) constructor functions; `function(items)`. For the `items` --- parameter see `xml.elem`. --- @see xml.elem --- @usage --- local new_parent, new_child = xml.tags 'mom, kid' --- doc = new_parent {new_child 'Bob', new_child 'Annie'} --- -- BobAnnie -function _M.tags(list) - local ctors = {} - if is_text(list) then - list = split(list:match("^%s*(.-)%s*$"),'%s*,%s*') - end - for i,tag in ipairs(list) do - local function ctor(items) - return _M.elem(tag,items) - end - ctors[i] = ctor - end - return unpack(ctors) -end - - ---- Adds a document Node, at current position. --- This updates the last inserted position to the new Node. --- @tparam string tag the tag name --- @tparam[opt={}] table attrs attributes (table of name-value pairs) --- @return the current node (`self`) --- @usage --- local doc = xml.new("main") --- doc:addtag("penlight", { hello = "world"}) --- doc:addtag("expat") -- added to 'penlight' since position moved --- print(doc) -->
-function Doc:addtag(tag, attrs) - local s = _M.new(tag, attrs) - self:add_child(s) - t_insert(self.last_add, s) - return self -end - - ---- Adds a text node, at current position. --- @tparam string text a string --- @return the current node (`self`) --- @usage --- local doc = xml.new("main") --- doc:text("penlight") --- doc:text("expat") --- print(doc) -->
-function Doc:text(text) - self:add_child(text) - return self -end - - ---- Moves current position up one level. --- @return the current node (`self`) -function Doc:up() - t_remove(self.last_add) - return self -end - - ---- Resets current position to top level. --- Resets to the `self` node. --- @return the current node (`self`) -function Doc:reset() - local last_add = self.last_add - for i = 1,#last_add do - last_add[i] = nil - end - return self -end - - ---- Append a child to the currrent Node (ignoring current position). --- @param child a child node (either text or a document) --- @return the current node (`self`) --- @usage --- local doc = xml.new("main") --- doc:add_direct_child("dog") --- doc:add_direct_child(xml.new("child")) --- doc:add_direct_child("cat") --- print(doc) -->
dogcat
-function Doc:add_direct_child(child) - t_insert(self, child) - return self -end - - ---- Append a child at the current position (without changing position). --- @param child a child node (either text or a document) --- @return the current node (`self`) --- @usage --- local doc = xml.new("main") --- doc:addtag("one") --- doc:add_child(xml.new("item1")) --- doc:add_child(xml.new("item2")) --- doc:add_child(xml.new("item3")) --- print(doc) -->
-function Doc:add_child(child) - (self.last_add[#self.last_add] or self):add_direct_child(child) - return self -end - - ---accessing attributes: useful not to have to expose implementation (attr) ---but also can allow attr to be nil in any future optimizations - - ---- Set attributes of a document node. --- Will add/overwite values, but will not remove existing ones. --- Operates on the Node itself, will not take position into account. --- @tparam table t a table containing attribute/value pairs --- @return the current node (`self`) -function Doc:set_attribs(t) - -- TODO: keep array part in sync - for k,v in pairs(t) do - self.attr[k] = v - end - return self -end - - ---- Set a single attribute of a document node. --- Operates on the Node itself, will not take position into account. --- @param a attribute --- @param v its value, pass in `nil` to delete the attribute --- @return the current node (`self`) -function Doc:set_attrib(a,v) - -- TODO: keep array part in sync - self.attr[a] = v - return self -end - - ---- Gets the attributes of a document node. --- Operates on the Node itself, will not take position into account. --- @return table with attributes (attribute/value pairs) -function Doc:get_attribs() - return self.attr -end - - - -local template_cache do - local templ_cache = {} - - -- @param templ a template, a string being valid xml to be parsed, or a Node object - function template_cache(templ) - if is_text(templ) then - if templ_cache[templ] then - -- cache hit - return templ_cache[templ] - - else - -- parse and cache - local ptempl, err = _M.parse(templ,false,true) - if not ptempl then - return nil, err - end - templ_cache[templ] = ptempl - return ptempl - end - end - - if is_tag(templ) then - return templ - end - - return nil, "template is not a document" - end -end - - -do - local function is_data(data) - return #data == 0 or type(data[1]) ~= 'table' - end - - - local function prepare_data(data) - -- a hack for ensuring that $1 maps to first element of data, etc. - -- Either this or could change the gsub call just below. - for i,v in ipairs(data) do - data[tostring(i)] = v - end - end - - --- create a substituted copy of a document, - -- @param template may be a document or a string representation which will be parsed and cached - -- @param data a table of name-value pairs or a list of such tables - -- @return an XML document - function Doc.subst(template, data) - if type(data) ~= 'table' or not next(data) then - return nil, "data must be a non-empty table" - end - - if is_data(data) then - prepare_data(data) - end - - local templ, err = template_cache(template) - if err then - return nil, err - end - - local function _subst(item) - return _M.clone(templ, function(s) - return s:gsub('%$(%w+)', item) - end) - end - - if is_data(data) then - return _subst(data) - end - - local list = {} - for _, item in ipairs(data) do - prepare_data(item) - t_insert(list, _subst(item)) - end - - if data.tag then - list = _M.elem(data.tag,list) - end - return list - end -end - - ---- Return the first child with a given tag name (non-recursive). --- @param tag the tag name --- @return the child Node found or `nil` if not found -function Doc:child_with_name(tag) - for _, child in ipairs(self) do - if child.tag == tag then - return child - end - end -end - - -do - -- @param self document node to traverse - -- @param tag tag-name to look for - -- @param list array table to add the matching ones to - -- @param recurse if truthy, recursivly search the node - local function _children_with_name(self, tag, list, recurse) - -- TODO: protect against recursion - for _, child in ipairs(self) do - if type(child) == 'table' then - if child.tag == tag then - t_insert(list, child) - end - if recurse then - _children_with_name(child, tag, list, recurse) - end - end - end - end - - --- Returns all elements in a document that have a given tag. - -- @tparam string tag a tag name - -- @tparam[opt=false] boolean dont_recurse optionally only return the immediate children with this tag name - -- @return a list of elements found, list will be empty if none was found. - function Doc:get_elements_with_name(tag, dont_recurse) - local res = {} - _children_with_name(self, tag, res, not dont_recurse) - return res - end -end - - - ---- Iterator over all children of a document node, including text nodes. --- This function is not recursive, so returns only direct child nodes. --- @return iterator that returns a single Node per iteration. -function Doc:children() - local i = 0; - return function (a) - i = i + 1 - return a[i]; - end, self, i; -end - - ---- Return the first child element of a node, if it exists. --- This will skip text nodes. --- @return first child Node or `nil` if there is none. -function Doc:first_childtag() - if #self == 0 then - return - end - for _, t in ipairs(self) do - if is_tag(t) then - return t - end - end -end - - ---- Iterator that matches tag names, and a namespace (non-recursive). --- @tparam[opt=nil] string tag tag names to return. Returns all tags if not provided. --- @tparam[opt=nil] string xmlns the namespace value ('xmlns' attribute) to return. If not --- provided will match all namespaces. --- @return iterator that returns a single Node per iteration. -function Doc:matching_tags(tag, xmlns) - -- TODO: this doesn't make sense??? namespaces are not "xmnls", as matched below - -- but "xmlns:name"... so should be a string-prefix match if anything... - xmlns = xmlns or self.attr.xmlns; - local tags = self - local next_i = 1 - local max_i = #tags - local node - return function () - for i = next_i, max_i do - node = tags[i]; - if (not tag or node.tag == tag) and - (not xmlns or xmlns == node.attr.xmlns) then - next_i = i + 1 - return node - end - end - end, tags, next_i -end - - ---- Iterator over all child tags of a document node. This will skip over --- text nodes. --- @return iterator that returns a single Node per iteration. -function Doc:childtags() - local i = 0; - return function (a) - local v - repeat - i = i + 1 - v = self[i] - if v and type(v) == 'table' then - return v - end - until not v - end, self[1], i; -end - - ---- Visit child Nodes of a node and call a function, possibly modifying the document. --- Text elements will be skipped. --- This is not recursive, so only direct children will be passed. --- @tparam function callback a function with signature `function(node)`, passed the node. --- The element will be updated with the returned value, or deleted if it returns `nil`. -function Doc:maptags(callback) - local i = 1; - - while i <= #self do - if is_tag(self[i]) then - local ret = callback(self[i]); - if ret == nil then - -- remove it - t_remove(self, i); - - else - -- update it - self[i] = ret; - i = i + 1; - end - else - i = i + 1 - end - end - - return self; -end - - -do - local escape_table = { - ["'"] = "'", - ['"'] = """, - ["<"] = "<", - [">"] = ">", - ["&"] = "&", - } - - --- Escapes a string for safe use in xml. - -- Handles quotes(single+double), less-than, greater-than, and ampersand. - -- @tparam string str string value to escape - -- @return escaped string - -- @usage - -- local esc = xml.xml_escape([["'<>&]]) --> ""'<>&" - function _M.xml_escape(str) - return (s_gsub(str, "['&<>\"]", escape_table)) - end -end -local xml_escape = _M.xml_escape - -do - local escape_table = { - quot = '"', - apos = "'", - lt = "<", - gt = ">", - amp = "&", - } - - --- Unescapes a string from xml. - -- Handles quotes(single+double), less-than, greater-than, and ampersand. - -- @tparam string str string value to unescape - -- @return unescaped string - -- @usage - -- local unesc = xml.xml_escape(""'<>&") --> [["'<>&]] - function _M.xml_unescape(str) - return (str:gsub( "&(%a+);", escape_table)) - end -end -local xml_unescape = _M.xml_unescape - --- pretty printing --- if indent, then put each new tag on its own line --- if attr_indent, put each new attribute on its own line -local function _dostring(t, buf, parentns, block_indent, tag_indent, attr_indent) - local nsid = 0 - local tag = t.tag - - local lf = "" - if tag_indent then - lf = '\n'..block_indent - end - - local alf = " " - if attr_indent then - alf = '\n'..block_indent..attr_indent - end - - t_insert(buf, lf.."<"..tag) - - local function write_attr(k,v) - if s_find(k, "\1", 1, true) then - nsid = nsid + 1 - local ns, attrk = s_match(k, "^([^\1]*)\1?(.*)$") - t_insert(buf, " xmlns:ns"..nsid.."='"..xml_escape(ns).."' ".."ns"..nsid..":"..attrk.."='"..xml_escape(v).."'") - - elseif not (k == "xmlns" and v == parentns) then - t_insert(buf, alf..k.."='"..xml_escape(v).."'"); - end - end - - -- it's useful for testing to have predictable attribute ordering, if available - if #t.attr > 0 then - -- TODO: the key-value list is leading, what if they are not in-sync - for _,k in ipairs(t.attr) do - write_attr(k,t.attr[k]) - end - else - for k, v in pairs(t.attr) do - write_attr(k,v) - end - end - - local len = #t - local has_children - - if len == 0 then - t_insert(buf, attr_indent and '\n'..block_indent.."/>" or "/>") - - else - t_insert(buf, ">"); - - for n = 1, len do - local child = t[n] - - if child.tag then - has_children = true - _dostring(child, buf, t.attr.xmlns, block_indent and block_indent..tag_indent, tag_indent, attr_indent) - - else - -- text element - t_insert(buf, xml_escape(child)) - end - end - - t_insert(buf, (has_children and lf or '')..""); - end -end - ---- Function to pretty-print an XML document. --- @param doc an XML document --- @tparam[opt] string|int b_ind an initial block-indent (required when `t_ind` is set) --- @tparam[opt] string|int t_ind an tag-indent for each level (required when `a_ind` is set) --- @tparam[opt] string|int a_ind if given, indent each attribute pair and put on a separate line --- @tparam[opt] string|bool xml_preface force prefacing with default or custom , if truthy then `<?xml version='1.0'?>` will be used as default. --- @return a string representation --- @see Doc:tostring -function _M.tostring(doc, b_ind, t_ind, a_ind, xml_preface) - local buf = {} - - if type(b_ind) == "number" then b_ind = (" "):rep(b_ind) end - if type(t_ind) == "number" then t_ind = (" "):rep(t_ind) end - if type(a_ind) == "number" then a_ind = (" "):rep(a_ind) end - - if xml_preface then - if type(xml_preface) == "string" then - buf[1] = xml_preface - else - buf[1] = "" - end - end - - _dostring(doc, buf, nil, b_ind, t_ind, a_ind, xml_preface) - - return t_concat(buf) -end - - -Doc.__tostring = _M.tostring - - ---- Method to pretty-print an XML document. --- Invokes `xml.tostring`. --- @tparam[opt] string|int b_ind an initial indent (required when `t_ind` is set) --- @tparam[opt] string|int t_ind an indent for each level (required when `a_ind` is set) --- @tparam[opt] string|int a_ind if given, indent each attribute pair and put on a separate line --- @tparam[opt="<?xml version='1.0'?>"] string xml_preface force prefacing with default or custom --- @return a string representation --- @see xml.tostring -function Doc:tostring(b_ind, t_ind, a_ind, xml_preface) - return _M.tostring(self, b_ind, t_ind, a_ind, xml_preface) -end - - ---- get the full text value of an element. --- @return a single string with all text elements concatenated --- @usage --- local doc = xml.new("main") --- doc:text("one") --- doc:add_child(xml.elem "two") --- doc:text("three") --- --- local t = doc:get_text() --> "onethree" -function Doc:get_text() - local res = {} - for i,el in ipairs(self) do - if is_text(el) then t_insert(res,el) end - end - return t_concat(res); -end - - -do - local function _copy(object, kind, parent, strsubst, lookup_table) - if type(object) ~= "table" then - if strsubst and is_text(object) then - return strsubst(object, kind, parent) - else - return object - end - end - - if lookup_table[object] then - error("recursion detected") - end - lookup_table[object] = true - - local new_table = {} - lookup_table[object] = new_table - - local tag = object.tag - new_table.tag = _copy(tag, '*TAG', parent, strsubst, lookup_table) - - if object.attr then - local res = {} - for attr, value in pairs(object.attr) do - if type(attr) == "string" then - res[attr] = _copy(value, attr, object, strsubst, lookup_table) - end - end - new_table.attr = res - end - - for index = 1, #object do - local v = _copy(object[index], '*TEXT', object, strsubst, lookup_table) - t_insert(new_table,v) - end - - return setmetatable(new_table, getmetatable(object)) - end - - --- Returns a copy of a document. - -- The `strsubst` parameter is a callback with signature `function(object, kind, parent)`. - -- - -- Param `kind` has the following values, and parameters: - -- - -- - `"*TAG"`: `object` is the tag-name, `parent` is the Node object. Returns the new tag name. - -- - -- - `"*TEXT"`: `object` is the text-element, `parent` is the Node object. Returns the new text value. - -- - -- - other strings not prefixed with `*`: `kind` is the attribute name, `object` is the - -- attribute value, `parent` is the Node object. Returns the new attribute value. - -- - -- @tparam Node|string doc a Node object or string (text node) - -- @tparam[opt] function strsubst an optional function for handling string copying - -- which could do substitution, etc. - -- @return copy of the document - -- @see Doc:filter - function _M.clone(doc, strsubst) - return _copy(doc, nil, nil, strsubst, {}) - end -end - - ---- Returns a copy of a document. --- This is the method version of `xml.clone`. --- @see xml.clone --- @name Doc:filter --- @tparam[opt] function strsubst an optional function for handling string copying -Doc.filter = _M.clone -- also available as method - -do - local function _compare(t1, t2, recurse_check) - - local ty1 = type(t1) - local ty2 = type(t2) - - if ty1 ~= ty2 then - return false, 'type mismatch' - end - - if ty1 == 'string' then - if t1 == t2 then - return true - else - return false, 'text '..t1..' ~= text '..t2 - end - end - - if ty1 ~= 'table' or ty2 ~= 'table' then - return false, 'not a document' - end - - if recurse_check[t1] then - return false, "recursive document" - end - recurse_check[t1] = true - - if t1.tag ~= t2.tag then - return false, 'tag '..t1.tag..' ~= tag '..t2.tag - end - - if #t1 ~= #t2 then - return false, 'size '..#t1..' ~= size '..#t2..' for tag '..t1.tag - end - - -- compare attributes - for k,v in pairs(t1.attr) do - local t2_value = t2.attr[k] - if type(k) == "string" then - if t2_value ~= v then return false, 'mismatch attrib' end - else - if t2_value ~= nil and t2_value ~= v then return false, "mismatch attrib order" end - end - end - for k,v in pairs(t2.attr) do - local t1_value = t1.attr[k] - if type(k) == "string" then - if t1_value ~= v then return false, 'mismatch attrib' end - else - if t1_value ~= nil and t1_value ~= v then return false, "mismatch attrib order" end - end - end - - -- compare children - for i = 1, #t1 do - local ok, err = _compare(t1[i], t2[i], recurse_check) - if not ok then - return ok, err - end - end - return true - end - - --- Compare two documents or elements. - -- Equality is based on tag, child nodes (text and tags), attributes and order - -- of those (order only fails if both are given, and not equal). - -- @tparam Node|string t1 a Node object or string (text node) - -- @tparam Node|string t2 a Node object or string (text node) - -- @treturn boolean `true` when the Nodes are equal. - function _M.compare(t1,t2) - return _compare(t1, t2, {}) - end -end - - ---- is this value a document element? --- @param d any value --- @treturn boolean `true` if it is a `table` with property `tag` being a string value. --- @name is_tag -_M.is_tag = is_tag - - -do - local function _walk(doc, depth_first, operation, recurse_check) - if not depth_first then operation(doc.tag, doc) end - for _,d in ipairs(doc) do - if is_tag(d) then - assert(not recurse_check[d], "recursion detected") - recurse_check[d] = true - _walk(d, depth_first, operation, recurse_check) - end - end - if depth_first then operation(doc.tag, doc) end - end - - --- Calls a function recursively over Nodes in the document. - -- Will only call on tags, it will skip text nodes. - -- The function signature for `operation` is `function(tag_name, Node)`. - -- @tparam Node|string doc a Node object or string (text node) - -- @tparam boolean depth_first visit child nodes first, then the current node - -- @tparam function operation a function which will receive the current tag name and current node. - function _M.walk(doc, depth_first, operation) - return _walk(doc, depth_first, operation, {}) - end -end - - -local html_empty_elements = { --lists all HTML empty (void) elements - br = true, - img = true, - meta = true, - frame = true, - area = true, - hr = true, - base = true, - col = true, - link = true, - input = true, - option = true, - param = true, - isindex = true, - embed = true, -} - ---- Parse a well-formed HTML file as a string. --- Tags are case-insenstive, DOCTYPE is ignored, and empty elements can be .. empty. --- @param s the HTML -function _M.parsehtml(s) - return _M.basic_parse(s,false,true) -end - ---- Parse a simple XML document using a pure Lua parser based on Robero Ierusalimschy's original version. --- @param s the XML document to be parsed. --- @param all_text if true, preserves all whitespace. Otherwise only text containing non-whitespace is included. --- @param html if true, uses relaxed HTML rules for parsing -function _M.basic_parse(s, all_text, html) - local stack = {} - local top = {} - - local function parseargs(s) - local arg = {} - s:gsub("([%w:%-_]+)%s*=%s*([\"'])(.-)%2", function (w, _, a) - if html then w = w:lower() end - arg[w] = xml_unescape(a) - end) - if html then - s:gsub("([%w:%-_]+)%s*=%s*([^\"']+)%s*", function (w, a) - w = w:lower() - arg[w] = xml_unescape(a) - end) - end - return arg - end - - t_insert(stack, top) - local ni,c,label,xarg, empty, _, istart - local i = 1 - local j - -- we're not interested in - _,istart = s_find(s,'^%s*<%?[^%?]+%?>%s*') - if not istart then -- or - _,istart = s_find(s,'^%s*%s*') - end - if istart then i = istart+1 end - while true do - ni,j,c,label,xarg, empty = s_find(s, "<([%/!]?)([%w:%-_]+)(.-)(%/?)>", i) - if not ni then break end - if c == "!" then -- comment - -- case where there's no space inside comment - if not (label:match '%-%-$' and xarg == '') then - if xarg:match '%-%-$' then -- we've grabbed it all - j = j - 2 - end - -- match end of comment - _,j = s_find(s, "-->", j, true) - end - else - local text = s_sub(s, i, ni-1) - if html then - label = label:lower() - if html_empty_elements[label] then empty = "/" end - end - if all_text or not s_find(text, "^%s*$") then - t_insert(top, xml_unescape(text)) - end - if empty == "/" then -- empty element tag - t_insert(top, setmetatable({tag=label, attr=parseargs(xarg), empty=1},Doc)) - elseif c == "" then -- start tag - top = setmetatable({tag=label, attr=parseargs(xarg)},Doc) - t_insert(stack, top) -- new level - else -- end tag - local toclose = t_remove(stack) -- remove top - top = stack[#stack] - if #stack < 1 then - error("nothing to close with "..label..':'..text) - end - if toclose.tag ~= label then - error("trying to close "..toclose.tag.." with "..label.." "..text) - end - t_insert(top, toclose) - end - end - i = j+1 - end - local text = s_sub(s, i) - if all_text or not s_find(text, "^%s*$") then - t_insert(stack[#stack], xml_unescape(text)) - end - if #stack > 1 then - error("unclosed "..stack[#stack].tag) - end - local res = stack[1] - return is_text(res[1]) and res[2] or res[1] -end - -do - local match do - - local function empty(attr) return not attr or not next(attr) end - - local append_capture do - -- returns the key,value pair from a table if it has exactly one entry - local function has_one_element(t) - local key,value = next(t) - if next(t,key) ~= nil then return false end - return key,value - end - - function append_capture(res,tbl) - if not empty(tbl) then -- no point in capturing empty tables... - local key - if tbl._ then -- if $_ was set then it is meant as the top-level key for the captured table - key = tbl._ - tbl._ = nil - if empty(tbl) then return end - end - -- a table with only one pair {[0]=value} shall be reduced to that value - local numkey,val = has_one_element(tbl) - if numkey == 0 then tbl = val end - if key then - res[key] = tbl - else -- otherwise, we append the captured table - t_insert(res,tbl) - end - end - end - end - - local function make_number(pat) - if pat:find '^%d+$' then -- $1 etc means use this as an array location - pat = tonumber(pat) - end - return pat - end - - local function capture_attrib(res,pat,value) - pat = make_number(pat:sub(2)) - res[pat] = value - return true - end - - function match(d,pat,res,keep_going) - local ret = true - if d == nil then d = '' end --return false end - -- attribute string matching is straight equality, except if the pattern is a $ capture, - -- which always succeeds. - if is_text(d) then - if not is_text(pat) then return false end - if _M.debug then print(d,pat) end - if pat:find '^%$' then - return capture_attrib(res,pat,d) - else - return d == pat - end - else - if _M.debug then print(d.tag,pat.tag) end - -- this is an element node. For a match to succeed, the attributes must - -- match as well. - -- a tagname in the pattern ending with '-' is a wildcard and matches like an attribute - local tagpat = pat.tag:match '^(.-)%-$' - if tagpat then - tagpat = make_number(tagpat) - res[tagpat] = d.tag - end - if d.tag == pat.tag or tagpat then - - if not empty(pat.attr) then - if empty(d.attr) then ret = false - else - for prop,pval in pairs(pat.attr) do - local dval = d.attr[prop] - if not match(dval,pval,res) then ret = false; break end - end - end - end - -- the pattern may have child nodes. We match partially, so that {P1,P2} shall match {X,P1,X,X,P2,..} - if ret and #pat > 0 then - local i,j = 1,1 - local function next_elem() - j = j + 1 -- next child element of data - if is_text(d[j]) then j = j + 1 end - return j <= #d - end - repeat - local p = pat[i] - -- repeated {{<...>}} patterns shall match one or more elements - -- so e.g. {P+} will match {X,X,P,P,X,P,X,X,X} - if is_tag(p) and p.repeated then - local found - repeat - local tbl = {} - ret = match(d[j],p,tbl,false) - if ret then - found = false --true - append_capture(res,tbl) - end - until not next_elem() or (found and not ret) - i = i + 1 - else - ret = match(d[j],p,res,false) - if ret then i = i + 1 end - end - until not next_elem() or i > #pat -- run out of elements or patterns to match - -- if every element in our pattern matched ok, then it's been a successful match - if i > #pat then return true end - end - if ret then return true end - else - ret = false - end - -- keep going anyway - look at the children! - if keep_going then - for child in d:childtags() do - ret = match(child,pat,res,keep_going) - if ret then break end - end - end - end - return ret - end - end - - --- does something... - function Doc:match(pat) - local err - pat,err = template_cache(pat) - if not pat then return nil, err end - _M.walk(pat,false,function(_,d) - if is_text(d[1]) and is_tag(d[2]) and is_text(d[3]) and - d[1]:find '%s*{{' and d[3]:find '}}%s*' then - t_remove(d,1) - t_remove(d,2) - d[1].repeated = true - end - end) - - local res = {} - local ret = match(self,pat,res,true) - return res,ret - end -end - - -return _M - diff --git a/test/init.fnl b/test/init.fnl index 38c6c21..2dbb9d0 100644 --- a/test/init.fnl +++ b/test/init.fnl @@ -1,5 +1,5 @@ ((require :busted.runner)) -(tset (require :fennel) :path "./?.fnl;./src/?.fnl") + (require :test.json-rpc-test) (require :test.string-processing-test) (require :test.lsp-test) diff --git a/test/json-rpc-test.fnl b/test/json-rpc-test.fnl index 1ef4ff0..1967b84 100644 --- a/test/json-rpc-test.fnl +++ b/test/json-rpc-test.fnl @@ -2,8 +2,7 @@ (local assert (require :luassert)) (local fennel (require :fennel)) -(local stringio (require :pl.stringio)) - +(local stringio (require :test.pl.stringio)) (local json-rpc (require :fennel-ls.json-rpc)) (describe "json-rpc" diff --git a/test/lsp-test.fnl b/test/lsp-test.fnl index e713161..ec2b6de 100644 --- a/test/lsp-test.fnl +++ b/test/lsp-test.fnl @@ -1,16 +1,9 @@ (import-macros {: assert-matches : describe : it} :test.macros) (local assert (require :luassert)) +(local {: ROOT-PATH : ROOT-URI} (require :test.util)) (local dispatch (require :fennel-ls.dispatch)) -(local stringx (require :pl.stringx)) -(local ROOT-PATH - (-> (io.popen "pwd") - (: :read :*a) - (stringx.strip) - (.. "/test/test-project"))) -(local ROOT-URI - (.. "file://" ROOT-PATH)) (local server-initialize-message {:id 1 diff --git a/src/pl/LICENSE.md b/test/pl/LICENSE.md similarity index 100% rename from src/pl/LICENSE.md rename to test/pl/LICENSE.md diff --git a/src/pl/stringio.lua b/test/pl/stringio.lua similarity index 100% rename from src/pl/stringio.lua rename to test/pl/stringio.lua diff --git a/test/string-processing-test.fnl b/test/string-processing-test.fnl index e84bf11..94f00b4 100644 --- a/test/string-processing-test.fnl +++ b/test/string-processing-test.fnl @@ -2,8 +2,6 @@ (local assert (require :luassert)) (local fennel (require :fennel)) -(local stringx (require :pl.stringx)) - (local util (require :fennel-ls.util)) (describe "document" diff --git a/test/util.fnl b/test/util.fnl index 16f699e..5f3c803 100644 --- a/test/util.fnl +++ b/test/util.fnl @@ -1,10 +1,9 @@ -(local stringx (require :pl.stringx)) (local dispatch (require :fennel-ls.dispatch)) (local ROOT-PATH (-> (io.popen "pwd") (: :read :*a) - (stringx.strip) + (: :sub 1 -2) ;; take off newline (.. "/test/test-project"))) (local ROOT-URI (.. "file://" ROOT-PATH))