Skip to content

Commit 7b2f24f

Browse files
committed
add expand
1 parent a76ee04 commit 7b2f24f

File tree

2 files changed

+149
-11
lines changed

2 files changed

+149
-11
lines changed

lua/plenary/path2.lua

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@
6767
---
6868
--- - `find_upwards` returns `nil` if file not found rather than an empty string
6969

70+
71+
-- TODO: could probably do with more `make_relative` tests
72+
-- - walk up close to root
73+
-- - add "walk_up" in test name
74+
-- TODO: shorten: i think `vim.list_contains` is not nvim-0.7 compat (maybe use like a set?)
75+
-- TODO: verify unix tests pass
76+
-- TODO: add windows test for path2_spec only?
77+
7078
local bit = require "plenary.bit"
7179
local uv = vim.loop
7280
local iswin = uv.os_uname().sysname == "Windows_NT"
@@ -80,6 +88,7 @@ local hasshellslash = vim.fn.exists "+shellslash" == 1
8088
---@field convert_altsep fun(self: plenary._Path, p:string): string
8189
---@field split_root fun(self: plenary._Path, part:string): string, string, string
8290
---@field join fun(self: plenary._Path, path: string, ...: string): string
91+
---@field expand fun(self: plenary._Path, parts: string[], sep: string?): string[]
8392

