Skip to content

Commit

Permalink
added validation logic for mode_select, added mode docs, added mode_s…
Browse files Browse the repository at this point in the history
…elect logic into MC_Show
  • Loading branch information
mrcawood committed Jan 16, 2025
1 parent 7bda4ef commit 976a7b7
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 24 deletions.
86 changes: 86 additions & 0 deletions docs/source/370_irreversible.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
.. _mode_select-label:

Mode Select: Irreversible Module Actions
======================================

Purpose
^^^^^^^

The mode select feature allows modulefiles to specify actions that should only be executed in specific modes (load or unload). This is particularly useful for operations that are irreversible or need special handling during module load/unload cycles.

Usage
^^^^^

Mode select is implemented through a table with a ``mode`` field that specifies when the action should be executed::

-- Execute only during load
setenv{
name = "MY_VAR",
value = "some_value",
mode = "load"
}

-- Execute only during unload
setenv{
name = "CLEANUP_VAR",
value = "cleanup_value",
mode = "unload"
}

Supported Modes
^^^^^^^^^^^^^^

The following modes are supported:

* ``load`` - Execute the action only when loading the module
* ``unload`` - Execute the action only when unloading the module

Important Notes
^^^^^^^^^^^^^

1. When using mode select, the specified action becomes irreversible in the opposite mode. For example:

* If an action is specified with ``mode = "load"``, it will not be automatically reversed during unload
* If an action is specified with ``mode = "unload"``, it will not be automatically reversed during load

2. To completely remove the effects of a mode-specific module, you may need to:

* Purge all modules (``module purge``)
* Start a new shell session
* Or manually reverse the changes

Example Scenario
^^^^^^^^^^^^^^^

Consider a module that needs to perform special cleanup during unload::

-- cleanup.lua
help([[
This module demonstrates mode-specific actions
that require special handling during unload.
]])

-- Normal reversible action
prepend_path("PATH", "/path/to/bin")

-- Special cleanup only during unload
setenv{
name = "CLEANUP_REQUIRED",
value = "true",
mode = "unload"
}

In this example:

* The PATH modification is reversible and handled normally
* The CLEANUP_REQUIRED variable is only set during unload
* Loading the module again will not automatically clear CLEANUP_REQUIRED

Best Practices
^^^^^^^^^^^^^

1. Use mode select sparingly and only when necessary
2. Document any irreversible changes in the module's help text
3. Consider providing helper functions or instructions for users to manually reverse changes
4. Test both load and unload scenarios thoroughly
5. Consider the impact on module collections and module restore operations
2 changes: 1 addition & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

Lmod: A New Environment Module System
=====================================

Expand Down Expand Up @@ -148,6 +147,7 @@ Advanced Topics
340_inherit
350_community
360_check_syntax
370_irreversible

Internal Structure of Lmod
^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
17 changes: 17 additions & 0 deletions rt/show/mf/Core/mode_test/1.0.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
help([[
Test module for showing mode-specific function definitions
]])

whatis("Name: Mode Test")
whatis("Version: 1.0")

-- Test showing a standard function
setenv("STANDARD_VAR", "value")

-- Test showing mode-specific functions
setenv{"MODE_VAR", "value", mode="load"}
prepend_path{"MODE_PATH", "/path", mode="unload"}

