#!/usr/bin/tclsh
#
# MODULECMD.TCL, a pure TCL implementation of the module command
# Copyright (C) 2002-2004 Mark Lakata
# Copyright (C) 2004-2017 Kent Mein
# Copyright (C) 2016-2021 Xavier Delaruelle
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

##########################################################################


# Runtime state properties (default value, proc to call to initialize state
# value?)
array set g_state_defs [list\
   autoinit {0}\
   clock_seconds {<undef> initStateClockSeconds}\
   domainname {<undef> {runCommand domainname}}\
   error_count {0}\
   extra_siteconfig_loaded {0}\
   false_rendered {0}\
   force {0}\
   hiding_threshold {0}\
   inhibit_errreport {0}\
   inhibit_interp {0}\
   init_error_report {0}\
   is_stderr_tty {<undef> initStateIsStderrTty}\
   is_win {<undef> initStateIsWin}\
   kernelversion {<undef> {runCommand uname -v}}\
   lm_info_cached {0}\
   machine [list $::tcl_platform(machine)]\
   nodename {<undef> {runCommand uname -n}}\
   os [list $::tcl_platform(os)]\
   osversion [list $::tcl_platform(osVersion)]\
   paginate {<undef> initStatePaginate}\
   path_separator {<undef> initStatePathSeparator}\
   report_format {regular}\
   reportfd {stderr initStateReportfd}\
   return_false {0}\
   siteconfig_loaded {0}\
   sub1_separator {&}\
   sub2_separator {|}\
   tcl_ext_lib_loaded {0}\
   tcl_version [list [info patchlevel]]\
   tcl_version_lt85 {<undef> initStateTclVersionLt85}\
   term_columns {<undef> initStateTermColumns}\
   usergroups {<undef> initStateUsergroups}\
   username {<undef> initStateUsername}\
]

# Configuration option properties (superseding environment variable, default
# value, is configuration lockable to default value, valid value list?,
# internal value representation?, proc to call to initialize option value,
# valid value list kind
array set g_config_defs [list\
   contact {MODULECONTACT root@localhost 0}\
   auto_handling {MODULES_AUTO_HANDLING 0 0 {0 1}}\
   avail_indepth {MODULES_AVAIL_INDEPTH 1 0 {0 1}}\
   avail_output {MODULES_AVAIL_OUTPUT {modulepath:alias:dirwsym:sym:tag:key} 0 {modulepath alias\
      dirwsym sym tag key} {} {} eltlist}\
   avail_terse_output {MODULES_AVAIL_TERSE_OUTPUT {modulepath:alias:dirwsym:sym:tag} 0\
      {modulepath alias dirwsym sym tag key} {} {} eltlist}\
   collection_pin_version {MODULES_COLLECTION_PIN_VERSION 0 0 {0 1}}\
   collection_target {MODULES_COLLECTION_TARGET <undef> 0}\
   color {MODULES_COLOR never 0 {never auto always} {0 1 2} initConfColor}\
   colors {MODULES_COLORS {} 0 {} {} initConfColors}\
   csh_limit {{} 4000 0}\
   extra_siteconfig {MODULES_SITECONFIG <undef> 1 {}}\
   home {MODULESHOME /usr/share 0}\
   icase {MODULES_ICASE never 0 {never search always}}\
   ignored_dirs {{} {CVS RCS SCCS .svn .git .SYNC .sos} 0}\
   implicit_requirement {MODULES_IMPLICIT_REQUIREMENT 1 0\
      {0 1}}\
   list_output {MODULES_LIST_OUTPUT {header:idx:sym:tag:key} 0 {header idx sym tag\
      key} {} {} eltlist}\
   list_terse_output {MODULES_LIST_TERSE_OUTPUT {header} 0 {header\
      idx sym tag key} {} {} eltlist}\
   locked_configs {{} {} 0}\
   mcookie_version_check {MODULES_MCOOKIE_VERSION_CHECK\
      1 0 {0 1}}\
   ml {MODULES_ML 1 0 {0 1}}\
   nearly_forbidden_days {MODULES_NEARLY_FORBIDDEN_DAYS 14\
      0 {0 365} {} {} intbe}\
   pager {MODULES_PAGER {/usr/bin/less -eFKRX} 0}\
   rcfile {MODULERCFILE <undef> 0}\
   run_quarantine {MODULES_RUN_QUARANTINE <undef> 0}\
   shells_with_ksh_fpath {MODULES_SHELLS_WITH_KSH_FPATH {} 0 {sh bash csh\
      tcsh fish} {} {} eltlist}\
   silent_shell_debug {MODULES_SILENT_SHELL_DEBUG <undef> 0 {0 1}}\
   siteconfig {{} /usr/share/etc/siteconfig.tcl 0}\
   tag_abbrev {MODULES_TAG_ABBREV {auto-loaded=aL:loaded=L:hidden=H:hidden-loaded=H:forbidden=F:nearly-forbidden=nF:sticky=S:super-sticky=sS} 0 {} {} initConfTagAbbrev}\
   tag_color_name {MODULES_TAG_COLOR_NAME {} 0 {} {}\
      initConfTagColorName}\
   tcl_ext_lib {{} {} 0 {} {} initConfTclExtLib}\
   term_background {MODULES_TERM_BACKGROUND dark 0 {dark light}}\
   term_width {MODULES_TERM_WIDTH 0 0 {0 1000} {} {} intbe}\
   unload_match_order {MODULES_UNLOAD_MATCH_ORDER returnlast 0\
      {returnlast returnfirst}}\
   implicit_default {MODULES_IMPLICIT_DEFAULT 1 1 {0 1}}\
   extended_default {MODULES_EXTENDED_DEFAULT 0 0 {0 1}}\
   advanced_version_spec {MODULES_ADVANCED_VERSION_SPEC 0 0 {0\
      1}}\
   search_match {MODULES_SEARCH_MATCH starts_with 0 {starts_with contains}}\
   set_shell_startup {MODULES_SET_SHELL_STARTUP 1 0 {0 1}}\
   verbosity {MODULES_VERBOSITY normal 0 {silent concise normal verbose\
      verbose2 trace debug debug2}}\
   wa_277 {MODULES_WA_277 0 0 {0 1}}\
]

# Get state value
proc getState {state {valifundef {}} {catchinitproc 0}} {
   if {![info exists ::g_states($state)]} {
      # fetch state properties (including its default value) if defined
      if {[info exists ::g_state_defs($state)]} {
         lassign $::g_state_defs($state) value initproclist
      } else {
         set value <undef>
         set initproclist {}
      }

      # call specific proc to initialize state if any
      if {$initproclist ne {}} {
         # catch init procedure error and report it as warning, so default
         # value will get set for state
         if {$catchinitproc} {
            if {[catch {set value [eval $initproclist]} errMsg]} {
               reportWarning $errMsg
            }
         } else {
            set value [eval $initproclist]
         }
      # overriden value coming the command-line
      } elseif {[info exists ::asked_$state]} {
         set value [set ::asked_$state]
      }

      # return passed value if undefined and no value record
      if {$value eq {<undef>}} {
         set value $valifundef
      } else {
         setState $state $value
      }
      return $value
   } else {
      return $::g_states($state)
   }
}

# Clear state
proc unsetState {state} {
   unset ::g_states($state)
   reportDebug "$state unset"
}

# Set state value
proc setState {state value} {
   set ::g_states($state) $value
   reportDebug "$state set to '$value'"
}

# Append each passed value to the existing state value list
proc lappendState {state args} {
   if {$state eq {-nodup}} {
      set state [lindex $args 0]
      # retrieve current val through getState to initialize it if still undef
      set value [getState $state]
      eval appendNoDupToList value [lrange $args 1 end]
      setState $state $value
   } else {
      eval lappend ::g_states($state) $args
      reportDebug "$state appended with '$args'"
   }
}

# Remove last element from state value list
proc lpopState {state} {
   setState $state [lrange [getState $state] 0 end-1]
}

# Return first element from state value list
proc topState {state} {
   return [lindex [getState $state] 0]
}

# Return last element from state value list
proc currentState {state} {
   return [lindex [getState $state] end]
}

# Get number of element from state value list
proc depthState {state} {
   return [llength [getState $state]]
}

# Check if state has been defined
proc isStateDefined {state} {
   return [info exists ::g_states($state)]
}

# Check if state equals passed value
proc isStateEqual {state value} {
   return [expr {[getState $state] eq $value}]
}

proc isConfigLocked {option} {
   return [expr {[lsearch -exact [getConf locked_configs] $option] != -1}]
}

# Get configuration option value
proc getConf {option {valifundef {}}} {
   if {![info exists ::g_configs($option)]} {
      # fetch option properties (including its default value)
      lassign $::g_config_defs($option) envvar value islockable validvallist\
         intvallist initproc validvallistkind

      # ensure option is not locked before superseding its default value
      if {!$islockable || ![isConfigLocked $option]} {
         # call specific proc to initialize config option if any
         if {$initproc ne {}} {
            set value [$initproc $envvar $value $validvallist $intvallist]
         } else {
            # overriden value coming from environment
            if {$envvar ne {} && [info exists ::env($envvar)]} {
               switch -- $validvallistkind {
                  eltlist {
                     # ignore non-valid values
                     if {![isDiffBetweenList [split $::env($envvar) :]\
                        $validvallist]} {
                        set value $::env($envvar)
                     }
                  }
                  intbe {
                     # ignore non-valid values
                     if {[string is integer -strict $::env($envvar)] &&\
                        $::env($envvar) >= [lindex $validvallist 0] &&\
                        $::env($envvar) <= [lindex $validvallist 1]} {
                        set value $::env($envvar)
                     }
                  }
                  {} {
                     # ignore non-valid values
                     if {[switch -- [llength $validvallist] {
                        0 {expr {1 == 1}}
                        1 {string is $validvallist -strict $::env($envvar)}
                        default {isInList $validvallist $::env($envvar)}
                     }]} {
                        set value $::env($envvar)
                     }
                  }
               }
            }

            # overriden value coming the command-line (already validated)
            if {[info exists ::asked_$option]} {
               set value [set ::asked_$option]
            }

            # convert value to its internal representation
            if {[llength $intvallist] > 0} {
               set value [lindex $intvallist [lsearch -exact $validvallist\
                  $value]]
            }
         }
      }

      # return passed value if undefined and no value record
      if {$value eq {<undef>}} {
         set value $valifundef
      } else {
         setConf $option $value
      }
      return $value
   } else {
      return $::g_configs($option)
   }
}

# Set configuration option value
proc setConf {option value} {
   set ::g_configs($option) $value
   reportDebug "$option set to '$value'"
}

# Unset configuration option value if it is set
proc unsetConf {option} {
   if {[info exists ::g_configs($option)]} {
      unset ::g_configs($option)
      reportDebug "$option unset"
   }
}

# Append each passed value to the existing config option value list
proc lappendConf {option args} {
   # retrieve current value through getConf to initialize it if still undef
   set value [getConf $option]
   eval appendNoDupToList value $args
   setConf $option $value
}

# Source site config which can be used to define global procedures or
# settings. We first look for the global siteconfig, then if an extra
# siteconfig is defined and allowed, source that file if it exists
proc sourceSiteConfig {} {
   lappend siteconfiglist [getConf siteconfig]
   for {set i 0} {$i < [llength $siteconfiglist]} {incr i} {
      set siteconfig [lindex $siteconfiglist $i]
      if {[file readable $siteconfig]} {
         reportDebug "Source site configuration ($siteconfig)"
         if {[catch {uplevel 1 source $siteconfig} errMsg]} {
            set errMsg "Site configuration source failed\n"
            # issue line number is lost due to uplevel use
            append errMsg [formatErrStackTrace $::errorInfo $siteconfig {}]
            reportErrorAndExit $errMsg
         }
         if {$siteconfig eq [getConf siteconfig]} {
            setState siteconfig_loaded 1
         } else {
            setState extra_siteconfig_loaded 1
         }
      }
      # check on extra_siteconfig after initial siteconfig loaded in case
      # it inhibits this extra load
      if {$siteconfig eq [getConf siteconfig] && [getConf\
         extra_siteconfig] ne {}} {
         lappend siteconfiglist [getConf extra_siteconfig]
      }
   }
}

# Used to tell if a machine is running Windows or not
proc initStateIsWin {} {
   return [expr {$::tcl_platform(platform) eq {windows}}]
}

# Get default path separator
proc initStatePathSeparator {} {
   return [expr {[getState is_win] ? {;} : {:}}]
}

# Detect if terminal is attached to stderr message channel
proc initStateIsStderrTty {} {
   return [expr {![catch {fconfigure stderr -mode}]}]
}

# Determine if pagination need to be started
proc initStatePaginate {} {
   set pager [getConf pager]
   # empty or 'cat' pager command means no-pager
   set no_cmds [list {} cat]
   # default pager enablement depends on pager command value
   set paginate [notInList $no_cmds [file tail [lindex $pager 0]]]

   # asked enablement could only nullify a previous asked disablement as it
   # requires a valid pager command configuration, which by default enables
   # pagination; some module command may also turn off pager; also if error
   # stream is not attached to a terminal
   if {$paginate && (([info exists ::asked_paginate] && !$::asked_paginate)\
      || [getState subcmd] eq {clear} || ([getState subcmd] eq {ml} &&\
      [lindex [getState subcmd_args] 0] eq {clear}) || ![getState\
      is_stderr_tty])} {
      set paginate 0
   }

   return $paginate
}

# start pager pipe process with defined configuration
proc initStateReportfd {} {
   # get default value
   lassign $::g_state_defs(reportfd) reportfd

   # start pager at first call and only if enabled
   if {[getState paginate]} {
      if {[catch {
         set reportfd [open "|[getConf pager] >@stderr 2>@stderr" w]
         fconfigure $reportfd -buffering line -blocking 1 -buffersize 65536
      } errMsg]} {
         # silently set reportfd to its fallback value to process warn msg
         set ::g_states(reportfd) $reportfd
         reportWarning $errMsg
      }
   }

   # startup content in case of structured output format (puts here rather
   # calling report proc to avoid infinite reportfd init loop
   if {[isStateEqual report_format json]} {
      puts -nonewline $reportfd \{
   }

   return $reportfd
}

# Provide columns number for output formatting
proc initStateTermColumns {} {
   set cols [getConf term_width]
   if {$cols == 0} {
      # determine col number from tty capabilites
      # tty info query depends on running OS
      switch -- $::tcl_platform(os) {
         SunOS {
            catch {regexp {columns = (\d+);} [exec stty] match cols} errMsg
         }
         {Windows NT} {
            catch {regexp {Columns:\s+(\d+)} [exec mode] match cols} errMsg
         }
         default {
            catch {set cols [lindex [exec stty size] 1]} errMsg
         }
      }
      # default size if tty cols cannot be found
      set cols [expr {![info exists cols] || $cols eq {0} ? 80 : $cols}]
   }
   return $cols
}

# Get all groups of user running modulecmd.tcl process
proc __initStateUsergroups {} {
   # ensure groups including space in their name (found on Cygwin/MSYS
   # platforms) are correctly set as list element
   if {[catch {
      return [split [string range [runCommand id -G -n -z] 0 end-1] \0]
   } errMsg]} {
      # fallback if '-z' option is not supported
      return [runCommand id -G -n]
   }
}

# Get name of user running modulecmd.tcl process
proc __initStateUsername {} {
   return [runCommand id -u -n]
}

# Get Epoch time (number of seconds elapsed since Unix epoch)
proc __initStateClockSeconds {} {
   return [clock seconds]
}

# Know if Tcl version is below 8.5.0
proc initStateTclVersionLt85 {} {
   return [expr {[versioncmp [getState tcl_version] 8.5.0] == -1}]
}

# Initialize Select Graphic Rendition table
proc initConfColors {envvar value validvallist intvallist} {
   # overriden value coming from environment
   if {[info exists ::env($envvar)]} {
      set colors_list $::env($envvar)
      if {[catch {
         # test overriden value could be set to a dummy array variable
         array set test_colors [split $colors_list {:=}]
      } errMsg ]} {
         # report issue as a debug message rather warning to avoid
         # disturbing user with a warning message in the middle of a
         # useful output as this table will be initialized at first use
         reportDebug "Ignore invalid value set in $envvar ($colors_list)"
         unset colors_list
      }
   }

   # if no valid override set use default color theme for terminal
   # background color kind (light or dark)
   if {![info exists colors_list]} {
      if {[getConf term_background] eq {light}} {
         set colors_list {hi=1:db=2:tr=2:se=2:er=31:wa=33:me=35:in=34:mp=1;34:di=34:al=36:sy=35:de=4:cm=32:aL=107:L=47:H=2:F=101:nF=103:S=106:sS=104}
      } else {
         set colors_list {hi=1:db=2:tr=2:se=2:er=91:wa=93:me=95:in=94:mp=1;94:di=94:al=96:sy=95:de=4:cm=92:aL=100:L=90;47:H=2:F=41:nF=43:S=46:sS=44}
      }
      if {[catch {
         array set test_colors [split $colors_list {:=}]
      } errMsg ]} {
         reportDebug "Ignore invalid default [getConf term_background]\
            background colors ($colors_list)"
         # define an empty list if no valid value set
         set colors_list {}
      }
   }

   # check each color defined and unset invalid codes
   set value {}
   foreach {elt col} [split $colors_list {:=}] {
      if {![regexp {^[\d;]+$} $col]} {
         reportDebug "Ignore invalid color code for '$elt' ($col)"
      } else {
         lappend value $elt=$col
      }
   }
   set value [join $value :]

   # set SGR table as an array to easily access rendition for each key
   array unset ::g_colors
   array set ::g_colors [split $value {:=}]

   return $value
}

# Initialize color configuration value
proc initConfColor {envvar value validvallist intvallist} {
   # overriden value coming from environment via standard variable
   # https://no-color.org/ and https://bixense.com/clicolors/
   if {[info exists ::env(NO_COLOR)]} {
      set value never
   } elseif {[info exists ::env(CLICOLOR)]} {
      if {$::env(CLICOLOR) eq {0}} {
         set value never
      } else {
         set value auto
      }
   } elseif {[info exists ::env(CLICOLOR_FORCE)] && $::env(CLICOLOR_FORCE)\
      ne {0}} {
      set value always
   }

   # overriden value coming from environment via Modules-specific variable
   if {$envvar ne {} && [info exists ::env($envvar)]} {
      # ignore non-valid values
      if {[llength $validvallist] == 0 || [isInList $validvallist\
         $::env($envvar)]} {
         set value $::env($envvar)
      }
   }

   # overriden value coming the command-line
   if {[info exists ::asked_color]} {
      set value [set ::asked_color]
   }

   # convert value to its internal representation
   if {[llength $intvallist] > 0} {
      set value [lindex $intvallist [lsearch -exact $validvallist $value]]
   }

   # disable color mode if no terminal attached except if 'always' asked
   if {$value != 0 && (![getState is_stderr_tty] || $value == 2)} {
      incr value -1
   }

   # initialize color theme if color mode enabled
   getConf colors

   return $value
}

# Initialize tcl_ext_lib configuration value
proc initConfTclExtLib {envvar value validvallist intvallist} {
   set libfile libtclenvmodules.so

   # determine lib directory
   set libdir /usr/lib
   #switch -- [getState machine] {
   #   x86_64 - aarch64 - ppc64le - s390x {
   #      set libdirmain @libdir64@
   #      set libdiralt @libdir32@
   #   }
   #   default {
   #      set libdirmain @libdir32@
   #      set libdiralt @libdir64@
   #   }
   #}
   ## use alternative arch lib if available and not main one
   #if {![file exists [file join $libdirmain $libfile]] && [file exists [file\
   #   join $libdiralt $libfile]]} {
   #   set libdir $libdiralt
   #} else {
   #   set libdir $libdirmain
   #}

   return [file join $libdir $libfile]
}

# Initialize module tag abbreviation table
proc initConfTagAbbrev {envvar value validvallist intvallist} {
   # overriden value coming from environment
   if {[info exists ::env($envvar)]} {
      if {[catch {
         # try to set the tag-abbreviation mapping table
         array set ::g_tagAbbrev [split $::env($envvar) {:=}]
         set value $::env($envvar)
      } errMsg ]} {
         reportWarning "Ignore invalid value set in $envvar ($::env($envvar))"
         array unset ::g_tagAbbrev
      }
   }

   # test default value
   if {![array exists ::g_tagAbbrev]} {
      if {[catch {
         array set ::g_tagAbbrev [split $value {:=}]
      } errMsg ]} {
         reportWarning "Ignore invalid default value for 'tag_abbrev' config\
            ($value)"
         array unset ::g_tagAbbrev
         # define an empty list if no valid value set
         set value {}
      }
   }

   # build abbrev:tagname array
   foreach {tag abbrev} [array get ::g_tagAbbrev] {
      # skip tags not relevant for current command, that share their
      # abbreviation with another tag
      switch -- $tag {
         hidden-loaded {
            set setabbrevtag [expr {[currentState commandname] eq {list}}]
         }
         hidden {
            set setabbrevtag [expr {[currentState commandname] eq {avail}}]
         }
         default {
            set setabbrevtag 1
         }
      }
      if {$setabbrevtag} {
         set ::g_abbrevTag($abbrev) $tag
      }
   }

   return $value
}

# Initialize module tag color name table
proc initConfTagColorName {envvar value validvallist intvallist} {
   # overriden value coming from environment
   if {[info exists ::env($envvar)]} {
      set value $::env($envvar)
   }

   # set table for efficient search
   foreach tag [split $value :] {
      set ::g_tagColorName($tag) 1
   }

   return $value
}

# Is currently set verbosity level is equal or higher than level passed as arg
proc isVerbosityLevel {name} {
   return [expr {[lsearch -exact [lindex $::g_config_defs(verbosity) 3]\
      [getConf verbosity]] >= [lsearch -exact [lindex\
      $::g_config_defs(verbosity) 3] $name]}]
}

# Is match performed in a case sensitive or insensitive manner
proc isIcase {} {
   # depending on current sub-command, list values that equal to a case
   # insensitive match enablement
   lappend enabledValList always
   if {[isInList [list avail whatis search paths] [currentState\
      commandname]]} {
      lappend enabledValList search
   }
   return [isInList $enabledValList [getConf icase]]
}


proc raiseErrorCount {} {
   setState error_count [expr {[getState error_count] + 1}]
}

proc renderFalse {} {
   if {[isStateDefined false_rendered]} {
      reportDebug {false already rendered}
   } elseif {[isStateDefined shelltype]} {
      # setup flag to render only once
      setState false_rendered 1

      # render a false value most of the time through a variable assignement
      # that will be looked at in the shell module function calling
      # modulecmd.tcl to return in turns a boolean status. Except for python
      # and cmake, the value assigned to variable is also returned as the
      # entire rendering status
      switch -- [getState shelltype] {
         sh - csh - fish {
            # no need to set a variable on real shells as last statement
            # result can easily be checked
            puts stdout {test 0 = 1;}
         }
         tcl {
            puts stdout {set _mlstatus 0;}
         }
         cmd {
            puts stdout {set errorlevel=1}
         }
         perl {
            puts stdout {{ no strict 'vars'; $_mlstatus = 0; }}
         }
         python {
            puts stdout {_mlstatus = False}
         }
         ruby {
            puts stdout {_mlstatus = false}
         }
         lisp {
            puts stdout {nil}
         }
         cmake {
            puts stdout {set(_mlstatus FALSE)}
         }
         r {
            puts stdout {mlstatus <- FALSE}
         }
      }
   }
}

proc renderTrue {} {
   reportDebug called.

   # render a true value most of the time through a variable assignement that
   # will be looked at in the shell module function calling modulecmd.tcl to
   # return in turns a boolean status. Except for python and cmake, the
   # value assigned to variable is also returned as the full rendering status
   switch -- [getState shelltype] {
      sh - csh - fish {
         # no need to set a variable on real shells as last statement
         # result can easily be checked
         puts stdout {test 0;}
      }
      tcl {
         puts stdout {set _mlstatus 1;}
      }
      cmd {
         puts stdout {set errorlevel=0}
      }
      perl {
         puts stdout {{ no strict 'vars'; $_mlstatus = 1; }}
      }
      python {
         puts stdout {_mlstatus = True}
      }
      ruby {
         puts stdout {_mlstatus = true}
      }
      lisp {
         puts stdout {t}
      }
      cmake {
         puts stdout {set(_mlstatus TRUE)}
      }
      r {
         puts stdout {mlstatus <- TRUE}
      }
   }
}

proc renderText {text} {
   reportDebug "called ($text)."

   # render a text value most of the time through a variable assignement that
   # will be looked at in the shell module function calling modulecmd.tcl to
   # return in turns a string value.
   switch -- [getState shelltype] {
      sh - csh - fish {
         foreach word $text {
            # no need to set a variable on real shells, echoing text will make
            # it available as result
            puts stdout "echo '$word';"
         }
      }
      tcl {
         puts stdout "set _mlstatus \"$text\";"
      }
      cmd {
         foreach word $text {
            puts stdout "echo $word"
         }
      }
      perl {
         puts stdout "{ no strict 'vars'; \$_mlstatus = '$text'; }"
      }
      python {
         puts stdout "_mlstatus = '$text'"
      }
      ruby {
         puts stdout "_mlstatus = '$text'"
      }
      lisp {
         puts stdout "(message \"$text\")"
      }
      cmake {
         puts stdout "set(_mlstatus \"$text\")"
      }
      r {
         puts stdout "mlstatus <- '$text'"
      }
   }
}

#
# Debug, Info, Warnings and Error message handling.
#

# save message when report is not currently initialized as we do not
# know yet if debug mode is enabled or not
proc reportDebug {message {showcaller 1} {caller _undef_}} {
   # get caller name
   if {$caller eq {_undef_} && $showcaller} {
      if {[info level] > 1} {
         set caller [lindex [info level -1] 0]
      } else {
         set caller {}
      }
   }
   lappend ::errreport_buffer [list reportDebug $message $showcaller $caller]
}

# regular procedure to use once error report is initialized
proc __reportDebug {message {showcaller 1} {caller _undef_}} {
   # display active interp details if not the main one
   set prefix [currentState debug_msg_prefix]
   # display caller name as prefix
   if {$showcaller && $caller ne {} && ($caller ne {_undef_} || [info level]\
      > 1)} {
      if {$caller eq {_undef_}} {
         set caller [lindex [info level -1] 0]
      }
      append prefix "$caller: "
   }
   report [sgr db "DEBUG $prefix$message"] 0 1
}

# alternative procedure used when debug is disabled
proc __reportDebugNop {args} {}

proc reportWarning {message {recordtop 0}} {
   reportError $message $recordtop WARNING wa 0
}

proc reportError {message {recordtop 0} {severity ERROR} {sgrkey er}\
   {raisecnt 1}} {
   lappend ::errreport_buffer [list reportError $message $recordtop $severity\
      $sgrkey $raisecnt]
}

proc __reportError {message {recordtop 0} {severity ERROR} {sgrkey er}\
   {raisecnt 1}} {
   # if report disabled, also disable error raise to get a coherent
   # behavior (if no message printed, no error code change)
   if {![getState inhibit_errreport]} {
      if {$raisecnt} {
         raiseErrorCount
      }
      set msgsgr "[sgr $sgrkey $severity]: $message"
      # record message to report it later on if a record id is found
      if {[currentState msgrecordid] ne {}} {
         recordMessage $msgsgr $recordtop
      # skip message report if silent
      } elseif {[isVerbosityLevel concise]} {
         # save error messages to render them all together in JSON format
         if {[isStateEqual report_format json]} {
            lappend ::g_report_erralist $severity $message
         } else {
            report $msgsgr 0 0 1
         }
      }
   }
}

# throw known error (call error with 'known error' code)
proc knerror {message {code MODULES_ERR_KNOWN}} {
   error $message {} $code
}

# save message if report is not yet initialized
proc reportErrorAndExit {message} {
   lappend ::errreport_buffer [list reportErrorAndExit $message]
}

# regular procedure to use once error report is initialized
proc __reportErrorAndExit {message} {
   raiseErrorCount
   renderFalse
   error $message {} MODULES_ERR_RENDERED
}

proc reportInternalBug {message {modfile {}}} {
   if {$modfile ne {}} {
      append message "\nIn '$modfile'"
   }
   append message "\nPlease contact <[getConf contact]>"
   reportError $message 0 {Module ERROR} me
}

proc reportInfo {message {title INFO}} {
   if {[isVerbosityLevel normal]} {
      # use reportError for conveniance but there is no error here
      reportError $message 0 $title in 0
   }
}

proc reportTrace {message {title TRACE}} {
   if {[isVerbosityLevel trace]} {
      # use reportError for conveniance but there is no error here
      reportError [sgr tr $message] 0 $title tr 0
   }
}

# trace procedure execution start
proc reportTraceExecEnter {cmdstring op} {
   set caller [expr {[info level] > 1 ? [lindex [info level -1] 0] : {}}]
   reportDebug $cmdstring 1 $caller
}

# is currently active message record id at top level
proc isMsgRecordIdTop {} {
   return [expr {[depthState msgrecordid] eq 1}]
}

# record messages on the eventual additional module evaluations that have
# occurred during the current evaluation
proc reportModuleEval {} {
   set evalid [currentState evalid]
   array set contexttitle {conun {Unloading conflict} reqlo {Loading\
      requirement} depre {Reloading dependent} depun {Unloading dependent}\
      urequn {Unloading useless requirement}}

   if {[info exists ::g_moduleEval($evalid)]} {
      foreach contextevallist $::g_moduleEval($evalid) {
         set modlist [lassign $contextevallist context]
         # skip context with no description title
         if {[info exists contexttitle($context)]} {
            # exclude hidden modules from report unless an high level of
            # verbosity is set
            if {[info exists ::g_moduleHiddenEval($evalid:$context)] &&\
               ![isVerbosityLevel verbose2]} {
               lassign [getDiffBetweenList $modlist\
                  $::g_moduleHiddenEval($evalid:$context)] modlist
            }

            if {[llength $modlist] > 0} {
               reportInfo [join $modlist] $contexttitle($context)
            }
         }
      }
      # purge list in case same evaluation is re-done afterward
      unset ::g_moduleEval($evalid)
   }
}

# render messages related to current record id under an header block
proc reportMsgRecord {header {hidden 0}} {
   set recid [currentState msgrecordid]
   if {[info exists ::g_msgRecord($recid)]} {
      # skip message report if silent (report even if hidden as soon as msgs
      # are associated to hidden module evaluation)
      if {[isVerbosityLevel concise]} {
         set tty_cols [getState term_columns]
         set padding {  }

         set dispmsg $header
         foreach msg $::g_msgRecord($recid) {
            # split lines if too large for terminal
            set first 1
            set max_idx [expr {$tty_cols - [string length $padding]}]
            # max expr function is not supported in Tcl8.4 and earlier
            if {$max_idx < 1} {set max_idx 1}
            set linelist [list]
            foreach line [split $msg \n] {
               set lineadd {}
               while {$lineadd ne $line} {
                  set line_max_idx $max_idx
                  # sgr tags consume no length
                  set eidx 0
                  while {[set sidx [string first "\033\[" $line $eidx]] !=\
                     -1} {
                     set eidx [string first m $line $sidx]
                     incr line_max_idx [expr {1 + $eidx - $sidx}]
                  }

                  # no split if no whitespace found to slice
                  if {[string length $line] > $line_max_idx && [set cut_idx\
                     [string last { } $line $line_max_idx]] != -1} {
                     set lineadd [string range $line 0 [expr {$cut_idx-1}]]
                     set line [string range $line [expr {$cut_idx+1}] end]
                  } else {
                     set lineadd $line
                  }
                  # skip empty line
                  if {[string trim $lineadd] ne {}} {
                     lappend linelist $lineadd
                  }
                  if {$first} {
                     set first 0
                     incr max_idx -[string length $padding]
                     if {$max_idx < 1} {set max_idx 1}
                  }
               }
            }

            # display each line
            set first 1
            foreach line $linelist {
               append dispmsg \n
               if {$first} {
                  set first 0
               } else {
                  append dispmsg $padding
               }
               append dispmsg $padding$line
            }
         }
         reportSeparateNextContent
         report $dispmsg
         reportSeparateNextContent
      }

      # purge message list in case same evaluation is re-done afterward
      unset ::g_msgRecord($recid)
   # report header if no other specific msg to output in verbose mode or in
   # normal verbosity mode if currently processing a cmd which triggers
   # multiple module evaluations that cannot be guessed by the user
   # if hidden flag is enabled report only if verbosity >= verbose2
   } elseif {(!$hidden && ([isVerbosityLevel verbose] || ([isVerbosityLevel\
      normal] && ([ongoingCommandName restore] || [ongoingCommandName\
      source])))) || ($hidden && [isVerbosityLevel verbose2])} {
      report $header
   }
}

# separate next content produced if any
proc reportSeparateNextContent {} {
   lappend ::errreport_buffer [list reportSeparateNextContent]
}

# regular procedure to use once error report is initialized
proc __reportSeparateNextContent {} {
   # hold or apply
   if {[depthState reportholdid] > 0} {
      lappend ::g_holdReport([currentState reportholdid]) [list\
         reportSeparateNextContent]
   } else {
      setState report_sep_next 1
   }
}

# save message for block rendering
proc recordMessage {message {recordtop 0}} {
   lappend ::g_msgRecord([expr {$recordtop ? [topState msgrecordid] :\
      [currentState msgrecordid]}]) $message
}

# check if some msg have been recorded for current evaluation
proc isMsgRecorded {} {
   return [info exists ::g_msgRecord([currentState msgrecordid])]
}

# filter and format error stack trace to only report useful content
proc formatErrStackTrace {errmsg loc {cmdlist {}}} {
   set headstr "\n    while executing\n"
   set splitstr "\n    invoked from within\n"
   set splitstrlen [string length $splitstr]
   set aftheadidx [string first $headstr $errmsg]
   if {$aftheadidx != -1} {
      incr aftheadidx [string length $headstr]
   }

   # get name of invalid command name to maintain it in error stack trace
   if {[string equal -length 22 {invalid command name "} $errmsg]} {
      set unkcmd [lindex [split [string range $errmsg 0 $aftheadidx] {"}] 1]
   } else {
      set unkcmd {}
   }

   # get list of modulecmd.tcl internal procedure to filter out from stack
   # skip this when no interp command list is provided
   if {[llength $cmdlist] > 0} {
      lassign [getDiffBetweenList [concat [info commands] [info procs]]\
         $cmdlist] filtercmdlist keepcmdlist
   } else {
      set filtercmdlist {}
   }

   # define commands to filter out from bottom of stack
   set filtercmdendlist [list {eval $modcontent} "source $loc" {uplevel 1\
      source $siteconfig}]

   # filter out modulecmd internal references at beginning of stack
   set internals 1
   while {$internals && $aftheadidx != -1} {
      # fetch erroneous command and its caller
      set stackelt [string range $errmsg $aftheadidx [string first\
         $splitstr $errmsg $aftheadidx]]
      lassign [split [lindex [split $stackelt {"}] 1]] cmd1 cmd2
      set cmdcaller [lindex [split [string range $stackelt [string last\
         {(procedure } $stackelt] end] {"}] 1]
      if {$cmd1 eq {eval}} {
         set cmd1 $cmd2
      }

      # filter out stack element refering to or called by an unknown procedure
      # (ie. a modulecmd.tcl internal procedure)
      if {[string index $cmd1 0] ne {$} && $cmd1 ne $unkcmd && ([isInList\
         $filtercmdlist $cmdcaller] || [isInList $filtercmdlist $cmd1])} {
         set errmsg [string replace $errmsg $aftheadidx [expr {[string first\
            $splitstr $errmsg] + $splitstrlen - 1}]]
      } else {
         set internals 0
      }
   }

   # filter out modulecmd internal references at end of stack
   set internals 1
   while {$internals} {
      set beffootidx [string last $splitstr $errmsg]
      set stackelt [string range $errmsg $beffootidx end]
      set cmd [lindex [split $stackelt {"}] 1]

      if {[isInList $filtercmdendlist $cmd]} {
         set errmsg [string replace $errmsg $beffootidx end]
      } else {
         set internals 0
      }
   }

   # replace error location at end of stack
   set lastnl [string last \n $errmsg]
   set lastline [string range $errmsg [expr {$lastnl + 1}] end]
   if {[string match {    ("eval" body line*} $lastline]} {
      set errmsg [string replace $errmsg $lastnl [expr {$lastnl + [string\
         length "    (\"eval\" body line"]}] "\n    (file \"$loc\" line"]
   } elseif {![string match {    (file *} $lastline]} {
      # add error location at end of stack
      append errmsg "\n    (file \"$loc\")"
   }

   return $errmsg
}

# Test if color is enabled and passed sgrkey is defined and not null
proc isSgrkeyColored {sgrkey} {
   return [expr {[getConf color] && [info exists ::g_colors($sgrkey)] &&\
      $::g_colors($sgrkey) ne {}}]
}

# Select Graphic Rendition of a string with passed sgr keys (if color enabled)
proc sgr {keylist str {himatchmap {}} {othkeylist {}}} {
   if {[getConf color]} {
      set sgrreset 22
      foreach sgrkey $keylist {
         if {[info exists ::g_colors($sgrkey)]} {
            # track color key that have been used
            if {![info exists ::g_used_colors($sgrkey)]} {
               set ::g_used_colors($sgrkey) 1
            }
            if {[info exists sgrset]} {
               append sgrset {;}
            }
            append sgrset $::g_colors($sgrkey)
            # if render bold or faint just reset that attribute, not all
            if {$sgrreset != 0 && $sgrset != 1 && $sgrset != 2} {
               set sgrreset 0
            }
         }
      }
      if {[llength $othkeylist] == 0} {
         # hightlight matching substring
         if {[llength $himatchmap] > 0} {
            set str [string map $himatchmap $str]
         }
         if {[info exists sgrset]} {
            set str "\033\[${sgrset}m$str\033\[${sgrreset}m"
         }
      } else {
         if {![info exists sgrset]} {
            set sgrset {}
         } else {
            append sgrset {;}
         }
         # determine each chunk where the other sgr keys apply
         set tagsgrlen [expr {int(ceil([string length $str]/[llength\
            $othkeylist]))}]
         for {set i 0} {$i < [llength $othkeylist]} {incr i} {
            set idx [expr {$i*$tagsgrlen}]
            set sgrkey [lindex $othkeylist $i]
            lappend sgridxlist $idx $::g_colors($sgrkey)
            # track color key that have been used
            if {![info exists ::g_used_colors($sgrkey)]} {
               set ::g_used_colors($sgrkey) 1
            }
         }
         # determine each chunk where the highlight applies
         set hiidxlist {}
         foreach {mstr sgrmstr} $himatchmap {
            set idx 0
            while {$idx != -1} {
               if {[set idx [string first $mstr $str $idx]] != -1} {
                  lappend hiidxlist $idx
                  incr idx [string length $mstr]
                  # add highlight end index unless if end of string
                  if {$idx < [string length $str]} {
                     lappend hiidxlist $idx
                  }
               }
            }
            # no need to look at next match string if this one was found
            if {[llength $hiidxlist] > 0} {
               break
            }
         }

         # mix other sgr chunks with highlighted chunks to define sgr codes
         set i 0
         set j 0
         set sgridx [lindex $sgridxlist 0]
         set hiidx [lindex $hiidxlist 0]
         set hicur 0
         set sgrcur {}
         set sgrrst {0;}
         while {$i < [llength $sgridxlist] || $j < [llength $hiidxlist]} {
            set sgrcode {}
            set cursgridx $sgridx
            # sgr chunk change
            if {$sgridx ne {} && ($hiidx eq {} || $sgridx <= $hiidx)} {
               incr i
               set sgrcur $sgrset[lindex $sgridxlist $i]
               set idx $sgridx
               if {$idx != 0} {
                  append sgrcode $sgrrst
               }
               append sgrcode $sgrcur
               incr i
               set sgridx [lindex $sgridxlist $i]
            }
            # highlight change
            if {$hiidx ne {} && ($cursgridx eq {} || $hiidx <= $cursgridx)} {
               set idx $hiidx
               set hicur [expr {!$hicur}]
               if {$hicur} {
                  if {$sgrcode ne {}} {
                     append sgrcode {;}
                  }
                  append sgrcode $::g_colors(hi)
               # restore current sgr set to only clear highlight
               } elseif {$sgrcode eq {}} {
                  append sgrcode $sgrrst $sgrcur
               }
               incr j
               set hiidx [lindex $hiidxlist $j]
            } elseif {$hicur} {
               append sgrcode {;} $::g_colors(hi)
            }
            lappend fullsgridxlist $idx $sgrcode
         }
         # reset sgr at end of string
         lappend fullsgridxlist [string length $str] 0

         # apply defined sgr codes to the string
         set stridx 0
         foreach {sgridx sgrcode} $fullsgridxlist {
            if {$sgridx != 0} {
               append sgrstr [string range $str $stridx [expr {$sgridx-1}]]
            }
            append sgrstr "\033\[${sgrcode}m"
            set stridx $sgridx
         }
         set str $sgrstr
      }
   }
   return $str
}

# Sort tags to return those matching defined sgr keys in a list up to a given
# maxnb number and if tag not set to be displayed by its name. Other elements
# are returned in a separate list
proc getTagSgrForModname {keylist maxnb} {
   set sgrkeylist {}
   if {[getConf color]} {
      set otherlist {}
      foreach key $keylist {
         if {[info exists ::g_colors($key)] && ![info exists\
            ::g_tagColorName($key)] && [llength $sgrkeylist] < $maxnb} {
            lappend sgrkeylist $key
         } else {
            lappend otherlist $key
         }
      }
   } else {
      set otherlist $keylist
   }
   return [list $sgrkeylist $otherlist]
}

# save message if report is not yet initialized
proc report {message {nonewline 0} {immed 0} {padnl 0}} {
   lappend ::errreport_buffer [list report $message $nonewline $immed $padnl]
}

# regular procedure to use once error report is initialized
proc __report {message {nonewline 0} {immed 0} {padnl 0}} {
   # hold or print output
   if {!$immed && [depthState reportholdid] > 0} {
      lappend ::g_holdReport([currentState reportholdid]) [list report\
         $message $nonewline $immed $padnl]
   } else {
      # produce blank line prior message if asked to
      if {[isStateDefined reportfd] && [isStateDefined report_sep_next]} {
         unsetState report_sep_next
         report [expr {[isStateEqual report_format json] ? {,} : {}}]
      }
      # prefix msg lines after first one with 2 spaces
      if {$padnl} {
         set first 1
         foreach line [split $message \n] {
            if {$first} {
               set first 0
            } else {
               append padmsg "\n  "
            }
            append padmsg $line
         }
         set message $padmsg
      }

      # protect from issue with fd, just ignore it
      catch {
         if {$nonewline} {
            puts -nonewline [getState reportfd] $message
         } else {
            puts [getState reportfd] $message
         }
      }
   }
}

# report error the correct way depending of its type
proc reportIssue {issuetype issuemsg {issuefile {}}} {
   switch -- $issuetype {
      invalid {
         reportInternalBug $issuemsg $issuefile
      }
      default {
         reportError $issuemsg
      }
   }
}

# report defined command (used in display evaluation mode)
proc reportCmd {cmd args} {
   # use Tcl native string representation of list
   if {$cmd eq {-nativeargrep}} {
      set cmd [lindex $args 0]
      set cmdargs [lrange $args 1 end]
   } else {
      set cmdargs [listTo tcl $args 0]
   }
   set extratab [expr {[string length $cmd] < 8 ? "\t" : {}}]
   report [sgr cm $cmd]$extratab\t$cmdargs

   # empty string returns if command result is another command input
   return {}
}

# report defined command (called as an execution trace)
proc reportCmdTrace {cmdstring args} {
   eval reportCmd $cmdstring
}

proc reportVersion {} {
   report {Modules Release 4.7.0\
      (2021-02-19)}
}

# disable error reporting (non-critical report only) unless debug enabled
proc inhibitErrorReport {} {
   if {![isVerbosityLevel trace]} {
      setState inhibit_errreport 1
   }
}

# init error report and output buffered messages
proc initErrorReport {} {
   # ensure init is done only once
   if {![isStateDefined init_error_report]} {
      setState init_error_report 1

      # ask for color init now as debug mode has already fire lines to render
      # and we want them to be reported first (not the color init lines)
      if {[isVerbosityLevel debug]} {
         getConf color
      }

      # trigger pager start if something needs to be printed, to guaranty
      # reportDebug calls during pager start are processed in buffer mode
      if {[isVerbosityLevel debug]} {
         getState reportfd
      }

      # replace report procedures used to bufferize messages until error
      # report being initialized by regular report procedures
      rename ::reportDebug {}
      if {[isVerbosityLevel debug]} {
         rename ::__reportDebug ::reportDebug
      } else {
         # set a disabled version if debug is disabled
         rename ::__reportDebugNop ::reportDebug
      }
      rename ::reportError {}
      rename ::__reportError ::reportError
      rename ::reportErrorAndExit {}
      rename ::__reportErrorAndExit ::reportErrorAndExit
      rename ::reportSeparateNextContent {}
      rename ::__reportSeparateNextContent ::reportSeparateNextContent
      rename ::report {}
      rename ::__report ::report

      # trace each procedure call
      if {[isVerbosityLevel debug2]} {
         # exclude core procedure from tracing
         set excl_prc_list [list report reportDebug reportTraceExecEnter\
            isInList notInList getState setState unsetState lappendState\
            lpopState currentState depthState isStateDefined isStateEqual sgr\
            getConf setConf unsetConf lappendConf]
         foreach prc [info procs] {
            if {[notInList $excl_prc_list $prc]} {
               trace add execution $prc enter reportTraceExecEnter
            }
         }
      }

      # now error report is init output every message saved in buffer; first
      # message will trigger message paging configuration and startup unless
      # already done if debug mode enabled
      foreach errreport $::errreport_buffer {
         eval $errreport
      }
   }
}

# drop or report held messages
proc releaseHeldReport {args} {
   foreach {holdid action} $args {
      if {[info exists ::g_holdReport($holdid)]} {
         if {$action eq {report}} {
            foreach repcall $::g_holdReport($holdid) {
               eval $repcall
            }
         }
         unset ::g_holdReport($holdid)
      }
   }
}

# check if element passed as argument (corresponding to a kind of information)
# should be part of output content
proc isEltInReport {elt} {
   # get config name relative to current sub-command and output format
   set conf [currentState commandname]
   if {[getState report_format] ne {regular}} {
      append conf _[getState report_format]
   }
   append conf _output
   set arrname ::g_$conf

   if {[info exists ::g_config_defs($conf)]} {
      # build value cache if it does not exist yet
      if {![array exists $arrname]} {
         array set $arrname {}
         foreach confelt [split [getConf $conf] :] {
            set ${arrname}($confelt) 1
         }
      }

      # check if elt is marked to be included in output
      set ret [info exists ${arrname}($elt)]
   } else {
      # always return ok in case no config option corresponds to the current
      # module sub-command and output format
      set ret 1
   }

   return $ret
}

# exit in a clean manner by closing interaction with external components
proc cleanupAndExit {code} {
   # finish output document if json format enabled
   if {[isStateEqual report_format json]} {
      # render error messages all together
      if {[info exists ::g_report_erralist]} {
         # ignite report first to get eventual error message from report
         # initialization in order 'foreach' got all messages prior firing
         report "\"errors\": \[" 1
         foreach {sev msg} $::g_report_erralist {
            # split message in lines
            lappend dispmsglist "\n{ \"severity\": \"$sev\", \"message\": \[\
               \"[join [split [charEscaped $msg \"] \n] {", "}]\" \] }"
         }
         report "[join $dispmsglist ,] \]"
      }
      # inhibit next content separator if output is ending
      if {[isStateDefined report_sep_next]} {
         unsetState report_sep_next
      }
      report \}
   }

   # close pager if enabled
   if {[isStateDefined reportfd] && ![isStateEqual reportfd stderr]} {
      catch {flush [getState reportfd]}
      catch {close [getState reportfd]}
   }

   exit $code
}

# helper procedures to format various messages
proc getHintUnFirstMsg {modlist} {
   return "HINT: Might try \"module unload [join $modlist]\" first."
}

proc getHintLoFirstMsg {modlist} {
   if {[llength $modlist] > 1} {
      set oneof {at least one of }
      set mod modules
   } else {
      set oneof {}
      set mod module
   }
   return "HINT: ${oneof}the following $mod must be loaded first: $modlist"
}

proc getErrConflictMsg {mod conlist} {
   return "$mod cannot be loaded due to a conflict.\n[getHintUnFirstMsg\
      $conlist]"
}

proc getErrPrereqMsg {mod prelist {load 1}} {
   lassign [if {$load} {list {} missing [getHintLoFirstMsg $prelist]}\
      {list un a [getHintUnFirstMsg $prelist]}] un mis hintmsg
   return "$mod cannot be ${un}loaded due to $mis prereq.\n$hintmsg"
}

proc getErrReqLoMsg {prelist} {
   return "Load of requirement '[join $prelist {' or '}]' failed"
}

proc getReqNotLoadedMsg {prelist} {
   return "Requirement '[join $prelist {' or '}]' is not loaded"
}

proc getDepLoadedMsg {prelist} {
   set is [expr {[llength $prelist] > 1 ? {are} : {is}}]
   return "Dependent '[join $prelist {' and '}]' $is loaded"
}

proc getErrConUnMsg {conlist} {
   return "Unload of conflicting '[join $conlist {' and '}]' failed"
}

proc getConIsLoadedMsg {conlist {loading 0}} {
   set is [expr {[llength $conlist] > 1 ? {are} : {is}}]
   set loaded [expr {$loading ? {loading} : {loaded}}]
   return "Conflicting '[join $conlist {' and '}]' $is $loaded"
}

proc getForbiddenMsg {mod} {
   set msg "Access to module '$mod' is denied"
   set extramsg [getModuleTagProp $mod forbidden message]
   if {$extramsg ne {}} {
      append msg \n$extramsg
   }
   return $msg
}

proc getNearlyForbiddenMsg {mod} {
   set after [getModuleTagProp $mod nearly-forbidden after]
   set msg "Access to module will be denied starting '$after'"
   set extramsg [getModuleTagProp $mod nearly-forbidden message]
   if {$extramsg ne {}} {
      append msg \n$extramsg
   }
   return $msg
}

proc getStickyUnloadMsg {mod} {
   return "Unload of sticky module '$mod' skipped"
}

proc getSuperStickyUnloadMsg {mod} {
   return "Unload of super-sticky module '$mod' skipped"
}

proc getStickyForcedUnloadMsg {mod} {
   return "Unload of sticky module '$mod' forced"
}

########################################################################
# Use a subordinate Tcl interpreter to execute modulefiles
#

# dummy proc to disable modulefile commands on some evaluation modes
proc nop {args} {}

# dummy proc for commands available on other Modules flavor but not here
proc nimp {cmd args} {
   reportWarning "'$cmd' command not implemented"
}

proc get-env {var {valifunset {}}} {
   # return current value if exists and not cleared
   if {[info exists ::env($var)] && ![info exists ::g_clearedEnvVars($var)]} {
      return $::env($var)
   } else {
      return $valifunset
   }
}

proc set-env {var val} {
   set mode [currentState mode]
   reportDebug "$var=$val"

   # an empty string value means unsetting variable on Windows platform, so
   # call unset-env to ensure variable will not be seen defined yet raising
   # an error when trying to access it
   if {[getState is_win] && $val eq {}} {
      unset-env $var
   } else {
      interp-sync-env set $var $val

      # variable is not cleared anymore if set again
      if {[info exists ::g_clearedEnvVars($var)]} {
         unset ::g_clearedEnvVars($var)
      }

      # propagate variable setup to shell environment on load and unload mode
      if {$mode eq {load} || $mode eq {unload}} {
         set ::g_stateEnvVars($var) new
      }
   }
}

proc reset-to-unset-env {var {val {}}} {
   interp-sync-env set $var $val
   # set var as cleared if val is empty
   if {$val eq {}} {
      set ::g_clearedEnvVars($var) 1
   }
}

proc unset-env {var {internal 0} {val {}}} {
   set mode [currentState mode]
   reportDebug "$var (internal=$internal, val=$val)"

   # clear value instead of unset it not to break variable later reference
   # in modulefile. clear whether variable set or not to get a later usage
   # consistent behavior whatever env is setup
   if {!$internal} {
      reset-to-unset-env $var $val
   # internal variables (like ref counter var) are purely unset if they exists
   } elseif {[info exists ::env($var)]} {
      interp-sync-env unset $var
      set intwasset 1
   }

   # propagate deletion in any case if variable is public and for internal
   # one only if variable was set
   if {($mode eq {load} || $mode eq {unload}) && (!$internal ||\
      [info exists intwasset])} {
      set ::g_stateEnvVars($var) del
   }
}

# synchronize environment variable change over all started sub interpreters
proc interp-sync-env {op var {val {}}} {
   set envvar ::env($var)

   # apply operation to main interpreter
   switch -- $op {
      set { set $envvar $val }
      unset { unset $envvar }
   }

   # apply operation to each sub-interpreters if not found autosynced
   if {[llength [interp slaves]] > 0} {
      reportDebug "$op var='$envvar', val='$val' on interp(s) [interp slaves]"

      foreach itrp [interp slaves] {
         switch -- $op {
            set {
               # no value pre-check on Windows platform as an empty value set
               # means unsetting variable which lead querying value to error
               if {[getState is_win] || ![interp eval $itrp [list info exists\
                  $envvar]] || [interp eval $itrp [list set $envvar]] ne\
                  $val} {
                  interp eval $itrp [list set $envvar $val]
               }
            }
            unset {
               if {[interp eval $itrp [list info exists $envvar]]} {
                  interp eval $itrp [list unset $envvar]
               }
            }
         }
      }
   }
}

# Initialize list of interp alias commands to define for given evaluation mode
proc initModfileModeAliases {mode aliasesVN aliasesPassArgVN tracesVN} {
   global g_modfilePerModeAliases
   upvar #0 $aliasesVN aliases
   upvar #0 $aliasesPassArgVN aliasesPassArg
   upvar #0 $tracesVN traces

   if {![info exists g_modfilePerModeAliases]} {
      set ::g_modfileBaseAliases [list versioncmp versioncmp getenv getenv\
         is-loaded is-loaded is-saved is-saved is-used is-used is-avail\
         is-avail uname uname module-info module-info exit exitModfileCmd\
         reportCmdTrace reportCmdTrace reportInternalBug reportInternalBug\
         reportWarning reportWarning reportError reportError raiseErrorCount\
         raiseErrorCount report report isVerbosityLevel isVerbosityLevel\
         isWin initStateIsWin puts putsModfileCmd readModuleContent\
         readModuleContent formatErrStackTrace formatErrStackTrace]

      # list of alias commands whose target procedure is adapted according to
      # the evaluation mode
      set ::g_modfileEvalModes {load unload display help test whatis}
      array set g_modfilePerModeAliases {
append-path    {append-path    remove-path    append-path    append-path    append-path    edit-path-wh  }
chdir          {chdir          nop            reportCmd      nop            nop            nop           }
conflict       {conflict       nop            reportCmd      nop            nop            nop           }
module         {module         module         reportCmd      nop            nop            nop           }
module-alias   {module-alias   module-alias   module-alias   module-alias   module-alias   module-alias  }
module-log     {nimp           nimp           reportCmd      nop            nop            nop           }
module-trace   {nimp           nimp           reportCmd      nop            nop            nop           }
module-user    {nimp           nimp           reportCmd      nop            nop            nop           }
module-verbosity {nimp         nimp           reportCmd      nop            nop            nop           }
module-version {module-version module-version module-version module-version module-version module-version}
module-virtual {module-virtual module-virtual module-virtual module-virtual module-virtual module-virtual}
module-forbid  {module-forbid  module-forbid  module-forbid  module-forbid  module-forbid  module-forbid }
module-hide    {module-hide    module-hide    module-hide    module-hide    module-hide    module-hide   }
module-tag     {module-tag     module-tag     module-tag     module-tag     module-tag     module-tag    }
module-whatis  {nop            nop            reportCmd      nop            nop            module-whatis }
prepend-path   {prepend-path   remove-path    prepend-path   prepend-path   prepend-path   edit-path-wh  }
prereq         {prereq         nop            reportCmd      nop            nop            nop           }
remove-path    {remove-path    remove-path-un remove-path    remove-path    remove-path    edit-path-wh  }
set-alias      {set-alias      set-alias-un   reportCmd      nop            nop            nop           }
set-function   {set-function   set-function-un reportCmd     nop            nop            nop           }
setenv         {setenv         setenv-un      setenv         setenv         setenv         setenv-wh     }
source-sh      {source-sh      source-sh-un   source-sh-di   nop            nop            nop           }
system         {system         system         reportCmd      nop            nop            nop           }
unset-alias    {unset-alias    nop            reportCmd      nop            nop            nop           }
unset-function {unset-function nop            reportCmd      nop            nop            nop           }
unsetenv       {unsetenv       unsetenv-un    unsetenv       unsetenv       unsetenv       setenv-wh     }
x-resource     {x-resource     x-resource     reportCmd      nop            nop            nop           }
      }
   }

   # alias commands where interpreter ref should be passed as argument
   array set aliasesPassArg [list puts __itrp__]

   # initialize list with all commands not dependent of the evaluation mode
   array set aliases $::g_modfileBaseAliases

   # add alias commands whose target command vary depending on the eval mode
   set modeidx [lsearch -exact $::g_modfileEvalModes $mode]
   foreach alias [array names g_modfilePerModeAliases] {
      set aliastarget [set aliases($alias) [lindex\
         $g_modfilePerModeAliases($alias) $modeidx]]
      # some target procedures need command name as first arg
      if {$aliastarget eq {reportCmd} || $aliastarget eq {nimp}} {
         set aliasesPassArg($alias) $alias
      # associate a trace command if per-mode alias command is not reportCmd
      # in display mode (except for source-sh)
      } elseif {$mode eq {display} && $alias ne {source-sh}} {
         set traces($alias) reportCmdTrace
      }
   }
}

proc execute-modulefile {modfile modname modspec {must_have_cookie 1}\
   {fetch_tags 1}} {
   lappendState modulefile $modfile
   lappendState modulename $modname
   lappendState specifiedname $modspec
   set mode [currentState mode]
   lappendState debug_msg_prefix\
      "\[#[depthState modulename]:$mode:$modname\] "

   # skip modulefile if interpretation has been inhibited
   if {[getState inhibit_interp]} {
      reportDebug "skipping $modfile"
      return 1
   }

   reportTrace "'$modfile' as '$modname'" {Evaluate modulefile}

   # gather all tags of evaluated modulefile
   if {$fetch_tags} {
      cacheCurrentModules 0
      collectModuleTags $modname
   }

   # inform that access to module will be soon denied
   if {$mode ne {unload} && [isModuleTagged $modname nearly-forbidden 1]} {
      reportWarning [getNearlyForbiddenMsg $modname]
   # fail unload attempt if module is sticky, unless if forced or reloading
   # also fail unload if mod is super-sticky even if forced, unless reloading
   } elseif {$mode eq {unload}} {
      if {[isModuleTagged $modname super-sticky 1] && [currentState\
         reloading_supersticky] ne $modname} {
         # restore changed states prior raising error
         lpopState debug_msg_prefix
         lpopState specifiedname
         lpopState modulename
         lpopState modulefile
         knerror [getSuperStickyUnloadMsg $modname]
      } elseif {[isModuleTagged $modname sticky 1] && [currentState\
         reloading_sticky] ne $modname} {
         if {[getState force]} {
            reportWarning [getStickyForcedUnloadMsg $modname]
         } else {
            # restore changed states prior raising error
            lpopState debug_msg_prefix
            lpopState specifiedname
            lpopState modulename
            lpopState modulefile
            knerror [getStickyUnloadMsg $modname]
         }
      }
   }

   if {![info exists ::g_modfileUntrackVars]} {
      # list variable that should not be tracked for saving
      array set ::g_modfileUntrackVars [list ModulesCurrentModulefile 1\
         must_have_cookie 1 modcontent 1 env 1]

      # commands that should be renamed before aliases setup
      array set ::g_modfileRenameCmds [list puts _puts]
   }
   # dedicate an interpreter per mode and per level of interpretation to have
   # a dedicated interpreter in case of cascaded multi-mode interpretations
   set itrp __modfile_${mode}_[depthState modulename]

   # evaluation mode-specific configuration
   set dumpCommandsVN g_modfile${mode}Commands
   set aliasesVN g_modfile${mode}Aliases
   set aliasesPassArgVN g_modfile${mode}AliasesPassArg
   set tracesVN g_modfile${mode}Traces
   if {![info exists ::$aliasesVN]} {
      initModfileModeAliases $mode $aliasesVN $aliasesPassArgVN $tracesVN
   }

   # create modulefile interpreter at first interpretation
   if {![interp exists $itrp]} {
      reportDebug "creating interp $itrp"
      interp create $itrp

      # record module tool properties
      interp eval $itrp set ::ModuleTool Modules
      interp eval $itrp set ::ModuleToolVersion {4.7.0}

      # dump initial interpreter state to restore it before each modulefile
      # interpretation. use same dump state for all modes/levels
      if {![info exists ::g_modfileVars]} {
         dumpInterpState $itrp g_modfileVars g_modfileArrayVars\
            g_modfileUntrackVars g_modfileProcs
      }

      # interp has just been created
      set fresh 1
   } else {
      set fresh 0
   }

   # reset interp state command before each interpretation
   resetInterpState $itrp $fresh g_modfileVars g_modfileArrayVars\
      g_modfileUntrackVars g_modfileProcs $aliasesVN $aliasesPassArgVN\
      $tracesVN g_modfileRenameCmds $dumpCommandsVN

   # reset modulefile-specific variable before each interpretation
   interp eval $itrp set ::ModulesCurrentModulefile "{$modfile}"
   interp eval $itrp set must_have_cookie $must_have_cookie

   set errorVal [interp eval $itrp {
      set modcontent [readModuleContent $::ModulesCurrentModulefile 1\
         $must_have_cookie]
      if {$modcontent eq {}} {
         return 1
      }
      info script $::ModulesCurrentModulefile
      # eval then call for specific proc depending mode under same catch
      set sourceFailed [catch {
         eval $modcontent
         switch -- [module-info mode] {
            help {
               if {[info procs ModulesHelp] eq {ModulesHelp}} {
                  ModulesHelp
               } else {
                  reportWarning "Unable to find ModulesHelp in\
                     $::ModulesCurrentModulefile."
               }
            }
            display {
               if {[info procs ModulesDisplay] eq {ModulesDisplay}} {
                  ModulesDisplay
               }
            }
            test {
               if {[info procs ModulesTest] eq {ModulesTest}} {
                  if {[string is true -strict [ModulesTest]]} {
                     report {Test result: PASS}
                  } else {
                     report {Test result: FAIL}
                     raiseErrorCount
                  }
               } else {
                  reportWarning "Unable to find ModulesTest in\
                     $::ModulesCurrentModulefile."
               }
            }
         }
      } errorMsg]
      if {$sourceFailed} {
         # no error in case of "continue" command
         # catch continue even if called outside of a loop
         if {$errorMsg eq {invoked "continue" outside of a loop}\
            || $sourceFailed == 4} {
            unset errorMsg
            return 0
         # catch break even if called outside of a loop
         } elseif {$errorMsg eq {invoked "break" outside of a loop}\
            || ($errorMsg eq {} && (![info exists ::errorInfo]\
            || $::errorInfo eq {}))} {
            unset errorMsg
            # report load/unload evaluation break if verbosity level >= normal
            if {([module-info mode load] || [module-info mode unload]) &&\
               [isVerbosityLevel normal]} {
               reportError {Module evaluation aborted}
            } else {
               raiseErrorCount
            }
            return 1
         } elseif {$errorCode eq {MODULES_ERR_SUBFAILED}} {
            # error counter and message already handled, just return error
            return 1
         } elseif {$errorCode eq {MODULES_ERR_GLOBALTOP}} {
            reportError $errorMsg 1
            return 1
         } elseif {$errorCode eq {MODULES_ERR_GLOBAL}} {
            reportError $errorMsg
            return 1
         } else {
            # format stack trace to report modulefile information only
            reportInternalBug [formatErrStackTrace $::errorInfo\
               $::ModulesCurrentModulefile [concat [info procs] [info\
               commands]]]
            return 1
         }
      } else {
         unset errorMsg
         return 0
      }
   }]

   reportDebug "exiting $modfile"
   lpopState debug_msg_prefix
   lpopState specifiedname
   lpopState modulename
   lpopState modulefile

   return $errorVal
}

# Smaller subset than main module load... This function runs modulerc and
# .version files
proc execute-modulerc {modfile modname modspec} {
   lappendState modulefile $modfile
   # push name to be found by module-alias and version
   lappendState modulename $modname
   lappendState specifiedname $modspec
   set ::ModulesVersion {}
   lappendState debug_msg_prefix "\[#[depthState modulename]:$modname\] "

   if {![info exists ::g_modrcUntrackVars]} {
      # list variable that should not be tracked for saving
      array set ::g_modrcUntrackVars [list ModulesCurrentModulefile 1\
         ModulesVersion 1 modcontent 1 env 1]

      # commands that should be renamed before aliases setup
      array set ::g_modrcRenameCmds [list]

      # list interpreter alias commands to define
      array set ::g_modrcAliases [list uname uname system system versioncmp\
         versioncmp chdir nop is-loaded is-loaded module-version\
         module-version module-alias module-alias module-virtual\
         module-virtual module-forbid module-forbid module-hide module-hide\
         module-tag module-tag module nop module-info module-info\
         module-trace nop module-verbosity nop module-user nop module-log nop\
         reportInternalBug reportInternalBug setModulesVersion\
         setModulesVersion readModuleContent readModuleContent\
         formatErrStackTrace formatErrStackTrace]

      # alias commands where an argument should be passed
      array set ::g_modrcAliasesPassArg [list]

      # trace commands that should be associated to aliases
      array set ::g_modrcAliasesTraces [list]
   }

   # dedicate an interpreter per level of interpretation to have in case of
   # cascaded interpretations a specific interpreter per level
   set itrp __modrc_[depthState modulename]

   reportTrace '$modfile' {Evaluate modulerc}
   # create modulerc interpreter at first interpretation
   if {![interp exists $itrp]} {
      reportDebug "creating interp $itrp"
      interp create $itrp

      # record module tool properties
      interp eval $itrp set ::ModuleTool Modules
      interp eval $itrp set ::ModuleToolVersion {4.7.0}

      # dump initial interpreter state to restore it before each modulerc
      # interpreation. use same dump state for all levels
      if {![info exists ::g_modrcVars]} {
         dumpInterpState $itrp g_modrcVars g_modrcArrayVars\
            g_modrcUntrackVars g_modrcProcs
      }

      # interp has just been created
      set fresh 1
   } else {
      set fresh 0
   }

   # reset interp state command before each interpretation
   resetInterpState $itrp $fresh g_modrcVars g_modrcArrayVars\
      g_modrcUntrackVars g_modrcProcs g_modrcAliases g_modrcAliasesPassArg\
      g_modrcAliasesTraces g_modrcRenameCmds g_modrcCommands

   interp eval $itrp set ::ModulesCurrentModulefile "{$modfile}"
   interp eval $itrp {set ::ModulesVersion {}}

   set errorVal [interp eval $itrp {
      set modcontent [readModuleContent $::ModulesCurrentModulefile]
      if {$modcontent eq {}} {
         # simply skip rc file, no exit on error here
         return 1
      }
      info script $::ModulesCurrentModulefile
      if [catch {eval $modcontent} errorMsg] {
         # format stack trace to report modulerc information only
         reportInternalBug [formatErrStackTrace $::errorInfo\
            $::ModulesCurrentModulefile [concat [info procs] [info commands]]]
         return 1
      } else {
         # pass ModulesVersion value to main interp
         if {[info exists ::ModulesVersion]} {
            setModulesVersion $::ModulesVersion
         }
         return 0
      }
   }]

   # default version set via ModulesVersion variable in .version file
   # override previously defined default version for modname
   lassign [getModuleNameVersion] mod modname modversion
   if {$modversion eq {.version} && $::ModulesVersion ne {}} {
      # ModulesVersion should target an element in current directory
      if {[string first / $::ModulesVersion] == -1} {
         setModuleResolution $modname/default $modname/$::ModulesVersion\
            default
      } else {
         reportError "Invalid ModulesVersion '$::ModulesVersion' defined"
      }
   }

   lpopState debug_msg_prefix
   lpopState specifiedname
   lpopState modulename
   lpopState modulefile

   return $::ModulesVersion
}

# Save list of the defined procedure and the global variables with their
# associated values set in sub interpreter passed as argument. Global
# structures are used to save these information and the name of these
# structures are provided as argument.
proc dumpInterpState {itrp dumpVarsVN dumpArrayVarsVN untrackVarsVN\
   dumpProcsVN} {
   upvar #0 $dumpVarsVN dumpVars
   upvar #0 $dumpArrayVarsVN dumpArrayVars
   upvar #0 $untrackVarsVN untrackVars
   upvar #0 $dumpProcsVN dumpProcs

   regexp {^__[a-z]+} $itrp itrpkind
   # save name and value for any other global variables
   foreach var [$itrp eval {info globals}] {
      if {![info exists untrackVars($var)]} {
         reportDebug "saving for $itrpkind var $var"
         if {[$itrp eval array exists ::$var]} {
            set dumpVars($var) [$itrp eval array get ::$var]
            set dumpArrayVars($var) 1
         } else {
            set dumpVars($var) [$itrp eval set ::$var]
         }
      }
   }

   # save name of every defined procedures
   foreach var [$itrp eval {info procs}] {
      set dumpProcs($var) 1
   }
   reportDebug "saving for $itrpkind proc list [array names dumpProcs]"
}

# Define commands to be known by sub interpreter.
proc initInterpCommands {itrp fresh aliasesVN aliasesPassArgVN tracesVN\
   renameCmdsVN} {
   upvar #0 $aliasesVN aliases
   upvar #0 $aliasesPassArgVN aliasesPassArg
   upvar #0 $tracesVN traces
   upvar #0 $renameCmdsVN renameCmds

   # rename some commands on freshly created interp before aliases defined
   # below overwrite them
   if {$fresh} {
      foreach cmd [array names renameCmds] {
         $itrp eval rename $cmd $renameCmds($cmd)
      }
   }

   # set interpreter alias commands each time to guaranty them being
   # defined and not overridden by modulefile or modulerc content
   foreach alias [array names aliases] {
      if {[info exists aliasesPassArg($alias)]} {
         set aliasarg $aliasesPassArg($alias)
         # pass current itrp reference on special keyword
         if {$aliasarg eq {__itrp__}} {
            set aliasarg $itrp
         }
         interp alias $itrp $alias {} $aliases($alias) $aliasarg
      } else {
         interp alias $itrp $alias {} $aliases($alias)
      }
   }

   foreach alias [array names traces] {
      interp eval $itrp [list trace add execution $alias leave\
         $traces($alias)]
   }
}

# Restore initial setup of sub interpreter passed as argument based on
# global structure previously filled with initial list of defined procedure
# and values of global variable.
proc resetInterpState {itrp fresh dumpVarsVN dumpArrayVarsVN untrackVarsVN\
   dumpProcsVN aliasesVN aliasesPassArgVN tracesVN renameCmdsVN\
   dumpCommandsVN} {
   upvar #0 $dumpVarsVN dumpVars
   upvar #0 $dumpArrayVarsVN dumpArrayVars
   upvar #0 $untrackVarsVN untrackVars
   upvar #0 $dumpProcsVN dumpProcs
   upvar #0 $dumpCommandsVN dumpCommands

   # look at list of defined procedures and delete those not part of the
   # initial state list. do not check if they have been altered as no vital
   # procedures lied there. note that if a Tcl command has been overridden
   # by a proc, it will be removed here and command will also disappear
   foreach var [$itrp eval {info procs}] {
      if {![info exists dumpProcs($var)]} {
         reportDebug "removing on $itrp proc $var"
         $itrp eval [list rename $var {}]
      }
   }

   # rename some commands and set aliases on interpreter
   initInterpCommands $itrp $fresh $aliasesVN $aliasesPassArgVN $tracesVN\
      $renameCmdsVN

   # dump interpreter command list here on first time as aliases should be
   # set prior to be found on this list for correct match
   if {![info exists dumpCommands]} {
      set dumpCommands [$itrp eval {info commands}]
      reportDebug "saving for $itrp command list $dumpCommands"
   # if current interpreter command list does not match initial list it
   # means that at least one command has been altered so we need to recreate
   # interpreter to guaranty proper functioning
   } elseif {$dumpCommands ne [$itrp eval {info commands}]} {
      reportDebug "missing command(s), recreating interp $itrp"
      interp delete $itrp
      interp create $itrp
      initInterpCommands $itrp 1 $aliasesVN $aliasesPassArgVN $tracesVN\
         $renameCmdsVN
   }

   # check every global variables currently set and correct them to restore
   # initial interpreter state. work on variables at the very end to ensure
   # procedures and commands are correctly defined
   foreach var [$itrp eval {info globals}] {
      if {![info exists untrackVars($var)]} {
         if {![info exists dumpVars($var)]} {
            reportDebug "removing on $itrp var $var"
            $itrp eval unset ::$var
         } elseif {![info exists dumpArrayVars($var)]} {
            if {$dumpVars($var) ne [$itrp eval set ::$var]} {
               reportDebug "restoring on $itrp var $var"
               if {[llength $dumpVars($var)] > 1} {
                  # restore value as list
                  $itrp eval set ::$var [list $dumpVars($var)]
               } else {
                  # brace value to be able to restore empty string
                  $itrp eval set ::$var "{$dumpVars($var)}"
               }
            }
         } else {
            if {$dumpVars($var) ne [$itrp eval array get ::$var]} {
               reportDebug "restoring on $itrp var $var"
               $itrp eval array set ::$var [list $dumpVars($var)]
            }
         }
      }
   }
}

########################################################################
# commands run from inside a module file
#

# Dictionary-style string comparison
# Use dictionary sort of lsort proc to compare two strings in the "string
# compare" fashion (returning -1, 0 or 1). Tcl dictionary-style comparison
# enables to compare software versions (ex: "1.10" is greater than "1.8")
proc versioncmp {str1 str2} {
   if {$str1 eq $str2} {
      return 0
   # put both strings in a list, then lsort it and get first element
   } elseif {[lindex [lsort -dictionary [list $str1 $str2]] 0] eq $str1} {
      return -1
   } else {
      return 1
   }
}

proc module-info {what {more {}}} {
   set mode [currentState mode]

   reportDebug "$what $more"

   switch -- $what {
      mode {
         if {$more ne {}} {
            set command [currentState commandname]
            return [expr {$mode eq $more || ($more eq {remove} && $mode eq \
               {unload}) || ($more eq {switch} && $command eq {switch})}]
         } else {
            return $mode
         }
      }
      command {
         set command [currentState commandname]
         if {$more eq {}} {
            return $command
         } else {
            return [expr {$command eq $more}]
         }
      }
      name {
         return [currentState modulename]
      }
      specified {
         return [currentState specifiedname]
      }
      shell {
         if {$more ne {}} {
            return [expr {[getState shell] eq $more}]
         } else {
            return [getState shell]
         }
      }
      flags {
         # C-version specific option, not relevant for Tcl-version but return
         # a zero integer value to avoid breaking modulefiles using it
         return 0
      }
      shelltype {
         if {$more ne {}} {
            return [expr {[getState shelltype] eq $more}]
         } else {
            return [getState shelltype]
         }
      }
      user {
         # C-version specific option, not relevant for Tcl-version but return
         # an empty value or false to avoid breaking modulefiles using it
         if {$more ne {}} {
            return 0
         } else {
            return {}
         }
      }
      alias {
         set ret [resolveModuleVersionOrAlias $more [isIcase]]
         if {$ret ne $more} {
            return $ret
         } else {
            return {}
         }
      }
      trace {
         return {}
      }
      tracepat {
         return {}
      }
      type {
         return Tcl
      }
      symbols {
         lassign [getModuleNameVersion $more 1] mod modname modversion
         set sym_list [getVersAliasList $mod]
         # if querying special symbol "default" but nothing found registered
         # on it, look at symbol registered on bare module name in case there
         # are symbols registered on it but no default symbol set yet to link
         # to them
         if {[llength $sym_list] == 0 && $modversion eq {default}} {
            set sym_list [getVersAliasList $modname]
         }
         return [join $sym_list :]
      }
      tags {
         if {$more ne {}} {
            return [isInList [getTagList [currentState modulename]] $more]
         } else {
            return [getTagList [currentState modulename]]
         }
      }
      version {
         lassign [getModuleNameVersion $more 1] mod
         return [resolveModuleVersionOrAlias $mod [isIcase]]
      }
      loaded {
         lassign [getModuleNameVersion $more 1] mod
         return [getLoadedMatchingName $mod returnall]
      }
      usergroups {
         if {[getState is_win]} {
            knerror "module-info usergroups not supported on Windows platform"
         } else {
            if {$more ne {}} {
               return [isInList [getState usergroups] $more]
            } else {
               return [getState usergroups]
            }
         }
      }
      username {
         if {[getState is_win]} {
            knerror "module-info username not supported on Windows platform"
         } else {
            if {$more ne {}} {
               return [expr {[getState username] eq $more}]
            } else {
               return [getState username]
            }
         }
      }
      default {
         knerror "module-info $what not supported"
         return {}
      }
   }
}

proc module-whatis {args} {
   set message [join $args]
   reportDebug $message
   lappend ::g_whatis $message

   return {}
}

# convert environment variable references in string to their values
# every local variable is prefixed by '0' to ensure they will not be
# overwritten through variable reference resolution process
proc resolvStringWithEnv {0str} {
   # fetch variable references in string
   set 0match_list [regexp -all -inline {\$[{]?([A-Za-z_][A-Za-z0-9_]*)[}]?}\
      ${0str}]
   if {[llength ${0match_list}] > 0} {
      # put in local scope every environment variable referred in string
      for {set 0i 1} {${0i} < [llength ${0match_list}]} {incr 0i 2} {
         set 0varname [lindex ${0match_list} ${0i}]
         if {![info exists ${0varname}]} {
            set ${0varname} [get-env ${0varname}]
         }
      }
      # resolv variable reference with values (now in local scope)
      set 0res [subst -nobackslashes -nocommands ${0str}]
   } else {
      set 0res ${0str}
   }

   reportDebug "'${0str}' resolved to '${0res}'"

   return ${0res}
}

# deduce modulepath from modulefile and module name
proc getModulepathFromModuleName {modfile modname} {
   return [string range $modfile 0 end-[string length /$modname]]
}

# deduce module name from modulefile and modulepath
proc getModuleNameFromModulepath {modfile modpath} {
   return [string range $modfile [string length $modpath/] end]
}

# extract module name from modulefile and currently enabled modulepaths
proc findModuleNameFromModulefile {modfile} {
   set ret {}

   foreach modpath [getModulePathList] {
      if {[string first $modpath/ $modfile/] == 0} {
         set ret [getModuleNameFromModulepath $modfile $modpath]
         break
      }
   }
   return $ret
}

# extract modulepath from modulefile and currently enabled modulepaths
proc findModulepathFromModulefile {modfile} {
   set ret {}

   foreach modpath [getModulePathList] {
      if {[string first $modpath/ $modfile/] == 0} {
         set ret $modpath
         break
      }
   }
   return $ret
}

# Determine with a name provided as argument the corresponding module name,
# version and name/version. Module name is guessed from current module name
# when shorthand version notation is used. Both name and version are guessed
# from current module if name provided is empty. If 'name_relative_tocur' is
# enabled then name argument may be interpreted as a name relative to the
# current modulefile directory (useful for module-version and module-alias
# for instance).
proc getModuleNameVersion {{name {}} {name_relative_tocur 0}} {
   set curmod [currentState modulename]
   set curmodname [file dirname $curmod]
   set curmodversion [file tail $curmod]

   if {$name eq {}} {
      set name $curmodname
      set version $curmodversion
   # check for shorthand version notation like "/version" or "./version"
   # only if we are currently interpreting a modulefile or modulerc
   } elseif {$curmod ne {} && [regexp {^\.?\/(.*)$} $name match version]} {
      # if we cannot distinguish a module name, raise error when shorthand
      # version notation is used
      if {[currentState modulefile] ne $curmod && $curmod ne {.modulerc}} {
         # name is the name of current module directory
         set name $curmodname
      } else {
         reportError "Invalid modulename '$name' found"
         return {}
      }
   } else {
      set name [string trimright $name /]
      set version [file tail $name]
      if {$name eq $version} {
         set version {}
      } else {
         set name [file dirname $name]
      }
      # name may correspond to last part of current module
      # if so name is replaced by current module name
      if {$name_relative_tocur && [file tail $curmodname] eq $name} {
         set name $curmodname
      }
   }

   if {$version eq {}} {
      set mod $name
   } else {
      set mod $name/$version
   }

   return [list $mod $name $version]
}

# Register alias or symbolic version deep resolution in a global array that
# can be used thereafter to get in one query the actual modulefile behind
# a virtual name. Also consolidate a global array that in the same manner
# list all the symbols held by modulefiles.
proc setModuleResolution {mod target {symver {}} {override_res_path 1}\
   {auto_symver 0}} {
   global g_moduleResolved g_resolvedHash g_resolvedPath g_symbolHash

   # find end-point module and register step-by-step path to get to it
   set res $target
   lappend res_path $res
   while {$mod ne $res && [info exists g_resolvedPath($res)]} {
      set res $g_resolvedPath($res)
      lappend res_path $res
   }

   # error if resolution end on initial module
   if {$mod eq $res} {
      reportError "Resolution loop on '$res' detected"
      return 0
   }

   # module name will be useful when registering symbol
   if {$symver ne {}} {
      lassign [getModuleNameVersion $mod] modfull modname
   }

   # change default symbol owner if previously given; auto symbol are defined
   # only if no default is pre-existing
   if {$symver eq {default} && !$auto_symver} {
      # alternative name "modname" is set when mod = "modname/default" both
      # names will be registered to be known for queries and resolution defs
      set modalt $modname

      if {[info exists g_moduleResolved($mod)]} {
         set prev $g_moduleResolved($mod)
         # there may not be a default in case of auto symbol
         if {[info exists g_symbolHash($prev)] && [set idx [lsearch -exact\
            $g_symbolHash($prev) default]] != -1} {
            reportDebug "remove symbol 'default' from '$prev'"
            set g_symbolHash($prev) [lreplace $g_symbolHash($prev) $idx $idx]
         }
      }
   }

   # register end-point resolution
   reportDebug "$mod resolved to $res"
   set g_moduleResolved($mod) $res
   # set first element of resolution path only if not already set or
   # scratching enabled, no change when propagating symbol along res path
   if {$override_res_path || ![info exists g_resolvedPath($mod)]} {
      set g_resolvedPath($mod) $target
   }
   lappend g_resolvedHash($res) $mod

   # also register resolution on alternative name if any
   if {[info exists modalt]} {
      reportDebug "$modalt resolved to $res"
      set g_moduleResolved($modalt) $res
      if {$override_res_path || ![info exists g_resolvedPath($modalt)]} {
         set g_resolvedPath($modalt) $target
      }
      lappend g_resolvedHash($res) $modalt
      # register name alternative to know their existence
      set ::g_moduleAltName($modalt) $mod
      set ::g_moduleAltName($mod) $modalt
   }

   # if other modules were pointing to this one, adapt resolution end-point
   set relmod_list {}
   if {[info exists g_resolvedHash($mod)]} {
      set relmod_list $g_resolvedHash($mod)
      unset g_resolvedHash($mod)
   }
   # also adapt resolution for modules pointing to the alternative name
   if {[info exists modalt] && [info exists g_resolvedHash($modalt)]} {
      set relmod_list [concat $relmod_list $g_resolvedHash($modalt)]
      unset g_resolvedHash($modalt)
   }
   foreach relmod $relmod_list {
      set g_moduleResolved($relmod) $res
      reportDebug "$relmod now resolved to $res"
      lappend g_resolvedHash($res) $relmod
   }

   # register and propagate symbols to the resolution path, execption made for
   # auto symbol which are stored separately and not propagated
   if {[info exists g_symbolHash($mod)]} {
      set sym_list $g_symbolHash($mod)
   } else {
      set sym_list {}
   }
   if {$symver ne {} && $auto_symver} {
      reportDebug "define auto symbolic version '$mod' targeting $target"
      set ::g_autoSymbol($mod) $target
   } elseif {$symver ne {} && !$auto_symver} {
      # merge symbol definitions in case of alternative name
      if {[info exists modalt] && [info exists g_symbolHash($modalt)]} {
         set sym_list [lsort -dictionary -unique [concat $sym_list\
            $g_symbolHash($modalt)]]
         reportDebug "set symbols '$sym_list' to $mod and $modalt"
         set g_symbolHash($mod) $sym_list
         set g_symbolHash($modalt) $sym_list
      }

      # dictionary-sort symbols and remove eventual duplicates
      set sym_list [lsort -dictionary -unique [concat $sym_list\
         [list $symver]]]

      # propagate symbols in g_symbolHash and g_moduleVersion toward the
      # resolution path, handle that locally if we still work on same
      # modulename, call for a proper resolution as soon as we change of
      # module to get this new resolution registered
      foreach modres $res_path {
         lassign [getModuleNameVersion $modres] modfull modresname
         if {$modname eq $modresname} {
            if {[info exists g_symbolHash($modres)]} {
               set modres_sym_list [lsort -dictionary -unique [concat\
                  $g_symbolHash($modres) $sym_list]]
            } else {
               set modres_sym_list $sym_list
            }
            # sync symbols of alternative name if any
            if {[info exists ::g_moduleAltName($modres)]} {
               set altmodres $::g_moduleAltName($modres)
               reportDebug "set symbols '$modres_sym_list' to $modres and\
                  $altmodres"
               set g_symbolHash($altmodres) $modres_sym_list
            } else {
               reportDebug "set symbols '$modres_sym_list' to $modres"
            }
            set g_symbolHash($modres) $modres_sym_list

            # register symbolic version for querying in g_moduleVersion
            foreach symelt $sym_list {
               set modvers $modresname/$symelt
               reportDebug "module-version $modvers = $modres"
               set ::g_moduleVersion($modvers) $modres
               set ::g_sourceVersion($modvers) [currentState modulefile]
               # record eventual missing resolution
               if {![info exists g_moduleResolved($modvers)]} {
                  set g_moduleResolved($modvers) $modres
                  reportDebug "$modvers resolved to $modres"
                  lappend g_resolvedHash($modres) $modvers
               }
            }
         # as we change of module name a proper resolution call should be
         # made (see below) and will handle the rest of the resolution path
         } else {
            set need_set_res 1
            break
         }
      }
   # when registering an alias, existing symbols on alias source name should
   # be broadcast along the resolution path with a proper resolution call
   # (see below)
   } else {
      lassign [getModuleNameVersion $target] modres modresname
      set need_set_res 1
   }

   # resolution needed to broadcast symbols along resolution path without
   # altering initial path already set for these symbols
   if {[info exists need_set_res]} {
      foreach symelt $sym_list {
         set modvers $modresname/$symelt
         reportDebug "set resolution for $modvers"
         setModuleResolution $modvers $modres $symelt 0
      }
   }

   return 1
}

# retrieve all names that resolve to passed mod
proc getAllModuleResolvedName {mod {flag_type 0} {modspec {}}} {
   set namelist {}
   set resmodlist {}
   set icase [isIcase]
   defineModEqProc $icase [getConf extended_default]

   # get parent directories of mod
   foreach modelt [split $mod /] {
      if {[info exists modroot]} {
         append modroot /
      }
      append modroot $modelt
      lappend resmodlist $modroot
   }

   # add additionnaly all the altnames set on directories, parents of mod
   # or on distant directories whose default version resolves to mod
   for {set i 0} {$i < [llength $resmodlist]} {incr i 1} {
      set modelt [getArrayKey ::g_resolvedHash [lindex $resmodlist $i] $icase]
      if {[info exists ::g_resolvedHash($modelt)]} {
         foreach resmod $::g_resolvedHash($modelt) {
            # if alternative name corresponds to one root name test if default
            # symbol is hidden
            set resmoddfl [expr {[lsearch -exact $resmodlist $resmod] != -1 ?\
               "$resmod/default" : {}}]
            # if alternative name corresponds to default symbol and is hidden
            # test if query matches bare module name
            set resmodpar [expr {[file tail $resmod] eq {default} ? [file\
               dirname $resmod] : {}}]

            # if modelt is not a parent directory of mod, check its resolution
            # points to mod (directly for alias/sym or indirectly for dir
            # whose default version bridge resolution toward mod)
            # if modspec arg is set, exclude hidden entries not explicitly
            # matching modspec. auto symbols cannot be hidden
            if {($modspec eq {} || ([info exists ::g_autoSymbol($resmod)] &&\
               $::g_autoSymbol($resmod) eq $mod) || (![isModuleHidden $resmod\
               $modspec] && ($resmoddfl eq {} || ![isModuleHidden $resmoddfl\
               $modspec])) || [modEq $modspec $resmod eqspec] || ($resmodpar\
               ne {} && [modEq $modspec $resmodpar eqspec])) && ([modEq\
               $modelt $mod eqstart] || $::g_moduleResolved($resmod) eq $mod\
               || $mod eq [lindex [getPathToModule\
               $::g_moduleResolved($resmod) {} 0] 1])} {
               # prefix automatically generated syms with type flag if asked
               if {$flag_type && [info exists ::g_moduleAlias($resmod)]} {
                  appendNoDupToList namelist al|$resmod
               } elseif {$flag_type && [info exists\
                  ::g_autoSymbol($resmod)]} {
                  appendNoDupToList namelist as|$resmod
               } else {
                  appendNoDupToList namelist $resmod
               }

               unset modroot
               foreach reselt [split [file dirname $resmod] /] {
                  if {[info exists modroot]} {
                     append modroot /
                  }
                  append modroot $reselt
                  appendNoDupToList resmodlist $modroot
               }
            }
         }
      }
   }
   return $namelist
}

# Specifies a default or alias version for a module that points to an
# existing module version Note that aliases defaults are stored by the
# short module name (not the full path) so aliases and defaults from one
# directory will apply to modules of the same name found in other
# directories.
proc module-version {args} {
   reportDebug $args
   lassign [getModuleNameVersion [lindex $args 0] 1] mod modname modversion

   # go for registration only if valid modulename
   if {$mod ne {}} {
      foreach version [lrange $args 1 end] {
         set aliasversion $modname/$version
         # do not alter a previously defined alias version
         if {![info exists ::g_moduleVersion($aliasversion)]} {
            setModuleResolution $aliasversion $mod $version
         } else {
            reportWarning "Symbolic version '$aliasversion' already defined"
         }
      }
   }

   return {}
}

proc module-alias {args} {
   lassign [getModuleNameVersion [lindex $args 0]] alias
   lassign [getModuleNameVersion [lindex $args 1] 1] mod

   reportDebug "$alias = $mod"

   if {[setModuleResolution $alias $mod]} {
      set ::g_moduleAlias($alias) $mod
      set ::g_sourceAlias($alias) [currentState modulefile]
   }

   return {}
}

proc module-virtual {args} {
   lassign [getModuleNameVersion [lindex $args 0]] mod
   set modfile [getAbsolutePath [lindex $args 1]]

   reportDebug "$mod = $modfile"

   set ::g_moduleVirtual($mod) $modfile
   set ::g_sourceVirtual($mod) [currentState modulefile]

   return {}
}

# Parse date time argument value and translate it into epoch time
proc __parseDateTimeArg {opt datetime} {
   if {[regexp {^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2})?$} $datetime match\
      timespec]} {
      # time specification is optional
      if {$timespec eq {}} {
         append datetime T00:00
      }
      # return corresponding epoch time
      return [clock scan $datetime -format %Y-%m-%dT%H:%M]
   } else {
      knerror "Incorrect $opt value '$datetime' (valid date time format is\
         'YYYY-MM-DD\[THH:MM\]')"
   }
}

# parse application critera arguments and determine if command applies
proc parseApplicationCriteriaArgs {aftbef nearsec args} {
   set otherargs {}

   # parse argument list
   foreach arg $args {
      if {[info exists nextargisval]} {
         set $nextargisval $arg
         unset nextargisval
      } elseif {[info exists nextargisdatetime]} {
         set ${nextargisdatetime}raw $arg
         # get epoch time from date time argument value
         set $nextargisdatetime [parseDateTimeArg $prevarg $arg]
         unset nextargisdatetime
      } else {
         switch -- $arg {
            --after - --before {
               # treat --after/--before as regular content if disabled
               if {!$aftbef} {
                  lappend otherargs $arg
               } elseif {[getState tcl_version_lt85]} {
                  knerror "Option '$arg' not supported on Tcl<8.5"
               } else {
                  set nextargisdatetime [string trimleft $arg -]
               }
            }
            --not-group - --not-user {
               if {[getState is_win]} {
                  knerror "Option '$arg' not supported on Windows platform"
               } else {
                  set nextargisval not[string range $arg 6 end]list
               }
            }
            default {
               lappend otherargs $arg
            }
         }
         set prevarg $arg
      }
   }

   if {[info exists nextargisval] || [info exists nextargisdatetime]} {
      knerror "Missing value for '$prevarg' option"
   }

   # does it apply to current user?
   set notuser [expr {[info exists notuserlist] && [isInList $notuserlist\
      [getState username]]}]
   set notgroup [expr {[info exists notgrouplist] && [isIntBetweenList\
      $notgrouplist [getState usergroups]]}]

   # does it apply currently?
   set isbefore [expr {[info exists before] && [getState clock_seconds] <\
      $before}]
   set isafter [expr {[info exists after] && [getState clock_seconds] >=\
      $after}]

   # are criteria met
   set apply [expr {!$notuser && !$notgroup && ($isbefore || $isafter ||\
      (![info exists before] && ![info exists after]))}]

   # is end limit near ?
   set isnearly [expr {!$apply && !$notuser && !$notgroup && [info exists\
      after] && !$isafter && [getState clock_seconds] >= ($after - $nearsec)}]
   if {![info exists afterraw]} {
      set afterraw {}
   }

   return [list $apply $isnearly $afterraw $otherargs]
}

proc module-forbid {args} {
   # parse application criteria arguments to determine if command apply
   lassign [eval parseApplicationCriteriaArgs 1 [expr {[getConf\
      nearly_forbidden_days] * 86400}] $args] apply isnearly after otherargs

   # parse remaining argument list, do it even if command does not apply to
   # raise any command specification error
   foreach arg $otherargs {
      if {[info exists nextargisval]} {
         set $nextargisval $arg
         unset nextargisval
      } else {
         switch -glob -- $arg {
            --nearly-message {
               set nextargisval nearlymessage
            }
            --message {
               set nextargisval message
            }
            -* {
               knerror "Invalid option '$arg'"
            }
            default {
               lappend modarglist $arg
            }
         }
         set prevarg $arg
      }
   }

   if {[info exists nextargisval]} {
      knerror "Missing value for '$prevarg' option"
   }

   if {![info exists modarglist]} {
      knerror {No module specified in argument}
   }

   # skip record if application criteria are not met
   if {$apply} {
      set proplist {}
      if {[info exists message]} {
         lappend proplist message $message
      }

      # record each forbid spec after parsing them
      foreach modarg [eval parseModuleVersionSpecifier 0 $modarglist] {
         setModspecTag $modarg forbidden $proplist
      }
   } elseif {$isnearly} {
      lappend proplist after $after
      if {[info exists nearlymessage]} {
         lappend proplist message $nearlymessage
      }
      # record each nearly forbid spec after parsing them
      foreach modarg [eval parseModuleVersionSpecifier 0 $modarglist] {
         setModspecTag $modarg nearly-forbidden $proplist
      }
   }
}

proc module-hide {args} {
   set hidinglvl 1
   set hiddenloaded 0

   # parse application criteria arguments to determine if command apply
   lassign [eval parseApplicationCriteriaArgs 1 0 $args] apply isnearly after\
      otherargs

   # parse remaining argument list, do it even if command does not apply to
   # raise any command specification error
   foreach arg $otherargs {
      switch -glob -- $arg {
         --hard {
            # hardened stealth
            set hidinglvl 2
         }
         --soft {
            # soften level of camouflage
            set hidinglvl 0
         }
         --hidden-loaded {
            # module should stay hidden once being loaded
            set hiddenloaded 1
         }
         -* {
            knerror "Invalid option '$arg'"
         }
         default {
            lappend modarglist $arg
         }
      }
   }

   if {![info exists modarglist]} {
      knerror {No module specified in argument}
   }

   # skip hide spec record if application criteria are not met
   if {$apply} {
      # record each hide spec after parsing them
      foreach modarg [eval parseModuleVersionSpecifier 0 $modarglist] {
         setModspecHidingLevel $modarg $hidinglvl
         if {$hiddenloaded} {
            setModspecTag $modarg hidden-loaded
         }
      }
   }
}

proc module-tag {args} {
   # parse application criteria arguments to determine if command apply
   lassign [eval parseApplicationCriteriaArgs 0 0 $args] apply isnearly after\
      otherargs

   # parse remaining argument list, do it even if command does not apply to
   # raise any command specification error
   foreach arg $otherargs {
      switch -glob -- $arg {
         -* {
            knerror "Invalid option '$arg'"
         }
         default {
            if {![info exists tag]} {
               set tag $arg
            } else {
               lappend modarglist $arg
            }
         }
      }
   }

   if {![info exists tag]} {
      knerror {No tag specified in argument}
   }
   if {![info exists modarglist]} {
      knerror {No module specified in argument}
   }
   if {[isInList [list loaded auto-loaded forbidden nearly-forbidden hidden\
      hidden-loaded] $tag]} {
      knerror "'$tag' is a reserved tag name and cannot be set"
   }

   # skip tag record if application criteria are not met
   if {$apply} {
      # record each hide spec after parsing them
      foreach modarg [eval parseModuleVersionSpecifier 0 $modarglist] {
         setModspecTag $modarg $tag
      }
   }
}

proc module {command args} {
   set mode [currentState mode]
   reportDebug "cmd='$command', args='$args'"

   # guess if called from top level
   set topcall [expr {[depthState modulename] == 0}]
   set tryhelpmsg [expr {$topcall ? "\nTry 'module --help' for more\
      information." : {}}]
   if {$topcall} {
      set msgprefix {}
   } else {
      set msgprefix {module: }
   }

   # resolve and check command name
   lassign [parseModuleCommandName $command help] command cmdvalid cmdempty
   # clear other args if no command name supplied
   if {$cmdempty} {
      set args {}
   }
   # raise error if supplied command is not known
   if {!$cmdvalid} {
      knerror "${msgprefix}Invalid command '$command'$tryhelpmsg"
   }

   # parse options, do that globally to ignore options not related to a given
   # module sub-command (exclude them from arg list)
   lassign [eval parseModuleCommandArgs $topcall $command 0 $args]\
      show_oneperline show_mtime show_filter search_filter search_match\
      dump_state addpath_pos not_req args

   # parse module version specification
   if {[isInList [list avail paths whatis load unload switch help test\
      display path is-avail] $command]} {
      set args [eval parseModuleVersionSpecifier 0 $args]
   }

   if {!$topcall} {
      # some commands can only be called from top level, not within modulefile
      switch -- $command {
         path - paths - autoinit - help - prepend-path - append-path -\
         remove-path - is-loaded - is-saved - is-used - is-avail -\
         info-loaded - clear - sh-to-mod {
            knerror "${msgprefix}Command '$command' not supported$tryhelpmsg"
         }
      }
      # other commands can only be called from modulefile evaluated from
      # command acting as top-level context (source and autoinit)
      if {([depthState modulename] > 1 || [notInList [list source autoinit]\
         [currentState commandname]]) && $command eq {config}} {
         knerror "${msgprefix}Command '$command' not supported$tryhelpmsg"
      }
      # no requirement should be recorded this module load/unload/switch cmd
      if {$not_req || ![getConf implicit_requirement]} {
         lappendState inhibit_req_record [currentState evalid]
      }
   }

   # argument number check
   switch -- $command {
      unload - source - display - initadd - initprepend - initrm - test -\
      is-avail {
         if {[llength $args] == 0} {
            set argnberr 1
         }
      }
      reload - aliases - list - purge - savelist - initlist - initclear -\
      autoinit {
         if {[llength $args] != 0} {
            set argnberr 1
         }
      }
      switch {
         if {[llength $args] == 0 || [llength $args] > 2} {
            set argnberr 1
         }
      }
      path - paths - info-loaded {
         if {[llength $args] != 1} {
            set argnberr 1
         }
      }
      search - save - restore - saverm - saveshow - clear {
         if {[llength $args] > 1} {
            set argnberr 1
         }
      }
      initswitch {
         if {[llength $args] != 2} {
            set argnberr 1
         }
      }
      prepend-path - append-path - remove-path - sh-to-mod {
         if {[llength $args] < 2} {
            set argnberr 1
         }
      }
      config {
         if {[llength $args] > 2} {
            set argnberr 1
         }
      }
   }
   if {[info exists argnberr]} {
      knerror "Unexpected number of args for '$command' command$tryhelpmsg"
   }

   # define if modfile should always be fully read even for validity check
   lappendState always_read_full_file [expr {![isInList [list path paths list\
      avail aliases] $command]}]
   lappendState commandname $command

   if {$topcall} {
      # Find and execute any global rc file found
      runModulerc
   }

   switch -- $command {
      load {
         # ignore flag used in collection to track non-user asked state
         set args [replaceFromList $args --notuasked]
         # no error raised on empty argument list to cope with
         # initadd command that may expect this behavior
         if {[llength $args] > 0} {
            set ret 0
            # if top command is source, consider module load commands made
            # within sourced file evaluation as top load command
            if {$topcall || ([depthState modulename] == 1 && (
               [aboveCommandName] eq {source} || [aboveCommandName] eq\
               {autoinit} || [isStateDefined rc_running]))} {
               set ret [eval cmdModuleLoad load 1 $args]
            } elseif {$mode eq {load}} {
               # load here if no auto mode, done through prereq elsewhere
               # inhibited if currently in DepRe context
               if {![getConf auto_handling] && [currentModuleEvalContext] ne\
                  {depre}} {
                  # attempt load of not already loaded modules
                  foreach arg $args {
                     if {![is-loaded $arg] && ![is-loading $arg]} {
                        lappend modlist $arg
                     }
                  }
                  if {[info exists modlist]} {
                     set ret [eval cmdModuleLoad reqlo 0 $modlist]
                     # ignore obtained error if force mode enabled
                     if {[getState force]} {
                        set ret 0
                     }
                  }
               }
               # register modulefiles to load as individual prereqs
               foreach arg $args {
                  prereq $arg
               }
            # mods unload is handled via UReqUn mechanism when auto enabled
            # (unless if implicit_requirement has been inhibited) also unloads
            # are triggered by ongoing reload, purge or restore commands
            } elseif {(![getConf auto_handling] || [getState\
               inhibit_req_record] eq [currentState evalid]) && [notInList\
               [list purge reload restore] [aboveCommandName]]} {
               # on unload mode, unload mods in reverse order, if loaded
               # prior this mod, if not user asked and not required by
               # other loaded mods
               set modlist [getLoadedModuleList]
               set modidx [lsearch -exact $modlist [currentState modulename]]
               if {$modidx != 0} {
                  set priormodlist [lrange $modlist 0 $modidx]
                  foreach arg [lreverse $args] {
                     if {[set unmod [getLoadedMatchingName $arg {} 0\
                        $priormodlist]] ne {}} {
                        if {[cmdModuleUnload urequn match 1 0 1 1 $unmod]} {
                           reportWarning "Unload of useless requirement\
                              $unmod failed" 1
                        }
                     }
                  }
               }
            }
            # sub-module interpretation failed, raise error
            if {$ret && !$topcall} {
               knerror {} MODULES_ERR_SUBFAILED
            }
         }
      }
      unload {
         # if top command is source, consider module load commands made
         # within sourced file evaluation as top load command
         if {$topcall || ([depthState modulename] == 1 && (
            [aboveCommandName] eq {source} || [aboveCommandName] eq\
            {autoinit} || [isStateDefined rc_running]))} {
            set ret [eval cmdModuleUnload unload match 1 0 0 0 $args]
         } elseif {$mode eq {load}} {
            # unload mods only on load mode, nothing done on unload mode as
            # the registered conflict guarantees the target module cannot
            # be loaded unless forced
            # do not unload module required by others even in force mode
            set ret [eval cmdModuleUnload conun match 0 0 0 1 $args]

            # register modulefiles to unload as individual conflicts
            foreach arg $args {
               # do not break on error yet, go through the whole modfile
               # evaluation in case conflict is solved later on
               catch {conflict $arg}
            }
            # sub-module interpretation failed, raise error
            if {$ret} {
               knerror {} MODULES_ERR_SUBFAILED
            }
         }
      }
      reload {
         cmdModuleReload
      }
      use {
         if {$topcall || $mode eq {load}} {
            eval cmdModuleUse $addpath_pos $args
         } else {
            eval cmdModuleUnuse $args
         }
      }
      unuse {
         eval cmdModuleUnuse $args
      }
      source {
         if {$topcall || $mode eq {load}} {
            eval cmdModuleSource $args
         } else {
            # on unload mode, unsource script in reverse order
            eval cmdModuleUnsource [lreverse $args]
         }
      }
      switch {
         # pass 'user asked state' to switch procedure
         set uasked [expr {$topcall || ([depthState modulename] == 1 &&\
            ([aboveCommandName] eq {source} || [aboveCommandName] eq\
            {autoinit} || [isStateDefined rc_running]))}]
         if {$uasked} {
            eval cmdModuleSwitch $uasked $args
         } else {
            # CAUTION: it is not recommended to use the `switch`
            # sub-command in modulefiles as this command is intended for
            # the command-line for a 2in1 operation. Could be removed from
            # the modulefile scope in a future release. Use `module unload`
            # and `module load` commands in modulefiles instead.

            switch -- $mode {
               load {
                  eval cmdModuleSwitch $uasked $args
               }
               unload {
                  # find what has been asked for unload and load
                  lassign $args swunmod swlomod
                  if {$swlomod eq {} && $swunmod ne {}} {
                     set swlomod $swunmod
                  }

                  # apply same mechanisms than for 'module load' and
                  # 'module unload' for an unload evaluation: nothing done
                  # for switched-off module and unload of switched-on
                  # module. If auto handling is enabled switched-on module
                  # is handled via UReqUn mechanism (unless if
                  # implicit_requirement has been inhibited). Also unloads are
                  # triggered by ongoing reload, purge or restore commands
                  if {(![getConf auto_handling] || [getState\
                     inhibit_req_record] eq [currentState evalid]) &&\
                     $swlomod ne {} && [notInList [list purge reload restore]\
                     [aboveCommandName]]} {
                     # unload mod if it was loaded prior this mod, not user
                     # asked and not required by another loaded module
                     set modlist [getLoadedModuleList]
                     set modidx [lsearch -exact $modlist [currentState\
                        modulename]]
                     if {$modidx != 0} {
                        set priormodlist [lrange $modlist 0 $modidx]
                        if {[set unmod [getLoadedMatchingName $swlomod {} 0\
                           $priormodlist]] ne {}} {
                           if {[cmdModuleUnload urequn match 1 0 1 1 $unmod]} {
                              reportWarning "Unload of useless requirement\
                                 $unmod failed" 1
                           }
                        }
                     }
                  }
               }
            }
         }
      }
      display {
         eval cmdModuleDisplay $args
      }
      avail {
         eval cmdModuleAvail $show_oneperline $show_mtime "{$show_filter}"\
            "{$search_filter}" "{$search_match}" $args
      }
      aliases {
         cmdModuleAliases
      }
      path {
         eval cmdModulePath $args
      }
      paths {
         eval cmdModulePaths $args
      }
      list {
         cmdModuleList $show_oneperline $show_mtime
      }
      whatis {
         if {$args ne {}} {
            foreach arg $args {
               cmdModuleWhatIs $arg
            }
         } else {
            cmdModuleWhatIs
         }
      }
      search {
         eval cmdModuleApropos $args
      }
      purge {
         eval cmdModulePurge
      }
      save {
         eval cmdModuleSave $args
      }
      restore {
         eval cmdModuleRestore $args
      }
      saverm {
         eval cmdModuleSaverm $args
      }
      saveshow {
         eval cmdModuleSaveshow $args
      }
      savelist {
         cmdModuleSavelist $show_oneperline $show_mtime
      }
      initadd {
         eval cmdModuleInit add $args
      }
      initprepend {
         eval cmdModuleInit prepend $args
      }
      initswitch {
         eval cmdModuleInit switch $args
      }
      initrm {
         eval cmdModuleInit rm $args
      }
      initlist {
         eval cmdModuleInit list $args
      }
      initclear {
         eval cmdModuleInit clear $args
      }
      autoinit {
         cmdModuleAutoinit
      }
      clear {
         # ensure empty string is correctly passed
         eval cmdModuleClear "{$args}"
      }
      config {
         eval cmdModuleConfig $dump_state $args
      }
      sh-to-mod {
         eval cmdModuleShToMod $args
      }
      help {
         eval cmdModuleHelp $args
      }
      test {
         eval cmdModuleTest $args
      }
      prepend-path - append-path - remove-path - is-loaded - is-saved -\
      is-used - is-avail {
         eval cmdModuleResurface $command $args
      }
      info-loaded {
         eval cmdModuleResurface module-info loaded $args
      }
   }
   lpopState commandname
   lpopState always_read_full_file

   if {!$topcall && ($not_req || ![getConf implicit_requirement])} {
      lpopState inhibit_req_record
   }

   # if called from top level render settings if any
   if {$topcall} {
      renderSettings
   }

   return {}
}

proc ml {args} {
   # filter out all known options from argument list to guess command name
   # without them in the way
   lassign [eval parseModuleCommandArgs 1 ml 1 $args] show_oneperline\
      show_mtime show_filter search_filter search_match dump_state\
      addpath_pos not_req fargs

   # determine if first argument is a known module sub-command
   lassign [parseModuleCommandName [lindex $fargs 0] list] command cmdvalid\
      cmdempty

   if {$cmdempty} {
      # consider empty string supplied as first argument as module name
      if {[llength $fargs] > 0} {
         set cmdvalid 0
      }
      set margs $args
   } else {
      # first argument was command name
      set margs [lrange $args 1 end]
   }

   # directly call module procedure if sub-command spotted as first argument
   # or no argument supplied
   if {$cmdvalid} {
      eval module $command $margs
   } else {
      # parse specified module and get list of mods to unload and mods to load
      lassign [eval parseModuleVersionSpecifier 1 $fargs] modunlist modlolist

      # main procedure has already raised error for badly written argument
      # like '-' or '--', but we need here to replay module-specific argument
      # parsing to raise error if some arg are not allowed on unload/load cmd
      set mlcmd [expr {[llength $modunlist] > 0 ? {unload} : {load}}]
      lassign [eval parseModuleCommandArgs 1 $mlcmd 0 $args] show_oneperline\
         show_mtime show_filter search_filter search_match dump_state\
         addpath_pos not_req fargs

      # define if modfile should always be fully read even for validity check
      lappendState always_read_full_file 1
      lappendState commandname ml

      # Find and execute any global rc file found
      runModulerc

      set ret 0
      pushSettings

      # first unload specified modules
      if {[llength $modunlist] > 0} {
         set ret [eval cmdModuleUnload unload match 1 0 0 0 $modunlist]
      }
      # then load other modules unless unload phase failed
      if {!$ret && [llength $modlolist] > 0} {
         set ret [eval cmdModuleLoad load 1 $modlolist]
      }

      # rollback changes if any load or unload failed
      if {$ret} {
         restoreSettings
      }
      popSettings

      lpopState commandname
      lpopState always_read_full_file

      renderSettings
   }

   return {}
}

proc getModshareVarName {var} {
   # specific modshare variable for DYLD-related variables as a suffixed
   # variable will lead to warning messages with this tool
   if {[string range $var 0 4] eq {DYLD_}} {
      return MODULES_MODSHARE_${var}
   } else {
      return ${var}_modshare
   }
}

proc setenv {var val} {
   reportDebug "var='$var', val='$val'"

   # clean any previously defined reference counter array
   unset-env [getModshareVarName $var] 1

   # Set the variable for later use during the modulefile evaluation
   set-env $var $val

   return {}
}

# undo setenv in unload mode
proc setenv-un {var val} {
   reportDebug "var='$var', val='$val'"

   # clean any existing reference counter array
   unset-env [getModshareVarName $var] 1

   # Add variable to the list of variable to unset in shell output code but
   # set it in interp context as done on load mode for later use during the
   # modulefile evaluation
   unset-env $var 0 $val

   return {}
}

# optimized setenv/unsetenv for whatis mode: init env variable with an empty
# value if undefined. do not care about value, just avoid variable to be
# undefined for later use during the modulefile evaluation
proc setenv-wh {var args} {
   if {![info exists ::env($var)]} {
      reportDebug "var='$var', val=''"
      set ::env($var) {}
   }
   return {}
}

proc getenv {var {valifundef _UNDEFINED_}} {
   reportDebug "var='$var', valifundef='$valifundef'"

   if {[currentState mode] ne {display}} {
      return [get-env $var $valifundef]
   } else {
      return "\$$var"
   }
}

proc unsetenv {var {val {}}} {
   reportDebug "var='$var', val='$val'"

   # clean any existing reference counter array
   unset-env [getModshareVarName $var] 1

   # Set the variable for later use during the modulefile evaluation
   unset-env $var

   return {}
}

# undo unsetenv in unload mode
proc unsetenv-un {var {val {}}} {
   if {$val ne {}} {
      return [setenv $var $val]
   } else {
      return [unsetenv $var]
   }
}

proc chdir {dir} {
   reportDebug $dir

   if {[file exists $dir] && [file isdirectory $dir]} {
      set ::g_changeDir $dir
   } else {
      # report issue but does not treat it as an error to have the
      # same behavior as C-version
      reportWarning "Cannot chdir to '$dir' for '[currentState modulename]'"
   }

   return {}
}

# superseed exit command to handle it if called within a modulefile
# rather than exiting the whole process
proc exitModfileCmd {{code 0}} {
   reportDebug ($code)

   if {[currentState mode] eq {load}} {
      setState inhibit_interp 1
   }

   # break to gently end interpretation of current modulefile
   return -code break
}

# enables sub interp to return ModulesVersion value to the main interp
proc setModulesVersion {val} {
   set ::ModulesVersion $val
}

# supersede puts command to catch content sent to stdout/stderr within
# modulefile in order to correctly send stderr content (if a pager has been
# enabled) or postpone content channel send after rendering on stdout the
# relative environment changes required by the modulefile
proc putsModfileCmd {itrp args} {
   reportDebug "$args (itrp=$itrp)"

   # determine if puts call targets the stdout or stderr channel
   switch -- [llength $args] {
      1 {
         set deferPuts 1
      }
      2 {
         switch -- [lindex $args 0] {
            -nonewline - stdout {
               set deferPuts 1
            }
            stderr {
               set reportArgs [list [lindex $args 1]]
            }
         }
      }
      3 {
         if {[lindex $args 0] eq {-nonewline}} {
            switch -- [lindex $args 1] {
               stdout {
                  set deferPuts 1
               }
               stderr {
                  set reportArgs [list [lindex $args 2] 1]
               }
            }
         } else {
            set wrongNumArgs 1
         }
      }
      default {
         set wrongNumArgs 1
      }
   }

   # raise error if bad argument number detected, do this here rather in _puts
   # not to confuse people with an error reported by an internal name (_puts)
   if {[info exists wrongNumArgs]} {
      knerror {wrong # args: should be "puts ?-nonewline? ?channelId? string"}
   # defer puts if it targets stdout (see renderSettings)
   } elseif {[info exists deferPuts]} {
      lappend ::g_stdoutPuts $args
   # if it targets stderr call report, which knows what channel to use
   } elseif {[info exists reportArgs]} {
      # report message only if not silent
      if {[isVerbosityLevel concise]} {
         eval report $reportArgs
      }
   # pass to real puts command if not related to stdout and do that in modfile
   # interpreter context to get access to eventual specific channel
   } else {
      # re-throw error as a known error for accurate stack trace print
      if {[catch {$itrp eval _puts $args} errMsg]} {
         knerror $errMsg MODULES_ERR_CUSTOM
      }
   }
}

########################################################################
# path fiddling
#
proc getReferenceCountArray {var separator} {
   # get reference counter set in environment
   set sharevar [getModshareVarName $var]
   array set refcount {}
   if {[info exists ::env($sharevar)]} {
      set modsharelist [psplit $::env($sharevar) [getState path_separator]]
      # ignore environment ref count variable if malformed
      if {([llength $modsharelist] % 2) == 0} {
         array set refcount $modsharelist
      } else {
         reportDebug "Reference counter value in '$sharevar' is malformed\
            ($modsharelist)"
      }
   }

   array set countarr {}
   if {[info exists ::env($var)]} {
      # do not skip a bare empty path entry that can also be found in
      # reference counter array (sometimes var is cleared by setting it
      # empty not unsetting it, ignore var in this case)
      if {$::env($var) eq {} && [info exists refcount()]} {
         lappend eltlist {}
      } else {
         set eltlist [split $::env($var) $separator]
      }

      # just go thought the elements of the variable, which means additional
      # elements part of the reference counter variable will be ignored
      foreach elt $eltlist {
         # no reference counter, means value has been set once
         if {![info exists refcount($elt)]} {
            set count 1
         # bad reference counter value is ignored
         } elseif {![string is digit -strict $refcount($elt)]} {
            reportDebug "Reference counter value for '$elt' in '$sharevar' is\
               erroneous ($refcount($elt))"
            set count 1
         } else {
            set count $refcount($elt)
         }
         set countarr($elt) $count
      }
   }

   set count_list [array get countarr]
   reportDebug "(var=$var, delim=$separator) got '$count_list'"

   return $count_list
}


proc unload-path {args} {
   reportDebug ($args)

   lassign [eval parsePathCommandArgs unload-path $args] separator\
      allow_dup idx_val var path_list

   array set countarr [getReferenceCountArray $var $separator]

   # Don't worry about dealing with this variable if it is already scheduled
   #  for deletion
   if {[info exists ::g_stateEnvVars($var)] && $::g_stateEnvVars($var) eq\
      {del}} {
      return {}
   }

   # save initial variable content to match index arguments
   set dir_list [split [get-env $var] $separator]
   # detect if empty env value means empty path entry
   if {[llength $dir_list] == 0 && [info exists countarr()]} {
      lappend dir_list {}
   }

   # build list of index to remove from variable
   set del_idx_list [list]
   foreach dir $path_list {
      # retrieve dir value if working on an index list
      if {$idx_val} {
         set idx $dir
         # go to next index if this one is not part of the existing range
         # needed to distinguish an empty value to an out-of-bound value
         if {$idx < 0 || $idx >= [llength $dir_list]} {
            continue
         } else {
            set dir [lindex $dir_list $idx]
         }
      }

      # update reference counter array
      if {[info exists countarr($dir)]} {
         incr countarr($dir) -1
         set newcount $countarr($dir)
         if {$countarr($dir) <= 0} {
            unset countarr($dir)
         }
      } else {
         set newcount 0
      }

      # get all entry indexes corresponding to dir
      set found_idx_list [lsearch -all -exact $dir_list $dir]

      # remove all found entries
      if {$newcount <= 0} {
         # only remove passed position in --index mode
         if {$idx_val} {
            lappend del_idx_list $idx
         } else {
            set del_idx_list [concat $del_idx_list $found_idx_list]
         }
      # if multiple entries found remove the extra entries compared to new
      # reference counter
      } elseif {[llength $found_idx_list] > $newcount} {
         # only remove passed position in --index mode
         if {$idx_val} {
            lappend del_idx_list $idx
         } else {
            # delete extra entries, starting from end of the list (on a path
            # variable, entries at the end have less priority than those at
            # the start)
            set del_idx_list [concat $del_idx_list [lrange $found_idx_list\
               $newcount end]]
         }
      }
   }

   # update variable if some element need to be removed
   if {[llength $del_idx_list] > 0} {
      set del_idx_list [lsort -integer -unique $del_idx_list]
      set newpath [list]
      set nbelem [llength $dir_list]
      # rebuild list of element without indexes set for deletion
      for {set i 0} {$i < $nbelem} {incr i} {
         if {[notInList $del_idx_list $i]} {
            lappend newpath [lindex $dir_list $i]
         }
      }
   } else {
      set newpath $dir_list
   }

   # set env variable and corresponding reference counter in any case
   if {[llength $newpath] == 0} {
      unset-env $var
   } else {
      set-env $var [join $newpath $separator]
   }

   set sharevar [getModshareVarName $var]
   if {[array size countarr] > 0} {
      set-env $sharevar [pjoin [array get countarr] [getState path_separator]]
   } else {
      unset-env $sharevar 1
   }
   return {}
}

proc add-path {pos args} {
   reportDebug "($args) pos=$pos"

   lassign [eval parsePathCommandArgs add-path $args] separator allow_dup\
      idx_val var path_list

   set sharevar [getModshareVarName $var]
   array set countarr [getReferenceCountArray $var $separator]

   if {$pos eq {prepend}} {
      set path_list [lreverse $path_list]
   }

   set val [get-env $var]

   foreach dir $path_list {
      if {![info exists countarr($dir)] || $allow_dup} {
         # ignore env var set empty if no empty entry found in reference
         # counter array (sometimes var is cleared by setting it empty not
         # unsetting it)
         if {$val ne {} || [info exists countarr()]} {
            set val [expr {$pos eq {prepend} ? "$dir$separator$val" :\
               "$val$separator$dir"}]
         } else {
            set val $dir
         }
      }
      if {[info exists countarr($dir)]} {
         incr countarr($dir)
      } else {
         set countarr($dir) 1
      }
   }

   set-env $var $val
   set-env $sharevar [pjoin [array get countarr] [getState path_separator]]

   return {}
}

# analyze argument list passed to a path command to set default value or raise
# error in case some attributes are missing
proc parsePathCommandArgs {cmd args} {
   # parse argument list
   set next_is_delim 0
   set allow_dup 0
   set idx_val 0
   foreach arg $args {
      # everything passed after variable name is considered a value
      if {[info exists var]} {
         # set multiple passed values in a list
         lappend val_raw_list $arg
      } else {
         switch -glob -- $arg {
            --index {
               if {$cmd eq {add-path}} {
                  reportWarning "--index option has no effect on $cmd"
               } else {
                  set idx_val 1
               }
            }
            --duplicates {
               if {$cmd eq {unload-path}} {
                  reportWarning "--duplicates option has no effect on $cmd"
               } else {
                  set allow_dup 1
               }
            }
            -d - -delim - --delim {
               set next_is_delim 1
            }
            --delim=* {
               set delim [string range $arg 8 end]
            }
            -* {
               knerror "invalid option '$arg' for $cmd"
            }
            default {
               if {$next_is_delim} {
                  set delim $arg
                  set next_is_delim 0
               } else {
                  set var $arg
               }
            }
         }
      }
   }

   # adapt with default value or raise error if some arguments are missing
   if {![info exists delim]} {
      set delim [getState path_separator]
   } elseif {$delim eq {}} {
      knerror "$cmd should get a non-empty path delimiter"
   }
   if {![info exists var]} {
      knerror "$cmd should get an environment variable name"
   } elseif {$var eq {}} {
      knerror "$cmd should get a valid environment variable name"
   }
   if {![info exists val_raw_list]} {
      knerror "$cmd should get a value for environment variable $var"
   }

   # set list of value to add
   set val_list [list]
   foreach val $val_raw_list {
      # check passed indexes are numbers
      if {$idx_val && ![string is integer -strict $val]} {
         knerror "$cmd should get valid number as index value"
      }

      switch -- $val \
         {} {
            # add empty entry in list
            lappend val_list {}
         } \
         $delim {
            knerror "$cmd cannot handle path equals to separator string"
         } \
         default {
            # split passed value with delimiter
            set val_list [concat $val_list [split $val $delim]]
         }
   }

   reportDebug "(delim=$delim, allow_dup=$allow_dup, idx_val=$idx_val,\
      var=$var, val=$val_list, nbval=[llength $val_list])"

   return [list $delim $allow_dup $idx_val $var $val_list]
}

proc prepend-path {args} {
   reportDebug $args

   # Set the variable for later use during the modulefile evaluation
   eval add-path prepend $args

   return {}
}

proc append-path {args} {
   reportDebug $args

   # Set the variable for later use during the modulefile evaluation
   eval add-path append $args

   return {}
}

proc remove-path {args} {
   reportDebug $args

   # Set the variable for later use during the modulefile evaluation
   eval unload-path $args

   return {}
}

# undo remove-path in unload mode
proc remove-path-un {args} {
   # clear variable if it does not exist on unload mode for later use
   # during the modulefile evaluation
   lassign [eval parsePathCommandArgs unload-path $args] separator\
      allow_dup idx_val var path_list
   if {![info exists ::env($var)]} {
      reset-to-unset-env $var
   }
}

# optimized *-path for whatis mode: init env variable with an empty value if
# undefined. do not care about value, just avoid variable to be undefined for
# later use during the modulefile evaluation
proc edit-path-wh {args} {
   reportDebug $args

   # get variable name
   lassign [eval parsePathCommandArgs edit-path-wh $args] separator\
      allow_dup idx_val var path_list

   if {![info exists ::env($var)]} {
      set ::env($var) {}
   }

   return {}
}

proc set-alias {alias what} {
   reportDebug "alias='$alias', val='$what'"

   set ::g_Aliases($alias) $what
   set ::g_stateAliases($alias) new

   return {}
}

# undo set-alias in unload mode
proc set-alias-un {alias what} {
   return [unset-alias $alias]
}

proc unset-alias {alias} {
   reportDebug alias='$alias'

   set ::g_Aliases($alias) {}
   set ::g_stateAliases($alias) del

   return {}
}

proc set-function {function what} {
   reportDebug "function='$function', val='$what'"

   set ::g_Functions($function) $what
   set ::g_stateFunctions($function) new

   return {}
}

# undo set-function in unload mode
proc set-function-un {function what} {
   return [unset-function $function]
}

proc unset-function {function} {
   reportDebug function='$function'

   set ::g_Functions($function) {}
   set ::g_stateFunctions($function) del

   return {}
}


proc is-loaded {args} {
   reportDebug $args
   # parse module version specification
   set args [eval parseModuleVersionSpecifier 0 $args]

   foreach mod $args {
      if {[getLoadedMatchingName $mod returnfirst] ne {}} {
         return 1
      }
   }
   # is something loaded whatever it is?
   return [expr {[llength $args] == 0 && [llength [getLoadedModuleList]] > 0}]
}

proc is-loading {args} {
   reportDebug $args

   foreach mod $args {
      if {[getLoadedMatchingName $mod returnfirst 1] ne {}} {
         return 1
      }
   }
   # is something else loading whatever it is?
   return [expr {[llength $args] == 0 && [llength [getLoadingModuleList]] >1}]
}

proc conflict {args} {
   reportDebug $args
   set currentModule [currentState modulename]
   # get module short name if loaded by its full pathname
   if {[set isfullpath [isModuleFullPath $currentModule]]} {
      set currentSModule [findModuleNameFromModulefile $currentModule]
   }
   defineModEqProc [isIcase] [getConf extended_default]

   # parse module version specification
   set args [eval parseModuleVersionSpecifier 0 $args]

   # register conflict list unless record inhibited for current iterp context
   if {[currentState inhibit_req_record] != [currentState evalid]} {
      eval setLoadedConflict "{$currentModule}" $args
   }

   foreach mod $args {
      # if the conflict module is loading and it does not correspond to
      # currently evaluated module, we cannot proceed
      set isloading [expr {![doesModuleMatchesName $currentModule $mod] &&\
         (!$isfullpath || ![doesModuleMatchesName $currentSModule $mod]) &&\
         [is-loading $mod]}]
      # if the conflicting module is loaded, we cannot either
      if {[is-loaded $mod] || $isloading} {
         set retisconun [isModuleEvaluated conun $currentModule $mod]
         # report message on currently evaluated module message block
         if {![set retiseval [isModuleEvaluated any $currentModule $mod]] ||\
            [currentState msgrecordid] ne [topState msgrecordid] ||\
            !$retisconun} {
            # more appropriate msg if an eval was attempted or is by-passed
            set msg [expr {$retiseval || [getState force] ?\
               [getConIsLoadedMsg [list $mod] $isloading] :\
               [getErrConflictMsg $currentModule $mod]}]

            # still proceed if force mode enabled
            if {[getState force]} {
               reportWarning $msg
               # indicate message has already been reported
               lappend ::report_conflict($currentModule) $mod
            } else {
               knerror $msg MODULES_ERR_GLOBAL
            }
         }
      }
   }

   return {}
}

proc prereq {args} {
   reportDebug $args
   set currentModule [currentState modulename]

   # parse module version specification
   set args [eval parseModuleVersionSpecifier 0 $args]

   # register prereq list (sets of optional prereq are registered as list)
   # unless record inhibited for current iterp context
   if {[currentState inhibit_req_record] != [currentState evalid]} {
      setLoadedPrereq $currentModule $args
   }

   # if dependency resolving is enabled try to load prereq
   if {[getConf auto_handling] && ![eval is-loaded $args] && ![eval\
      is-loading $args]} {
      set imax [llength $args]
      set prereqloaded 0
      # if prereq list specified, try to load first then
      # try next if load of first module not successful
      for {set i 0} {$i<$imax && $prereqloaded==0} {incr i 1} {
         set arg [lindex $args $i]

         # hold output of each evaluation until they are all done to drop
         # those that failed if one succeed
         set curholdid load-$i-$arg
         lappendState reportholdid $curholdid
         if {[catch {cmdModuleLoad reqlo 0 $arg} errorMsg]} {
            # if an error is raised, release output and rethrow the error
            # (could be raised if no modulepath defined for instance)
            lpopState reportholdid
            lappend holdidlist $curholdid report
            eval releaseHeldReport $holdidlist
            knerror $errorMsg
         }
         lpopState reportholdid

         if {[is-loaded $arg]} {
            set prereqloaded 1
            # set previous reports to be dropped as this one succeed
            if {[info exists holdidlist]} {
               foreach {holdid action} $holdidlist {
                  lappend newholdidlist $holdid drop
               }
               set holdidlist $newholdidlist
            }
         }
         lappend holdidlist $curholdid report
      }
      # output held messages
      eval releaseHeldReport $holdidlist
   }

   if {![eval is-loaded $args] && ![eval is-loading $args]} {
      set retisreqlo [eval isModuleEvaluated reqlo "{$currentModule}" $args]
      # report message on currently evaluated module message block
      if {![set retiseval [eval isModuleEvaluated any "{$currentModule}"\
         $args]] || [currentState msgrecordid] ne [topState msgrecordid] ||\
         !$retisreqlo} {

         # more appropriate msg if an evaluation was attempted or is by-passed
         set msg [expr {$retiseval || [getState force] ? [getReqNotLoadedMsg\
            $args] : [getErrPrereqMsg $currentModule $args]}]
         # still proceed if force mode enabled
         if {[getState force]} {
            reportWarning $msg
         # no error raise if done later
         } elseif {$retisreqlo} {
            reportError $msg
         } else {
            knerror $msg MODULES_ERR_GLOBAL
         }
      }

      # raise reqlo-specific msg to top level if attempted
      if {$retisreqlo} {
         set msg [getErrReqLoMsg $args]
         if {[getState force]} {
            reportWarning $msg 1
         } else {
            knerror $msg MODULES_ERR_GLOBALTOP
         }
      }
   }

   return {}
}

proc x-resource {resource {value {}}} {
   reportDebug "($resource, $value)"

   # sometimes x-resource value may be provided within resource name
   # as the "x-resource {Ileaf.popup.saveUnder: True}" example provided
   # in manpage. so here is an attempt to extract real resource name and
   # value from resource argument
   if {[string length $value] == 0 && ![file exists $resource]} {
      # look first for a space character as delimiter, then for a colon
      set sepapos [string first { } $resource]
      if { $sepapos == -1 } {
         set sepapos [string first : $resource]
      }

      if { $sepapos > -1 } {
         set value [string range $resource [expr {$sepapos + 1}] end]
         set resource [string range $resource 0 [expr {$sepapos - 1}]]
         reportDebug "corrected ($resource, $value)"
      } else {
         # if not a file and no value provided x-resource cannot be
         # recorded as it will produce an error when passed to xrdb
         reportWarning "x-resource $resource is not a valid string or file"
         return {}
      }
   }

   # check current environment can handle X11 resource edition elsewhere exit
   if {[catch {runCommand xrdb -query} errMsg]} {
      knerror "X11 resources cannot be edited, issue spotted\n[sgr er\
         ERROR]: $errMsg" MODULES_ERR_GLOBAL
   }

   # if a resource does hold an empty value in g_newXResources or
   # g_delXResources arrays, it means this is a resource file to parse
   if {[currentState mode] eq {load}} {
      set ::g_newXResources($resource) $value
   } else {
      set ::g_delXResources($resource) $value
   }

   return {}
}

proc uname {what} {
   reportDebug $what

   return [switch -- $what {
      sysname {getState os}
      machine {getState machine}
      nodename - node {getState nodename}
      release {getState osversion}
      domain {getState domainname}
      version {getState kernelversion}
      default {knerror "uname $what not supported"}
   }]
}

# run shell command
proc system {args} {
   reportDebug $args

   set mode [currentState mode]
   set status {}

   switch -- $mode {
      load - unload {
         # run through the appropriate shell
         if {[getState is_win]} {
            set shell cmd.exe
            set shellarg /c
         } else {
            set shell /bin/sh
            set shellarg -c
         }

         if {[catch {exec >&@stderr $shell $shellarg [join $args]}]} {
             # non-zero exit status, get it:
             set status [lindex $::errorCode 2]
         } else {
             # exit status was 0
             set status 0
         }
      }
   }

   return $status
}

# test at least one of the collections passed as argument exists
proc is-saved {args} {
   reportDebug $args

   foreach coll $args {
      lassign [getCollectionFilename $coll] collfile colldesc
      if {[file exists $collfile]} {
         return 1
      }
   }
   # is something saved whatever it is?
   return [expr {[llength $args] == 0 && [llength [findCollections]] > 0}]
}

# test at least one of the directories passed as argument is set in MODULEPATH
proc is-used {args} {
   reportDebug $args

   set modpathlist [getModulePathList]
   foreach path $args {
      # transform given path in an absolute path to compare with dirs
      # registered in the MODULEPATH env var which are returned absolute.
      set abspath [getAbsolutePath $path]
      if {[isInList $modpathlist $abspath]} {
         return 1
      }
   }
   # is something used whatever it is?
   return [expr {[llength $args] == 0 && [llength $modpathlist] > 0}]
}

# test at least one of the modulefiles passed as argument exists
proc is-avail {args} {
   reportDebug $args
   # parse module version specification
   set args [eval parseModuleVersionSpecifier 0 $args]
   set ret 0

   # disable error reporting to avoid modulefile errors
   # to pollute result. Only if not already inhibited
   set alreadyinhibit [getState inhibit_errreport]
   if {!$alreadyinhibit} {
      inhibitErrorReport
   }

   foreach mod $args {
      lassign [getPathToModule $mod] modfile modname
      if {$modfile ne {}} {
         set ret 1
         break
      }
   }

   # re-enable only is it was disabled from this procedure
   if {!$alreadyinhibit} {
      setState inhibit_errreport 0
   }
   return $ret
}

proc execShAndGetEnv {shell script args} {
   set sep {%ModulesShToMod%}
   set shdesc [concat [list $script] $args]
   set sherr 0
   set shellopts [list]

   upvar ignvarlist ignvarlist
   set ignvarlist [list OLDPWD PWD _ _AST_FEATURES PS1]

   # define shell command to run to source script and analyze the environment
   # changes it performs
   switch -- [file tail $shell] {
      dash - sh {
         # declare is not supported by dash but functions cannot be retrieved
         # anyway, so keep using declare and throw errors out to avoid overall
         # execution error. dash does not pass arguments to sourced script but
         # it does not raise error if arguments are set
         set command "export -p; echo $sep; declare -f 2>/dev/null; echo\
            $sep; alias; echo $sep; pwd; echo $sep; . [listTo shell $shdesc]\
            2>&1; echo $sep; export -p; echo $sep; declare -f 2>/dev/null;\
            echo $sep; alias; echo $sep; pwd"
         set varre {export (\S+?)=["']?(.*?)["']?$}
         set funcre {(\S+?) \(\)\s?\n?{\s?\n(.+?)\n}$}
         set aliasre {(\S+?)='(.*?)'$}
         set varvalmap [list {\"} {"} \\\\ \\]
         set alvalmap [list {'\''} ' {'"'"'} ']
      }
      bash {
         set command "export -p; echo $sep; declare -f; echo $sep; alias;\
            echo $sep; pwd; echo $sep; . [listTo shell $shdesc] 2>&1; echo\
            $sep; export -p; echo $sep; declare -f; echo $sep; alias; echo\
            $sep; pwd"
         set varre {declare -x (\S+?)="(.*?)"$}
         set funcre {(\S+?) \(\)\s?\n{\s?\n(.+?)\n}$}
         set aliasre {alias (\S+?)='(.*?)'$}
         set varvalmap [list {\"} {"} \\\\ \\]
         set alvalmap [list {'\''} ']
         lappend shellopts --noprofile --norc
      }
      ksh - ksh93 {
         set command "typeset -x; echo $sep; typeset +f | while read f; do\
            typeset -f \${f%\\(\\)}; echo; done; echo $sep; alias; echo $sep;\
            pwd; echo $sep; . [listTo shell $shdesc] 2>&1; echo $sep; typeset\
            -x; echo $sep; typeset +f | while read f; do typeset -f\
            \${f%\\(\\)}; echo; done; echo $sep; alias; echo $sep; pwd"
         set varre {(\S+?)=\$?'?(.*?)'?$}
         set funcre {(\S+?)\(\) {\n?(.+?)}[;\n]?$}
         set aliasre {(\S+?)=\$?'?(.*?)'?$}
         set varvalmap [list {\'} ']
         set alvalmap [list {\"} {"} {\\'} ' {\'} ' {\\\\} {\\}]
      }
      zsh {
         set command "typeset -x; echo $sep; declare -f; echo $sep; alias;\
            echo $sep; pwd; echo $sep; . [listTo shell $shdesc] 2>&1; echo\
            $sep; typeset -x; echo $sep; declare -f; echo $sep; alias; echo\
            $sep; pwd"
         set varre {(\S+?)=\$?'?(.*?)'?$}
         set funcre {(\S+?) \(\) {\n(.+?)\n}$}
         set aliasre {(\S+?)=\$?'?(.*?)'?$}
         set varvalmap [list {'\''} ']
         set alvalmap [list {'\''} ']
      }
      csh - tcsh {
         set command "setenv; echo $sep; echo $sep; alias; echo $sep; pwd;\
            echo $sep; source [listTo shell $shdesc] >& /dev/stdout; echo\
            $sep; setenv; echo $sep; echo $sep; alias; echo $sep; pwd"
         set varre {(\S+?)=(.*?)$}
         set aliasre {(\S+?)\t(.*?)$}
         set varvalmap [list]
         set alvalmap [list]
         lappend shellopts -f
      }
      fish {
         # exclude builtins and fish-specific functions from search to reduce
         # the number of functions to parse
         set getfunc {set funcout (string match -r -v $funcfilter (functions\
            -n) | while read f; functions $f; end);}
         set command "set -xgL; echo '$sep'; set funcfilter (string join '|'\
            (string replace \\\[ \\\\\\\[ (builtin -n)))\\|fish\\.\\*;\
            $getfunc; $getfunc; string split \$funcout; echo '$sep'; string\
            split \$funcout; echo '$sep'; pwd; echo '$sep'; source [listTo\
            shell $shdesc] 2>&1; or exit \$status; echo '$sep'; set -xgL;\
            echo '$sep'; $getfunc; string split \$funcout; echo '$sep';\
            string split \$funcout; echo '$sep'; pwd"
         set varre {^(\S+?\M) ?'?(.*?)'?$}
         # exclude alias from function list
         set funcre {^function (\S+?)(?: [^\n]*?--description\
            (?!'?alias)[^\n]+)?\n(.+?)\nend$}
         # fetch aliases from available functions
         set aliasre {^function (\S+?) [^\n]*?--description\
            '?alias[^\n]+\n\s*(.+?)\nend$}
         # translate back fish-specific code
         set varvalmap [list {'  '} : {\'} ' {\"} \" \\\\ \\]
         set alvalmap [list { $argv;} {}]

         # fish builtins change LS_COLORS variable
         lappend ignvarlist LS_COLORS
      }
      default {
         knerror "Shell '$shell' not supported"
      }
   }

   if {![file exists $script]} {
      knerror "Script '$script' cannot be found"
   }

   set shellpath [getCommandPath $shell]
   if {$shellpath eq {}} {
      knerror "Shell '$shell' cannot be found"
   }
   set shellexec [concat [list $shellpath] $shellopts [list -c $command]]

   reportDebug "running '$shellexec'"
   if {[catch {set output [eval exec $shellexec]} output]} {
      set sherr 1
   }

   # link result variables to calling context
   upvar cwdbefout cwdbefout cwdaftout cwdaftout

   # extract each output sections
   set idx 0
   foreach varout {varbefout funcbefout aliasbefout cwdbefout scriptout\
      varaftout funcaftout aliasaftout cwdaftout} {
      if {[set sepidx [string first $sep $output $idx]] == -1} {
         set $varout [string trimright [string range $output $idx end] \n]
         if {$varout ne {cwdaftout} && !$sherr} {
            knerror "Unexpected output when sourcing '$shdesc' in shell\
               '$shell'"
         }
      } else {
         set $varout [string trimright [string range $output $idx [expr\
            {$sepidx - 1}]] \n]
         set idx [expr {$sepidx + [string length $sep] + 1}]
      }
      # remove expected Tcl error message
      if {$sherr && $varout eq {scriptout} && [set erridx [string\
         last {child process exited abnormally} [set $varout]]] != -1} {
         set $varout [string range [set $varout] 0 [expr {$erridx - 2}]]
      }
   }
   if {$sepidx != -1 && !$sherr} {
      knerror "Unexpected output when sourcing '$shdesc' in shell '$shell'"
   }

   reportDebug "script output is '$scriptout'"
   if {$sherr} {
      # throw error if script had an issue, send script output along if any
      set errmsg "Script '$script' exited abnormally"
      if {$scriptout ne {}} {
         append errmsg "\n  with following output\n$scriptout"
      }
      knerror $errmsg
   }

   # link result variables to calling context
   upvar varbef varbef varaft varaft
   upvar funcbef funcbef funcaft funcaft
   upvar aliasbef aliasbef aliasaft aliasaft

   # extract environment variable information
   foreach {out arr} [list varbefout varbef varaftout varaft] {
      foreach {match name value} [regexp -all -inline -lineanchor $varre [set\
         $out]] {
         # convert shell-specific escaping
         set ${arr}($name) [string map $varvalmap $value]
      }
   }
   # extract function information if function supported by shell
   if {[info exists funcre]} {
      foreach {out arr} [list funcbefout funcbef funcaftout funcaft] {
         foreach {match name value} [regexp -all -inline -lineanchor $funcre\
            [set $out]] {
            # no specific escaping to convert for functions
            set ${arr}($name) $value
         }
      }
   }
   # extract alias information
   foreach {out arr} [list aliasbefout aliasbef aliasaftout aliasaft] {
      foreach {match name value} [regexp -all -inline -lineanchor $aliasre\
         [set $out]] {
         set ${arr}($name) [string map $alvalmap $value]
      }
   }
}

# execute script with args through shell and convert environment changes into
# corresponding modulefile commands
proc sh-to-mod {args} {
   set modcontent [list]
   set pathsep [getState path_separator]

   # evaluate script and retrieve environment before and after evaluation
   # procedure will set result variables in current context
   eval execShAndGetEnv $args

   # check environment variable change
   lassign [getDiffBetweenArray varbef varaft] notaft diff notbef
   foreach name $notaft {
      if {[notInList $ignvarlist $name]} {
         lappend modcontent [list unsetenv $name]
      }
   }
   foreach name $diff {
      if {[notInList $ignvarlist $name]} {
         # new value is totally different (also consider a bare ':' as a
         # totally different value to avoid erroneous matches)
         if {$varbef($name) eq $pathsep || [set idx [string first\
            $varbef($name) $varaft($name)]] == -1} {
            lappend modcontent [list setenv $name $varaft($name)]
         } else {
            # content should be prepended
            if {$idx > 0} {
               set modcmd [list prepend-path]
               # check from the end to get the largest chunk to prepend
               set idx [string last $varbef($name) $varaft($name)]
               # get delimiter from char found between new and existing value
               set delim [string index $varaft($name) [expr {$idx - 1}]]
               if {$delim ne $pathsep} {
                  lappend modcmd -d $delim
               }
               lappend modcmd $name
               # split value and remove duplicate entries
               set vallist [list]
               eval appendNoDupToList vallist [split [string range\
                  $varaft($name) 0 [expr {$idx - 2}]] $delim]
               # an empty element is added
               if {[llength $vallist] == 0} {
                  lappend vallist {}
               }
               lappend modcontent [concat $modcmd $vallist]
            }
            # content should be appended
            if {($idx + [string length $varbef($name)]) < [string length\
               $varaft($name)]} {
               set modcmd [list append-path]
               set delim [string index $varaft($name) [expr {$idx + [string\
                  length $varbef($name)]}]]
               if {$delim ne $pathsep} {
                  lappend modcmd -d $delim
               }
               lappend modcmd $name
               set vallist [list]
               eval appendNoDupToList vallist [split [string range\
                  $varaft($name) [expr {$idx + [string length $varbef($name)]\
                  + 1}] end] $delim]
               if {[llength $vallist] == 0} {
                  lappend vallist {}
               }
               lappend modcontent [concat $modcmd $vallist]
            }
         }
      }
   }
   foreach name $notbef {
      if {[notInList $ignvarlist $name]} {
         if {[string first $pathsep $varaft($name)] == -1} {
            lappend modcontent [list setenv $name $varaft($name)]
         } else {
            # define a path-like variable if path separator found in it
            # split value and remove duplicate entries
            set vallist [list]
            eval appendNoDupToList vallist [split $varaft($name) $pathsep]
            lappend modcontent [concat [list prepend-path $name] $vallist]
         }
      }
   }
   # check function change
   lassign [getDiffBetweenArray funcbef funcaft] notaft diff notbef
   foreach name $notaft {
      lappend modcontent [list unset-function $name]
   }
   foreach name [concat $diff $notbef] {
      lappend modcontent [list set-function $name \n$funcaft($name)]
   }
   # check alias change
   lassign [getDiffBetweenArray aliasbef aliasaft] notaft diff notbef
   foreach name $notaft {
      lappend modcontent [list unset-alias $name]
   }
   foreach name [concat $diff $notbef] {
      lappend modcontent [list set-alias $name $aliasaft($name)]
   }
   # check current working directory change
   if {$cwdbefout ne $cwdaftout} {
      lappend modcontent [list chdir $cwdaftout]
   }

   # sort result to ensure consistent output whatever the evaluation shell
   set modcontent [lsort -dictionary $modcontent]

   reportDebug "resulting env changes '$modcontent'"
   return $modcontent
}

proc source-sh {shell script args} {
   # evaluate script and get the environment changes it performs translated
   # into modulefile commands
   set shtomodargs [concat [list $shell $script] $args]
   set modcontent [eval sh-to-mod $shtomodargs]

   # register resulting modulefile commands
   setLoadedSourceSh [currentState modulename] [concat [list $shtomodargs]\
      $modcontent]

   # evaluate resulting modulefile commands
   foreach modcmd $modcontent {
      eval $modcmd
   }
}

# undo source-sh in unload mode
proc source-sh-un {shell script args} {
   set shtomodargs [concat [list $shell $script] $args]
   set modsrcsh [getLoadedSourceSh [currentState modulename]]

   # find commands resulting from source-sh evaluation recorded in env
   if {[set idx [lsearch -exact $modsrcsh $shtomodargs]] != -1 } {
      set modcontent [lindex $modsrcsh [expr {$idx + 1}]]
   } else {
      set modcontent {}
   }

   # get name of current module unload Tcl interp
   set itrp __modfile_[currentState mode]_[depthState modulename]

   # evaluate each recorded command in unload Tcl interp to get them reversed
   foreach modcmd $modcontent {
      interp eval $itrp $modcmd
   }
}

# report underlying modulefile cmds in display mode
proc source-sh-di {shell script args} {
   set shtomodargs [concat [list $shell $script] $args]

   # if module loaded, get as much content from environment as possible
   if {[is-loaded [currentState modulename]]} {
      set modsrcsh [getLoadedSourceSh [currentState modulename]]

      # find commands resulting from source-sh evaluation recorded in env
      if {[set idx [lsearch -exact $modsrcsh $shtomodargs]] != -1 } {
         set reccontent [lindex $modsrcsh [expr {$idx + 1}]]
      } else {
         set reccontent {}
      }

      # need to evaluate script to get alias and function definition
      eval execShAndGetEnv $shtomodargs

      set modcontent {}
      foreach cmd $reccontent {
         # build modulefile content to show with recorded elements in env
         # and alias/function definition obtained by reevaluating script
         switch -- [lindex $cmd 0] {
            set-alias {
               set alname [lindex $cmd 1]
               if {[info exists aliasaft($alname)]} {
                  set albody $aliasaft($alname)
               } else {
                  set albody {}
               }
               lappend modcontent [list set-alias $alname $albody]
            }
            set-function {
               set fnname [lindex $cmd 1]
               if {[info exists funcaft($fnname)]} {
                  set fnbody \n$funcaft($fnname)
               } else {
                  set fnbody {}
               }
               lappend modcontent [list set-function $fnname $fnbody]
            }
            default {
               lappend modcontent $cmd
            }
         }
      }
   # not loaded, so get full content from script evaluation
   } else {
      set modcontent [eval sh-to-mod $shtomodargs]
   }

   # get name of current module unload Tcl interp
   set itrp __modfile_[currentState mode]_[depthState modulename]

   # evaluate each recorded command in display Tcl interp to get them printed
   foreach modcmd $modcontent {
      interp eval $itrp $modcmd
   }
}

########################################################################
# internal module procedures
#

proc aboveCommandName {} {
   return [lindex [getState commandname] end-1]
}

proc ongoingCommandName {commandName} {
   return [expr {[lsearch -exact [getState commandname] $commandName] != -1}]
}

# stack of message recording/eval unique identifiers
proc pushMsgRecordId {recid {setmsgid 1}} {
   lappendState evalid $recid
   if {$setmsgid} {
      lappendState msgrecordid $recid
   }
}

proc popMsgRecordId {{setmsgid 1}} {
   lpopState evalid
   if {$setmsgid} {
      lpopState msgrecordid
   }
}

proc clearAllMsgRecordId {} {
   if {[isStateDefined evalid]} {
      unsetState evalid
   }
   if {[isStateDefined msgrecordid]} {
      unsetState msgrecordid
   }
}

# gather for the current top evaluation the information on all evaluations
# happening under its umbrella
proc registerModuleEval {context mod {unset 0} {failedcontext {}}} {
   set evalid [topState evalid]
   set contextset 0

   # add mod to existing evaluation context list
   if {[info exists ::g_moduleEval($evalid)]} {
      for {set i 0} {$i < [llength $::g_moduleEval($evalid)]} {incr i 1} {
         set contextevallist [lindex $::g_moduleEval($evalid) $i]
         if {[lindex $contextevallist 0] eq $context} {
            if {$unset} {
               set contextevallist [replaceFromList $contextevallist $mod]
            } else {
               lappend contextevallist $mod
            }
            set ::g_moduleEval($evalid) [expr {[llength $contextevallist] > 1\
               ? [lreplace $::g_moduleEval($evalid) $i $i $contextevallist]\
               : [lreplace $::g_moduleEval($evalid) $i $i]}]
            set contextset 1
            break
         }
      }
   }

   # add mod to new evaluation context list
   if {!$unset && !$contextset} {
      lappend ::g_moduleEval($evalid) [list $context $mod]
   }

   # add mod to failed evaluation list
   if {$unset} {
      lappend ::g_moduleFailedEval($evalid) $failedcontext $mod
   }
}

# record that module evaluation is set hidden
proc registerModuleEvalHidden {context mod} {
   set evalid [topState evalid]
   lappend ::g_moduleHiddenEval($evalid:$context) $mod
}

# get context of currently evaluated module
proc currentModuleEvalContext {} {
   return [lindex $::g_moduleEvalAttempt([currentState modulename]) end]
}

# record module evaluation attempt and corresponding context
proc registerModuleEvalAttempt {context mod} {
   appendNoDupToList ::g_moduleEvalAttempt($mod) $context
}

proc unregisterModuleEvalAttempt {context mod} {
   set ::g_moduleEvalAttempt($mod) [replaceFromList\
      $::g_moduleEvalAttempt($mod) $context]
}

# is at least one module passed as argument evaluated in passed context
proc isModuleEvaluated {context exclmod args} {
   set ret 0
   set icase [isIcase]
   # look at all evaluated mod except excluded one (currently evaluated mod)
   foreach evalmod [lsearch -all -inline -not [array names\
      ::g_moduleEvalAttempt] $exclmod] {
      set evalmatch 0
      # test arguments against all names of evaluated module (translate
      # eventual modspec in evalmod into module names, in case module
      # evaluation stopped prior module name setup)
      foreach mod [concat [getAllModulesFromVersSpec $evalmod]\
         [getAllModuleResolvedName $evalmod]] {
         foreach name $args {
            if {[modEq $name $mod eqstart]} {
               set evalmatch 1
               if {$context eq {any} || [isInList\
                  $::g_moduleEvalAttempt($evalmod) $context]} {
                  set ret 1
               }
               break
            }
         }
         if {$evalmatch} {
            break
         }
      }
      if {$ret} {
         break
      }
   }
   return $ret
}

# was passed mod already evaluated for context and failed
proc isModuleEvalFailed {context mod} {
   set ret 0
   set evalid [topState evalid]
   if {[info exists ::g_moduleFailedEval($evalid)]} {
      foreach {curcon curmod} $::g_moduleFailedEval($evalid) {
         if {$context eq $curcon && $mod eq $curmod} {
            set ret 1
            break
         }
      }
   }
   return $ret
}

# return list of currently loading modules in stack
proc getLoadingModuleList {} {
   set modlist [list]
   for {set i 0} {$i < [depthState modulename]} {incr i 1} {
      if {[lindex [getState mode] $i] eq {load}} {
         lappend modlist [lindex [getState modulename] $i]
      }
   }
   return $modlist
}

# return list of currently loading modulefiles in stack
proc getLoadingModuleFileList {} {
   set modlist [list]
   for {set i 0} {$i < [depthState modulefile]} {incr i 1} {
      if {[lindex [getState mode] $i] eq {load}} {
         lappend modlist [lindex [getState modulefile] $i]
      }
   }
   return $modlist
}

# return list of currently unloading modules in stack
proc getUnloadingModuleList {} {
   set modlist [list]
   for {set i 0} {$i < [depthState modulename]} {incr i 1} {
      if {[lindex [getState mode] $i] eq {unload}} {
         lappend modlist [lindex [getState modulename] $i]
      }
   }
   return $modlist
}
# return list of loaded modules by parsing LOADEDMODULES env variable
proc getLoadedModuleList {{filter_empty 1}} {
   set modlist [list]
   foreach mod [split [get-env LOADEDMODULES] [getState path_separator]] {
      # ignore empty element
      if {$mod ne {} || !$filter_empty} {
         lappend modlist $mod
      }
   }
   return $modlist
}

# return list of loaded module files by parsing _LMFILES_ env variable
proc getLoadedModuleFileList {} {
   set modfilelist [list]
   foreach modfile [split [get-env _LMFILES_] [getState path_separator]] {
      # ignore empty element
      if {$modfile ne {}} {
         lappend modfilelist $modfile
      }
   }
   return $modfilelist
}

# return list of declared source-sh by loaded module by parsing
# MODULES_LMSOURCESH
proc getLoadedModuleSourceShList {} {
   set modsrcshlist [list]
   set pathsep [getState path_separator]
   set sub1sep [getState sub1_separator]
   set sub2sep [getState sub2_separator]
   set unsermap [list <EnvModEscPS> $pathsep <EnvModEscS1> $sub1sep\
      <EnvModEscS2> $sub2sep]

   foreach modsrcshser [split [get-env MODULES_LMSOURCESH] $pathsep] {
      set srcshlist [split $modsrcshser $sub1sep]
      # ignore empty element (1 is meaningless as first elt is loaded mod)
      if {[llength $srcshlist] > 1} {
         set modsrcsh {}
         # keep first arg as string and other args as lists
         foreach srcsh $srcshlist {
            # unescape delimiter chars used in content
            if {[llength $modsrcsh] == 0} {
               lappend modsrcsh [string map $unsermap $srcsh]
            } else {
               lappend modsrcsh [string map $unsermap [split $srcsh $sub2sep]]
            }
         }
         lappend modsrcshlist $modsrcsh
      }
   }
   return $modsrcshlist
}

# return list of loaded module declared conflict by parsing MODULES_LMCONFLICT
proc getLoadedModuleConflictList {} {
   set modconlist [list]
   # get sub level separator that serialize second level of info in env var
   set sub1sep [getState sub1_separator]
   foreach modconser [split [get-env MODULES_LMCONFLICT] [getState\
      path_separator]] {
      # recover range specifier ':' from its serialized form '<'
      set conlist [split [string map {< :} $modconser] $sub1sep]
      # ignore empty element (1 is meaningless as first elt is loaded mod)
      if {[llength $conlist] > 1} {
         lappend modconlist $conlist
      }
   }
   return $modconlist
}

# return list of loaded module declared prereq by parsing MODULES_LMPREREQ
proc getLoadedModulePrereqList {} {
   set modprelist [list]
   set sub1sep [getState sub1_separator]
   # get sub sub level separator that serialize third level of info in env var
   set sub2sep [getState sub2_separator]
   foreach modpreser [split [get-env MODULES_LMPREREQ] [getState\
      path_separator]] {
      # recover range specifier ':' from its serialized form '<'
      set prelist [split [string map {< :} $modpreser] $sub1sep]
      # ignore empty element (1 is meaningless as first elt is loaded mod)
      if {[llength $prelist] > 1} {
         set modpre {}
         # keep first arg as string and other args as lists
         foreach pre $prelist {
            if {[llength $modpre] == 0} {
               lappend modpre $pre
            } else {
               lappend modpre [split $pre $sub2sep]
            }
         }

         lappend modprelist $modpre
      }
   }
   return $modprelist
}

# return list of loaded module asked by user by parsing MODULES_LMNOTUASKED
proc getLoadedModuleNotUserAskedList {} {
   set nuaskedlist [list]
   foreach mod [split [get-env MODULES_LMNOTUASKED] [getState\
      path_separator]] {
      # ignore empty element
      if {$mod ne {}} {
         lappend nuaskedlist $mod
      }
   }
   return $nuaskedlist
}

# return list of loaded module declared altnames by parsing MODULES_LMALTNAME
proc getLoadedModuleAltnameList {} {
   set modaltlist [list]
   set sub1sep [getState sub1_separator]
   foreach modaltser [split [get-env MODULES_LMALTNAME] [getState\
      path_separator]] {
      set altlist [split $modaltser $sub1sep]
      # ignore empty element (1 is meaningless as first elt is loaded mod)
      if {[llength $altlist] > 1} {
         lappend modaltlist $altlist
      }
   }
   return $modaltlist
}

# return list of loaded module declared tags by parsing MODULES_LMTAG
proc getLoadedModuleTagList {} {
   set modtaglist [list]
   set sub1sep [getState sub1_separator]
   foreach modtagser [split [get-env MODULES_LMTAG] [getState\
      path_separator]] {
      set taglist [split $modtagser $sub1sep]
      # ignore empty element (1 is meaningless as first elt is loaded mod)
      if {[llength $taglist] > 1} {
         lappend modtaglist $taglist
      }
   }
   return $modtaglist
}

# sort passed module list following both loaded and dependency orders
proc sortModulePerLoadedAndDepOrder {modlist {nporeq 0} {loading 0}} {
   # sort per loaded order
   set sortlist {}
   if {[llength $modlist] > 0} {
      foreach lmmod [getLoadedModuleList] {
         if {[isInList $modlist $lmmod]} {
            lappend sortlist $lmmod
         }
      }
      # also sort eventual loading modules if asked
      if {$loading} {
         foreach loadingmod [lreverse [getLoadingModuleList]] {
            if {[isInList $modlist $loadingmod]} {
               lappend sortlist $loadingmod
            }
         }
      }
   }

   # then refine sort with dependencies between loaded modules: a dependent
   # module should be placed prior the loaded module requiring it
   set reqListVar [expr {$nporeq ? {::g_moduleNPODepend} :\
      {::g_moduleDepend}}]
   set i 0
   set imax [llength $sortlist]
   while {$i < $imax} {
      set mod [lindex $sortlist $i]
      set jmin $imax

      if {[info exists ${reqListVar}($mod)]} {
         # goes over all dependend modules to find the first one in the loaded
         # order list located after requiring mod
         foreach lmmodlist [set ${reqListVar}($mod)] {
            foreach lmmod $lmmodlist {
               set j [lsearch -exact $sortlist $lmmod]
               if {$j > $i && $j < $jmin} {
                  set jmin $j
                  set jminmod $lmmod
               }
            }
         }
      }

      # move first dependent module found after currently inspected mod right
      # before it
      if {$jmin != $imax} {
         set sortlist [linsert [lreplace $sortlist $jmin $jmin] $i $jminmod]
      # or go to next element in list if current element has not been changed
      } else {
         incr i
      }
   }

   return $sortlist
}

# return list of module paths by parsing MODULEPATH env variable
# behavior param enables to exit in error when no MODULEPATH env variable
# is set. by default an empty list is returned if no MODULEPATH set
# resolv_var param tells if environement variable references in path elements
# should be resolved or passed as-is in result list
# set_abs param applies an absolute path name convertion to path elements
# if enabled
proc getModulePathList {{behavior returnempty} {resolv_var 1} {set_abs 1}} {
   if {[info exists ::env(MODULEPATH)]} {
      set modpathlist [list]
      foreach modpath [split $::env(MODULEPATH) [getState path_separator]] {
         # ignore empty element
         if {$modpath ne {}} {
            if {$resolv_var} {
               set modpath [resolvStringWithEnv $modpath]
            }
            if {$set_abs} {
               set modpath [getAbsolutePath $modpath]
            }
            appendNoDupToList modpathlist $modpath
         }
      }
      return $modpathlist
   } elseif {$behavior eq {exiterronundef}} {
      reportErrorAndExit {No module path defined}
   } else {
      return {}
   }
}

proc setModspecTag {modspec tag {props {}}} {
   reportDebug "Set tag '$tag' with properties '$props' on module\
      specification '$modspec'"

   # record tag list for mod root to optimize search
   set modroot [getModuleRootFromVersSpec $modspec]
   if {![info exists ::g_moduleTagRoot($modroot)]} {
      lappend ::g_moduleTagRoot($modroot) $tag
      set idx 0
      set new 1
   } else {
      set idx [lsearch -exact $::g_moduleTagRoot($modroot) $tag]
      if {$idx == -1} {
         set idx [llength $::g_moduleTagRoot($modroot)]
         lappend ::g_moduleTagRoot($modroot) $tag
         set new 1
      }
   }

   # then record mod spec and props at idx defined for tag. new spec are
   # appended and firstly matching spec is returned with its props on search
   if {[info exists new]} {
      lappend ::g_moduleTagRootSpec($modroot) [list $modspec $props]
   } else {
      set tagrootlist [lindex $::g_moduleTagRootSpec($modroot) $idx]
      lappend tagrootlist $modspec $props
      lset ::g_moduleTagRootSpec($modroot) $idx $tagrootlist
   }
}

proc getModuleTag {mod tag {cmp eqstart}} {
   set modroot [lindex [file split $mod] 0]
   # look if mod matches one of the module specs applying to mod root
   if {[info exists ::g_moduleTagRoot($modroot)]} {
      set idx [lsearch -exact $::g_moduleTagRoot($modroot) $tag]
      if {$idx == -1} {
         return {}
      } else {
         if {[info procs modEq] eq {}} {
            defineModEqProc [isIcase] [getConf extended_default]
         }
         foreach {tmodspec tprops} [lindex $::g_moduleTagRootSpec($modroot)\
            $idx] {
            # first matching module spec wins with its properties
            if {[modEq $tmodspec $mod $cmp]} {
               return [list $tag $tprops]
            }
         }
      }
   }
   return {}
}

proc isModuleTagged {mod tag {collected 0} {cmp eqstart}} {
   # retrieve tag information from collected tags or raw info
   if {$collected} {
      return [isInList [getTagList $mod] $tag]
   } else {
      return [expr {[llength [getModuleTag $mod $tag $cmp]] != 0}]
   }
}

proc getModuleTagProp {mod tag prop} {
   set ret {}
   array set tags [getModuleTag $mod $tag]

   if {[info exists tags($tag)]} {
      array set props $tags($tag)
      if {[info exists props($prop)]} {
         set ret $props($prop)
      }
   }

   return $ret
}

# Gather all tags applying to a given module
proc collectModuleTags {mod} {
   set modroot [lindex [file split $mod] 0]
   # look if mod matches one of the module specs applying to mod root
   if {[info exists ::g_moduleTagRoot($modroot)]} {
      for {set i 0} {$i < [llength $::g_moduleTagRoot($modroot)]} {incr i} {
         set tag [lindex $::g_moduleTagRoot($modroot) $i]
         # skip already known tag
         if {![info exists ::g_tagHash($mod)] || [notInList\
            $::g_tagHash($mod) $tag]} {
            foreach {tmodspec tprops} [lindex\
               $::g_moduleTagRootSpec($modroot) $i] {
               # add tag if one modspec matches mod
               if {[modEq $tmodspec $mod eqstart]} {
                  setModuleTag $mod $tag
                  break
               }
            }
         }
      }
   }
}

proc setModuleTag {mod args} {
   eval lappend "{::g_tagHash($mod)}" $args
}

proc getTagList {mod {serialized 0}} {
   if {[info exists ::g_tagHash($mod)]} {
      if {$serialized} {
         # get tag info as a string that can be registered in an env var
         # remove loaded/auto-loaded/hidden tags from the list to register
         lassign [getDiffBetweenList $::g_tagHash($mod) [list loaded\
            auto-loaded hidden]] tag_list
         if {[llength $tag_list] > 0} {
            set ret [join [concat [list $mod] $tag_list] [getState\
               sub1_separator]]
         } else {
            set ret {}
         }
      } else {
         set ret [lsort -dictionary $::g_tagHash($mod)]
         reportDebug "'$mod' has tag list '$ret'"
      }
   } else {
      set ret {}
   }
   return $ret
}

proc abbrevTagList {taglist} {
   set ret [list]
   foreach tag $taglist {
      if {[info exists ::g_tagAbbrev($tag)]} {
         # empty string abbrev means no tag report
         if {$::g_tagAbbrev($tag) ne {}} {
            lappend ret $::g_tagAbbrev($tag)
         }
      } else {
         lappend ret $tag
      }
   }
   return [lsort -dictionary $ret]
}

proc isModuleDotHidden {mod} {
   foreach elt [split $mod /] {
      if {[string index $elt 0] eq {.}} {
         return 1
      }
   }
   return 0
}

proc getModuleHidingLevel {mod} {
   set modroot [lindex [file split $mod] 0]
   # look if mod matches one of the hidden module specs applying to mod root
   if {[info exists ::g_moduleHideRoot($modroot)]} {
      for {set lvl 2} {$lvl > -1} {incr lvl -1} {
         foreach hmodspec [lindex $::g_moduleHideRoot($modroot) $lvl] {
            if {[modEq $hmodspec $mod eqstart]} {
               return $lvl
            }
         }
      }
   }
   return -1
}

proc setModspecHidingLevel {modspec lvl} {
   # skip record if an higher hiding level is already set
   if {![info exists ::g_moduleHide($modspec)] || $lvl >\
      $::g_moduleHide($modspec)} {
      reportDebug "Record hidden module specification '$modspec' (lvl=$lvl)"
      set ::g_moduleHide($modspec) $lvl

      # record hidden mod spec for mod root to optimize search
      set modroot [getModuleRootFromVersSpec $modspec]
      if {![info exists ::g_moduleHideRoot($modroot)]} {
         set ::g_moduleHideRoot($modroot) [list [list] [list] [list]]
      }
      # record in list corresponding to hidden level to search from strongest
      # to weakest hidden level
      set hiderootlvllist [lindex $::g_moduleHideRoot($modroot) $lvl]
      lappend hiderootlvllist $modspec
      lset ::g_moduleHideRoot($modroot) $lvl $hiderootlvllist
   }
}

# test if mod is declared hidden or has one element in its name starting with
# dot character. mod is considered hidden depending on their hiding level,
# current search query and hiding threshold. when retdetails option is
# enabled, mod hiding level and query match hind are also returned
proc isModuleHidden {mod {modspec {}} {retdetails 0}} {
   set defhidlvl [set hidlvl [getModuleHidingLevel $mod]]
   if {$hidlvl >= [getState hiding_threshold]} {
      # soft hidden mods are considered matched if their root name matches
      # search query, other kind of hidden mods must fully matches query
      set hidmatch [expr {$hidlvl == 0 ? [expr {[modStartNb $mod $modspec]\
         > 0}] : [modEq $modspec $mod eqspec]}]
   } else {
      set hidlvl -1
      set hidmatch 0
   }
   if {$hidlvl < 1 && [set isdot [isModuleDotHidden $mod]] && [getState\
      hiding_threshold] < 1} {
      set hidlvl 1
      # dot hidden are considered matched if remaining string part after
      # search query is not dot hidden
      set hidmatch [expr {[set i [modStartNb $mod $modspec]] > 0 &&\
         ![isModuleDotHidden [join [lrange [file split $mod] $i end] /]]}]
   }
   # hidden if hiding level greater or equal hiding threshold and not
   # matched or if matched hard hiding level are kept hidden
   set ishid [expr {$hidlvl != -1 && (!$hidmatch || $hidlvl > 1)}]

   # is module hidden by definition (whatever hiding threshold or match)
   set hidbydef [expr {$defhidlvl > 0 || ([info exists isdot] && $isdot)}]

   set ret [expr {$retdetails ? [list $hidlvl $hidmatch $hidbydef $ishid]\
      : $ishid}]

   return $ret
}

# check if module name is specified as a full pathname (not a name relative
# to a modulepath)
proc isModuleFullPath {mod} {
   return [regexp {^(|\.|\.\.)/} $mod]
}

# check if a module corresponds to a virtual module (module name
# does not corresponds to end of the modulefile name)
proc isModuleVirtual {mod modfile} {
   return [expr {[string first $mod $modfile end-[string length $mod]] == -1}]
}

# Return the full pathname and modulename to the module.
# Resolve aliases and default versions if the module name is something like
# "name/version" or just "name" (find default version).
proc getPathToModule {mod {indir {}} {report_issue 1} {look_loaded no}\
   {excdir {}}} {
   reportDebug "finding '$mod' in '$indir' (report_issue=$report_issue,\
      look_loaded=$look_loaded, excdir='$excdir')"

   if {$mod eq {}} {
      set retlist [list {} 0 none {Invalid empty module name}]
   # try first to look at loaded modules if enabled to find maching module
   # or to find a closest match (used when switching with single name arg)
   } elseif {$look_loaded ne {no}} {
      switch -- $look_loaded {
         match {set getLoadedNameProc getLoadedMatchingName}
         close {set getLoadedNameProc getLoadedWithClosestName}
      }
      set retlist [if {[set lm [$getLoadedNameProc $mod]] ne {}} {list\
         [getModulefileFromLoadedModule $lm] $lm} {list {} $mod notloaded}]
   # Check for $mod specified as a full pathname
   } elseif {[isModuleFullPath $mod]} {
      set mod [getAbsolutePath $mod]
      # note that a raw filename as an argument returns the full
      # path as the module name
      lassign [checkValidModule $mod] check_valid check_msg
      switch -- $check_valid {
         true {
            set retlist [list $mod $mod]
         }
         invalid - accesserr {
            set retlist [list {} $mod $check_valid $check_msg $mod]
         }
      }
   } else {
      set dir_list [expr {$indir ne {} ? $indir : [getModulePathList\
         exiterronundef]}]
      # remove excluded directories (already searched)
      foreach dir $excdir {
         set dir_list [replaceFromList $dir_list $dir]
      }

      set icase [isIcase]
      defineGetEqArrayKeyProc $icase [getConf extended_default] [getConf\
         implicit_default]

      # Now search for $mod in module paths
      set modspec $mod
      foreach dir $dir_list {
         # get list of modules corresponding to searched query
         array unset mod_list
         array set mod_list [getModules $dir $mod 0 [list rc_defs_included\
            resolve]]

         set prevmod {}
         set mod_res {}
         # loop to resolve correct modulefile in case specified mod is a
         # directory that should be analyzed to get default mod in it
         while {$prevmod ne $mod} {
            set mod [getEqArrayKey mod_list $mod]
            set prevmod $mod
            if {[info exists mod_list($mod)]} {
               switch -- [lindex $mod_list($mod) 0] {
                  alias - version {
                     set newmod [resolveModuleVersionOrAlias $mod $icase]
                     if {[info exists mod_list($newmod)]} {
                        set mod $newmod
                     } else {
                        # restart search on new modulename, constrained to
                        # specified dir if set, if not found in current res
                        return [getPathToModule $newmod $indir $report_issue]
                     }
                  }
                  directory {
                     # is implicit default disabled and none explicitly set?
                     if {[lindex $mod_list($mod) 1] eq {}} {
                        set retlist [list {} $mod none "No default version\
                           defined for '$mod'"]
                     } else {
                        # Move to default element in directory
                        set mod $mod/[lindex $mod_list($mod) 1]
                        # restart search if default element is an hidden dir
                        if {![info exists mod_list($mod)] && [isModuleHidden\
                           $mod $modspec]} {
                           return [getPathToModule $mod $indir $report_issue]
                        }
                     }
                  }
                  modulefile {
                     # If mod was a file in this path, return that file
                     set retlist [list $dir/$mod $mod]
                  }
                  virtual {
                     # return virtual name with file it targets
                     set retlist [list [lindex $mod_list($mod) 2] $mod]
                  }
                  invalid - accesserr {
                     # may found mod but issue, so end search with error
                     set retlist [concat [list {} $mod] $mod_list($mod)]
                  }
               }
            }
         }
         # break loop if found something (valid or invalid module)
         # elsewhere go to next path
         if {[info exists retlist]} {
            break
         }
      }
   }

   # set result if nothing found
   if {![info exists retlist]} {
      set retlist [list {} $mod none "Unable to locate a modulefile for\
         '$mod'"]
   # update result if forbidden
   } elseif {[isModuleTagged [lindex $retlist 1] forbidden]} {
      set retlist [list {} [lindex $retlist 1] accesserr [getForbiddenMsg\
         [lindex $retlist 1]]]
   }
   if {[lindex $retlist 0] ne {}} {
      reportTrace "'[lindex $retlist 1]' ([lindex $retlist 0]) matching\
         '$mod'" {Select module}
   # no error if we look at loaded modules and passed mod not found loaded
   } elseif {[lindex $retlist 2] ne {notloaded} && $report_issue} {
      eval reportIssue [lrange $retlist 2 4]
   }
   return $retlist
}

proc isModuleLoaded {mod} {
   cacheCurrentModules

   return [info exists ::g_loadedModules($mod)]
}

proc getModulefileFromLoadedModule {mod} {
   if {[isModuleLoaded $mod]} {
      return $::g_loadedModules($mod)
   } else {
      return {}
   }
}

proc isModulefileLoaded {modfile} {
   cacheCurrentModules

   return [info exists ::g_loadedModuleFiles($modfile)]
}

proc getModuleFromLoadedModulefile {modfile {idx all}} {
   if {[isModulefileLoaded $modfile]} {
      if {$idx eq {all}} {
         return $::g_loadedModuleFiles($modfile)
      } else {
         return [lindex $::g_loadedModuleFiles($modfile) $idx]
      }
   } else {
      return {}
   }
}

proc isModuleLoading {mod} {
   return [isInList [getLoadingModuleList] $mod]
}

proc isModulefileLoading {modfile} {
   return [isInList [getLoadingModuleFileList] $modfile]
}

proc getModuleFromLoadingModulefile {modfile {idx all}} {
   if {[isModulefileLoading $modfile]} {
      set loadingmodlist [getLoadingModuleList]
      foreach i [lsearch -all -exact [getLoadingModuleFileList] $modfile] {
         lappend modlist [lindex $loadingmodlist $i]
      }

      if {$idx eq {all}} {
         return $modlist
      } else {
         return [lindex $modlist $idx]
      }
   } else {
      return {}
   }
}

proc setLoadedModule {mod modfile uasked} {
   set ::g_loadedModules($mod) $modfile
   # a loaded modfile may correspond to multiple loaded virtual modules
   lappend ::g_loadedModuleFiles($modfile) $mod
   # record if mod has been asked by user and relative loaded/auto-loaded tag
   if {$uasked} {
      set ::g_loadedModuleUasked($mod) 1
      setModuleTag $mod loaded
   } else {
      setModuleTag $mod auto-loaded
   }
   # build dependency chain
   setModuleDependency $mod
}

proc unsetLoadedModule {mod modfile} {
   unset ::g_loadedModules($mod)
   # a loaded modfile may correspond to multiple loaded virtual modules
   if {[llength $::g_loadedModuleFiles($modfile)] == 1} {
      unset ::g_loadedModuleFiles($modfile)
   } else {
      set ::g_loadedModuleFiles($modfile) [replaceFromList\
         $::g_loadedModuleFiles($modfile) $mod]
   }
   if {[info exists ::g_loadedModuleUasked($mod)]} {
      unset ::g_loadedModuleUasked($mod)
   }
   # update dependencies
   unsetModuleDependency $mod
}

# Define procedure to get how many parts between passed name and mod are equal
# Adapt procedure code whether icase is enabled or disabled
proc defineModStartNbProc {icase} {
   set procname modStartNbProc
   if {$icase} {
      append procname Icase
   }
   # define proc if not done yet or if it was defined for another context
   if {[info procs modStartNb] eq {} || $::g_modStartNb_proc ne $procname} {
      if {[info exists ::g_modStartNb_proc]} {
         rename ::modStartNb ::$::g_modStartNb_proc
      }
      rename ::$procname ::modStartNb
      set ::g_modStartNb_proc $procname
   }
}

# alternative definitions of modStartNb proc
proc modStartNbProc {mod name} {
   # first compare against name's parent chunk by chunk
   set modname [getModuleNameFromVersSpec $name]
   if {$modname eq {.}} {
      set i 0
      set imax 0
   } else {
      set namesplit [split $modname /]
      set modsplit [split $mod /]
      # min expr function is not supported in Tcl8.4 and earlier
      set imax [if {[llength $namesplit] < [llength $modsplit]} {llength\
         $namesplit} {llength $modsplit}]
      for {set i 0} {$i < $imax} {incr i} {
         if {![string equal [lindex $modsplit $i] [lindex $namesplit $i]]} {
            break
         }
      }
   }
   # if name's parent matches check if full name also matches
   if {$i == $imax && [modEq $name $mod eqstart]} {
      incr i
   }
   return $i
}
proc modStartNbProcIcase {mod name} {
   set modname [getModuleNameFromVersSpec $name]
   if {$modname eq {.}} {
      set i 0
      set imax 0
   } else {
      set namesplit [split $modname /]
      set modsplit [split $mod /]
      set imax [if {[llength $namesplit] < [llength $modsplit]} {llength\
         $namesplit} {llength $modsplit}]
      for {set i 0} {$i < $imax} {incr i} {
         if {![string equal -nocase [lindex $modsplit $i] [lindex $namesplit\
            $i]]} {
            break
         }
      }
   }
   if {$i == $imax && [modEq $name $mod eqstart]} {
      incr i
   }
   return $i
}

# check if name matches passed mod name or one of its alternative name
proc doesModuleMatchesName {mod name} {
   cacheCurrentModules
   set ret 0

   # check if main or alternative names of loaded mod matches passed name
   foreach matchmod [concat [list $mod] [getLoadedAltname $mod]] {
      if {[modEq $name $matchmod eqstart]} {
         set ret 1
         break
      }
   }
   return $ret
}

# check if name matches one name of passed loading mod (main or alternative)
proc doesLoadingModuleMatchesName {mod name} {
   set ret 0

   # check if main or alternative names of loading mod matches passed name
   # directly look at all resolved names structure as alternative names for
   # loading modules are not yet registered elsewhere
   foreach matchmod [concat [list $mod] [getAllModuleResolvedName $mod]] {
      if {[modEq $name $matchmod eqstart]} {
         set ret 1
         break
      }
   }
   return $ret
}

# return the currently loaded module whose name is the closest to the
# name passed as argument. if no loaded module match at least one part
# of the passed name, an empty string is returned.
proc getLoadedWithClosestName {name} {
   set ret {}
   set retmax 1

   if {[isModuleFullPath $name]} {
      set fullname [getAbsolutePath $name]
      # if module is passed as full modulefile path name, get corresponding
      # short name from used modulepaths
      if {[set shortname [findModuleNameFromModulefile $fullname]] ne {}} {
         set nametosplit $shortname
      # or look at lmfile names to return the eventual exact match
      } else {
         # module may be loaded with its full path name
         if {[isModuleLoaded $fullname]} {
            set ret $fullname
         # or name corresponds to the _lmfiles_ entry of a virtual modules in
         # which case lastly loaded virtual module is returned
         } elseif {[isModulefileLoaded $fullname]} {
            set ret [getModuleFromLoadedModulefile $fullname end]
         }
      }
   } else {
      set nametosplit $name
   }

   if {[info exists nametosplit]} {
      cacheCurrentModules
      set icase [isIcase]
      defineModStartNbProc $icase
      defineModEqProc $icase [getConf extended_default]
      # compare name to each currently loaded module name
      foreach mod [getLoadedModuleList] {
         # if module loaded as fullpath but test name not, try to get loaded
         # mod short name (with currently used modulepaths) to compare it
         if {[isModuleFullPath $mod] && [set modname\
            [findModuleNameFromModulefile $mod]] ne {}} {
            # no alt name to retrieve if module has been loaded full path
            set matchmodlist [list $modname]
         } else {
            # add alternative names of mod to the matching list
            set matchmodlist [concat [list $mod] [getLoadedAltname $mod]]
         }

         # compare each element of the name to find closest answer. in case of
         # equality, last loaded module will be returned as it overwrites
         # previously found value
         foreach matchmod $matchmodlist {
            if {[set i [modStartNb $matchmod $nametosplit]] >= $retmax} {
               set retmax $i
               set ret $mod
               break
            }
         }
      }
   }

   reportDebug "'$ret' closest to '$name'"
   return $ret
}

# return the currently loaded module whose name is equal or include the name
# passed as argument. if no loaded module match, an empty string is returned.
# loading: look at currently loading modules instead of loaded if loading == 1
# lmlist: only take into account passed loaded module list not all loaded mods
proc getLoadedMatchingName {name {behavior {}} {loading 0} {lmlist {}}} {
   set ret {}
   set retmax 0
   # get default behavior from unload_match_order config
   if {$behavior eq {}} {
      set behavior [getConf unload_match_order]
   }

   # use loading-specific procedures instead of loaded-specific ones
   if {$loading} {
      set isModulefileLoaded isModulefileLoading
      set getModuleFromLoadedModulefile getModuleFromLoadingModulefile
      set getLoadedModuleList getLoadingModuleList
      set doesModuleMatchesName doesLoadingModuleMatchesName
   } else {
      set isModulefileLoaded isModulefileLoaded
      set getModuleFromLoadedModulefile getModuleFromLoadedModulefile
      set getLoadedModuleList getLoadedModuleList
      set doesModuleMatchesName doesModuleMatchesName
   }

   # fetch currently loaded/loading module name is no list provided
   if {[llength $lmlist] == 0} {
      set lmlist [$getLoadedModuleList]
   }

   # if module is passed as full modulefile path name, look at lmfile names
   # to return the eventual exact match
   if {[isModuleFullPath $name]} {
      set mod [getAbsolutePath $name]
      # if module is loaded with its full path name loadedmodules entry is
      # equivalent to _lmfiles_ corresponding entry so only check _lmfiles_
      if {[$isModulefileLoaded $mod]} {
         # a loaded modfile may correspond to multiple loaded virtual modules
         switch -- $behavior {
            returnlast {
               # the last loaded/loading module will be returned
               set ret [$getModuleFromLoadedModulefile $mod end]
            }
            returnfirst {
               # the first loaded/loading module will be returned
               set ret [$getModuleFromLoadedModulefile $mod 0]
            }
            returnall {
               # all loaded/loading modules will be returned
               set ret [$getModuleFromLoadedModulefile $mod]
            }
         }
      }
   } elseif {$name ne {}} {
      defineModEqProc [isIcase] [getConf extended_default]
      # compare name to each currently loaded/loading module name, if multiple
      # mod match name:
      foreach mod $lmlist {
         # if module loaded as fullpath but test name not, try to get loaded
         # mod short name (with currently used modulepaths) to compare it
         if {[isModuleFullPath $mod] && [set modname\
            [findModuleNameFromModulefile $mod]] ne {}} {
            set matchmod $modname
         } else {
            set matchmod $mod
         }
         if {[$doesModuleMatchesName $matchmod $name]} {
            switch -- $behavior {
               returnlast {
                  # the last loaded module will be returned
                  set ret $mod
               }
               returnfirst {
                  # the first loaded module will be returned
                  set ret $mod
                  break
               }
               returnall {
                  # all loaded modules will be returned
                  lappend ret $mod
               }
            }
         }
      }
   }

   reportDebug "'$ret' matches '$name'"
   return $ret
}

proc setLoadedSourceSh {mod args} {
   foreach arg $args {
      # each arg is a list with source-sh call string at index 0 and resulting
      # modulefile commands at all later index positions
      set shtomodargs [lindex $arg 0]
      set modcontent [lrange $arg 1 end]
      if {![info exists ::g_loadedModuleSourceSh($mod)] || [notInList\
         $::g_loadedModuleSourceSh($mod) $shtomodargs]} {
         # filter alias or function definition not to record them
         set filtmodcont [list]
         foreach modcmdlist $modcontent {
            set modcmd [lindex $modcmdlist 0]
            if {$modcmd eq {set-alias} || $modcmd eq {set-function}} {
               # set an empty body to make valid unset-* call
               lappend filtmodcont [concat [lrange $modcmdlist 0 1] [list {}]]
            } else {
               lappend filtmodcont $modcmdlist
            }
         }
         lappend ::g_loadedModuleSourceSh($mod) $shtomodargs $filtmodcont
      }
   }
}

proc unsetLoadedSourceSh {mod} {
   if {[info exists ::g_loadedModuleSourceSh($mod)]} {
      unset ::g_loadedModuleSourceSh($mod)
   }
}

proc getLoadedSourceSh {mod {serialized 0}} {
   set ret {}

   set pathsep [getState path_separator]
   set sub1sep [getState sub1_separator]
   set sub2sep [getState sub2_separator]
   set sermap [list $pathsep <EnvModEscPS> $sub1sep <EnvModEscS1>\
      $sub2sep <EnvModEscS2>]

   if {[info exists ::g_loadedModuleSourceSh($mod)]} {
      if {$serialized} {
         foreach {shtomodargs modcontent} $::g_loadedModuleSourceSh($mod) {
            # escape delimiter chars if used in content
            set shtomodargsser [string map $sermap $shtomodargs]
            set modcontentser [string map $sermap $modcontent]
            lappend modsrcsh $shtomodargsser$sub2sep[join $modcontentser\
               $sub2sep]
         }
         set ret [join [concat [list $mod] $modsrcsh] $sub1sep]
      } else {
         set ret $::g_loadedModuleSourceSh($mod)
      }
   }

   return $ret
}

proc setLoadedConflict {mod args} {
   eval appendNoDupToList "{::g_loadedModuleConflict($mod)}" $args
}

proc unsetLoadedConflict {mod} {
   if {[info exists ::g_loadedModuleConflict($mod)]} {
      unset ::g_loadedModuleConflict($mod)
   }
}

proc getLoadedConflict {mod {serialized 0}} {
   set ret {}

   if {[info exists ::g_loadedModuleConflict($mod)]} {
      if {$serialized} {
         # get conflict info as a string that can be registered in an env var
         # translate range specifier ':' into '<' to distinguish from path sep
         set ret [string map {: <} [join [concat [list $mod]\
            $::g_loadedModuleConflict($mod)] [getState sub1_separator]]]
      } else {
         set ret $::g_loadedModuleConflict($mod)
      }
   }

   return $ret
}

proc doesModuleConflict {mod} {
   set does 0
   set modconlist {}
   set moddecconlist {}
   defineModEqProc [isIcase] [getConf extended_default]
   # get module short name if loaded by its full pathname
   if {[set isfullpath [isModuleFullPath $mod]]} {
      set smod [findModuleNameFromModulefile $mod]
   }

   # check if any loaded module has declared a conflict
   foreach modcon [array names ::g_loadedModuleConflict] {
      # look if some loaded or loading modules correspond to conflict defined
      # by mod
      if {$modcon eq $mod || ($isfullpath && $modcon eq $smod)} {
         foreach withmod $::g_loadedModuleConflict($modcon) {
            # skip own reflexive conflict (look at mod main and alternative
            # names) and those already known
            if {![doesModuleMatchesName $mod $withmod] && (!$isfullpath ||\
               ![doesModuleMatchesName $smod $withmod]) && ([set lmmodlist\
               [getLoadedMatchingName $withmod returnall]] ne {} || [set\
               lmmodlist [getLoadedMatchingName $withmod returnall 1]] ne {})} {
               # multiple loaded module may match conflict declared name
               foreach lmmod $lmmodlist {
                  appendNoDupToList modconlist $lmmod
               }
               appendNoDupToList moddecconlist $withmod
               set does 1
            }
         }
      # other loaded module declared conflicts (skipping those already known)
      } elseif {[notInList $modconlist $modcon]} {
         foreach withmod $::g_loadedModuleConflict($modcon) {
            # check if mod or one of its alt name match conflict
            if {[doesModuleMatchesName $mod $withmod] || ($isfullpath &&\
               [doesModuleMatchesName $smod $withmod])} {
               lappend modconlist $modcon
               lappend moddecconlist $modcon
               set does 1
               break
            }
         }
      }
   }

   reportDebug "'$mod' conflicts with '$modconlist' (declared as '$moddecconlist')"
   return [list $does $modconlist $moddecconlist]
}

proc setLoadedPrereq {mod args} {
   eval appendNoDupToList "{::g_loadedModulePrereq($mod)}" $args
}

proc unsetLoadedPrereq {mod} {
   if {[info exists ::g_loadedModulePrereq($mod)]} {
      unset ::g_loadedModulePrereq($mod)
   }
}

proc getLoadedPrereq {mod {serialized 0}} {
   set ret {}

   set sub2sep [getState sub2_separator]
   if {[info exists ::g_loadedModulePrereq($mod)]} {
      if {$serialized} {
         # get prereq info as a string that can be registered in an env var
         foreach pre $::g_loadedModulePrereq($mod) {
            lappend modpre [join $pre $sub2sep]
         }
         # translate range specifier ':' into '<' to distinguish from path sep
         set ret [string map {: <} [join [concat [list $mod] $modpre]\
            [getState sub1_separator]]]
      } else {
         set ret $::g_loadedModulePrereq($mod)
      }
   }

   return $ret
}

proc setLoadedAltname {mod args} {
   foreach arg $args {
      switch -- [string range $arg 0 2] {
         al| {
            appendNoDupToList ::g_loadedModuleAliasAltname($mod) [string\
               range $arg 3 end]
         }
         as| {
            appendNoDupToList ::g_loadedModuleAutoAltname($mod) [string range\
               $arg 3 end]
         }
         default {
            appendNoDupToList ::g_loadedModuleAltname($mod) $arg
         }
      }
      if {[string range $arg 0 2] eq {as|}} {
      } else {
      }
   }
}

proc unsetLoadedAltname {mod} {
   if {[info exists ::g_loadedModuleAltname($mod)]} {
      unset ::g_loadedModuleAltname($mod)
   }
   if {[info exists ::g_loadedModuleAliasAltname($mod)]} {
      unset ::g_loadedModuleAliasAltname($mod)
   }
   if {[info exists ::g_loadedModuleAutoAltname($mod)]} {
      unset ::g_loadedModuleAutoAltname($mod)
   }
}

proc getLoadedAltname {mod {serialized 0} {typelist {sym alias autosym}}} {
   set ret {}

   if {[info exists ::g_loadedModuleAltname($mod)] && [isInList $typelist\
      sym]} {
      set ret $::g_loadedModuleAltname($mod)
   }
   if {[info exists ::g_loadedModuleAliasAltname($mod)] && [isInList\
      $typelist alias]} {
      if {$serialized} {
         # add a 'al|' prefix to each alias to distinguish them when
         # serialized
         foreach altname $::g_loadedModuleAliasAltname($mod) {
            lappend ret al|$altname
         }
      } else {
         set ret [concat $ret $::g_loadedModuleAliasAltname($mod)]
      }
   }
   if {[info exists ::g_loadedModuleAutoAltname($mod)] && [isInList $typelist\
      autosym]} {
      if {$serialized} {
         # add a 'as|' prefix to each auto sym to distinguish them when
         # serialized
         foreach altname $::g_loadedModuleAutoAltname($mod) {
            lappend ret as|$altname
         }
      } else {
         set ret [concat $ret $::g_loadedModuleAutoAltname($mod)]
      }
   }

   if {$ret ne {} && $serialized} {
      # get altname info as a string that can be registered in an env var
      set ret [join [concat [list $mod] $ret] [getState sub1_separator]]
   }

   return $ret
}

proc isModuleUserAsked {mod} {
   cacheCurrentModules

   return [info exists ::g_loadedModuleUasked($mod)]
}

# register conflict violation state between loaded modules
proc setModuleConflictViolation {mod modconlist} {
   reportDebug "set conflict violation state for '$mod'"
   set ::g_conflictViolation($mod) $modconlist
   # also update violation state for loaded mod conflicting with mod
   foreach lmmod $modconlist {
      if {[appendNoDupToList ::g_conflictViolation($lmmod) $mod]} {
         reportDebug "set/update conflict violation state for '$lmmod'"
      }
   }
}

# unregister conflict violation state between modules
proc unsetModuleConflictViolation {mod} {
   if {[info exists ::g_conflictViolation($mod)]} {
      # also update violation state for loaded mod conflicting with mod
      foreach lmmod $::g_conflictViolation($mod) {
         set convio [replaceFromList\
            $::g_conflictViolation($lmmod) $mod]
         reportDebug "unset/update conflict violation state for '$lmmod'"
         if {[llength $convio] == 0} {
            unset ::g_conflictViolation($lmmod)
         } else {
            set ::g_conflictViolation($lmmod) $convio
         }
      }
      reportDebug "unset conflict violation state for '$mod'"
      unset ::g_conflictViolation($mod)
   }
}

# build dependency chain between loaded modules based on registered prereqs
proc setModuleDependency {mod} {
   set modlist [getLoadedModuleList]
   defineModEqProc [isIcase] [getConf extended_default]
   # only look at modules loaded prior current one to find requirements,
   # modules loaded afterward are unmet dependencies as dependents have
   # not been reloaded after them
   set modidx [lsearch -exact $modlist $mod]
   set modnpolist [lrange $modlist [expr {$modidx + 1}] end]
   set modlist [lrange $modlist 0 $modidx]
   # reverse list to get closest match if returning lastly loaded module
   if {[getConf unload_match_order] eq {returnlast}} {
      set modlist [lreverse $modlist]
   }
   set deplist {}
   set depnpolist {}

   foreach prereq [getLoadedPrereq $mod] {
      # get corresponding loaded module for each element of the prereq order
      set lmprelist {}
      set lmnpolist {}
      foreach modpre $prereq {
         set lmfound {}
         foreach lmmod $modlist {
            if {[doesModuleMatchesName $lmmod $modpre]} {
               set lmfound $lmmod
               break
            }
         }

         # register an unmet dependency/requirement if no loaded mod matches
         if {$lmfound eq {}} {
            reportDebug "set an unmet requirement on '$modpre' for '$mod'"
            lappend ::g_moduleUnmetDep($mod) $modpre
            lappend ::g_unmetDepHash($modpre) $mod
         # add matching mod elsewhere
         } else {
            appendNoDupToList lmprelist $lmfound
            appendNoDupToList lmnpolist $lmfound
         }

         # look if requirement can be found in the No Particular Order list
         foreach lmmod $modnpolist {
            if {[doesModuleMatchesName $lmmod $modpre]} {
               appendNoDupToList lmnpolist $lmmod
               break
            }
         }
      }

      switch -- [llength $lmprelist] {
         0 {
            # prereq not satisfied
            reportDebug "set prereq violation state for '$mod'"
            lappend ::g_prereqViolation($mod) $prereq
         }
         1 {
            set lmmod [lindex $lmprelist 0]
            lappend deplist [list $lmmod]
            # set 'is depended by' relations
            lappend ::g_dependHash($lmmod) [list $mod]
         }
         default {
            lappend deplist $lmprelist
            # many modules in prereq list, means they all set an optional dep
            foreach lmmod $lmprelist {
               lappend ::g_dependHash($lmmod) [list $mod 1]
            }
         }
      }

      # build 'is depended by' relations not taking loading order into account
      switch -- [llength $lmnpolist] {
         0 {
            # even on No Particular Order mode, prereq is not satisfied
            reportDebug "set NPO prereq violation state for '$mod'"
            lappend ::g_prereqNPOViolation($mod) $prereq
         }
         1 {
            set lmmod [lindex $lmnpolist 0]
            lappend depnpolist [list $lmmod]
            # set 'is depended by' relations
            lappend ::g_dependNPOHash($lmmod) [list $mod]
         }
         default {
            lappend depnpolist $lmnpolist
            # many modules in prereq list, means they all set an optional dep
            foreach lmmod $lmnpolist {
               lappend ::g_dependNPOHash($lmmod) [list $mod 1]
            }
         }
      }
   }

   # conflict not satisfied
   lassign [doesModuleConflict $mod] doescon modconlist
   if {$doescon} {
      setModuleConflictViolation $mod $modconlist
   }

   # update eventual registered unmet dependencies
   foreach modpre [array names ::g_unmetDepHash] {
      if {[doesModuleMatchesName $mod $modpre]} {
         reportDebug "refresh requirements targetting '$modpre'"
         foreach lmmod $::g_unmetDepHash($modpre) {
            if {[isInList [getDependentLoadedModuleList [list $lmmod] 0 0]\
               $mod]} {
               reportDebug "skip deps refresh for '$lmmod' as dep cycle\
                  detected with '$mod'"

               # remove dependency link in no particular order structs to
               # avoid cycle first in 'is depended by' struct
               if {[info exists ::g_dependNPOHash($mod)]} {
                  set depmodlist $::g_dependNPOHash($mod)
                  for {set i 0} {$i < [llength $depmodlist]} {incr i 1} {
                     if {[lindex [lindex $depmodlist $i] 0] eq $lmmod} {
                        set depmodlist [lreplace $depmodlist $i $i]
                        break
                     }
                  }
                  set ::g_dependNPOHash($mod) $depmodlist
                  reportDebug "update NPO dependent of '$mod' to\
                     '$depmodlist'"
               }
               # then update 'depend on' struct
               set lmmoddepnpolist {}
               foreach depmodlist $::g_moduleNPODepend($lmmod) {
                  if {[set depidx [lsearch -exact $depmodlist $mod]] != -1} {
                     set depmodlist [lreplace $depmodlist $depidx $depidx]
                     # implies to update consistenly alternate requirement or
                     # violation state if no alternative loaded
                     switch -- [llength $depmodlist] {
                        0 {
                           # do not know exact prereq name, so use correspond.
                           # loaded module matching it
                           lappend ::g_prereqNPOViolation($lmmod) $mod
                           reportDebug "set NPO prereq violation state for\
                              '$lmmod'"
                        }
                        1 {
                           # update alternate loaded mod which became a strong
                           # requirement
                           set altmod [lindex $depmodlist 0]
                           set ::g_dependNPOHash($altmod) [replaceFromList\
                              $::g_dependNPOHash($altmod) [list $lmmod 1]\
                              $lmmod]
                           reportDebug "update NPO dependent of '$altmod' to\
                              '$::g_dependNPOHash($altmod)'"
                        }
                     }
                  }
                  lappend lmmoddepnpolist $depmodlist
               }
               reportDebug "update NPO requirement of '$lmmod' to\
                  '$lmmoddepnpolist'"
               set ::g_moduleNPODepend($lmmod) $lmmoddepnpolist
            } else {
               # refresh actual dependencies of targetting mod
               unsetModuleDependency $lmmod
               setModuleDependency $lmmod
            }
         }
      }
   }

   # set 'depends on' relation
   reportDebug "set requirements of '$mod' to '$deplist'"
   set ::g_moduleDepend($mod) $deplist
   reportDebug "set NPO requirements of '$mod' to '$depnpolist'"
   set ::g_moduleNPODepend($mod) $depnpolist
}

# update dependency chain when unloading module
proc unsetModuleDependency {mod} {
   foreach lmmodlist $::g_moduleDepend($mod) {
      set manymod [expr {[llength $lmmodlist] > 1}]

      # unset 'is depended by' mod relations
      foreach lmmod $lmmodlist {
         if {[info exists ::g_dependHash($lmmod)]} {
            if {$manymod} {
               set hashdep [list $mod 1]
            } else {
               set hashdep [list $mod]
            }
            set ::g_dependHash($lmmod) [replaceFromList\
               $::g_dependHash($lmmod) $hashdep]
            if {[llength $::g_dependHash($lmmod)] == 0} {
               unset ::g_dependHash($lmmod)
            }
         }
      }
   }
   # unset mod's 'depends on' relation
   reportDebug "unset requirements of '$mod'"
   unset ::g_moduleDepend($mod)

   foreach lmmodlist $::g_moduleNPODepend($mod) {
      set manymod [expr {[llength $lmmodlist] > 1}]

      # unset 'is depended by' mod relations
      foreach lmmod $lmmodlist {
         if {[info exists ::g_dependNPOHash($lmmod)]} {
            if {$manymod} {
               set hashdep [list $mod 1]
            } else {
               set hashdep [list $mod]
            }
            set ::g_dependNPOHash($lmmod) [replaceFromList\
               $::g_dependNPOHash($lmmod) $hashdep]
            if {[llength $::g_dependNPOHash($lmmod)] == 0} {
               unset ::g_dependNPOHash($lmmod)
            }
         }
      }
   }
   # unset mod's No Particular Order 'depends on' relation
   reportDebug "unset NPO requirements of '$mod'"
   unset ::g_moduleNPODepend($mod)

   # unset eventual violation states
   if {[info exists ::g_prereqViolation($mod)]} {
      reportDebug "unset prereq violation state for '$mod'"
      unset ::g_prereqViolation($mod)
   }
   if {[info exists ::g_prereqNPOViolation($mod)]} {
      reportDebug "unset NPO prereq violation state for '$mod'"
      unset ::g_prereqNPOViolation($mod)
   }
   unsetModuleConflictViolation $mod

   # unset eventual registered unmet dependencies
   if {[info exists ::g_moduleUnmetDep($mod)]} {
      foreach ummod $::g_moduleUnmetDep($mod) {
         if {[info exists ::g_unmetDepHash($ummod)]} {
            set ::g_unmetDepHash($ummod) [replaceFromList\
               $::g_unmetDepHash($ummod) $mod]
            if {[llength $::g_unmetDepHash($ummod)] == 0} {
               unset ::g_unmetDepHash($ummod)
            }
         }
      }
      reportDebug "unset unmet requirements for '$mod'"
      unset ::g_moduleUnmetDep($mod)
   }

   # unset mod's 'is depended by' relations
   set hashdeplist [getDirectDependentList $mod]
   if {[llength $hashdeplist] > 0} {
      reportDebug "refresh dependent of '$mod'"
      foreach lmmod $hashdeplist {
         # refresh actual dependencies of targetting mod
         unsetModuleDependency $lmmod
         setModuleDependency $lmmod
      }
   }
}

# returns if any loaded module (if passed mod is empty) or passed mod and all
# its requirement chain satisfy their loading constraints (prereq & conflict)
proc areModuleConstraintsSatisfied {{mod {}} {nporeq 0}} {
   set ret 1
   cacheCurrentModules

   # are requirements loaded after their dependent included or not
   if {$nporeq} {
      set reqVioVar ::g_prereqNPOViolation
      set reqListVar ::g_moduleNPODepend
   } else {
      set reqVioVar ::g_prereqViolation
      set reqListVar ::g_moduleDepend
   }

   # check if any loaded module violates its prereq or conflict constraints
   if {$mod eq {}} {
      if {[array size ::g_conflictViolation] > 0 || [array size\
         $reqVioVar] > 0} {
         set ret 0
      }
   } else {
      set fulllist [list $mod]
      for {set i 0} {$i < [llength $fulllist]} {incr i 1} {
         set depmod [lindex $fulllist $i]

         # check if depmod violates its prereq or conflict constraints
         if {[info exists ::g_conflictViolation($depmod)] || [info exists\
            ${reqVioVar}($depmod)]} {
            # found violation among the requirement chain of mod so the
            # constraint of mod are not satisfied
            set ret 0
            break
         }
         # add requirements of depmod to the module to check list
         foreach lmmodlist [set ${reqListVar}($depmod)] {
            eval appendNoDupToList fulllist $lmmodlist
         }
      }
   }

   return $ret
}

# return list of loaded modules having an unmet requirement on passed mod
# and their recursive dependent
proc getUnmetDependentLoadedModuleList {mod} {
   reportDebug "get dependent of upcoming loaded '$mod'"
   set unmetdeplist {}
   set depmodlist {}
   defineModEqProc [isIcase] [getConf extended_default]

   # skip dependent analysis if mod has a conflict with a loaded module
   lassign [doesModuleConflict $mod] doescon modconlist
   if {!$doescon} {
      foreach ummod [array names ::g_unmetDepHash] {
         if {[doesModuleMatchesName $mod $ummod]} {
            foreach depmod $::g_unmetDepHash($ummod) {
               lappend depmodlist $depmod
               # temporarily remove prereq violation of depmod if mod
               # load solves it (no other prereq is missing)
               if {[info exists ::g_prereqViolation($depmod)]} {
                  foreach prereq $::g_prereqViolation($depmod) {
                     foreach modpre $prereq {
                        # also temporarily remove prereq violation for
                        # requirements loaded after dependent module
                        if {[doesModuleMatchesName $mod $modpre] ||\
                           [is-loaded $modpre]} {
                           # backup original violation to restore it later
                           if {![info exists preunvioarr($depmod)]} {
                              set preunvioarr($depmod)\
                                 $::g_prereqViolation($depmod)
                           }
                           # temporarily remove matching violation
                           set ::g_prereqViolation($depmod) [replaceFromList\
                              $::g_prereqViolation($depmod) $prereq]
                           if {[llength $::g_prereqViolation($depmod)] == 0} {
                              unset ::g_prereqViolation($depmod)
                           }
                           break
                        }
                     }
                  }
               }
            }
         }
      }
   }

   # select dependent if all its constraint are now satisfied (after removing
   # eventual prereq violation toward mod)
   foreach depmod $depmodlist {
      if {[areModuleConstraintsSatisfied $depmod]} {
         appendNoDupToList unmetdeplist $depmod
      }
   }

   # get dependent of dependent
   set deplist [getDependentLoadedModuleList $unmetdeplist 0 0 0 0 1]

   # restore temporarily lift prereq violation
   if {[array exists preunvioarr]} {
      foreach depmod [array names preunvioarr] {
         set ::g_prereqViolation($depmod) $preunvioarr($depmod)
      }
   }

   set sortlist [sortModulePerLoadedAndDepOrder [concat $unmetdeplist\
      $deplist]]
   reportDebug "got '$sortlist'"
   return $sortlist
}

# return list of loaded modules declaring a prereq on passed mod with
# distinction made with strong prereqs (no alternative loaded) or weak and
# also with prereq loaded after their dependent module
proc getDirectDependentList {mod {strong 0} {nporeq 0} {loading 0}\
   {othmodlist {}}} {
   set deplist {}

   # include or not requirements loaded after their dependent
   if {$nporeq} {
      set depListVar ::g_dependNPOHash
      set reqListVar ::g_moduleNPODepend
   } else {
      set depListVar ::g_dependHash
      set reqListVar ::g_moduleDepend
   }

   if {[info exists ${depListVar}($mod)]} {
      foreach depmod [set ${depListVar}($mod)] {
         set add 1
         # skip optional dependency if only looking for strong ones
         # look at an additionally processed mod list to determine if all
         # mods from a dependent list (composed of optional parts) are part
         # of the search, which means mod is not optional but strong dependent
         if {$strong && [llength $depmod] > 1} {
            foreach lmmodlist [set ${reqListVar}([lindex $depmod 0])] {
               if {[isInList $lmmodlist $mod]} {
                  foreach lmmod $lmmodlist {
                     # other mod part of the opt list is not there so mod
                     # is considered optional
                     if {[notInList $othmodlist $lmmod]} {
                        set add 0
                        break
                     }
                  }
                  break
               }
            }
         }

         if {$add} {
            lappend deplist [lindex $depmod 0]
         }
      }
   }

   # take currently loading modules into account if asked
   if {$loading} {
      set modlist [getLoadedModuleList]
      defineModEqProc [isIcase] [getConf extended_default]
      # reverse list to get closest match if returning lastly loaded module
      if {[getConf unload_match_order] eq {returnlast}} {
         set modlist [lreverse $modlist]
      }
      foreach loadingmod [getLoadingModuleList] {
         foreach prereq [getLoadedPrereq $loadingmod] {
            set lmprelist {}
            set moddep 0
            foreach modpre $prereq {
               foreach lmmod $modlist {
                  if {[doesModuleMatchesName $lmmod $modpre]} {
                     lappend lmprelist $lmmod
                     if {$lmmod eq $mod} {
                        set moddep 1
                     }
                     break
                  }
               }
            }
            if {$moddep && (!$strong || [llength $lmprelist] == 1)} {
               lappend deplist $loadingmod
               break
            }
         }
      }
   }

   return $deplist
}

# gets the list of all loaded modules which are dependent of passed modlist
# ordered by load position. strong argument controls whether only the active
# dependent modules should be returned or also those that are optional. direct
# argument controls if only dependent module directly requiring passed mods
# should be returned or its full dependent tree. nporeq argument tells if
# requirement loaded after their dependent should be returned. sat_constraint
# argument controls whether only the loaded module satisfying their constraint
# should be part or not of the resulting list. being_unload argument controls
# whether loaded modules in conflict with one or multiple modules from modlist
# should be added to the dependent list as these modules are currently being
# unloaded and these conflicting loaded modules should be refreshed.
proc getDependentLoadedModuleList {modlist {strong 1} {direct 1} {nporeq 0}\
   {loading 1} {sat_constraint 0} {being_unload 0}} {
   reportDebug "get loaded mod dependent of '$modlist' (strong=$strong,\
      direct=$direct, nporeq=$nporeq, loading=$loading,\
      sat_constraint=$sat_constraint, being_unload=$being_unload)"

   set deplist {}
   set fulllist $modlist
   # look at consistent requirements for unloading modules
   set unlonporeq [expr {$being_unload ? 0 : $nporeq}]
   foreach mod $modlist {
      # no duplicates or modules from query list
      eval appendNoDupToList fulllist [getDirectDependentList $mod $strong\
         $unlonporeq $loading $fulllist]
   }

   if {$being_unload} {
      # invite modules in violation with mods to be part of the dependent list
      # with their own dependent modules as mod is being unloaded. Achieve so
      # by faking that conflict violation is gone
      foreach mod $modlist {
         lassign [doesModuleConflict $mod] doescon modconlist
         if {$doescon} {
            unsetModuleConflictViolation $mod
            set conunvioarr($mod) $modconlist
            eval appendNoDupToList fulllist $modconlist
         }
      }
   }
   set unloadingmodlist [getUnloadingModuleList]
   for {set i [llength $modlist]} {$i < [llength $fulllist]} {incr i 1} {
      set depmod [lindex $fulllist $i]

      # skip already added mod or mod violating constraints if asked
      if {!$sat_constraint || [areModuleConstraintsSatisfied $depmod\
         $nporeq]} {
         # get dependent mod of dep mod when looking at full dep tree
         if {!$direct} {
            eval appendNoDupToList fulllist [getDirectDependentList $depmod\
               $strong $nporeq 0 $fulllist]
         }
         # avoid module currently unloading from result list
         if {[notInList $unloadingmodlist $depmod]} {
            lappend deplist $depmod
         }
      }
   }

   # restore conflict violation if any
   if {[array exists conunvioarr]} {
      foreach conunvio [array names conunvioarr] {
         setModuleConflictViolation $conunvio $conunvioarr($conunvio)
      }
   }

   # sort complete result list to match both loaded and dependency orders
   set sortlist [sortModulePerLoadedAndDepOrder $deplist $nporeq $loading]
   reportDebug "got '$sortlist'"
   return $sortlist
}

# test if passed 'mod' could be automatically unloaded or not, which means it
# has been loaded automatically and no loaded modules require it anymore.
# unmodlist: pass a list of modules that are going to be unloaded
proc isModuleUnloadable {mod {unmodlist {}}} {
   set ret 1
   # get currently unloading modules if no specific unmodlist set
   if {[llength $unmodlist] == 0} {
      set unmodlist [getUnloadingModuleList]
   }

   if {[isModuleUserAsked $mod]} {
      set ret 0
   } else {
      # mod is unloadable if all its dependent are unloaded or unloading
      foreach depmod [getDirectDependentList $mod] {
         if {[notInList $unmodlist $depmod]} {
            set ret 0
            break
         }
      }
   }

   return $ret
}

# gets the list of all loaded modules which are required by passed modlist
# ordered by load position.
proc getRequiredLoadedModuleList {modlist} {
   reportDebug "get mods required by '$modlist'"

   # search over all list of loaded modules, starting with passed module
   # list, then adding in turns their requirements
   set fulllist $modlist
   for {set i 0} {$i < [llength $fulllist]} {incr i 1} {
      # gets the list of loaded modules which are required by depmod
      eval appendNoDupToList fulllist $::g_moduleDepend([lindex $fulllist $i])
   }

   # sort complete result list to match both loaded and dependency orders
   set sortlist [sortModulePerLoadedAndDepOrder [lrange $fulllist [llength\
      $modlist] end]]
   reportDebug "got '$sortlist'"
   return $sortlist
}

# finds required modules that can be unloaded if passed modules are unloaded:
# they have been loaded automatically and are not depended (mandatory or
# optionally) by other module
proc getUnloadableLoadedModuleList {modlist} {
   reportDebug "get unloadable mods once '$modlist' unloaded"

   # search over all list of unloaded modules, starting with passed module
   # list, then adding in turns unloadable requirements
   set fulllist $modlist
   for {set i 0} {$i < [llength $fulllist]} {incr i 1} {
      set depmod [lindex $fulllist $i]
      # gets the list of loaded modules which are required by depmod
      set deplist {}
      foreach lmmodlist $::g_moduleDepend($depmod) {
         foreach lmmod $lmmodlist {
            if {[notInList $fulllist $lmmod]} {
               lappend deplist $lmmod
            }
         }
      }

      # get those required module that have been automatically loaded and are
      # only required by modules currently being unloaded
      foreach lmmod $deplist {
         if {[isModuleUnloadable $lmmod $fulllist]} {
            lappend fulllist $lmmod
         }
      }
   }

   # sort complete result list to match both loaded and dependency orders
   set sortlist [sortModulePerLoadedAndDepOrder [lrange $fulllist [llength\
      $modlist] end]]
   reportDebug "got '$sortlist'"
   return $sortlist
}

# runs the global RC files if they exist
proc runModulerc {} {
   set rclist {}

   reportDebug running...

   if {[set rcfile [getConf rcfile]] ne {}} {
      # if MODULERCFILE is a dir, look at a modulerc file in it
      if {[file isdirectory $rcfile]\
         && [file isfile $rcfile/modulerc]} {
         lappend rclist $rcfile/modulerc
      } elseif {[file isfile $rcfile]} {
         lappend rclist $rcfile
      }
   }
   if {[file isfile /usr/share/etc/rc]} {
      lappend rclist /usr/share/etc/rc
   }
   if {[info exists ::env(HOME)] && [file isfile $::env(HOME)/.modulerc]} {
      lappend rclist $::env(HOME)/.modulerc
   }

   setState rc_running 1
   foreach rc $rclist {
      if {[file readable $rc]} {
         reportDebug "Executing $rc"
         cmdModuleSource $rc
         lappendState rc_loaded $rc
      }
   }
   unsetState rc_running

   # identify alias or symbolic version set in these global RC files to be
   # able to include them or not in output or resolution processes
   array set ::g_rcAlias [array get ::g_moduleAlias]
   array set ::g_rcVersion [array get ::g_moduleVersion]
   array set ::g_rcVirtual [array get ::g_moduleVirtual]
}

# how many settings bundle are currently saved
proc getSavedSettingsStackDepth {} {
   return [llength $::g_SAVE_g_loadedModules]
}

# manage settings to save as a stack to have a separate set of settings
# for each module loaded or unloaded in order to be able to restore the
# correct set in case of failure
proc pushSettings {} {
   foreach var {env g_clearedEnvVars g_Aliases g_stateEnvVars g_stateAliases\
      g_stateFunctions g_Functions g_newXResources g_delXResources\
      g_loadedModules g_loadedModuleFiles g_loadedModuleUasked\
      g_loadedModuleConflict g_loadedModulePrereq g_loadedModuleAltname\
      g_loadedModuleAutoAltname g_loadedModuleAliasAltname g_moduleDepend\
      g_dependHash g_moduleNPODepend g_dependNPOHash g_prereqViolation\
      g_prereqNPOViolation g_conflictViolation g_moduleUnmetDep\
      g_unmetDepHash g_moduleEval g_moduleHiddenEval} {
      eval "lappend ::g_SAVE_$var \[array get ::$var\]"
   }
   # save non-array variable and indication if it was set
   foreach var {g_changeDir g_stdoutPuts g_return_text} {
      if {[info exists ::$var]} {
         eval "lappend ::g_SAVE_$var \[list 1 \[set ::$var\]\]"
      } else {
         eval "lappend ::g_SAVE_$var \[list 0 {}\]"
      }
   }
   reportDebug "settings saved (#[getSavedSettingsStackDepth])"
}

proc popSettings {} {
   set flushedid [getSavedSettingsStackDepth]
   foreach var {env g_clearedEnvVars g_Aliases g_stateEnvVars g_stateAliases\
      g_stateFunctions g_Functions g_newXResources g_delXResources\
      g_changeDir g_stdoutPuts g_return_text\
      g_loadedModules g_loadedModuleFiles g_loadedModuleUasked\
      g_loadedModuleConflict g_loadedModulePrereq g_loadedModuleAltname\
      g_loadedModuleAutoAltname g_loadedModuleAliasAltname g_moduleDepend\
      g_dependHash g_moduleNPODepend g_dependNPOHash g_prereqViolation\
      g_prereqNPOViolation g_conflictViolation g_moduleUnmetDep\
      g_unmetDepHash g_moduleEval g_moduleHiddenEval} {
      eval "set ::g_SAVE_$var \[lrange \$::g_SAVE_$var 0 end-1\]"
   }
   reportDebug "previously saved settings flushed (#$flushedid)"
}

proc restoreSettings {} {
   foreach var {g_clearedEnvVars g_Aliases g_stateEnvVars g_stateAliases\
      g_stateFunctions g_Functions g_newXResources g_delXResources\
      g_loadedModules g_loadedModuleFiles g_loadedModuleUasked\
      g_loadedModuleConflict g_loadedModulePrereq g_loadedModuleAltname\
      g_loadedModuleAutoAltname g_loadedModuleAliasAltname g_moduleDepend\
      g_dependHash g_moduleNPODepend g_dependNPOHash g_prereqViolation\
      g_prereqNPOViolation g_conflictViolation g_moduleUnmetDep\
      g_unmetDepHash g_moduleEval g_moduleHiddenEval} {
      # clear current $var arrays
      if {[info exists ::$var]} {
         eval "unset ::$var; array set ::$var {}"
      }
      eval "array set ::$var \[lindex \$::g_SAVE_$var end\]"
   }
   # specific restore mechanism for ::env as unsetting this array will make
   # Tcl stop monitoring env accesses and not update env variables anymore
   set envvarlist [list]
   foreach {var val} [lindex $::g_SAVE_env end] {
      lappend envvarlist $var
      interp-sync-env set $var $val
   }
   foreach var [array names ::env] {
      if {[notInList $envvarlist $var]} {
         interp-sync-env unset $var
      }
   }
   # restore non-array variable if it was set
   foreach var {g_changeDir g_stdoutPuts g_return_text} {
      if {[info exists ::$var]} {
         eval "unset ::$var"
      }
      eval "lassign \[lindex \$::g_SAVE_$var end\] isdefined val"
      if {$isdefined} {
         set ::$var $val
      }
   }
   reportDebug "previously saved settings restored\
      (#[getSavedSettingsStackDepth])"
}

proc renderSettings {} {
   global g_stateEnvVars g_stateAliases g_stateFunctions g_newXResources\
      g_delXResources

   reportDebug called.

   # required to work on cygwin, shouldn't hurt real linux
   fconfigure stdout -translation lf

   # preliminaries if there is stuff to render
   if {[getState autoinit] || [array size g_stateEnvVars] > 0 ||\
      [array size g_stateAliases] > 0 || [array size g_newXResources] > 0 ||\
      [array size g_stateFunctions] > 0 || [array size g_delXResources] > 0\
      || [info exists ::g_changeDir] || [info exists ::g_stdoutPuts] ||\
      [info exists ::g_return_text]} {
      switch -- [getState shelltype] {
         python {
            puts stdout {import os}
         }
      }
      set has_rendered 1
   } else {
      set has_rendered 0
   }

   if {[getState autoinit]} {
      renderAutoinit
   }

   # new environment variables
   foreach var [array names g_stateEnvVars] {
      switch -- $g_stateEnvVars($var) {
         new {
            switch -- [getState shelltype] {
               csh {
                  set val [charEscaped $::env($var)]
                  # csh barfs on long env vars
                  if {[getState shell] eq {csh} && [string length $val] >\
                     [getConf csh_limit]} {
                     if {$var eq {PATH}} {
                        reportWarning "PATH exceeds [getConf csh_limit]\
                           characters, truncating and appending\
                           /usr/bin:/bin ..."
                        set val [string range $val 0 [expr {[getConf\
                           csh_limit] - 1}]]:/usr/bin:/bin
                     } else {
                        reportWarning "$var exceeds [getConf csh_limit]\
                           characters, truncating..."
                         set val [string range $val 0 [expr {[getConf\
                           csh_limit]  - 1}]]
                     }
                  }
                  puts stdout "setenv $var $val;"
               }
               sh {
                  puts stdout "$var=[charEscaped $::env($var)];\
                     export $var;"
               }
               fish {
                  set val [charEscaped $::env($var)]
                  # fish shell has special treatment for PATH variable
                  # so its value should be provided as a list separated
                  # by spaces not by semi-colons
                  if {$var eq {PATH}} {
                     regsub -all : $val { } val
                  }
                  puts stdout "set -xg $var $val;"
               }
               tcl {
                  set val $::env($var)
                  puts stdout "set ::env($var) {$val};"
               }
               cmd {
                  set val $::env($var)
                  puts stdout "set $var=$val"
               }
               perl {
                  set val [charEscaped $::env($var) \']
                  puts stdout "\$ENV{'$var'} = '$val';"
               }
               python {
                  set val [charEscaped $::env($var) \']
                  puts stdout "os.environ\['$var'\] = '$val'"
               }
               ruby {
                  set val [charEscaped $::env($var) \']
                  puts stdout "ENV\['$var'\] = '$val'"
               }
               lisp {
                  set val [charEscaped $::env($var) \"]
                  puts stdout "(setenv \"$var\" \"$val\")"
               }
               cmake {
                  set val [charEscaped $::env($var) \"]
                  puts stdout "set(ENV{$var} \"$val\")"
               }
               r {
                  set val [charEscaped $::env($var) {\\'}]
                  puts stdout "Sys.setenv('$var'='$val')"
               }
            }
         }
         del {
            switch -- [getState shelltype] {
               csh {
                  puts stdout "unsetenv $var;"
               }
               sh {
                  puts stdout "unset $var;"
               }
               fish {
                  puts stdout "set -e $var;"
               }
               tcl {
                  puts stdout "catch {unset ::env($var)};"
               }
               cmd {
                  puts stdout "set $var="
               }
               perl {
                  puts stdout "delete \$ENV{'$var'};"
               }
               python {
                  puts stdout "os.environ\['$var'\] = ''"
                  puts stdout "del os.environ\['$var'\]"
               }
               ruby {
                  puts stdout "ENV\['$var'\] = nil"
               }
               lisp {
                  puts stdout "(setenv \"$var\" nil)"
               }
               cmake {
                  puts stdout "unset(ENV{$var})"
               }
               r {
                  puts stdout "Sys.unsetenv('$var')"
               }
            }
         }
      }
   }

   foreach var [array names g_stateAliases] {
      switch -- $g_stateAliases($var) {
         new {
            set val $::g_Aliases($var)
            # convert $n in !!:n and $* in !* on csh (like on compat version)
            if {[getState shelltype] eq {csh}} {
               regsub -all {([^\\]|^)\$([0-9]+)} $val {\1!!:\2} val
               regsub -all {([^\\]|^)\$\*} $val {\1!*} val
            }
            # unescape \$ after now csh-specific conversion is over
            regsub -all {\\\$} $val {$} val
            switch -- [getState shelltype] {
               csh {
                  set val [charEscaped $val]
                  puts stdout "alias $var $val;"
               }
               sh {
                  set val [charEscaped $val]
                  puts stdout "alias $var=$val;"
               }
               fish {
                  set val [charEscaped $val]
                  puts stdout "alias $var $val;"
               }
               cmd {
                  puts stdout "doskey $var=$val"
               }
            }
         }
         del {
            switch -- [getState shelltype] {
               csh {
                  puts stdout "unalias $var;"
               }
               sh {
                  puts stdout "unalias $var;"
               }
               fish {
                  puts stdout "functions -e $var;"
               }
               cmd {
                  puts stdout "doskey $var="
               }
            }
         }
      }
   }
   foreach funcname [array names g_stateFunctions] {
      switch -- $g_stateFunctions($funcname) {
         new {
            # trim function body to smoothly add a finishing ;
            set val [string trim $::g_Functions($funcname) "; \t\n\r"]
            switch -- [getState shelltype] {
               sh {
                  puts stdout "$funcname () { $val; }; export $funcname;"
               }
               fish {
                  puts stdout "function $funcname; $val; end;"
               }
            }
         }
         del {
            switch -- [getState shelltype] {
               sh {
                  puts stdout "unset -f $funcname;"
               }
               fish {
                  puts stdout "functions -e $funcname;"
               }
            }
         }
      }
   }

   # preliminaries for x-resources stuff
   if {[array size g_newXResources] > 0 || [array size g_delXResources] > 0} {
      switch -- [getState shelltype] {
         python {
            puts stdout {import subprocess}
         }
         ruby {
            puts stdout {require 'open3'}
         }
      }
   }

   # new x resources
   if {[array size g_newXResources] > 0} {
      # xrdb executable has already be verified in x-resource
      set xrdb [getCommandPath xrdb]
      foreach var [array names g_newXResources] {
         set val $g_newXResources($var)
         # empty val means that var is a file to parse
         if {$val eq {}} {
            switch -- [getState shelltype] {
               sh - csh - fish {
                  puts stdout "$xrdb -merge $var;"
               }
               tcl {
                  puts stdout "exec $xrdb -merge $var;"
               }
               perl {
                  puts stdout "system(\"$xrdb -merge $var\");"
               }
               python {
                  set var [charEscaped $var \']
                  puts stdout "subprocess.Popen(\['$xrdb',\
                     '-merge', '$var'\])"
               }
               ruby {
                  set var [charEscaped $var \']
                  puts stdout "Open3.popen2('$xrdb -merge $var')"
               }
               lisp {
                  puts stdout "(shell-command-to-string \"$xrdb\
                     -merge $var\")"
               }
               cmake {
                  puts stdout "execute_process(COMMAND $xrdb -merge $var)"
               }
               r {
                  set var [charEscaped $var {\\'}]
                  puts stdout "system('$xrdb -merge $var')"
               }
            }
         } else {
            switch -- [getState shelltype] {
               sh - csh - fish {
                  set var [charEscaped $var \"]
                  set val [charEscaped $val \"]
                  puts stdout "echo \"$var: $val\" | $xrdb -merge;"
               }
               tcl {
                  puts stdout "set XRDBPIPE \[open \"|$xrdb -merge\" r+\];"
                  set var [charEscaped $var \"]
                  set val [charEscaped $val \"]
                  puts stdout "puts \$XRDBPIPE \"$var: $val\";"
                  puts stdout {close $XRDBPIPE;}
                  puts stdout {unset XRDBPIPE;}
               }
               perl {
                  puts stdout "open(XRDBPIPE, \"|$xrdb -merge\");"
                  set var [charEscaped $var \"]
                  set val [charEscaped $val \"]
                  puts stdout "print XRDBPIPE \"$var: $val\\n\";"
                  puts stdout {close XRDBPIPE;}
               }
               python {
                  set var [charEscaped $var \']
                  set val [charEscaped $val \']
                  puts stdout "subprocess.Popen(\['$xrdb', '-merge'\],\
                     stdin=subprocess.PIPE).communicate(input='$var:\
                     $val\\n')"
               }
               ruby {
                  set var [charEscaped $var \']
                  set val [charEscaped $val \']
                  puts stdout "Open3.popen2('$xrdb -merge') {|i,o,t| i.puts\
                     '$var: $val'}"
               }
               lisp {
                  puts stdout "(shell-command-to-string \"echo $var:\
                     $val | $xrdb -merge\")"
               }
               cmake {
                  set var [charEscaped $var \"]
                  set val [charEscaped $val \"]
                  puts stdout "execute_process(COMMAND echo \"$var: $val\"\
                     COMMAND $xrdb -merge)"
               }
               r {
                  set var [charEscaped $var {\\'}]
                  set val [charEscaped $val {\\'}]
                  puts stdout "system('$xrdb -merge', input='$var: $val')"
               }
            }
         }
      }
   }

   if {[array size g_delXResources] > 0} {
      set xrdb [getCommandPath xrdb]
      set xres_to_del {}
      foreach var [array names g_delXResources] {
         # empty val means that var is a file to parse
         if {$g_delXResources($var) eq {}} {
            # xresource file has to be parsed to find what resources
            # are declared there and need to be unset
            foreach fline [split [exec $xrdb -n load $var] \n] {
               lappend xres_to_del [lindex [split $fline :] 0]
            }
         } else {
            lappend xres_to_del $var
         }
      }

      # xresource strings are unset by emptying their value since there
      # is no command of xrdb that can properly remove one property
      switch -- [getState shelltype] {
         sh - csh - fish {
            foreach var $xres_to_del {
               puts stdout "echo \"$var:\" | $xrdb -merge;"
            }
         }
         tcl {
            foreach var $xres_to_del {
               puts stdout "set XRDBPIPE \[open \"|$xrdb -merge\" r+\];"
               set var [charEscaped $var \"]
               puts stdout "puts \$XRDBPIPE \"$var:\";"
               puts stdout {close $XRDBPIPE;}
               puts stdout {unset XRDBPIPE;}
            }
         }
         perl {
            foreach var $xres_to_del {
               puts stdout "open(XRDBPIPE, \"|$xrdb -merge\");"
               set var [charEscaped $var \"]
               puts stdout "print XRDBPIPE \"$var:\\n\";"
               puts stdout {close XRDBPIPE;}
            }
         }
         python {
            foreach var $xres_to_del {
               set var [charEscaped $var \']
               puts stdout "subprocess.Popen(\['$xrdb', '-merge'\],\
                  stdin=subprocess.PIPE).communicate(input='$var:\\n')"
            }
         }
         ruby {
            foreach var $xres_to_del {
               set var [charEscaped $var \']
               puts stdout "Open3.popen2('$xrdb -merge') {|i,o,t| i.puts\
                  '$var:'}"
            }
         }
         lisp {
            foreach var $xres_to_del {
               puts stdout "(shell-command-to-string \"echo $var: |\
                  $xrdb -merge\")"
            }
         }
         cmake {
            foreach var $xres_to_del {
               set var [charEscaped $var \"]
               puts stdout "execute_process(COMMAND echo \"$var:\"\
                  COMMAND $xrdb -merge)"
            }
         }
         r {
            foreach var $xres_to_del {
               set var [charEscaped $var {\\'}]
               puts stdout "system('$xrdb -merge', input='$var:')"
            }
         }
      }
   }

   if {[info exists ::g_changeDir]} {
      switch -- [getState shelltype] {
         sh - csh - fish {
            puts stdout "cd '$::g_changeDir';"
         }
         tcl {
            puts stdout "cd \"$::g_changeDir\";"
         }
         cmd {
            puts stdout "cd $::g_changeDir"
         }
         perl {
            puts stdout "chdir '$::g_changeDir';"
         }
         python {
            puts stdout "os.chdir('$::g_changeDir')"
         }
         ruby {
            puts stdout "Dir.chdir('$::g_changeDir')"
         }
         lisp {
            puts stdout "(shell-command-to-string \"cd '$::g_changeDir'\")"
         }
         r {
            puts stdout "setwd('$::g_changeDir')"
         }
      }
      # cannot change current directory of cmake "shell"
   }

   # send content deferred during modulefile interpretation
   if {[info exists ::g_stdoutPuts]} {
      foreach putsArgs $::g_stdoutPuts {
         eval puts $putsArgs
         # check if a finishing newline will be needed after content sent
         set needPutsNl [expr {[lindex $putsArgs 0] eq {-nonewline}}]
      }
      if {$needPutsNl} {
         puts stdout {}
      }
   }

   # return text value if defined even if error happened
   if {[info exists ::g_return_text]} {
      reportDebug {text value should be returned.}
      renderText $::g_return_text
   } elseif {[getState error_count] > 0} {
      reportDebug "[getState error_count] error(s) detected."
      renderFalse
   } elseif {[getState return_false]} {
      reportDebug {false value should be returned.}
      renderFalse
   } elseif {$has_rendered} {
      # finish with true statement if something has been put
      renderTrue
   }
}

proc renderAutoinit {} {
   reportDebug called.

   # automatically detect which tclsh should be used for
   # future module commands
   set tclshbin [info nameofexecutable]

   # ensure script path is absolute
   set ::argv0 [getAbsolutePath $::argv0]

   switch -- [getState shelltype] {
      csh {
         set pre_hi {set _histchars = $histchars; unset histchars;}
         set post_hi {set histchars = $_histchars; unset _histchars;}
         set pre_pr {set _prompt=$prompt:q; set prompt="";}
         set post_pr {set prompt=$_prompt:q; unset _prompt;}
         # apply workaround for Tcsh history if set
         set eval_cmd [expr {[getConf wa_277] ? "eval `$tclshbin $::argv0\
            [getState shell] \\!*`;" :  "eval \"`$tclshbin $::argv0 [getState\
            shell] \\!*:q`\";"}]
         set pre_ex {set _exit="$status";}
         set post_ex {test 0 = $_exit}

         set fdef "if ( \$?histchars && \$?prompt )\
alias module '$pre_hi $pre_pr $eval_cmd $pre_ex $post_hi $post_pr $post_ex' ;
if ( \$?histchars && ! \$?prompt )\
alias module '$pre_hi $eval_cmd $pre_ex $post_hi $post_ex' ;
if ( ! \$?histchars && \$?prompt )\
alias module '$pre_pr $eval_cmd $pre_ex $post_pr $post_ex' ;
if ( ! \$?histchars && ! \$?prompt ) alias module '$eval_cmd' ;"
         if {[getConf ml]} {
            append fdef {
alias ml 'module ml \!*' ;}
         }
      }
      sh {
         # Considering the diversity of ways local variables are handled
         # through the sh-variants ('local' known everywhere except on ksh,
         # 'typeset' known everywhere except on pure-sh, and on some systems
         # the pure-sh is in fact a 'ksh'), no local variables are defined and
         # these variables that should have been local are unset at the end

         # on zsh, word splitting should be enabled explicitly
         set wsplit [expr {[getState shell] eq {zsh} ? {=} : {}}]
         # only redirect module from stderr to stdout when session is
         # attached to a terminal to avoid breaking non-terminal session
         # (scp, sftp, etc)
         set fname [expr {[getState is_stderr_tty] ? {_module_raw} : {module}}]
         # build quarantine mechanism in module function
         # an empty runtime variable is set even if no corresponding
         # MODULES_RUNENV_* variable found, as var cannot be unset on
         # modified environment command-line
         set fdef "${fname}() {"
         append fdef {
   unset _mlshdbg;
   if [ "${MODULES_SILENT_SHELL_DEBUG:-0}" = '1' ]; then
      case "$-" in
         *v*x*) set +vx; _mlshdbg='vx' ;;
         *v*) set +v; _mlshdbg='v' ;;
         *x*) set +x; _mlshdbg='x' ;;
         *) _mlshdbg='' ;;
      esac;
   fi;}
         append fdef "
   unset _mlre _mlIFS;
   if \[ -n \"\${IFS+x}\" \]; then
      _mlIFS=\$IFS;
   fi;
   IFS=' ';
   for _mlv in \${${wsplit}MODULES_RUN_QUARANTINE:-}; do"
         append fdef {
      if [ "${_mlv}" = "${_mlv##*[!A-Za-z0-9_]}" -a "${_mlv}" = "${_mlv#[0-9]}" ]; then
         if [ -n "`eval 'echo ${'$_mlv'+x}'`" ]; then
            _mlre="${_mlre:-}${_mlv}_modquar='`eval 'echo ${'$_mlv'}'`' ";
         fi;
         _mlrv="MODULES_RUNENV_${_mlv}";
         _mlre="${_mlre:-}${_mlv}='`eval 'echo ${'$_mlrv':-}'`' ";
      fi;
   done;
   if [ -n "${_mlre:-}" ]; then}
         append fdef "\n      eval `eval \${${wsplit}_mlre} $tclshbin $::argv0\
[getState shell] '\"\$@\"'`;
   else
      eval `$tclshbin $::argv0 [getState shell] \"\$@\"`;
   fi;"
#         append fdef "
#   eval `$tclshbin $::argv0 [getState shell] \"\$@\"`;"
         append fdef {
   _mlstatus=$?;}
         append fdef {
   if [ -n "${_mlIFS+x}" ]; then
      IFS=$_mlIFS;
   else
      unset IFS;
   fi;
   unset _mlre _mlv _mlrv _mlIFS;}
         append fdef {
   if [ -n "${_mlshdbg:-}" ]; then
      set -$_mlshdbg;
   fi;
   unset _mlshdbg;}
         append fdef {
   return $_mlstatus;}
         append fdef "\n};"
         if {[getState is_stderr_tty]} {
            append fdef "\nmodule() { _module_raw \"\$@\" 2>&1; };"
         }
         if {[getConf ml]} {
            append fdef {
ml() { module ml "$@"; };}
         }
      }
      fish {
         set fdef [expr {[getState is_stderr_tty] ? "function _module_raw\n" :\
            "function module\n"}]
         append fdef {   set -l _mlre ''; set -l _mlv; set -l _mlrv;
   for _mlv in (string split ' ' $MODULES_RUN_QUARANTINE)
      if string match -r '^[A-Za-z_][A-Za-z0-9_]*$' $_mlv >/dev/null
         if set -q $_mlv
            set _mlre $_mlre$_mlv"_modquar='$$_mlv' "
         end
         set _mlrv "MODULES_RUNENV_$_mlv"
         set _mlre "$_mlre$_mlv='$$_mlrv' "
      end
   end
   if [ -n "$_mlre" ]
      set _mlre "env $_mlre"
   end}
         # use "| source -" rather than "eval" to be able
         # to redirect stderr after stdout being evaluated
         append fdef "\n   eval \$_mlre $tclshbin $::argv0 [getState shell]\
            (string escape -- \$argv) | source -\n"
#         append fdef "   eval $tclshbin $::argv0 [getState shell]\
            (string escape -- \$argv) | source -\n"
         if {[getState is_stderr_tty]} {
            append fdef {end
function module
   _module_raw $argv 2>&1
end}
         } else {
            append fdef end
         }
         if {[getConf ml]} {
            append fdef {
function ml
   module ml $argv
end}
         }
      }
      tcl {
         set fdef "proc module {args} {"
         append fdef {
   set _mlre {};
   if {[info exists ::env(MODULES_RUN_QUARANTINE)]} {
      foreach _mlv [split $::env(MODULES_RUN_QUARANTINE) " "] {
         if {[regexp {^[A-Za-z_][A-Za-z0-9_]*$} $_mlv]} {
            if {[info exists ::env($_mlv)]} {
               lappend _mlre "${_mlv}_modquar=$::env($_mlv)"
            }
            set _mlrv "MODULES_RUNENV_${_mlv}"
            lappend _mlre [expr {[info exists ::env($_mlrv)] ?\
               "${_mlv}=$::env($_mlrv)" : "${_mlv}="}]
         }
      }
      if {[llength $_mlre] > 0} {
         set _mlre [linsert $_mlre 0 "env"]
      }
   }}
      append fdef {
   set _mlstatus 1;}
         append fdef "\n   catch {eval exec \$_mlre \"$tclshbin\"\
            \"$::argv0\" \"[getState shell]\" \$args 2>@stderr} script\n"
#         append fdef "\n   catch {eval exec \"$tclshbin\"\
            \"$::argv0\" \"[getState shell]\" \$args 2>@stderr} script\n"
         append fdef {   eval $script;
   return $_mlstatus}
         append fdef "\n}"
         if {[getConf ml]} {
            append fdef {
proc ml {args} {
   return [eval module ml $args]
}}
         }
      }
      cmd {
         reportErrorAndExit {No autoinit mode available for 'cmd' shell}
      }
      perl {
         set fdef "sub module {"
         append fdef {
   my $_mlre = '';
   if (defined $ENV{'MODULES_RUN_QUARANTINE'}) {
      foreach my $_mlv (split(' ', $ENV{'MODULES_RUN_QUARANTINE'})) {
         if ($_mlv =~ /^[A-Za-z_][A-Za-z0-9_]*$/) {
            if (defined $ENV{$_mlv}) {
               $_mlre .= "${_mlv}_modquar='$ENV{$_mlv}' ";
            }
            my $_mlrv = "MODULES_RUNENV_$_mlv";
            $_mlre .= "$_mlv='$ENV{$_mlrv}' ";
        }
      }
      if ($_mlre ne "") {
         $_mlre = "env $_mlre";
      }
   }}
         append fdef {
   my $args = '';
   if (@_ > 0) {
      $args = '"' . join('" "', @_) . '"';
   }
   my $_mlstatus = 1;}
         append fdef "\n   eval `\${_mlre}$tclshbin $::argv0 perl \$args`;\n"
#         append fdef "\n   eval `$tclshbin $::argv0 perl \$args`;\n"
         append fdef {   return $_mlstatus;}
         append fdef "\n}"
         if {[getConf ml]} {
            append fdef {
sub ml {
   return module('ml', @_);
}}
         }
      }
      python {
         set fdef {import re, subprocess
def module(*arguments):}
         append fdef {
   _mlre = os.environ.copy()
   if 'MODULES_RUN_QUARANTINE' in os.environ:
      for _mlv in os.environ['MODULES_RUN_QUARANTINE'].split():
         if re.match('^[A-Za-z_][A-Za-z0-9_]*$', _mlv):
            if _mlv in os.environ:
               _mlre[_mlv + '_modquar'] = os.environ[_mlv]
            _mlrv = 'MODULES_RUNENV_' + _mlv
            if _mlrv in os.environ:
               _mlre[_mlv] = os.environ[_mlrv]
            else:
               _mlre[_mlv] = ''}
         append fdef {
   ns = {}}
         append fdef "\n   exec(subprocess.Popen(\['$tclshbin',\
            '$::argv0', 'python'\] + list(arguments),\
            stdout=subprocess.PIPE, env=_mlre).communicate()\[0\], ns)\n"
#         append fdef "\n   exec(subprocess.Popen(\['$tclshbin',\
            '$::argv0', 'python'\] + list(arguments),\
            stdout=subprocess.PIPE).communicate()\[0\], ns)\n"
         append fdef {   if '_mlstatus' in ns:
      _mlstatus = ns['_mlstatus']
   else:
      _mlstatus = True
   return _mlstatus}
         if {[getConf ml]} {
            append fdef {
def ml(*arguments):
   return module('ml', *arguments)
}
         }
      }
      ruby {
         set fdef {class ENVModule
   def ENVModule.module(*args)}
         append fdef {
      _mlre = ''
      if ENV.has_key?('MODULES_RUN_QUARANTINE') then
         ENV['MODULES_RUN_QUARANTINE'].split(' ').each do |_mlv|
            if _mlv =~ /^[A-Za-z_][A-Za-z0-9_]*$/ then
               if ENV.has_key?(_mlv) then
                  _mlre << _mlv + "_modquar='" + ENV[_mlv].to_s + "' "
               end
               _mlrv = 'MODULES_RUNENV_' + _mlv
               _mlre << _mlv + "='" + ENV[_mlrv].to_s + "' "
            end
         end
         unless _mlre.empty?
            _mlre = 'env ' + _mlre
         end
      end}
         append fdef {
      if args[0].kind_of?(Array) then
         args = args[0]
      end
      if args.length == 0 then
         args = ''
      else
         args = "\"#{args.join('" "')}\""
      end
      _mlstatus = true}
         append fdef "\n      eval `#{_mlre}$tclshbin $::argv0 ruby #{args}`\n"
#         append fdef "\n      eval `$tclshbin $::argv0 ruby #{args}`\n"
         append fdef {      return _mlstatus
   end}
         if {[getConf ml]} {
            append fdef {
   def ENVModule.ml(*args)
      return ENVModule.module('ml', *args)
   end}
         }
         append fdef {
end}
      }
      lisp {
         reportErrorAndExit {lisp mode autoinit not yet implemented}
      }
      cmake {
         set pre_exec "\n      execute_process(COMMAND \${_mlre} $tclshbin\
            $::argv0 cmake "
#         set pre_exec "\n      execute_process(COMMAND $tclshbin\
            $::argv0 cmake "
         set post_exec "\n         OUTPUT_FILE \${tempfile_name})\n"
         set fdef {function(module)
   cmake_policy(SET CMP0007 NEW)}
         append fdef {
   set(_mlre "")
   if(DEFINED ENV{MODULES_RUN_QUARANTINE})
      string(REPLACE " " ";" _mlv_list "$ENV{MODULES_RUN_QUARANTINE}")
      foreach(_mlv ${_mlv_list})
         if(${_mlv} MATCHES "^[A-Za-z_][A-Za-z0-9_]*$")
            if(DEFINED ENV{${_mlv}})
               set(_mlre "${_mlre}${_mlv}_modquar=$ENV{${_mlv}};")
            endif()
            set(_mlrv "MODULES_RUNENV_${_mlv}")
            set(_mlre "${_mlre}${_mlv}=$ENV{${_mlrv}};")
        endif()
      endforeach()
      if (NOT "${_mlre}" STREQUAL "")
         set(_mlre "env;${_mlre}")
      endif()
   endif()}
         append fdef {
   set(_mlstatus TRUE)
   execute_process(COMMAND mktemp -t moduleinit.cmake.XXXXXXXXXXXX
      OUTPUT_VARIABLE tempfile_name
      OUTPUT_STRIP_TRAILING_WHITESPACE)
   if(${ARGC} EQUAL 1)}
            # adapt command definition depending on the number of args to be
            # able to pass to some extend (<5 args) empty string element to
            # modulecmd (no other way as empty element in ${ARGV} are skipped
            append fdef "$pre_exec\"\${ARGV0}\"$post_exec"
            append fdef {   elseif(${ARGC} EQUAL 2)}
            append fdef "$pre_exec\"\${ARGV0}\" \"\${ARGV1}\"$post_exec"
            append fdef {   elseif(${ARGC} EQUAL 3)}
            append fdef "$pre_exec\"\${ARGV0}\" \"\${ARGV1}\"\
               \"\${ARGV2}\"$post_exec"
            append fdef {   elseif(${ARGC} EQUAL 4)}
            append fdef "$pre_exec\"\${ARGV0}\" \"\${ARGV1}\"\
               \"\${ARGV2}\" \"\${ARGV3}\"$post_exec"
            append fdef {   else()}
            append fdef "$pre_exec\${ARGV}$post_exec"
            append fdef {   endif()
   if(EXISTS ${tempfile_name})
      include(${tempfile_name})
      file(REMOVE ${tempfile_name})
   endif()
   set(module_result ${_mlstatus} PARENT_SCOPE)
endfunction(module)}
         if {[getConf ml]} {
            append fdef {
function(ml)
   module(ml ${ARGV})
   set(module_result ${module_result} PARENT_SCOPE)
endfunction(ml)}
         }
      }
      r {
         set fdef "module <- function(...){"
         append fdef {
   mlre <- ''
   if (!is.na(Sys.getenv('MODULES_RUN_QUARANTINE', unset=NA))) {
      for (mlv in strsplit(Sys.getenv('MODULES_RUN_QUARANTINE'), ' ')[[1]]) {
         if (grepl('^[A-Za-z_][A-Za-z0-9_]*$', mlv)) {
            if (!is.na(Sys.getenv(mlv, unset=NA))) {
               mlre <- paste0(mlre, mlv, "_modquar='", Sys.getenv(mlv), "' ")
            }
            mlrv <- paste0('MODULES_RUNENV_', mlv)
            mlre <- paste0(mlre, mlv, "='", Sys.getenv(mlrv), "' ")
         }
      }
      if (mlre != '') {
         mlre <- paste0('env ', mlre)
      }
   }}
         append fdef {
   arglist <- as.list(match.call())
   arglist[1] <- 'r'
   args <- paste0('"', paste0(arglist, collapse='" "'), '"')}
         append fdef "\n   cmd <- paste(mlre, '$tclshbin', '$::argv0', args,\
            sep=' ')\n"
#         append fdef "\n   cmd <- paste('$tclshbin', '$::argv0', args,\
            sep=' ')\n"
         append fdef {   mlstatus <- TRUE
   hndl <- pipe(cmd)
   eval(expr = parse(file=hndl))
   close(hndl)
   invisible(mlstatus)}
         append fdef "\n}"
         if {[getConf ml]} {
            append fdef {
ml <- function(...){
   module('ml', ...)
}}
         }
      }
   }

   # output function definition
   puts stdout $fdef
}

proc cacheCurrentModules {{exitonerr 1}} {
   # parse loaded modules information only once, global arrays are updated
   # afterwards when module commands update loaded modules state
   if {![isStateDefined lm_info_cached]} {
      setState lm_info_cached 1
      # mark specific as well as generic modules as loaded
      set i 0
      set modfilelist [getLoadedModuleFileList]
      set modlist [getLoadedModuleList]
      set nuaskedlist [getLoadedModuleNotUserAskedList]

      if {[llength $modlist] == [llength $modfilelist]} {
         # cache declared tags of loaded modules
         foreach modtag [getLoadedModuleTagList] {
            eval setModuleTag $modtag
         }

         # cache declared alternative names of loaded modules
         foreach modalt [getLoadedModuleAltnameList] {
            eval setLoadedAltname $modalt
         }

         # cache declared source-sh of loaded modules
         foreach modsrcsh [getLoadedModuleSourceShList] {
            eval setLoadedSourceSh $modsrcsh
         }

         # cache declared conflict of loaded modules
         foreach modcon [getLoadedModuleConflictList] {
            # parse module version specification to record translation
            foreach modconelt [lrange $modcon 1 end] {
               eval parseModuleVersionSpecifier 0 $modconelt
            }
            eval setLoadedConflict $modcon
         }

         # cache declared prereq of loaded modules, prior to setLoadedModule
         # which triggers dependency chain build
         foreach modpre [getLoadedModulePrereqList] {
            # parse module version specification to record translation
            foreach modpreeltlist [lrange $modpre 1 end] {
               foreach modpreelt $modpreeltlist {
                  eval parseModuleVersionSpecifier 0 $modpreelt
               }
            }
            eval setLoadedPrereq $modpre
         }

         foreach mod $modlist {
            setLoadedModule $mod [lindex $modfilelist $i] [notInList\
               $nuaskedlist $mod]
            incr i
         }

         reportDebug "$i loaded"
      } else {
         set errproc [expr {$exitonerr ? {reportErrorAndExit} :\
            {reportError}}]
         $errproc "Loaded environment state is\
            inconsistent\nLOADEDMODULES=$modlist\n_LMFILES_=$modfilelist"
      }
   }
}

# This proc resolves module aliases or version aliases to the real module name
# and version.
proc resolveModuleVersionOrAlias {name icase} {
   set name [getArrayKey ::g_moduleResolved $name $icase]
   if {[info exists ::g_moduleResolved($name)]} {
      set ret $::g_moduleResolved($name)
   } else {
      set ret $name
   }

   reportTrace "'$name' into '$ret'" Resolve
   return $ret
}

proc charEscaped {str {charlist { \\\t\{\}|<>!;#^$&*?"'`()}}} {
   return [regsub -all "\(\[$charlist\]\)" $str {\\\1}]
}

proc charUnescaped {str {charlist { \\\t\{\}|<>!;#^$&*?"'`()}}} {
   return [regsub -all "\\\\\(\[$charlist\]\)" $str {\1}]
}

proc strTo {lang str {esc 1}} {
   switch -- $lang {
      tcl { set enco \{; set encc \}}
      shell { set enco '; set encc '}
   }
   # escape all special characters
   if {$esc} {
      set str [charEscaped $str]
   }
   # enclose if empty or if contain a space character unless already escaped
   if {$str eq {} || (!$esc && [regexp {\s} $str])} {
      set str "$enco$str$encc"
   }
   return $str
}

proc listTo {lang lst {esc 1}} {
   set lout [list]
   # transform each list element
   foreach str $lst {
      lappend lout [strTo $lang $str $esc]
   }
   return [join $lout { }]
}

# find command path and remember it
proc getCommandPath {cmd} {
   return [lindex [auto_execok $cmd] 0]
}

# find then run command or raise error if command not found
proc runCommand {cmd args} {
   set cmdpath [getCommandPath $cmd]
   if {$cmdpath eq {}} {
      knerror "Command '$cmd' cannot be found" MODULES_ERR_GLOBAL
   } else {
      return [eval exec $cmdpath $args]
   }
}

proc getAbsolutePath {path} {
   # currently executing a modulefile or rc, so get the directory of this file
   if {[currentState modulefile] ne {}} {
      set curdir [file dirname [currentState modulefile]]
   # elsewhere get module command current working directory
   } else {
      # register pwd at first call
      if {![isStateDefined cwd]} {
         setState cwd [pwd]
      }
      set curdir [getState cwd]
   }

   # empty result if empty path
   if {$path eq {}} {
      set abspath {}
   # consider path absolute if it starts with a variable ref
   } elseif {[string index $path 0] eq {$}} {
      set abspath $path
   } else {
      set abslist {}
      # get a first version of the absolute path by joining the current
      # working directory to the given path. if given path is already absolute
      # 'file join' will not break it as $curdir will be ignored as soon a
      # beginning '/' character is found on $path. this first pass also clean
      # extra '/' character. then each element of the path is analyzed to
      # clear "." and ".." components.
      foreach elt [file split [file join $curdir $path]] {
         if {$elt eq {..}} {
            # skip ".." element if it comes after root element, remove last
            # element elsewhere
            if {[llength $abslist] > 1} {
               set abslist [lreplace $abslist end end]
            }
         # skip any "." element
         } elseif {$elt ne {.}} {
            lappend abslist $elt
         }
      }
      set abspath [eval file join $abslist]
   }

   # return cleaned absolute path
   return $abspath
}

# if no exact match found but icase mode is enabled then search if an icase
# match exists among all array key elements, select dictionary highest version
# if multiple icase matches are returned
proc getArrayKey {arrname name icase} {
   if {$icase} {
      upvar $arrname arr
      if {![info exists arr($name)]} {
         foreach elt [lsort -dictionary -decreasing [array names arr]] {
            if {[string equal -nocase $name $elt]} {
               reportDebug "key '$elt' in array '$arrname' matches '$name'"
               set name $elt
               break
            }
         }
      }
   }
   return $name
}

# Define procedure to compare module names set as array keys against pattern.
# Adapt procedure code whether implicit_default is enabled or disabled
proc defineGetEqArrayKeyProc {icase extdfl impdfl} {
   set procname getEqArrayKeyProc
   if {$impdfl} {
      append procname Impdfl
   }

   # define proc if not done yet or if it was defined for another context
   if {[info procs getEqArrayKey] eq {} || $::g_getEqArrayKey_proc ne\
      $procname} {
      if {[info exists ::g_getEqArrayKey_proc]} {
         rename ::getEqArrayKey ::$::g_getEqArrayKey_proc
      }
      rename ::$procname ::getEqArrayKey
      set ::g_getEqArrayKey_proc $procname
   }

   # also define modEq which is called by getEqArrayKey
   defineModEqProc $icase $extdfl
}

# alternative definitions of getEqArrayKey proc
proc getEqArrayKeyProcImpdfl {arrname name} {
   set icase [isIcase]
   upvar $arrname arr

   # extract single module specified if any
   lassign [getModuleVersSpec $name] mod modname
   # check name eventual icase match
   set mod [getArrayKey arr [string trimright $mod /] $icase]

   if {$mod ne {} && [info exists arr($mod)]} {
      set match $mod
   } else {
      set mlist {}
      foreach elt [array names arr] {
         if {[modEq $name $elt]} {
            lappend mlist $elt
         }
      }
      if {[llength $mlist] == 1} {
         set match [lindex $mlist 0]
      # in case multiple modules match query, check directory default and
      # return it if it is part of match list, elsewhere return highest result
      } elseif {[llength $mlist] > 1} {
         # get corresponding icase parent directory
         set pname [getArrayKey arr $modname $icase]
         if {[info exists arr($pname)]} {
            set dfl $pname/[lindex $arr($pname) 1]
         }
         # resolve symbolic version entries
         foreach elt $mlist {
            if {[lindex $arr($elt) 0] eq {version}} {
               lappend mrlist [lindex $arr($elt) 1]
            } else {
               lappend mrlist $elt
            }
         }
         if {[info exists dfl] && [isInList $mrlist $dfl]} {
            set match $dfl
         } else {
            set match [lindex [lsort -dictionary $mrlist] end]
         }
      }
   }
   if {[info exists match]} {
      reportDebug "key '$match' in array '$arrname' matches '$name'"
      set name $match
   }
   return $name
}
proc getEqArrayKeyProc {arrname name} {
   set icase [isIcase]
   upvar $arrname arr

   lassign [getModuleVersSpec $name] mod modname
   # check name eventual icase match
   set mod [getArrayKey arr [string trimright $mod /] $icase]

   if {$mod ne {} && [info exists arr($mod)]} {
      set match $mod
   } else {
      set mlist {}
      foreach elt [array names arr] {
         if {[modEq $name $elt]} {
            lappend mlist $elt
         }
      }
      # must have a default part of result even if only one result
      if {[llength $mlist] >= 1} {
         # get corresponding icase parent directory
         set pname [getArrayKey arr $modname $icase]
         if {[info exists arr($pname)]} {
            set dfl $pname/[lindex $arr($pname) 1]
         }
         # resolve symbolic version entries
         foreach elt $mlist {
            if {[lindex $arr($elt) 0] eq {version}} {
               lappend mrlist [lindex $arr($elt) 1]
            } else {
               lappend mrlist $elt
            }
         }
         if {[info exists dfl] && [isInList $mrlist $dfl]} {
            set match $dfl
         } else {
            # raise error as no default part of result
            upvar retlist retlist
            set retlist [list {} $name none "No default version\
               defined for '$name'"]
         }
      }
   }
   if {[info exists match]} {
      reportDebug "key '$match' in array '$arrname' matches '$name'"
      set name $match
   }
   return $name
}

# split string while ignore any separator character that is espaced
proc psplit {str sep} {
   # use standard split if no sep character found
   if {[string first \\$sep $str] == -1} {
      set res [split $str $sep]
   } else {
      set previdx -1
      set idx [string first $sep $str]
      while {$idx != -1} {
         # look ahead if found separator is escaped
         if {[string index $str [expr {$idx-1}]] ne "\\"} {
            # unescape any separator character when adding to list
            lappend res [charUnescaped [string range $str [expr {$previdx+1}]\
               [expr {$idx-1}]] $sep]
            set previdx $idx
         }
         set idx [string first $sep $str [expr {$idx+1}]]
      }

      lappend res [charUnescaped [string range $str [expr {$previdx+1}] end]\
         $sep]
   }

   return $res
}

# join list while escape any character equal to separator
proc pjoin {lst sep} {
   # use standard join if no sep character found
   if {[string first $sep $lst] == -1} {
      set res [join $lst $sep]
   } else {
      set res {}
      foreach elt $lst {
         # preserve empty entries
         if {[info exists not_first]} {
            append res $sep
         } else {
            set not_first 1
         }
         # escape any separator character when adding to string
         append res [charEscaped $elt $sep]
      }
   }

   return $res
}

# Is provided string a version number: consider first element of string if
# '.' character used in it. [0-9af] on this first part is considered valid
# anything else could be used in latter elements
proc isVersion {str} {
   return [string is xdigit -strict [lindex [split $str .] 0]]
}

# Return number of occurences of passed character in passed string
proc countChar {str char} {
   return [expr {[string length $str] - [string length [string map [list\
      $char {}] $str]]}]
}

# provide a lreverse proc for Tcl8.4 and earlier
if {[info commands lreverse] eq {}} {
   proc lreverse {l} {
      set r [list]
      for {set i [expr {[llength $l] - 1}]} {$i >= 0} {incr i -1} {
         lappend r [lindex $l $i]
      }
      return $r
   }
}

# provide a lassign proc for Tcl8.4 and earlier
if {[info commands lassign] eq {}} {
   proc lassign {values args} {
      uplevel 1 [list foreach $args [linsert $values end {}] break]
      lrange $values [llength $args] end
   }
}

proc isInList {lst elt} {
   return [expr {[lsearch -exact $lst $elt] != -1}]
}

proc notInList {lst elt} {
   return [expr {[lsearch -exact $lst $elt] == -1}]
}

proc appendNoDupToList {lstname args} {
   set ret 0
   upvar $lstname lst
   foreach elt $args {
      if {![info exists lst] || [notInList $lst $elt]} {
         lappend lst $elt
         set ret 1
      }
   }
   return $ret
}

proc replaceFromList {list1 item {item2 {}}} {
    while {[set xi [lsearch -exact $list1 $item]] >= 0} {
       set list1 [if {[string length $item2] == 0} {lreplace $list1 $xi $xi}\
         {lreplace $list1 $xi $xi $item2}]
    }

    return $list1
}

# test if 2 lists have at least one element in common
proc isIntBetweenList {list1 list2} {
   foreach elt $list1 {
      if {[isInList $list2 $elt]} {
         return 1
      }
   }
   return 0
}

# test if 2 lists have at least one element in diff
proc isDiffBetweenList {list1 list2} {
   foreach elt $list1 {
      if {[notInList $list2 $elt]} {
         return 1
      }
   }
   return 0
}

# returns elements from list1 not part of list2 and elements from list2 not
# part of list1
proc getDiffBetweenList {list1 list2} {
   set res1 [list]
   set res2 [list]

   foreach elt $list1 {
      if {[notInList $list2 $elt]} {
         lappend res1 $elt
      }
   }
   foreach elt $list2 {
      if {[notInList $list1 $elt]} {
         lappend res2 $elt
      }
   }

   return [list $res1 $res2]
}

# return elements from arr1 not in arr2, elements from arr1 in arr2 but with a
# different value and elements from arr2 not in arr1
proc getDiffBetweenArray {arrname1 arrname2} {
   upvar $arrname1 arr1
   upvar $arrname2 arr2
   set notin2 [list]
   set diff [list]
   set notin1 [list]

   foreach name [array names arr1] {
      # element in arr1 not in arr2
      if {![info exists arr2($name)]} {
         lappend notin2 $name
      # element present in both arrays but with a different value
      } elseif {$arr1($name) ne $arr2($name)} {
         lappend diff $name
      }
   }

   foreach name [array names arr2] {
      # element in arr2 not in arr1
      if {![info exists arr1($name)]} {
         lappend notin1 $name
      }
   }

   return [list $notin2 $diff $notin1]
}

proc parseAccessIssue {modfile} {
   # retrieve and return access issue message
   if {[regexp {POSIX .* \{(.*)\}$} $::errorCode match errMsg]} {
      return "[string totitle $errMsg] on '$modfile'"
   } else {
      return "Cannot access '$modfile'"
   }
}

proc checkValidModule {modfile} {
   reportDebug $modfile

   # test file only once, cache result obtained to minimize file query
   return [expr {[info exists ::g_modfileValid($modfile)]\
      ? $::g_modfileValid($modfile)\
      : [set ::g_modfileValid($modfile) [readModuleContent $modfile 1 1 1]]}]
}

# get file modification time, cache it at first query, use cache afterward
proc getFileMtime {fpath} {
   if {[info exists ::g_fileMtime($fpath)]} {
      return $::g_fileMtime($fpath)
   } else {
      return [set ::g_fileMtime($fpath) [file mtime $fpath]]
   }
}

# define proc that will be used as fallback to command provided by extension
# library in case this library is not loaded
proc __readFile {filename {firstline 0} {must_have_cookie 0}} {
   set fid [open $filename r]
   if {$firstline} {
      set fdata [gets $fid]
   } else {
      # read a first data chunk and check if magic cookie is there if
      # mandatory. skip read of any additional content if mandatory cookie is
      # not found
      set fdata [read $fid 4096]
      if {![eof $fid] && (!$must_have_cookie || [string equal -length 8\
         $fdata {#%Module}])} {
         append fdata [read $fid]
      }
   }
   close $fid
   return $fdata
}

proc readModuleContent {modfile {report_read_issue 0} {must_have_cookie 1}\
   {only_check_validity 0}} {
   reportDebug $modfile
   set res {}

   # read file
   if {[catch {
      if {[info exists ::g_modfileContent($modfile)]} {
         lassign $::g_modfileContent($modfile) fh fdata
      } else {
         # only read beginning of file if just checking validity and not
         # asked to always fully read files. when full file should be read
         # cookie is checked if asked to avoid to fully read non modfile
         set fdata [readFile $modfile [expr {$only_check_validity &&\
            ![currentState always_read_full_file]}] $must_have_cookie]
         # extract magic cookie (first word of modulefile)
         set fh [string trimright [lindex [split [string range $fdata 0 32]]\
            0] #]
         # cache full file read to minimize file operations
         if {!$only_check_validity || [currentState always_read_full_file]} {
            set ::g_modfileContent($modfile) [list $fh $fdata]
         }
      }
   } errMsg ]} {
      if {$report_read_issue} {
         set msg [parseAccessIssue $modfile]
         if {$only_check_validity} {
            set res [list accesserr $msg]
         } else {
            reportError $msg
         }
      }
   } else {
      # check module validity if magic cookie is mandatory
      if {$must_have_cookie && ![string equal -length 8 $fh {#%Module}]} {
         set msg {Magic cookie '#%Module' missing}
         if {$only_check_validity} {
            set res [list invalid $msg]
         } else {
            reportInternalBug $msg $modfile
         }
      # check if min version requirement is met if magic cookie is mandatory
      } elseif {$must_have_cookie && [string length $fh] > 8 &&\
         [getConf mcookie_version_check] && [versioncmp {4.7.0}\
         [string range $fh 8 end]] <0} {
         set msg "Modulefile requires at least Modules version [string range\
            $fh 8 end]"
         if {$only_check_validity} {
            set res [list invalid $msg]
         } else {
            reportInternalBug $msg $modfile
         }
      } else {
         if {$only_check_validity} {
            # set validity information as result
            set res [list true {}]
         } else {
            # set file content as result
            set res $fdata
         }
      }
   }

   return $res
}

# If given module maps to default or other symbolic versions, a list of
# those versions is returned. This takes module/version as an argument.
proc getVersAliasList {mod} {
   set sym_list {}
   if {[info exists ::g_symbolHash($mod)]} {
      set sym_list $::g_symbolHash($mod)
      # withdraw hidden symbol from list
      if {[info exists ::g_hiddenSymHash($mod)]} {
         lassign [getDiffBetweenList $sym_list $::g_hiddenSymHash($mod)]\
            sym_list
      }
   }

   reportDebug "'$mod' has symbolic version list '$sym_list'"
   return $sym_list
}

proc doesModuleHaveSym {mod} {
   # is there any non-hidden symbol for mod
   return [expr {[info exists ::g_symbolHash($mod)] && (![info exists\
      ::g_hiddenSymHash($mod)] || [llength [lindex [getDiffBetweenList\
      $::g_symbolHash($mod) $::g_hiddenSymHash($mod)] 0]] > 0)}]
}

# get list of elements located in a directory passed as argument. a flag is
# set after each element to know if it is considered hidden or not. a
# fetch_dotversion argument controls whether .version file should be looked at
# in directory .proc will be used as a fallback to command provided by
# extension library
proc __getFilesInDirectory {dir fetch_dotversion} {
   set dir_list [list]

   # try then catch any issue rather than test before trying
   # workaround 'glob -nocomplain' which does not return permission
   # error on Tcl 8.4, so we need to avoid registering issue if
   # raised error is about a no match
   if {[catch {set elt_list [glob $dir/*]} errMsg]} {
      if {$errMsg eq "no files matched glob pattern \"$dir/*\""} {
         set elt_list {}
      } else {
         # rethrow other error to catch it in caller proc
         error $errMsg $::errorInfo $::errorCode
      }
   }

   # Add each element in the current directory to the list
   foreach elt $elt_list {
      lappend dir_list $elt 0
   }

   # search for hidden files
   foreach elt [glob -nocomplain -types hidden -directory $dir -tails *] {
      switch -- $elt {
         . - .. { }
         .modulerc - .version {
            if {($fetch_dotversion || $elt ne {.version}) && [file readable\
               $dir/$elt]} {
               lappend dir_list $dir/$elt 0
            }
         }
         default {
            lappend dir_list $dir/$elt 1
         }
      }
   }

   return $dir_list
}

# Check a module name does match query at expected depth level when indepth
# search is disabled. Define procedure on the fly to adapt its
# code to indepth configuration option and querydepth and test mode params.
proc defineDoesModMatchAtDepthProc {indepth querydepth test} {
   set procprops $indepth:$querydepth:$test

   # define proc if not done yet or if it was defined for another context
   if {[info procs doesModMatchAtDepth] eq {} ||\
      $::g_doesModMatchAtDepth_procprops ne $procprops} {
      if {[info exists ::g_doesModMatchAtDepth_procprops]} {
         rename ::doesModMatchAtDepth {}
      }
      set ::g_doesModMatchAtDepth_procprops $procprops

      # define optimized procedure
      if {$indepth} {
         set atdepth {$mod}
      } else {
         set atdepth "\[join \[lrange \[split \$mod /\] 0 $querydepth\] /\]"
      }
      proc doesModMatchAtDepth {mod} "return \[modEqStatic $atdepth $test *\]"
   }
}

# Define procedure to check module version equals pattern. Adapt procedure
# code whether icase and extended_default are enabled or disabled
proc defineModVersCmpProc {icase extdfl} {
   set procname modVersCmpProc
   if {$icase} {
      append procname Icase
   }
   if {$extdfl} {
      append procname Extdfl
   }

   # define proc if not done yet or if it was defined for another context
   if {[info procs modVersCmp] eq {} || $::g_modVersCmp_proc ne $procname} {
      if {[info exists ::g_modVersCmp_proc]} {
         rename ::modVersCmp ::$::g_modVersCmp_proc
      }
      rename ::$procname ::modVersCmp
      set ::g_modVersCmp_proc $procname
   }
}

# alternative definitions of modVersCmp proc
proc modVersCmpProc {cmpspec versspec modvers test {psuf {}}} {
   set ret 0
   switch -- $cmpspec {
      in {
         foreach vers $versspec {
            append vers $psuf
            if {$test eq {eqstart}} {
               set ret [string equal -length [string length $vers/] $vers/\
                  $modvers/]
            } else {
               set ret [string $test $vers $modvers]
            }
            if {$ret} {
               break
            }
         }
      }
      ge {
         # as we work here on a version range: psuf suffix is ignored, checks
         # are always extended_default-enabled (as 1.2 includes 1.2.12 for
         # instance) and equal, eqstart and match tests are equivalent
         set ret [expr {[isVersion $modvers] && ([versioncmp $modvers\
            $versspec] != -1 || [string match $versspec.* $modvers])}]
      }
      le {
         # 'ge' comment also applies here
         set ret [expr {[isVersion $modvers] && ([versioncmp $versspec\
            $modvers] != -1 || [string match $versspec.* $modvers])}]
      }
      be {
         # 'ge' comment also applies here
         lassign $versspec lovers hivers
         set ret [expr {[isVersion $modvers] && ([versioncmp $modvers\
            $lovers] != -1 || [string match $lovers.* $modvers]) &&\
            ([versioncmp $hivers $modvers] != -1 || [string match\
            $hivers.* $modvers])}]
      }
   }
   return $ret
}
proc modVersCmpProcIcase {cmpspec versspec modvers test {psuf {}}} {
   set ret 0
   switch -- $cmpspec {
      in {
         foreach vers $versspec {
            append vers $psuf
            if {$test eq {eqstart}} {
               set ret [string equal -nocase -length [string length $vers/]\
                  $vers/ $modvers/]
            } else {
               set ret [string $test -nocase $vers $modvers]
            }
            if {$ret} {
               break
            }
         }
      }
      ge {
         set ret [expr {[isVersion $modvers] && ([versioncmp $modvers\
            $versspec] != -1 || [string match -nocase $versspec.* $modvers])}]
      }
      le {
         set ret [expr {[isVersion $modvers] && ([versioncmp $versspec\
            $modvers] != -1 || [string match -nocase $versspec.* $modvers])}]
      }
      be {
         lassign $versspec lovers hivers
         set ret [expr {[isVersion $modvers] && ([versioncmp $modvers\
            $lovers] != -1 || [string match $lovers.* $modvers]) &&\
            ([versioncmp $hivers $modvers] != -1 || [string match -nocase\
            $hivers.* $modvers])}]
      }
   }
   return $ret
}
proc modVersCmpProcExtdfl {cmpspec versspec modvers test {psuf {}}} {
   set ret 0
   switch -- $cmpspec {
      in {
         foreach vers $versspec {
            append vers $psuf
            if {$test eq {eqstart}} {
               set ret [string equal -length [string length $vers/] $vers/\
                  $modvers/]
            } else {
               set ret [string $test $vers $modvers]
            }
            if {$ret || [string match $vers.* $modvers]} {
               set ret 1
               break
            }
         }
      }
      ge {
         set ret [expr {[isVersion $modvers] && ([versioncmp $modvers\
            $versspec] != -1 || [string match $versspec.* $modvers])}]
      }
      le {
         set ret [expr {[isVersion $modvers] && ([versioncmp $versspec\
            $modvers] != -1 || [string match $versspec.* $modvers])}]
      }
      be {
         lassign $versspec lovers hivers
         set ret [expr {[isVersion $modvers] && ([versioncmp $modvers\
            $lovers] != -1 || [string match $lovers.* $modvers]) &&\
            ([versioncmp $hivers $modvers] != -1 || [string match\
            $hivers.* $modvers])}]
      }
   }
   return $ret
}
proc modVersCmpProcIcaseExtdfl {cmpspec versspec modvers test {psuf {}}} {
   set ret 0
   switch -- $cmpspec {
      in {
         # check if one version in list matches
         foreach vers $versspec {
            append vers $psuf
            if {$test eq {eqstart}} {
               set ret [string equal -nocase -length [string length $vers/]\
                  $vers/ $modvers/]
            } else {
               set ret [string $test -nocase $vers $modvers]
            }
            # try the extended default match
            if {$ret || [string match -nocase $vers.* $modvers]} {
               set ret 1
               break
            }
         }
      }
      ge {
         set ret [expr {[isVersion $modvers] && ([versioncmp $modvers\
            $versspec] != -1 || [string match -nocase $versspec.* $modvers])}]
      }
      le {
         set ret [expr {[isVersion $modvers] && ([versioncmp $versspec\
            $modvers] != -1 || [string match -nocase $versspec.* $modvers])}]
      }
      be {
         lassign $versspec lovers hivers
         set ret [expr {[isVersion $modvers] && ([versioncmp $modvers\
            $lovers] != -1 || [string match $lovers.* $modvers]) &&\
            ([versioncmp $hivers $modvers] != -1 || [string match -nocase\
            $hivers.* $modvers])}]
      }
   }
   return $ret
}

# Setup a hardwire version of modEq procedure called modEqStatic. This
# optimized procedure already knows the module pattern to compare to, whose
# specification has already been resolved at procedure definition time, which
# saves lot of processing time.
proc defineModEqStaticProc {icase extdfl modspec} {
   set procprops $icase:$extdfl:$modspec

   # define proc if not done yet or if it was defined for another context
   if {[info procs modEqStatic] eq {} || $::g_modEqStatic_procprops ne\
      $procprops} {
      if {[info exists ::g_modEqStatic_procprops]} {
         rename ::modEqStatic {}
      } else {
         # also define modVersCmp which is called by modEqStatic
         defineModVersCmpProc $icase $extdfl
      }
      set ::g_modEqStatic_procprops $procprops

      # define optimized procedure
      lassign [getModuleVersSpec $modspec] pmod pmodname cmpspec versspec\
         pmodnamere pmodescglob
      # trim dup trailing / char and adapt pmod suffix if it starts with /
      if {[string index $pmod end] eq {/}} {
         set pmod [string trimright $pmod /]/
         set endwslash 1
      } else {
         set endwslash 0
      }
      set nocasearg [expr {$icase ? {-nocase } : {}}]
      set pmodnameslen [string length $pmodname/]
      if {$pmod ne {} || $modspec eq {}} {
         set procbody "
            set pmod {$pmod}
            if {\$psuf ne {}} {
               if {$endwslash && \[string index \$psuf 0\] eq {/}} {
                  append pmod \[string range \$psuf 1 end\]
               } else {
                  append pmod \$psuf
               }
            }
            if {\$test eq {eqstart}} {
               set ret \[string equal $nocasearg-length \[string length\
                  \$pmod/\] \$pmod/ \$mod/\]
            } else {
               if {\$test eq {matchin}} {
                  set test match
                  set pmod *\$pmod
               }
               set ret \[string \$test $nocasearg\$pmod \$mod\]
            }"
         if {$extdfl} {
            append procbody "
               if {!\$ret && \[string first / \$pmod\] != -1} {
                  if {\$test eq {match}} {
                     set pmodextdfl \$pmod.*
                  } else {
                     set pmodextdfl {$pmodescglob.*}
                  }
                  set ret \[string match $nocasearg\$pmodextdfl \$mod\]
               }"
         }
      } else {
         set procbody "
            set pmodname {$pmodname}
            set pmodnamere {$pmodnamere}
            if {\$test eq {matchin}} {
               set test match
               if {\$pmodnamere ne {}} {
                  set pmodnamere .*\$pmodnamere
               } else {
                  set pmodnamere {.*$pmodname}
               }
            }
            if {(\$pmodnamere ne {} && \$test eq {match} && \[regexp\
               $nocasearg (^\$pmodnamere)/ \$mod/ rematch pmodname\]) ||\
               \[string equal $nocasearg -length $pmodnameslen {$pmodname/}\
               \$mod/\]} {
               set modvers \[string range \$mod \[string length \$pmodname/\]\
                  end\]
               set ret \[modVersCmp {$cmpspec} {$versspec} \$modvers \$test\
                  \$psuf\]
            } else {
               set ret 0
            }"
      }
      append procbody "
         return \$ret"
      proc modEqStatic {mod {test equal} {psuf {}}} $procbody
   }
}

# Define procedure to check module name equals pattern. Adapt procedure
# code whether icase and extended_default are enabled or disabled
proc defineModEqProc {icase extdfl} {
   set procname modEqProc
   if {$icase} {
      append procname Icase
   }
   if {$extdfl} {
      append procname Extdfl
   }

   # define proc if not done yet or if it was defined for another context
   if {[info procs modEq] eq {} || $::g_modEq_proc ne $procname} {
      if {[info exists ::g_modEq_proc]} {
         rename ::modEq ::$::g_modEq_proc
      }
      rename ::$procname ::modEq
      set ::g_modEq_proc $procname
   }

   # also define modVersCmp which is called by modEq
   defineModVersCmpProc $icase $extdfl
}

# alternative definitions of modEq proc
proc modEqProc {pattern mod {test equal} {trspec 1}} {
   # extract specified module name from name and version spec
   if {$trspec} {
      lassign [getModuleVersSpec $pattern] pmod pmodname cmpspec versspec\
         pmodnamere pmodescglob
   } else {
      set pmod $pattern
   }
   # trim dup trailing / char
   if {[string index $pmod end] eq {/}} {
      set pmod [string trimright $pmod /]/
   }
   # specified module can be translated in a simple mod name/vers or is empty
   if {$pmod ne {} || $pattern eq {}} {
      if {$test eq {eqstart}} {
         set ret [string equal -length [string length $pmod/] $pmod/ $mod/]
      } else {
         # contains test
         if {$test eq {matchin}} {
            set test match
            set pmod *$pmod
         } elseif {$test eq {eqspec}} {
            set test equal
         }
         set ret [string $test $pmod $mod]
      }
   } elseif {$test eq {eqspec}} {
      # test equality against all version described in spec (list or range
      # boundaries), trspec is considered enabled
      foreach pmod [getAllModulesFromVersSpec $pattern] {
         if {[set ret [string equal $pmod $mod]]} {
            break
         }
      }
   } else {
      # contains test
      if {$test eq {matchin}} {
         set test match
         if {$pmodnamere ne {}} {
            set pmodnamere .*$pmodnamere
         } else {
            set pmodnamere .*$pmodname
         }
      }
      # for more complex specification, first check if module name matches
      # use a regexp test if module name contains wildcard characters
      if {($pmodnamere ne {} && $test eq {match} && [regexp (^$pmodnamere)/\
         $mod/ rematch pmodname]) || [string equal -length [string length\
         $pmodname/] $pmodname/ $mod/]} {
         # then compare versions
         set modvers [string range $mod [string length $pmodname/] end]
         set ret [modVersCmp $cmpspec $versspec $modvers $test]
      } else {
         set ret 0
      }
   }
   return $ret
}
proc modEqProcIcase {pattern mod {test equal} {trspec 1}} {
   if {$trspec} {
      lassign [getModuleVersSpec $pattern] pmod pmodname cmpspec versspec\
         pmodnamere pmodescglob
   } else {
      set pmod $pattern
   }
   if {[string index $pmod end] eq {/}} {
      set pmod [string trimright $pmod /]/
   }
   if {$pmod ne {} || $pattern eq {}} {
      if {$test eq {eqstart}} {
         set ret [string equal -nocase -length [string length $pmod/] $pmod/\
            $mod/]
      } else {
         # contains test
         if {$test eq {matchin}} {
            set test match
            set pmod *$pmod
         } elseif {$test eq {eqspec}} {
            set test equal
         }
         set ret [string $test -nocase $pmod $mod]
      }
   } elseif {$test eq {eqspec}} {
      # test equality against all version described in spec (list or range
      # boundaries), trspec is considered enabled
      foreach pmod [getAllModulesFromVersSpec $pattern] {
         if {[set ret [string equal -nocase $pmod $mod]]} {
            break
         }
      }
   } else {
      # contains test
      if {$test eq {matchin}} {
         set test match
         if {$pmodnamere ne {}} {
            set pmodnamere .*$pmodnamere
         } else {
            set pmodnamere .*$pmodname
         }
      }
      # for more complex specification, first check if module name matches
      # use a regexp test if module name contains wildcard characters
      if {($pmodnamere ne {} && $test eq {match} && [regexp -nocase\
         (^$pmodnamere)/ $mod/ rematch pmodname]) || [string equal -nocase\
         -length [string length $pmodname/] $pmodname/ $mod/]} {
         # then compare versions
         set modvers [string range $mod [string length $pmodname/] end]
         set ret [modVersCmp $cmpspec $versspec $modvers $test]
      } else {
         set ret 0
      }
   }
   return $ret
}
proc modEqProcExtdfl {pattern mod {test equal} {trspec 1}} {
   if {$trspec} {
      lassign [getModuleVersSpec $pattern] pmod pmodname cmpspec versspec\
         pmodnamere pmodescglob
   } else {
      set pmod $pattern
   }
   if {[string index $pmod end] eq {/}} {
      set pmod [string trimright $pmod /]/
   }
   if {$pmod ne {} || $pattern eq {}} {
      if {$test eq {eqstart}} {
         set ret [string equal -length [string length $pmod/] $pmod/ $mod/]
      } else {
         # contains test
         if {$test eq {matchin}} {
            set test match
            set pmod *$pmod
         } elseif {$test eq {eqspec}} {
            set test equal
            set eqspec 1
         }
         set ret [string $test $pmod $mod]
      }
      # try the extended default match if not root module and not eqspec test
      if {![info exists eqspec] && !$ret && [string first / $pmod] != -1} {
         if {$test eq {match}} {
            set pmodextdfl $pmod.*
         } else {
            set pmodextdfl $pmodescglob.*
         }
         set ret [string match $pmodextdfl $mod]
      }
   } elseif {$test eq {eqspec}} {
      # test equality against all version described in spec (list or range
      # boundaries), trspec is considered enabled
      foreach pmod [getAllModulesFromVersSpec $pattern] {
         if {[set ret [string equal $pmod $mod]]} {
            break
         }
      }
   } else {
      # contains test
      if {$test eq {matchin}} {
         set test match
         if {$pmodnamere ne {}} {
            set pmodnamere .*$pmodnamere
         } else {
            set pmodnamere .*$pmodname
         }
      }
      # for more complex specification, first check if module name matches
      # use a regexp test if module name contains wildcard characters
      if {($pmodnamere ne {} && $test eq {match} && [regexp (^$pmodnamere)/\
         $mod/ rematch pmodname]) || [string equal -length [string length\
         $pmodname/] $pmodname/ $mod/]} {
         # then compare versions
         set modvers [string range $mod [string length $pmodname/] end]
         set ret [modVersCmp $cmpspec $versspec $modvers $test]
      } else {
         set ret 0
      }
   }
   return $ret
}
proc modEqProcIcaseExtdfl {pattern mod {test equal} {trspec 1}} {
   if {$trspec} {
      lassign [getModuleVersSpec $pattern] pmod pmodname cmpspec versspec\
         pmodnamere pmodescglob
   } else {
      set pmod $pattern
   }
   if {[string index $pmod end] eq {/}} {
      set pmod [string trimright $pmod /]/
   }
   if {$pmod ne {} || $pattern eq {}} {
      if {$test eq {eqstart}} {
         set ret [string equal -nocase -length [string length $pmod/] $pmod/\
            $mod/]
      } else {
         # contains test
         if {$test eq {matchin}} {
            set test match
            set pmod *$pmod
         } elseif {$test eq {eqspec}} {
            set test equal
            set eqspec 1
         }
         set ret [string $test -nocase $pmod $mod]
      }
      # try the extended default match if not root module and not eqspec test
      if {![info exists eqspec] && !$ret && [string first / $pmod] != -1} {
         if {$test eq {match}} {
            set pmodextdfl $pmod.*
         } else {
            set pmodextdfl $pmodescglob.*
         }
         set ret [string match -nocase $pmodextdfl $mod]
      }
   } elseif {$test eq {eqspec}} {
      # test equality against all version described in spec (list or range
      # boundaries), trspec is considered enabled
      foreach pmod [getAllModulesFromVersSpec $pattern] {
         if {[set ret [string equal -nocase $pmod $mod]]} {
            break
         }
      }
   } else {
      # contains test
      if {$test eq {matchin}} {
         set test match
         if {$pmodnamere ne {}} {
            set pmodnamere .*$pmodnamere
         } else {
            set pmodnamere .*$pmodname
         }
      }
      # for more complex specification, first check if module name matches
      # use a regexp test if module name contains wildcard characters
      if {($pmodnamere ne {} && $test eq {match} && [regexp -nocase\
         (^$pmodnamere)/ $mod/ rematch pmodname]) || [string equal -nocase\
         -length [string length $pmodname/] $pmodname/ $mod/]} {
         # then compare versions
         set modvers [string range $mod [string length $pmodname/] end]
         set ret [modVersCmp $cmpspec $versspec $modvers $test]
      } else {
         set ret 0
      }
   }
   return $ret
}

# check if an existing findModules cache entry matches current search by
# evaluating search ids. if an exact match cannot be found, look at saved
# searches that contains current search (superset of looked elements), extra
# elements will be filtered-out by GetModules
proc findModulesInMemCache {searchid} {
   # exact same search is cached
   if {[info exists ::g_foundModulesMemCache($searchid)]} {
      set match_searchid $searchid
      set mod_list $::g_foundModulesMemCache($searchid)
   # look for a superset search
   } else {
      set match_searchid {}
      set mod_list {}
      foreach cacheid [array names ::g_foundModulesMemCache] {
         # cache id acts as pattern to check if it contains current search
         if {[string match $cacheid $searchid]} {
            set match_searchid $cacheid
            set mod_list $::g_foundModulesMemCache($cacheid)
            break
         }
      }
   }

   return [list $match_searchid $mod_list]
}

# finds all module-related files matching mod in the module path dir
proc findModules {dir mod depthlvl fetch_mtime} {
   reportDebug "finding '$mod' in $dir (depthlvl=$depthlvl,\
      fetch_mtime=$fetch_mtime)"

   # generated search id (for cache search/save) by compacting given args
   set searchid $dir:$mod:$depthlvl:$fetch_mtime

   # look at memory cache for a compatible result
   lassign [findModulesInMemCache $searchid] cache_searchid cache_list
   if {$cache_searchid ne {}} {
      reportDebug "use cache entry '$cache_searchid'"
      return $cache_list
   }

   defineModEqStaticProc [isIcase] [getConf extended_default] $mod

   # every entries are requested
   set findall [expr {$mod eq {} || $mod eq {*}}]

   # use catch protection to handle non-readable and non-existent dir
   if {[catch {
      set full_list {}
      foreach {fpelt hid} [getFilesInDirectory $dir 0] {
         set elt [file tail $fpelt]
         # include any .modulerc file found at the modulepath root
         if {$elt eq {.modulerc} || $findall || [modEqStatic $elt match]} {
            lappend full_list $fpelt
         }
      }
   }]} {
      return {}
   }

   foreach igndir [getConf ignored_dirs] {
      set ignored_dirs($igndir) 1
   }
   array set mod_list {}
   for {set i 0} {$i < [llength $full_list]} {incr i 1} {
      set element [lindex $full_list $i]
      set tail [file tail $element]
      set modulename [getModuleNameFromModulepath $element $dir]
      set parentname [file dirname $modulename]
      set moddepthlvl [llength [file split $modulename]]
      if {[file isdirectory $element]} {
         if {![info exists ignored_dirs($tail)]} {
            if {[catch {
               set elt_list [getFilesInDirectory $element 1]
            } errMsg]} {
               set mod_list($modulename) [list accesserr [parseAccessIssue\
                  $element] $element]
            } else {
               # Add each element in the current directory to the list
               foreach {fpelt hid} $elt_list {
                  lappend full_list $fpelt
                  # Flag hidden files
                  if {$hid} {
                     set hidden_list($fpelt) 1
                  }
               }
            }
         }
      } else {
         switch -glob -- $tail {
            .modulerc {
               set mod_list($modulename) [list modulerc]
            }
            .version {
               # skip .version file from different depth level than search
               # targets if no in depth mode is enabled
               if {$depthlvl == 0 || $moddepthlvl == $depthlvl} {
                  set mod_list($modulename) [list modulerc]
               }
            }
            *~ - *,v - \#*\# { }
            default {
               # skip modfile in no in depth mode search if it does not relate
               # to targeted depth level and one valid modfile has already be
               # found for the dirs lying at other depth level
               if {$depthlvl == 0 || $moddepthlvl == $depthlvl || ![info\
                  exists modfile_indir($parentname)]} {
                  lassign [checkValidModule $element] check_valid check_msg
                  switch -- $check_valid {
                     true {
                        set mtime [expr {$fetch_mtime ? [getFileMtime\
                           $element] : {}}]
                        set mod_list($modulename) [list modulefile $mtime\
                           $element]

                        # a valid modfile has been found in directory
                        if {![info exists hidden_list($element)]} {
                           set modfile_indir($parentname) 1
                        }
                     }
                     default {
                        # register check error and relative message to get it
                        # in case of direct access of this module element, but
                        # no registering in parent directory structure as
                        # element is not valid
                        set mod_list($modulename) [list $check_valid\
                           $check_msg $element]
                     }
                  }
               }
            }
         }
      }
   }

   reportDebug "found [array names mod_list]"

   # cache search results
   reportDebug "create cache entry '$searchid'"
   set found_list [array get mod_list]
   set ::g_foundModulesMemCache($searchid) $found_list

   return $found_list
}

proc getModules {dir {mod {}} {fetch_mtime 0} {search {}} {filter {}}} {
   global g_sourceAlias g_sourceVersion g_sourceVirtual g_rcAlias\
      g_moduleAlias g_rcVersion g_moduleVersion g_rcVirtual g_moduleVirtual\
      g_rcfilesSourced

   reportDebug "get '$mod' in $dir (fetch_mtime=$fetch_mtime, search=$search,\
      filter=$filter)"

   # generated search id (for cache search/save) by compacting given args
   set searchid $dir:$mod:$fetch_mtime:$search:$filter

   # look at memory cache for a compatible result
   if {[info exists ::g_gotModulesMemCache($searchid)]} {
      reportDebug "use cache entry '$searchid'"
      return $::g_gotModulesMemCache($searchid)
   }

   # extract one module name from query
   set modqe [getOneModuleFromVersSpec $mod]

   # perform an in depth search or not
   set indepth [expr {![isInList $search noindepth]}]

   # set a default if none defined on directory entries
   set implicitdfl [getConf implicit_default]

   # automatically define latest and default sym for all modules
   set autosymbol [expr {$implicitdfl && [getConf advanced_version_spec]}]

   # match passed name against any part of avail module names
   set contains [isInList $search contains]
   set mtest [expr {$contains ? {matchin} : {match}}]

   set icase [isIcase]

   set wild [isInList $search wild]

   # will only keep default or latest elts in the end or remove plain dirs
   set filtering [expr {$filter eq {noplaindir}}]
   set keeping [expr {!$filtering && $filter ne {}}]

   # check search query string corresponds to directory
   set querydir [string trimright $modqe *]
   set isquerydir [expr {[string index $querydir end] eq {/}}]
   set querydir [string trimright $querydir /]
   set querydepth [countChar $modqe /]

   # get directory relative to module name
   set moddir [getModuleNameFromVersSpec $mod]
   set hasmoddir [expr {$moddir ne {.}}]
   set modroot [getModuleRootFromVersSpec $mod]

   set earlyfilter [expr {!$contains && !$wild && $modroot eq [string\
      map {* {} ? {}} $modroot]}]

   # are result entries gathered in a resolution context ?
   set resctx [isInList $search resolve]

   # if search for global or user rc alias only, no dir lookup is performed
   # and aliases from g_rcAlias are returned
   if {[isInList $search rc_alias_only]} {
      set add_rc_defs 1
      array set found_list {}
   } else {
      # find modules by searching mod root name in order to catch all module
      # related entries to correctly computed auto symbols afterward

      if {$contains} {
         set findmod *
      } else {
         set findmod $modroot
         # if searched mod is an empty or flat element append wildcard
         # character to match anything starting with mod
         if {$wild && !$hasmoddir && [string index $findmod end] ne {*}} {
            append findmod *
         }
      }

      # add alias/version definitions from global or user rc to result
      set add_rc_defs [isInList $search rc_defs_included]

      # if no indepth mode search, pass the depth level of the search query
      set depthlvl [expr {$indepth ? 0 : [expr {$querydepth + 1}]}]

      array set found_list [findModules $dir $findmod $depthlvl $fetch_mtime]
   }

   # Phase #1: consolidate every kind of entries (directory, modulefile,
   # symbolic version, alias and virtual module) in found_list

   array set err_list {}
   array set versmod_list {}

   foreach elt [lsort [array names found_list]] {
      switch -- [lindex $found_list($elt) 0] {
         modulerc {
            # process rc files them remove them from found_list
            if {![info exists g_rcfilesSourced($dir/$elt)]} {
               execute-modulerc $dir/$elt $elt $elt
               # Keep track of already sourced rc files not to run them again
               set g_rcfilesSourced($dir/$elt) 1
            }
            unset found_list($elt)
         }
         modulefile {
         }
         default {
            # flag entries with error
            set err_list($elt) 1
         }
      }
   }

   # add all versions found when parsing .version or .modulerc files in this
   # directory or in global or user rc definitions
   foreach vers [array names g_moduleVersion] {
      set versmod $g_moduleVersion($vers)
      if {($dir ne {} && [string first $dir/ $g_sourceVersion($vers)] == 0)\
         || [info exists g_rcVersion($vers)]} {
         set found_list($vers) [list version $versmod]

         # build module symbol list
         lappend versmod_list($versmod) $vers
      # add global/user rc def to module symbol list in any cases
      } elseif {!$add_rc_defs && [info exists g_rcVersion($vers)]} {
         lappend versmod_list($versmod) $vers
      }
   }

   # add aliases found when parsing .version or .modulerc files in this
   # directory (skip aliases not registered from this directory except if
   # global or user rc definitions should be included)
   foreach alias [array names g_moduleAlias] {
      if {($dir ne {} && [string first $dir/ $g_sourceAlias($alias)] == 0)\
         || ($add_rc_defs && [info exists g_rcAlias($alias)])} {
         set found_list($alias) [list alias $g_moduleAlias($alias)]
      }
   }

   # add virtual mods found when parsing .version or .modulerc files in this
   # directory (skip virtual mods not registered from this directory except if
   # global or user rc definitions should be included)
   foreach virt [array names g_moduleVirtual] {
      if {($dir ne {} && [string first $dir/ $g_sourceVirtual($virt)] == 0)\
         || ($add_rc_defs && [info exists g_rcVirtual($virt)])} {
         lassign [checkValidModule $g_moduleVirtual($virt)] check_valid\
            check_msg
         switch -- $check_valid {
            true {
               set mtime [expr {$fetch_mtime ? [getFileMtime\
                  $g_moduleVirtual($virt)] : {}}]
               # set mtime at index 1 like a modulefile entry
               set found_list($virt) [list virtual $mtime\
                  $g_moduleVirtual($virt)]
            }
            default {
               # register check error and relative message to get it in
               # case of direct access of this module element
               set found_list($virt) [list $check_valid $check_msg\
                  $g_moduleVirtual($virt)]
               set err_list($virt) 1
            }
         }
      }
   }

   # Phase #2: filter-out dynamically hidden or expired elements

   # define module name and version comparison procs
   defineModStartNbProc $icase
   defineModEqProc $icase [getConf extended_default]
   defineModEqStaticProc $icase [getConf extended_default] $mod

   # remove hidden elements unless they are (or their symbols) targeted by
   # search query.
   foreach elt [array names found_list] {
      if {$earlyfilter && ![modEq $modroot $elt eqstart]} {
         unset found_list($elt)
         continue
      }
      if {[lassign [isModuleHidden $elt $mod 1] hidlvl hidmatch hidbydef]} {
         # is there a symbol that matches query (bare module name query
         # matches default symbol on resolve context or if onlydefaults filter
         # is applied)
         if {!$hidmatch && [info exists versmod_list($elt)]} {
            foreach eltsym $versmod_list($elt) {
               if {[modEqStatic $eltsym] || (($resctx || $filter eq\
                  {onlydefaults}) && "$mod/default" eq $eltsym)} {
                  set hidmatch 1
                  break
               }
            }
         }
         # consider 'default' symbols are explicitely specified if
         # onlydefaults filter applied or resolving bare module name
         if {!$hidmatch && [lindex $found_list($elt) 0] eq {version} && [file\
            tail $elt] eq {default} && ($filter eq {onlydefaults} || ($resctx\
            && "$mod/default" eq $elt))} {
            set hidmatch 1
         }

         # not hidden if matched unless if hard hiding apply
         if {!$hidmatch || $hidlvl > 1} {
            # record hidden symbol, not to display it in listModules
            if {[lindex $found_list($elt) 0] eq {version}} {
               lappend ::g_hiddenSymHash([lindex $found_list($elt) 1]) [file\
                  tail $elt]
            }
            # transform forbidden module in error entry if it specifically
            # matches search query
            if {$hidlvl == 2 && $hidmatch && [isModuleTagged $elt\
               forbidden]} {
               set found_list($elt) [list accesserr [getForbiddenMsg $elt]]
               set err_list($elt) 1
            } else {
               unset found_list($elt)
            }
         }
      }
      # apply hidden tag if an hidden definition apply to module
      if {$hidbydef} {
         setModuleTag $elt hidden
      }
   }

   # Phase #3: elaborate directory content with default element selection

   array set dir_list {}
   array set autosym_list {}

   # build list of elements contained in each directory
   foreach elt [array names found_list] {
      # add a ref to element in its parent directory unless element has error
      # or is a symbolic version then recursively add parent element until
      # reaching top directory
      if {![info exists err_list($elt)] && [lindex $found_list($elt) 0] ne\
         {version}} {
         set direlt $elt
         while {[set pardir [file dirname $direlt]] ne {.}} {
            appendNoDupToList dir_list($pardir) [file tail $direlt]
            set direlt $pardir
         }
      }
   }

   # determine default element for each directory and record sorted elt list
   # unless if an alias or a virtual module has overwritten directory entry
   foreach elt [array names dir_list] {
      if {![info exists found_list($elt)]} {
         set dir_list($elt) [lsort -dictionary $dir_list($elt)]
         # get default element: explicitely defined default (whether it exists
         # or is in error) or implicit default if enabled
         if {[info exists found_list($elt/default)] && [lindex\
            $found_list($elt/default) 0] eq {version}} {
            set dfl [file tail [lindex $found_list($elt/default) 1]]
         } elseif {$implicitdfl} {
            set dfl [lindex $dir_list($elt) end]
         } else {
            set dfl {}
         }
         # record directory properties
         set found_list($elt) [concat [list directory $dfl] $dir_list($elt)]

         # automatically define symbols for all modules matching query if
         # these names do not exist yet or if in error, in which case only
         # auto symbol resolution is set
         if {$autosymbol && (!$hasmoddir || [modEq $modroot $elt eqstart])} {
            if {![info exists found_list($elt/default)] || [info exists\
               err_list($elt/default)]} {
               if {![info exists found_list($elt/default)]} {
                  set found_list($elt/default) [list version $elt/$dfl]
                  set autosym_list($elt/default) 1
               }
               setModuleResolution $elt/default $elt/$dfl default 1 1
            }
            if {![info exists found_list($elt/latest)] || [info exists\
               err_list($elt/latest)]} {
               set lat [lindex $dir_list($elt) end]
               if {![info exists found_list($elt/latest)]} {
                  set found_list($elt/latest) [list version $elt/$lat]
                  set autosym_list($elt/latest) 1
               }
               setModuleResolution $elt/latest $elt/$lat latest 1 1
            }
         }
      }
   }

   # Phase #4: filter results to keep those matching search query

   # define module name and version comparison procs
   defineDoesModMatchAtDepthProc $indepth $querydepth $mtest

   # element to include in output
   set report_dirwsym [isEltInReport dirwsym]

   array set mod_list {}
   array set fdir_list {}
   array set keep_list {}

   # keep element matching query, add directory of element matching query,
   # also add directory to result if query name finishes with trailing slash;
   # only keep auto syms if fully matched or version not specified in query;
   # (hidden elements have been filtered on phase 2)
   foreach elt [array names found_list] {
      set elt_type [lindex $found_list($elt) 0]
      if {(($wild && [doesModMatchAtDepth $elt]) || (!$wild && ([modEqStatic\
         $elt match /*] || [modEqStatic $elt match] || ($hasmoddir &&\
         $elt_type eq {directory} && [modEq $moddir $elt]))) || ($isquerydir\
         && $elt_type eq {directory} && [modEq $querydir $elt match 0])) &&\
         (![info exists autosym_list($elt)] || ([countChar $elt /]\
         != $querydepth && !$contains) || [modEqStatic $elt]) && ![info\
         exists mod_list($elt)]} {
         if {$elt_type eq {directory}} {
            # add matching directory to the result list, its entries will be
            # computed in a second time and directory will be dropped if it
            # has no entry in the end
            set mod_list($elt) [list directory]
            # add dir to the filter dir list to enable its removal in next
            # step if dir is empty
            if {![info exists fdir_list($elt)]} {
               set fdir_list($elt) {}
            }
         } else {
            set mod_list($elt) $found_list($elt)
         }

         # version may matches query but not its target, so it should be in
         # this case manually added to result (if it exists)
         if {$elt_type eq {version}} {
            # resolve eventual icase target
            set versmod [getArrayKey found_list [lindex $mod_list($elt) 1]\
               $icase]
            # add target to dir struct (not version) if not already recorded
            set direlt .

            # recursively add targets to result (process directory content if
            # target is a directory
            set tgt_list [list $versmod]
            for {set i 0} {$i < [llength $tgt_list]} {incr i} {
               set tgt [lindex $tgt_list $i]

               if {![info exists mod_list($tgt)]} {
                  if {[info exists found_list($tgt)]} {
                     set mod_list($tgt) $found_list($tgt)
                     # version target is directory: recursively add content
                     if {[lindex $mod_list($tgt) 0] eq {directory}} {
                        foreach tgtelt $dir_list($tgt) {
                           lappend tgt_list $tgt/$tgtelt
                        }
                        # add dir to the filter dir list to enable its removal
                        # in next step if dir is empty
                        if {![info exists fdir_list($tgt)]} {
                           set fdir_list($tgt) {}
                        }
                     }
                  }

                  # record target in dir struct if part of found elts or if
                  # hidden but should not be in error
                  set pardir [file dirname $tgt]
                  if {([info exists found_list($tgt)] || ($pardir ne {.} &&\
                     ![info exists found_list($pardir)] && [isModuleHidden\
                     $tgt $mod])) && ![info exists err_list($tgt)]} {
                     # create parent directory if it does not exist
                     if {$pardir ne {.} && ![info exists\
                        found_list($pardir)]} {
                        set found_list($pardir) [list directory]
                        set mod_list($pardir) [list directory]
                     }

                     if {$i == 0} {
                        set direlt $tgt
                     } else {
                        lappend fdir_list([file dirname $tgt]) [file tail\
                           $tgt]
                     }
                  }
               }
            }
         # skip adding element to directory content if in error
         } elseif {[info exists err_list($elt)]} {
            set direlt .
         } else {
            set direlt $elt
         }

         # track directory content, as directory are also reported to their
         # parent directory the directory structure is also tracked
         if {[set pardir [file dirname $direlt]] ne {.}} {
            lappend fdir_list($pardir) [file tail $direlt]
         # track top level entries that will be kept if result is filtered
         } elseif {$keeping && $direlt ne {.} && $elt_type ne {directory}} {
            set keep_list($elt) 1
         }
      }
   }

   # determine default element for each directory and record sorted element
   # list unless directory entry has been overwritten by a different module
   # kind or unless only matching directory should be part of result
   foreach elt [lsort -decreasing [array names fdir_list]] {
      if {[lindex $found_list($elt) 0] eq {directory} && ([info exists\
         mod_list($elt)] || $keeping)} {
         set fdir_list($elt) [lsort -dictionary $fdir_list($elt)]
         # get default element: explicitely defined default if included in
         # result or not found or implicit default if enabled
         if {[info exists found_list($elt/default)] && [lindex\
            $found_list($elt/default) 0] eq {version} && ([info exists\
            mod_list([set versmod [lindex $found_list($elt/default) 1]])] ||\
            ![info exists found_list($versmod)])} {
            set dfl [file tail $versmod]
         } elseif {$implicitdfl} {
            set dfl [lindex $fdir_list($elt) end]
         } else {
            set dfl {}
         }
         # remove empty dirs
         if {[llength $fdir_list($elt)] == 0} {
            unset mod_list($elt)
            unset fdir_list($elt)
            # remove unset dir reference in parent directory. parent dir
            # will be treated after unset dir (due to decreasing sort) if it
            # needs to get in turn unset
            if {[set pardir [file dirname $elt]] ne {.}} {
               set fdir_list($pardir) [replaceFromList $fdir_list($pardir)\
                  [file tail $elt]]
            }
         } else {
            # record directory properties
            set mod_list($elt) [concat [list directory $dfl] $fdir_list($elt)]

            # list elements to keep for filtering step
            if {$keeping} {
               if {$filter eq {onlylatest}} {
                  set keepelt $elt/[lindex $fdir_list($elt) end]
               } elseif {$dfl ne {}} {
                  set keepelt $elt/$dfl
               } else {
                  set keepelt {}
               }

               # keep directory if its element depth is deeper than query
               if {!$indepth && [countChar $keepelt /] > $querydepth} {
                  set keep_list($elt) 1
               # otherwise only keep existing modules (not directories)
               } elseif {[info exists mod_list($keepelt)] && [lindex\
                  $mod_list($keepelt) 0] ne {directory}} {
                  set keep_list($keepelt) 1
               }
            # when noplaindir filtering, only keep dirs with syms when indepth
            # enabled or if correponds to query depth when indepth disabled
            } elseif {$filtering && $filter eq {noplaindir} &&\
               (($indepth && (!$report_dirwsym || ![doesModuleHaveSym $elt]))\
               || (!$indepth && [countChar $elt /] != $querydepth))} {
               unset mod_list($elt)
            }
         }
      }
   }

   # now all matching modulefiles are settled, only keep those found at search
   # query depth level if 'noindepth' mode asked
   if {!$indepth} {
      # remove entries with more filename path separator than query pattern
      foreach elt [array names mod_list] {
         if {[countChar $elt /] > $querydepth} {
            unset mod_list($elt)
         }
      }
   }

   # if result should be filtered, only keep marked elements
   if {$keeping} {
      foreach elt [array names mod_list] {
         if {![info exists keep_list($elt)]} {
            unset mod_list($elt)
         }
      }
   }

   # Phase #5: consolidate tags set for retained modules

   if {[currentState commandname] eq {avail}} {
      # load tags from loaded modules in the global structure prior collecting
      # tags found during this getModules evaluation
      cacheCurrentModules 0

      foreach elt [array names mod_list] {
         switch -- [lindex $mod_list($elt) 0] {
            alias - modulefile - virtual {
               # gather all tags applying to elt
               collectModuleTags $elt
            }
         }
      }
   }

   reportTrace "{[array names mod_list]} matching '$mod' in '$dir'" {Get\
      modules}

   # cache search results
   reportDebug "create cache entry '$searchid'"
   set got_list [array get mod_list]
   set ::g_gotModulesMemCache($searchid) $got_list

   return $got_list
}

# format an element with its syms for display in a list
proc formatListEltToDisplay {elt eltsgr eltsuffix sym_list symsgr show_syms\
   sgrdef tag_list show_tags {himatchmap {}}} {
   # fetch sgr codes from tags to apply directly on main element
   if {$show_tags && [llength $tag_list] > 0} {
      # if more codes than character in elt, additionnal codes apply to the
      # side tag list
      lassign [getTagSgrForModname $tag_list [string length $elt]] tagsgrlist\
         tag_list
   } else {
      set tagsgrlist {}
   }
   # display default sym graphically over element name
   if {$show_syms} {
      if {[set defidx [lsearch -exact $sym_list default]] != -1 && $sgrdef} {
         set sym_list [lreplace $sym_list $defidx $defidx]
         lappend eltsgrlist de
      }
   }

   set disp $elt$eltsuffix
   lappend eltsgrlist $eltsgr
   set dispsgr [sgr $eltsgrlist $elt $himatchmap $tagsgrlist]$eltsuffix

   # format remaining sym list
   if {$show_syms && [llength $sym_list] > 0} {
      # track if a symbol has been reported excluding sym for alias '@'
      if {![info exists ::g_used_sym_nocolor] && ([llength $sym_list] > 1 ||
         [lindex $sym_list 0] ne {@})} {
         set ::g_used_sym_nocolor 1
      }
      append disp "([join $sym_list :])"
      set symssgr [sgr se (]
      foreach sym $sym_list {
         if {![info exists colonsgr]} {
            set colonsgr [sgr se :]
         } else {
            append symssgr $colonsgr
         }
         append symssgr [sgr $symsgr $sym]
      }
      append symssgr [sgr se )]
      append dispsgr $symssgr
   }

   # format tag list if any remaining
   if {$show_tags && [llength $tag_list] > 0} {
      append disp " <[join $tag_list :]>"
      set tagssgr " [sgr se <]"
      foreach tag $tag_list {
         # track tag name or abbreviation that have been used
         if {![info exists ::g_used_tags($tag)]} {
            set ::g_used_tags($tag) 1
         }
         if {![info exists commasgr]} {
            set commasgr [sgr se :]
         } else {
            append tagssgr $commasgr
         }
         # try to sgr in case a code apply to the tag
         append tagssgr [sgr $tag $tag]
      }
      append tagssgr [sgr se >]
      append dispsgr $tagssgr
   }

   return [list $disp $dispsgr]
}

# format an element with its syms for a long/detailled display in a list
proc formatListEltToLongDisplay {elt eltsgr eltsuffix sym_list symsgr mtime\
   sgrdef {himatchmap {}}} {
   set disp $elt$eltsuffix
   set displen [string length $disp]
   # display default sym graphically over element name
   if {[set defidx [lsearch -exact $sym_list default]] != -1 && $sgrdef} {
      set sym_list [lreplace $sym_list $defidx $defidx]
      lappend eltsgrlist de
   }
   lappend eltsgrlist $eltsgr
   set dispsgr [sgr $eltsgrlist $elt $himatchmap]$eltsuffix
   # format remaining sym list
   if {[llength $sym_list] > 0} {
      set symslen [string length [join $sym_list :]]
      foreach sym $sym_list {
         if {![info exists colonsgr]} {
            set colonsgr [sgr se :]
         } else {
            append symssgr $colonsgr
         }
         append symssgr [sgr $symsgr $sym]
      }
   } else {
      set symssgr {}
      set symslen 0
   }
   set nbws1 [expr {40 - $displen}]
   set nbws2 [expr {20 - $symslen + [expr {$nbws1 < 0 ? $nbws1 : 0}]}]
   return [list $disp $dispsgr[string repeat { } $nbws1]$symssgr[string\
      repeat { } $nbws2]$mtime]
}

proc formatArrayValToJson {vallist} {
   return [expr {[llength $vallist] > 0 ? "\[ \"[join $vallist {", "}]\" \]"\
      : {[]}}]
}

# format an element with its syms for a json display in a list
proc formatListEltToJsonDisplay {elt args} {
   set disp "\"$elt\": \{ \"name\": \"$elt\""
   foreach {key vtype val} $args {
      append disp ", \"$key\": "
      append disp [expr {$vtype eq {a} ? [formatArrayValToJson $val] :\
         "\"$val\""}]
   }
   append disp "\}"

   return $disp
}

# Prepare a map list to translate later on a substring in its highlighted
# counterpart. Translate substring into all module it specifies in case of an
# advanced version specification. Each string obtained is right trimmed from
# wildcard. No highlight is set for strings still containing wildcard chars
# after right trim operation. No highlist map is returned at all if hightlight
# rendering is disabled.
proc prepareMapToHightlightSubstr {substr} {
   set maplist {}
   if {[sgr hi {}] ne {}} {
      foreach m [getAllModulesFromVersSpec $substr] {
         set m [string trimright $m {*?}]
         if {$m ne {} && [string first * $m] == -1 && [string first ? $m] ==\
            -1} {
            lappend maplist $m [sgr hi $m]
         }
      }
   }
   return $maplist
}

# Format list of modules obtained from a getModules call in upper context
proc reportModules {mod header hsgrkey hstyle show_mtime show_idx\
   one_per_line theader_cols excluded_tag {mod_list_order {}}} {
   # link to the result module list obtained in caller context
   upvar mod_list mod_list

   # output is JSON format
   set json [isStateEqual report_format json]

   # elements to include in output
   set report_sym [isEltInReport sym]
   set report_tag [isEltInReport tag]
   set report_alias [isEltInReport alias]

   # prepare list of tag abbreviations that can be substituted and list of
   # tags whose name should be colored
   getConf tag_abbrev
   getConf tag_color_name

   # prepare results for display
   set alias_colored [isSgrkeyColored al]
   set default_colored [isSgrkeyColored de]
   set himatchmap [prepareMapToHightlightSubstr $mod]
   set clean_list {}

   # treat elements in specified order if any
   foreach elt [if {[llength $mod_list_order] == 0} {array names mod_list}\
      {set mod_list_order}] {
      set sym_list [getVersAliasList $elt]
      # fetch tags but clear excluded tag
      set tag_list [replaceFromList [getTagList $elt] $excluded_tag]
      # abbrieviate tags unless for json output
      if {!$json} {
         set tag_list [abbrevTagList $tag_list]
      }
      set dispsgr {}
      # ignore "version" entries as symbolic version are treated
      # along to their relative modulefile not independently
      switch -- [lindex $mod_list($elt) 0] {
         directory {
            if {$json} {
               set dispsgr [formatListEltToJsonDisplay $elt type s directory\
                  symbols a $sym_list]
            } elseif {$show_mtime} {
               # append / char after name to clearly indicate this is a dir
               lassign [formatListEltToLongDisplay $elt di / $sym_list sy {}\
                  $default_colored $himatchmap] disp dispsgr
            } else {
               lassign [formatListEltToDisplay $elt di / $sym_list sy\
                  $report_sym $default_colored {} 0 $himatchmap] disp dispsgr
            }
         }
         modulefile - virtual {
            if {$json} {
               set dispsgr [formatListEltToJsonDisplay $elt type s modulefile\
                  symbols a $sym_list tags a $tag_list pathname s [lindex\
                  $mod_list($elt) 2]]
            } elseif {$show_mtime} {
               set clock_mtime [expr {[lindex $mod_list($elt) 1] ne {} ?\
                  [clock format [lindex $mod_list($elt) 1] -format {%Y/%m/%d\
                  %H:%M:%S}] : {}}]
               # add to display file modification time in addition
               # to potential syms
               lassign [formatListEltToLongDisplay $elt {} {} $sym_list sy\
                  $clock_mtime $default_colored $himatchmap] disp dispsgr
            } else {
               lassign [formatListEltToDisplay $elt {} {} $sym_list sy\
                  $report_sym $default_colored $tag_list $report_tag\
                  $himatchmap] disp dispsgr
            }
         }
         alias {
            if {$json} {
               set dispsgr [formatListEltToJsonDisplay $elt type s alias\
                  symbols a $sym_list tags a $tag_list target s [lindex\
                  $mod_list($elt) 1]]
            } elseif {$show_mtime} {
               lassign [formatListEltToLongDisplay $elt al " -> [lindex\
                  $mod_list($elt) 1]" $sym_list sy {} $default_colored\
                  $himatchmap] disp dispsgr
            } elseif {$report_alias} {
               # add a '@' sym to indicate elt is an alias if not colored
               if {!$alias_colored} {
                  lappend sym_list @
                  # track use of '@' sym to add it to the output key
                  if {![info exists ::g_used_alias_nocolor]} {
                     set ::g_used_alias_nocolor 1
                  }
               }
               lassign [formatListEltToDisplay $elt al {} $sym_list sy\
                  $report_sym $default_colored $tag_list $report_tag\
                  $himatchmap] disp dispsgr
            }
         }
      }
      if {$dispsgr ne {}} {
         if {$json} {
            lappend clean_list $dispsgr
         } else {
            lappend clean_list $disp
            set sgrmap($disp) $dispsgr
         }
      }
   }

   set len_list {}
   set max_len 0
   if {$json} {
      upvar 0 clean_list display_list
      if {![info exists display_list]} {
         set display_list {}
      }
   } else {
      set display_list {}
      # dictionary-sort results unless if output order is specified
      if {[llength $mod_list_order] == 0} {
         set clean_list [lsort -dictionary $clean_list]
      }
      foreach disp $clean_list {
         # compute display element length list on sorted result
         lappend display_list $sgrmap($disp)
         lappend len_list [set len [string length $disp]]
         if {$len > $max_len} {
            set max_len $len
         }
      }
   }

   # output table header if needed and not yet done
   if {[llength $display_list] > 0 && $show_mtime && ![isStateDefined\
      theader_shown]} {
      setState theader_shown 1
      eval displayTableHeader $theader_cols
   }

   # output formatted elements
   displayElementList $header $hsgrkey $hstyle $one_per_line $show_idx\
      $display_list $len_list $max_len
}

proc showModulePath {} {
   set modpathlist [getModulePathList]
   if {[llength $modpathlist] > 0} {
      report {Search path for module files (in search order):}
      foreach path $modpathlist {
         report "  [sgr mp $path]"
      }
   } else {
      reportWarning {No directories on module search path}
   }
}

proc displayTableHeader {sgrkey args} {
   foreach {title col_len} $args {
      set col "- [sgr $sgrkey $title] "
      append col [string repeat - [expr {$col_len - [string length $title] -\
         3}]]
      lappend col_list $col
   }

   report [join $col_list .]
}

proc displaySeparatorLine {{title {}} {sgrkey {}}} {
   set tty_cols [getState term_columns]
   if {$title eq {}} {
      # adapt length if screen width is very small
      set max_rep 67
      set rep [expr {$tty_cols > $max_rep ? $max_rep : $tty_cols}]
      report [string repeat - $rep]
   } else {
      set len [string length $title]
      # max expr function is not supported in Tcl8.4 and earlier
      if {[set lrep [expr {($tty_cols - $len - 2)/2}]] < 1} {
         set lrep 1
      }
      if {[set rrep [expr {$tty_cols - $len - 2 - $lrep}]] < 1} {
         set rrep 1
      }
      report "[string repeat - $lrep] [sgr $sgrkey $title] [string repeat -\
         $rrep]"
   }
}

# get a list of elements and print them in a column or in a
# one-per-line fashion
proc displayElementList {header sgrkey hstyle one_per_line display_idx\
   display_list {len_list {}} {max_len 0}} {
   set elt_cnt [llength $display_list]
   reportDebug "header=$header, sgrkey=$sgrkey, hstyle=$hstyle,\
      elt_cnt=$elt_cnt, max_len=$max_len, one_per_line=$one_per_line,\
      display_idx=$display_idx"

   # end proc if no element are to print
   if {$elt_cnt == 0} {
      return
   }
   # output is JSON format
   set json [isStateEqual report_format json]

   # display header if any provided
   if {$header ne {noheader}} {
      if {$json} {
         report "\"$header\": \{"
      } elseif {$hstyle eq {sepline}} {
         displaySeparatorLine $header $sgrkey
      } else {
         report [sgr $sgrkey $header]:
      }
   }

   if {$json} {
      set displist [join $display_list ,\n]
   # display one element per line
   } elseif {$one_per_line} {
      if {$display_idx} {
         set idx 1
         foreach elt $display_list {
            append displist [format {%2d) %s } $idx $elt] \n
            incr idx
         }
      } else {
         append displist [join $display_list \n] \n
      }
   # elsewhere display elements in columns
   } else {
      if {$display_idx} {
         # save room for numbers and spacing: 2 digits + ) + space
         set elt_prefix_len 4
      } else {
         set elt_prefix_len 0
      }
      # save room for two spaces after element
      set elt_suffix_len 2

      # compute rows*cols grid size with optimized column number
      # the size of each column is computed to display as much column
      # as possible on each line
      incr max_len $elt_suffix_len
      foreach len $len_list {
         lappend elt_len [incr len $elt_suffix_len]
      }

      set tty_cols [getState term_columns]
      # find valid grid by starting with non-optimized solution where each
      # column length is equal to the length of the biggest element to display
      set cur_cols [expr {int(($tty_cols - $elt_prefix_len) / $max_len)}]
      # max expr function is not supported in Tcl8.4 and earlier
      if {$cur_cols < 0} {set cur_cols 0}
      # when display is found too short to display even one column
      if {$cur_cols == 0} {
         set cols 1
         set rows $elt_cnt
         array set col_width [list 0 $max_len]
      } else {
         set cols 0
         set rows 0
      }
      set last_round 0
      set restart_loop 0
      while {$cur_cols > $cols} {
         if {!$restart_loop} {
            if {$last_round} {
               incr cur_rows
            } else {
               set cur_rows [expr {int(ceil(double($elt_cnt) / $cur_cols))}]
            }
            for {set i 0} {$i < $cur_cols} {incr i} {
               set cur_col_width($i) 0
            }
            for {set i 0} {$i < $cur_rows} {incr i} {
               set row_width($i) 0
            }
            set istart 0
         } else {
            set istart [expr {$col * $cur_rows}]
            # only remove width of elements from current col
            for {set row 0} {$row < ($i % $cur_rows)} {incr row} {
               incr row_width($row) -[expr {$pre_col_width + $elt_prefix_len}]
            }
         }
         set restart_loop 0
         for {set i $istart} {$i < $elt_cnt} {incr i} {
            set col [expr {int($i / $cur_rows)}]
            set row [expr {$i % $cur_rows}]
            # restart loop if a column width change
            if {[lindex $elt_len $i] > $cur_col_width($col)} {
               set pre_col_width $cur_col_width($col)
               set cur_col_width($col) [lindex $elt_len $i]
               set restart_loop 1
               break
            }
            # end search of maximum number of columns if computed row width
            # is larger than terminal width
            if {[incr row_width($row) +[expr {$cur_col_width($col) \
               + $elt_prefix_len}]] > $tty_cols} {
               # start last optimization pass by increasing row number until
               # reaching number used for previous column number, by doing so
               # this number of column may pass in terminal width, if not
               # fallback to previous number of column
               if {$last_round && $cur_rows == $rows} {
                  incr cur_cols -1
               } else {
                  set last_round 1
               }
               break
            }
         }
         # went through all elements without reaching terminal width limit so
         # this number of column solution is valid, try next with a greater
         # column number
         if {$i == $elt_cnt} {
            set cols $cur_cols
            set rows $cur_rows
            array set col_width [array get cur_col_width]
            # number of column is fixed if last optimization round has started
            # reach end also if there is only one row of results
            if {!$last_round && $rows > 1} {
               incr cur_cols
            }
         }

      }
      reportDebug list=$display_list
      reportDebug "rows/cols=$rows/$cols,\
         lastcol_item_cnt=[expr {int($elt_cnt % $rows)}]"

      for {set row 0} {$row < $rows} {incr row} {
         for {set col 0} {$col < $cols} {incr col} {
            set index [expr {$col * $rows + $row}]
            if {$index < $elt_cnt} {
               if {$display_idx} {
                  append displist [format "%2d) " [expr {$index +1}]]
               }
               # cannot use 'format' as strings may contain SGR codes
               append displist [lindex $display_list $index][string repeat\
                  { } [expr {$col_width($col) - [lindex $len_list $index]}]]
            }
         }
         append displist \n
      }
   }
   if {$json && $header ne {noheader}} {
      append displist "\n\}"
   }
   report $displist 1
   reportSeparateNextContent
}

# Report an output key to help understand what the SGR used on this output
# correspond to
proc displayKey {} {
   array set skipsgr [list hi 1 db 1 tr 1 se 1 er 1 wa 1 me 1 in 1 cm 1]
   array set typesgr [list mp modulepath di [list directory <SGR>/ 10] al\
      module-alias sy [list {symbolic-version} [sgr se (]<SGR>[sgr se )] 18]\
      de [list {default-version}]]

   set display_list {}
   set len_list {}
   foreach key [array names ::g_used_colors] {
      # sgr key matches a basic modulefile type
      if {[info exists typesgr($key)]} {
         # the way to describe key is already defined
         if {[llength $typesgr($key)] > 1} {
            lassign $typesgr($key) desc desctmp len
            set desc [string map [list <SGR> [sgr $key $desc]] $desctmp]
         } else {
            set desc [lindex $typesgr($key) 0]
         }
         if {$key eq {sy} && [info exists ::g_used_sym_nocolor]} {
            unset ::g_used_sym_nocolor
         }
      # key is a tag abbreviation
      } elseif {[info exists ::g_abbrevTag($key)]} {
         set desc $::g_abbrevTag($key)
      # if not part of the ignored list, this key corresponds to a tag name
      } elseif {![info exists skipsgr($key)]} {
         set desc $key
      }
      if {[info exists desc]} {
         # define key description
         if {![info exists len]} {
            set len [string length $desc]
            set desc [sgr $key $desc]
         }
         lappend display_list $desc
         lappend len_list $len
         unset desc
         unset len
      }
   }

   # add key for alias if '@' put in parentheses
   if {[info exists ::g_used_alias_nocolor]} {
      lappend display_list "[sgr se (]@[sgr se )]=module-alias"
      lappend len_list 9
   }
   # add key for symbolic version if any put in parentheses but no color
   if {[info exists ::g_used_sym_nocolor]} {
      lappend display_list "[sgr se (]symbolic-version[sgr se )]"
      lappend len_list 18
   }

   # add key for module tag if any put in angle brackets
   if {[array exists ::g_used_tags]} {
      lappend display_list "[sgr se <]module-tag[sgr se >]"
      lappend len_list 12
   }
   # report translation of each uncolored tag abbreviation that have been used
   foreach tag [array names ::g_used_tags] {
      if {![info exists ::g_used_colors($tag)] && [info exists\
         ::g_abbrevTag($tag)]} {
         lappend display_list [sgr se <]$tag[sgr se >]=$::g_abbrevTag($tag)
         lappend len_list [expr {[string length $tag] + [string length\
            $::g_abbrevTag($tag)] + 3}]
      }
   }

   # find largest element
   set max_len 0
   foreach len $len_list {
      if {$len > $max_len} {
         set max_len $len
      }
   }

   if {[llength $display_list] > 0} {
      # display header
      report Key:
      # display key content
      displayElementList noheader {} {} 0 0 $display_list $len_list $max_len
   }
}

# Return conf value and from where an eventual def value has been overridden
proc displayConfig {val env_var {asked 0} {trans {}} {locked 0}} {
   array set transarr $trans

   # get overridden value and know what has overridden it
   if {$asked} {
      set defby " (cmd-line)"
   } elseif {$env_var ne {} && !$locked && [info exists ::env($env_var)]} {
      set defby " (env-var)"
   } elseif {$locked} {
      set defby " (locked)"
   } else {
      set defby {}
   }

   # translate fetched value if translation table exists
   if {[info exists transarr($val)]} {
      set val $transarr($val)
   }

   return $val$defby
}

# build list of what to undo then do to move from an initial list to a target
# list, eventually checking element presence in extra from/to lists
proc getMovementBetweenList {from to {extfrom {}} {extto {}}} {
   reportDebug "from($from) to($to) with extfrom($extfrom) extto($extto)"

   set undo {}
   set do {}

   # determine what element to undo then do
   # to restore a target list from a current list
   # with preservation of the element order
   set imax [if {[llength $to] > [llength $from]} {llength $to} {llength\
      $from}]
   set list_equal 1
   for {set i 0} {$i < $imax} {incr i} {
      set to_obj [lindex $to $i]
      set from_obj [lindex $from $i]
      # check from/to element presence in extra from/to list
      set in_extfrom [isInList $extfrom $from_obj]
      set in_extto [isInList $extto $to_obj]
      # are elts the sames and are both part of or missing from extra lists
      if {$to_obj ne $from_obj || $in_extfrom != $in_extto} {
         set list_equal 0
      }
      if {$list_equal == 0} {
         if {$to_obj ne {}} {
            lappend do $to_obj
         }
         if {$from_obj ne {}} {
            lappend undo $from_obj
         }
      }
   }

   return [list $undo $do]
}

# build list of currently loaded modules where modulename is registered minus
# module version if loaded version is the default one. a helper list may be
# provided and looked at prior to module search
proc getSimplifiedLoadedModuleList {{helper_raw_list {}}\
   {helper_list {}}} {
   reportDebug called.

   set curr_mod_list {}
   set curr_nuasked_list {}
   set modpathlist [getModulePathList]
   foreach mod [getLoadedModuleList] {
      # if mod found in a previous LOADEDMODULES list use simplified
      # version of this module found in relative helper list (previously
      # computed simplified list)
      if {[set helper_idx [lsearch -exact $helper_raw_list $mod]] != -1} {
         set simplemod [lindex $helper_list $helper_idx]
      # look through modpaths for a simplified mod name if not full path
      } elseif {![isModuleFullPath $mod] && [llength $modpathlist] > 0} {
         set modfile [getModulefileFromLoadedModule $mod]
         set parentmod [file dirname $mod]
         set simplemod $mod
         # simplify to parent name as long as it resolves to current mod
         while {$parentmod ne {.}} {
            lassign [getPathToModule $parentmod $modpathlist 0] parentfile
            if {$parentfile eq $modfile} {
               set simplemod $parentmod
               set parentmod [file dirname $parentmod]
            } else {
               set parentmod .
            }
         }
      } else {
         set simplemod $mod
      }
      lappend curr_mod_list $simplemod
      # record not user asked module list in simplified version form
      if {![isModuleUserAsked $mod]} {
         lappend curr_nuasked_list $simplemod
      }
   }

   return [list $curr_mod_list $curr_nuasked_list]
}

# return saved collections found in user directory which corresponds to
# enabled collection target if any set.
proc findCollections {} {
   if {[info exists ::env(HOME)]} {
      set coll_search $::env(HOME)/.module/*
   } else {
      reportErrorAndExit {HOME not defined}
   }

   # find saved collections (matching target suffix)
   # a target is a domain on which a collection is only valid.
   # when a target is set, only the collections made for that target
   # will be available to list and restore, and saving will register
   # the target footprint
   set colltarget [getConf collection_target]
   if {$colltarget ne {}} {
      append coll_search .$colltarget
   }

   # workaround 'glob -nocomplain' which does not return permission
   # error on Tcl 8.4, so we need to avoid raising error if no match
   # glob excludes by default files starting with "."
   if {[catch {set coll_list [glob $coll_search]} errMsg ]} {
      if {$errMsg eq "no files matched glob pattern \"$coll_search\""} {
         set coll_list {}
      } else {
         reportErrorAndExit "Cannot access collection directory.\n$errMsg"
      }
   }

   return $coll_list
}

# get filename corresponding to collection name provided as argument.
# name provided may already be a file name. collection description name
# (with target info if any) is returned along with collection filename
proc getCollectionFilename {coll} {
   # initialize description with collection name
   set colldesc $coll

   if {$coll eq {}} {
      reportErrorAndExit {Invalid empty collection name}
   # is collection a filepath
   } elseif {[string first / $coll] > -1} {
      # collection target has no influence when
      # collection is specified as a filepath
      set collfile $coll
   # elsewhere collection is a name
   } elseif {[info exists ::env(HOME)]} {
      set collfile $::env(HOME)/.module/$coll
      # if a target is set, append the suffix corresponding
      # to this target to the collection file name
      set colltarget [getConf collection_target]
      if {$colltarget ne {}} {
         append collfile .$colltarget
         # add knowledge of collection target on description
         append colldesc " (for target \"$colltarget\")"
      }
   } else {
      reportErrorAndExit {HOME not defined}
   }

   return [list $collfile $colldesc]
}

# generate collection content based on provided path and module lists
proc formatCollectionContent {path_list mod_list nuasked_list {sgr 0}} {
   set content {}

   # graphically enhance module command if asked
   set modcmd [expr {$sgr ? [sgr cm module] : {module}}]

   # start collection content with modulepaths
   foreach path $path_list {
      # enclose path if space character found in it
      if {[string first { } $path] != -1} {
         set path "{$path}"
      }
      # 'module use' prepends paths by default so we clarify
      # path order here with --append flag
      append content "$modcmd use --append $path" \n
   }

   # then add modules
   foreach mod $mod_list {
      # mark modules not asked by user to restore the user asked state
      set opt [expr {[isInList $nuasked_list $mod] ? {--notuasked } : {}}]
      # enclose module if space character found in it
      if {[string first { } $mod] != -1} {
         set mod "{$mod}"
      }
      append content "$modcmd load $opt$mod" \n
   }

   return $content
}

# read given collection file and return the path and module lists it defines
proc readCollectionContent {collfile colldesc} {
   # init lists (maybe coll does not set mod to load)
   set path_list {}
   set mod_list {}
   set nuasked_list {}

   # read file
   if {[catch {
      set fdata [split [readFile $collfile] \n]
   } errMsg ]} {
      reportErrorAndExit "Collection $colldesc cannot be read.\n$errMsg"
   }

   # analyze collection content
   foreach fline $fdata {
      if {[regexp {module use (.*)$} $fline match patharg] == 1} {
         # paths are appended by default
         set stuff_path append
         # manage multiple paths and path options specified on single line,
         # for instance "module use --append path1 path2 path3", with list
         # representation of patharg (which handles quoted elements containing
         # space in their name)
         foreach path $patharg {
            # following path is asked to be appended
            if {($path eq {--append}) || ($path eq {-a})\
               || ($path eq {-append})} {
               set stuff_path append
            # following path is asked to be prepended
            # collection generated with 'save' does not prepend
            } elseif {($path eq {--prepend}) || ($path eq {-p})\
               || ($path eq {-prepend})} {
               set stuff_path prepend
            } else {
               # ensure given path is absolute to be able to correctly
               # compare with paths registered in MODULEPATH
               set path [getAbsolutePath $path]
               # add path to end of list
               if {$stuff_path eq {append}} {
                  lappend path_list $path
               # insert path to first position
               } else {
                  set path_list [linsert $path_list 0 $path]
               }
            }
         }
      } elseif {[regexp {module load (.*)$} $fline match modarg] == 1} {
         # manage multiple modules specified on a single line, for instance
         # "module load mod1 mod2 mod3", with list representation of modarg
         # make a list of modules that were not directly asked by user
         set cleanlist [lsearch -all -inline -not -exact $modarg\
            --notuasked]
         if {[llength $modarg] != [llength $cleanlist]} {
            set nuasked_list [concat $nuasked_list $cleanlist]
         }
         set mod_list [concat $mod_list $cleanlist]
      }
   }

   return [list $path_list $mod_list $nuasked_list]
}

# analyze/translate command name passed to module
proc parseModuleCommandName {command defaultcmd} {
   set cmdempty 0

   # resolve command if alias or shortcut name used
   switch -- $command {
      add               {set command load}
      rm - remove       {set command unload}
      show              {set command display}
      apropos - keyword {set command search}
      {} {
         # if empty string supplied translate to default command
         set command $defaultcmd
         set cmdempty 1
      }
      default {
         # specific match for shortcut names
         set cmdlen [string length $command]
         foreach {match minlen sccmd} {load 2 load unload 4 unload delete 3\
            unload refresh 3 reload reload 3 reload switch 2 switch swap 2\
            switch display 2 display available 2 avail aliases 2 aliases list\
            2 list whatis 2 whatis purge 2 purge initadd 5 initadd initload 6\
            initadd initprepend 5 initprepend initswitch 6 initswitch\
            initswap 6 initswitch initunload 8 initrm initlist 5 initlist} {
            if {$cmdlen >= $minlen && [string equal -length $cmdlen $command\
               $match]} {
               set command $sccmd
               break
            }
         }
      }
   }

   set cmdvalid [isInList [list load unload reload use unuse source switch\
      display avail aliases path paths list whatis search purge save restore\
      saverm saveshow savelist initadd initprepend initswitch initrm initlist\
      initclear autoinit clear config help test prepend-path append-path\
      remove-path is-loaded is-saved is-used is-avail info-loaded sh-to-mod]\
      $command]

   return [list $command $cmdvalid $cmdempty]
}

# analyze arg list passed to a module cmd to set options
proc parseModuleCommandArgs {topcall cmd ignerr args} {
   set show_oneperline 0
   set show_mtime 0
   set show_filter {}
   set search_filter [expr {[getConf avail_indepth] ? {} : {noindepth}}]
   set search_match [getConf search_match]
   set dump_state 0
   set addpath_pos prepend
   set not_req 0
   set otherargs {}

   # parse argument list
   foreach arg $args {
      if {[info exists nextargisval]} {
         set $nextargisval $arg
         unset nextargisval
      } elseif {[info exists ignore_next_arg]} {
         unset ignore_next_arg
      } else {
         switch -glob -- $arg {
            -j - --json {
               # enable json output only on supported command
               if {[isInList [list avail savelist list search whatis] $cmd]} {
                  setState report_format json
                  set show_oneperline 0
                  set show_mtime 0
               }
            }
            -t - --terse {
               set show_oneperline 1
               set show_mtime 0
               setState report_format terse
            }
            -l - --long {
               set show_mtime 1
               set show_oneperline 0
               setState report_format long
            }
            -o {
               # option is only valid for specific sub-commands
               if {[isInList [list avail list] $cmd]} {
                  set nextargisval asked_output
                  set output_arg -o
               } else {
                  if {!$ignerr} {
                     knerror "Unsupported option '$arg' on $cmd sub-command"
                  }
                  set ignore_next_arg 1
               }
            }
            --output=* {
               # option is only valid for specific sub-commands
               if {[isInList [list avail list] $cmd]} {
                  set asked_output [string range $arg 9 end]
                  set output_arg --output
               } elseif {!$ignerr} {
                  knerror "Unsupported option '--output' on $cmd sub-command"
               }
            }
            --append - -append {
               if {$cmd eq {use}} {
                  set addpath_pos append
               } else {
                  lappend otherargs $arg
               }
            }
            -p - --prepend - -prepend {
               if {$cmd eq {use}} {
                  set addpath_pos prepend
               } else {
                  lappend otherargs $arg
               }
            }
            --all {
               # include hidden modules only on a limited set of command
               if {[isInList [list avail aliases search whatis ml list]\
                  $cmd]} {
                  setState hiding_threshold 2
               } else {
                  lappend otherargs $arg
               }
            }
            -a {
               # -a option has a different meaning whether sub-command is use
               # or one of the search/listing sub-commands
               if {$cmd eq {use}} {
                  set addpath_pos append
               } elseif {[isInList [list avail aliases search whatis ml list]\
                  $cmd]} {
                  setState hiding_threshold 2
               } else {
                  lappend otherargs $arg
               }
            }
            -d - --default {
               # in case of *-path command, -d means --delim
               if {$arg eq {-d} && [string match *-path $cmd]} {
                  lappend otherargs $arg
               } else {
                  set show_filter onlydefaults
               }
            }
            -L - --latest {
               set show_filter onlylatest
            }
            -C - --contains {
               set search_match contains
            }
            -S - --starts-with {
               set search_match starts_with
            }
            --indepth {
               # empty value means 'in depth' as it is default behavior
               set search_filter {}
            }
            --no-indepth {
               set search_filter noindepth
            }
            --dump-state {
               set dump_state 1
            }
            --auto - --no-auto - -f - --force {
               reportWarning "Unsupported option '$arg'"
            }
            --not-req {
               if {!$topcall && [isInList [list load unload switch] $cmd]} {
                  set not_req 1
               } else {
                  knerror "Unsupported option '$arg' on $cmd sub-command"
               }
            }
            --output {
               knerror "Missing value for '$arg' option"
            }
            default {
               lappend otherargs $arg
            }
         }
         set prevarg $arg
      }
   }

   if {[info exists nextargisval]} {
      knerror "Missing value for '$prevarg' option"
   }

   if {[info exists asked_output]} {
      if {[isInList [list long json] [getState report_format]]} {
         knerror "Unsupported option '$output_arg' on [getState\
            report_format] output mode"
      } else {
         # get config name relative to current sub-command and output format
         set outputconf $cmd
         if {[getState report_format] ne {regular}} {
            append outputconf _[getState report_format]
         }
         append outputconf _output

         # check option value is coherent with current sub-command
         if {[isDiffBetweenList [split $asked_output :] [lindex\
            $::g_config_defs($outputconf) 3]]} {
            if {!$ignerr} {
               knerror "Invalid element in value list for '$output_arg'\
                  option on $cmd sub-command\nAllowed elements are: [lindex\
                  $::g_config_defs($outputconf) 3] (separated by ':')"
            }
         } else {
            set ::asked_$outputconf $asked_output
         }
      }
   }

   reportDebug "(show_oneperline=$show_oneperline, show_mtime=$show_mtime,\
      show_filter=$show_filter, search_filter=$search_filter,\
      search_match=$search_match, dump_state=$dump_state,\
      addpath_pos=$addpath_pos, not_req=$not_req, otherargs=$otherargs)"
   return [list $show_oneperline $show_mtime $show_filter $search_filter\
      $search_match $dump_state $addpath_pos $not_req $otherargs]
}

# when advanced_version_spec option is enabled, parse argument list to set in
# a global context version specification of modules passed as argument.
# specification may vary whether it comes from the ml or another command.
proc parseModuleVersionSpecifier {mlspec args} {
   # skip arg parse if proc was already call with same arg set by an upper
   # proc. check all args to ensure current arglist does not deviate from
   # what was previously parsed
   foreach arg $args {
      if {![info exists ::g_moduleVersSpec($arg)]} {
         set need_parse 1
         break
      }
   }

   if {[info exists need_parse]} {
      if {[getConf advanced_version_spec]} {
         set invalidversspec 0
         set invalidversrange 0
         set mlunload 0
         set arglist [list]
         set unarglist [list]
         foreach arg $args {
            # extract module name and version specifier from arg
            set curverslist [lassign [split $arg @] curmod]
            # preserve empty argument
            if {$curmod ne {} || $arg eq {}} {
               # save previous module version spec and transformed arg if any
               if {[info exists modarglist]} {
                  set modarg [join $modarglist]
                  if {[info exists modname] && ($modname ne {} || $modspec eq {})} {
                     setModuleVersSpec $modarg $modname $cmpspec $versspec
                     # rework args to have 1 str element for whole module spec
                     # append to unload list if ml spec and - prefix used
                     if {$mlunload} {
                        lappend unarglist $modarg
                     } else {
                        lappend arglist $modarg
                     }
                  } else {
                     knerror "No module name defined in argument '$modarg'"
                  }
                  unset modarglist
               }
               if {$mlspec} {
                  if {[string index $curmod 0] eq {-}} {
                     set arg [string range $arg 1 end]
                     set curmod [string range $curmod 1 end]
                     set mlunload 1
                  } else {
                     set mlunload 0
                  }
               }
               set modname $curmod
               set modspec {}
            }

            set modspec [lindex $curverslist end]
            set versspecislist [expr {[string first , $modspec] != -1}]
            set versspecisrange [expr {[string first : $modspec] != -1}]
            # no deep version specification allowed nor list/range mix
            if {[string first / $modspec] != -1 || ($versspecislist &&\
               $versspecisrange)} {
               set invalidversspec 1
            # ',' separates multiple versions
            } elseif {$versspecislist} {
               set cmpspec in
               set versspec [split $modspec ,]
               # empty element in list is erroneous
               set invalidversspec [expr {[lsearch -exact $versspec {}] != -1}]
            # ':' separates range elements
            } elseif {$versspecisrange} {
               set versspec [split $modspec :]
               set lovers [lindex $versspec 0]
               set hivers [lindex $versspec 1]
               if {[llength $versspec] != 2 || ($lovers eq {} && $hivers eq\
                  {})} {
                  set invalidversspec 1
               } elseif {($lovers ne {} && ![isVersion $lovers]) || ($hivers\
                  ne {} && ![isVersion $hivers])} {
                  set invalidversrange 1
               # greater or equal
               } elseif {$hivers eq {}} {
                  set cmpspec ge
                  set versspec $lovers
               # lower or equal
               } elseif {$lovers eq {}} {
                  set cmpspec le
                  set versspec $hivers
               # between or equal
               } elseif {[versioncmp $lovers $hivers] == 1} {
                  set invalidversrange 1
               } else {
                  set cmpspec be
               }
            } else {
               set cmpspec eq
               set versspec $modspec
            }
            if {$invalidversspec} {
               knerror "Invalid version specifier '$modspec'"
            }
            if {$invalidversrange} {
               knerror "Invalid version range '$modspec'"
            }
            # keep arg enclosed if composed of several words
            if {[string first { } $arg] != -1} {
               lappend modarglist "{$arg}"
            } else {
               lappend modarglist $arg
            }
         }
         # transform last args
         if {[info exists modarglist]} {
            set modarg [join $modarglist]
            if {[info exists modname] && ($modname ne {} || $modspec eq {})} {
               setModuleVersSpec $modarg $modname $cmpspec $versspec
               # rework args to have 1 string element for whole module spec
               # append to unload list if ml spec and - prefix used
               if {$mlunload} {
                  lappend unarglist $modarg
               } else {
                  lappend arglist $modarg
               }
            } else {
               knerror "No module name defined in argument '$modarg'"
            }
         }
      } else {
         set unarglist [list]
         set arglist [list]

         foreach arg $args {
            if {$mlspec && [string index $arg 0] eq {-}} {
               set modname [string range $arg 1 end]
               set mlunload 1
            } else {
               set modname $arg
               set mlunload 0
            }
            # keep arg enclosed if composed of several words
            if {[string first { } $modname] != -1} {
               set modarg "{$modname}"
            } else {
               set modarg $modname
            }
            # record spec, especially needed if arg is enclosed
            setModuleVersSpec $modarg $modname eq {}
            # append to unload list if ml spec and - prefix used
            if {$mlunload} {
               lappend unarglist $modarg
            } else {
               lappend arglist $modarg
            }
         }
      }
   } else {
      set arglist $args
   }

   if {$mlspec} {
      return [list $unarglist $arglist]
   } else {
      return $arglist
   }
}

proc setModuleVersSpec {modarg modname cmpspec versspec} {
   # translate @loaded version into currently loaded mod matching modname
   if {$cmpspec eq {eq} && $versspec eq {loaded}} {
      if {[set lmmod [getLoadedMatchingName $modname]] ne {}} {
         set modname [file dirname $lmmod]
         set versspec [file tail $lmmod]
      } else {
         knerror "No loaded version found for '$modname' module"
      }
   }
   # save module root name
   set modroot [lindex [file split $modname] 0]
   # save module single designation if any and module name
   if {$versspec eq {}} {
      set mod $modname
      set modname [file dirname $modname]
   } else {
      set modname [string trimright $modname /]
      if {$cmpspec ne {eq}} {
         set mod {}
      } else {
         set mod $modname/$versspec
      }
   }
   # save a regexp-ready version of modname (apply
   # non-greedy quantifier to '*', to avoid matching final
   # '/' in string comparison
   set modnamere [string map {. \\. + \\+ * .*? ? .} $modname]
   if {$modname eq $modnamere} {
      set modnamere {}
   }
   # save a glob-special-chars escaped version of mod
   set modescglob [string map {* \\* ? \\?} $mod]

   reportDebug "Set module '$mod' (escglob '$modescglob'),  module name\
      '$modname' (re '$modnamere'), module root '$modroot', version cmp\
      '$cmpspec' and version(s) '$versspec' for argument '$modarg'"
   set ::g_moduleVersSpec($modarg) [list $mod $modname $cmpspec $versspec\
      $modnamere $modescglob $modroot]
}

proc getModuleVersSpec {modarg} {
   if {[info exists ::g_moduleVersSpec($modarg)]} {
      return $::g_moduleVersSpec($modarg)
   } else {
      return [list $modarg [file dirname $modarg] {} {} {} [string map {* \\*\
         ? \\?} $modarg] [lindex [file split $modarg] 0]]
   }
}

# get module name from module name and version spec if parsed
proc getModuleNameFromVersSpec {modarg} {
   if {[info exists ::g_moduleVersSpec($modarg)]} {
      lassign $::g_moduleVersSpec($modarg) mod modname
   } else {
      set modname [file dirname $modarg]
   }
   return $modname
}

# get module root name from module name and version spec if parsed
proc getModuleRootFromVersSpec {modarg} {
   if {[info exists ::g_moduleVersSpec($modarg)]} {
      set modroot [lindex $::g_moduleVersSpec($modarg) 6]
   } else {
      set modroot [lindex [file split $modarg] 0]
   }
   return $modroot
}

# translate module name version spec to return all modules mentionned
proc getAllModulesFromVersSpec {modarg} {
   if {[info exists ::g_moduleVersSpec($modarg)]} {
      lassign $::g_moduleVersSpec($modarg) mod modname cmpspec versspec
      if {$mod eq {} && $cmpspec ne {eq}} {
         foreach vers $versspec {
            lappend modlist $modname/$vers
         }
      } else {
         # add empty mod specification if cmpspec is 'eq'
         lappend modlist $mod
      }
   } else {
      lappend modlist $modarg
   }

   return $modlist
}

# translate module name version spec to return one module mentionned
proc getOneModuleFromVersSpec {modarg} {
   if {[info exists ::g_moduleVersSpec($modarg)]} {
      lassign $::g_moduleVersSpec($modarg) mod modname cmpspec versspec
      if {$mod eq {} && $cmpspec ne {eq}} {
         set mod $modname/[lindex $versspec 0]
      }
   } else {
      set mod $modarg
   }

   return $mod
}

# unload phase of a list of modules reload process
proc reloadModuleListUnloadPhase {lmname {force 0} {errmsgtpl {}} {context\
   unload}} {
   upvar $lmname lmlist
   # unload one by one to ensure same behavior whatever auto_handling state
   foreach mod [lreverse $lmlist] {
      # record hint that mod will be reloaded (useful in case mod is sticky)
      lappendState reloading_sticky $mod
      lappendState reloading_supersticky $mod
      # save user asked state before it vanishes
      set isuasked($mod) [isModuleUserAsked $mod]
      # force unload even if requiring mods are not part of the unload list
      # (violation state) as modules are loaded again just after
      if {[cmdModuleUnload $context match 0 1 0 0 $mod]} {
         # avoid failing module on load phase
         set lmlist [replaceFromList $lmlist $mod]
         set errMsg [string map [list _MOD_ $mod] $errmsgtpl]
         if {$force} {
            # errMsg will always be set as force mode could not be enabled
            # for reload sub-cmd which provides an empty msg template
            reportWarning $errMsg 1
         # stop if one unload fails unless force mode enabled
         } else {
            lpopState reloading_sticky
            lpopState reloading_supersticky
            knerror $errMsg
         }
      }
      lpopState reloading_sticky
      lpopState reloading_supersticky
   }
   return [array get isuasked]
}

# load phase of a list of modules reload process
proc reloadModuleListLoadPhase {lmname isuaskedlist {force 0} {errmsgtpl {}}\
   {context load}} {
   upvar $lmname lmlist
   array set isuasked $isuaskedlist

   # loads are made with auto handling mode disabled to avoid disturbances
   # from a missing prereq automatically reloaded, so these module loads may
   # fail as prereq may not be satisfied anymore
   setConf auto_handling 0
   foreach mod $lmlist {
      # reload module with user asked property preserved
      if {[cmdModuleLoad $context $isuasked($mod) $mod]} {
         set errMsg [string map [list _MOD_ $mod] $errmsgtpl]
         if {$force} {
            # errMsg will always be set as force mode could not be enabled
            # for reload sub-cmd which provides an empty msg template
            reportWarning $errMsg 1
         # stop if one load fails unless force mode enabled
         } else {
            knerror $errMsg
         }
      }
   }
   setConf auto_handling 1
}

# test if loaded module 'mod' is sticky and if stickyness definition applies
# to one of the reloading module
proc isStickynessReloading {mod reloading_modlist {tag sticky}} {
   set res 0
   if {[isModuleTagged $mod $tag 1]} {
      # evalute the module-tag commands that are related to mod
      set dir [getModulepathFromModuleName [getModulefileFromLoadedModule\
         $mod] $mod]
      getModules $dir $mod 0 [list rc_defs_included]

      # if tag specifically applies to fully qualified module, exact same
      # module should be found in reload list
      if {[isModuleTagged $mod $tag 0 equal]} {
         set res [isInList $reloading_modlist $mod]
         reportDebug "stickyness ($tag) applies to '$mod', is reloading=$res"
      } else {
         # find most restrictive sticky tag definition
         set modprev $mod
         while {[set modpar [file  dirname $modprev]] ne {.}} {
            if {![isModuleTagged $modpar $tag]} {
               set stickypar $modprev
               break
            }
            set modprev $modpar
         }
         if {![info exists stickypar]} {
            set stickypar $modprev
         }
         # check if a loading mod satisfies sticky rules
         foreach modlo $reloading_modlist {
            if {[modEq $stickypar $modlo eqstart]} {
               # found match
               set res 1
               break
            }
         }
         reportDebug "stickyness ($tag) applies to '$stickypar', is\
            reloading=$res"
      }
   }

   return $res
}

########################################################################
# command line commands
#
proc cmdModuleList {show_oneperline show_mtime} {
   set json [isStateEqual report_format json]

   # load tags from loaded modules
   cacheCurrentModules

   # filter-out hidden loaded modules unless all module should be seen
   if {[getState hiding_threshold] > 1} {
      set loadedmodlist [getLoadedModuleList]
   } else {
      set loadedmodlist [list]
      foreach mod [getLoadedModuleList] {
         if {![isModuleTagged $mod hidden-loaded 1]} {
            lappend loadedmodlist $mod
         }
      }
   }

   if {[llength $loadedmodlist] == 0} {
      if {!$json && [isEltInReport header]} {
         report {No Modulefiles Currently Loaded.}
      }
   } else {
      # build mod_list array with loaded modules
      foreach mod $loadedmodlist {
         set modfile [getModulefileFromLoadedModule $mod]
         set mtime [expr {$show_mtime && [file exists $modfile] ?\
            [getFileMtime $modfile] : {}}]
         set mod_list($mod) [list modulefile $mtime $modfile]

         # fetch symbols from loaded environment information
         set modname [file dirname $mod]
         set sym_list {}
         foreach altname [getLoadedAltname $mod 0 sym] {
            # skip non-symbol entry
            if {$altname ne $modname} {
               lappend sym_list [file tail $altname]
            }
         }
         set ::g_symbolHash($mod) [lsort -dictionary $sym_list]
      }

      set one_per_line [expr {$show_mtime || $show_oneperline}]
      set show_idx [expr {!$show_mtime && [isEltInReport idx]}]
      set header [expr {!$json && [isEltInReport header] ? {Currently Loaded\
         Modulefiles} : {noheader}}]
      set theader_cols [list hi Package 39 Versions 19 {Last mod.} 19]

      reportModules {} $header {} terse $show_mtime $show_idx $one_per_line\
         $theader_cols loaded $loadedmodlist

      # display output key
      if {!$show_mtime && !$json && [isEltInReport key]} {
         displayKey
      }
   }
}

proc cmdModuleDisplay {args} {
   reportDebug "displaying $args"

   lappendState mode display
   set first_report 1
   foreach mod $args {
      lassign [getPathToModule $mod] modfile modname
      if {$modfile ne {}} {
         # only one separator lines between 2 modules
         if {$first_report} {
            displaySeparatorLine
            set first_report 0
         }
         report [sgr hi $modfile]:\n
         execute-modulefile $modfile $modname $mod
         displaySeparatorLine
      }
   }
   lpopState mode
}

proc cmdModulePaths {mod} {
   reportDebug ($mod)

   set dir_list [getModulePathList exiterronundef]
   foreach dir $dir_list {
      array unset mod_list
      array set mod_list [getModules $dir $mod 0 [list rc_defs_included]]

      # prepare list of dirs for alias/symbol target search, will first search
      # in currently looked dir, then in other dirs following precedence order
      set target_dir_list [concat [list $dir] [replaceFromList $dir_list\
         $dir]]

      # forcibly enable implicit_default to resolve alias target when it
      # points to a directory
      setConf implicit_default 1

      # build list of modulefile to print
      foreach elt [array names mod_list] {
         switch -- [lindex $mod_list($elt) 0] {
            modulefile {
               lappend ::g_return_text $dir/$elt
            }
            virtual {
               lappend ::g_return_text [lindex $mod_list($elt) 2]
            }
            alias - version {
               # resolve alias target
               set aliastarget [lindex $mod_list($elt) 1]
               lassign [getPathToModule $aliastarget $target_dir_list 0]\
                  modfile modname
               # add module target as result instead of alias
               if {$modfile ne {} && ![info exists mod_list($modname)]} {
                  lappend ::g_return_text $modfile
               }
            }
         }
      }

      # reset implicit_default to restore behavior defined
      unsetConf implicit_default
   }

   # sort results if any and remove duplicates
   if {[info exists ::g_return_text]} {
      set ::g_return_text [lsort -dictionary -unique $::g_return_text]
   } else {
      # set empty value to return empty if no result
      set ::g_return_text {}
   }
}

proc cmdModulePath {mod} {
   reportDebug ($mod)
   lassign [getPathToModule $mod] modfile modname
   # if no result set empty value to return empty
   if {$modfile eq {}} {
      set ::g_return_text {}
   } else {
      lappend ::g_return_text $modfile
   }
}

proc cmdModuleWhatIs {{mod {}}} {
   cmdModuleSearch $mod {}
}

proc cmdModuleApropos {{search {}}} {
   cmdModuleSearch {} $search
}

proc cmdModuleSearch {{mod {}} {search {}}} {
   reportDebug "($mod, $search)"

   # disable error reporting to avoid modulefile errors
   # to mix with valid search results
   inhibitErrorReport

   set json [isStateEqual report_format json]

   set icase [isIcase]
   defineModEqProc $icase [getConf extended_default]

   lappend searchmod rc_defs_included
   if {$mod eq {}} {
      lappend searchmod wild
   }
   set foundmod 0
   lappendState mode whatis
   set dir_list [getModulePathList exiterronundef]
   foreach dir $dir_list {
      array unset mod_list
      array set mod_list [getModules $dir $mod 0 $searchmod]
      array unset interp_list
      array set interp_list {}

      # forcibly enable implicit_default to resolve alias target when it
      # points to a directory
      setConf implicit_default 1

      # build list of modulefile to interpret
      foreach elt [array names mod_list] {
         switch -- [lindex $mod_list($elt) 0] {
            modulefile {
               if {[isModuleTagged $elt forbidden]} {
                  # register any error occuring on element matching search
                  if {[modEq $mod $elt]} {
                     set err_list($elt) [list accesserr [getForbiddenMsg\
                        $elt]]
                  }
               } else {
                  set interp_list($elt) $dir/$elt
                  # register module name in a global list (shared across
                  # modulepaths) to get hints when solving aliases/version
                  set full_list($elt) 1
               }
            }
            virtual {
               if {[isModuleTagged $elt forbidden]} {
                  # register any error occuring on element matching search
                  if {[modEq $mod $elt]} {
                     set err_list($elt) [list accesserr [getForbiddenMsg\
                        $elt]]
                  }
               } else {
                  set interp_list($elt) [lindex $mod_list($elt) 2]
                  set full_list($elt) 1
               }
            }
            alias {
               # resolve alias target
               set elt_target [lindex $mod_list($elt) 1]
               if {![info exists full_list($elt_target)]} {
                  lassign [getPathToModule $elt_target $dir 0]\
                     modfile modname issuetype issuemsg
                  # add module target as result instead of alias
                  if {$modfile ne {} && ![info exists mod_list($modname)]} {
                     set interp_list($modname) $modfile
                     set full_list($modname) 1
                  } elseif {$modfile eq {}} {
                     # if module target not found in current modulepath add to
                     # list for global search after initial modulepath lookup
                     if {[string first {Unable to locate} $issuemsg] == 0} {
                        set extra_search($modname) [list $dir [modEq $mod\
                           $elt]]
                     # register resolution error if alias name matches search
                     } elseif {[modEq $mod $elt]} {
                        set err_list($modname) [list $issuetype $issuemsg]
                     }
                  }
               }
            }
            version {
               # report error of version target if matching query
               set elt_target [getArrayKey mod_list [lindex $mod_list($elt) 1]\
                  $icase]
               if {[info exists mod_list($elt_target)] && [isInList [list\
                  invalid accesserr] [lindex $mod_list($elt_target) 0]] &&\
                  [modEq $mod $elt]} {
                  set err_list($elt_target) $mod_list($elt_target)
               } elseif {![info exists mod_list($elt_target)]} {
                  set extra_search($elt_target) [list $dir [modEq $mod $elt]]
               }
            }
            invalid - accesserr {
               # register any error occuring on element matching search
               if {[modEq $mod $elt]} {
                  set err_list($elt) $mod_list($elt)
               }
            }
         }
      }

      # reset implicit_default to restore behavior defined
      unsetConf implicit_default

      # in case during modulepath lookup we find an alias target we were
      # looking for in previous modulepath, remove this element from global
      # search list
      foreach elt [array names extra_search] {
         if {[info exists full_list($elt)]} {
            unset extra_search($elt)
         }
      }

      # save results from this modulepath for interpretation step as there
      # is an extra round of search to match missing alias target, we cannot
      # process modulefiles found immediately
      if {[array size interp_list] > 0} {
         set interp_save($dir) [array get interp_list]
      }
   }

   # forcibly enable implicit_default to resolve alias target when it points
   # to a directory
   setConf implicit_default 1

   # find target of aliases in all modulepath except the one already tried
   foreach elt [array names extra_search] {
      lassign [getPathToModule $elt {} 0 no [lindex $extra_search($elt) 0]]\
         modfile modname issuetype issuemsg issuefile
      # found target so append it to results in corresponding modulepath
      if {$modfile ne {}} {
         # get belonging modulepath dir depending of module kind
         if {[isModuleVirtual $modname $modfile]} {
            set dir [findModulepathFromModulefile\
               $::g_sourceVirtual($modname)]
         } else {
            set dir [getModulepathFromModuleName $modfile $modname]
         }
         array unset interp_list
         if {[info exists interp_save($dir)]} {
            array set interp_list $interp_save($dir)
         }
         set interp_list($modname) $modfile
         set interp_save($dir) [array get interp_list]
      # register resolution error if primal alias name matches search
      } elseif {$modfile eq {} && [lindex $extra_search($elt) 1]} {
         set err_list($modname) [list $issuetype $issuemsg $issuefile]
      }
   }

   # reset implicit_default to restore behavior defined
   unsetConf implicit_default

   # prepare string translation to highlight search query string
   set matchmodmap [prepareMapToHightlightSubstr $mod]
   set matchsearchmap [prepareMapToHightlightSubstr $search]

   # interpret all modulefile we got for each modulepath
   foreach dir $dir_list {
      if {[info exists interp_save($dir)]} {
         array unset interp_list
         array set interp_list $interp_save($dir)
         set foundmod 1
         set display_list {}
         # interpret every modulefiles obtained to get their whatis text
         foreach elt [lsort -dictionary [array names interp_list]] {
            set ::g_whatis {}
            execute-modulefile $interp_list($elt) $elt $elt

            # treat whatis as a multi-line text
            if {$search eq {} || [regexp -nocase $search $::g_whatis]} {
               if {$json} {
                  lappend display_list [formatListEltToJsonDisplay $elt\
                     whatis a $::g_whatis]
               } else {
                  set eltsgr [string map $matchmodmap $elt]
                  foreach line $::g_whatis {
                     set linesgr [string map $matchsearchmap $line]
                     lappend display_list "[string repeat { } [expr {20 -\
                        [string length $elt]}]]$eltsgr: $linesgr"
                  }
               }
            }
         }

         displayElementList $dir mp sepline 1 0 $display_list
      }
   }
   lpopState mode

   setState inhibit_errreport 0

   # report errors if a modulefile was searched but not found
   if {$mod ne {} && !$foundmod} {
      # no error registered means nothing was found to match search
      if {![array exists err_list]} {
         set err_list($mod) [list none "Unable to locate a modulefile for\
            '$mod'"]
      }
      foreach elt [array names err_list] {
         eval reportIssue $err_list($elt)
      }
   }
}

proc cmdModuleSwitch {uasked old {new {}}} {
   # if a single name is provided it matches for the module to load and in
   # this case the module to unload is searched to find the closest match
   # (loaded module that shares at least the same root name)
   if {$new eq {}} {
      set new $old
      set unload_match close
   } else {
      set unload_match match
   }
   # save orig names to register them as deps if called from modulefile
   set argnew $new
   if {$new eq $old} {
      set argold {}
   } else {
      set argold $old
   }

   reportDebug "old='$old' new='$new' (uasked=$uasked)"

   # extend requirement recording inhibition to switch subcontext
   set inhibit_req_rec [expr {[currentState inhibit_req_record] ==\
      [currentState evalid]}]

   # record sumup messages from underlying unload/load actions under the same
   # switch message record id to report (evaluation messages still go under
   # their respective unload/load block
   pushMsgRecordId switch-$old-$new-[depthState modulename]
   if {$inhibit_req_rec} {
      lappendState inhibit_req_record [currentState evalid]
   }

   # enable unload of sticky mod if stickyness is preserved on swapped-on mod
   # need to resolve swapped-off module here to get stickyness details
   lassign [getPathToModule $old {} 0 $unload_match] modfile oldmod
   if {[set sticky_reload [isStickynessReloading $oldmod $new]]} {
      lappendState reloading_sticky $oldmod
   }
   if {[set supersticky_reload [isStickynessReloading $oldmod $new\
      super-sticky]]} {
      lappendState reloading_supersticky $oldmod
   }
   set ret [cmdModuleUnload swunload $unload_match 1 0 0 0 $old]
   if {$sticky_reload} {
      lpopState reloading_sticky
   }
   if {$supersticky_reload} {
      lpopState reloading_supersticky
   }

   # register modulefile to unload as conflict if an unload module is
   # mentionned on this module switch command set in a modulefile
   set orig_auto_handling [getConf auto_handling]
   if {!$uasked && $argold ne {}} {
      # skip conflict declaration if old spec matches new as in this case
      # switch means *replace loaded version of mod by this specific version*
      lassign [getPathToModule $new] newmodfile newmod
      if {$newmod eq {} || ![modEq $argold $newmod eqstart]} {
         # temporarily disable auto handling just to record deps, not to try
         # to load or unload them (already tried)
         setConf auto_handling 0
         catch {conflict $argold}
         setConf auto_handling $orig_auto_handling
      }
   }

   # attempt load and depre reload only if unload succeed
   if {!$ret} {
      cmdModuleLoad swload $uasked $new

      if {[getConf auto_handling] && [info exists deprelist] && [llength\
         $deprelist] > 0} {
         # cmdModuleUnload handles the DepUn, UReqUn mechanisms and the unload
         # phase of the DepRe mechanism. List of DepRe mods and their user
         # asked state is set from cmdModuleUnload procedure to be used here
         # for the load phase of the DepRe mechanism.
         # Try DepRe load phase: load failure will not lead to switch failure
         reloadModuleListLoadPhase deprelist [array get isuasked]\
            1 {Reload of dependent _MOD_ failed} depre
      }

      # report a summary of automated evaluations if no error
      reportModuleEval
   }

   # report all recorded sumup messages for this evaluation unless both old
   # and new modules are set hidden, old was auto loaded and this switch is
   # done by a modfile
   reportMsgRecord "Switching from [sgr hi $old] to [sgr hi $new]" [expr\
      {$oldhidden && !$olduasked && $newhidden && !$uasked}]
   popMsgRecordId
   if {$inhibit_req_rec} {
      lpopState inhibit_req_record
   }

   # register modulefile to load as prereq when called from modulefile
   if {!$uasked && !$ret && $argnew ne {}} {
      setConf auto_handling 0
      prereq $argnew
      setConf auto_handling $orig_auto_handling
   }
}

proc cmdModuleSave {{coll default}} {
   reportDebug $coll

   if {![areModuleConstraintsSatisfied]} {
      reportErrorAndExit {Cannot save collection, some module constraints are\
         not satistied}
   }

   # format collection content, version number of modulefile are saved if
   # version pinning is enabled
   if {[getConf collection_pin_version]} {
      set curr_mod_list [getLoadedModuleList]
      set curr_nuasked_list [getLoadedModuleNotUserAskedList]
   } else {
      lassign [getSimplifiedLoadedModuleList] curr_mod_list curr_nuasked_list
   }
   set save [formatCollectionContent [getModulePathList returnempty 0]\
      $curr_mod_list $curr_nuasked_list]

   if { [string length $save] == 0} {
      reportErrorAndExit {Nothing to save in a collection}
   }

   # get coresponding filename and its directory
   lassign [getCollectionFilename $coll] collfile colldesc
   set colldir [file dirname $collfile]

   if {![file exists $colldir]} {
      reportDebug "Creating $colldir"
      file mkdir $colldir
   } elseif {![file isdirectory $colldir]} {
      reportErrorAndExit "$colldir exists but is not a directory"
   }

   reportDebug "Saving $collfile"

   if {[catch {
      set fid [open $collfile w]
      puts $fid $save
      close $fid
   } errMsg ]} {
      reportErrorAndExit "Collection $colldesc cannot be saved.\n$errMsg"
   }
}

proc cmdModuleRestore {{coll default}} {
   reportDebug $coll

   # get coresponding filename
   lassign [getCollectionFilename $coll] collfile colldesc

   if {![file exists $collfile]} {
      reportErrorAndExit "Collection $colldesc cannot be found"
   }

   # read collection
   lassign [readCollectionContent $collfile $colldesc] coll_path_list\
      coll_mod_list coll_nuasked_list

   # collection should at least define a path or a mod
   if {[llength $coll_path_list] == 0 && [llength $coll_mod_list] == 0} {
      reportErrorAndExit "$colldesc is not a valid collection"
   }

   # forcibly enable implicit_default to restore colls saved in this mode
   setConf implicit_default 1

   # load tags from loaded modules
   cacheCurrentModules

   # fetch what is currently loaded
   set curr_path_list [getModulePathList returnempty 0]
   # get current loaded module list in simplified and raw versions
   # these lists may be used later on, see below
   set curr_mod_list_raw [getLoadedModuleList]
   set curr_nuasked_list_raw [getLoadedModuleNotUserAskedList]
   lassign [getSimplifiedLoadedModuleList] curr_mod_list curr_nuasked_list

   # determine what module to unload to restore collection
   # from current situation with preservation of the load order
   lassign [getMovementBetweenList $curr_mod_list $coll_mod_list\
      $curr_nuasked_list $coll_nuasked_list] mod_to_unload mod_to_load
   # determine unload movement with raw loaded list in case versions are
   # pinning in saved collection
   lassign [getMovementBetweenList $curr_mod_list_raw $coll_mod_list\
      $curr_nuasked_list_raw $coll_nuasked_list] mod_to_unload_raw\
      mod_to_load_raw
   if {[llength $mod_to_unload] > [llength $mod_to_unload_raw]} {
      set mod_to_unload $mod_to_unload_raw
   }

   # proceed as well for modulepath
   lassign [getMovementBetweenList $curr_path_list $coll_path_list] \
      path_to_unuse path_to_use

   # create an eval id to track successful/failed module evaluations
   pushMsgRecordId restore-$coll-[depthState modulename] 0

   # unload modules one by one (no dependency auto unload)
   foreach mod [lreverse $mod_to_unload] {
      if {[set sticky_reload [isStickynessReloading $mod $mod_to_load]]} {
         lappendState reloading_sticky $mod
      }
      if {[set supersticky_reload [isStickynessReloading $mod $mod_to_load\
         super-sticky]]} {
         lappendState reloading_supersticky $mod
      }
      cmdModuleUnload unload match 0 0 0 0 $mod
      if {$sticky_reload} {
         lpopState reloading_sticky
      }
      if {$supersticky_reload} {
         lpopState reloading_supersticky
      }
   }
   # unuse paths
   if {[llength $path_to_unuse] > 0} {
      eval cmdModuleUnuse [lreverse $path_to_unuse]
   }

   # since unloading a module may unload other modules or
   # paths, what to load/use has to be determined after
   # the undo phase, so current situation is fetched again
   set curr_path_list [getModulePathList returnempty 0]

   # here we may be in a situation were no more path is left
   # in module path, so we cannot easily compute the simplified loaded
   # module list. so we provide two helper lists: simplified and raw
   # versions of the loaded module list computed before starting to
   # unload modules. these helper lists may help to learn the
   # simplified counterpart of a loaded module if it was already loaded
   # before starting to unload modules
   lassign [getSimplifiedLoadedModuleList $curr_mod_list_raw $curr_mod_list]\
      curr_mod_list curr_nuasked_list
   set curr_mod_list_raw [getLoadedModuleList]
   set curr_nuasked_list_raw [getLoadedModuleNotUserAskedList]

   # determine what module to load to restore collection
   # from current situation with preservation of the load order
   lassign [getMovementBetweenList $curr_mod_list $coll_mod_list\
      $curr_nuasked_list $coll_nuasked_list] mod_to_unload mod_to_load
   # determine load movement with raw loaded list in case versions are
   # pinning in saved collection
   lassign [getMovementBetweenList $curr_mod_list_raw $coll_mod_list\
      $curr_nuasked_list_raw $coll_nuasked_list] mod_to_unload_raw\
      mod_to_load_raw
   if {[llength $mod_to_load] > [llength $mod_to_load_raw]} {
      set mod_to_load $mod_to_load_raw
   }

   # proceed as well for modulepath
   lassign [getMovementBetweenList $curr_path_list $coll_path_list] \
      path_to_unuse path_to_use

   # reset implicit_default to restore behavior defined
   unsetConf implicit_default

   # use paths
   if {[llength $path_to_use] > 0} {
      # always append path here to guaranty the order
      # computed above in the movement lists
      eval cmdModuleUse append $path_to_use
   }

   # load modules one by one with user asked state preserved
   foreach mod $mod_to_load {
      cmdModuleLoad load [notInList $coll_nuasked_list $mod] $mod
   }

   popMsgRecordId 0
}

proc cmdModuleSaverm {{coll default}} {
   reportDebug $coll

   # avoid to remove any kind of file with this command
   if {[string first / $coll] > -1} {
      reportErrorAndExit {Command does not remove collection specified as\
         filepath}
   }

   # get coresponding filename
   lassign [getCollectionFilename $coll] collfile colldesc

   if {![file exists $collfile]} {
      reportErrorAndExit "Collection $colldesc cannot be found"
   }

   # attempt to delete specified colletion
   if {[catch {
      file delete $collfile
   } errMsg ]} {
      reportErrorAndExit "Collection $colldesc cannot be removed.\n$errMsg"
   }
}

proc cmdModuleSaveshow {{coll default}} {
   reportDebug $coll

   # get coresponding filename
   lassign [getCollectionFilename $coll] collfile colldesc

   if {![file exists $collfile]} {
      reportErrorAndExit "Collection $colldesc cannot be found"
   }

   # read collection
   lassign [readCollectionContent $collfile $colldesc] coll_path_list\
      coll_mod_list coll_nuasked_list

   # collection should at least define a path or a mod
   if {[llength $coll_path_list] == 0 && [llength $coll_mod_list] == 0} {
      reportErrorAndExit "$colldesc is not a valid collection"
   }

   displaySeparatorLine
   report [sgr hi $collfile]:\n
   report [formatCollectionContent $coll_path_list $coll_mod_list\
      $coll_nuasked_list 1]
   displaySeparatorLine
}

proc cmdModuleSavelist {show_oneperline show_mtime} {
   # if a target is set, only list collection matching this
   # target (means having target as suffix in their name)
   set colltarget [getConf collection_target]
   if {$colltarget ne {}} {
      set suffix .$colltarget
      set targetdesc " (for target \"$colltarget\")"
   } else {
      set suffix {}
      set targetdesc {}
   }

   set json [isStateEqual report_format json]

   reportDebug "list collections for target \"$colltarget\""

   set coll_list [findCollections]

   if { [llength $coll_list] == 0} {
      if {!$json} {
         report "No named collection$targetdesc."
      }
   } else {
      set list {}
      if {!$json} {
         if {$show_mtime} {
            displayTableHeader hi Collection 59 {Last mod.} 19
         }
         report "Named collection list$targetdesc:"
      }
      set display_list {}
      set len_list {}
      set max_len 0
      if {$show_mtime || $show_oneperline} {
         set display_idx 0
         set one_per_line 1
      } else {
         set display_idx 1
         set one_per_line 0
      }

      foreach coll [lsort -dictionary $coll_list] {
         # remove target suffix from names to display
         regsub $suffix$ [file tail $coll] {} mod
         if {$json} {
            lappend display_list [formatListEltToJsonDisplay $mod target s\
               $colltarget pathname s $coll]
         # no need to test mod consistency as findCollections does not return
         # collection whose name starts with "."
         } elseif {$show_mtime} {
            set filetime [clock format [getFileMtime $coll]\
               -format {%Y/%m/%d %H:%M:%S}]
            lappend display_list [format %-60s%19s $mod $filetime]
         } else {
            lappend display_list $mod
            lappend len_list [set len [string length $mod]]
            if {$len > $max_len} {
               set max_len $len
            }
         }
      }

      displayElementList noheader {} {} $one_per_line $display_idx\
         $display_list $len_list $max_len
   }
}


proc cmdModuleSource {args} {
   reportDebug $args
   foreach fpath $args {
      set absfpath [getAbsolutePath $fpath]
      if {$fpath eq {}} {
         reportErrorAndExit {File name empty}
      } elseif {[file exists $absfpath]} {
         lappendState mode load
         # relax constraint of having a magic cookie at the start of the
         # modulefile to execute as sourced files may need more flexibility
         # as they may be managed outside of the modulefile environment like
         # the initialization modulerc file
         execute-modulefile $absfpath $absfpath $absfpath 0 0
         lpopState mode
      } else {
         reportErrorAndExit "File $fpath does not exist"
      }
   }
}

proc cmdModuleUnsource {args} {
   reportDebug $args
   foreach fpath $args {
      set absfpath [getAbsolutePath $fpath]
      if {$fpath eq {}} {
         reportErrorAndExit {File name empty}
      } elseif {[file exists $absfpath]} {
         lappendState mode unload
         # relax constraint of having a magic cookie at the start of the
         # modulefile to execute as sourced files may need more flexibility
         # as they may be managed outside of the modulefile environment like
         # the initialization modulerc file
         execute-modulefile $absfpath $absfpath $absfpath 0 0
         lpopState mode
      } else {
         reportErrorAndExit "File $fpath does not exist"
      }
   }
}

proc cmdModuleLoad {context uasked args} {
   reportDebug "loading $args (context=$context, uasked=$uasked)"

   set ret 0
   lappendState mode load
   foreach mod $args {
      # if a switch action is ongoing...
      if {$context eq {swload}} {
         set swprocessing 1
         # context is ReqLo if switch is called from a modulefile
         if {![isMsgRecordIdTop]} {
            set context reqlo
         }
         upvar newhidden hidden
      }
      # loading module is visible by default
      set hidden 0

      # record evaluation attempt on specified module name
      registerModuleEvalAttempt $context $mod
      lassign [getPathToModule $mod] modfile modname
      if {$modfile eq {}} {
         set ret 1
         # go to next module to unload
         continue
      }

      if {[isModuleEvalFailed load $modname]} {
         reportDebug "$modname ($modfile) load was already tried and failed"
         # nullify this evaluation attempt to avoid duplicate issue report
         unregisterModuleEvalAttempt $context $mod
         continue
      }

      # if a switch action is ongoing...
      if {[info exists swprocessing]} {
         # pass the DepRe mod list to the calling cmdModuleSwitch procedure to
         # let it handle the load phase of the DepRe mechanism along with the
         # DepRe modules set from switched off module.
         upvar deprelist swdeprelist
         upvar isuasked isuasked

         # transmit loaded mod name for switch report summary
         uplevel 1 set new $modname
      }

      # set a unique id to record messages related to this evaluation.
      set msgrecid load-$modname-[depthState modulename]
      pushMsgRecordId $msgrecid

      # record evaluation attempt on actual module name
      registerModuleEvalAttempt $context $modname
      registerModuleEvalAttempt $context $modfile

      # check if passed modname correspond to an already loaded modfile
      # and get its loaded name (in case it has been loaded as full path)
      set loadedmodname [getLoadedMatchingName $modname]
      if {$loadedmodname ne {}} {
         set modname $loadedmodname
      }

      pushSettings
      if {[set errCode [catch {
         if {[isModuleLoaded $modname] || [isModuleLoading $modname]} {
            reportDebug "$modname ($modfile) already loaded/loading"
            # report module is already loaded if verbose2 or higher level
            if {[isModuleLoaded $modname] && [isVerbosityLevel verbose2]} {
               reportInfo "Module '$modname' is already loaded"
               reportMsgRecord "Loading [sgr hi $modname]"
            }
            # exit treatment but no need to restore settings
            continue
         }

         # register altname of modname prior any conflict check
         eval setLoadedAltname "{$modname}" [getAllModuleResolvedName\
            $modname 1 $mod]

         if {[getConf auto_handling]} {
            # get loaded modules holding a requirement on modname and able to
            # be reloaded
            set deprelist [getUnmetDependentLoadedModuleList $modname]
            reportDebug "depre mod list is '$deprelist'"

            # Reload all modules that have declared a prereq on mod as they
            # may take benefit from their prereq availability if it is newly
            # loaded. First perform unload phase of the reload, prior mod load
            # to ensure these dependent modules are unloaded with the same
            # loaded prereq as when they were loaded
            if {[llength $deprelist] > 0} {
               array set isuasked [reloadModuleListUnloadPhase deprelist\
                  [getState force] {Unload of dependent _MOD_ failed} depun]
               if {[info exists swprocessing]} {
                  if {[info exists swdeprelist]} {
                     set swdeprelist [concat $deprelist $swdeprelist]
                  } else {
                     set swdeprelist $deprelist
                  }
               }
            }
         }

         if {[execute-modulefile $modfile $modname $mod]} {
            break
         }

         # register this evaluation on the main one that triggered it (after
         # load evaluation to report correct order with other evaluations)
         registerModuleEval $context $modname

         # raise an error if a conflict violation is detected
         # do that after modfile evaluation to give it the chance to solve its
         # (module unload) conflicts through its evaluation
         lassign [doesModuleConflict $modname] doescon modconlist\
            moddecconlist
         set retisconun [eval isModuleEvaluated conun "{$modname}"\
            $modconlist]
         if {![set retiseval [eval isModuleEvaluated any "{$modname}"\
            $modconlist]] || [currentState msgrecordid] ne [topState\
            msgrecordid] || !$retisconun} {
            # more appropriate msg if an evaluation was attempted or is
            # by-passed. error is reported using declared conflict name (as if
            # it was raised raised from a conflict modulefile command)
            set conmsg [expr {$retiseval || [getState force] ?\
               [getConIsLoadedMsg $moddecconlist [is-loading $modconlist]] :\
               [getErrConflictMsg $modname $moddecconlist]}]
         }

         # still proceed if force mode enabled
         if {[getState force] && $doescon} {
            defineModEqProc [isIcase] [getConf extended_default]
            # report warning if not already done
            set report_con 1
            if {[info exists ::report_conflict($modname)]} {
               # check if conflict has not been already reported with an
               # alternative name
               foreach modalt [concat [getLoadedAltname $modconlist]\
                  $modconlist $moddecconlist] {
                  foreach reportmod $::report_conflict($modname) {
                     if {[doesModuleMatchesName $modalt $reportmod]} {
                        set report_con 0
                        break
                     }
                  }
               }
            }
            if {$report_con && [info exists conmsg]} {
               reportWarning $conmsg
            }
            # raise conun-specific msg to top level if attempted
            if {$retisconun} {
               reportWarning [getErrConUnMsg $moddecconlist] 1
            }
         } elseif {$doescon} {
            if {$retisconun} {
               if {[info exists conmsg]} {
                  reportError $conmsg
               }
               # raise conun-specific msg to top level if attempted
               knerror [getErrConUnMsg $moddecconlist]
            } else {
               set errlocalreport 1
               knerror $conmsg
            }
         }

         # loading visibility depends on hidden-loaded tag
         set hidden [isModuleTagged $modname hidden-loaded 1]

         add-path append LOADEDMODULES $modname
         # allow duplicate modfile entries for virtual modules
         add-path append --duplicates _LMFILES_ $modfile
         # update cache arrays
         setLoadedModule $modname $modfile $uasked

         # register declared source-sh in environment
         if {[set modsrcsh [getLoadedSourceSh $modname 1]] ne {}} {
            add-path append MODULES_LMSOURCESH $modsrcsh
         }

         # register declared conflict in environment
         if {[set modcon [getLoadedConflict $modname 1]] ne {}} {
            add-path append MODULES_LMCONFLICT $modcon
         }

         # declare the prereq of this module
         if {[set modpre [getLoadedPrereq $modname 1]] ne {}} {
            add-path append MODULES_LMPREREQ $modpre
         }

         # declare module as not asked by user (automatically loaded as
         # dependency) if it is the case
         if {!$uasked} {
            add-path append MODULES_LMNOTUASKED $modname
         }

         # declare the alternative names of this module
         if {[set modalt [getLoadedAltname $modname 1]] ne {}} {
            add-path append MODULES_LMALTNAME $modalt
         }

         # declare the tags of this module
         if {[set modtag [getTagList $modname 1]] ne {}} {
            add-path append MODULES_LMTAG $modtag
         }

         # Load phase of dependent module reloading. These modules can adapt
         # now that mod is seen loaded. Except if switch action ongoing (DepRe
         # load phase will occur from switch)
         if {[getConf auto_handling] && [llength $deprelist] > 0 && ![info\
            exists swprocessing]} {
            reloadModuleListLoadPhase deprelist [array get isuasked]\
               [getState force] {Reload of dependent _MOD_ failed} depre
         }

         # consider evaluation hidden if hidden loaded module is auto loaded
         # and no specific messages are recorded for this evaluation
         if {$hidden && !$uasked && ![isMsgRecorded]} {
            registerModuleEvalHidden $context $modname
         }

         # report a summary of automated evaluations if no error
         reportModuleEval
      } errMsg]] != 0 && $errCode != 4} {
         # in case of error report module info even if set hidden
         set hidden 0
         if {$errMsg ne {}} {
            reportError $errMsg [expr {![info exists errlocalreport]}]
         }
         # report switched-on module load failure under switch info block
         # unless the above reportError call already put a mesg to this block
         if {[info exists swprocessing] && ($errMsg eq {} || [info exists\
            errlocalreport])} {
            # warn as this issue does not lead to a rollback of switch action
            reportWarning "Load of switched-on $modname failed" 1
         }
         # rollback settings if some evaluation went wrong
         set ret 1
         restoreSettings
         # remove from successfully evaluated module list
         registerModuleEval $context $modname 1 load
         unset -nocomplain errlocalreport
      }
      popSettings

      # report all recorded messages for this evaluation except if module were
      # already loaded
      if {$errCode != 4} {
         reportMsgRecord "Loading [sgr hi $modname]" [expr {$hidden &&\
            !$uasked}]
      }
      popMsgRecordId
   }
   lpopState mode

   return $ret
}

proc cmdModuleUnload {context match auto force onlyureq onlyndep args} {
   reportDebug "unloading $args (context=$context, match=$match, auto=$auto,\
      force=$force, onlyureq=$onlyureq, onlyndep=$onlyndep)"

   set ret 0
   lappendState mode unload
   foreach mod $args {
      # if a switch action is ongoing...
      if {$context eq {swunload}} {
         set swprocessing 1
         # context is ConUn if switch is called from a modulefile
         if {![isMsgRecordIdTop]} {
            set context conun
         }
         upvar oldhidden hidden
         upvar olduasked uasked
      }
      # unloading module is visible by default
      set hidden 0
      set uasked 1

      # record evaluation attempt on specified module name
      registerModuleEvalAttempt $context $mod
      # resolve by also looking at matching loaded module
      lassign [getPathToModule $mod {} 1 $match] modfile modname errkind
      if {$modfile eq {}} {
         # no error return if module is not loaded
         if {$errkind eq {notloaded}} {
            reportDebug "$modname is not loaded"
            # report module is not loaded if verbose2 or higher level
            if {[isVerbosityLevel verbose2]} {
               set msgrecid unload-$modname-[depthState modulename]
               pushMsgRecordId $msgrecid
               reportInfo "Module '$modname' is not loaded"
               reportMsgRecord "Unloading [sgr hi $modname]"
            }
         } else {
            set ret 1
         }
         # go to next module to unload
         continue
      }

      if {$onlyureq && ![isModuleUnloadable $modname]}  {
         reportDebug "$modname ($modfile) is required by loaded module or\
            asked by user"
         continue
      }

      if {[isModuleEvalFailed unload $modname]} {
         reportDebug "$modname ($modfile) unload was already tried and failed"
         # nullify this evaluation attempt to avoid duplicate issue report
         unregisterModuleEvalAttempt $context $mod
         continue
      }

      # if a switch action is ongoing...
      if {[info exists swprocessing]} {
         # pass the DepRe mod list to the calling cmdModuleSwitch
         # procedure to let it handle the load phase of the DepRe
         # mechanism once the switched-to module will be loaded
         upvar deprelist deprelist
         upvar isuasked isuasked

         # transmit unloaded mod name for switch report summary
         uplevel 1 set old $modname
      }
      # set a unique id to record messages related to this evaluation.
      set msgrecid unload-$modname-[depthState modulename]
      pushMsgRecordId $msgrecid

      # record evaluation attempt on actual module name
      registerModuleEvalAttempt $context $modname
      registerModuleEvalAttempt $context $modfile

      pushSettings
      if {[set errCode [catch {
         # error if unloading module violates a registered prereq
         # and auto handling mode is disabled
         set prereq_list [getDependentLoadedModuleList [list $modname]]
         if {[llength $prereq_list] > 0 && (![getConf auto_handling] ||\
            !$auto)} {
            # force mode should not affect if we only look for mods w/o dep
            if {([getState force] || $force) && !$onlyndep} {
               # in case unload is called for a DepRe mechanism do not warn
               # about prereq violation enforced as it is due to the dependent
               # module which is already in a violation state
               # warn in case of a purge
               if {$auto || !$force || [currentState commandname] eq {purge}} {
                  reportWarning [getDepLoadedMsg $prereq_list]
               }
            } else {
               set errlocalreport 1
               # exit treatment but no need to set return code to error if
               # called from a 'module unload' command in a modulefile in a
               # load evaluation mode, as set conflict will raise error at end
               # of modulefile evaluation
               if {$onlyndep} {
                  set errharmless 1
               }
               knerror [expr {[eval isModuleEvaluated any "{$modname}"\
                  $prereq_list] ? [getDepLoadedMsg $prereq_list] :\
                  [getErrPrereqMsg $modname $prereq_list 0]}]
            }
         }

         if {[getConf auto_handling] && $auto} {
            # compute lists of modules to update due to modname unload prior
            # unload to get requirement info before it vanishes

            # DepUn: Dependent to Unload (modules actively requiring modname
            # or a module part of this DepUn batch)
            set depunnpolist [getDependentLoadedModuleList [list $modname] 1\
               0 1 0]
            set depunlist [getDependentLoadedModuleList [list $modname] 1 0 0 0]
            # look at both regular dependencies or No Particular Order
            # dependencies: use NPO result if situation can be healed with NPO
            # dependencies, which will be part of DepRe list to restore the
            # correct loading order for them
            if {[llength $depunnpolist] <= [llength $depunlist]} {
               set depunlist $depunnpolist
            }
            reportDebug "depun mod list is '$depunlist'"

            # do not check for UReqUn mods coming from DepUn modules as these
            # DepUn modules are reloaded
            if {[info exists swprocessing]} {
               set urequnqry [list $modname]
            } else {
               set urequnqry [concat $depunlist [list $modname]]
            }

            # UReqUn: Useless Requirement to Unload (autoloaded requirements
            # of modname or DepUn modules not required by any remaining mods)
            set urequnlist [getUnloadableLoadedModuleList $urequnqry]
            reportDebug "urequn mod list is '$urequnlist'"

            # DepRe: Dependent to Reload (modules optionnaly dependent or in
            # conflict with modname, DepUn or UReqUn modules + modules
            # dependent of a module part of this DepRe batch)
            set deprelist [getDependentLoadedModuleList [concat $urequnlist\
               $depunlist [list $modname]] 0 0 1 0 1 1]
            reportDebug "depre mod list is '$deprelist'"

            # DepUn mods are merged into the DepRe list as an attempt to
            # reload these DepUn mods is made once switched-to mod loaded
            if {[info exists swprocessing]} {
               set deprelist [sortModulePerLoadedAndDepOrder [concat\
                  $depunlist $deprelist] 1]
               set depunlist {}
            }

            # Reload of all DepRe mods, as they may adapt from the mod unloads
            # happening here. First perform unload phase of the reload, prior
            # mod unloads to ensure these dependent mods are unloaded with the
            # same loaded prereq as when they were loaded. Avoid modules not
            # satisfying their constraint.
            if {[llength $deprelist] > 0} {
               array set isuasked [reloadModuleListUnloadPhase deprelist\
                  [getState force] {Unload of dependent _MOD_ failed} depun]
            }

            # DepUn modules unload prior main mod unload
            if {[llength $depunlist] > 0} {
               foreach unmod [lreverse $depunlist] {
                  if {[cmdModuleUnload depun match 0 0 0 0 $unmod]} {
                     # stop if one unload fails unless force mode enabled
                     set errMsg "Unload of dependent $unmod failed"
                     if {[getState force] || $force} {
                        reportWarning $errMsg 1
                     } else {
                        knerror $errMsg
                     }
                  }
               }
            }
         }

         # register this evaluation on the main one that triggered it (prior
         # unload evaluation to report correct order with other evaluations)
         registerModuleEval $context $modname

         if {[execute-modulefile $modfile $modname $mod]} {
            break
         }

         # unloading visibility depends on hidden-loaded tag
         set hidden [isModuleTagged $modname hidden-loaded 1]

         # get module position in loaded list to remove corresponding loaded
         # modulefile (entry at same position in _LMFILES_)
         # need the unfiltered loaded module list to get correct index
         set lmidx [lsearch -exact [getLoadedModuleList 0] $modname]
         unload-path LOADEDMODULES $modname
         unload-path --index _LMFILES_ $lmidx
         if {![set uasked [isModuleUserAsked $modname]]} {
            unload-path MODULES_LMNOTUASKED $modname
         }
         # update cache arrays
         unsetLoadedModule $modname $modfile

         # unregister declared source-sh
         if {[set modsrcsh [getLoadedSourceSh $modname 1]] ne {}} {
            unload-path MODULES_LMSOURCESH $modsrcsh
         }
         unsetLoadedSourceSh $modname

         # unregister declared conflict
         if {[set modcon [getLoadedConflict $modname 1]] ne {}} {
            unload-path MODULES_LMCONFLICT $modcon
         }
         unsetLoadedConflict $modname

         # unset prereq declared for this module
         if {[llength [set modpre [getLoadedPrereq $modname]]] > 0} {
            unload-path MODULES_LMPREREQ [getLoadedPrereq $modname 1]
         }
         unsetLoadedPrereq $modname

         # unset alternative names declared for this module
         if {[llength [set modalt [getLoadedAltname $modname]]] >0} {
            unload-path MODULES_LMALTNAME [getLoadedAltname $modname 1]
         }
         unsetLoadedAltname $modname

         # unset tags declared for this module
         if {[set modtag [getTagList $modname 1]] ne {}} {
            unload-path MODULES_LMTAG $modtag
         }

         if {[getConf auto_handling] && $auto} {
            # UReqUn modules unload now DepUn+main mods are unloaded
            if {[llength $urequnlist] > 0} {
               set urequnlist [lreverse $urequnlist]
               for {set i 0} {$i < [llength $urequnlist]} {incr i 1} {
                  set unmod [lindex $urequnlist $i]
                  if {[cmdModuleUnload urequn match 0 0 0 0 $unmod]} {
                     # just warn if UReqUn module cannot be unloaded, main
                     # unload process continues, just the UReqUn modules that
                     # are required by unmod (whose unload failed) are
                     # withdrawn from UReqUn module list
                     reportWarning "Unload of useless requirement $unmod\
                        failed" 1
                     lassign [getDiffBetweenList $urequnlist\
                        [getRequiredLoadedModuleList [list $unmod]]]\
                        urequnlist
                  }
               }
            }

            # DepRe modules load phase now DepUn+UReqUn+main mods are unloaded
            # except if a switch action is ongoing as this DepRe load phase
            # will occur after the new mod load
            if {[llength $deprelist] > 0 && ![info exists swprocessing]} {
               reloadModuleListLoadPhase deprelist [array get isuasked]\
                  [getState force] {Reload of dependent _MOD_ failed} depre
            }
         }

         # consider evaluation hidden if hidden loaded module was auto loaded
         # and no specific messages are recorded for this evaluation
         if {$hidden && !$uasked && ![isMsgRecorded]} {
            registerModuleEvalHidden $context $modname
         }

         # report a summary of automated evaluations if no error
         reportModuleEval
      } errMsg]] != 0 && $errCode != 4} {
         # in case of error report module info even if set hidden
         set hidden 0
         if {$errMsg ne {}} {
            reportError $errMsg [expr {![info exists errlocalreport]}]
         }
         # report switched-off module unload failure under switch info block
         # unless the above reportError call already put a mesg to this block
         if {[info exists swprocessing] && ($errMsg eq {} || [info exists\
            errlocalreport])} {
            reportError "Unload of switched-off $modname failed" 1
         }
         # rollback settings if some evaluation went wrong
         if {![info exists errharmless]} {
            set ret 1
            restoreSettings
            # remove from successfully evaluated module list
            registerModuleEval $context $modname 1 unload
         }
         unset -nocomplain errlocalreport errharmless
      }
      popSettings

      # report all recorded messages for this evaluation (hide evaluation if
      # loaded mod is set hidden, has been automatically loaded and unloaded)
      reportMsgRecord "Unloading [sgr hi $modname]" [expr {$hidden &&\
         !$uasked && [depthState evalid] != 1}]
      popMsgRecordId
   }
   lpopState mode

   return $ret
}

proc cmdModulePurge {} {
   reportDebug called.

   # create an eval id to track successful/failed module evaluations
   pushMsgRecordId purge-[depthState modulename] 0

   # unload one by one to ensure same behavior whatever auto_handling state
   # force it to handle loaded modules in violation state
   # remove dependent modules if force mode enabled
   set onlyndep [expr {![getState force]}]
   eval cmdModuleUnload unload match 0 1 0 $onlyndep [lreverse\
      [getLoadedModuleList]]

   popMsgRecordId 0
}

proc cmdModuleReload {args} {
   # reload all loaded modules if no module list passed
   if {[llength $args] == 0} {
      set lmlist [getLoadedModuleList]
   } else {
      set lmlist $args
   }
   reportDebug "reloading $lmlist"

   # create an eval id to track successful/failed module evaluations
   pushMsgRecordId reload-[depthState modulename] 0

   # no reload of all loaded modules attempt if constraints are violated
   if {[llength $args] == 0 && ![areModuleConstraintsSatisfied]} {
      reportError {Cannot reload modules, some of their constraints are not\
         satistied}
   } else {
      pushSettings
      if {[set errCode [catch {
         # run unload then load-again phases
         array set isuasked [reloadModuleListUnloadPhase lmlist]
         reloadModuleListLoadPhase lmlist [array get isuasked]
      } errMsg]] == 1} {
         # rollback settings if some evaluation went wrong
         restoreSettings
      }
      popSettings
   }

   popMsgRecordId 0
}

proc cmdModuleAliases {} {
   # disable error reporting to avoid modulefile errors
   # to mix with avail results
   inhibitErrorReport

   # parse paths to fill g_moduleAlias and g_moduleVersion
   foreach dir [getModulePathList exiterronundef] {
      getModules $dir {} 0 {}
   }

   setState inhibit_errreport 0

   set display_list {}
   foreach name [lsort -dictionary [array names ::g_moduleAlias]] {
      # exclude hidden aliases from result
      if {![isModuleHidden $name]} {
         lappend display_list "[sgr al $name] -> $::g_moduleAlias($name)"
      }
   }
   displayElementList Aliases hi sepline 1 0 $display_list

   set display_list {}
   foreach name [lsort -dictionary [array names ::g_moduleVersion]] {
      # exclude hidden versions or versions targeting an hidden module
      if {![isModuleHidden $name] && ![isModuleHidden\
         $::g_moduleVersion($name)]} {
         lappend display_list "[sgr sy $name] -> $::g_moduleVersion($name)"
      }
   }
   displayElementList Versions hi sepline 1 0 $display_list
}

proc cmdModuleAvail {show_oneperline show_mtime show_filter search_filter\
   search_match args} {
   if {[llength $args] == 0} {
      lappend args *
   }

   if {$show_mtime || $show_oneperline} {
      set one_per_line 1
      set hstyle terse
      set theader_cols [list hi Package/Alias 39 Versions 19 {Last mod.} 19]
   } else {
      set one_per_line 0
      set hstyle sepline
      set theader_cols {}
   }

   # set a default filter (do not print dirs with no sym) if none set
   if {$show_filter eq {}} {
      set show_filter noplaindir
   }

   # elements to include in output
   set report_modulepath [isEltInReport modulepath]

   # consolidate search filters
   lappend search_filter $search_match wild
   set search_rc_filter $search_filter
   lappend search_rc_filter rc_alias_only

   # disable error reporting to avoid modulefile errors
   # to mix with avail results
   inhibitErrorReport

   foreach mod $args {
      array unset mod_list
      # look if aliases have been defined in the global or user-specific
      # modulerc and display them if any in a dedicated list
      array set mod_list [getModules {} $mod $show_mtime $search_rc_filter\
         $show_filter]

      if {$report_modulepath} {
         reportModules $mod {global/user modulerc} hi $hstyle $show_mtime 0\
            $one_per_line $theader_cols hidden-loaded
      }

      foreach dir [getModulePathList exiterronundef] {
         if {$report_modulepath} {
            array unset mod_list
            # get module list (process full dir content and do not exit when
            # err is raised from a modulerc)
            array set mod_list [getModules $dir $mod $show_mtime\
               $search_filter $show_filter]
            reportModules $mod $dir mp $hstyle $show_mtime 0 $one_per_line\
               $theader_cols hidden-loaded
         } else {
            # add result if not already added from an upper priority modpath
            foreach {elt props} [getModules $dir $mod $show_mtime\
               $search_filter $show_filter] {
               if {![info exists mod_list($elt)]} {
                  set mod_list($elt) $props
               }
            }
         }
      }

      # no report by modulepath, mix all aggregated results
      if {!$report_modulepath} {
         reportModules $mod noheader {} {} $show_mtime 0 $one_per_line\
            $theader_cols hidden-loaded
      }
   }

   # display output key
   if {!$show_mtime && ![isStateEqual report_format json] && [isEltInReport\
      key]} {
      displayKey
   }

   setState inhibit_errreport 0
}

proc cmdModuleUse {pos args} {
   reportDebug $args

   if {$args eq {}} {
      showModulePath
   } else {
      foreach path $args {
         if {$path eq {}} {
            reportError {Directory name empty}
         } else {
            # tranform given path in an absolute path to avoid dependency to
            # the current work directory. except if this path starts with a
            # variable reference
            if {[string index $path 0] ne {$}} {
               set path [getAbsolutePath $path]
            }
            if {[file isdirectory [resolvStringWithEnv $path]]} {
               lappendState mode load
               catch {add-path $pos MODULEPATH $path}
               lpopState mode
            } else {
               reportError "Directory '$path' not found"
            }
         }
      }
   }
}

proc cmdModuleUnuse {args} {
   reportDebug $args

   if {$args eq {}} {
      showModulePath
   } else {
      foreach path $args {
         # get current module path list
         # no absolute path conversion for the moment
         if {![info exists modpathlist]} {
            set modpathlist [getModulePathList returnempty 0 0]
         }

         # skip empty string
         if {$path eq {}} {
            reportError {Directory name empty}
            continue
         }

         # transform given path in an absolute path which should have been
         # registered in the MODULEPATH env var. however for compatibility
         # with previous behavior where relative paths were registered in
         # MODULEPATH given path is first checked against current path list
         set abspath [getAbsolutePath $path]
         if {[isInList $modpathlist $path]} {
            set unusepath $path
         } elseif {[isInList $modpathlist $abspath]} {
            set unusepath $abspath
         } else {
            set unusepath {}
         }

         if {$unusepath ne {}} {
            lappendState mode unload
            catch {
               unload-path MODULEPATH $unusepath
            }
            lpopState mode

            # refresh path list after unload
            set modpathlist [getModulePathList returnempty 0 0]
            if {[isInList $modpathlist $unusepath]} {
               reportWarning "Did not unuse $unusepath"
            }
         }
      }
   }
}

proc cmdModuleAutoinit {} {
   reportDebug called.

   # flag to make renderSettings define the module command
   setState autoinit 1

   # initialize env variables around module command
   lappendState mode load

   # register command location
   setenv MODULES_CMD [getAbsolutePath $::argv0]

   # define current Modules version if versioning enabled
   #if {![info exists ::env(MODULE_VERSION)]} {
   #   setenv MODULE_VERSION 4.7.0
   #   setenv MODULE_VERSION_STACK 4.7.0
   #}

   # initialize default MODULEPATH and LOADEDMODULES
   if {[get-env MODULEPATH] eq {}} {
      # set modpaths defined in modulespath config file if it exists, use file
      # in etcdir if it exists, dot file in initdir elsewhere
      set modulespath [expr {[file exists /usr/share/etc/modulespath] ?\
         {/usr/share/etc/modulespath} : {/usr/share/init/.modulespath}}]
      if {[file readable $modulespath]} {
         set fdata [split [readFile $modulespath] \n]
         foreach fline $fdata {
            if {[regexp {^\s*(.*?)\s*(#.*|)$} $fline match patharg] == 1\
               && $patharg ne {}} {
               foreach path [split $patharg :] {
                  # resolve path directory in case wildcard character used
                  set globlist [glob -types d -nocomplain $path]
                  if {[llength $globlist] == 0} {
                     lappend pathlist $path
                  } else {
                     eval lappend pathlist $globlist
                  }
               }
            }
         }
         if {[info exists pathlist]} {
            eval cmdModuleUse append $pathlist
         }
      }

      if {![info exists ::env(MODULEPATH)]} {
         setenv MODULEPATH {}
      }
   }
   if {![info exists ::env(LOADEDMODULES)]} {
      setenv LOADEDMODULES {}
   }

   # source initialization modulerc if any and if no env already initialized
   # use initrc file in etcdir if any, modulerc file in initdir otherwise
   if {[get-env MODULEPATH] eq {} && [get-env LOADEDMODULES] eq {}} {
      set initrc [expr {[file exists /usr/share/etc/initrc] ? {/usr/share/etc/initrc} :\
         {/usr/share/init/modulerc}}]
      if {[file exists $initrc]} {
         setState rc_running 1
         cmdModuleSource $initrc
         unsetState rc_running
      }
   }

   # default MODULESHOME
   setenv MODULESHOME [getConf home]

   # append dir where to find module function for ksh (to get it defined in
   # interactive and non-interactive sub-shells). also applies for shells
   # listed in shells_with_ksh_fpath conf
   if {[isInList [concat [split [getConf shells_with_ksh_fpath] :] [list\
      ksh]] [getState shell]]} {
      append-path FPATH /usr/share/init/ksh-functions
   }

   # define Modules init script as shell startup file
   if {[getConf set_shell_startup] && [isInList [list sh csh fish]\
      [getState shelltype]]} {
      # setup ENV variables to get module defined in sub-shells (works for
      # 'sh' and 'ksh' in interactive mode and 'sh' (zsh-compat), 'bash' and
      # 'ksh' (zsh-compat) in non-interactive mode.
      setenv ENV /usr/share/init/profile.sh
      setenv BASH_ENV /usr/share/init/bash
   }

   lpopState mode
}

proc cmdModuleInit {args} {
   set init_cmd [lindex $args 0]
   set init_list [lrange $args 1 end]
   set notdone 1
   set nomatch 1

   reportDebug $args

   # Define startup files for each shell
   set files(csh) [list .modules .cshrc .cshrc_variables .login]
   set files(tcsh) [list .modules .tcshrc .cshrc .cshrc_variables .login]
   set files(sh) [list .modules .bash_profile .bash_login .profile .bashrc]
   set files(bash) $files(sh)
   set files(ksh) $files(sh)
   set files(fish) [list .modules .config/fish/config.fish]
   set files(zsh) [list .modules .zshrc .zshenv .zlogin]

   # Process startup files for this shell
   set current_files $files([getState shell])
   foreach filename $current_files {
      if {$notdone} {
         set filepath $::env(HOME)
         append filepath / $filename

         reportDebug "Looking at $filepath"
         if {[file readable $filepath] && [file isfile $filepath]} {
            set newinit {}
            set thismatch 0

            foreach curline [split [readFile $filepath] \n] {
               # Find module load/add command in startup file
               set comments {}
               if {$notdone && [regexp {^([ \t]*module[ \t]+(load|add)[\
                  \t]*)(.*)} $curline match cmd subcmd modules]} {
                  set nomatch 0
                  set thismatch 1
                  regexp {([ \t]*\#.+)} $modules match comments
                  regsub {\#.+} $modules {} modules

                  # remove existing references to the named module from
                  # the list Change the module command line to reflect the
                  # given command
                  switch -- $init_cmd {
                     list {
                        if {![info exists notheader]} {
                           report "[getState shell] initialization file\
                              \$HOME/$filename loads modules:"
                           set notheader 0
                        }
                        report \t$modules
                     }
                     add {
                        foreach newmodule $init_list {
                           set modules [replaceFromList $modules $newmodule]
                        }
                        lappend newinit "$cmd$modules $init_list$comments"
                        # delete new modules in potential next lines
                        set init_cmd rm
                     }
                     prepend {
                        foreach newmodule $init_list {
                           set modules [replaceFromList $modules $newmodule]
                        }
                        lappend newinit "$cmd$init_list $modules$comments"
                        # delete new modules in potential next lines
                        set init_cmd rm
                     }
                     rm {
                        set oldmodcount [llength $modules]
                        foreach oldmodule $init_list {
                           set modules [replaceFromList $modules $oldmodule]
                        }
                        set modcount [llength $modules]
                        lappend newinit [expr {$modcount > 0 ?\
                           "$cmd$modules$comments" : [string trim $cmd]}]
                        if {$oldmodcount > $modcount} {
                           set notdone 0
                        }
                     }
                     switch {
                        set oldmodule [lindex $init_list 0]
                        set newmodule [lindex $init_list 1]
                        set newmodules [replaceFromList $modules\
                           $oldmodule $newmodule]
                        lappend newinit $cmd$newmodules$comments
                        if {$modules ne $newmodules} {
                           set notdone 0
                        }
                     }
                     clear {
                        lappend newinit [string trim $cmd]
                     }
                  }
               } elseif {$curline ne {}} {
                  # copy the line from the old file to the new
                  lappend newinit $curline
               }
            }

            if {$init_cmd ne {list} && $thismatch} {
               reportDebug "Writing $filepath"
               if {[catch {
                  set fid [open $filepath w]
                  puts $fid [join $newinit \n]
                  close $fid
               } errMsg ]} {
                  reportErrorAndExit "Init file $filepath cannot be\
                     written.\n$errMsg"
               }
            }
         }
      }
   }

   # quit in error if command was not performed due to no match
   if {$nomatch && $init_cmd ne {list}} {
      reportErrorAndExit "Cannot find a 'module load' command in any of the\
         '[getState shell]' startup files"
   }
}

# provide access to modulefile specific commands from the command-line, making
# them standing as a module sub-command (see module procedure)
proc cmdModuleResurface {cmd args} {
   reportDebug "cmd='$cmd', args='$args'"

   lappendState mode load
   lappendState commandname $cmd

   # run modulefile command and get its result
   if {[catch {eval $cmd $args} res]} {
      # report error if any and return false
      reportError $res
   } else {
      # register result depending of return kind (false or text)
      switch -- $cmd {
         module-info {
            set ::g_return_text $res
         }
         default {
            if {$res == 0} {
               # render false if command returned false
               setState return_false 1
            }
         }
      }
   }

   lpopState commandname
   lpopState mode
}

proc cmdModuleTest {args} {
   reportDebug "testing $args"

   lappendState mode test
   set first_report 1
   foreach mod $args {
      lassign [getPathToModule $mod] modfile modname
      if {$modfile ne {}} {
         # only one separator lines between 2 modules
         if {$first_report} {
            displaySeparatorLine
            set first_report 0
         }
         report "Module Specific Test for [sgr hi $modfile]:\n"
         execute-modulefile $modfile $modname $mod
         displaySeparatorLine
      }
   }
   lpopState mode
}

proc cmdModuleClear {{doit {}}} {
   reportDebug "($doit)"
   # fetch confirmation if no arg passed and force mode disabled
   if {$doit eq {} && ![getState force]} {
      # ask for it if stdin is attached to a terminal
      if {![catch {fconfigure stdin -mode}]} {
         report "Are you sure you want to clear all loaded modules!? \[n\] " 1
         flush [getState reportfd]
      }
      # fetch stdin content even if not attached to terminal in case some
      # content has been piped to this channel
      set doit [gets stdin]
   }

   # should be confirmed or forced to proceed
   if {[string equal -nocase -length 1 $doit y] || [getState force]} {
      set vartoclear [list LOADEDMODULES MODULES_LMALTNAME MODULES_LMCONFLICT\
         MODULES_LMNOTUASKED MODULES_LMPREREQ MODULES_LMSOURCESH\
         MODULES_LMTAG _LMFILES_]

      # add any reference counter variable to the list to unset
      set vartoclear [concat $vartoclear [array names ::env -glob *_modshare]\
         [array names ::env -glob MODULES_MODSHARE_*]]

      # unset all Modules runtime variables
      lappendState mode load
      foreach var $vartoclear {
         unset-env $var
      }
      lpopState mode
   } else {
      reportInfo "Modules runtime information were not cleared"
   }
}

proc cmdModuleConfig {dump_state args} {
   # parse arguments
   set nameunset 0
   switch -- [llength $args] {
      1 {
         lassign $args name
      }
      2 {
         lassign $args name value
         # check if configuration should be set or unset
         if {$name eq {--reset}} {
            set name $value
            set nameunset 1
            unset value
         }
      }
   }

   reportDebug "dump_state='$dump_state', reset=$nameunset,\
      name=[expr {[info exists name] ? "'$name'" : {<undef>}}], value=[expr\
      {[info exists value] ? "'$value'" : {<undef>}}]"

   foreach option [array names ::g_config_defs] {
      lassign $::g_config_defs($option) confvar($option) defval\
         conflockable($option) confvalid($option) vtrans initproc\
         confvalidkind($option)
      set confval($option) [getConf $option <undef>]
      set confvtrans($option) {}
      for {set i 0} {$i < [llength $vtrans]} {incr i} {
         lappend confvtrans($option) [lindex $vtrans $i] [lindex\
            $confvalid($option) $i]
      }
   }

   # catch any environment variable set for modulecmd run-time execution
   foreach runenvvar [array names ::env -glob MODULES_RUNENV_*] {
      set runenvconf [string tolower [string range $runenvvar 8 end]]
      set confval($runenvconf) [get-env $runenvvar]
      # enable modification of runenv conf
      set confvar($runenvconf) $runenvvar
      set confvalid($runenvconf) {}
      set conflockable($runenvconf) {}
      set confvtrans($runenvconf) {}
      set confvalidkind($runenvconf) {}
   }

   if {[info exists name] && ![info exists confval($name)]} {
      reportErrorAndExit "Configuration option '$name' does not exist"
   # set configuration
   } elseif {[info exists name] && ($nameunset || [info exists value])} {
      if {$confvar($name) eq {}} {
         reportErrorAndExit "Configuration option '$name' cannot be altered"
      } elseif {$conflockable($name) eq {1} && [isConfigLocked $name]} {
         reportErrorAndExit "Configuration option '$name' is locked"
      } elseif {$nameunset} {
         # unset configuration variable
         lappendState mode load
         unsetenv $confvar($name)
         lpopState mode
      } elseif {[llength $confvalid($name)] > 0} {
         switch -- $confvalidkind($name) {
            eltlist {
               # check each element in value list
               if {[isDiffBetweenList [split $value :] $confvalid($name)]} {
                  reportErrorAndExit "Invalid element in value list for\
                     config. option '$name'\nAllowed elements are:\
                     $confvalid($name) (separated by ':')"
               } else {
                  set validval 1
               }
            }
            intbe {
               if {[string is integer -strict $value] && $value >= [lindex\
                  $confvalid($name) 0] && $value <= [lindex $confvalid($name)\
                  1]} {
                  set validval 1
               } else {
                  reportErrorAndExit "Invalid value for configuration option\
                     '$name'\nValue should be an integer comprised between\
                     [lindex $confvalid($name) 0] and [lindex\
                     $confvalid($name) 1]"
               }
            }
            {} {
               if {([llength $confvalid($name)] == 1 && ![string is\
                  $confvalid($name) -strict $value]) || ([llength\
                  $confvalid($name)] > 1 && [notInList $confvalid($name)\
                  $value])} {
                  reportErrorAndExit "Valid values for configuration option\
                     '$name' are: $confvalid($name)"
               } else {
                  set validval 1
               }
            }
         }
      } else {
         set validval 1
      }

      if {[info exists validval]} {
         # effectively set configuration variable
         lappendState mode load
         setenv $confvar($name) $value
         lpopState mode
      }
      # clear cached value for config if any
      unsetConf $name
   # report configuration
   } else {
      reportVersion
      reportSeparateNextContent
      displayTableHeader hi {Config. name} 24 {Value (set by if default\
         overridden)} 54

      # report all configs or just queried one
      if {[info exists name]} {
         set varlist [list $name]
      } else {
         set varlist [lsort [array names confval]]
      }

      foreach var $varlist {
         set valrep [displayConfig $confval($var) $confvar($var) [info exists\
            ::asked_$var] $confvtrans($var) [expr {$conflockable($var) eq {1}\
            && [isConfigLocked $var]}]]
         append displist [format {%-25s %s} $var $valrep] \n
      }
      report $displist 1
      reportSeparateNextContent

      if {$dump_state} {
         displayTableHeader hi {State name} 24 {Value} 54
         # define each attribute/fetched state value pair
         foreach state [array names ::g_state_defs] {
            set stateval($state) [getState $state <undef> 1]
         }
         # also get dynamic states (with no prior definition)
         foreach state [array names ::g_states] {
            if {![info exists stateval($state)]} {
               set stateval($state) [getState $state]
            }
         }

         unset displist
         foreach state [lsort [array names stateval]] {
            append displist [format {%-25s %s} $state $stateval($state)] \n
         }
         report $displist 1
         reportSeparateNextContent

         # report environment variable set related to Modules
         displayTableHeader hi {Env. variable} 24 {Value} 54
         set envvar_list {}
         foreach var [list LOADEDMODULES _LMFILES_ MODULE* *_modshare\
            *_modquar *_module*] {
            set envvar_list [concat $envvar_list [array names ::env -glob\
               $var]]
         }
         unset displist
         foreach var [lsort -unique $envvar_list] {
            append displist [format {%-25s %s} $var $::env($var)] \n
         }
         report $displist 1
      }
   }
}

proc cmdModuleShToMod {args} {
   set scriptargs [lassign $args shell script]

   # evaluate script and get the environment changes it performs translated
   # into modulefile commands
   set modcontent [eval sh-to-mod $args]

   # output resulting modulefile
   if {[llength $modcontent] > 0} {
      report "#%Module"
      # format each command with tabs and colors if enabled
      foreach modcmd $modcontent {
         eval reportCmd -nativeargrep $modcmd
      }
   }
}

proc cmdMlHelp {} {
   reportVersion
   report {Usage: ml [options] [command] [args ...]
       ml [options] [[-]modulefile ...]

Examples:
  ml                 equivalent to: module list
  ml foo bar         equivalent to: module load foo bar
  ml -foo -bar baz   equivalent to: module unload foo bar; module load baz
  ml avail -t        equivalent to: module avail -t

See 'module --help' to get available commands and options.}
}

proc cmdModuleHelp {args} {
   lappendState mode help
   set first_report 1
   foreach arg $args {
      lassign [getPathToModule $arg] modfile modname

      if {$modfile ne {}} {
         # only one separator lines between 2 modules
         if {$first_report} {
            displaySeparatorLine
            set first_report 0
         }
         report "Module Specific Help for [sgr hi $modfile]:\n"
         execute-modulefile $modfile $modname $arg
         displaySeparatorLine
      }
   }
   lpopState mode
   if {[llength $args] == 0} {
      reportVersion
      report {Usage: module [options] [command] [args ...]

Loading / Unloading commands:
  add | load      modulefile [...]  Load modulefile(s)
  rm | unload     modulefile [...]  Remove modulefile(s)
  purge                             Unload all loaded modulefiles
  reload | refresh                  Unload then load all loaded modulefiles
  switch | swap   [mod1] mod2       Unload mod1 and load mod2

Listing / Searching commands:
  list            [-t|-l|-j]        List loaded modules
  avail   [-d|-L] [-t|-l|-j] [-a] [-S|-C] [--indepth|--no-indepth] [mod ...]
                                    List all or matching available modules
  aliases         [-a]              List all module aliases
  whatis [-a] [-j] [modulefile ...] Print whatis information of modulefile(s)
  apropos | keyword | search [-a] [-j] str
                                    Search all name and whatis containing str
  is-loaded       [modulefile ...]  Test if any of the modulefile(s) are loaded
  is-avail        modulefile [...]  Is any of the modulefile(s) available
  info-loaded     modulefile        Get full name of matching loaded module(s)

Collection of modules handling commands:
  save            [collection|file] Save current module list to collection
  restore         [collection|file] Restore module list from collection or file
  saverm          [collection]      Remove saved collection
  saveshow        [collection|file] Display information about collection
  savelist        [-t|-l|-j]        List all saved collections
  is-saved        [collection ...]  Test if any of the collection(s) exists

Shell's initialization files handling commands:
  initlist                          List all modules loaded from init file
  initadd         modulefile [...]  Add modulefile to shell init file
  initrm          modulefile [...]  Remove modulefile from shell init file
  initprepend     modulefile [...]  Add to beginning of list in init file
  initswitch      mod1 mod2         Switch mod1 with mod2 from init file
  initclear                         Clear all modulefiles from init file

Environment direct handling commands:
  prepend-path [-d c] var val [...] Prepend value to environment variable
  append-path [-d c] var val [...]  Append value to environment variable
  remove-path [-d c] var val [...]  Remove value from environment variable

Other commands:
  help            [modulefile ...]  Print this or modulefile(s) help info
  display | show  modulefile [...]  Display information about modulefile(s)
  test            [modulefile ...]  Test modulefile(s)
  use     [-a|-p] dir [...]         Add dir(s) to MODULEPATH variable
  unuse           dir [...]         Remove dir(s) from MODULEPATH variable
  is-used         [dir ...]         Is any of the dir(s) enabled in MODULEPATH
  path            modulefile        Print modulefile path
  paths           modulefile        Print path of matching available modules
  clear           [-f]              Reset Modules-specific runtime information
  source          scriptfile [...]  Execute scriptfile(s)
  config [--dump-state|name [val]]  Display or set Modules configuration
  sh-to-mod       shell shellscript [arg ...]
                                    Make modulefile from script env changes

Switches:
  -t | --terse    Display output in terse format
  -l | --long     Display output in long format
  -j | --json     Display output in JSON format
  -o LIST | --output=LIST
                  Define elements to output on 'avail' or 'list' sub-commands
                  in addition to module names (LIST is made of items like
                  'sym', 'tag' or 'key' separated by ':')
  -a | --all      Include hidden modules in search
  -d | --default  Only show default versions available
  -L | --latest   Only show latest versions available
  -S | --starts-with
                  Search modules whose name begins with query string
  -C | --contains Search modules whose name contains query string
  -i | --icase    Case insensitive match
  -a | --append   Append directory to MODULEPATH (on 'use' sub-command)
  -p | --prepend  Prepend directory to MODULEPATH
  --auto          Enable automated module handling mode
  --no-auto       Disable automated module handling mode
  -f | --force    By-pass dependency consistency or confirmation dialog

Options:
  -h | --help     This usage info
  -V | --version  Module version
  -D | --debug    Enable debug messages
  -T | --trace    Enable trace messages
  -v | --verbose  Enable verbose messages
  -s | --silent   Turn off error, warning and informational messages
  --paginate      Pipe mesg output into a pager if stream attached to terminal
  --no-pager      Do not pipe message output into a pager
  --color[=WHEN]  Colorize the output; WHEN can be 'always' (default if
                  omitted), 'auto' or 'never'
  -w COLS | --width=COLS
                  Set output width to COLS columns.}
   }
}

########################################################################
# main program

# needed on a gentoo system. Shouldn't hurt since it is
# supposed to be the default behavior
fconfigure stderr -translation auto

if {[catch {
   # parse all command-line arguments before doing any action, no output is
   # made during argument parse to wait for potential paging to be setup
   set show_help 0
   set show_version 0
   setState cmdline "$argv0 $argv"

   # Load extension library if enabled
   if {[file readable [getConf tcl_ext_lib]]} {
      reportDebug "Load Tcl extension library ([getConf tcl_ext_lib])"
      load [file normalize [getConf tcl_ext_lib]] Envmodules
      setState tcl_ext_lib_loaded 1
   }
   # use fallback procs if extension library is not loaded
   if {[info commands readFile] eq {}} {
      rename ::__readFile ::readFile
      rename ::__getFilesInDirectory ::getFilesInDirectory
      rename ::__initStateUsergroups ::initStateUsergroups
      rename ::__initStateUsername ::initStateUsername
      rename ::__initStateClockSeconds ::initStateClockSeconds
      rename ::__parseDateTimeArg ::parseDateTimeArg
   }

   # source site configuration script if any
   sourceSiteConfig

   # Parse shell
   setState shell [lindex $argv 0]
   switch -- [getState shell] {
      sh - bash - ksh - zsh {
         setState shelltype sh
      }
      csh - tcsh {
         setState shelltype csh
      }
      fish - cmd - tcl - perl - python - ruby - lisp - cmake - r {
         setState shelltype [getState shell]
      }
      default {
         reportErrorAndExit "Unknown shell type \'([getState shell])\'"
      }
   }

   # extract options and command switches from other args
   set otherargv {}
   set extraargv {}
   set ddelimarg 0
   # split first arg if multi-word string detected for compat with previous
   # doc on module usage with scripting language: module('load mod1 mod2')
   set argtoparse [if {[llength [lindex $argv 1]] > 1} {concat [split [lindex\
      $argv 1]] [lrange $argv 2 end]} {lrange $argv 1 end}]
   foreach arg $argtoparse {
      if {[info exists ignore_next_arg]} {
         unset ignore_next_arg
      } elseif {[info exists nextargisextraargv]} {
         lappend extraargv $arg
         unset nextargisextraargv
      } elseif {[info exists nextargisval]} {
         set $nextargisval $arg
         unset nextargisval
      } else {
         switch -glob -- $arg {
            -T - --trace {
               set asked_verbosity trace
            }
            -D - -DD - --debug {
               set asked_verbosity [expr {$arg eq {-DD} || ([info exists\
                  asked_verbosity] && [isInList {debug debug2}\
                  $asked_verbosity]) ? {debug2} : {debug}}]
            }
            -s - --silent {
               set asked_verbosity silent
            }
            -v - -vv - --verbose {
               set asked_verbosity [expr {$arg eq {-vv} || ([info exists\
                  asked_verbosity] && [isInList {verbose verbose2}\
                  $asked_verbosity]) ? {verbose2} : {verbose}}]
            }
            --help - -h {
               set show_help 1
            }
            -V - --version {
               set show_version 1
            }
            --paginate {
               set asked_paginate 1
            }
            --no-pager {
               set asked_paginate 0
            }
            --auto {
               set asked_auto_handling 1
            }
            --no-auto {
               set asked_auto_handling 0
            }
            -f - --force {
               set asked_force 1
            }
            --color* {
               set asked_color [string range $arg 8 end]
               if {$asked_color eq {}} {
                  set asked_color always
               } elseif {[notInList [lindex $::g_config_defs(color) 3] $asked_color]} {
                  unset asked_color
               }
            }
            -o {
               # add with next arg to the command-specific switches
               lappend extraargv $arg
               set nextargisextraargv 1
            }
            --width* {
               set asked_term_width [string range $arg 8 end]
               set  term_width_arg --width
               if {$asked_term_width eq {}} {
                  set asked_term_width 0
               }
            }
            -w {
               set nextargisval asked_term_width
               set  term_width_arg -w
            }
            -t - --terse - -l - --long - --default - -L - --latest - -S -\
            --starts-with - -C - --contains - -j - --json - --output=* {
               # command-specific switches that can for compatibility be
               # passed before the command name, so add them to a specific
               # arg list to ensure command name as first position argument
               lappend extraargv $arg
            }
            -d {
               # in case of *-path command, -d means --delim
               if {$ddelimarg} {
                  lappend otherargv $arg
               } else {
                  lappend extraargv $arg
               }
            }
            -a - --append - -append - --all - -p - --prepend - -prepend -\
            --delim - -delim - --delim=* - -delim=* - --duplicates - --index\
            - --notuasked - --indepth - --no-indepth - --dump-state -\
            --reset {
               # command-specific switches interpreted later on
               lappend otherargv $arg
            }
            append-path - prepend-path - remove-path {
               # detect *-path commands to say -d means --delim, not --default
               set ddelimarg 1
               lappend otherargv $arg
            }
            -i - --icase {
               set asked_icase always
            }
            --human - -c - --create - --userlvl=* {
               # ignore C-version specific option, no error only warning
               reportWarning "Unsupported option '$arg'"
            }
            -u - --userlvl {
               reportWarning "Unsupported option '$arg'"
               # also ignore argument value
               set ignore_next_arg 1
            }
            --output {
               reportErrorAndExit "Missing value for '$arg' option\nTry\
                  'module --help' for more information."
            }
            {-} - {--} - {--*} {
               reportErrorAndExit "Invalid option '$arg'\nTry 'module --help'\
                  for more information."
            }
            -* {
               # spare argument if ml command is called
               if {[lindex $otherargv 0] ne {ml}} {
                  reportErrorAndExit "Invalid option '$arg'\nTry 'module\
                     --help' for more information."
               } else {
                  lappend otherargv $arg
               }
            }
            default {
               lappend otherargv $arg
            }
         }
         set prevarg $arg
      }
   }

   if {[info exists nextargisextraargv]} {
      reportErrorAndExit "Missing value for '$prevarg' option\nTry 'module\
         --help' for more information."
   }
   if {[info exists asked_term_width]} {
      set rangewidth [lindex $::g_config_defs(term_width) 3]
      if {[string is integer -strict $::asked_term_width] && \
         $::asked_term_width >= [lindex $rangewidth 0] &&\
         $::asked_term_width <= [lindex $rangewidth 1]} {
         set validval 1
      } else {
         reportErrorAndExit "Invalid value for option\
            '$term_width_arg'\nValue should be an integer comprised between\
            [lindex $rangewidth 0] and [lindex $rangewidth 1]"
      }
   }

   setState subcmd [lindex $otherargv 0]
   set otherargv [concat [lreplace $otherargv 0 0] $extraargv]
   setState subcmd_args $otherargv
   # call ml frontend if it is asked command
   if {[getState subcmd] eq {ml}} {
      set execcmdlist [concat [list ml] $otherargv]
   } else {
      set execcmdlist [concat [list module [getState subcmd]] $otherargv]
   }

   # now options are known initialize error report (start pager if enabled)
   initErrorReport

   # put back quarantine variables in env, if quarantine mechanism supported
   if {[getConf run_quarantine] ne {} && [getState shelltype] ne {csh}} {
      foreach var [split [getConf run_quarantine]] {
         # check variable name is valid
         if {[regexp {^[A-Za-z_][A-Za-z0-9_]*$} $var]} {
            set quarvar ${var}_modquar
            # put back value
            if {[info exists env($quarvar)]} {
               reportDebug "Release '$var' environment variable from\
                  quarantine ($env($quarvar))"
               set env($var) $env($quarvar)
               unset env($quarvar)
            # or unset env var if no value found in quarantine
            } elseif {[info exists env($var)]} {
               reportDebug "Unset '$var' environment variable after\
                  quarantine"
               unset env($var)
            }
         } elseif {[string length $var] > 0} {
            reportWarning "Bad variable name set in MODULES_RUN_QUARANTINE\
               ($var)"
         }
      }
   }

   if {$show_help} {
      if {[getState subcmd] eq {ml}} {
         cmdMlHelp
      } else {
         cmdModuleHelp
      }
      cleanupAndExit 0
   }
   if {$show_version} {
      reportVersion
      cleanupAndExit 0
   }

   # no modulefile is currently being interpreted
   lappendState modulefile {}

   # eval needed to pass otherargv as list to module proc
   eval $execcmdlist
} errMsg ]} {
   # re-enable error report in case it was previously inhibited
   setState inhibit_errreport 0
   # remove any message record id to render next error
   clearAllMsgRecordId
   # render error if not done yet
   if {$errorCode ne {MODULES_ERR_RENDERED}} {
      raiseErrorCount
      renderFalse
   }
   # report stack trace in addition to the error message if error is unknown
   if {[notInList [list MODULES_ERR_RENDERED MODULES_ERR_KNOWN] $errorCode]} {
      set errMsg "$errorInfo\n[sgr hi {Please report this issue at\
         https://github.com/cea-hpc/modules/issues}]"
   }
   reportError $errMsg
   # init error report here in case the error raised before the regular init
   initErrorReport
   cleanupAndExit 1
}

cleanupAndExit 0

# ;;; Local Variables: ***
# ;;; mode:tcl ***
# ;;; End: ***
# vim:set tabstop=3 shiftwidth=3 expandtab autoindent:
