Skip to content

Commit 47494d8

Browse files
committed
rework env var expansion
1 parent 1211b5c commit 47494d8

File tree

2 files changed

+155
-62
lines changed

2 files changed

+155
-62
lines changed

lua/plenary/path2.lua

Lines changed: 101 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -136,31 +136,70 @@ end
136136

137137
---@param parts string[]
138138
---@param sep string
139-
---@return string[] new_path
139+
---@return string[] new_parts
140140
function _WindowsPath:expand(parts, sep)
141-
-- Variables have a percent sign on both sides: %ThisIsAVariable%
142-
-- The variable name can include spaces, punctuation and mixed case:
143-
-- %_Another Ex.ample%
144-
-- But they aren't case sensitive
145-
--
146-
-- A variable name may include any of the following characters:
147-
-- A-Z, a-z, 0-9, # $ ' ( ) * + , - . ? @ [ ] _ { } ~
148-
-- The first character of the name must not be numeric.
149-
150-
-- this would be MUCH cleaner to implement with LPEG but backwards compatibility...
151-
local pattern = "%%[A-Za-z#$'()*+,%-.?@[%]_{}~][A-Za-z0-9#$'()*+,%-.?@[%]_{}~]*%%"
152-
153141
local new_parts = {}
142+
143+
local function add_expand(sub_parts, var, part, start, end_)
144+
---@diagnostic disable-next-line: missing-parameter
145+
local val = vim.uv.os_getenv(var)
146+
if val then
147+
table.insert(sub_parts, (val:gsub("\\", sep)))
148+
else
149+
table.insert(sub_parts, part:sub(start, end_))
150+
end
151+
end
152+
154153
for _, part in ipairs(parts) do
155-
part = part:gsub(pattern, function(m)
156-
local var_name = m:sub(2, -2)
154+
local sub_parts = {}
155+
local i = 1
157156

158-
---@diagnostic disable-next-line: missing-parameter
159-
local var = uv.os_getenv(var_name)
160-
return var and (var:gsub("\\", sep)) or m
161-
end)
157+
while i <= #part do
158+
local ch = part:sub(i, i)
159+
if ch == "'" then -- no expansion inside single quotes
160+
local end_ = part:find("'", i + 1, true)
161+
if end_ then
162+
table.insert(sub_parts, part:sub(i, end_))
163+
i = end_
164+
else
165+
table.insert(sub_parts, ch)
166+
end
167+
elseif ch == "%" then
168+
local end_ = part:find("%", i + 1, true)
169+
if end_ then
170+
local var = part:sub(i + 1, end_ - 1)
171+
add_expand(sub_parts, var, part, i, end_)
172+
i = end_
173+
else
174+
table.insert(sub_parts, ch)
175+
end
176+
elseif ch == "$" then
177+
local nextch = part:sub(i + 1, i + 1)
178+
if nextch == "$" then
179+
i = i + 1
180+
table.insert(sub_parts, ch)
181+
elseif nextch == "{" then
182+
local end_ = part:find("}", i + 2, true)
183+
if end_ then
184+
local var = part:sub(i + 2, end_ - 1)
185+
add_expand(sub_parts, var, part, i, end_)
186+
i = end_
187+
else
188+
table.insert(sub_parts, ch)
189+
end
190+
else
191+
local end_ = part:find("[^%w_]", i + 1, false) or #part + 1
192+
local var = part:sub(i + 1, end_ - 1)
193+
add_expand(sub_parts, var, part, i, end_ - 1)
194+
i = end_ - 1
195+
end
196+
else
197+
table.insert(sub_parts, ch)
198+
end
199+
i = i + 1
200+
end
162201

163-
table.insert(new_parts, part)
202+
table.insert(new_parts, table.concat(sub_parts))
164203
end
165204

166205
return new_parts
@@ -232,28 +271,47 @@ function _PosixPath:join(path, ...)
232271
end
233272

