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