shell.py: Update to use cooker.buildFile()
[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         oldcmd = cooker.configuration.cmd
155         cooker.configuration.cmd = cmd
156
157         td = taskdata.TaskData(cooker.configuration.abort)
158         localdata = data.createCopy(cooker.configuration.data)
159         data.update_data(localdata)
160         data.expandKeys(localdata)
161
162         try:
163             tasks = []
164             for name in names:
165                 td.add_provider(localdata, cooker.status, name)
166                 providers = td.get_provider(name)
167
168                 if len(providers) == 0:
169                     raise Providers.NoProvider
170
171                 tasks.append([name, "do_%s" % cooker.configuration.cmd])
172
173             td.add_unresolved(localdata, cooker.status)
174             
175             rq = runqueue.RunQueue(cooker, localdata, cooker.status, td, tasks)
176             rq.prepare_runqueue()
177             rq.execute_runqueue()
178
179         except Providers.NoProvider:
180             print "ERROR: No Provider"
181             last_exception = Providers.NoProvider
182
183         except runqueue.TaskFailure, fnids:
184             for fnid in fnids:
185                 print "ERROR: '%s' failed" % td.fn_index[fnid]
186             last_exception = runqueue.TaskFailure
187
188         except build.EventException, e:
189             print "ERROR: Couldn't build '%s'" % names
190             last_exception = e
191
192         cooker.configuration.cmd = oldcmd
193
194     build.usage = "<providee>"
195
196     def clean( self, params ):
197         """Clean a providee"""
198         self.build( params, "clean" )
199     clean.usage = "<providee>"
200
201     def compile( self, params ):
202         """Execute 'compile' on a providee"""
203         self.build( params, "compile" )
204     compile.usage = "<providee>"
205
206     def configure( self, params ):
207         """Execute 'configure' on a providee"""
208         self.build( params, "configure" )
209     configure.usage = "<providee>"
210
211     def edit( self, params ):
212         """Call $EDITOR on a providee"""
213         name = params[0]
214         bbfile = self._findProvider( name )
215         if bbfile is not None:
216             os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), bbfile ) )
217         else:
218             print "ERROR: Nothing provides '%s'" % name
219     edit.usage = "<providee>"
220
221     def environment( self, params ):
222         """Dump out the outer BitBake environment"""
223         cooker.showEnvironment()
224
225     def exit_( self, params ):
226         """Leave the BitBake Shell"""
227         debugOut( "setting leave_mainloop to true" )
228         global leave_mainloop
229         leave_mainloop = True
230
231     def fetch( self, params ):
232         """Fetch a providee"""
233         self.build( params, "fetch" )
234     fetch.usage = "<providee>"
235
236     def fileBuild( self, params, cmd = "build" ):
237         """Parse and build a .bb file"""
238         global last_exception
239         name = params[0]
240         bf = completeFilePath( name )
241         print "SHELL: Calling '%s' on '%s'" % ( cmd, bf )
242
243         oldcmd = cooker.configuration.cmd
244         cooker.configuration.cmd = cmd
245
246         try:
247             cooker.buildFile(bf)
248         except parse.ParseError:
249             print "ERROR: Unable to open or parse '%s'" % bf
250         except build.EventException, e:
251             print "ERROR: Couldn't build '%s'" % name
252             last_exception = e
253
254         cooker.configuration.cmd = oldcmd
255     fileBuild.usage = "<bbfile>"
256
257     def fileClean( self, params ):
258         """Clean a .bb file"""
259         self.fileBuild( params, "clean" )
260     fileClean.usage = "<bbfile>"
261
262     def fileEdit( self, params ):
263         """Call $EDITOR on a .bb file"""
264         name = params[0]
265         os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), completeFilePath( name ) ) )
266     fileEdit.usage = "<bbfile>"
267
268     def fileRebuild( self, params ):
269         """Rebuild (clean & build) a .bb file"""
270         self.fileBuild( params, "rebuild" )
271     fileRebuild.usage = "<bbfile>"
272
273     def fileReparse( self, params ):
274         """(re)Parse a bb file"""
275         bbfile = params[0]
276         print "SHELL: Parsing '%s'" % bbfile
277         parse.update_mtime( bbfile )
278         cooker.bb_cache.cacheValidUpdate(bbfile)
279         fromCache = cooker.bb_cache.loadData(bbfile, cooker.configuration.data)
280         cooker.bb_cache.sync()
281         if False: #fromCache:
282             print "SHELL: File has not been updated, not reparsing"
283         else:
284             print "SHELL: Parsed"
285     fileReparse.usage = "<bbfile>"
286
287     def abort( self, params ):
288         """Toggle abort task execution flag (see bitbake -k)"""
289         cooker.configuration.abort = not cooker.configuration.abort
290         print "SHELL: Abort Flag is now '%s'" % repr( cooker.configuration.abort )
291
292     def force( self, params ):
293         """Toggle force task execution flag (see bitbake -f)"""
294         cooker.configuration.force = not cooker.configuration.force
295         print "SHELL: Force Flag is now '%s'" % repr( cooker.configuration.force )
296
297     def help( self, params ):
298         """Show a comprehensive list of commands and their purpose"""
299         print "="*30, "Available Commands", "="*30
300         allcmds = cmds.keys()
301         allcmds.sort()
302         for cmd in allcmds:
303             function,numparams,usage,helptext = cmds[cmd]
304             print "| %s | %s" % (usage.ljust(30), helptext)
305         print "="*78
306
307     def lastError( self, params ):
308         """Show the reason or log that was produced by the last BitBake event exception"""
309         if last_exception is None:
310             print "SHELL: No Errors yet (Phew)..."
311         else:
312             reason, event = last_exception.args
313             print "SHELL: Reason for the last error: '%s'" % reason
314             if ':' in reason:
315                 msg, filename = reason.split( ':' )
316                 filename = filename.strip()
317                 print "SHELL: Dumping log file for last error:"
318                 try:
319                     print open( filename ).read()
320                 except IOError:
321                     print "ERROR: Couldn't open '%s'" % filename
322
323     def match( self, params ):
324         """Dump all files or providers matching a glob expression"""
325         what, globexpr = params
326         if what == "files":
327             self._checkParsed()
328             for key in globfilter( cooker.status.pkg_fn.keys(), globexpr ): print key
329         elif what == "providers":
330             self._checkParsed()
331             for key in globfilter( cooker.status.pkg_pn.keys(), globexpr ): print key
332         else:
333             print "Usage: match %s" % self.print_.usage
334     match.usage = "<files|providers> <glob>"
335
336     def new( self, params ):
337         """Create a new .bb file and open the editor"""
338         dirname, filename = params
339         packages = '/'.join( data.getVar( "BBFILES", cooker.configuration.data, 1 ).split('/')[:-2] )
340         fulldirname = "%s/%s" % ( packages, dirname )
341
342         if not os.path.exists( fulldirname ):
343             print "SHELL: Creating '%s'" % fulldirname
344             os.mkdir( fulldirname )
345         if os.path.exists( fulldirname ) and os.path.isdir( fulldirname ):
346             if os.path.exists( "%s/%s" % ( fulldirname, filename ) ):
347                 print "SHELL: ERROR: %s/%s already exists" % ( fulldirname, filename )
348                 return False
349             print "SHELL: Creating '%s/%s'" % ( fulldirname, filename )
350             newpackage = open( "%s/%s" % ( fulldirname, filename ), "w" )
351             print >>newpackage,"""DESCRIPTION = ""
352 SECTION = ""
353 AUTHOR = ""
354 HOMEPAGE = ""
355 MAINTAINER = ""
356 LICENSE = "GPL"
357 PR = "r0"
358
359 SRC_URI = ""
360
361 #inherit base
362
363 #do_configure() {
364 #
365 #}
366
367 #do_compile() {
368 #
369 #}
370
371 #do_stage() {
372 #
373 #}
374
375 #do_install() {
376 #
377 #}
378 """
379             newpackage.close()
380             os.system( "%s %s/%s" % ( os.environ.get( "EDITOR" ), fulldirname, filename ) )
381     new.usage = "<directory> <filename>"
382
383     def package( self, params ):
384         """Execute 'package' on a providee"""
385         self.build( params, "package" )
386     package.usage = "<providee>"
387
388     def pasteBin( self, params ):
389         """Send a command + output buffer to the pastebin at http://rafb.net/paste"""
390         index = params[0]
391         contents = self._shell.myout.buffer( int( index ) )
392         sendToPastebin( "output of " + params[0], contents )
393     pasteBin.usage = "<index>"
394
395     def pasteLog( self, params ):
396         """Send the last event exception error log (if there is one) to http://rafb.net/paste"""
397         if last_exception is None:
398             print "SHELL: No Errors yet (Phew)..."
399         else:
400             reason, event = last_exception.args
401             print "SHELL: Reason for the last error: '%s'" % reason
402             if ':' in reason:
403                 msg, filename = reason.split( ':' )
404                 filename = filename.strip()
405                 print "SHELL: Pasting log file to pastebin..."
406
407                 file = open( filename ).read()
408                 sendToPastebin( "contents of " + filename, file )
409
410     def patch( self, params ):
411         """Execute 'patch' command on a providee"""
412         self.build( params, "patch" )
413     patch.usage = "<providee>"
414
415     def parse( self, params ):
416         """(Re-)parse .bb files and calculate the dependency graph"""
417         cooker.status = cache.CacheData()
418         ignore = data.getVar("ASSUME_PROVIDED", cooker.configuration.data, 1) or ""
419         cooker.status.ignored_dependencies = set( ignore.split() )
420         cooker.handleCollections( data.getVar("BBFILE_COLLECTIONS", cooker.configuration.data, 1) )
421
422         (filelist, masked) = cooker.collect_bbfiles()
423         cooker.parse_bbfiles(filelist, masked, cooker.myProgressCallback)
424         cooker.buildDepgraph()
425         global parsed
426         parsed = True
427         print
428
429     def reparse( self, params ):
430         """(re)Parse a providee's bb file"""
431         bbfile = self._findProvider( params[0] )
432         if bbfile is not None:
433             print "SHELL: Found bbfile '%s' for '%s'" % ( bbfile, params[0] )
434             self.fileReparse( [ bbfile ] )
435         else:
436             print "ERROR: Nothing provides '%s'" % params[0]
437     reparse.usage = "<providee>"
438
439     def getvar( self, params ):
440         """Dump the contents of an outer BitBake environment variable"""
441         var = params[0]
442         value = data.getVar( var, cooker.configuration.data, 1 )
443         print value
444     getvar.usage = "<variable>"
445
446     def peek( self, params ):
447         """Dump contents of variable defined in providee's metadata"""
448         name, var = params
449         bbfile = self._findProvider( name )
450         if bbfile is not None:
451             the_data = cooker.bb_cache.loadDataFull(bbfile, cooker.configuration.data)
452             value = the_data.getVar( var, 1 )
453             print value
454         else:
455             print "ERROR: Nothing provides '%s'" % name
456     peek.usage = "<providee> <variable>"
457
458     def poke( self, params ):
459         """Set contents of variable defined in providee's metadata"""
460         name, var, value = params
461         bbfile = self._findProvider( name )
462         if bbfile is not None:
463             print "ERROR: Sorry, this functionality is currently broken"
464             #d = cooker.pkgdata[bbfile]
465             #data.setVar( var, value, d )
466
467             # mark the change semi persistant
468             #cooker.pkgdata.setDirty(bbfile, d)
469             #print "OK"
470         else:
471             print "ERROR: Nothing provides '%s'" % name
472     poke.usage = "<providee> <variable> <value>"
473
474     def print_( self, params ):
475         """Dump all files or providers"""
476         what = params[0]
477         if what == "files":
478             self._checkParsed()
479             for key in cooker.status.pkg_fn.keys(): print key
480         elif what == "providers":
481             self._checkParsed()
482             for key in cooker.status.providers.keys(): print key
483         else:
484             print "Usage: print %s" % self.print_.usage
485     print_.usage = "<files|providers>"
486
487     def python( self, params ):
488         """Enter the expert mode - an interactive BitBake Python Interpreter"""
489         sys.ps1 = "EXPERT BB>>> "
490         sys.ps2 = "EXPERT BB... "
491         import code
492         interpreter = code.InteractiveConsole( dict( globals() ) )
493         interpreter.interact( "SHELL: Expert Mode - BitBake Python %s\nType 'help' for more information, press CTRL-D to switch back to BBSHELL." % sys.version )
494
495     def showdata( self, params ):
496         """Show the parsed metadata for a given providee"""
497         cooker.showEnvironment(None, params)
498     showdata.usage = "<providee>"
499
500     def setVar( self, params ):
501         """Set an outer BitBake environment variable"""
502         var, value = params
503         data.setVar( var, value, cooker.configuration.data )
504         print "OK"
505     setVar.usage = "<variable> <value>"
506
507     def rebuild( self, params ):
508         """Clean and rebuild a .bb file or a providee"""
509         self.build( params, "clean" )
510         self.build( params, "build" )
511     rebuild.usage = "<providee>"
512
513     def shell( self, params ):
514         """Execute a shell command and dump the output"""
515         if params != "":
516             print commands.getoutput( " ".join( params ) )
517     shell.usage = "<...>"
518
519     def stage( self, params ):
520         """Execute 'stage' on a providee"""
521         self.build( params, "stage" )
522     stage.usage = "<providee>"
523
524     def status( self, params ):
525         """<just for testing>"""
526         print "-" * 78
527         print "building list = '%s'" % cooker.building_list
528         print "build path = '%s'" % cooker.build_path
529         print "consider_msgs_cache = '%s'" % cooker.consider_msgs_cache
530         print "build stats = '%s'" % cooker.stats
531         if last_exception is not None: print "last_exception = '%s'" % repr( last_exception.args )
532         print "memory output contents = '%s'" % self._shell.myout._buffer
533
534     def test( self, params ):
535         """<just for testing>"""
536         print "testCommand called with '%s'" % params
537
538     def unpack( self, params ):
539         """Execute 'unpack' on a providee"""
540         self.build( params, "unpack" )
541     unpack.usage = "<providee>"
542
543     def which( self, params ):
544         """Computes the providers for a given providee"""
545         # Need to use taskData for this information
546         item = params[0]
547
548         self._checkParsed()
549
550         preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, cooker.configuration.data, 1 )
551         if not preferred: preferred = item
552
553         try:
554             lv, lf, pv, pf = Providers.findBestProvider(preferred, cooker.configuration.data, cooker.status)
555         except KeyError:
556             lv, lf, pv, pf = (None,)*4
557
558         try:
559             providers = cooker.status.providers[item]
560         except KeyError:
561             print "SHELL: ERROR: Nothing provides", preferred
562         else:
563             for provider in providers:
564                 if provider == pf: provider = " (***) %s" % provider
565                 else:              provider = "       %s" % provider
566                 print provider
567     which.usage = "<providee>"
568
569 ##########################################################################
570 # Common helper functions
571 ##########################################################################
572
573 def completeFilePath( bbfile ):
574     """Get the complete bbfile path"""
575     if not cooker.status: return bbfile
576     if not cooker.status.pkg_fn: return bbfile
577     for key in cooker.status.pkg_fn.keys():
578         if key.endswith( bbfile ):
579             return key
580     return bbfile
581
582 def sendToPastebin( desc, content ):
583     """Send content to http://oe.pastebin.com"""
584     mydata = {}
585     mydata["lang"] = "Plain Text"
586     mydata["desc"] = desc
587     mydata["cvt_tabs"] = "No"
588     mydata["nick"] = "%s@%s" % ( os.environ.get( "USER", "unknown" ), socket.gethostname() or "unknown" )
589     mydata["text"] = content
590     params = urllib.urlencode( mydata )
591     headers = {"Content-type": "application/x-www-form-urlencoded","Accept": "text/plain"}
592
593     host = "rafb.net"
594     conn = httplib.HTTPConnection( "%s:80" % host )
595     conn.request("POST", "/paste/paste.php", params, headers )
596
597     response = conn.getresponse()
598     conn.close()
599
600     if response.status == 302:
601         location = response.getheader( "location" ) or "unknown"
602         print "SHELL: Pasted to http://%s%s" % ( host, location )
603     else:
604         print "ERROR: %s %s" % ( response.status, response.reason )
605
606 def completer( text, state ):
607     """Return a possible readline completion"""
608     debugOut( "completer called with text='%s', state='%d'" % ( text, state ) )
609
610     if state == 0:
611         line = readline.get_line_buffer()
612         if " " in line:
613             line = line.split()
614             # we are in second (or more) argument
615             if line[0] in cmds and hasattr( cmds[line[0]][0], "usage" ): # known command and usage
616                 u = getattr( cmds[line[0]][0], "usage" ).split()[0]
617                 if u == "<variable>":
618                     allmatches = cooker.configuration.data.keys()
619                 elif u == "<bbfile>":
620                     if cooker.status.pkg_fn is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
621                     else: allmatches = [ x.split("/")[-1] for x in cooker.status.pkg_fn.keys() ]
622                 elif u == "<providee>":
623                     if cooker.status.pkg_fn is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
624                     else: allmatches = cooker.status.providers.iterkeys()
625                 else: allmatches = [ "(No tab completion available for this command)" ]
626             else: allmatches = [ "(No tab completion available for this command)" ]
627         else:
628             # we are in first argument
629             allmatches = cmds.iterkeys()
630
631         completer.matches = [ x for x in allmatches if x[:len(text)] == text ]
632         #print "completer.matches = '%s'" % completer.matches
633     if len( completer.matches ) > state:
634         return completer.matches[state]
635     else:
636         return None
637
638 def debugOut( text ):
639     if debug:
640         sys.stderr.write( "( %s )\n" % text )
641
642 def columnize( alist, width = 80 ):
643     """
644     A word-wrap function that preserves existing line breaks
645     and most spaces in the text. Expects that existing line
646     breaks are posix newlines (\n).
647     """
648     return reduce(lambda line, word, width=width: '%s%s%s' %
649                   (line,
650                    ' \n'[(len(line[line.rfind('\n')+1:])
651                          + len(word.split('\n',1)[0]
652                               ) >= width)],
653                    word),
654                   alist
655                  )
656
657 def globfilter( names, pattern ):
658     return fnmatch.filter( names, pattern )
659
660 ##########################################################################
661 # Class MemoryOutput
662 ##########################################################################
663
664 class MemoryOutput:
665     """File-like output class buffering the output of the last 10 commands"""
666     def __init__( self, delegate ):
667         self.delegate = delegate
668         self._buffer = []
669         self.text = []
670         self._command = None
671
672     def startCommand( self, command ):
673         self._command = command
674         self.text = []
675     def endCommand( self ):
676         if self._command is not None:
677             if len( self._buffer ) == 10: del self._buffer[0]
678             self._buffer.append( ( self._command, self.text ) )
679     def removeLast( self ):
680         if self._buffer:
681             del self._buffer[ len( self._buffer ) - 1 ]
682         self.text = []
683         self._command = None
684     def lastBuffer( self ):
685         if self._buffer:
686             return self._buffer[ len( self._buffer ) -1 ][1]
687     def bufferedCommands( self ):
688         return [ cmd for cmd, output in self._buffer ]
689     def buffer( self, i ):
690         if i < len( self._buffer ):
691             return "BB>> %s\n%s" % ( self._buffer[i][0], "".join( self._buffer[i][1] ) )
692         else: return "ERROR: Invalid buffer number. Buffer needs to be in (0, %d)" % ( len( self._buffer ) - 1 )
693     def write( self, text ):
694         if self._command is not None and text != "BB>> ": self.text.append( text )
695         if self.delegate is not None: self.delegate.write( text )
696     def flush( self ):
697         return self.delegate.flush()
698     def fileno( self ):
699         return self.delegate.fileno()
700     def isatty( self ):
701         return self.delegate.isatty()
702
703 ##########################################################################
704 # Class BitBakeShell
705 ##########################################################################
706
707 class BitBakeShell:
708
709     def __init__( self ):
710         """Register commands and set up readline"""
711         self.commandQ = Queue.Queue()
712         self.commands = BitBakeShellCommands( self )
713         self.myout = MemoryOutput( sys.stdout )
714         self.historyfilename = os.path.expanduser( "~/.bbsh_history" )
715         self.startupfilename = os.path.expanduser( "~/.bbsh_startup" )
716
717         readline.set_completer( completer )
718         readline.set_completer_delims( " " )
719         readline.parse_and_bind("tab: complete")
720
721         try:
722             readline.read_history_file( self.historyfilename )
723         except IOError:
724             pass  # It doesn't exist yet.
725
726         print __credits__
727
728     def cleanup( self ):
729         """Write readline history and clean up resources"""
730         debugOut( "writing command history" )
731         try:
732             readline.write_history_file( self.historyfilename )
733         except:
734             print "SHELL: Unable to save command history"
735
736     def registerCommand( self, command, function, numparams = 0, usage = "", helptext = "" ):
737         """Register a command"""
738         if usage == "": usage = command
739         if helptext == "": helptext = function.__doc__ or "<not yet documented>"
740         cmds[command] = ( function, numparams, usage, helptext )
741
742     def processCommand( self, command, params ):
743         """Process a command. Check number of params and print a usage string, if appropriate"""
744         debugOut( "processing command '%s'..." % command )
745         try:
746             function, numparams, usage, helptext = cmds[command]
747         except KeyError:
748             print "SHELL: ERROR: '%s' command is not a valid command." % command
749             self.myout.removeLast()
750         else:
751             if (numparams != -1) and (not len( params ) == numparams):
752                 print "Usage: '%s'" % usage
753                 return
754
755             result = function( self.commands, params )
756             debugOut( "result was '%s'" % result )
757
758     def processStartupFile( self ):
759         """Read and execute all commands found in $HOME/.bbsh_startup"""
760         if os.path.exists( self.startupfilename ):
761             startupfile = open( self.startupfilename, "r" )
762             for cmdline in startupfile:
763                 debugOut( "processing startup line '%s'" % cmdline )
764                 if not cmdline:
765                     continue
766                 if "|" in cmdline:
767                     print "ERROR: '|' in startup file is not allowed. Ignoring line"
768                     continue
769                 self.commandQ.put( cmdline.strip() )
770
771     def main( self ):
772         """The main command loop"""
773         while not leave_mainloop:
774             try:
775                 if self.commandQ.empty():
776                     sys.stdout = self.myout.delegate
777                     cmdline = raw_input( "BB>> " )
778                     sys.stdout = self.myout
779                 else:
780                     cmdline = self.commandQ.get()
781                 if cmdline:
782                     allCommands = cmdline.split( ';' )
783                     for command in allCommands:
784                         pipecmd = None
785                         #
786                         # special case for expert mode
787                         if command == 'python':
788                             sys.stdout = self.myout.delegate
789                             self.processCommand( command, "" )
790                             sys.stdout = self.myout
791                         else:
792                             self.myout.startCommand( command )
793                             if '|' in command: # disable output
794                                 command, pipecmd = command.split( '|' )
795                                 delegate = self.myout.delegate
796                                 self.myout.delegate = None
797                             tokens = shlex.split( command, True )
798                             self.processCommand( tokens[0], tokens[1:] or "" )
799                             self.myout.endCommand()
800                             if pipecmd is not None: # restore output
801                                 self.myout.delegate = delegate
802
803                                 pipe = popen2.Popen4( pipecmd )
804                                 pipe.tochild.write( "\n".join( self.myout.lastBuffer() ) )
805                                 pipe.tochild.close()
806                                 sys.stdout.write( pipe.fromchild.read() )
807                         #
808             except EOFError:
809                 print
810                 return
811             except KeyboardInterrupt:
812                 print
813
814 ##########################################################################
815 # Start function - called from the BitBake command line utility
816 ##########################################################################
817
818 def start( aCooker ):
819     global cooker
820     cooker = aCooker
821     bbshell = BitBakeShell()
822     bbshell.processStartupFile()
823     bbshell.main()
824     bbshell.cleanup()
825
826 if __name__ == "__main__":
827     print "SHELL: Sorry, this program should only be called by BitBake."