def __init__( self,
            launch_dir, api_args, cmd_files, interactive, piped_input, diagnostic, verbose ):

        # Set passed in values and link to my Session Specification
        self.launch_dir = launch_dir
        self.api_args = api_args # These args are passed through to the API
        self.spec = Session_Spec()
        self.verbose = verbose # initial setting passed in from the command line
        self.diagnostic = diagnostic # initial setting passed in from the command line

        # Initialize the API
        self.api = API( *api_args )

        # Initialized UI specific (non-API) features
        self.ui_cmd = {}
        self.ui_alias = {}
        self.exit_commands = ['q', 'quit', 'exit', 'ciao', 'bye']
        self.init_ui_cmd()

        # Initialize the DB session
        self.editor = mi_RDB.db_Session()

        # Special handling of stdio if a command file is piped in
        if piped_input:
            self.mode = "piped"
            self.interact()
            if not cmd_files and not interactive:
                exit(0)
            if interactive:
                # Switch standard input to tty for interactive session
                sys.stdin = open('/dev/tty', 'r')
        if cmd_files:
            self.mode = "batch"
            self.process_command_files( cmd_files, interactive )
            if not interactive:
                self.editor.close()
                exit(0)

        # Start prompting for commands
        self.mode = "interactive"
        self.interact()

        # User ended the command session, clean up and quit
        self.editor.close()
    def ui_refresh( self, arg_map=None ):
        """
        Re-reads the API.

        """
        self.api = API( *self.api_args )