-- Test showing a variable used in a mode-specific function
local t = {"VAR", "value", mode="load"}
setenv(t)
1 change: 1 addition & 0 deletions rt/show/show.tdesc
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ testdescript = {
runLmod --location show a #14
runLmod --terse show a #15
runLmod show showMe #16
runLmod show mode_test #17
HOME=$ORIG_HOME
Expand Down
22 changes: 21 additions & 1 deletion src/MC_Show.lua
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,27 @@ M.build_unload = MainControl.do_not_build_unload
M.color_banner = MainControl.color_banner

local function l_ShowCmd(name,...)
A[#A+1] = ShowCmdStr(name, ...)
local args = pack(...)
dbg.start{"l_ShowCmd(name=\"",name,"\")"}
for i=1,args.n do
dbg.print{" arg[",i,"] type=",type(args[i]),", value=",args[i],"\n"}
if type(args[i]) == "table" then
dbg.print{" table contents:\n"}
for k,v in pairs(args[i]) do
dbg.print{" key=",k,", value=",v,"\n"}
end
end
end

-- Use ShowCmdTbl for mode-specific functions
if args.n == 1 and type(args[1]) == "table" and args[1].mode then
dbg.print{" Using ShowCmdTbl\n"}
A[#A+1] = ShowCmdTbl(name, args[1])
else
dbg.print{" Using ShowCmdStr\n"}
A[#A+1] = ShowCmdStr(name, ...)
end
dbg.fini("l_ShowCmd")
end

local function l_Show_help(...)
Expand Down
85 changes: 63 additions & 22 deletions src/modfuncs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@ local max = math.max
local _concatTbl = table.concat
local pack = (_VERSION == "Lua 5.1") and argsPack or table.pack -- luacheck: compat
local unpack = (_VERSION == "Lua 5.1") and unpack or table.unpack -- luacheck: compat

-- List of functions that support mode-select
local mode_select_functions = {
setenv = true,
pushenv = true,
unsetenv = true,
prepend_path = true,
append_path = true,
remove_path = true,
load = true
}

--------------------------------------------------------------------------
-- Special table concat function that knows about strings and numbers.
-- @param aa Input array
Expand Down Expand Up @@ -192,16 +204,6 @@ end
-- @param cmdName The command which is getting its arguments validated.
-- @param t The table containing mode selector input
local function l_validateModeSelector(cmdName, t)
if (t.mode == nil) then
mcp:report{msg="e_Mode_Not_Set", fn = myFileName(), cmdName = cmdName}
return false
end

if (#t.mode == 0) then
mcp:report{msg="e_Mode_Not_Set", fn = myFileName(), cmdName = cmdName}
return false
end

local validModes = {load = true, unload = true}
for i = 1, #t.mode do
if not validModes[t.mode[i]] then
Expand All @@ -221,25 +223,64 @@ local function l_list_2_Tbl(first_elem, ...)
local my_mcp = nil
local t = nil
local action = nil
if ( type(first_elem) == "table" )then

-- First check if this is a mode-select capable function call
local is_mode_select_capable = false
local cmdName = "unknown"
if type(first_elem) == "table" and first_elem.cmdName then
cmdName = first_elem.cmdName
is_mode_select_capable = mode_select_functions[cmdName]
end

dbg.print{"Mode selection debug:\n"}
dbg.print{"Function name: ", cmdName, "\n"}
dbg.print{"First elem type: ", type(first_elem), "\n"}
dbg.print{"Is mode-select capable: ", is_mode_select_capable, "\n"}

if type(first_elem) == "table" then
t = first_elem
t.kind = "table"
local my_mode = mode()
local modeA = t.mode or {}

-- Validate mode selector input
if not l_validateModeSelector(t.cmdName or "unknown", t) then
return MCPQ, t
end
-- Only do mode validation for mode-select capable functions using curly brace syntax
if is_mode_select_capable and t.kind == "table" and not t.__waterMark then
dbg.print{"Checking mode for mode-select capable function\n"}
dbg.print{"Current mode: ", my_mode, "\n"}
dbg.print{"Has mode key: ", t.mode ~= nil, "\n"}

-- For mode-select capable functions using curly brace syntax, mode MUST be specified
if not t.mode then
dbg.print{"Error: Mode-select capable function called with curly brace syntax but no mode key\n"}
mcp:report{msg="e_Mode_Not_Set", fn = myFileName(), cmdName = cmdName}
return MCPQ, t
end

for i = 1,#modeA do
if (my_mode == modeA[i]) then
action = true
my_mcp = MCP
break
-- Mode must be a non-empty table
if type(t.mode) ~= "table" or #t.mode == 0 then
dbg.print{"Error: Mode must be a non-empty table\n"}
mcp:report{msg="e_Mode_Not_Set", fn = myFileName(), cmdName = cmdName}
return MCPQ, t
end
end

-- Validate the mode values
if not l_validateModeSelector(cmdName, t) then
return MCPQ, t
end

-- Check if current mode matches any of the specified modes
local modeA = t.mode
for i = 1,#modeA do
if (my_mode == modeA[i]) then
action = true
my_mcp = MCP
break
end
end
else
-- Non mode-select function or non-curly brace syntax
action = true
my_mcp = mcp
end
else
t = pack(first_elem, ...)
t.kind = "list"
Expand Down
34 changes: 34 additions & 0 deletions src/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,40 @@ function ShowCmdStr(name, ...)
return concatTbl(b,"")
end

--------------------------------------------------------------------------
-- This routine formats table-style module commands to match the modulefile format
-- @param name The command name (e.g. "setenv")
-- @param t The table of arguments
function ShowCmdTbl(name, t)
dbg.start{"ShowCmdTbl(",name,", t)"}

local a = {}
a[#a + 1] = s_indentString
a[#a + 1] = name
a[#a + 1] = "{"

-- Handle numeric indices first
for i = 1, #t do
if i > 1 then
a[#a + 1] = ","
end
a[#a + 1] = '"' .. t[i] .. '"'
end

-- Only add mode field if present
if t.mode then
if #t > 0 then
a[#a + 1] = ","
end
a[#a + 1] = 'mode="' .. t.mode .. '"'
end

a[#a + 1] = "}\n"

dbg.fini("ShowCmdTbl")
return concatTbl(a,"")
end

--------------------------------------------------------------------------
-- This routine prints a help message. This is used by MC_Show
function ShowHelpStr(...)
Expand Down

0 comments on commit 976a7b7

Please sign in to comment.