bitbake/lib/bb/utils.py:
[vuplus_bitbake] / lib / bb / shell.py
index 9159bb1..b86dc97 100644 (file)
@@ -1,6 +1,7 @@
 #!/usr/bin/env python
 # ex:ts=4:sw=4:sts=4:et
 # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+##########################################################################
 #
 # Copyright (C) 2005 Michael 'Mickey' Lauer <mickey@Vanille.de>, Vanille Media
 #
 # this program; if not, write to the Free Software Foundation, Inc., 59 Temple
 # Place, Suite 330, Boston, MA 02111-1307 USA.
 #
+##########################################################################
+#
+# Thanks to:
+# * Holger Freyther <zecke@handhelds.org>
+# * Justin Patrin <papercrane@reversefold.com>
+#
+##########################################################################
 
 """
 BitBake Shell
 
-General Question to be decided: Make it a full-fledged Python Shell or
-retain the simple command line interface like it is at the moment?
-
-TODO:
-    * readline completion (file and provider?)
-    * specify tasks
-    * specify force
-    * command to clean stamps
-    * command to reparse one bbfile
+IDEAS:
+    * list defined tasks per package
+    * list classes
+    * toggle force
+    * command to reparse just one (or more) bbfile(s)
     * automatic check if reparsing is necessary (inotify?)
-    * bb file wizard
-    * call editor on bb file
-    * clean-and-rebuild bbfile
+    * frontend for bb file manipulation
+    * more shell-like features:
+        - output control, i.e. pipe output into grep, sort, etc.
+        - job control, i.e. bring running commands into background and foreground
+    * start parsing in background right after startup
+    * ncurses interface
+
+PROBLEMS:
+    * force doesn't always work
+    * readline completion for commands with more than one parameters
+
 """
 
+##########################################################################
+# Import and setup global variables
+##########################################################################
+
 try:
     set
 except NameError:
     from sets import Set as set
-import sys, os, imp, readline
+import sys, os, imp, readline, socket, httplib, urllib, commands, popen2, copy, shlex, Queue, fnmatch
 imp.load_source( "bitbake", os.path.dirname( sys.argv[0] )+"/bitbake" )
-from bb import make, data
+from bb import data, parse, build, fatal
 
