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