234273
---@param parts string[]
235-
---@return string[] new_path
274+
---@return string[] new_parts
236275
function _PosixPath:expand(parts)
237-
-- Environment variable names used by the utilities in the Shell and
238-
-- Utilities volume of IEEE Std 1003.1-2001 consist solely of uppercase
239-
-- letters, digits, and the '_' (underscore) from the characters defined in
240-
-- Portable Character Set and do not begin with a digit. Other characters may
241-
-- be permitted by an implementation; applications shall tolerate the
242-
-- presence of such names.
243-
244-
local pattern = "%$[A-Z_][A-Z0-9_]*"
276+
local function add_expand(sub_parts, var, part, start, end_)
277+
---@diagnostic disable-next-line: missing-parameter
278+
local val = vim.uv.os_getenv(var)
279+
if val then
280+
table.insert(sub_parts, val)
281+
else
282+
table.insert(sub_parts, part:sub(start, end_))
283+
end
284+
end
245285

246286
local new_parts = {}
247287
for _, part in ipairs(parts) do
248-
part = part:gsub(pattern, function(m)
249-
local var_name = m:sub(2)
250-
251-
---@diagnostic disable-next-line: missing-parameter
252-
local var = uv.os_getenv(var_name)
253-
return var or m
254-
end)
288+
local i = 1
289+
local sub_parts = {}
290+
while i <= #part do
291+
local ch = part:sub(i, i)
292+
if ch == "$" then
293+
if part:sub(i + 1, i + 1) == "{" then
294+
local end_ = part:find("}", i + 2, true)
295+
if end_ then
296+
local var = part:sub(i + 2, end_ - 1)
297+
add_expand(sub_parts, var, part, i, end_)
298+
i = end_
299+
else
300+
table.insert(sub_parts, ch)
301+
end
302+
else
303+
local end_ = part:find("[^%w_]", i + 1, false) or #part + 1
304+
local var = part:sub(i + 1, end_ - 1)
305+
add_expand(sub_parts, var, part, i, end_ - 1)
306+
i = end_ - 1
307+
end
308+
else
309+
table.insert(sub_parts, ch)
310+
end
311+
i = i + 1
312+
end
255313

256-
table.insert(new_parts, part)
314+
table.insert(new_parts, table.concat(sub_parts))
257315
end
258316

259317
return new_parts
@@ -714,15 +772,17 @@ function Path:absolute()
714772
end
715773

716774
--- get the environment variable expanded filename
775+
--- also expand ~/ but NOT ~user/ constructs
717776
---@return string
718777
function Path:expand()
719778
local relparts = self._flavor:expand(self.relparts, self.sep)
720779
local filename = self:_filename(nil, nil, relparts)
721780

722-
filename = filename:gsub("^~([^" .. self.sep .. "]+)" .. self.sep, function(m)
723-
return Path:new(self.path.home):parent().filename .. self.sep .. m .. self.sep
724-
end)
725-
return (filename:gsub("^~", self.path.home))
781+
if filename:sub(1, 2) == "~" .. self.sep then
782+
filename = self.path.home .. filename:sub(2)
783+
end
784+
785+
return filename
726786
end
727787

728788
---@param ... plenary.Path2Args

tests/plenary/path2_spec.lua

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,37 +1249,72 @@ SOFTWARE.]]
12491249
uv.os_setenv("FOOVAR", "foo")
12501250
uv.os_setenv("BARVAR", "bar")
12511251