-__version__ = 0.1
-__credits__ = """BitBake Shell Version %2.1f (C) 2005 Michael 'Mickey' Lauer <mickey@Vanille.de>
+__version__ = "0.5.3"
+__credits__ = """BitBake Shell Version %s (C) 2005 Michael 'Mickey' Lauer <mickey@Vanille.de>
 Type 'help' for more information, press CTRL-D to exit.""" % __version__
 
 cmds = {}
+leave_mainloop = False
+last_exception = None
 cooker = None
 parsed = False
-debug = False
+initdata = None
+debug = os.environ.get( "BBSHELL_DEBUG", "" )
+
+##########################################################################
+# Class BitBakeShellCommands
+##########################################################################
+
+class BitBakeShellCommands:
+    """This class contains the valid commands for the shell"""
+
+    def __init__( self, shell ):
+        """Register all the commands"""
+        self._shell = shell
+        for attr in BitBakeShellCommands.__dict__:
+            if not attr.startswith( "_" ):
+                if attr.endswith( "_" ):
+                    command = attr[:-1].lower()
+                else:
+                    command = attr[:].lower()
+                method = getattr( BitBakeShellCommands, attr )
+                debugOut( "registering command '%s'" % command )
+                # scan number of arguments
+                usage = getattr( method, "usage", "" )
+                if usage != "<...>":
+                    numArgs = len( usage.split() )
+                else:
+                    numArgs = -1
+                shell.registerCommand( command, method, numArgs, "%s %s" % ( command, usage ), method.__doc__ )
+
+    def _checkParsed( self ):
+        if not parsed:
+            print "SHELL: This command needs to parse bbfiles..."
+            self.parse( None )
+
+    def _findProvider( self, item ):
+        self._checkParsed()
+        preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, cooker.configuration.data, 1 )
+        if not preferred: preferred = item
+        try:
+            lv, lf, pv, pf = cooker.findBestProvider( preferred )
+        except KeyError:
+            if item in cooker.status.providers:
+                pf = cooker.status.providers[item][0]
+            else:
+                pf = None
+        return pf
+
+    def alias( self, params ):
+        """Register a new name for a command"""
+        new, old = params
+        if not old in cmds:
+            print "ERROR: Command '%s' not known" % old
+        else:
+            cmds[new] = cmds[old]
+            print "OK"
+    alias.usage = "<alias> <command>"
+
+    def buffer( self, params ):
+        """Dump specified output buffer"""
+        index = params[0]
+        print self._shell.myout.buffer( int( index ) )
+    buffer.usage = "<index>"
 
-def rebuildCommand( params ):
-    """Clean and rebuild a .bb file or a provider"""
-    print "BBSHELL: sorry, not yet implemented :/"
+    def buffers( self, params ):
+        """Show the available output buffers"""
+        commands = self._shell.myout.bufferedCommands()
+        if not commands:
+            print "SHELL: No buffered commands available yet. Start doing something."
+        else:
+            print "="*35, "Available Output Buffers", "="*27
+            for index, cmd in enumerate( commands ):
+                print "| %s %s" % ( str( index ).ljust( 3 ), cmd )
+            print "="*88
+
+    def build( self, params, cmd = "build" ):
+        """Build a providee"""
+        globexpr = params[0]
+        self._checkParsed()
+        names = globfilter( cooker.status.pkg_pn.keys(), globexpr )
+        if len( names ) == 0: names = [ globexpr ]
+        print "SHELL: Building %s" % ' '.join( names )
+
+        oldcmd = cooker.configuration.cmd
+        cooker.configuration.cmd = cmd
+        cooker.build_cache = []
+        cooker.build_cache_fail = []
+
+        for name in names:
+            try:
+                cooker.buildProvider( name, data.getVar("BUILD_ALL_DEPS", cooker.configuration.data, True) )
+            except build.EventException, e:
+                print "ERROR: Couldn't build '%s'" % name
+                global last_exception
+                last_exception = e
+                break
+
+        cooker.configuration.cmd = oldcmd
+
+    build.usage = "<providee>"
+
+    def clean( self, params ):
+        """Clean a providee"""
+        self.build( params, "clean" )
+    clean.usage = "<providee>"
+
+    def compile( self, params ):
+        """Execute 'compile' on a providee"""
+        self.build( params, "compile" )
+    compile.usage = "<providee>"
+
+    def configure( self, params ):
+        """Execute 'configure' on a providee"""
+        self.build( params, "configure" )
+    configure.usage = "<providee>"
 
-def buildCommand( params ):
-    """Build a .bb file or a provider"""
-    try:
+    def edit( self, params ):
+        """Call $EDITOR on a providee"""
         name = params[0]
-    except IndexError:
-        print "Usage: build <bbfile|provider>"
-    else:
-        if name.endswith( ".bb" ):
-            cooker.executeOneBB( os.path.abspath( name ) )
-        else:
-            if not parsed:
-                print "BBSHELL: D'oh! The .bb files haven't been parsed yet. Next time call 'parse' before building stuff. This time I'll do it for 'ya."
-                parseCommand( None )
-            cooker.buildPackage( name )
-
-def parseCommand( params ):
-    """(Re-)parse .bb files and calculate the dependency graph"""
-    cooker.status = cooker.ParsingStatus()
-    ignore = data.getVar("ASSUME_PROVIDED", make.cfg, 1) or ""
-    cooker.status.ignored_dependencies = set( ignore.split() )
-    cooker.handleCollections( data.getVar("BBFILE_COLLECTIONS", make.cfg, 1) )
-
-    make.collect_bbfiles( cooker.myProgressCallback )
-    cooker.buildDepgraph()
-    global parsed
-    parsed = True
-    print
-
-def environmentCommand( params ):
-    """Dump out the outer BitBake environment (see bbread)"""
-    data.emit_env(sys.__stdout__, make.cfg, True)
-
-def printCommand( params ):
-    """Print the contents of an outer BitBake environment variable"""
-    try:
+        bbfile = self._findProvider( name )
+        if bbfile is not None:
+            os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), bbfile ) )
+        else:
+            print "ERROR: Nothing provides '%s'" % name
+    edit.usage = "<providee>"
+
+    def environment( self, params ):
+        """Dump out the outer BitBake environment (see bbread)"""
+        data.emit_env(sys.__stdout__, cooker.configuration.data, True)
+
+    def exit_( self, params ):
+        """Leave the BitBake Shell"""
+        debugOut( "setting leave_mainloop to true" )
+        global leave_mainloop
+        leave_mainloop = True
+
+    def fetch( self, params ):
+        """Fetch a providee"""
+        self.build( params, "fetch" )
+    fetch.usage = "<providee>"
+
+    def fileBuild( self, params, cmd = "build" ):
+        """Parse and build a .bb file"""
+        name = params[0]
+        bf = completeFilePath( name )
+        print "SHELL: Calling '%s' on '%s'" % ( cmd, bf )
+
+        oldcmd = cooker.configuration.cmd
+        cooker.configuration.cmd = cmd
+        cooker.build_cache = []
+        cooker.build_cache_fail = []
+
+        thisdata = copy.deepcopy( initdata )
+        # Caution: parse.handle modifies thisdata, hence it would
+        # lead to pollution cooker.configuration.data, which is
+        # why we use it on a safe copy we obtained from cooker right after
+        # parsing the initial *.conf files
+        try:
+            bbfile_data = parse.handle( bf, thisdata )
+        except parse.ParseError:
+            print "ERROR: Unable to open or parse '%s'" % bf
+        else:
+            item = data.getVar('PN', bbfile_data, 1)
+            data.setVar( "_task_cache", [], bbfile_data ) # force
+            try:
+                cooker.tryBuildPackage( os.path.abspath( bf ), item, bbfile_data )
+            except build.EventException, e:
+                print "ERROR: Couldn't build '%s'" % name
+                global last_exception
+                last_exception = e
+
+        cooker.configuration.cmd = oldcmd
+    fileBuild.usage = "<bbfile>"
+
+    def fileClean( self, params ):
+        """Clean a .bb file"""
+        self.fileBuild( params, "clean" )
+    fileClean.usage = "<bbfile>"
+
+    def fileEdit( self, params ):
+        """Call $EDITOR on a .bb file"""
+        name = params[0]
+        os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), completeFilePath( name ) ) )
+    fileEdit.usage = "<bbfile>"
+
+    def fileRebuild( self, params ):
+        """Rebuild (clean & build) a .bb file"""
+        self.fileClean( params )
+        self.fileBuild( params )
+    fileRebuild.usage = "<bbfile>"
+
+    def fileReparse( self, params ):
+        """(re)Parse a bb file"""
+        bbfile = params[0]
+        print "SHELL: Parsing '%s'" % bbfile
+        parse.update_mtime( bbfile )
+        bb_data, fromCache = cooker.load_bbfile( bbfile )
+        cooker.pkgdata[bbfile] = bb_data
+        if fromCache:
+            print "SHELL: File has not been updated, not reparsing"
+        else:
+            print "SHELL: Parsed"
+    fileReparse.usage = "<bbfile>"
+
+    def force( self, params ):
+        """Toggle force task execution flag (see bitbake -f)"""
+        cooker.configuration.force = not cooker.configuration.force
+        print "SHELL: Force Flag is now '%s'" % repr( cooker.configuration.force )
+
+    def help( self, params ):
+        """Show a comprehensive list of commands and their purpose"""
+        print "="*30, "Available Commands", "="*30
+        allcmds = cmds.keys()
+        allcmds.sort()
+        for cmd in allcmds:
+            function,numparams,usage,helptext = cmds[cmd]
+            print "| %s | %s" % (usage.ljust(30), helptext)
+        print "="*78
+
+    def lastError( self, params ):
+        """Show the reason or log that was produced by the last BitBake event exception"""
+        if last_exception is None:
+            print "SHELL: No Errors yet (Phew)..."
+        else:
+            reason, event = last_exception.args
+            print "SHELL: Reason for the last error: '%s'" % reason
+            if ':' in reason:
+                msg, filename = reason.split( ':' )
+                filename = filename.strip()
+                print "SHELL: Dumping log file for last error:"
+                try:
+                    print open( filename ).read()
+                except IOError:
+                    print "ERROR: Couldn't open '%s'" % filename
+
+    def match( self, params ):
+        """Dump all files or providers matching a glob expression"""
+        what, globexpr = params
+        if what == "files":
+            self._checkParsed()
+            for key in globfilter( cooker.pkgdata.keys(), globexpr ): print key
+        elif what == "providers":
+            self._checkParsed()
+            for key in globfilter( cooker.status.pkg_pn.keys(), globexpr ): print key
+        else:
+            print "Usage: match %s" % self.print_.usage
+    match.usage = "<files|providers> <glob>"
+
+    def new( self, params ):
+        """Create a new .bb file and open the editor"""
+        dirname, filename = params
+        packages = '/'.join( data.getVar( "BBFILES", cooker.configuration.data, 1 ).split('/')[:-2] )
+        fulldirname = "%s/%s" % ( packages, dirname )
+
+        if not os.path.exists( fulldirname ):
+            print "SHELL: Creating '%s'" % fulldirname
+            os.mkdir( fulldirname )
+        if os.path.exists( fulldirname ) and os.path.isdir( fulldirname ):
+            if os.path.exists( "%s/%s" % ( fulldirname, filename ) ):
+                print "SHELL: ERROR: %s/%s already exists" % ( fulldirname, filename )
+                return False
+            print "SHELL: Creating '%s/%s'" % ( fulldirname, filename )
+            newpackage = open( "%s/%s" % ( fulldirname, filename ), "w" )
+            print >>newpackage,"""DESCRIPTION = ""
+SECTION = ""
+AUTHOR = ""
+HOMEPAGE = ""
+MAINTAINER = ""
+LICENSE = "GPL"
+PR = "r0"
+
+SRC_URI = ""
+
+#inherit base
+
+#do_configure() {
+#
+#}
+
+#do_compile() {
+#
+#}
+
+#do_stage() {
+#
+#}
+
+#do_install() {
+#
+#}
+"""
+            newpackage.close()
+            os.system( "%s %s/%s" % ( os.environ.get( "EDITOR" ), fulldirname, filename ) )
+    new.usage = "<directory> <filename>"
+
+    def pasteBin( self, params ):
+        """Send a command + output buffer to http://pastebin.com"""
+        index = params[0]
+        contents = self._shell.myout.buffer( int( index ) )
+        status, error, location = sendToPastebin( contents )
+        if status == 302:
+            print "SHELL: Pasted to %s" % location
+        else:
+            print "ERROR: %s %s" % ( status, error )
+    pasteBin.usage = "<index>"
+
+    def pasteLog( self, params ):
+        """Send the last event exception error log (if there is one) to http://pastebin.com"""
+        if last_exception is None:
+            print "SHELL: No Errors yet (Phew)..."
+        else:
+            reason, event = last_exception.args
+            print "SHELL: Reason for the last error: '%s'" % reason
+            if ':' in reason:
+                msg, filename = reason.split( ':' )
+                filename = filename.strip()
+                print "SHELL: Pasting log file to pastebin..."
+
+                status, error, location = sendToPastebin( open( filename ).read() )
+
+                if status == 302:
+                    print "SHELL: Pasted to %s" % location
+                else:
+                    print "ERROR: %s %s" % ( status, error )
+
+    def patch( self, params ):
+        """Execute 'patch' command on a providee"""
+        self.build( params, "patch" )
+    patch.usage = "<providee>"
+
+    def parse( self, params ):
+        """(Re-)parse .bb files and calculate the dependency graph"""
+        cooker.status = cooker.ParsingStatus()
+        ignore = data.getVar("ASSUME_PROVIDED", cooker.configuration.data, 1) or ""
+        cooker.status.ignored_dependencies = set( ignore.split() )
+        cooker.handleCollections( data.getVar("BBFILE_COLLECTIONS", cooker.configuration.data, 1) )
+
+        cooker.collect_bbfiles( cooker.myProgressCallback )
+        cooker.buildDepgraph()
+        global parsed
+        parsed = True
+        print
+
+    def reparse( self, params ):
+        """(re)Parse a providee's bb file"""
+        bbfile = self._findProvider( params[0] )
+        if bbfile is not None:
+            print "SHELL: Found bbfile '%s' for '%s'" % ( bbfile, params[0] )
+            self.fileReparse( [ bbfile ] )
+        else:
+            print "ERROR: Nothing provides '%s'" % params[0]
+    reparse.usage = "<providee>"
+
+    def getvar( self, params ):
+        """Dump the contents of an outer BitBake environment variable"""
         var = params[0]
