def __init__(self, argv=['modm.py']): """Save arguments and initialize member variables. Arguments: argv -- should be called with `sys.argv`, otherwise a list with at least one item has to be provided """ # Save arguments self.argv = argv # Disable use of colors if environment variable is set to 'off' value if self.color_setting_var in os.environ and ( os.environ[self.color_setting_var].lower() in ['no', 'off', 'false']): self.use_colors = False else: self.use_colors = True # Create Bash evaluation instance self.be = BashEval(self.use_colors) # Set admin email address to environment variable if found, otherwise # to built-in default if self.admin_email_var in os.environ: self.admin_email = os.environ[self.admin_email_var] else: self.admin_email = self.admin_default_email # Init other members self.cmd = None self.args = [] self.env = None self.modules = [] self.parser = None # Set init variables to False self.is_init_argv = False self.is_init_env = False self.is_init_modules = False self.is_init_parser = False
def __init__(self, env=Env(), basheval=BashEval()): """Save arguments to class and initialize list of valid commands. Arguments: env -- object to handle environment variables basheval -- object to convert commands to Bash evaluation strings """ # Save arguments self.env = env self.be = basheval # Init commands self.commands = dict() self.init_commands() # Init other members self.do_unload = False
class Modm: """ Class to manage modules. Supports loading, unloading and listing of modules. Provides built-in help. """ help_file_suffix = '.txt' help_file_dir = 'doc' module_help_file = '.help' module_default_file = '.default' module_category_file = '.category' modules_path_var = 'MODM_MODULES_PATH' modules_loaded_var = 'MODM_LOADED_MODULES' admin_email_var = 'MODM_ADMIN_EMAIL' color_setting_var = 'MODM_USE_COLORS' admin_default_email = 'root@localhost' available_commands = ['avail', 'status', 'config', 'help', 'list', 'load', 'unload'] def __init__(self, argv=['modm.py']): """Save arguments and initialize member variables. Arguments: argv -- should be called with `sys.argv`, otherwise a list with at least one item has to be provided """ # Save arguments self.argv = argv # Disable use of colors if environment variable is set to 'off' value if self.color_setting_var in os.environ and ( os.environ[self.color_setting_var].lower() in ['no', 'off', 'false']): self.use_colors = False else: self.use_colors = True # Create Bash evaluation instance self.be = BashEval(self.use_colors) # Set admin email address to environment variable if found, otherwise # to built-in default if self.admin_email_var in os.environ: self.admin_email = os.environ[self.admin_email_var] else: self.admin_email = self.admin_default_email # Init other members self.cmd = None self.args = [] self.env = None self.modules = [] self.parser = None # Set init variables to False self.is_init_argv = False self.is_init_env = False self.is_init_modules = False self.is_init_parser = False def run(self): """Call `runsafe()` in try-except block to catch irregular errors and print command string from BashEval.""" try: self.rununsafe() except Exception as e: self.be.clear() self.be.error("An unknown error occurred.", internal=True) self.be.error("Please send an email with the command you used and " + "the error message printed above to '{e}'." .format(e=self.admin_email), internal=True) raise finally: sys.stdout.write(self.be.cmdstring()) def rununsafe(self): """Parse command line arguments and execute specified command.""" # Init command line arguments self.init_argv() # Parse command for alternatives command, alternatives = self.parse_command(self.cmd) # Check command and execute appropriate method if command in ['help', '--help']: self.cmd_help() elif command in ['--version']: self.cmd_version() elif command in ['avail', 'status']: self.cmd_avail() elif command in ['config']: self.cmd_config() elif command in ['list']: self.cmd_list() elif command in ['load']: self.cmd_load() elif command in ['unload']: self.cmd_unload() elif command is None and alternatives is None: # If no command was given, show usage information self.be.error("No command given.") self.print_help('usage') elif command is None and len(alternatives) > 0: # If partial command was specified but is ambiguous, show list of # valid possibilities self.be.error("Command '{c}' is ambiguous. See 'modm help'.".format( c=self.cmd)) self.be.echo() self.be.echo("Did you mean one of these?") for a in alternatives: self.be.echo(" {u}[{t}]".format( u=a[0:len(self.cmd)+1], t=a[len(self.cmd)+1:])) else: arg_type = "Option" if self.cmd[0] == '-' else "Command" self.be.error("{t} '{c}' not recognized.".format( t=arg_type, c=self.cmd)) self.print_help('usage') def init_argv(self): """Initialize command line arguments if not yet done.""" if not self.is_init_argv: # Command is set to None if none was found self.cmd = self.argv[1] if len(self.argv) > 1 else None # Arguments are set to empty list if none were found self.args = self.argv[2:] if len(self.argv) > 2 else [] self.is_init_argv = True def init_env(self): """Initialize environment handler if not yet done.""" if not self.is_init_env: self.env = Env(modpath_var=self.modules_path_var, modloaded_var=self.modules_loaded_var) self.is_init_env = True def init_modules(self): """Initialize all modules if not yet done. Check all module paths for all available modules and their versions, defaults, categories etc. For a module with a given name, the first module found in the modules path overrides the settings for all later modules. However, it is possible that later versions are appended. """ # Only initialize if not yet done if not self.is_init_modules: self.init_env() # Iterate over directories containing the modules for modules_directory in self.env.modpath: # Iterate over module folders but skip hidden folders for name in [d for d in os.listdir(modules_directory) if os.path.isdir(os.path.join(modules_directory, d)) and not d.startswith('.')]: # Get module index (if existing) index = self.find_module(name) # If module does not yet exist, create a new one if index == None: self.modules.append(Module()) index = len(self.modules) - 1 # Set module name if not yet set if self.modules[index].name is None: self.modules[index].name = name # Set module versions modpath = os.path.join(modules_directory, name) for modfile in [os.path.join(modpath, f) for f in os.listdir(modpath) if os.path.isfile(os.path.join(modpath, f))]: _, modversion = self.decode_file(modfile) # Check if filename matches any of the special names if modversion == self.module_default_file: # Set default version if self.modules[index].default is None: defaultversion = '' with open(modfile, 'r') as f: defaultversion = f.readline().strip() defaultversionfile = os.path.join(modpath, defaultversion) if os.path.isfile(defaultversionfile): self.modules[index].default = ( defaultversionfile) elif modversion == self.module_help_file: # Set help file if self.modules[index].help_file is None: self.modules[index].help_file = ( os.path.join(modpath, self.module_help_file)) elif modversion == self.module_category_file: # Set category if self.modules[index].category is None: with open(modfile, 'r') as f: category = f.readline().strip() self.modules[index].category = ( category if category else None) else: # Add version file if modversion not in [ os.path.basename(v) for v in self.modules[index].versions]: self.modules[index].versions.append(modfile) # Set loaded if modfile in self.env.modloaded: self.modules[index].loaded = modfile # Delete modules without versions (i.e. without module files) self.modules = [m for m in self.modules if len(m.versions) > 0] # Sort modules self.modules = natsorted(self.modules, key=lambda m: m.name) # Sort versions for i in range(len(self.modules)): self.modules[i].versions = natsorted(self.modules[i].versions, key=lambda v: os.path.basename(v)) # Set default modules for modules that do not have one for i in range(len(self.modules)): if self.modules[i].default is None: self.modules[i].default = self.modules[i].versions[-1] # Set initialized state self.is_init_modules = True def init_parser(self): """Initialize module filer parser if not yet done.""" if not self.is_init_parser: self.parser = ModfileParser(self.env, self.be) def find_module(self, name, strict=False): """Return the index of a module with name `name`. If no module was found with this name, return None. By default only the module name is checked, however, if `name` contains a version number and `strict` is set to true, also the version is checked for existence. """ # Get name, version modname, modversion = self.decode_name(name) # Check all modules for the name (and possibly version) for i, module in enumerate(self.modules): if module.name == modname: if strict and modversion is not None and (modversion not in map(os.path.basename, module.versions)): return None else: return i return None def get_module_file(self, name): """Get a module file from the name. If no version is specified, return default module. """ # Get module index i = self.find_module(name) if i is None: return None else: # If module was found, decode name modname, modversion = self.decode_name(name) # Return default if no module version was found if not modversion: return self.modules[i].default # Otherwise return module file else: modules = [modfile for modfile in self.modules[i].versions if os.path.basename(modfile) == modversion] return modules[0] if len(modules) > 0 else None def decode_file(self, modfile): """Split a module file path into its module name and version components. """ head, modversion = os.path.split(modfile) _, modname = os.path.split(head) return modname, modversion def decode_name(self, name): """Split a module name into its module name and version components.""" head, tail = os.path.split(name) modname = tail if head == '' else head modversion = tail if (head != '' and tail != '') else None return modname, modversion def parse_command(self, cmd): """Parse (partial) command for known commands. Return tuple of full command name and a list of alternatives if ambiguous. """ # Return None if no command was specified if cmd is None: return None, None # Get list of matching commands commands = [c for c in self.available_commands if c.startswith(cmd)] # If only one command matches, this is it: list of alternatives is empty if len(commands) == 1: command = commands[0] alternatives = [] # Otherwise return no command but only the alternatives else: command = None alternatives = commands # Return tuple return command, alternatives def print_file(self, path, kind='normal'): """Open file at `path` and print its contents using BashEval.""" with open(path, 'r') as f: self.be.echo(f.read(), newline=False, kind=kind, width=2048) def print_help(self, topic): """Print help file associated with `topic`. Issues error message if topic was not found.""" # Get help file path as combination of modm.py file path and topic # plus help file extension help_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), Modm.help_file_dir, topic + Modm.help_file_suffix) # If file exists, print its contents if os.path.isfile(help_file): self.print_file(help_file) # Otherwise print error message. Since this should never happen, it is # an internal error else: self.be.error("Help file '{f}' not found.".format(f=help_file), internal=True) self.be.error("Please send an email with the command you used and " + "the error message printed above to '{e}'.".format( e=self.admin_email), internal=True) def print_modules(self, modules): """All modules in list `modules`, sorted by category.""" # Determine all categories and the maximum string length of the modules maxlength = 0 categories = set() for module in modules: maxlength = max(maxlength, len(module.name)) categories.add(module.category.strip().upper() if module.category else None) # Natsort categories and put 'None' at the end if present if None in categories: categories = natsorted([c for c in categories if c is not None]) categories.append(None) else: categories = natsorted(list(categories)) # Print modules by category first = True for category in categories: # For the first category, no newline is needed if first: first = False else: self.be.echo('') # Print category name self.be.echo(category if category else '<UNCATEGORIZED>') # Print each module in category for module in [m for m in modules if ( (True if category is None else False) if ( m.category is None) else ( m.category.strip().upper() == category))]: # Get all versions of module versions = [] for version in module.versions: v = os.path.basename(version) if version == module.default: v = v + '(default)' if version in self.env.modloaded: v = self.be.highlight(v + '*', kind='info') versions.append(v) # Print module together with all its version, in which # the default version and the currently loaded version are # marked specially self.be.echo(' {m:{l}} {v}'.format(m=module.name+':', l=maxlength+1, v=', '.join(versions))) def is_loaded(self, name): """Return True if module `name` is loaded, else False.""" modname, _ = self.decode_name(name) if modname in [modname for modname, _ in map(self.decode_file, self.env.modloaded)]: return True else: return False def load_module(self, name): """Load module `name` if it is not yet loaded.""" # Get full path to module file modfile = self.get_module_file(name) # If module file was not found, return if modfile is None: return # Otherwise, if module is already loaded, return elif modfile in self.env.modloaded: return # Otherwise, parse module file for loading and add module to list of # loaded modules else: if self.parser.load(modfile): self.env.add_loaded_module(modfile) def unload_module(self, name): """Unload module `name` if it is currently loaded.""" # Get module name and version modname, modversion = self.decode_name(name) # Get module file, name, and version from currently loaded modules for modfile, (modnamefile, modversionfile) in zip( self.env.modloaded, map(self.decode_file, self.env.modloaded)): # If module name matches, unload module and remove module from # list of loaded modules if modname == modnamefile and ( modversion is None or modversion == modversionfile): if self.parser.unload(modfile): self.env.modloaded.remove(modfile) def process_modified(self): """Check all modified environment variables and unset/export them as needed. """ # Iterate over all variables that were modified for var in [var for var in self.env.variables.values() if var.is_modified()]: # Unset variable if it is marked for unset if var.is_unset(): self.be.unset(var.get_name()) # Otherwise update its value else: self.be.export(*var.get_export()) def cmd_avail(self): """Command 'avail': list all available modules by category.""" self.init_env() self.init_modules() self.print_modules(self.modules) def cmd_config(self): """Command 'config': show configuration for module or Modm itself.""" # Show configuration for Modm if no argument was specified if len(self.args) == 0: self.init_env() self.be.echo("Modm executable script path variable: MODM_PY") self.be.echo("Current script path: ", newline=False) self.be.echo(os.environ['MODM_PY'], kind='info') self.be.echo() self.be.echo("Module path variable: {v}".format( v=self.modules_path_var)) self.be.echo("Currently active paths:") for p in self.env.modpath: self.be.echo(p, kind='info') self.be.echo() self.be.echo("Loaded modules variable: {v}".format( v=self.modules_loaded_var)) self.be.echo("Currently loaded module files:") for m in self.env.modloaded: self.be.echo(m, kind='info') self.be.echo() self.be.echo("Admin email address variable: {v}".format( v=self.admin_email_var)) self.be.echo("Current email address: ", newline=False) self.be.echo(self.admin_email, kind='info') self.be.echo() self.be.echo("Color settings variable: {v}".format( v=self.color_setting_var)) self.be.echo("Current setting for color usage: ", newline=False) self.be.echo("ON" if self.use_colors else "OFF", kind='info') self.be.echo("(if variable is set to 'OFF', colors are disabled, " + "otherwise enabled)") # Otherwise show configuration for module and ignore further arguments else: self.init_modules() name = self.args[0] modfile = self.get_module_file(name) # If module file was not found, print error if modfile is None: self.be.error("Module '{m}' not found.".format(m=name)) # Otherwise parse module file for configuration information else: module = self.modules[self.find_module(name)] self.be.echo("Module name: ", newline=False) self.be.echo(module.name, kind='info') self.be.echo("Available versions: ", newline=False) self.be.echo(', '.join( [os.path.basename(v) for v in module.versions]), kind='info') self.be.echo("Default version: ", newline=False) self.be.echo(os.path.basename(module.default), kind='info') self.be.echo("Category: ", newline=False) self.be.echo(module.category.upper() if module.category else "<UNCATEGORIZED>", kind='info') self.be.echo("Module help available: ", newline=False) self.be.echo("yes" if module.help_file else "no", kind='info') self.be.echo("Module file for '{n}': ".format(n=name), newline=False) self.be.echo(modfile, kind='info') self.be.echo("Module file content:") self.print_file(modfile, kind='info') def cmd_help(self): """Command 'help': show help on commands or modules.""" # Get help topic from arguments and parse for commands topic = self.args[0] if len(self.args) > 0 else None command, alternatives = self.parse_command(topic) # If no topic was specified, show usage information if self.cmd in ['--help'] or topic is None: self.print_help('usage') # If a command was specified, print its help file elif command in ['help']: self.print_help(os.path.join('commands', 'help')) elif command in ['avail', 'status']: self.print_help(os.path.join('commands', 'avail')) elif command in ['config']: self.print_help(os.path.join('commands', 'config')) elif command in ['list']: self.print_help(os.path.join('commands', 'list')) elif command in ['load']: self.print_help(os.path.join('commands', 'load')) elif command in ['unload']: self.print_help(os.path.join('commands', 'unload')) # If no command was determined but there are alternatives, show a list # of ambiguous commands elif command is None and len(alternatives) > 0: self.be.error("Command '{c}' is ambiguous. See 'modm help'.".format( c=topic)) self.be.echo() self.be.echo("Did you mean one of these?") for a in alternatives: self.be.echo(" {u}[{t}]".format( u=a[0:len(self.cmd)+1], t=a[len(self.cmd)+1:])) # Otherwise, check modules for help else: self.init_modules() index = self.find_module(topic) # If no module was found with the name `topic`, show error if index is None: self.be.error("Unknown help topic '{t}'.".format(t=topic)) self.be.error("See 'modm help help' for a list of help topics.") # Otherwise show error if no help file was set for the module elif self.modules[index].help_file is None: self.be.error("No help available for module '{m}'.".format( m=self.modules[index].name)) # Otherwise (help file was found for module), print help file else: self.print_file(self.modules[index].help_file) def cmd_list(self): """Command 'list': show all currently loaded modules.""" self.init_modules() # Print all loaded modules in an easily parsable list for modfile in natsorted(self.env.modloaded): head, modversion = os.path.split(modfile) _, modname = os.path.split(head) self.be.echo(os.path.join(modname, modversion)) def cmd_load(self): """Command 'load': load all specified module files.""" self.init_modules() self.init_parser() # Try to load each argument as a module for name in self.args: index = self.find_module(name, strict=True) # If module was found, load it if index is not None: # If a version of the module is currently loaded, unload it if self.is_loaded(name): self.unload_module(self.decode_name(name)[0]) self.load_module(name) # Otherwise print error else: self.be.error("Module '{m}' not found.".format(m=name)) # After loading all modules, act on all environment variables that # have changed self.process_modified() self.be.export(self.env.modloaded_var, self.env.get_modloaded_str()) def cmd_unload(self): """Command 'unload': unload all specified module files.""" self.init_modules() self.init_parser() # Unload each argument for name in self.args: self.unload_module(name) # After unloadig all modules, act on all environment variables that # have changed self.process_modified() self.be.export(self.env.modloaded_var, self.env.get_modloaded_str()) def cmd_version(self): """Print version information.""" self.be.echo("modm version {v}".format(v=__version__))
class Modm: """ Class to manage modules. Supports loading, unloading and listing of modules. Provides built-in help. """ help_file_suffix = '.txt' help_file_dir = 'doc' module_help_file = '.help' module_default_file = '.default' module_category_file = '.category' modules_path_var = 'MODM_MODULES_PATH' modules_loaded_var = 'MODM_LOADED_MODULES' admin_email_var = 'MODM_ADMIN_EMAIL' color_setting_var = 'MODM_USE_COLORS' admin_default_email = 'root@localhost' available_commands = [ 'avail', 'status', 'config', 'help', 'list', 'load', 'unload' ] def __init__(self, argv=['modm.py']): """Save arguments and initialize member variables. Arguments: argv -- should be called with `sys.argv`, otherwise a list with at least one item has to be provided """ # Save arguments self.argv = argv # Disable use of colors if environment variable is set to 'off' value if self.color_setting_var in os.environ and ( os.environ[self.color_setting_var].lower() in ['no', 'off', 'false']): self.use_colors = False else: self.use_colors = True # Create Bash evaluation instance self.be = BashEval(self.use_colors) # Set admin email address to environment variable if found, otherwise # to built-in default if self.admin_email_var in os.environ: self.admin_email = os.environ[self.admin_email_var] else: self.admin_email = self.admin_default_email # Init other members self.cmd = None self.args = [] self.env = None self.modules = [] self.parser = None # Set init variables to False self.is_init_argv = False self.is_init_env = False self.is_init_modules = False self.is_init_parser = False def run(self): """Call `runsafe()` in try-except block to catch irregular errors and print command string from BashEval.""" try: self.rununsafe() except Exception as e: self.be.clear() self.be.error("An unknown error occurred.", internal=True) self.be.error( "Please send an email with the command you used and " + "the error message printed above to '{e}'.".format( e=self.admin_email), internal=True) raise finally: sys.stdout.write(self.be.cmdstring()) def rununsafe(self): """Parse command line arguments and execute specified command.""" # Init command line arguments self.init_argv() # Parse command for alternatives command, alternatives = self.parse_command(self.cmd) # Check command and execute appropriate method if command in ['help', '--help']: self.cmd_help() elif command in ['--version']: self.cmd_version() elif command in ['avail', 'status']: self.cmd_avail() elif command in ['config']: self.cmd_config() elif command in ['list']: self.cmd_list() elif command in ['load']: self.cmd_load() elif command in ['unload']: self.cmd_unload() elif command is None and alternatives is None: # If no command was given, show usage information self.be.error("No command given.") self.print_help('usage') elif command is None and len(alternatives) > 0: # If partial command was specified but is ambiguous, show list of # valid possibilities self.be.error( "Command '{c}' is ambiguous. See 'modm help'.".format( c=self.cmd)) self.be.echo() self.be.echo("Did you mean one of these?") for a in alternatives: self.be.echo(" {u}[{t}]".format(u=a[0:len(self.cmd) + 1], t=a[len(self.cmd) + 1:])) else: arg_type = "Option" if self.cmd[0] == '-' else "Command" self.be.error("{t} '{c}' not recognized.".format(t=arg_type, c=self.cmd)) self.print_help('usage') def init_argv(self): """Initialize command line arguments if not yet done.""" if not self.is_init_argv: # Command is set to None if none was found self.cmd = self.argv[1] if len(self.argv) > 1 else None # Arguments are set to empty list if none were found self.args = self.argv[2:] if len(self.argv) > 2 else [] self.is_init_argv = True def init_env(self): """Initialize environment handler if not yet done.""" if not self.is_init_env: self.env = Env(modpath_var=self.modules_path_var, modloaded_var=self.modules_loaded_var) self.is_init_env = True def init_modules(self): """Initialize all modules if not yet done. Check all module paths for all available modules and their versions, defaults, categories etc. For a module with a given name, the first module found in the modules path overrides the settings for all later modules. However, it is possible that later versions are appended. """ # Only initialize if not yet done if not self.is_init_modules: self.init_env() # Iterate over directories containing the modules for modules_directory in self.env.modpath: # Iterate over module folders but skip hidden folders for name in [ d for d in os.listdir(modules_directory) if os.path.isdir(os.path.join(modules_directory, d)) and not d.startswith('.') ]: # Get module index (if existing) index = self.find_module(name) # If module does not yet exist, create a new one if index == None: self.modules.append(Module()) index = len(self.modules) - 1 # Set module name if not yet set if self.modules[index].name is None: self.modules[index].name = name # Set module versions modpath = os.path.join(modules_directory, name) for modfile in [ os.path.join(modpath, f) for f in os.listdir(modpath) if os.path.isfile(os.path.join(modpath, f)) ]: _, modversion = self.decode_file(modfile) # Check if filename matches any of the special names if modversion == self.module_default_file: # Set default version if self.modules[index].default is None: defaultversion = '' with open(modfile, 'r') as f: defaultversion = f.readline().strip() defaultversionfile = os.path.join( modpath, defaultversion) if os.path.isfile(defaultversionfile): self.modules[index].default = ( defaultversionfile) elif modversion == self.module_help_file: # Set help file if self.modules[index].help_file is None: self.modules[index].help_file = (os.path.join( modpath, self.module_help_file)) elif modversion == self.module_category_file: # Set category if self.modules[index].category is None: with open(modfile, 'r') as f: category = f.readline().strip() self.modules[index].category = ( category if category else None) else: # Add version file if modversion not in [ os.path.basename(v) for v in self.modules[index].versions ]: self.modules[index].versions.append(modfile) # Set loaded if modfile in self.env.modloaded: self.modules[index].loaded = modfile # Delete modules without versions (i.e. without module files) self.modules = [m for m in self.modules if len(m.versions) > 0] # Sort modules self.modules = natsorted(self.modules, key=lambda m: m.name) # Sort versions for i in range(len(self.modules)): self.modules[i].versions = natsorted( self.modules[i].versions, key=lambda v: os.path.basename(v)) # Set default modules for modules that do not have one for i in range(len(self.modules)): if self.modules[i].default is None: self.modules[i].default = self.modules[i].versions[-1] # Set initialized state self.is_init_modules = True def init_parser(self): """Initialize module filer parser if not yet done.""" if not self.is_init_parser: self.parser = ModfileParser(self.env, self.be) def find_module(self, name, strict=False): """Return the index of a module with name `name`. If no module was found with this name, return None. By default only the module name is checked, however, if `name` contains a version number and `strict` is set to true, also the version is checked for existence. """ # Get name, version modname, modversion = self.decode_name(name) # Check all modules for the name (and possibly version) for i, module in enumerate(self.modules): if module.name == modname: if strict and modversion is not None and ( modversion not in map(os.path.basename, module.versions)): return None else: return i return None def get_module_file(self, name): """Get a module file from the name. If no version is specified, return default module. """ # Get module index i = self.find_module(name) if i is None: return None else: # If module was found, decode name modname, modversion = self.decode_name(name) # Return default if no module version was found if not modversion: return self.modules[i].default # Otherwise return module file else: modules = [ modfile for modfile in self.modules[i].versions if os.path.basename(modfile) == modversion ] return modules[0] if len(modules) > 0 else None def decode_file(self, modfile): """Split a module file path into its module name and version components. """ head, modversion = os.path.split(modfile) _, modname = os.path.split(head) return modname, modversion def decode_name(self, name): """Split a module name into its module name and version components.""" head, tail = os.path.split(name) modname = tail if head == '' else head modversion = tail if (head != '' and tail != '') else None return modname, modversion def parse_command(self, cmd): """Parse (partial) command for known commands. Return tuple of full command name and a list of alternatives if ambiguous. """ # Return None if no command was specified if cmd is None: return None, None # Get list of matching commands commands = [c for c in self.available_commands if c.startswith(cmd)] # If only one command matches, this is it: list of alternatives is empty if len(commands) == 1: command = commands[0] alternatives = [] # Otherwise return no command but only the alternatives else: command = None alternatives = commands # Return tuple return command, alternatives def print_file(self, path, kind='normal'): """Open file at `path` and print its contents using BashEval.""" with open(path, 'r') as f: self.be.echo(f.read(), newline=False, kind=kind, width=2048) def print_help(self, topic): """Print help file associated with `topic`. Issues error message if topic was not found.""" # Get help file path as combination of modm.py file path and topic # plus help file extension help_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), Modm.help_file_dir, topic + Modm.help_file_suffix) # If file exists, print its contents if os.path.isfile(help_file): self.print_file(help_file) # Otherwise print error message. Since this should never happen, it is # an internal error else: self.be.error("Help file '{f}' not found.".format(f=help_file), internal=True) self.be.error( "Please send an email with the command you used and " + "the error message printed above to '{e}'.".format( e=self.admin_email), internal=True) def print_modules(self, modules): """All modules in list `modules`, sorted by category.""" # Determine all categories and the maximum string length of the modules maxlength = 0 categories = set() for module in modules: maxlength = max(maxlength, len(module.name)) categories.add( module.category.strip().upper() if module.category else None) # Natsort categories and put 'None' at the end if present if None in categories: categories = natsorted([c for c in categories if c is not None]) categories.append(None) else: categories = natsorted(list(categories)) # Print modules by category first = True for category in categories: # For the first category, no newline is needed if first: first = False else: self.be.echo('') # Print category name self.be.echo(category if category else '<UNCATEGORIZED>') # Print each module in category for module in [ m for m in modules if ((True if category is None else False) if ( m.category is None) else ( m.category.strip().upper() == category)) ]: # Get all versions of module versions = [] for version in module.versions: v = os.path.basename(version) if version == module.default: v = v + '(default)' if version in self.env.modloaded: v = self.be.highlight(v + '*', kind='info') versions.append(v) # Print module together with all its version, in which # the default version and the currently loaded version are # marked specially self.be.echo(' {m:{l}} {v}'.format(m=module.name + ':', l=maxlength + 1, v=', '.join(versions))) def is_loaded(self, name): """Return True if module `name` is loaded, else False.""" modname, _ = self.decode_name(name) if modname in [ modname for modname, _ in map(self.decode_file, self.env.modloaded) ]: return True else: return False def load_module(self, name): """Load module `name` if it is not yet loaded.""" # Get full path to module file modfile = self.get_module_file(name) # If module file was not found, return if modfile is None: return # Otherwise, if module is already loaded, return elif modfile in self.env.modloaded: return # Otherwise, parse module file for loading and add module to list of # loaded modules else: if self.parser.load(modfile): self.env.add_loaded_module(modfile) def unload_module(self, name): """Unload module `name` if it is currently loaded.""" # Get module name and version modname, modversion = self.decode_name(name) # Get module file, name, and version from currently loaded modules for modfile, (modnamefile, modversionfile) in zip( self.env.modloaded, map(self.decode_file, self.env.modloaded)): # If module name matches, unload module and remove module from # list of loaded modules if modname == modnamefile and (modversion is None or modversion == modversionfile): if self.parser.unload(modfile): self.env.modloaded.remove(modfile) def process_modified(self): """Check all modified environment variables and unset/export them as needed. """ # Iterate over all variables that were modified for var in [ var for var in self.env.variables.values() if var.is_modified() ]: # Unset variable if it is marked for unset if var.is_unset(): self.be.unset(var.get_name()) # Otherwise update its value else: self.be.export(*var.get_export()) def cmd_avail(self): """Command 'avail': list all available modules by category.""" self.init_env() self.init_modules() self.print_modules(self.modules) def cmd_config(self): """Command 'config': show configuration for module or Modm itself.""" # Show configuration for Modm if no argument was specified if len(self.args) == 0: self.init_env() self.be.echo("Modm executable script path variable: MODM_PY") self.be.echo("Current script path: ", newline=False) self.be.echo(os.environ['MODM_PY'], kind='info') self.be.echo() self.be.echo( "Module path variable: {v}".format(v=self.modules_path_var)) self.be.echo("Currently active paths:") for p in self.env.modpath: self.be.echo(p, kind='info') self.be.echo() self.be.echo("Loaded modules variable: {v}".format( v=self.modules_loaded_var)) self.be.echo("Currently loaded module files:") for m in self.env.modloaded: self.be.echo(m, kind='info') self.be.echo() self.be.echo("Admin email address variable: {v}".format( v=self.admin_email_var)) self.be.echo("Current email address: ", newline=False) self.be.echo(self.admin_email, kind='info') self.be.echo() self.be.echo("Color settings variable: {v}".format( v=self.color_setting_var)) self.be.echo("Current setting for color usage: ", newline=False) self.be.echo("ON" if self.use_colors else "OFF", kind='info') self.be.echo( "(if variable is set to 'OFF', colors are disabled, " + "otherwise enabled)") # Otherwise show configuration for module and ignore further arguments else: self.init_modules() name = self.args[0] modfile = self.get_module_file(name) # If module file was not found, print error if modfile is None: self.be.error("Module '{m}' not found.".format(m=name)) # Otherwise parse module file for configuration information else: module = self.modules[self.find_module(name)] self.be.echo("Module name: ", newline=False) self.be.echo(module.name, kind='info') self.be.echo("Available versions: ", newline=False) self.be.echo(', '.join( [os.path.basename(v) for v in module.versions]), kind='info') self.be.echo("Default version: ", newline=False) self.be.echo(os.path.basename(module.default), kind='info') self.be.echo("Category: ", newline=False) self.be.echo(module.category.upper() if module.category else "<UNCATEGORIZED>", kind='info') self.be.echo("Module help available: ", newline=False) self.be.echo("yes" if module.help_file else "no", kind='info') self.be.echo("Module file for '{n}': ".format(n=name), newline=False) self.be.echo(modfile, kind='info') self.be.echo("Module file content:") self.print_file(modfile, kind='info') def cmd_help(self): """Command 'help': show help on commands or modules.""" # Get help topic from arguments and parse for commands topic = self.args[0] if len(self.args) > 0 else None command, alternatives = self.parse_command(topic) # If no topic was specified, show usage information if self.cmd in ['--help'] or topic is None: self.print_help('usage') # If a command was specified, print its help file elif command in ['help']: self.print_help(os.path.join('commands', 'help')) elif command in ['avail', 'status']: self.print_help(os.path.join('commands', 'avail')) elif command in ['config']: self.print_help(os.path.join('commands', 'config')) elif command in ['list']: self.print_help(os.path.join('commands', 'list')) elif command in ['load']: self.print_help(os.path.join('commands', 'load')) elif command in ['unload']: self.print_help(os.path.join('commands', 'unload')) # If no command was determined but there are alternatives, show a list # of ambiguous commands elif command is None and len(alternatives) > 0: self.be.error( "Command '{c}' is ambiguous. See 'modm help'.".format(c=topic)) self.be.echo() self.be.echo("Did you mean one of these?") for a in alternatives: self.be.echo(" {u}[{t}]".format(u=a[0:len(self.cmd) + 1], t=a[len(self.cmd) + 1:])) # Otherwise, check modules for help else: self.init_modules() index = self.find_module(topic) # If no module was found with the name `topic`, show error if index is None: self.be.error("Unknown help topic '{t}'.".format(t=topic)) self.be.error( "See 'modm help help' for a list of help topics.") # Otherwise show error if no help file was set for the module elif self.modules[index].help_file is None: self.be.error("No help available for module '{m}'.".format( m=self.modules[index].name)) # Otherwise (help file was found for module), print help file else: self.print_file(self.modules[index].help_file) def cmd_list(self): """Command 'list': show all currently loaded modules.""" self.init_modules() # Print all loaded modules in an easily parsable list for modfile in natsorted(self.env.modloaded): head, modversion = os.path.split(modfile) _, modname = os.path.split(head) self.be.echo(os.path.join(modname, modversion)) def cmd_load(self): """Command 'load': load all specified module files.""" self.init_modules() self.init_parser() # Try to load each argument as a module for name in self.args: index = self.find_module(name, strict=True) # If module was found, load it if index is not None: # If a version of the module is currently loaded, unload it if self.is_loaded(name): self.unload_module(self.decode_name(name)[0]) self.load_module(name) # Otherwise print error else: self.be.error("Module '{m}' not found.".format(m=name)) # After loading all modules, act on all environment variables that # have changed self.process_modified() self.be.export(self.env.modloaded_var, self.env.get_modloaded_str()) def cmd_unload(self): """Command 'unload': unload all specified module files.""" self.init_modules() self.init_parser() # Unload each argument for name in self.args: self.unload_module(name) # After unloadig all modules, act on all environment variables that # have changed self.process_modified() self.be.export(self.env.modloaded_var, self.env.get_modloaded_str()) def cmd_version(self): """Print version information.""" self.be.echo("modm version {v}".format(v=__version__))