1252-
describe("unix", function()
1253-
if iswin then
1254-
return
1255-
end
1252+
it_cross_plat("match simple valid $ env vars", function()
1253+
assert.are.same("foo", Path:new("$FOOVAR"):expand())
1254+
assert.are.same("foo$", Path:new("$FOOVAR$"):expand())
1255+
assert.are.same(Path:new("foo/bar/baz").filename, Path:new("$FOOVAR/$BARVAR/baz"):expand())
1256+
assert.are.same(Path:new("foo/bar baz").filename, Path:new("$FOOVAR/$BARVAR baz"):expand())
1257+
assert.are.same(Path:new("foo/$BARVARbaz").filename, Path:new("$FOOVAR/$BARVARbaz"):expand())
1258+
end)
12561259

1257-
it("match valid env var", function()
1258-
local p = Path:new "$FOOVAR/$BARVAR/baz"
1259-
assert.are.same("foo/bar/baz", p:expand())
1260-
end)
1260+
it_cross_plat("match simple valid $ env vars with braces", function()
1261+
assert.are.same(Path:new("foo/bar/baz").filename, Path:new("${FOOVAR}/${BARVAR}/baz"):expand())
1262+
assert.are.same(Path:new("foo/bar baz").filename, Path:new("${FOOVAR}/${BARVAR} baz"):expand())
1263+
end)
12611264

1262-
it("ignore invalid env var", function()
1263-
local p = Path:new "foo/$NOT_A_REAL_ENV_VAR/baz"
1264-
assert.are.same(p.filename, p:expand())
1265-
end)
1265+
it_cross_plat("ignore unset $ env var", function()
1266+
local p = Path:new "foo/$NOT_A_REAL_ENV_VAR/baz"
1267+
assert.are.same(p.filename, p:expand())
1268+
end)
1269+
1270+
it_cross_plat("ignore empty $", function()
1271+
local p = Path:new "foo/$/bar$baz$"
1272+
assert.are.same(p.filename, p:expand())
1273+
end)
1274+
1275+
it_cross_plat("ignore empty ${}", function()
1276+
local p = Path:new "foo/${}/bar${}"
1277+
assert.are.same(p.filename, p:expand())
12661278
end)
12671279

12681280
describe("windows", function()
12691281
if not iswin then
12701282
return
12711283
end
12721284

1273-
it_win("match valid env var", function()
1274-
local p = Path:new "%foovar%/%BARVAR%/baz"
1275-
local expect = Path:new "foo/bar/baz"
1276-
assert.are.same(expect.filename, p:expand())
1285+
uv.os_setenv("{foovar", "foo1")
1286+
uv.os_setenv("{foovar}", "foo2")
1287+
1288+
it_win("match valid %% env var", function()
1289+
assert.are.same(Path:new("foo/bar/baz").filename, Path:new("%foovar%/%BARVAR%/baz"):expand())
1290+
assert.are.same(Path:new("foo1/bar/baz").filename, Path:new("%{foovar%/%BARVAR%/baz"):expand())
1291+
assert.are.same(Path:new("foo2/bar/baz").filename, Path:new("%{foovar}%/%BARVAR%/baz"):expand())
1292+
assert.are.same(Path:new("foo/bar baz").filename, Path:new("%foovar%/%BARVAR% baz"):expand())
1293+
end)
1294+
1295+
it_win("empty %%", function()
1296+
local p = Path:new "foo/%%/baz%%"
1297+
assert.are.same(p.filename, p:expand())
12771298
end)
12781299

1279-
it_win("ignore invalid env var", function()
1300+
it_win("match special char env var with ${}", function()
1301+
assert.are.same(Path:new("foo1/bar/baz").filename, Path:new("${{foovar}/%BARVAR%/baz"):expand())
1302+
assert.are.same(Path:new("foo1}/bar/baz").filename, Path:new("${{foovar}}/%BARVAR%/baz"):expand())
1303+
end)
1304+
1305+
it_win("ignore unset %% env var", function()
12801306
local p = Path:new "foo/%NOT_A_REAL_ENV_VAR%/baz"
12811307
assert.are.same(p.filename, p:expand())
12821308
end)
1309+
1310+
it_win("ignore quoted vars", function()
1311+
local paths = { "'%foovar%'", "'${foovar}'", "'$foovar'" }
1312+
for _, p in ipairs(paths) do
1313+
---@diagnostic disable-next-line: cast-local-type
1314+
p = Path:new(p)
1315+
assert.are.same(p.filename, p:expand())
1316+
end
1317+
end)
12831318
end)
12841319

12851320
it_cross_plat("matches ~", function()
@@ -1288,11 +1323,9 @@ SOFTWARE.]]
12881323
assert.are.same(expect.filename, p:expand())
12891324
end)
12901325

1291-
it_cross_plat("matches ~user", function()
1326+
it_cross_plat("does not matches ~user", function()
12921327
local p = Path:new "~otheruser/hello"
1293-
local home = Path:new(path.home):parent() / "otheruser"
1294-
local expect = home / "hello"
1295-
assert.are.same(expect.filename, p:expand())
1328+
assert.are.same(p.filename, p:expand())
12961329
end)
12971330

12981331
uv.os_unsetenv "FOOVAR"

0 commit comments

Comments
 (0)