-    except IndexError:
-        print "Usage: print <variable>"
-    else:
-        value = data.getVar( var, make.cfg, 1 )
+        value = data.getVar( var, cooker.configuration.data, 1 )
         print value
+    getvar.usage = "<variable>"
+
+    def peek( self, params ):
+        """Dump contents of variable defined in providee's metadata"""
+        name, var = params
+        bbfile = self._findProvider( name )
+        if bbfile is not None:
+            value = cooker.pkgdata[bbfile].getVar( var, 1 )
+            print value
+        else:
+            print "ERROR: Nothing provides '%s'" % name
+    peek.usage = "<providee> <variable>"
+
+    def poke( self, params ):
+        """Set contents of variable defined in providee's metadata"""
+        name, var, value = params
+        bbfile = self._findProvider( name )
+        d = cooker.pkgdata[bbfile]
+        if bbfile is not None:
+            data.setVar( var, value, d )
+
+            # mark the change semi persistant
+            cooker.pkgdata.setDirty(bbfile, d)
+            print "OK"
+        else:
+            print "ERROR: Nothing provides '%s'" % name
+    poke.usage = "<providee> <variable> <value>"
+
+    def print_( self, params ):
+        """Dump all files or providers"""
+        what = params[0]
+        if what == "files":
+            self._checkParsed()
+            for key in cooker.pkgdata.keys(): print key
+        elif what == "providers":
+            self._checkParsed()
+            for key in cooker.status.providers.keys(): print key
+        else:
+            print "Usage: print %s" % self.print_.usage
+    print_.usage = "<files|providers>"
 
