1# pyshyacc.py - PLY grammar definition for pysh 2# 3# Copyright 2007 Patrick Mezard 4# 5# This software may be used and distributed according to the terms 6# of the GNU General Public License, incorporated herein by reference. 7 8"""PLY grammar file. 9""" 10import os.path 11import sys 12 13import bb.pysh.pyshlex as pyshlex 14tokens = pyshlex.tokens 15 16from ply import yacc 17import bb.pysh.sherrors as sherrors 18 19class IORedirect: 20 def __init__(self, op, filename, io_number=None): 21 self.op = op 22 self.filename = filename 23 self.io_number = io_number 24 25class HereDocument: 26 def __init__(self, op, name, content, io_number=None): 27 self.op = op 28 self.name = name 29 self.content = content 30 self.io_number = io_number 31 32def make_io_redirect(p): 33 """Make an IORedirect instance from the input 'io_redirect' production.""" 34 name, io_number, io_target = p 35 assert name=='io_redirect' 36 37 if io_target[0]=='io_file': 38 io_type, io_op, io_file = io_target 39 return IORedirect(io_op, io_file, io_number) 40 elif io_target[0]=='io_here': 41 io_type, io_op, io_name, io_content = io_target 42 return HereDocument(io_op, io_name, io_content, io_number) 43 else: 44 assert False, "Invalid IO redirection token %s" % repr(io_type) 45 46class SimpleCommand: 47 """ 48 assigns contains (name, value) pairs. 49 """ 50 def __init__(self, words, redirs, assigns): 51 self.words = list(words) 52 self.redirs = list(redirs) 53 self.assigns = list(assigns) 54 55class Pipeline: 56 def __init__(self, commands, reverse_status=False): 57 self.commands = list(commands) 58 assert self.commands #Grammar forbids this 59 self.reverse_status = reverse_status 60 61class AndOr: 62 def __init__(self, op, left, right): 63 self.op = str(op) 64 self.left = left 65 self.right = right 66 67class ForLoop: 68 def __init__(self, name, items, cmds): 69 self.name = str(name) 70 self.items = list(items) 71 self.cmds = list(cmds) 72 73class WhileLoop: 74 def __init__(self, condition, cmds): 75 self.condition = list(condition) 76 self.cmds = list(cmds) 77 78class UntilLoop: 79 def __init__(self, condition, cmds): 80 self.condition = list(condition) 81 self.cmds = list(cmds) 82 83class FunDef: 84 def __init__(self, name, body): 85 self.name = str(name) 86 self.body = body 87 88class BraceGroup: 89 def __init__(self, cmds): 90 self.cmds = list(cmds) 91 92class IfCond: 93 def __init__(self, cond, if_cmds, else_cmds): 94 self.cond = list(cond) 95 self.if_cmds = if_cmds 96 self.else_cmds = else_cmds 97 98class Case: 99 def __init__(self, name, items): 100 self.name = name 101 self.items = items 102 103class SubShell: 104 def __init__(self, cmds): 105 self.cmds = cmds 106 107class RedirectList: 108 def __init__(self, cmd, redirs): 109 self.cmd = cmd 110 self.redirs = list(redirs) 111 112def get_production(productions, ptype): 113 """productions must be a list of production tuples like (name, obj) where 114 name is the production string identifier. 115 Return the first production named 'ptype'. Raise KeyError if None can be 116 found. 117 """ 118 for production in productions: 119 if production is not None and production[0]==ptype: 120 return production 121 raise KeyError(ptype) 122 123#------------------------------------------------------------------------------- 124# PLY grammar definition 125#------------------------------------------------------------------------------- 126 127def p_multiple_commands(p): 128 """multiple_commands : newline_sequence 129 | complete_command 130 | multiple_commands complete_command""" 131 if len(p)==2: 132 if p[1] is not None: 133 p[0] = [p[1]] 134 else: 135 p[0] = [] 136 else: 137 p[0] = p[1] + [p[2]] 138 139def p_complete_command(p): 140 """complete_command : list separator 141 | list""" 142 if len(p)==3 and p[2] and p[2][1] == '&': 143 p[0] = ('async', p[1]) 144 else: 145 p[0] = p[1] 146 147def p_list(p): 148 """list : list separator_op and_or 149 | and_or""" 150 if len(p)==2: 151 p[0] = [p[1]] 152 else: 153 #if p[2]!=';': 154 # raise NotImplementedError('AND-OR list asynchronous execution is not implemented') 155 p[0] = p[1] + [p[3]] 156 157def p_and_or(p): 158 """and_or : pipeline 159 | and_or AND_IF linebreak pipeline 160 | and_or OR_IF linebreak pipeline""" 161 if len(p)==2: 162 p[0] = p[1] 163 else: 164 p[0] = ('and_or', AndOr(p[2], p[1], p[4])) 165 166def p_maybe_bang_word(p): 167 """maybe_bang_word : Bang""" 168 p[0] = ('maybe_bang_word', p[1]) 169 170def p_pipeline(p): 171 """pipeline : pipe_sequence 172 | bang_word pipe_sequence""" 173 if len(p)==3: 174 p[0] = ('pipeline', Pipeline(p[2][1:], True)) 175 else: 176 p[0] = ('pipeline', Pipeline(p[1][1:])) 177 178def p_pipe_sequence(p): 179 """pipe_sequence : command 180 | pipe_sequence PIPE linebreak command""" 181 if len(p)==2: 182 p[0] = ['pipe_sequence', p[1]] 183 else: 184 p[0] = p[1] + [p[4]] 185 186def p_command(p): 187 """command : simple_command 188 | compound_command 189 | compound_command redirect_list 190 | function_definition""" 191 192 if p[1][0] in ( 'simple_command', 193 'for_clause', 194 'while_clause', 195 'until_clause', 196 'case_clause', 197 'if_clause', 198 'function_definition', 199 'subshell', 200 'brace_group',): 201 if len(p) == 2: 202 p[0] = p[1] 203 else: 204 p[0] = ('redirect_list', RedirectList(p[1], p[2][1:])) 205 else: 206 raise NotImplementedError('%s command is not implemented' % repr(p[1][0])) 207 208def p_compound_command(p): 209 """compound_command : brace_group 210 | subshell 211 | for_clause 212 | case_clause 213 | if_clause 214 | while_clause 215 | until_clause""" 216 p[0] = p[1] 217 218def p_subshell(p): 219 """subshell : LPARENS compound_list RPARENS""" 220 p[0] = ('subshell', SubShell(p[2][1:])) 221 222def p_compound_list(p): 223 """compound_list : term 224 | newline_list term 225 | term separator 226 | newline_list term separator""" 227 productions = p[1:] 228 try: 229 sep = get_production(productions, 'separator') 230 if sep[1]!=';': 231 raise NotImplementedError() 232 except KeyError: 233 pass 234 term = get_production(productions, 'term') 235 p[0] = ['compound_list'] + term[1:] 236 237def p_term(p): 238 """term : term separator and_or 239 | and_or""" 240 if len(p)==2: 241 p[0] = ['term', p[1]] 242 else: 243 if p[2] is not None and p[2][1] == '&': 244 p[0] = ['term', ('async', p[1][1:])] + [p[3]] 245 else: 246 p[0] = p[1] + [p[3]] 247 248def p_maybe_for_word(p): 249 # Rearrange 'For' priority wrt TOKEN. See p_for_word 250 """maybe_for_word : For""" 251 p[0] = ('maybe_for_word', p[1]) 252 253def p_for_clause(p): 254 """for_clause : for_word name linebreak do_group 255 | for_word name linebreak in sequential_sep do_group 256 | for_word name linebreak in wordlist sequential_sep do_group""" 257 productions = p[1:] 258 do_group = get_production(productions, 'do_group') 259 try: 260 items = get_production(productions, 'in')[1:] 261 except KeyError: 262 raise NotImplementedError('"in" omission is not implemented') 263 264 try: 265 items = get_production(productions, 'wordlist')[1:] 266 except KeyError: 267 items = [] 268 269 name = p[2] 270 p[0] = ('for_clause', ForLoop(name, items, do_group[1:])) 271 272def p_name(p): 273 """name : token""" #Was NAME instead of token 274 p[0] = p[1] 275 276def p_in(p): 277 """in : In""" 278 p[0] = ('in', p[1]) 279 280def p_wordlist(p): 281 """wordlist : wordlist token 282 | token""" 283 if len(p)==2: 284 p[0] = ['wordlist', ('TOKEN', p[1])] 285 else: 286 p[0] = p[1] + [('TOKEN', p[2])] 287 288def p_case_clause(p): 289 """case_clause : Case token linebreak in linebreak case_list Esac 290 | Case token linebreak in linebreak case_list_ns Esac 291 | Case token linebreak in linebreak Esac""" 292 if len(p) < 8: 293 items = [] 294 else: 295 items = p[6][1:] 296 name = p[2] 297 p[0] = ('case_clause', Case(name, [c[1] for c in items])) 298 299def p_case_list_ns(p): 300 """case_list_ns : case_list case_item_ns 301 | case_item_ns""" 302 p_case_list(p) 303 304def p_case_list(p): 305 """case_list : case_list case_item 306 | case_item""" 307 if len(p)==2: 308 p[0] = ['case_list', p[1]] 309 else: 310 p[0] = p[1] + [p[2]] 311 312def p_case_item_ns(p): 313 """case_item_ns : pattern RPARENS linebreak 314 | pattern RPARENS compound_list linebreak 315 | LPARENS pattern RPARENS linebreak 316 | LPARENS pattern RPARENS compound_list linebreak""" 317 p_case_item(p) 318 319def p_case_item(p): 320 """case_item : pattern RPARENS linebreak DSEMI linebreak 321 | pattern RPARENS compound_list DSEMI linebreak 322 | LPARENS pattern RPARENS linebreak DSEMI linebreak 323 | LPARENS pattern RPARENS compound_list DSEMI linebreak""" 324 if len(p) < 7: 325 name = p[1][1:] 326 else: 327 name = p[2][1:] 328 329 try: 330 cmds = get_production(p[1:], "compound_list")[1:] 331 except KeyError: 332 cmds = [] 333 334 p[0] = ('case_item', (name, cmds)) 335 336def p_pattern(p): 337 """pattern : token 338 | pattern PIPE token""" 339 if len(p)==2: 340 p[0] = ['pattern', ('TOKEN', p[1])] 341 else: 342 p[0] = p[1] + [('TOKEN', p[2])] 343 344def p_maybe_if_word(p): 345 # Rearrange 'If' priority wrt TOKEN. See p_if_word 346 """maybe_if_word : If""" 347 p[0] = ('maybe_if_word', p[1]) 348 349def p_maybe_then_word(p): 350 # Rearrange 'Then' priority wrt TOKEN. See p_then_word 351 """maybe_then_word : Then""" 352 p[0] = ('maybe_then_word', p[1]) 353 354def p_if_clause(p): 355 """if_clause : if_word compound_list then_word compound_list else_part Fi 356 | if_word compound_list then_word compound_list Fi""" 357 else_part = [] 358 if len(p)==7: 359 else_part = p[5] 360 p[0] = ('if_clause', IfCond(p[2][1:], p[4][1:], else_part)) 361 362def p_else_part(p): 363 """else_part : Elif compound_list then_word compound_list else_part 364 | Elif compound_list then_word compound_list 365 | Else compound_list""" 366 if len(p)==3: 367 p[0] = p[2][1:] 368 else: 369 else_part = [] 370 if len(p)==6: 371 else_part = p[5] 372 p[0] = ('elif', IfCond(p[2][1:], p[4][1:], else_part)) 373 374def p_while_clause(p): 375 """while_clause : While compound_list do_group""" 376 p[0] = ('while_clause', WhileLoop(p[2][1:], p[3][1:])) 377 378def p_maybe_until_word(p): 379 # Rearrange 'Until' priority wrt TOKEN. See p_until_word 380 """maybe_until_word : Until""" 381 p[0] = ('maybe_until_word', p[1]) 382 383def p_until_clause(p): 384 """until_clause : until_word compound_list do_group""" 385 p[0] = ('until_clause', UntilLoop(p[2][1:], p[3][1:])) 386 387def p_function_definition(p): 388 """function_definition : fname LPARENS RPARENS linebreak function_body""" 389 p[0] = ('function_definition', FunDef(p[1], p[5])) 390 391def p_function_body(p): 392 """function_body : compound_command 393 | compound_command redirect_list""" 394 if len(p)!=2: 395 raise NotImplementedError('functions redirections lists are not implemented') 396 p[0] = p[1] 397 398def p_fname(p): 399 """fname : TOKEN""" #Was NAME instead of token 400 p[0] = p[1] 401 402def p_brace_group(p): 403 """brace_group : Lbrace compound_list Rbrace""" 404 p[0] = ('brace_group', BraceGroup(p[2][1:])) 405 406def p_maybe_done_word(p): 407 #See p_assignment_word for details. 408 """maybe_done_word : Done""" 409 p[0] = ('maybe_done_word', p[1]) 410 411def p_maybe_do_word(p): 412 """maybe_do_word : Do""" 413 p[0] = ('maybe_do_word', p[1]) 414 415def p_do_group(p): 416 """do_group : do_word compound_list done_word""" 417 #Do group contains a list of AndOr 418 p[0] = ['do_group'] + p[2][1:] 419 420def p_simple_command(p): 421 """simple_command : cmd_prefix cmd_word cmd_suffix 422 | cmd_prefix cmd_word 423 | cmd_prefix 424 | cmd_name cmd_suffix 425 | cmd_name""" 426 words, redirs, assigns = [], [], [] 427 for e in p[1:]: 428 name = e[0] 429 if name in ('cmd_prefix', 'cmd_suffix'): 430 for sube in e[1:]: 431 subname = sube[0] 432 if subname=='io_redirect': 433 redirs.append(make_io_redirect(sube)) 434 elif subname=='ASSIGNMENT_WORD': 435 assigns.append(sube) 436 else: 437 words.append(sube) 438 elif name in ('cmd_word', 'cmd_name'): 439 words.append(e) 440 441 cmd = SimpleCommand(words, redirs, assigns) 442 p[0] = ('simple_command', cmd) 443 444def p_cmd_name(p): 445 """cmd_name : TOKEN""" 446 p[0] = ('cmd_name', p[1]) 447 448def p_cmd_word(p): 449 """cmd_word : token""" 450 p[0] = ('cmd_word', p[1]) 451 452def p_maybe_assignment_word(p): 453 #See p_assignment_word for details. 454 """maybe_assignment_word : ASSIGNMENT_WORD""" 455 p[0] = ('maybe_assignment_word', p[1]) 456 457def p_cmd_prefix(p): 458 """cmd_prefix : io_redirect 459 | cmd_prefix io_redirect 460 | assignment_word 461 | cmd_prefix assignment_word""" 462 try: 463 prefix = get_production(p[1:], 'cmd_prefix') 464 except KeyError: 465 prefix = ['cmd_prefix'] 466 467 try: 468 value = get_production(p[1:], 'assignment_word')[1] 469 value = ('ASSIGNMENT_WORD', value.split('=', 1)) 470 except KeyError: 471 value = get_production(p[1:], 'io_redirect') 472 p[0] = prefix + [value] 473 474def p_cmd_suffix(p): 475 """cmd_suffix : io_redirect 476 | cmd_suffix io_redirect 477 | token 478 | cmd_suffix token 479 | maybe_for_word 480 | cmd_suffix maybe_for_word 481 | maybe_done_word 482 | cmd_suffix maybe_done_word 483 | maybe_do_word 484 | cmd_suffix maybe_do_word 485 | maybe_until_word 486 | cmd_suffix maybe_until_word 487 | maybe_assignment_word 488 | cmd_suffix maybe_assignment_word 489 | maybe_if_word 490 | cmd_suffix maybe_if_word 491 | maybe_then_word 492 | cmd_suffix maybe_then_word 493 | maybe_bang_word 494 | cmd_suffix maybe_bang_word""" 495 try: 496 suffix = get_production(p[1:], 'cmd_suffix') 497 token = p[2] 498 except KeyError: 499 suffix = ['cmd_suffix'] 500 token = p[1] 501 502 if isinstance(token, tuple): 503 if token[0]=='io_redirect': 504 p[0] = suffix + [token] 505 else: 506 #Convert maybe_* to TOKEN if necessary 507 p[0] = suffix + [('TOKEN', token[1])] 508 else: 509 p[0] = suffix + [('TOKEN', token)] 510 511def p_redirect_list(p): 512 """redirect_list : io_redirect 513 | redirect_list io_redirect""" 514 if len(p) == 2: 515 p[0] = ['redirect_list', make_io_redirect(p[1])] 516 else: 517 p[0] = p[1] + [make_io_redirect(p[2])] 518 519def p_io_redirect(p): 520 """io_redirect : io_file 521 | IO_NUMBER io_file 522 | io_here 523 | IO_NUMBER io_here""" 524 if len(p)==3: 525 p[0] = ('io_redirect', p[1], p[2]) 526 else: 527 p[0] = ('io_redirect', None, p[1]) 528 529def p_io_file(p): 530 #Return the tuple (operator, filename) 531 """io_file : LESS filename 532 | LESSAND filename 533 | GREATER filename 534 | GREATAND filename 535 | DGREAT filename 536 | LESSGREAT filename 537 | CLOBBER filename""" 538 #Extract the filename from the file 539 p[0] = ('io_file', p[1], p[2][1]) 540 541def p_filename(p): 542 #Return the filename 543 """filename : TOKEN""" 544 p[0] = ('filename', p[1]) 545 546def p_io_here(p): 547 """io_here : DLESS here_end 548 | DLESSDASH here_end""" 549 p[0] = ('io_here', p[1], p[2][1], p[2][2]) 550 551def p_here_end(p): 552 """here_end : HERENAME TOKEN""" 553 p[0] = ('here_document', p[1], p[2]) 554 555def p_newline_sequence(p): 556 # Nothing in the grammar can handle leading NEWLINE productions, so add 557 # this one with the lowest possible priority relatively to newline_list. 558 """newline_sequence : newline_list""" 559 p[0] = None 560 561def p_newline_list(p): 562 """newline_list : NEWLINE 563 | newline_list NEWLINE""" 564 p[0] = None 565 566def p_linebreak(p): 567 """linebreak : newline_list 568 | empty""" 569 p[0] = None 570 571def p_separator_op(p): 572 """separator_op : COMMA 573 | COMMA COMMA 574 | AMP""" 575 p[0] = p[1] 576 577def p_separator(p): 578 """separator : separator_op linebreak 579 | newline_list""" 580 if len(p)==2: 581 #Ignore newlines 582 p[0] = None 583 else: 584 #Keep the separator operator 585 p[0] = ('separator', p[1]) 586 587def p_sequential_sep(p): 588 """sequential_sep : COMMA linebreak 589 | newline_list""" 590 p[0] = None 591 592# Low priority TOKEN => for_word conversion. 593# Let maybe_for_word be used as a token when necessary in higher priority 594# rules. 595def p_for_word(p): 596 """for_word : maybe_for_word""" 597 p[0] = p[1] 598 599def p_if_word(p): 600 """if_word : maybe_if_word""" 601 p[0] = p[1] 602 603def p_then_word(p): 604 """then_word : maybe_then_word""" 605 p[0] = p[1] 606 607def p_done_word(p): 608 """done_word : maybe_done_word""" 609 p[0] = p[1] 610 611def p_do_word(p): 612 """do_word : maybe_do_word""" 613 p[0] = p[1] 614 615def p_until_word(p): 616 """until_word : maybe_until_word""" 617 p[0] = p[1] 618 619def p_assignment_word(p): 620 """assignment_word : maybe_assignment_word""" 621 p[0] = ('assignment_word', p[1][1]) 622 623def p_bang_word(p): 624 """bang_word : maybe_bang_word""" 625 p[0] = ('bang_word', p[1][1]) 626 627def p_token(p): 628 """token : TOKEN 629 | Fi""" 630 p[0] = p[1] 631 632def p_empty(p): 633 'empty :' 634 p[0] = None 635 636# Error rule for syntax errors 637def p_error(p): 638 msg = [] 639 w = msg.append 640 if p: 641 w('%r\n' % p) 642 w('followed by:\n') 643 for i in range(5): 644 n = yacc.token() 645 if not n: 646 break 647 w(' %r\n' % n) 648 else: 649 w('Unexpected EOF') 650 raise sherrors.ShellSyntaxError(''.join(msg)) 651 652# Build the parser 653try: 654 import pyshtables 655except ImportError: 656 outputdir = os.path.dirname(__file__) 657 if not os.access(outputdir, os.W_OK): 658 outputdir = '' 659 yacc.yacc(tabmodule = 'pyshtables', outputdir = outputdir, debug = 0) 660else: 661 yacc.yacc(tabmodule = 'pysh.pyshtables', write_tables = 0, debug = 0) 662 663 664def parse(input, eof=False, debug=False): 665 """Parse a whole script at once and return the generated AST and unconsumed 666 data in a tuple. 667 668 NOTE: eof is probably meaningless for now, the parser being unable to work 669 in pull mode. It should be set to True. 670 """ 671 lexer = pyshlex.PLYLexer() 672 remaining = lexer.add(input, eof) 673 if lexer.is_empty(): 674 return [], remaining 675 if debug: 676 debug = 2 677 return yacc.parse(lexer=lexer, debug=debug), remaining 678 679#------------------------------------------------------------------------------- 680# AST rendering helpers 681#------------------------------------------------------------------------------- 682 683def format_commands(v): 684 """Return a tree made of strings and lists. Make command trees easier to 685 display. 686 """ 687 if isinstance(v, list): 688 return [format_commands(c) for c in v] 689 if isinstance(v, tuple): 690 if len(v)==2 and isinstance(v[0], str) and not isinstance(v[1], str): 691 if v[0] == 'async': 692 return ['AsyncList', map(format_commands, v[1])] 693 else: 694 #Avoid decomposing tuples like ('pipeline', Pipeline(...)) 695 return format_commands(v[1]) 696 return format_commands(list(v)) 697 elif isinstance(v, IfCond): 698 name = ['IfCond'] 699 name += ['if', map(format_commands, v.cond)] 700 name += ['then', map(format_commands, v.if_cmds)] 701 name += ['else', map(format_commands, v.else_cmds)] 702 return name 703 elif isinstance(v, ForLoop): 704 name = ['ForLoop'] 705 name += [repr(v.name)+' in ', map(str, v.items)] 706 name += ['commands', map(format_commands, v.cmds)] 707 return name 708 elif isinstance(v, AndOr): 709 return [v.op, format_commands(v.left), format_commands(v.right)] 710 elif isinstance(v, Pipeline): 711 name = 'Pipeline' 712 if v.reverse_status: 713 name = '!' + name 714 return [name, format_commands(v.commands)] 715 elif isinstance(v, Case): 716 name = ['Case'] 717 name += [v.name, format_commands(v.items)] 718 elif isinstance(v, SimpleCommand): 719 name = ['SimpleCommand'] 720 if v.words: 721 name += ['words', map(str, v.words)] 722 if v.assigns: 723 assigns = [tuple(a[1]) for a in v.assigns] 724 name += ['assigns', map(str, assigns)] 725 if v.redirs: 726 name += ['redirs', map(format_commands, v.redirs)] 727 return name 728 elif isinstance(v, RedirectList): 729 name = ['RedirectList'] 730 if v.redirs: 731 name += ['redirs', map(format_commands, v.redirs)] 732 name += ['command', format_commands(v.cmd)] 733 return name 734 elif isinstance(v, IORedirect): 735 return ' '.join(map(str, (v.io_number, v.op, v.filename))) 736 elif isinstance(v, HereDocument): 737 return ' '.join(map(str, (v.io_number, v.op, repr(v.name), repr(v.content)))) 738 elif isinstance(v, SubShell): 739 return ['SubShell', map(format_commands, v.cmds)] 740 else: 741 return repr(v) 742 743def print_commands(cmds, output=sys.stdout): 744 """Pretty print a command tree.""" 745 def print_tree(cmd, spaces, output): 746 if isinstance(cmd, list): 747 for c in cmd: 748 print_tree(c, spaces + 3, output) 749 else: 750 print >>output, ' '*spaces + str(cmd) 751 752 formatted = format_commands(cmds) 753 print_tree(formatted, 0, output) 754 755 756def stringify_commands(cmds): 757 """Serialize a command tree as a string. 758 759 Returned string is not pretty and is currently used for unit tests only. 760 """ 761 def stringify(value): 762 output = [] 763 if isinstance(value, list): 764 formatted = [] 765 for v in value: 766 formatted.append(stringify(v)) 767 formatted = ' '.join(formatted) 768 output.append(''.join(['<', formatted, '>'])) 769 else: 770 output.append(value) 771 return ' '.join(output) 772 773 return stringify(format_commands(cmds)) 774 775 776def visit_commands(cmds, callable): 777 """Visit the command tree and execute callable on every Pipeline and 778 SimpleCommand instances. 779 """ 780 if isinstance(cmds, (tuple, list)): 781 map(lambda c: visit_commands(c,callable), cmds) 782 elif isinstance(cmds, (Pipeline, SimpleCommand)): 783 callable(cmds) 784