8493
---@class plenary._WindowsPath : plenary._Path
8594
local _WindowsPath = {
@@ -200,6 +209,38 @@ function _WindowsPath:join(path, ...)
200209
return result_drive .. result_root .. table.concat(parts)
201210
end
202211

212+
---@param parts string[]
213+
---@param sep string
214+
---@return string[] new_path
215+
function _WindowsPath:expand(parts, sep)
216+
-- Variables have a percent sign on both sides: %ThisIsAVariable%
217+
-- The variable name can include spaces, punctuation and mixed case:
218+
-- %_Another Ex.ample%
219+
-- But they aren't case sensitive
220+
--
221+
-- A variable name may include any of the following characters:
222+
-- A-Z, a-z, 0-9, # $ ' ( ) * + , - . ? @ [ ] _ { } ~
223+
-- The first character of the name must not be numeric.
224+
225+
-- this would be MUCH cleaner to implement with LPEG but backwards compatibility...
226+
local pattern = "%%[A-Za-z#$'()*+,%-.?@[%]_{}~][A-Za-z0-9#$'()*+,%-.?@[%]_{}~]*%%"
227+
228+
local new_parts = {}
229+
for _, part in ipairs(parts) do
230+
part = part:gsub(pattern, function(m)
231+
local var_name = m:sub(2):sub(1, -2)
232+
233+
---@diagnostic disable-next-line: missing-parameter
234+
local var = uv.os_getenv(var_name)
235+
return var and (var:gsub("\\", sep)) or m
236+
end)
237+
238+
table.insert(new_parts, part)
239+
end
240+
241+
return new_parts
242+
end
243+
203244
---@class plenary._PosixPath : plenary._Path
204245
local _PosixPath = {
205246
sep = "/",
@@ -249,6 +290,34 @@ function _PosixPath:join(path, ...)
249290
return table.concat(parts)
250291
end
251292

293+
---@param parts string[]
294+
---@return string[] new_path
295+
function _PosixPath:expand(parts)
296+
-- Environment variable names used by the utilities in the Shell and
297+
-- Utilities volume of IEEE Std 1003.1-2001 consist solely of uppercase
298+
-- letters, digits, and the '_' (underscore) from the characters defined in
299+
-- Portable Character Set and do not begin with a digit. Other characters may
300+
-- be permitted by an implementation; applications shall tolerate the
301+
-- presence of such names.
302+
303+
local pattern = "%$[A-Z_][A-Z0-9_]*"
304+
305+
local new_parts = {}
306+
for _, part in ipairs(parts) do
307+
part = part:gsub(pattern, function(m)
308+
local var_name = m:sub(2)
309+
310+
---@diagnostic disable-next-line: missing-parameter
311+
local var = uv.os_getenv(var_name)
312+
return var or m
313+
end)
314+
315+
table.insert(new_parts, part)
316+
end
317+
318+
return new_parts
319+
end
320+
252321
local S_IF = {
253322
-- S_IFDIR = 0o040000 # directory
254323
DIR = 0x4000,
@@ -650,6 +719,9 @@ end
650719
--- if given path doesn't exists and isn't already an absolute path, creates
651720
--- one using the cwd
652721
---
722+
--- DOES NOT expand environment variables and home/user constructs (`~` and `~user`).
723+
--- Use `expand` for this.
724+
---
653725
--- respects 'shellslash' on Windows
654726
---@return string
655727
function Path:absolute()
@@ -675,6 +747,18 @@ function Path:absolute()
675747
return self._absolute
676748
end
677749

750+
--- get the environment variable expanded filename
751+
---@return string
752+
function Path:expand()
753+
local relparts = self._flavor:expand(self.relparts, self.sep)
754+
local filename = self:_filename(nil, nil, relparts)
755+
756+
filename = filename:gsub("^~([^" .. self.sep .. "]+)" .. self.sep, function(m)
757+
return Path:new(self.path.home):parent().filename .. self.sep .. m .. self.sep
758+
end)
759+
return (filename:gsub("^~", self.path.home))
760+
end
761+
678762
---@param ... plenary.Path2Args
679763
---@return plenary.Path2
680764
function Path:joinpath(...)

tests/plenary/path2_spec.lua

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,25 @@ local function set_shellslash(bool)
1313
end
1414
end
1515

16+
local function it_win(name, test_fn)
17+
if not hasshellslash then
18+
it(name, test_fn)
19+
else
20+
local orig = vim.o.shellslash
21+
vim.o.shellslash = true
22+
it(name .. " (shellslash)", test_fn)
23+
24+
vim.o.shellslash = false
25+
it(name .. " (noshellslash)", test_fn)
26+
vim.o.shellslash = orig
27+
end
28+
end
29+
1630
local function it_cross_plat(name, test_fn)
1731
if not iswin then
1832
it(name .. " - unix", test_fn)
1933
else
20-
if not hasshellslash then
21-
it(name .. " - windows", test_fn)
22-
else
23-
local orig = vim.o.shellslash
24-
vim.o.shellslash = true
25-
it(name .. " - windows (shellslash)", test_fn)
26-
27-
vim.o.shellslash = false
28-
it(name .. " - windows (noshellslash)", test_fn)
29-
vim.o.shellslash = orig
30-
end
34+
it_win(name .. " - windows", test_fn)
3135
end
3236
end
3337

@@ -1123,4 +1127,54 @@ SOFTWARE.]]
11231127
assert.is_nil(res)
11241128
end)
11251129
end)
1130+
1131+
describe("expand", function()
1132+
uv.os_setenv("BARVAR", "bar")
1133+
1134+
describe("unix", function()
1135+
if iswin then
1136+
return
1137+
end
1138+
1139+
it("match valid env var", function()
1140+
local p = Path:new "foo/$BARVAR/baz"
1141+
assert.are.same("foo/bar/baz", p:expand())
1142+
end)
1143+
1144+
it_win("ignore invalid env var", function()
1145+
local p = Path:new "foo/$NOT_A_REAL_ENV_VAR/baz"
1146+
assert.are.same(p.filename, p:expand())
1147+
end)
1148+
end)
1149+
1150+
describe("windows", function()
1151+
if not iswin then
1152+
return
1153+
end
1154+
1155+
it_win("match valid env var", function()
1156+
local p = Path:new "foo/%BARVAR%/baz"
1157+
local expect = Path:new "foo/bar/baz"
1158+
assert.are.same(expect.filename, p:expand())
1159+
end)
1160+
1161+
it_win("ignore invalid env var", function()
1162+
local p = Path:new "foo/%NOT_A_REAL_ENV_VAR%/baz"
1163+
assert.are.same(p.filename, p:expand())
1164+
end)
1165+
end)
1166+
1167+
it_cross_plat("matches ~", function()
1168+
local p = Path:new "~/hello"
1169+
local expect = Path:new { path.home, "hello" }
1170+
assert.are.same(expect.filename, p:expand())
1171+
end)
1172+
1173+
it_cross_plat("matches ~user", function()
1174+
local p = Path:new "~otheruser/hello"
1175+
local home = Path:new(path.home):parent() / "otheruser"
1176+
local expect = home / "hello"
1177+
assert.are.same(expect.filename, p:expand())
1178+
end)
1179+
end)
11261180
end)

0 commit comments

Comments
 (0)