-def setVarCommand( params ):
-    """Set an outer BitBake environment variable"""
-    try:
+    def python( self, params ):
+        """Enter the expert mode - an interactive BitBake Python Interpreter"""
+        sys.ps1 = "EXPERT BB>>> "
+        sys.ps2 = "EXPERT BB... "
+        import code
+        interpreter = code.InteractiveConsole( dict( globals() ) )
+        interpreter.interact( "SHELL: Expert Mode - BitBake Python %s\nType 'help' for more information, press CTRL-D to switch back to BBSHELL." % sys.version )
+
+    def showdata( self, params ):
+        """Execute 'showdata' on a providee"""
+        self.build( params, "showdata" )
+    showdata.usage = "<providee>"
+
+    def setVar( self, params ):
+        """Set an outer BitBake environment variable"""
         var, value = params
-    except ValueError, IndexError:
-        print "Usage: set <variable> <value>"
-    else:
-        data.setVar( var, value, make.cfg )
+        data.setVar( var, value, cooker.configuration.data )
         print "OK"
+    setVar.usage = "<variable> <value>"
+
+    def rebuild( self, params ):
+        """Clean and rebuild a .bb file or a providee"""
+        self.build( params, "clean" )
+        self.build( params, "build" )
+    rebuild.usage = "<providee>"
+
+    def shell( self, params ):
+        """Execute a shell command and dump the output"""
+        if params != "":
+            print commands.getoutput( " ".join( params ) )
+    shell.usage = "<...>"
+
+    def stage( self, params ):
+        """Execute 'stage' on a providee"""
+        self.build( params, "stage" )
+    stage.usage = "<providee>"
+
+    def status( self, params ):
+        """<just for testing>"""
+        print "-" * 78
+        print "build cache = '%s'" % cooker.build_cache
+        print "build cache fail = '%s'" % cooker.build_cache_fail
+        print "building list = '%s'" % cooker.building_list
+        print "build path = '%s'" % cooker.build_path
+        print "consider_msgs_cache = '%s'" % cooker.consider_msgs_cache
+        print "build stats = '%s'" % cooker.stats
+        if last_exception is not None: print "last_exception = '%s'" % repr( last_exception.args )
+        print "memory output contents = '%s'" % self._shell.myout._buffer
+
+    def test( self, params ):
+        """<just for testing>"""
+        print "testCommand called with '%s'" % params
+
+    def unpack( self, params ):
+        """Execute 'unpack' on a providee"""
+        self.build( params, "unpack" )
+    unpack.usage = "<providee>"
+
+    def which( self, params ):
+        """Computes the providers for a given providee"""
+        item = params[0]
+
+        self._checkParsed()
+
+        preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, cooker.configuration.data, 1 )
+        if not preferred: preferred = item
+
+        try:
+            lv, lf, pv, pf = cooker.findBestProvider( preferred )
+        except KeyError:
+            lv, lf, pv, pf = (None,)*4
+
+        try:
+            providers = cooker.status.providers[item]
+        except KeyError:
+            print "SHELL: ERROR: Nothing provides", preferred
+        else:
+            for provider in providers:
+                if provider == pf: provider = " (***) %s" % provider
+                else:              provider = "       %s" % provider
+                print provider
+    which.usage = "<providee>"
+
+##########################################################################
+# Common helper functions
+##########################################################################
+
+def completeFilePath( bbfile ):
+    """Get the complete bbfile path"""
+    if not cooker.pkgdata: return bbfile
+    for key in cooker.pkgdata.keys():
+        if key.endswith( bbfile ):
+            return key
+    return bbfile
+
+def sendToPastebin( content ):
+    """Send content to http://www.pastebin.com"""
+    mydata = {}
+    mydata["parent_pid"] = ""
+    mydata["format"] = "bash"
+    mydata["code2"] = content
+    mydata["paste"] = "Send"
+    mydata["poster"] = "%s@%s" % ( os.environ.get( "USER", "unknown" ), socket.gethostname() or "unknown" )
+    params = urllib.urlencode( mydata )
+    headers = {"Content-type": "application/x-www-form-urlencoded","Accept": "text/plain"}
+
+    conn = httplib.HTTPConnection( "pastebin.com:80" )
+    conn.request("POST", "/", params, headers )
 
