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