Merge the BBCLASSEXTEND code from Poky. This allows once recipe to provide mutliple...
[bitbake.git] / lib / bb / shell.py
1 # ex:ts=4:sw=4:sts=4:et
2 # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3 ##########################################################################
4 #
5 # Copyright (C) 2005-2006 Michael 'Mickey' Lauer <mickey@Vanille.de>
6 # Copyright (C) 2005-2006 Vanille Media
7 #
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License version 2 as
10 # published by the Free Software Foundation.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License along
18 # with this program; if not, write to the Free Software Foundation, Inc.,
19 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 #
21 ##########################################################################
22 #
23 # Thanks to:
24 # * Holger Freyther <zecke@handhelds.org>
25 # * Justin Patrin <papercrane@reversefold.com>
26 #
27 ##########################################################################
28
29 """
30 BitBake Shell
31
32 IDEAS:
33     * list defined tasks per package
34     * list classes
35     * toggle force
36     * command to reparse just one (or more) bbfile(s)
37     * automatic check if reparsing is necessary (inotify?)
38     * frontend for bb file manipulation
39     * more shell-like features:
40         - output control, i.e. pipe output into grep, sort, etc.
41         - job control, i.e. bring running commands into background and foreground
42     * start parsing in background right after startup
43     * ncurses interface
44
45 PROBLEMS:
46     * force doesn't always work
47     * readline completion for commands with more than one parameters
48
49 """
50
51 ##########################################################################
52 # Import and setup global variables
53 ##########################################################################
54
55 try:
56     set
57 except NameError:
58     from sets import Set as set
59 import sys, os, readline, socket, httplib, urllib, commands, popen2, copy, shlex, Queue, fnmatch
60 from bb import data, parse, build, fatal, cache, taskdata, runqueue, providers as Providers
61
62 __version__ = "0.5.3.1"
63 __credits__ = """BitBake Shell Version %s (C) 2005 Michael 'Mickey' Lauer <mickey@Vanille.de>
64 Type 'help' for more information, press CTRL-D to exit.""" % __version__
65
66 cmds = {}
67 leave_mainloop = False
68 last_exception = None
69 cooker = None
70 parsed = False
71 debug = os.environ.get( "BBSHELL_DEBUG", "" )
72
73 ##########################################################################
74 # Class BitBakeShellCommands
75 ##########################################################################
76
77 class BitBakeShellCommands:
78     """This class contains the valid commands for the shell"""
79
80     def __init__( self, shell ):
81         """Register all the commands"""
82         self._shell = shell
83         for attr in BitBakeShellCommands.__dict__:
84             if not attr.startswith( "_" ):
85                 if attr.endswith( "_" ):
86                     command = attr[:-1].lower()
87                 else:
88                     command = attr[:].lower()
89                 method = getattr( BitBakeShellCommands, attr )
90                 debugOut( "registering command '%s'" % command )
91                 # scan number of arguments
92                 usage = getattr( method, "usage", "" )
93                 if usage != "<...>":
94                     numArgs = len( usage.split() )
95                 else:
96                     numArgs = -1
97                 shell.registerCommand( command, method, numArgs, "%s %s" % ( command, usage ), method.__doc__ )
98
99     def _checkParsed( self ):
100         if not parsed:
101             print "SHELL: This command needs to parse bbfiles..."
102             self.parse( None )
103
104     def _findProvider( self, item ):
105         self._checkParsed()
106         # Need to use taskData for this information
107         preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, cooker.configuration.data, 1 )
108         if not preferred: preferred = item
109         try:
110             lv, lf, pv, pf = Providers.findBestProvider(preferred, cooker.configuration.data, cooker.status)
111         except KeyError:
112             if item in cooker.status.providers:
113                 pf = cooker.status.providers[item][0]
114             else:
115                 pf = None
116         return pf
117
118     def alias( self, params ):
119         """Register a new name for a command"""
120         new, old = params
121         if not old in cmds:
122             print "ERROR: Command '%s' not known" % old
123         else:
124             cmds[new] = cmds[old]
125             print "OK"
126     alias.usage = "<alias> <command>"
127
128     def buffer( self, params ):
129         """Dump specified output buffer"""
130         index = params[0]
131         print self._shell.myout.buffer( int( index ) )
132     buffer.usage = "<index>"
133
134     def buffers( self, params ):
135         """Show the available output buffers"""
136         commands = self._shell.myout.bufferedCommands()
137         if not commands:
138             print "SHELL: No buffered commands available yet. Start doing something."
139         else:
140             print "="*35, "Available Output Buffers", "="*27
141             for index, cmd in enumerate( commands ):
142                 print "| %s %s" % ( str( index ).ljust( 3 ), cmd )
143             print "="*88
144
145     def build( self, params, cmd = "build" ):
146         """Build a providee"""
147         global last_exception
148         globexpr = params[0]
149         self._checkParsed()
150         names = globfilter( cooker.status.pkg_pn.keys(), globexpr )
151         if len( names ) == 0: names = [ globexpr ]
152         print "SHELL: Building %s" % ' '.join( names )
153
154         td = taskdata.TaskData(cooker.configuration.abort)
155         localdata = data.createCopy(cooker.configuration.data)
156         data.update_data(localdata)
157         data.expandKeys(localdata)
158
159         try:
160             tasks = []
161             for name in names:
162                 td.add_provider(localdata, cooker.status, name)
163                 providers = td.get_provider(name)
164
165                 if len(providers) == 0:
166                     raise Providers.NoProvider
167
168                 tasks.append([name, "do_%s" % cmd])
169
170             td.add_unresolved(localdata, cooker.status)
171             
172             rq = runqueue.RunQueue(cooker, localdata, cooker.status, td, tasks)
173             rq.prepare_runqueue()
174             rq.execute_runqueue()
175
176         except Providers.NoProvider:
177             print "ERROR: No Provider"
178             last_exception = Providers.NoProvider
179
180         except runqueue.TaskFailure, fnids:
181             for fnid in fnids:
182                 print "ERROR: '%s' failed" % td.fn_index[fnid]
183             last_exception = runqueue.TaskFailure
184
185         except build.EventException, e:
186             print "ERROR: Couldn't build '%s'" % names
187             last_exception = e
188
189
190     build.usage = "<providee>"
191
192     def clean( self, params ):
193         """Clean a providee"""
194         self.build( params, "clean" )
195     clean.usage = "<providee>"
196
197     def compile( self, params ):
198         """Execute 'compile' on a providee"""
199         self.build( params, "compile" )
200     compile.usage = "<providee>"
201
202     def configure( self, params ):
203         """Execute 'configure' on a providee"""
204         self.build( params, "configure" )
205     configure.usage = "<providee>"
206
207     def edit( self, params ):
208         """Call $EDITOR on a providee"""
209         name = params[0]
210         bbfile = self._findProvider( name )
211         if bbfile is not None:
212             os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), bbfile ) )
213         else:
214             print "ERROR: Nothing provides '%s'" % name
215     edit.usage = "<providee>"
216
217     def environment( self, params ):
218         """Dump out the outer BitBake environment"""
219         cooker.showEnvironment()
220
221     def exit_( self, params ):
222         """Leave the BitBake Shell"""
223         debugOut( "setting leave_mainloop to true" )
224         global leave_mainloop
225         leave_mainloop = True
226
227     def fetch( self, params ):
228         """Fetch a providee"""
229         self.build( params, "fetch" )
230     fetch.usage = "<providee>"
231
232     def fileBuild( self, params, cmd = "build" ):
233         """Parse and build a .bb file"""
234         global last_exception
235         name = params[0]
236         bf = completeFilePath( name )
237         print "SHELL: Calling '%s' on '%s'" % ( cmd, bf )
238
239         try:
240             cooker.buildFile(bf, cmd)
241         except parse.ParseError:
242             print "ERROR: Unable to open or parse '%s'" % bf
243         except build.EventException, e:
244             print "ERROR: Couldn't build '%s'" % name
245             last_exception = e
246
247     fileBuild.usage = "<bbfile>"
248
249     def fileClean( self, params ):
250         """Clean a .bb file"""
251         self.fileBuild( params, "clean" )
252     fileClean.usage = "<bbfile>"
253
254     def fileEdit( self, params ):
255         """Call $EDITOR on a .bb file"""
256         name = params[0]
257         os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), completeFilePath( name ) ) )
258     fileEdit.usage = "<bbfile>"
259
260     def fileRebuild( self, params ):
261         """Rebuild (clean & build) a .bb file"""
262         self.fileBuild( params, "rebuild" )
263     fileRebuild.usage = "<bbfile>"
264
265     def fileReparse( self, params ):
266         """(re)Parse a bb file"""
267         bbfile = params[0]
268         print "SHELL: Parsing '%s'" % bbfile
269         parse.update_mtime( bbfile )
270         cooker.bb_cache.cacheValidUpdate(bbfile)
271         fromCache = cooker.bb_cache.loadData(bbfile, cooker.configuration.data, cooker.status)
272         cooker.bb_cache.sync()
273         if False: #fromCache:
274             print "SHELL: File has not been updated, not reparsing"
275         else:
276             print "SHELL: Parsed"
277     fileReparse.usage = "<bbfile>"
278
279     def abort( self, params ):
280         """Toggle abort task execution flag (see bitbake -k)"""
281         cooker.configuration.abort = not cooker.configuration.abort
282         print "SHELL: Abort Flag is now '%s'" % repr( cooker.configuration.abort )
283
284     def force( self, params ):
285         """Toggle force task execution flag (see bitbake -f)"""
286         cooker.configuration.force = not cooker.configuration.force
287         print "SHELL: Force Flag is now '%s'" % repr( cooker.configuration.force )
288
289     def help( self, params ):
290         """Show a comprehensive list of commands and their purpose"""
291         print "="*30, "Available Commands", "="*30
292         allcmds = cmds.keys()
293         allcmds.sort()
294         for cmd in allcmds:
295             function,numparams,usage,helptext = cmds[cmd]
296             print "| %s | %s" % (usage.ljust(30), helptext)
297         print "="*78
298
299     def lastError( self, params ):
300         """Show the reason or log that was produced by the last BitBake event exception"""
301         if last_exception is None:
302             print "SHELL: No Errors yet (Phew)..."
303         else:
304             reason, event = last_exception.args
305             print "SHELL: Reason for the last error: '%s'" % reason
306             if ':' in reason:
307                 msg, filename = reason.split( ':' )
308                 filename = filename.strip()
309                 print "SHELL: Dumping log file for last error:"
310                 try:
311                     print open( filename ).read()
312                 except IOError:
313                     print "ERROR: Couldn't open '%s'" % filename
314
315     def match( self, params ):
316         """Dump all files or providers matching a glob expression"""
317         what, globexpr = params
318         if what == "files":
319             self._checkParsed()
320             for key in globfilter( cooker.status.pkg_fn.keys(), globexpr ): print key
321         elif what == "providers":
322             self._checkParsed()
323             for key in globfilter( cooker.status.pkg_pn.keys(), globexpr ): print key
324         else:
325             print "Usage: match %s" % self.print_.usage
326     match.usage = "<files|providers> <glob>"
327
328     def new( self, params ):
329         """Create a new .bb file and open the editor"""
330         dirname, filename = params
331         packages = '/'.join( data.getVar( "BBFILES", cooker.configuration.data, 1 ).split('/')[:-2] )
332         fulldirname = "%s/%s" % ( packages, dirname )
333
334         if not os.path.exists( fulldirname ):
335             print "SHELL: Creating '%s'" % fulldirname
336             os.mkdir( fulldirname )
337         if os.path.exists( fulldirname ) and os.path.isdir( fulldirname ):
338             if os.path.exists( "%s/%s" % ( fulldirname, filename ) ):
339                 print "SHELL: ERROR: %s/%s already exists" % ( fulldirname, filename )
340                 return False
341             print "SHELL: Creating '%s/%s'" % ( fulldirname, filename )
342             newpackage = open( "%s/%s" % ( fulldirname, filename ), "w" )
343             print >>newpackage,"""DESCRIPTION = ""
344 SECTION = ""
345 AUTHOR = ""
346 HOMEPAGE = ""
347 MAINTAINER = ""
348 LICENSE = "GPL"
349 PR = "r0"
350
351 SRC_URI = ""
352
353 #inherit base
354
355 #do_configure() {
356 #
357 #}
358
359 #do_compile() {
360 #
361 #}
362
363 #do_stage() {
364 #
365 #}
366
367 #do_install() {
368 #
369 #}
370 """
371             newpackage.close()
372             os.system( "%s %s/%s" % ( os.environ.get( "EDITOR" ), fulldirname, filename ) )
373     new.usage = "<directory> <filename>"
374
375     def package( self, params ):
376         """Execute 'package' on a providee"""
377         self.build( params, "package" )
378     package.usage = "<providee>"
379
380     def pasteBin( self, params ):
381         """Send a command + output buffer to the pastebin at http://rafb.net/paste"""
382         index = params[0]
383         contents = self._shell.myout.buffer( int( index ) )
384         sendToPastebin( "output of " + params[0], contents )
385     pasteBin.usage = "<index>"
386
387     def pasteLog( self, params ):
388         """Send the last event exception error log (if there is one) to http://rafb.net/paste"""
389         if last_exception is None:
390             print "SHELL: No Errors yet (Phew)..."
391         else:
392             reason, event = last_exception.args
393             print "SHELL: Reason for the last error: '%s'" % reason
394             if ':' in reason:
395                 msg, filename = reason.split( ':' )
396                 filename = filename.strip()
397                 print "SHELL: Pasting log file to pastebin..."
398
399                 file = open( filename ).read()
400                 sendToPastebin( "contents of " + filename, file )
401
402     def patch( self, params ):
403         """Execute 'patch' command on a providee"""
404         self.build( params, "patch" )
405     patch.usage = "<providee>"
406
407     def parse( self, params ):
408         """(Re-)parse .bb files and calculate the dependency graph"""
409         cooker.status = cache.CacheData()
410         ignore = data.getVar("ASSUME_PROVIDED", cooker.configuration.data, 1) or ""
411         cooker.status.ignored_dependencies = set( ignore.split() )
412         cooker.handleCollections( data.getVar("BBFILE_COLLECTIONS", cooker.configuration.data, 1) )
413
414         (filelist, masked) = cooker.collect_bbfiles()
415         cooker.parse_bbfiles(filelist, masked, cooker.myProgressCallback)
416         cooker.buildDepgraph()
417         global parsed
418         parsed = True
419         print
420
421     def reparse( self, params ):
422         """(re)Parse a providee's bb file"""
423         bbfile = self._findProvider( params[0] )
424         if bbfile is not None:
425             print "SHELL: Found bbfile '%s' for '%s'" % ( bbfile, params[0] )
426             self.fileReparse( [ bbfile ] )
427         else:
428             print "ERROR: Nothing provides '%s'" % params[0]
429     reparse.usage = "<providee>"
430
431     def getvar( self, params ):
432         """Dump the contents of an outer BitBake environment variable"""
433         var = params[0]
434         value = data.getVar( var, cooker.configuration.data, 1 )
435         print value
436     getvar.usage = "<variable>"
437
438     def peek( self, params ):
439         """Dump contents of variable defined in providee's metadata"""
440         name, var = params
441         bbfile = self._findProvider( name )
442         if bbfile is not None:
443             the_data = cooker.bb_cache.loadDataFull(bbfile, cooker.configuration.data)
444             value = the_data.getVar( var, 1 )
445             print value
446         else:
447             print "ERROR: Nothing provides '%s'" % name
448     peek.usage = "<providee> <variable>"
449
450     def poke( self, params ):
451         """Set contents of variable defined in providee's metadata"""
452         name, var, value = params
453         bbfile = self._findProvider( name )
454         if bbfile is not None:
455             print "ERROR: Sorry, this functionality is currently broken"
456             #d = cooker.pkgdata[bbfile]
457             #data.setVar( var, value, d )
458
459             # mark the change semi persistant
460             #cooker.pkgdata.setDirty(bbfile, d)
461             #print "OK"
462         else:
463             print "ERROR: Nothing provides '%s'" % name
464     poke.usage = "<providee> <variable> <value>"
465
466     def print_( self, params ):
467         """Dump all files or providers"""
468         what = params[0]
469         if what == "files":
470             self._checkParsed()
471             for key in cooker.status.pkg_fn.keys(): print key
472         elif what == "providers":
473             self._checkParsed()
474             for key in cooker.status.providers.keys(): print key
475         else:
476             print "Usage: print %s" % self.print_.usage
477     print_.usage = "<files|providers>"
478
479     def python( self, params ):
480         """Enter the expert mode - an interactive BitBake Python Interpreter"""
481         sys.ps1 = "EXPERT BB>>> "
482         sys.ps2 = "EXPERT BB... "
483         import code
484         interpreter = code.InteractiveConsole( dict( globals() ) )
485         interpreter.interact( "SHELL: Expert Mode - BitBake Python %s\nType 'help' for more information, press CTRL-D to switch back to BBSHELL." % sys.version )
486
487     def showdata( self, params ):
488         """Execute 'showdata' on a providee"""
489         cooker.showEnvironment(None, params)
490     showdata.usage = "<providee>"
491
492     def setVar( self, params ):
493         """Set an outer BitBake environment variable"""
494         var, value = params
495         data.setVar( var, value, cooker.configuration.data )
496         print "OK"
497     setVar.usage = "<variable> <value>"
498
499     def rebuild( self, params ):
500         """Clean and rebuild a .bb file or a providee"""
501         self.build( params, "clean" )
502         self.build( params, "build" )
503     rebuild.usage = "<providee>"
504
505     def shell( self, params ):
506         """Execute a shell command and dump the output"""
507         if params != "":
508             print commands.getoutput( " ".join( params ) )
509     shell.usage = "<...>"
510
511     def stage( self, params ):
512         """Execute 'stage' on a providee"""
513         self.build( params, "populate_staging" )
514     stage.usage = "<providee>"
515
516     def status( self, params ):
517         """<just for testing>"""
518         print "-" * 78
519         print "building list = '%s'" % cooker.building_list
520         print "build path = '%s'" % cooker.build_path
521         print "consider_msgs_cache = '%s'" % cooker.consider_msgs_cache
522         print "build stats = '%s'" % cooker.stats
523         if last_exception is not None: print "last_exception = '%s'" % repr( last_exception.args )
524         print "memory output contents = '%s'" % self._shell.myout._buffer
525
526     def test( self, params ):
527         """<just for testing>"""
528         print "testCommand called with '%s'" % params
529
530     def unpack( self, params ):
531         """Execute 'unpack' on a providee"""
532         self.build( params, "unpack" )
533     unpack.usage = "<providee>"
534
535     def which( self, params ):
536         """Computes the providers for a given providee"""
537         # Need to use taskData for this information
538         item = params[0]
539
540         self._checkParsed()
541
542         preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, cooker.configuration.data, 1 )
543         if not preferred: preferred = item
544
545         try:
546             lv, lf, pv, pf = Providers.findBestProvider(preferred, cooker.configuration.data, cooker.status)
547         except KeyError:
548             lv, lf, pv, pf = (None,)*4
549
550         try:
551             providers = cooker.status.providers[item]
552         except KeyError:
553             print "SHELL: ERROR: Nothing provides", preferred
554         else:
555             for provider in providers:
556                 if provider == pf: provider = " (***) %s" % provider
557                 else:              provider = "       %s" % provider
558                 print provider
559     which.usage = "<providee>"
560
561 ##########################################################################
562 # Common helper functions
563 ##########################################################################
564
565 def completeFilePath( bbfile ):
566     """Get the complete bbfile path"""
567     if not cooker.status: return bbfile
568     if not cooker.status.pkg_fn: return bbfile
569     for key in cooker.status.pkg_fn.keys():
570         if key.endswith( bbfile ):
571             return key
572     return bbfile
573
574 def sendToPastebin( desc, content ):
575     """Send content to http://oe.pastebin.com"""
576     mydata = {}
577     mydata["lang"] = "Plain Text"
578     mydata["desc"] = desc
579     mydata["cvt_tabs"] = "No"
580     mydata["nick"] = "%s@%s" % ( os.environ.get( "USER", "unknown" ), socket.gethostname() or "unknown" )
581     mydata["text"] = content
582     params = urllib.urlencode( mydata )
583     headers = {"Content-type": "application/x-www-form-urlencoded","Accept": "text/plain"}
584
585     host = "rafb.net"
586     conn = httplib.HTTPConnection( "%s:80" % host )
587     conn.request("POST", "/paste/paste.php", params, headers )
588
589     response = conn.getresponse()
590     conn.close()
591
592     if response.status == 302:
593         location = response.getheader( "location" ) or "unknown"
594         print "SHELL: Pasted to http://%s%s" % ( host, location )
595     else:
596         print "ERROR: %s %s" % ( response.status, response.reason )
597
598 def completer( text, state ):
599     """Return a possible readline completion"""
600     debugOut( "completer called with text='%s', state='%d'" % ( text, state ) )
601
602     if state == 0:
603         line = readline.get_line_buffer()
604         if " " in line:
605             line = line.split()
606             # we are in second (or more) argument
607             if line[0] in cmds and hasattr( cmds[line[0]][0], "usage" ): # known command and usage
608                 u = getattr( cmds[line[0]][0], "usage" ).split()[0]
609                 if u == "<variable>":
610                     allmatches = cooker.configuration.data.keys()
611                 elif u == "<bbfile>":
612                     if cooker.status.pkg_fn is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
613                     else: allmatches = [ x.split("/")[-1] for x in cooker.status.pkg_fn.keys() ]
614                 elif u == "<providee>":
615                     if cooker.status.pkg_fn is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
616                     else: allmatches = cooker.status.providers.iterkeys()
617                 else: allmatches = [ "(No tab completion available for this command)" ]
618             else: allmatches = [ "(No tab completion available for this command)" ]
619         else:
620             # we are in first argument
621             allmatches = cmds.iterkeys()
622
623         completer.matches = [ x for x in allmatches if x[:len(text)] == text ]
624         #print "completer.matches = '%s'" % completer.matches
625     if len( completer.matches ) > state:
626         return completer.matches[state]
627     else:
628         return None
629
630 def debugOut( text ):
631     if debug:
632         sys.stderr.write( "( %s )\n" % text )
633
634 def columnize( alist, width = 80 ):
635     """
636     A word-wrap function that preserves existing line breaks
637     and most spaces in the text. Expects that existing line
638     breaks are posix newlines (\n).
639     """
640     return reduce(lambda line, word, width=width: '%s%s%s' %
641                   (line,
642                    ' \n'[(len(line[line.rfind('\n')+1:])
643                          + len(word.split('\n',1)[0]
644                               ) >= width)],
645                    word),
646                   alist
647                  )
648
649 def globfilter( names, pattern ):
650     return fnmatch.filter( names, pattern )
651
652 ##########################################################################
653 # Class MemoryOutput
654 ##########################################################################
655
656 class MemoryOutput:
657     """File-like output class buffering the output of the last 10 commands"""
658     def __init__( self, delegate ):
659         self.delegate = delegate
660         self._buffer = []
661         self.text = []
662         self._command = None
663
664     def startCommand( self, command ):
665         self._command = command
666         self.text = []
667     def endCommand( self ):
668         if self._command is not None:
669             if len( self._buffer ) == 10: del self._buffer[0]
670             self._buffer.append( ( self._command, self.text ) )
671     def removeLast( self ):
672         if self._buffer:
673             del self._buffer[ len( self._buffer ) - 1 ]
674         self.text = []
675         self._command = None
676     def lastBuffer( self ):
677         if self._buffer:
678             return self._buffer[ len( self._buffer ) -1 ][1]
679     def bufferedCommands( self ):
680         return [ cmd for cmd, output in self._buffer ]
681     def buffer( self, i ):
682         if i < len( self._buffer ):
683             return "BB>> %s\n%s" % ( self._buffer[i][0], "".join( self._buffer[i][1] ) )
684         else: return "ERROR: Invalid buffer number. Buffer needs to be in (0, %d)" % ( len( self._buffer ) - 1 )
685     def write( self, text ):
686         if self._command is not None and text != "BB>> ": self.text.append( text )
687         if self.delegate is not None: self.delegate.write( text )
688     def flush( self ):
689         return self.delegate.flush()
690     def fileno( self ):
691         return self.delegate.fileno()
692     def isatty( self ):
693         return self.delegate.isatty()
694
695 ##########################################################################
696 # Class BitBakeShell
697 ##########################################################################
698
699 class BitBakeShell:
700
701     def __init__( self ):
702         """Register commands and set up readline"""
703         self.commandQ = Queue.Queue()
704         self.commands = BitBakeShellCommands( self )
705         self.myout = MemoryOutput( sys.stdout )
706         self.historyfilename = os.path.expanduser( "~/.bbsh_history" )
707         self.startupfilename = os.path.expanduser( "~/.bbsh_startup" )
708
709         readline.set_completer( completer )
710         readline.set_completer_delims( " " )
711         readline.parse_and_bind("tab: complete")
712
713         try:
714             readline.read_history_file( self.historyfilename )
715         except IOError:
716             pass  # It doesn't exist yet.
717
718         print __credits__
719
720     def cleanup( self ):
721         """Write readline history and clean up resources"""
722         debugOut( "writing command history" )
723         try:
724             readline.write_history_file( self.historyfilename )
725         except:
726             print "SHELL: Unable to save command history"
727
728     def registerCommand( self, command, function, numparams = 0, usage = "", helptext = "" ):
729         """Register a command"""
730         if usage == "": usage = command
731         if helptext == "": helptext = function.__doc__ or "<not yet documented>"
732         cmds[command] = ( function, numparams, usage, helptext )
733
734     def processCommand( self, command, params ):
735         """Process a command. Check number of params and print a usage string, if appropriate"""
736         debugOut( "processing command '%s'..." % command )
737         try:
738             function, numparams, usage, helptext = cmds[command]
739         except KeyError:
740             print "SHELL: ERROR: '%s' command is not a valid command." % command
741             self.myout.removeLast()
742         else:
743             if (numparams != -1) and (not len( params ) == numparams):
744                 print "Usage: '%s'" % usage
745                 return
746
747             result = function( self.commands, params )
748             debugOut( "result was '%s'" % result )
749
750     def processStartupFile( self ):
751         """Read and execute all commands found in $HOME/.bbsh_startup"""
752         if os.path.exists( self.startupfilename ):
753             startupfile = open( self.startupfilename, "r" )
754             for cmdline in startupfile:
755                 debugOut( "processing startup line '%s'" % cmdline )
756                 if not cmdline:
757                     continue
758                 if "|" in cmdline:
759                     print "ERROR: '|' in startup file is not allowed. Ignoring line"
760                     continue
761                 self.commandQ.put( cmdline.strip() )
762
763     def main( self ):
764         """The main command loop"""
765         while not leave_mainloop:
766             try:
767                 if self.commandQ.empty():
768                     sys.stdout = self.myout.delegate
769                     cmdline = raw_input( "BB>> " )
770                     sys.stdout = self.myout
771                 else:
772                     cmdline = self.commandQ.get()
773                 if cmdline:
774                     allCommands = cmdline.split( ';' )
775                     for command in allCommands:
776                         pipecmd = None
777                         #
778                         # special case for expert mode
779                         if command == 'python':
780                             sys.stdout = self.myout.delegate
781                             self.processCommand( command, "" )
782                             sys.stdout = self.myout
783                         else:
784                             self.myout.startCommand( command )
785                             if '|' in command: # disable output
786                                 command, pipecmd = command.split( '|' )
787                                 delegate = self.myout.delegate
788                                 self.myout.delegate = None
789                             tokens = shlex.split( command, True )
790                             self.processCommand( tokens[0], tokens[1:] or "" )
791                             self.myout.endCommand()
792                             if pipecmd is not None: # restore output
793                                 self.myout.delegate = delegate
794
795                                 pipe = popen2.Popen4( pipecmd )
796                                 pipe.tochild.write( "\n".join( self.myout.lastBuffer() ) )
797                                 pipe.tochild.close()
798                                 sys.stdout.write( pipe.fromchild.read() )
799                         #
800             except EOFError:
801                 print
802                 return
803             except KeyboardInterrupt:
804                 print
805
806 ##########################################################################
807 # Start function - called from the BitBake command line utility
808 ##########################################################################
809
810 def start( aCooker ):
811     global cooker
812     cooker = aCooker
813     bbshell = BitBakeShell()
814     bbshell.processStartupFile()
815     bbshell.main()
816     bbshell.cleanup()
817
818 if __name__ == "__main__":
819     print "SHELL: Sorry, this program should only be called by BitBake."