-def init():
-    """Register commands and set up readline"""
-    registerCommand( "help", showHelp )
-    registerCommand( "exit", exitShell )
-    
-    registerCommand( "build", buildCommand )
-    registerCommand( "environment", environmentCommand )
-    registerCommand( "rebuild", rebuildCommand )
-    registerCommand( "parse", parseCommand )
-    registerCommand( "print", printCommand )
-    registerCommand( "set", setVarCommand )
-    
-    readline.set_completer( completer )
-    readline.parse_and_bind("tab: complete")
-
-def exitShell( params ):
-    """Leave the BitBake Shell"""
-    sys.exit(0)
-
-def completer( *args, **kwargs ):
-    print "completer called", args, kwargs
-    return None
-
-def showCredits():
-    print __credits__
-
-def showHelp( *args ):
-    """Show a comprehensive list of commands and their purpose"""
-    print "="*35, "Available Commands", "="*35
-    for cmd, func in cmds.items():
-        print "| %s | %s" % (cmd.ljust(max([len(x) for x in cmds.keys()])), func.__doc__)
-    print "="*88
-
-def registerCommand( command, function ):
-    cmds[command] = function
-
-def processCommand( command, params ):
-    if debug: print "(processing command '%s'...)" % command
-    if command in cmds:
-        result = cmds[command]( params )
+    response = conn.getresponse()
+    conn.close()
+
+    return response.status, response.reason, response.getheader( "location" ) or "unknown"
+
+def completer( text, state ):
+    """Return a possible readline completion"""
+    debugOut( "completer called with text='%s', state='%d'" % ( text, state ) )
+
+    if state == 0:
+        line = readline.get_line_buffer()
+        if " " in line:
+            line = line.split()
+            # we are in second (or more) argument
+            if line[0] in cmds and hasattr( cmds[line[0]][0], "usage" ): # known command and usage
+                u = getattr( cmds[line[0]][0], "usage" ).split()[0]
+                if u == "<variable>":
+                    allmatches = cooker.configuration.data.keys()
+                elif u == "<bbfile>":
+                    if cooker.pkgdata is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
+                    else: allmatches = [ x.split("/")[-1] for x in cooker.pkgdata.keys() ]
+                elif u == "<providee>":
+                    if cooker.pkgdata is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
+                    else: allmatches = cooker.status.providers.iterkeys()
+                else: allmatches = [ "(No tab completion available for this command)" ]
+            else: allmatches = [ "(No tab completion available for this command)" ]
+        else:
+            # we are in first argument
+            allmatches = cmds.iterkeys()
+
+        completer.matches = [ x for x in allmatches if x[:len(text)] == text ]
+        #print "completer.matches = '%s'" % completer.matches
+    if len( completer.matches ) > state:
+        return completer.matches[state]
     else:
-        print "Error: '%s' command is not a valid command." % command
-        return
-    if debug: print "(result was '%s')" % result
+        return None
+
+def debugOut( text ):
+    if debug:
+        sys.stderr.write( "( %s )\n" % text )
+
+def columnize( alist, width = 80 ):
+    """
+    A word-wrap function that preserves existing line breaks
+    and most spaces in the text. Expects that existing line
+    breaks are posix newlines (\n).
+    """
+    return reduce(lambda line, word, width=width: '%s%s%s' %
+                  (line,
+                   ' \n'[(len(line[line.rfind('\n')+1:])
+                         + len(word.split('\n',1)[0]
+                              ) >= width)],
+                   word),
+                  alist
+                 )
+
+def globfilter( names, pattern ):
+    return fnmatch.filter( names, pattern )
+
+##########################################################################
+# Class MemoryOutput
+##########################################################################
+
+class MemoryOutput:
+    """File-like output class buffering the output of the last 10 commands"""
+    def __init__( self, delegate ):
+        self.delegate = delegate
+        self._buffer = []
+        self.text = []
+        self._command = None
+
+    def startCommand( self, command ):
+        self._command = command
+        self.text = []
+    def endCommand( self ):
+        if self._command is not None:
+            if len( self._buffer ) == 10: del self._buffer[0]
+            self._buffer.append( ( self._command, self.text ) )
+    def removeLast( self ):
+        if self._buffer:
+            del self._buffer[ len( self._buffer ) - 1 ]
+        self.text = []
+        self._command = None
+    def lastBuffer( self ):
+        if self._buffer:
+            return self._buffer[ len( self._buffer ) -1 ][1]
+    def bufferedCommands( self ):
+        return [ cmd for cmd, output in self._buffer ]
+    def buffer( self, i ):
+        if i < len( self._buffer ):
+            return "BB>> %s\n%s" % ( self._buffer[i][0], "".join( self._buffer[i][1] ) )
+        else: return "ERROR: Invalid buffer number. Buffer needs to be in (0, %d)" % ( len( self._buffer ) - 1 )
+    def write( self, text ):
+        if self._command is not None and text != "BB>> ": self.text.append( text )
+        if self.delegate is not None: self.delegate.write( text )
+    def flush( self ):
+        return self.delegate.flush()
+    def fileno( self ):
+        return self.delegate.fileno()
+    def isatty( self ):
+        return self.delegate.isatty()
+
+##########################################################################
+# Class BitBakeShell
+##########################################################################
 
