diff --git a/lua/pl/app.lua b/lua/pl/app.lua index 736e166e..bfe69793 100644 --- a/lua/pl/app.lua +++ b/lua/pl/app.lua @@ -10,6 +10,18 @@ local path = require 'pl.path' local app = {} + +--- Sets/clears an environment variable default, to use with `utils.getenv`. +-- This links to `utils.setenv_default`. +-- @function setenv_default +-- @usage -- override the global with this implementation to control +-- -- environment variables with defaults on application level. +-- os.getenv = require("pl.utils").getenv +-- +-- utils.setenv_default("MY_APP_CONFIG", "~/.my_app") +app.setenv_default = utils.setenv_default + + --- return the name of the current script running. -- The name will be the name as passed on the command line -- @return string filename @@ -69,8 +81,8 @@ function app.require_here (base, nofollow) 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. +-- These will look like `'~/.SNAME/file'`, with '~' expanded through `path.expanduser` and +-- `SNAME` is the filename of the script without `'.lua'` extension (see `script_name`). -- If the directory does not exist, it will be created. -- @string file a filename (w/out path) -- @return a full pathname, or nil @@ -78,8 +90,8 @@ end -- @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 +-- print(app.appfile 'some\test.txt') +-- -- C:\Documents and Settings\steve\.testapp\some\test.txt function app.appfile(file) local sfullname, err = app.script_name() if not sfullname then return utils.raise(err) end @@ -94,7 +106,9 @@ function app.appfile(file) end --- return string indicating operating system. --- @return 'Windows','OSX' or whatever uname returns (e.g. 'Linux') +-- @treturn[1] string 'Windows' on Windows platforms +-- @treturn[2] string 'OSX' on Apple platforms +-- @treturn[3] string whatever `uname` returns on other platforms (e.g. 'Linux') function app.platform() if path.is_windows then return 'Windows' diff --git a/lua/pl/path.lua b/lua/pl/path.lua index 600bf057..a890f9b6 100644 --- a/lua/pl/path.lua +++ b/lua/pl/path.lua @@ -14,11 +14,11 @@ -- 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 getenv = utils.getenv local assert_string,raise = utils.assert_string,utils.raise local res,lfs = _G.pcall(_G.require,'lfs') diff --git a/lua/pl/utils.lua b/lua/pl/utils.lua index 7c641035..8636cfba 100644 --- a/lua/pl/utils.lua +++ b/lua/pl/utils.lua @@ -585,6 +585,49 @@ end --- OS functions -- @section OS-functions +do + local env_defaults = {} -- table to track defaults for env variables + local original_getenv = os.getenv -- in case the global function gets patched with this implementation + + + --- Gets an environment variable, whilst falling back to defaults. The defaults can + -- be set using `utils.setenv`. All Penlight modules will use this method to retrieve + -- environment variables. + -- The name is case-sensitive, except on Windows. + -- @tparam string name the environment variable to lookup. + -- @treturn[1] string the value retrieved + -- @treturn[2] nil if the variables wasn't found, and didn't have a default set + -- @see utils.setenv_default + -- @see app.setenv_default + function utils.getenv(name) + utils.assert_string(1, name) + name = is_windows and name:lower() or name + return original_getenv(name) or env_defaults[name] + end + + + + --- Sets/clears an environment variable default, to use with `utils.getenv`. + -- The name is case-sensitive, except on Windows. + -- @tparam string name the environment variable name to set a default for. + -- @tparam[opt] string value the value to assign as a default, if `nil` the default will be cleared. + -- @return nothing + -- @see utils.getenv + -- @see app.setenv_default + function utils.setenv_default(name, value) + utils.assert_string(1, name) + name = is_windows and name:lower() or name + if value == nil then + env_defaults[name] = nil + else + utils.assert_string(1, value) + env_defaults[name] = value + end + end +end + + + --- 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 @@ -598,8 +641,8 @@ function utils.executeex(cmd, bin) local errfile = os.tmpname() if is_windows and not outfile:find(':') then - outfile = os.getenv('TEMP')..outfile - errfile = os.getenv('TEMP')..errfile + outfile = utils.getenv('TEMP')..outfile + errfile = utils.getenv('TEMP')..errfile end cmd = cmd .. " > " .. utils.quote_arg(outfile) .. " 2> " .. utils.quote_arg(errfile) diff --git a/spec/path_spec.lua b/spec/path_spec.lua index 7f18f29d..93bfef96 100644 --- a/spec/path_spec.lua +++ b/spec/path_spec.lua @@ -19,23 +19,25 @@ end describe("pl.path", function() - local path + local pl_path + local pl_utils local mock_envs local old_get_env before_each(function() + pl_utils = require "pl.utils" mock_envs = {} - old_get_env = os.getenv - os.getenv = function(name) -- luacheck: ignore + old_get_env = pl_utils.getenv + pl_utils.getenv = function(name) -- luacheck: ignore return mock_envs[name] end package.loaded["pl.path"] = nil - path = require "pl.path" + pl_path = require "pl.path" end) after_each(function() package.loaded["pl.path"] = nil - os.getenv = old_get_env -- luacheck: ignore + pl_utils.getenv = old_get_env -- luacheck: ignore end) @@ -46,7 +48,7 @@ describe("pl.path", function() mock_envs = { HOME = "/home/user", } - assert.equal("/home/user/file", path.expanduser("~/file")) + assert.equal("/home/user/file", pl_path.expanduser("~/file")) end) @@ -54,7 +56,7 @@ describe("pl.path", function() mock_envs = {} assert.same( { nil, "failed to expand '~' (HOME not set)" }, - { path.expanduser("~/file")} + { pl_path.expanduser("~/file")} ) end) @@ -63,7 +65,7 @@ describe("pl.path", function() mock_envs = {} assert.same( { nil, "failed to expand '~' (HOME, USERPROFILE, and HOMEDRIVE and/or HOMEPATH not set)" }, - { path.expanduser("~/file")} + { pl_path.expanduser("~/file")} ) end) @@ -75,7 +77,7 @@ describe("pl.path", function() HOMEDRIVE = "C:", HOMEPATH = "\\home\\user3", } - assert.equal("\\home\\user1\\file", path.expanduser("~\\file")) + assert.equal("\\home\\user1\\file", pl_path.expanduser("~\\file")) end) @@ -86,7 +88,7 @@ describe("pl.path", function() HOMEDRIVE = "C:", HOMEPATH = "\\home\\user3", } - assert.equal("\\home\\user2\\file", path.expanduser("~\\file")) + assert.equal("\\home\\user2\\file", pl_path.expanduser("~\\file")) end) @@ -97,7 +99,7 @@ describe("pl.path", function() HOMEDRIVE = "C:", HOMEPATH = "\\home\\user3", } - assert.equal("C:\\home\\user3\\file", path.expanduser("~\\file")) + assert.equal("C:\\home\\user3\\file", pl_path.expanduser("~\\file")) end) end) diff --git a/spec/utils-getenv-setenv_spec.lua b/spec/utils-getenv-setenv_spec.lua new file mode 100644 index 00000000..a50af58e --- /dev/null +++ b/spec/utils-getenv-setenv_spec.lua @@ -0,0 +1,108 @@ +local utils = require("pl.utils") + +describe("pl.utils", function() + + local TEST_NAME = "name" .. tostring(math.random(1000)) + + after_each(function() + utils.setenv_default(TEST_NAME, nil) + end) + + + describe("setenv_default", function () + + it("errors if name isn't a string", function() + assert.has.errors(function() + utils.setenv_default(nil, "value") + end) + assert.has.errors(function() + utils.setenv_default(123, "value") + end) + end) + + + it("doesn't error if name is a string", function() + assert.has.no.errors(function() + utils.setenv_default(TEST_NAME, "value") + end) + end) + + + it("errors if value isn't a string", function() + assert.has.errors(function() + utils.setenv_default(TEST_NAME, 123) + end) + assert.has.errors(function() + utils.setenv_default(123, "value") + end) + end) + + + it("doesn't error if value is nil or a string", function() + assert.has.no.errors(function() + utils.setenv_default(TEST_NAME, "value") + end) + assert.has.no.errors(function() + utils.setenv_default(TEST_NAME, nil) + end) + end) + + end) + + + + describe("getenv", function () + + it("errors if name isn't a string", function() + assert.has.errors(function() + utils.getenv(nil) + end) + assert.has.errors(function() + utils.getenv(123) + end) + end) + + + it("doesn't error if name is a string", function() + assert.has.no.errors(function() + utils.getenv(TEST_NAME) + end) + end) + + + it("returns values set by setenv_default", function() + utils.setenv_default(TEST_NAME, "value") + assert.equal(utils.getenv(TEST_NAME), "value") + + utils.setenv_default(TEST_NAME, nil) + assert.is_nil(utils.getenv(TEST_NAME)) + end) + + + it("returns defaults only as fallback", function() + -- PATH is set on all systems, so we use that to test + finally(function() + utils.setenv_default("PATH", nil) + end) + + utils.setenv_default("PATH", "value") + assert.not_nil(utils.getenv("PATH")) + assert.not_equal(utils.getenv("PATH"), "value") + end) + + + if utils.is_windows then + it("is case-insensitive on Windows", function() + finally(function() + utils.setenv_default(TEST_NAME:upper(), nil) + end) + + assert.is_nil(utils.getenv(TEST_NAME:lower())) -- verify it's unset first + utils.setenv_default(TEST_NAME:upper(), "value") + assert.equal(utils.getenv(TEST_NAME:lower()), "value") + end) + end + + end) + +end)