/
path.lua
253 lines (229 loc) · 8.59 KB
/
path.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
---
-- Path utils.
--
-- **Definition of path** used by this file:
--
-- - it is **not Lua path**, Lua path meaning the way Lua provide table indexing.
-- - especially, **'table[ ]'** notation is not supported
-- - **element separator is '.'** (dot)
-- - provided **path is always cleaned** before being processed:
-- - if the path starts and/or ends with one or several dots, there are automatically removed.<br />
-- e.g.: `'...toto.tutu....'` means `'toto.tutu'`
-- - internal repetitions of successives dots are replaced by a single dot.<br />
-- e.g.: `'toto...tutu.tata..foo.bar'` means `'toto.tutu.tata.foo.bar'`
-- - if a path element can be **converted to a number**, then it is returned as a number,
-- otherwise all elements or paths are returned as strings.<br />
-- e.g.: `split('a.2', 1)` -> `('a', 2)` (where 2 is returned as a number) <br />
-- e.g.: `split('a.b.2e3.c', 1)` -> `('a', 'b.2000.c')`
--
-- @module utils.path
--
local checks = require"checks"
local M = { }
--public API
local split,clean,segments, get, set, gsplit, concat, find
local function pathconcat(pt, starti, endi)
local t = {}
local prev
local empties = 0
starti = starti or 1
endi = endi or #pt
for i = starti, endi do
local v = pt[i]
if not v then break
elseif v == '' then
empties = empties+1
else
table.insert(t, prev)
prev = v
end
end
table.insert(t, prev)
--log('PATH', 'INFO', "pathconcat(%s, %d, %d) generates table %s, wants indexes %d->%d",
-- sprint(pt), starti, endi, sprint(t), 1, endi-starti+1-empties)
return table.concat(t, '.', 1, endi-starti+1-empties)
end
--------------------------------------------------------------------------------
-- Concatenates a sequence of path strings together.
--
-- @function [parent=#utils.path] concat
-- @param varargs list of strings to concatenate into a valid path
-- @return resulting path as a string
--
function concat(...)
return pathconcat({...})
end
--------------------------------------------------------------------------------
-- Cleans a path.
--
-- Removes trailing/preceding/doubling '.'.
--
-- @function [parent=#utils.path] clean
-- @param path string containing the path to clean.
-- @return cleaned path as a string.
--
function clean(path)
checks('string')
local p = segments(path)
return pathconcat(p)
end
--------------------------------------------------------------------------------
-- Sets a value in a tree-like table structure.
--
-- The value to set is indicated by the path relative to the table.
-- This function creates the table structure to store the value, unless the value to set is nil.
-- If the value to set is nil and the table structure already exists then the value is set to nil.
-- If the value is not nil, then the table structure is always created/overwritten and the value set.
--
-- @function [parent=#utils.path] set
-- @param t table where to set the value.
-- @param path can be either a string (see @{#(utils.path).split})
-- or an array where path[1] is the root and path[n] is the leaf.
-- @param value the value to set.
--
function set(t, path, value)
checks('table', 'string|table', '?')
local p = type(path)=='string' and segments(path) or path
local k = table.remove(p)
local t = find(t, p, value~=nil) -- only create the table structure if the value to set is non nil!
-- local t = findtable(t, p, true) -- for the creation of the table structure, even if the value to set is non nil!
if t then t[k] = value end
end
--------------------------------------------------------------------------------
-- Gets the value of a table field. The field can be in a sub table.
--
-- The field to get is indicated by a path relative to the table.
--
-- @function [parent=#utils.path] get
-- @param t table where to set the value.
-- @param path can be eiher a string (see @{split}) or an array where path[1] is the root and path[n] is the leaf.
-- @return value if the field is found.
-- @return nil otherthise.
--
function get(t, path)
checks('table', 'string|table')
local p = type(path)=='string' and segments(path) or path
local k = table.remove(p)
if not k then return t end
local t = find(t, p)
return t and t[k]
end
--------------------------------------------------------------------------------
-- Enumerates path partitions in a for-loop generator, starting from the right.
--
-- For instance, `gsplit "a.b.c"` will generate successively
-- `("a.b.c", ""), ("a.b", "c"), ("a", "b.c"), ("", "a.b.c")`.
--
-- @function [parent=#utils.path] gsplit
-- @param path the path as a string
-- @return the for-loop iterator function
--
function gsplit (path)
checks ('string')
local segs = segments(path)
local nsegs = #segs
local limit = nsegs
local function f()
if limit == -1 then return nil, nil end
local a, b = pathconcat(segs, 1, limit), pathconcat(segs, limit+1, nsegs)
limit = limit - 1
return a, b
end
return f
end
--------------------------------------------------------------------------------
-- Splits a path into two halves, can be used to get path root, tail etc.
--
-- The content of the two halves depends on 'n' param: there will be `n` segments in the first half if `n>0`,
-- `-n` segments in the second half if `n<0`.
--
-- If there are less then `n` segments, returns the path argument followed by
-- an empty path.
--
-- If there are less then `-n` segments, returns an empty path followed by the
-- path argument.
--
-- Note that if a half is a single element and that this element can be converted into a number,
-- it is returned as a number.
--
-- @function [parent=#utils.path] split
-- @param path the path as a string
-- @param n number defining how the path is splitted (see above description).
-- @return the two halves
-- @usage local root, tail = split('a.b.c', 1)
-- ->root contains 'a', tail contains 'b.c'
--
function split(path, n)
checks('string', 'number')
local segments = segments(path)
if n>#segments then return path, ''
elseif -n>#segments then return '', path
else
if n<0 then n=#segments+n end
return pathconcat(segments, 1, n), pathconcat(segments, n+1, #segments)
end
end
--------------------------------------------------------------------------------
-- Splits a path into segments.
--
-- Each segment is delimited by '.' pattern.
--
-- @function [parent=#utils.path] segments
-- @param path string containing the path to split.
-- @return list of split path elements.
--
function segments(path)
checks('string')
local t = {}
local index, newindex, elt = 1
repeat
newindex = path:find(".", index, true) or #path+1 --last round
elt = path:sub(index, newindex-1)
elt = tonumber(elt) or elt
if elt and elt ~= "" then table.insert(t, elt) end
index = newindex+1
until newindex==#path+1
return t
end
--------------------------------------------------------------------------------
-- Retrieves the element in a sub-table corresponding to the path.
--
-- @function [parent=#utils.path] find
-- @param t is the table to look into.
-- @param path can be either a string (see @{segments}) or an array where
-- `path[1]` is the root and `path[n]` is the leaf.
-- @param force parameter allows to create intermediate tables as specified
-- by the path, if necessary.
-- @return returned values depend on force value:
--
-- * if force is false (or nil), find returns the table if it finds one,
-- or it returns nil followed by the subpath that points to non table value
--
-- * if force is true, find overwrites or create tables as necessary so
-- it always returns a table.
--
-- * if force is 'noowr', find creates tables as necessary but does not
-- overwrite non-table values. So as with `force=false`, it only returns a
-- table if possible, and nil followed by the path that points to the first
-- neither-table-nor-nil value otherwise.
--
-- @usage config = {toto={titi={tutu = 5}}}
-- find(config, "toto.titi") -- will return the table titi
--
function find(t, path, force)
checks('table', 'string|table', '?')
path = type(path)=="string" and segments(path) or path
for i, n in ipairs(path) do
local v = t[n]
if type(v) ~= "table" then
if not force or (force=="noowr" and v~=nil) then return nil, pathconcat(path, 1, i)
else v = {} t[n] = v end
end
t = v
end
return t
end
--public API
M.split=split; M.clean=clean; M.segments=segments; M.get=get; M.set=set;
M.gsplit=gsplit; M.concat=concat; M.find=find
return M