-def main():
-    while True:
+class BitBakeShell:
+
+    def __init__( self ):
+        """Register commands and set up readline"""
+        self.commandQ = Queue.Queue()
+        self.commands = BitBakeShellCommands( self )
+        self.myout = MemoryOutput( sys.stdout )
+        self.historyfilename = os.path.expanduser( "~/.bbsh_history" )
+        self.startupfilename = os.path.expanduser( "~/.bbsh_startup" )
+
+        readline.set_completer( completer )
+        readline.set_completer_delims( " " )
+        readline.parse_and_bind("tab: complete")
+
+        try:
+            readline.read_history_file( self.historyfilename )
+        except IOError:
+            pass  # It doesn't exist yet.
+
+        print __credits__
+
+        # save initial cooker configuration (will be reused in file*** commands)
+        global initdata
+        initdata = copy.deepcopy( cooker.configuration.data )
+
+    def cleanup( self ):
+        """Write readline history and clean up resources"""
+        debugOut( "writing command history" )
+        try:
+            readline.write_history_file( self.historyfilename )
+        except:
+            print "SHELL: Unable to save command history"
+
+    def registerCommand( self, command, function, numparams = 0, usage = "", helptext = "" ):
+        """Register a command"""
+        if usage == "": usage = command
+        if helptext == "": helptext = function.__doc__ or "<not yet documented>"
+        cmds[command] = ( function, numparams, usage, helptext )
+
+    def processCommand( self, command, params ):
+        """Process a command. Check number of params and print a usage string, if appropriate"""
+        debugOut( "processing command '%s'..." % command )
         try:
