Skip to content

Commit c537b77

Browse files
committed
feat(path): add expandvars
1 parent dc40d37 commit c537b77

6 files changed

Lines changed: 121 additions & 9 deletions

File tree

spec/ntpath_spec.lua

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,6 @@ local tbl = mods.tbl
1818
local tbl_keys = tbl.keys
1919
local fmt = string.format
2020

21-
local function with_env(env, fn)
22-
stub(os, "getenv", function(name)
23-
return env[name]
24-
end)
25-
fn()
26-
os.getenv:revert() ---@diagnostic disable-line: undefined-field
27-
end
28-
2921
describe("mods.ntpath", function()
3022
local cwd = lfs.currentdir()
3123

@@ -517,6 +509,30 @@ describe("mods.ntpath", function()
517509
{ "~" , { [[C:\Users\eric]] }},
518510
{ "~test", { nil, "home directory for user is not set" }},
519511
}
512+
},
513+
expandvars = {
514+
{
515+
env = { foo = "bar", ["{foo"] = "baz1", ["{foo}"] = "baz2" },
516+
{ "foo" , { "foo" }},
517+
{ "$foo bar" , { "bar bar" }},
518+
{ "${foo}bar" , { "barbar" }},
519+
{ "$[foo]bar" , { "$[foo]bar" }},
520+
{ "$bar bar" , { "$bar bar" }},
521+
{ "$?bar" , { "$?bar" }},
522+
{ "$foo}bar" , { "bar}bar" }},
523+
{ "${foo" , { "${foo" }},
524+
{ "${{foo}}" , { "baz1}" }},
525+
{ "$foo$foo" , { "barbar" }},
526+
{ "$bar$bar" , { "$bar$bar" }},
527+
{ "%foo% bar" , { "bar bar" }},
528+
{ "%foo%bar" , { "barbar" }},
529+
{ "%foo%%foo%" , { "barbar" }},
530+
{ "%%foo%%foo%foo%", { "%foo%foobar" }},
531+
{ "%?bar%" , { "%?bar%" }},
532+
{ "%foo%%bar" , { "bar%bar" }},
533+
{ "'%foo%'%bar" , { "'%foo%'%bar" }},
534+
{ "bar'%foo%" , { "bar'%foo%" }},
535+
},
520536
}
521537
}
522538

spec/path_spec.lua

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,4 +617,20 @@ describe("mods.path", function()
617617
end
618618
end
619619
end
620+
621+
it("expandvars()", function()
622+
with_env({ foo = "bar", ["{foo"] = "baz1", ["{foo}"] = "baz2" }, function()
623+
assert.are_same({ "foo" }, { posixpath.expandvars("foo") })
624+
assert.are_same({ "bar bar" }, { posixpath.expandvars("$foo bar") })
625+
assert.are_same({ "barbar" }, { posixpath.expandvars("${foo}bar") })
626+
assert.are_same({ "$[foo]bar" }, { posixpath.expandvars("$[foo]bar") })
627+
assert.are_same({ "$bar bar" }, { posixpath.expandvars("$bar bar") })
628+
assert.are_same({ "$?bar" }, { posixpath.expandvars("$?bar") })
629+
assert.are_same({ "bar}bar" }, { posixpath.expandvars("$foo}bar") })
630+
assert.are_same({ "${foo" }, { posixpath.expandvars("${foo") })
631+
assert.are_same({ "baz1}" }, { posixpath.expandvars("${{foo}}") })
632+
assert.are_same({ "barbar" }, { posixpath.expandvars("$foo$foo") })
633+
assert.are_same({ "$bar$bar" }, { posixpath.expandvars("$bar$bar") })
634+
end)
635+
end)
620636
end)

src/mods/ntpath.lua

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,47 @@ function M.commonpath(paths)
485485
return first_drive .. first_root .. concat(common, SEP)
486486
end
487487