class Session:
    """
    Session

    All user interaction occurs in the context of a Session.

    """
    def __init__( self,
            launch_dir, api_args, cmd_files, interactive, piped_input, diagnostic, verbose ):

        # Set passed in values and link to my Session Specification
        self.launch_dir = launch_dir
        self.api_args = api_args # These args are passed through to the API
        self.spec = Session_Spec()
        self.verbose = verbose # initial setting passed in from the command line
        self.diagnostic = diagnostic # initial setting passed in from the command line

        # Initialize the API
        self.api = API( *api_args )

        # Initialized UI specific (non-API) features
        self.ui_cmd = {}
        self.ui_alias = {}
        self.exit_commands = ['q', 'quit', 'exit', 'ciao', 'bye']
        self.init_ui_cmd()

        # Initialize the DB session
        self.editor = mi_RDB.db_Session()

        # Special handling of stdio if a command file is piped in
        if piped_input:
            self.mode = "piped"
            self.interact()
            if not cmd_files and not interactive:
                exit(0)
            if interactive:
                # Switch standard input to tty for interactive session
                sys.stdin = open('/dev/tty', 'r')
        if cmd_files:
            self.mode = "batch"
            self.process_command_files( cmd_files, interactive )
            if not interactive:
                self.editor.close()
                exit(0)

        # Start prompting for commands
        self.mode = "interactive"
        self.interact()

        # User ended the command session, clean up and quit
        self.editor.close()

    def extract_arg_item( self, arg_text ):
        """
        Extracts the leftmost argument name - value pair from the supplied text
        and returns the end char position of the matched text.

        """
        for f, p in self.spec.arg_extract: # function, pattern
            r = p.match( arg_text ) # Matches leftmost arg-value pair, if any
            if r:
                # Apply the extraction function for this pattern
                a, v, match_end, pattern = f( r )
                # Chop off what we just extracted on the left
                arg_text = None if match_end >= len( arg_text ) - 1 \
                    else arg_text[match_end:].lstrip()
                return a, v, pattern, arg_text

        # No pattern matched
        raise mi_Syntax_Error( "<op> <subject> [<args>]" )

    def parse_app_args( self, arg_text ):
        """
        Simple version of parse_ui_args which does no validation.  It simply
        produces the arg_map which can be later validated by the api.

        """
        # Later we will fuse both parse_ui/app_args functions
        # but they are separate now for tesing/experimentation

        arg_map = {} # the parsed data goes here

        # No need to parse the arg text if there is a ? in it anywhere
        # as we will just print the complete list of required args
        if "?" in arg_text:
            arg_map["help"] = True  # Detected when building the API command
            return arg_map

        # line is unparsed portion of arg_text
        # strip it and remove any internal single or double quotes
        arg_text = arg_text.strip().replace("'","").replace('"',"")

        match_end = 0 # total length of matched groups for line chopping
        while arg_text:
            a, v, pattern, arg_text = self.extract_arg_item( arg_text )
            arg_map[a] = v
            # pattern not used for app args

        return arg_map

    def parse_ui_args( self, op, arg_text ):
        """
        Parses the args portion of a single text command line to produce an arg_map.
        This is a dictionary of arg:value and switch:True pairs.  The arg_map can be
        later supplied to the op's designated implementation function.

        """
        arg_map = {} # the parsed data goes here
        arg_text = arg_text.strip() # line is unparsed portion of arg_text
        fset = set() # For grouping comparison

        # Build the arg map
        match_end = 0 # total length of matched groups for line chopping
        while arg_text:
            a, v, pattern, arg_text = self.extract_arg_item( arg_text )

            if pattern == 'value': # matches arg value pattern, ex: -s domain
                # validate( a ) # validate_uiarg(a) or validate_apparg(a)
                if a not in self.ui_cmd[op]['syntax']:
                    # Arg not defined for this op
                    raise mi_Syntax_Error( self.ui_cmd[op]['help'] )

                if self.ui_cmd[op]['syntax'][a]['action'] != 'store':
                    # Arg does not take a value for this op
                    raise mi_Syntax_Error( self.ui_cmd[op]['help'] )

            elif pattern == 'flag': # matches flat pattern, ex: -f
                if a not in self.ui_cmd[op]['syntax']:
                    # Arg not defined for this op
                    raise mi_Syntax_Error( self.ui_cmd[op]['help'] )

                arg_spec = self.ui_cmd[op]['syntax'][a] # for brevity

                if arg_spec['action'] == 'store':
                    try:
                        v = arg_spec['default']
                    except KeyError:
                        # Value should have been specified since there was no default
                        raise mi_Syntax_Error( self.ui_cmd[op]['help'] )
                elif arg_spec['action'] == 'switch':
                    v = True
                else: # action must be 'store' or 'switch', fatal error
                    raise mi_Error( 'No action defined for flag: ' + a )
            else:
                # A regex was matched, but it's not valid for a ui command
                # For example, a comma separated list is not an acceptable ui arg
                raise mi_Syntax_Error( self.ui_cmd[op]['help'] )

            # Add arg_map entry a, v, will overwrite any duplicate
            arg_map[ self.ui_cmd[op]['syntax'][a]['var'] ] = v.strip() if v else None

            # Update group
            fset.add(a)


        # Ensure that each required flag has been provided
        # and has a value.  Switches will always carry a True value.
        for flag in self.ui_cmd[op]['syntax']:
            if 'required' not in self.ui_cmd[op]['syntax'][flag]:
                continue # Don't worry about optional flags
            if self.ui_cmd[op]['syntax'][flag]['var'] not in arg_map:
                raise mi_Syntax_Error( self.ui_cmd[op]['help'] )

        # Enforce grouping rules
        if not arg_map and not self.ui_cmd[op].get('grouping'):
            # No groups and no args
            return arg_map

        group_found = False
        # Find 

        for group in self.ui_cmd[op]['grouping']:
            if fset == set(group): # Order doesn't matter, so we use sets
                group_found = True
                break
        if not group_found:
            # There is no legal grouping that corresponds to what we parsed
            raise mi_Syntax_Error( self.ui_cmd[op]['help'] )
        
        return arg_map

    # <<< UI Command Functions

    def ui_refresh( self, arg_map=None ):
        """
        Re-reads the API.

        """
        self.api = API( *self.api_args )

    def ui_help( self, arg_map=None ):
        """
        Prints command line help

        """
        if not arg_map:
            # Print everything, starting with UI commands
            print()
            print("UI Commands")
            print("---")
            print()
            for u in self.ui_cmd:
                print( self.ui_cmd[u]['help'] )
            print()
            print( "Exit with: [ " + " | ".join(self.exit_commands) + " ]" )

            # Print all app commands
            self.api.show_help( arg_map )

            # Print guide to more detailed help
            print( "Type a partial command with a question mark for help on a particular item." )
            print ("   example: help new attr ?" )
            print()


    def ui_toggle_verbose( self, arg_map ):
        """
        Toggles verbose mode where API calls are printed before being invoked.

        """
        self.verbose = not( self.verbose )
        print( "Verbose mode {}".format( "ON" if self.verbose else "OFF") )


    def ui_toggle_diagnostic( self, arg_map ):
        """
        Toggles diagnostic mode where API calls are printed, but not invoked.

        """
        self.diagnostic = not( self.diagnostic )
        print( "Diagnostic mode {}".format( "ON" if self.diagnostic else "OFF") )

    
    def ui_focus( self, arg_map ):
        """
        Sets or clears a focus attribute, or clears all focus attributes.

        """
        if 'subject_to_clear' in arg_map:
            # Can't use get() since value might be None
            # Either clear all defaults or the specified subject
            self.api.clear_default( arg_map['subject_to_clear'] )
            return

        if not arg_map.get('subject'):
            # Return all default values (if any have been set)
            for s, v in self.api.get_all_defaults():
                print('{} : {}'.format(s, v))
            return

        # A subject has been specified
        if arg_map.get('value'):
            # Set the subject's default to provided value
            s, v = self.api.set_default( arg_map['subject'], arg_map['value'] )
            return

        # Subject, but no value specified, so return the subject's current default value or None
        s, v = self.api.get_default_for_subject( arg_map['subject'])
        print( '{} : {}'.format(s, v) )


    def ui_process_cmd_file( self, arg_map ):
        """
        Reads and processes a single command file during an interactive
        session.

        """
        cmd_file = arg_map['file'] if os.path.isabs(arg_map['file']) else \
                os.path.join( self.launch_dir, arg_map['file'] )
        try:
            cf = open( cmd_file )
        except IOError:
            mi_File_Error("Could not open", cmd_file )
            return
        print()
        print("Reading file: " + cmd_file )
        print ()

        self.mode = "file"
        for command in cf:
            command = strip_comment_ws( command )
            if not command:
                continue
            print( "* " + command )
            try:
                self.process( command )
            except mi_Quiet_Error:
                print()
                print( "Aborted file: " + cmd_file )
                print()
                return # to interactive session
            finally:
                self.mode = "interactive"
        print()
        print( "End of file: " + cmd_file )
        print()

    def init_ui_cmd( self ):
        """
        Build up a list of recognized UI commands with required args.

        """
        self.ui_cmd['read'] = {
                'func':Session.ui_process_cmd_file,
                'syntax':{
                            'f':{'action':'store', 'var':'file'},
                    },
                'grouping':( ('f') ),
                'help':""
            }

        self.ui_cmd['diagnostic'] = {
                'func':Session.ui_toggle_diagnostic,
                'syntax':{},
                'grouping':( () ),
                'help':""
            }

        self.ui_cmd['verbose'] = {
                'func':Session.ui_toggle_verbose,
                'syntax':{},
                'grouping':( () ),
                'help':""
            }

        self.ui_cmd['refresh'] = {
                'func':Session.ui_refresh,
                'syntax':{},
                'grouping':( () ),
                'help':""
            }

        self.ui_cmd['focus'] = { # name of op
                    'func':Session.ui_focus, # Session function that implements op
                    'syntax':{ # flag specs
                            # action: store or switch, var: name of stored value
                            # 'required' means flag is required, otherwise flag is optional
                            # default:value means user can omit a value
                            # No value is provided with a switch action example:
                            # 'f':{'action':'switch', 'var':'force'},
                            's':{'action':'store', 'var':'subject'},
                            'v':{'action':'store', 'var':'value'},
                            'c':{'action':'store', 'var':'subject_to_clear', 'default':None}
                        },
                    # possible flag groupings on command line
                    # () means that a command with no args is okay
                    'grouping':( (), ('c'), ('s'), ('s', 'v') ),
                    'help':"" # generated below
                }

        self.ui_cmd['h'] = {
                    'func':Session.ui_help,
                    'syntax':{},
#                            's':{'action':'store', 'var':'subject'},
#                            'op':{'action':'store', 'var':'op'}
#                        },
                    'grouping':( () ),
                    'help':""
                }

        self.ui_alias = {
                'h': 'h', 'help' : 'h',
                'focus' : 'focus', 'f' : 'focus',
                'r':'refresh', 'refresh':'refresh',
                'read':'read', 'run':'read',
                'diagnostic':'diagnostic', 'd':'diagnostic',
                'verbose':'verbose', 'v':'verbose'
            }

        # Create help syntax dictionary with entry for each command
        for op in self.ui_cmd:
            args_optional = False
            group_strings = [] # Help string for each grouping
            for grouping in self.ui_cmd[op]['grouping']:
                if not grouping: # Empty grouping means 'no args' is okay
                    args_optional = True
                    continue
                gstr = "" # Help string for this group
                for flag in grouping:
                    fstr = "" # Help string for this flag
                    flag_spec = self.ui_cmd[op]['syntax'][flag] # for brevity
                    # Add flag syntax to help string
                    if flag_spec['action'] == 'store':
                        if 'default' in flag_spec:
                            # stores a default value -f [value]
                            fstr += "-{} [{}] ".format( flag, flag_spec['var'] )
                        else:
                            # stores a required value -f value
                            fstr += "-{} {} ".format( flag, flag_spec['var'] )
                    elif flag_spec['action'] == 'switch':
                            # no value
                            fstr += "-{}".format( flag )
                    # Append to group string
                    gstr += fstr
                # Add the flag help to the current group
                if gstr:
                    group_strings.append( gstr )
            # Join all the groups together with 'or' delimiter
            arg_help = ' | '.join(group_strings)
            # Wrap all args in 'optional' [] braces if the op may be used without args
            #if 'required' not in [f for f in self.ui_cmd[op]['syntax']]:
            if args_optional:
                arg_help = " [ " + arg_help + " ]"
            self.ui_cmd[op]['help'] = op + " " + arg_help

    def process_command_files( self, cmd_files, interactive ):
        """
        Process each command (line) from each command file.  Exits if any command
        fails unless interactive mode has been requested.

        """
        for cmd_fname in cmd_files:
            print()
            print("Reading file: " + cmd_fname )
            print()
            try:
                cf = open( cmd_fname )
            except:
                mi_File_Error("Could not open", cmd_fname )
            for command in cf:
                command = strip_comment_ws( command )
                if not command:
                    continue
                print( "* " + command )
                try:
                    self.process( command )
                except:
                    # If a command fails, no point in reading the rest of the file
                    # since the error will likely cascade.  Stop processing files.
                    print()
                    print( "Aborted file: " + cmd_fname )
                    print()
                    if not interactive:
                        exit(1)
                    return # Will enter an interactive session
            print()
            print( "End of file: " + cmd_fname )
            print()

    def interact( self ):
        """
        Interactive command loop.  Repeatedly prompts for a raw line of input.

        """
        if self.mode == "piped":
            print()
            print("--- Processing commands from input pipe ---")
            print()
        else:
            # Print start of session message
            print()
            print( self.spec.title +  " " + "Version: " + self.spec.version )
            print()
            print( "Developer: " + self.spec.developer + " / " + self.spec.copyright )
            print( self.spec.license )
            print()
            print ("? <subject> to get valid operations, ex: ? domain")
            print ("<op> <subject> without any args to get required args, ex: new domain")
            print( "h, help for help and q to quit" )
            print()

        # Prompt for commands and process them until a quit command is detected
        while True:
            line = None
            while not line: # ignore blank lines
                try:
                    if self.mode == "interactive":
                        line = input( self.spec.prompt )
                    else:
                        line = input()
                        line = None if line.startswith( COMMENT_CHAR ) else line
                except EOFError:
                    # We'll get this when processing an input stream
                    if self.mode == "piped":
                        print()
                        print("--- Finished processing input pipe ---")
                        print()
                        return
                    else:
                        print("Ctrl-D detected.")
                        print("Bye.")
                        print()
                        break

            if line in self.exit_commands:
                print("Bye.")
                print()
                break
            if self.mode != "interactive":
                print( "* " + line.strip() )
            try:
                self.process( line )
            except mi_Command_Error:
                # Error message has been printed, continue to next prompt
                continue

    def process( self, line ):
        """
        Process line

        """
        # Initially assume it is a UI command with two parts <UIOP> <UIARGS>
        term = line.split( None, 1 )

        # Is this a valid UI command?
        if term[UIOP] in self.ui_alias:
            # replace alias with official ui command
            term[UIOP] = self.ui_alias[term[UIOP]]
            
            # Split off the arg portion of the command line
            arg_text = "" if len(term) < 2 else term[UIARGS]
            
            # Parse the args
            arg_map = self.parse_ui_args( term[UIOP], arg_text )

            # Execute the UI command
            self.ui_cmd[term[UIOP]]['func']( self, arg_map )
            return

        # Assert: Not a UI command, possibly a legal App command

        # Break the line into 1-3 parts, <op> <subject> <args>
        term = line.split( None, 2 )

        if len(term) < 2: # We need at least an op and a subject
            raise mi_Syntax_Error( "<op> <subject> [arg, ...]" )
            if self.mode in {'batch', 'file'}:
                raise mi_Quiet_Error()
            return

        if len(term) == 2:
            term.append( "" ) # to avoid index error later

        # Assert term is a list of three elements OP, SUB, ARGS
        arg_map = self.parse_app_args( term[ARGS] )
        command = self.api.command_to_call( term[SUB], term[OP], arg_map )
        try:
            relations, attrs = self.editor.exec_command(
                    command['call'], command['pvals'], command['ovals'],
                    self.diagnostic, self.verbose
                )
        except mi_DB_Error:
            if self.mode in {'batch', 'file'}:
                raise mi_Quiet_Error()
            return # Non-fatal error was printed

        if attrs: # Any expected return value?
            print("<----")
            hlen = 0
            for a in attrs:
                print(a, end="\t")
                hlen += len(a) + 3
            else:
                print()
                print("="*hlen)
            for r in relations:
                print(r)
            else:
                print("="*hlen)