-            cmdline = raw_input( "BB>> " )
-            if cmdline:
-                if ' ' in cmdline:
-                    processCommand( cmdline.split()[0], cmdline.split()[1:] )
+            function, numparams, usage, helptext = cmds[command]
+        except KeyError:
+            print "SHELL: ERROR: '%s' command is not a valid command." % command
+            self.myout.removeLast()
+        else:
+            if (numparams != -1) and (not len( params ) == numparams):
+                print "Usage: '%s'" % usage
+                return
+
+            result = function( self.commands, params )
+            debugOut( "result was '%s'" % result )
+
+    def processStartupFile( self ):
+        """Read and execute all commands found in $HOME/.bbsh_startup"""
+        if os.path.exists( self.startupfilename ):
+            startupfile = open( self.startupfilename, "r" )
+            for cmdline in startupfile:
+                debugOut( "processing startup line '%s'" % cmdline )
+                if not cmdline:
+                    continue
+                if "|" in cmdline:
+                    print "ERROR: '|' in startup file is not allowed. Ignoring line"
+                    continue
+                self.commandQ.put( cmdline.strip() )
+
+    def main( self ):
+        """The main command loop"""
+        while not leave_mainloop:
+            try:
+                if self.commandQ.empty():
+                    sys.stdout = self.myout.delegate
+                    cmdline = raw_input( "BB>> " )
+                    sys.stdout = self.myout
                 else:
-                    processCommand( cmdline, "" )
-        except EOFError:
-            print
-            return
-        except KeyboardInterrupt:
-            print
+                    cmdline = self.commandQ.get()
+                if cmdline:
+                    allCommands = cmdline.split( ';' )
+                    for command in allCommands:
+                        pipecmd = None
+                        #
+                        # special case for expert mode
+                        if command == 'python':
+                            sys.stdout = self.myout.delegate
+                            self.processCommand( command, "" )
+                            sys.stdout = self.myout
+                        else:
+                            self.myout.startCommand( command )
+                            if '|' in command: # disable output
+                                command, pipecmd = command.split( '|' )
+                                delegate = self.myout.delegate
+                                self.myout.delegate = None
+                            tokens = shlex.split( command, True )
+                            self.processCommand( tokens[0], tokens[1:] or "" )
+                            self.myout.endCommand()
+                            if pipecmd is not None: # restore output
+                                self.myout.delegate = delegate
+
+                                pipe = popen2.Popen4( pipecmd )
+                                pipe.tochild.write( "\n".join( self.myout.lastBuffer() ) )
+                                pipe.tochild.close()
+                                sys.stdout.write( pipe.fromchild.read() )
+                        #
+            except EOFError:
+                print
+                return
+            except KeyboardInterrupt:
+                print
+
+##########################################################################
+# Start function - called from the BitBake command line utility
+##########################################################################
 
 def start( aCooker ):
     global cooker
     cooker = aCooker
-    showCredits()
-    init()
-    main()
+    bbshell = BitBakeShell()
+    bbshell.processStartupFile()
+    bbshell.main()
+    bbshell.cleanup()
 
 if __name__ == "__main__":
-    print "BBSHELL: Sorry, this program should only be called by BitBake."
+    print "SHELL: Sorry, this program should only be called by BitBake."