488+
function M._expand_percent_vars(p)
489+
local out = {}
490+
local i = 1
491+
local n = #p
492+
493+
while i <= n do
494+
local ch = sub(p, i, i)
495+
if ch ~= "%" then
496+
out[#out + 1] = ch
497+
i = i + 1
498+
else
499+
if i > 1 and sub(p, i - 1, i - 1) == "'" then
500+
local close = find(p, "%", i + 1, true)
501+
if close then
502+
out[#out + 1] = sub(p, i, close)
503+
i = close + 1
504+
else
505+
out[#out + 1] = "%"
506+
i = i + 1
507+
end
508+
elseif i < n and sub(p, i + 1, i + 1) == "%" then
509+
out[#out + 1] = "%"
510+
i = i + 2
511+
else
512+
local close = find(p, "%", i + 1, true)
513+
if not close then
514+
out[#out + 1] = "%"
515+
i = i + 1
516+
else
517+
local name = sub(p, i + 1, close - 1)
518+
local v = getenv(name)
519+
out[#out + 1] = v and v or ("%" .. name .. "%")
520+
i = close + 1
521+
end
522+
end
523+
end
524+
end
525+
526+
return concat(out)
527+
end
528+
488529
setmetatable(M, {
489530
__index = function(t, k)
490531
local v = path[k]

src/mods/path.lua

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ local rfind = str.rfind
1919
local concat = table.concat
2020
local find = string.find
2121
local fmt = string.format
22+
local getenv = os.getenv
2223
local gmatch = string.gmatch
2324
local gsub = string.gsub
2425
local lower = string.lower
@@ -109,6 +110,13 @@ local function match_part(name, pattern, case_sensitive)
109110
return match(name, "^" .. s .. "$") ~= nil
110111
end
111112

113+
local function expandvars(getenv_fn, str, pattern, prefix, suffix)
114+
return gsub(str, pattern, function(name)
115+
local v = getenv_fn(name)
116+
return v and v or (prefix .. name .. suffix)
117+
end)
118+
end
119+
112120
function M._splitext(p, sep, altsep, extsep)
113121
local sep_index = rfind(p, sep) or 0
114122
if altsep then
@@ -161,6 +169,13 @@ function M.drive(p)
161169
return (splitroot(p))
162170
end
163171

172+
function M.expandvars(p)
173+
assert_arg(1, p, "string")
174+
local res = expandvars(getenv, p, "%${([^}]*)}", "${", "}")
175+
res = expandvars(getenv, res, "%$([%w_]+)", "$", "")
176+
return is_win and mods.ntpath._expand_percent_vars(res) or res
177+
end
178+
164179
function M.is_relative_to(p, other)
165180
assert_arg(1, p, "string")
166181
assert_arg(2, other, "string")
@@ -330,6 +345,7 @@ if _TEST then
330345
-- stylua: ignore start
331346
function M._set_windows_semantics() set_semantics(true) end
332347
function M._set_posix_semantics() set_semantics(false) end
348+
getenv = function(name) return os.getenv(name) end
333349
end
334350

335351
return M

types/ntpath.lua

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@
2121
---@class mods.ntpath:mods.path
2222
local M = {}
2323

24+
---@ignore
25+
---
26+
---Expand percent-style variables in a string.
27+
---
28+
---@param p string
29+
---@return string expanded
30+
---@private
31+
function M._expand_percent_vars(p) end
2432
---
2533
---Return `true` when `path` points to a mount root.
2634
---

types/path.lua

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,21 @@ function M.dirname(path) end
199199
---@nodiscard
200200
function M.expanduser(path) end
201201

202+
---
203+
---Expand vars in a path (`$VAR`/`${VAR}` everywhere, `%VAR%` on Windows).
204+
---
205+
---```lua
206+
---path.expandvars("$HOME/bin") --> "/home/me/bin"
207+
---path.expandvars("${XDG_CONFIG_HOME}/nvim") --> "/home/me/.config/nvim"
208+
---path.expandvars("%USERPROFILE%\\bin") --> "C:\\Users\\me\\bin"
209+
---path.expandvars("$UNKNOWN/bin") --> "$UNKNOWN/bin"
210+
---```
211+
---
212+
---@param path string Path containing variable placeholders.
213+
---@return string expandedPath Path with variable values substituted.
214+
---@nodiscard
215+
function M.expandvars(path) end
216+
202217
---
203218
---Return the current user's home directory path.
204219
---
@@ -483,7 +498,7 @@ function M.match(path, pattern, case_sensitive) end
483498
---Convert a `file://` URI to a local absolute path.
484499
---
485500
---```lua
486-
---path.from_uri("file://localhost/tmp/a.txt") --> "/tmp/a.txt", nil
501+
---path.from_uri("file://localhost/tmp/a.txt") --> "/tmp/a.txt"
487502
---```
488503
---
489504
---@param uri string URI value.

0 commit comments

Comments
 (0)