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