├── .gitignore ├── .idea ├── Pysh.iml ├── deployment.xml ├── dictionaries │ └── Vence.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml ├── modules.xml ├── vcs.xml └── workspace.xml ├── Pipfile ├── Pipfile.lock ├── README.md ├── pysh.png ├── pysh.py └── pysh ├── __init__.py ├── command ├── __init__.py ├── cd.py ├── echo.py ├── exit.py ├── hash.py ├── help.py ├── history.py ├── kill.py ├── ls.py ├── ps.py └── pysh.py ├── contrib ├── LineEdit │ ├── __init__.py │ ├── action.py │ └── line_edit.py ├── __init__.py ├── control.py ├── control_flow.py ├── keyword.py ├── parser.py └── tools.py └── manage ├── __init__.py ├── dispatch.py ├── env.py └── middleware.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | .idea -------------------------------------------------------------------------------- /.idea/Pysh.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /.idea/deployment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /.idea/dictionaries/Vence.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | arian 5 | arianx 6 | baka 7 | bksp 8 | contexte 9 | contextlib 10 | curdir 11 | derc 12 | getch 13 | maxlen 14 | offseted 15 | pids 16 | pycache 17 | pysh 18 | readline 19 | redirector 20 | resutls 21 | stdin 22 | stdouttemp 23 | userprofile 24 | 25 | 26 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 34 | 35 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 237 | 238 | 239 | 240 | 241 | 261 | 262 | 263 | 283 | 284 | 285 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 1527828886709 325 | 329 | 330 | 1528195994914 331 | 336 | 337 | 1528197677225 338 | 343 | 344 | 1528201826507 345 | 350 | 351 | 1528471430521 352 | 357 | 358 | 1528554516617 359 | 364 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 435 | 436 | 460 | 461 | 462 | 464 | 465 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | sys 500 | Python 501 | EXPRESSION 502 | 503 | 504 | Variable 505 | Python 506 | EXPRESSION 507 | 508 | 509 | self 510 | Python 511 | EXPRESSION 512 | 513 | 514 | sa 515 | Python 516 | EXPRESSION 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | 3 | url = "https://pypi.python.org/simple" 4 | verify_ssl = true 5 | name = "pypi" 6 | 7 | 8 | [packages] 9 | 10 | 11 | 12 | [dev-packages] 13 | 14 | 15 | 16 | [requires] 17 | 18 | python_version = "3.6" 19 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "4e55147db217bb4120f6e68cb8cad7bc37011457441ce0eb9d97308315625834" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": {}, 19 | "develop": {} 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Simple interactive shell script powered by python 2 | 3 | Features: 4 | - High scalability. You can quickly extend the functions in the shell by registering to the framework. 5 | - Pure python. No external libraries are required 6 | 7 | Usage: 8 | 1. Clone this repository. 9 | 2. Enter the directory. 10 | 3. Run `python3 .\pysh.py`. 11 | 4. Type the `help` to get the help info. 12 | -------------------------------------------------------------------------------- /pysh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arianxx/Pysh/d44f7303bc4ce3764a300830d361115200ad3602/pysh.png -------------------------------------------------------------------------------- /pysh.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pysh import Pysh 4 | 5 | if __name__ == '__main__': 6 | Pysh.start() 7 | -------------------------------------------------------------------------------- /pysh/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 这里提供内部命令的导入,以及一些外部工具 4 | """ 5 | from .command import pysh, exit, ps, history, ls, ps, cd, kill, hash, help, \ 6 | echo 7 | from .manage.dispatch import dispatch 8 | from .manage.env import Application 9 | 10 | 11 | class Pysh: 12 | @classmethod 13 | def start(cls): 14 | dispatch.dispatch('pysh') 15 | 16 | class App: 17 | app = Application() 18 | 19 | register = app.register -------------------------------------------------------------------------------- /pysh/command/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arianxx/Pysh/d44f7303bc4ce3764a300830d361115200ad3602/pysh/command/__init__.py -------------------------------------------------------------------------------- /pysh/command/cd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | from ..manage.env import Application 5 | 6 | app = Application() 7 | 8 | @app.register 9 | class cd: 10 | help = None 11 | path = None 12 | usage = """ 13 | Usage: 14 | cd path:改变当前工作目录为 path 15 | cd --help:显示这个帮助信息 16 | """ 17 | def __init__(self, *args): 18 | if '--help' in args: 19 | self.help = True 20 | else: 21 | try: 22 | self.path = args[0].strip('"').strip("'") 23 | except IndexError as e: 24 | print('没有提供路径') 25 | 26 | def handler(self): 27 | if self.path: 28 | try: 29 | os.chdir(self.path) 30 | except FileNotFoundError as e: 31 | print(e) 32 | return False 33 | except NotADirectoryError as e: 34 | print(e) 35 | return False 36 | elif self.help: 37 | print(self.usage) 38 | 39 | return True 40 | -------------------------------------------------------------------------------- /pysh/command/echo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..manage.env import Application 3 | 4 | app = Application() 5 | 6 | 7 | @app.register 8 | class echo: 9 | help = False 10 | usage = """ 11 | Usage: 12 | echo [arg1] [arg2] ...:依次打印参数 13 | echo --help(default):打印本帮助 14 | """ 15 | 16 | def __init__(self, *args): 17 | self.args = args or [] 18 | if not self.args or '--help' in self.args: 19 | self.help = True 20 | 21 | def handler(self): 22 | if self.help: 23 | print(self.usage) 24 | else: 25 | for arg in self.args: 26 | if arg.startswith("'"): 27 | arg = arg.strip("'") 28 | elif arg.startswith('"'): 29 | arg = arg.strip('"') 30 | 31 | print(arg, sep=' ') 32 | 33 | return True 34 | -------------------------------------------------------------------------------- /pysh/command/exit.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..manage.env import Application 3 | 4 | app = Application() 5 | 6 | class ShellExit(SystemExit): 7 | pass 8 | 9 | @app.register 10 | class exit: 11 | help = None 12 | usage = """ 13 | Usage: 14 | 退出当前shell 15 | """ 16 | def __init__(self, *args): 17 | self.args = args or [] 18 | 19 | if '--help' in args: 20 | self.help = True 21 | 22 | def handler(self): 23 | if self.help: 24 | print(self.usage) 25 | else: 26 | raise ShellExit -------------------------------------------------------------------------------- /pysh/command/hash.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..manage.env import Application, EnvVariable 3 | 4 | app = Application() 5 | 6 | 7 | @app.register 8 | class hash: 9 | clear = False 10 | show = False 11 | help = False 12 | usage = """ 13 | Usage: 14 | hash -l:查看缓存的路径 15 | hash -r: 清空缓存的外部命令路径 16 | hash --help(default):显示本帮助 17 | """ 18 | 19 | def __init__(self, *args): 20 | self.args = args or [] 21 | if not self.args or '--help' in self.args or not self.args: 22 | self.help = True 23 | elif '-l' in self.args: 24 | self.show = True 25 | elif '-r' in self.args: 26 | self.clear = True 27 | 28 | def handler(self): 29 | rv = True 30 | 31 | if self.help: 32 | print(self.usage) 33 | elif self.show: 34 | for name, path in EnvVariable.cached.items(): 35 | print(name + ' : ' + path) 36 | elif self.clear: 37 | length = len(EnvVariable.cached) 38 | if not EnvVariable.clear_cached(): 39 | rv = False 40 | else: 41 | print('已经清空 {} 外部命令路径'.format(length)) 42 | 43 | return rv 44 | -------------------------------------------------------------------------------- /pysh/command/help.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..contrib.LineEdit import KeyMapping 3 | from ..contrib.keyword import Symbol, Keyword 4 | from ..manage.env import Application 5 | 6 | app = Application() 7 | 8 | 9 | @app.register 10 | class help: 11 | verbose_1 = False 12 | verbose_2 = False 13 | verbose_3 = False 14 | le = False 15 | help = False 16 | usage = """ 17 | Usage: 18 | help(default):显示本帮助 19 | help -v:显示注册的命令 20 | help -vv:显示注册的命令,以及它们各自的用法(如果有) 21 | help -vvv:显示注册的命令、用法,以及内建的特殊符号和关键字 22 | help -le: 显示行编辑字符 23 | """ 24 | 25 | def __init__(self, *args): 26 | self.args = args or [] 27 | if '-v' in self.args: 28 | self.verbose_1 = True 29 | elif '-vv' in self.args: 30 | self.verbose_2 = True 31 | elif '-vvv' in self.args: 32 | self.verbose_3 = True 33 | elif '-le' in self.args: 34 | self.le = True 35 | else: 36 | self.help = True 37 | 38 | def handler(self): 39 | if self.help: 40 | print(self.usage) 41 | elif self.verbose_1: 42 | apps = Application.registered_app() 43 | print('注册的命令有:') 44 | for app in apps: 45 | print(app) 46 | elif self.verbose_2: 47 | apps = Application.registered_app() 48 | print('注册的命令有:') 49 | for name, instance in apps.items(): 50 | print('\n', name) 51 | try: 52 | print('\t', instance.usage) 53 | except AttributeError: 54 | pass 55 | elif self.verbose_3: 56 | symbols = Symbol.registered_symbol() 57 | keywords = Keyword.registered_keyword() 58 | apps = Application.registered_app() 59 | 60 | print('注册的命令有:') 61 | for name, instance in apps.items(): 62 | print('\n', name) 63 | try: 64 | print('\t', instance.usage) 65 | except AttributeError: 66 | pass 67 | 68 | print('\n注册的特殊符号有:') 69 | for key in symbols: 70 | print(key, ':', symbols[key].char) 71 | try: 72 | print('\tderc: ', symbols[key].derc) 73 | except AttributeError: 74 | pass 75 | 76 | print('\n注册的关键字有:') 77 | for key in keywords: 78 | print(key, ':', keywords[key].words) 79 | try: 80 | print('\tderc:', keywords[key].derc) 81 | except AttributeError: 82 | pass 83 | elif self.le: 84 | mapping = KeyMapping._get_map_dict() 85 | 86 | print('\n行编辑字符:') 87 | for key, value in mapping.items(): 88 | print(key, ' : ', value.encode()) 89 | 90 | return True 91 | -------------------------------------------------------------------------------- /pysh/command/history.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..manage.env import Application 3 | 4 | app = Application() 5 | 6 | @app.register 7 | class history: 8 | num = None 9 | maxlen = None 10 | new_maxlen = None 11 | clear = None 12 | clear_num = None 13 | help = None 14 | usage = """ 15 | Usage: 16 | history [num]:查看num条数量的历史命令,num默认为100 17 | history --maxlen [num]:如果没有num,查看能容纳的最大历史数量;如果有num,更新maxlen为num 18 | history --clear [num]:清空最近num条数量的历史记录,num默认为maxlen 19 | history --help: 显示这条帮助信息 20 | """ 21 | 22 | def __init__(self, *args): 23 | self.args = args 24 | 25 | if '--help' in self.args: 26 | self.help = True 27 | elif '--maxlen' in self.args: 28 | self.maxlen = True 29 | if len(self.args) > 1: 30 | maxlen_index = self.args.index('--maxlen') 31 | self.new_maxlen = self.args[maxlen_index + 1] 32 | elif '--clear' in self.args: 33 | self.clear = True 34 | if len(self.args) > 1: 35 | clear_num_index = self.args.index('--clear') 36 | self.clear_num = self.args[clear_num_index + 1] 37 | else: 38 | if len(self.args) > 0: 39 | try: 40 | self.num = int(self.args[0]) 41 | except ValueError as e: 42 | print(e) 43 | self.num = 0 44 | 45 | def handler(self): 46 | History = self.env['History'] 47 | 48 | if self.help: 49 | print(self.usage) 50 | elif self.maxlen: 51 | if self.new_maxlen is not None: 52 | History.new_maxlen(self.new_maxlen) 53 | else: 54 | print(History.maxlen()) 55 | elif self.clear: 56 | if self.clear_num is not None: 57 | for _ in range(self.clear_num): 58 | History.history.pop() 59 | else: 60 | History.history.clear() 61 | else: 62 | if self.num is not None: 63 | length = len(History.history) 64 | for index in range(length, length - self.num, -1): 65 | print(History.history[index-1]) 66 | else: 67 | for h in History.history: 68 | print(h) 69 | 70 | return True 71 | -------------------------------------------------------------------------------- /pysh/command/kill.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..manage.env import Application 3 | 4 | app = Application() 5 | 6 | 7 | @app.register 8 | class kill: 9 | help = False 10 | usage = """ 11 | Usage: 12 | kill [pid1] [pid2] ...:杀死给出的进程 13 | kill --help(default):显示本帮助 14 | """ 15 | 16 | def __init__(self, *args): 17 | self.args = args or [] 18 | if '--help' in self.args or not self.args: 19 | self.help = True 20 | 21 | def handler(self): 22 | if self.help: 23 | print(self.usage) 24 | else: 25 | process = self.env['Processing'].get_records() 26 | ids = list(map(int, self.args)) 27 | for id in ids: 28 | if id not in process.keys(): 29 | print('{} 是一个无效的pid.'.format(str(id))) 30 | elif not process[id]['instance'].backend: 31 | print('{} 是一个前台命令,你不能终止它.'.format(str(id))) 32 | else: 33 | try: 34 | process[id]['instance'].terminate() 35 | except AttributeError as e: 36 | # 杀死当前阻塞的pysh进程时,会弹出AttributeError 37 | print('你不能杀死阻塞中的pysh进程。请使用 exit 退出。') 38 | else: 39 | print('pid 为 {} 的进程 {} 已经被你终止.'.format( 40 | str(id), 41 | process[id]['NAME'], 42 | )) 43 | 44 | return True 45 | -------------------------------------------------------------------------------- /pysh/command/ls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | from ..manage.env import Application 5 | 6 | app = Application() 7 | 8 | 9 | @app.register 10 | class ls: 11 | almost_all = True 12 | all = None 13 | ignore_back_backups = None 14 | list = None 15 | reverse = None 16 | help = None 17 | paths = None 18 | usage = """ 19 | Usage: 20 | ls [optional] [path1] [path2] ... 21 | Optional: 22 | ls -A, -almost-all: 列出除了 . 和 .. 的所有文件 23 | ls -a, -all: 列出目录下的所有文件,包括以. 开头的隐含文件 24 | ls -B, -ignore-back-backups:不列出任何以 ~ 字符结束的文件 25 | ls -l, -list:列排列输出 26 | ls -r, -reverse:以相反顺序排序 27 | ls --help:列出这份帮助说明 28 | """ 29 | 30 | def __init__(self, *args): 31 | self.args = args or [] 32 | if '-A' in self.args or '-almost-all' in self.args: 33 | self.almost_all = True 34 | 35 | if '-a' in self.args or '-all' in self.args: 36 | self.all = True 37 | 38 | if '-B' in self.args or '-ignore-back-backups' in self.args: 39 | self.ignore_back_backups = True 40 | 41 | if '-r' in self.args or '-reverse' in self.args: 42 | self.reverse = True 43 | 44 | if '-l' in self.args or '-list' in self.args: 45 | self.list = True 46 | 47 | if '--help' in self.args: 48 | self.help = True 49 | 50 | self.paths = [] 51 | path_start = None 52 | for arg in self.args: 53 | if '-' not in arg: 54 | path_start = args.index(arg) 55 | break 56 | if path_start is not None: 57 | self.paths = self.args[path_start:] 58 | 59 | if not len(self.paths): 60 | self.paths += ['.'] 61 | 62 | def handler(self): 63 | if self.help: 64 | print(self.usage) 65 | return 66 | 67 | for path in self.paths: 68 | path = path.strip('"').strip("'") 69 | try: 70 | dirs = os.listdir(path) 71 | except FileNotFoundError as e: 72 | print(e) 73 | return False 74 | except NotADirectoryError as e: 75 | print(e) 76 | return False 77 | except OSError as e: 78 | print(e) 79 | return False 80 | else: 81 | if self.ignore_back_backups: 82 | dirs = [dir for dir in dirs if not dir.endswith('~')] 83 | if self.all: 84 | dirs = ['.', '..'] + dirs 85 | if self.almost_all: 86 | dirs = [dir for dir in dirs if not dir.startswith('.')] 87 | if self.reverse: 88 | dirs.reverse() 89 | 90 | if self.list: 91 | print('\n'.join(dirs)) 92 | else: 93 | print(' '.join(dirs)) 94 | return True 95 | -------------------------------------------------------------------------------- /pysh/command/ps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..manage.env import Application 3 | 4 | app = Application() 5 | 6 | @app.register 7 | class ps: 8 | usage = """ 9 | Usage: 10 | ps:查看所有运行中的命令。 11 | ps [name] [name] ...:查看给出名字的运行的命令。 12 | ps --help 13 | """ 14 | 15 | def __init__(self, *args): 16 | self.args = args 17 | 18 | def handler(self): 19 | if '--help' in self.args: 20 | print(self.usage) 21 | return True 22 | else: 23 | process = self.env['Processing'].get_records() 24 | print('{:<10}{:<10}{:<10}{:<10}'.format('PID', 'NAME', 'BACKEND', 'TIME')) 25 | if len(self.args) > 0: 26 | pids = [] 27 | for arg in self.args: 28 | pids += [pid 29 | for pid in process.keys() 30 | if process[pid]['NAME'] == arg] 31 | else: 32 | pids = [pid for pid in process.keys()] 33 | 34 | for pid in pids: 35 | print('{:<10}{:<10}{:<10}{:<10}'.format( 36 | pid, 37 | process[pid]['NAME'], 38 | str(process[pid]['instance'].backend), 39 | str(process[pid]['TIME']), 40 | )) 41 | 42 | return True 43 | -------------------------------------------------------------------------------- /pysh/command/pysh.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | shell本身也是一个应用,可以通过各种方式访问和控制。初次启动时,直接通过dispatch模块启动shell。 4 | 5 | TODO: 将参数判断抽象出来,成为单独的一层,应用只需注册参数类型和列表即可,具体判断交由上层执行。 6 | """ 7 | 8 | import os 9 | import sys 10 | 11 | from .exit import ShellExit 12 | from ..contrib.LineEdit import LineInput 13 | from ..contrib.parser import Parser 14 | from ..manage.env import Application 15 | 16 | app = Application() 17 | 18 | @app.register 19 | class pysh: 20 | line_symbol = '#' 21 | logout = 'logout' 22 | slogan = """ 23 | ******************************* 24 | 25 | Welcome to Pysh! 26 | Type 'help' to get the help. 27 | Copyright (c) 2018 ArianX 28 | 29 | ******************************* 30 | """ 31 | usage = """ 32 | Usage: 33 | pysh:启动pysh shell 34 | """ 35 | 36 | def __init__(self, *args): 37 | pass 38 | 39 | def handler(self): 40 | print(self.slogan) 41 | 42 | self._poll() 43 | 44 | return True 45 | 46 | def _poll(self): 47 | global input 48 | 49 | if sys.platform == 'win32': 50 | print('win下行编辑退格时有奇怪的错误,已经关闭行编辑功能') 51 | print('切换至linux平台体验行编辑(WSL也不行\n') 52 | else: 53 | input = LineInput 54 | 55 | while True: 56 | line_slogan = self.line_symbol + self.curdir + '$' 57 | try: 58 | raw_command = input(line_slogan).strip() 59 | except KeyboardInterrupt: 60 | print('\n',self.logout) 61 | break 62 | 63 | if not raw_command: 64 | continue 65 | else: 66 | parser = Parser(raw_command) 67 | 68 | try: 69 | parser.run() 70 | except EOFError: 71 | # 如果是输入流被重定向到文件,文件读尽时会弹出这个错误,代表执行完毕,退出shell 72 | raise ShellExit 73 | except ShellExit: 74 | print(self.logout) 75 | break 76 | 77 | return True 78 | 79 | @property 80 | def curdir(self): 81 | path = os.path.abspath(os.path.curdir) 82 | path = path.strip('C:').replace("\\", '/') 83 | return path 84 | -------------------------------------------------------------------------------- /pysh/contrib/LineEdit/__init__.py: -------------------------------------------------------------------------------- 1 | from .action import * 2 | from .line_edit import LineInput, KeyMapping 3 | -------------------------------------------------------------------------------- /pysh/contrib/LineEdit/action.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import sys 3 | 4 | from pysh.command.exit import ShellExit 5 | from pysh.manage.middleware import Completer 6 | from .line_edit import LineInput, KeyMapping, show_history, LineEndError 7 | 8 | 9 | @LineInput.add_action([KeyMapping.end1, KeyMapping.end2]) 10 | def line_end(buffer): 11 | raise LineEndError() 12 | 13 | 14 | @LineInput.add_action([KeyMapping.bksp, KeyMapping.clear_prev_char]) 15 | def bksp(buffer): 16 | """ 17 | 退格删除 18 | """ 19 | buffer.pop() 20 | 21 | 22 | @LineInput.add_action([KeyMapping.completion]) 23 | def completion(buffer): 24 | """ 25 | Tab键命令补全 26 | """ 27 | if not buffer.content: 28 | return 29 | 30 | text = ''.join(buffer.content) 31 | results = [] 32 | for num in itertools.count(): 33 | result = Completer.search_symbol(text, num) 34 | if not result: 35 | break 36 | else: 37 | results.append(result) 38 | 39 | results = list(set(results)) 40 | 41 | if len(results) == 1: 42 | buffer.clear() 43 | buffer.content = list(results[0]) 44 | elif not results: 45 | return False 46 | else: 47 | print('\n') 48 | for result in results: 49 | print(result, end=' ') 50 | print('\n') 51 | 52 | print(buffer.slogan, ':', text, sep='', end='') 53 | sys.stdout.flush() 54 | 55 | return True 56 | 57 | 58 | @LineInput.add_action([KeyMapping.cursor_to_prev, KeyMapping.left]) 59 | def left(buffer): 60 | """ 61 | 左方向键,光标左移。 62 | """ 63 | if buffer.offset < len(buffer.content): 64 | buffer.offset += 1 65 | 66 | 67 | @LineInput.add_action([KeyMapping.cursor_to_next, KeyMapping.right]) 68 | def right(buffer): 69 | """ 70 | 右方向键,光标右移 71 | """ 72 | if buffer.offset > 0: 73 | buffer.offset -= 1 74 | 75 | 76 | @LineInput.add_action([KeyMapping.prev_history, KeyMapping.up]) 77 | def up(buffer): 78 | """ 79 | 上方向键,上一条历史记录 80 | """ 81 | show_history(buffer, 1) 82 | 83 | 84 | @LineInput.add_action([KeyMapping.next_history, KeyMapping.down]) 85 | def down(cls, buffer): 86 | """ 87 | 下方向键,下一条历史记录 88 | """ 89 | show_history(buffer, 0) 90 | 91 | 92 | @LineInput.add_action([KeyMapping.clear_prev_cursor]) 93 | def clear_prev_cursor(buffer): 94 | """ 95 | 清除光标之前的内容 96 | """ 97 | length = len(buffer.content) 98 | buffer._prev_remove = buffer.content[:length - buffer.offset] 99 | buffer.content = buffer.content[length - buffer.offset:] 100 | 101 | 102 | @LineInput.add_action([KeyMapping.recover_content]) 103 | def recover_content(buffer): 104 | """ 105 | 恢复之前清除的内容 106 | """ 107 | if buffer._prev_remove: 108 | buffer.content = buffer._prev_remove + buffer.content 109 | 110 | 111 | @LineInput.add_action([KeyMapping.cursor_to_top]) 112 | def cursor_to_top(buffer): 113 | """ 114 | 移动光标到行首 115 | """ 116 | buffer.offset = len(buffer.content) 117 | 118 | 119 | @LineInput.add_action([KeyMapping.cursor_to_end]) 120 | def cursor_to_end(buffer): 121 | """ 122 | 移动光标到行尾 123 | """ 124 | buffer.offset = 0 125 | 126 | 127 | @LineInput.add_action([KeyMapping.clear_prev_word]) 128 | def clear_prev_word(buffer): 129 | """ 130 | 清除光标之前的一个单词 131 | """ 132 | index = len(buffer.content) - buffer.offset - 1 133 | while index >= 0: 134 | if buffer.content[index] != ' ' \ 135 | and buffer.content[index] != '\t': 136 | buffer.content.pop(index) 137 | else: 138 | break 139 | 140 | index -= 1 141 | 142 | 143 | @LineInput.add_action([KeyMapping.clear_cursor_to_end]) 144 | def clear_cursor_to_end(buffer): 145 | """ 146 | 清除光标之后的内容 147 | """ 148 | if buffer.offset: 149 | buffer.content = buffer.content[:-buffer.offset] 150 | 151 | 152 | @LineInput.add_action([KeyMapping.swap_two_char]) 153 | def swap_two_char(buffer): 154 | """ 155 | 交换光标之前的两个字符 156 | """ 157 | index = len(buffer.content) - buffer.offset - 1 158 | buffer.content[index], buffer.content[index - 1] = buffer.content[index - 1], buffer.content[index] 159 | 160 | 161 | @LineInput.add_action([KeyMapping.exit_shell, KeyMapping.exit_program]) 162 | def exit_shell(buffer): 163 | """ 164 | 退出shell 165 | """ 166 | print('\n') 167 | raise ShellExit 168 | -------------------------------------------------------------------------------- /pysh/contrib/LineEdit/line_edit.py: -------------------------------------------------------------------------------- 1 | """ 2 | 实现行编辑的模块。 3 | 用这个模块中的LineInput类替换标准的input函数以在想要实现可行编辑的地方使用行编辑。 4 | """ 5 | import re 6 | import sys 7 | from collections import defaultdict 8 | 9 | from pysh.contrib.tools import getch, LineEndError 10 | from pysh.manage.env import History 11 | 12 | 13 | class Buffer: 14 | """ 15 | 存放当前编辑行的信息 16 | """ 17 | 18 | def __init__(self): 19 | # 光标位置。离末尾字符的偏移量 20 | self.offset = 0 21 | # 已经打印出来,显示在屏幕上的字符 22 | self.showed = 0 23 | # 已经打印出的光标的偏移量 24 | self.offseted = 0 25 | # 组合键缓冲区 26 | self._group_key = '' 27 | # 处于历史记录的位置 28 | self._history_index = -1 29 | # 前一次ctrl + U删除的内容 30 | self._prev_remove = [] 31 | # 缓冲区的字符 32 | self.content = [] 33 | 34 | def _group_key_buffer(self, key): 35 | """ 36 | 部分键比较特殊,是字符组合形式。比如按一下方向键产生三个字符 37 | 所以为了识别这类键建立一个特殊的方法。 38 | """ 39 | if key == KeyMapping.esc: 40 | self._group_key = KeyMapping.esc 41 | return False 42 | elif self._group_key.startswith(KeyMapping.esc): 43 | self._group_key += key 44 | 45 | if self._group_key == KeyMapping.left: 46 | self.pop() 47 | self._group_key = '' 48 | return KeyMapping.left 49 | elif self._group_key == KeyMapping.right: 50 | self.pop() 51 | self._group_key = '' 52 | return KeyMapping.right 53 | elif self._group_key == KeyMapping.up: 54 | self.pop() 55 | self._group_key = '' 56 | return KeyMapping.up 57 | elif self._group_key == KeyMapping.down: 58 | self.pop() 59 | self._group_key = '' 60 | return KeyMapping.down 61 | elif self._group_key == KeyMapping.home: 62 | self.pop() 63 | self.pop() 64 | self._group_key = '' 65 | return KeyMapping.home 66 | elif self._group_key == KeyMapping.end: 67 | self.pop() 68 | self.pop() 69 | self._group_key = '' 70 | return KeyMapping.end 71 | 72 | if len(self._group_key) >= 4: 73 | self._group_key = '' 74 | 75 | return False 76 | 77 | def flush(self): 78 | result = ''.join(self.content) 79 | self.offset = 0 80 | self.content.clear() 81 | return result 82 | 83 | def clear(self): 84 | self.pos = 0 85 | self.offset = 0 86 | self.content.clear() 87 | 88 | def add(self, value): 89 | self.content.append(value) 90 | return True 91 | 92 | def pop(self): 93 | index = len(self.content) - self.offset - 1 94 | if index >= 0: 95 | return self.content.pop(index) 96 | else: 97 | pass 98 | 99 | def insert(self, value, offset=None): 100 | if not offset: 101 | self.content.append(value) 102 | else: 103 | self.content.insert(len(self.content) - offset, value) 104 | 105 | def show(self): 106 | print(' ' * self.offseted, sep='', end='') 107 | print('\x08' * self.showed, ' ' * self.showed, '\x08' * self.showed, sep='', end='') 108 | content = ''.join(self.content) 109 | print(content, sep='', end='') 110 | print('\x08' * self.offset, sep='', end='') 111 | self.showed = len(content) 112 | self.offseted = self.offset 113 | sys.stdout.flush() 114 | 115 | 116 | class KeyMapping: 117 | end1 = '\n' 118 | end2 = '\r' 119 | bksp = '\x08' # 退格 120 | completion = '\t' # tab 121 | esc = '\x1b' 122 | left = '\x1b[D' 123 | right = '\x1b[C' 124 | up = '\x1b[A' 125 | down = '\x1b[B' 126 | clear_prev_cursor = '\x15' # ctrl + u 127 | clear_prev_char = '\x08' # ctrl + h 128 | exit_shell = '\x04' # ctrl + d 129 | exit_program = '\x03' # ctrl + c 130 | prev_history = '\x10' # ctrl + p 131 | next_history = '\x0e' # ctrl + n 132 | recover_content = '\x19' # ctrl + y 133 | cursor_to_top = '\x01' # ctrl + a 134 | cursor_to_end = '\x05' # ctrl + e 135 | clear_prev_word = '\x17' # ctrl + w 136 | clear_cursor_to_end = '\x0b' # ctrl + k 137 | swap_two_char = '\x14' # ctrl + t 138 | cursor_to_next = '\x06' # ctrl + f 139 | cursor_to_prev = '\x02' # ctrl + b 140 | home = '\x1b[1~' # home键 141 | end = '\x1b[4~' # end键 142 | 143 | direct_prev_history = '!!' 144 | prev_n_history = r'^!-(?P\d+)$' 145 | echo_prev_history = r'^!-(?P\d+):p$' 146 | last_like_history = r'^!\?(?P[a-zA-Z0-9]+)\?$' 147 | 148 | @classmethod 149 | def _get_map_dict(cls): 150 | return {name: getattr(cls, name) for name in dir(cls) \ 151 | if not name.startswith('_')} 152 | 153 | 154 | class LineInput: 155 | dispatcher = defaultdict(list) 156 | 157 | @staticmethod 158 | def __new__(cls, slogan): 159 | buffer = Buffer() 160 | buffer.slogan = slogan 161 | 162 | buffer.clear() 163 | 164 | print(slogan, ':', sep='', end='') 165 | sys.stdout.flush() 166 | 167 | while True: 168 | char = getch().strip('\x00') 169 | try: 170 | if not len(char) == 1: 171 | for ch in char: 172 | cls._handler(ch, buffer) 173 | else: 174 | cls._handler(char, buffer) 175 | except LineEndError: 176 | print('\n') 177 | break 178 | 179 | text = cls._command_handler(buffer.flush(), buffer) 180 | return text 181 | 182 | @classmethod 183 | def add_action(cls, symbols=(None)): 184 | def decorator(func): 185 | for symbol in symbols: 186 | cls.dispatcher[symbol].append(func) 187 | return func 188 | 189 | return decorator 190 | 191 | @classmethod 192 | def _handler(cls, char, buffer): 193 | is_group = buffer._group_key_buffer(char) 194 | 195 | if is_group: 196 | char = is_group 197 | elif ord(char) in range(32, 127): 198 | buffer.insert(char, buffer.offset) 199 | elif char not in cls.dispatcher: 200 | char = None 201 | 202 | if char in cls.dispatcher: 203 | for func in cls.dispatcher[char]: 204 | func(buffer) 205 | 206 | buffer.show() 207 | 208 | @classmethod 209 | def _command_handler(cls, text, buffer): 210 | if text == KeyMapping.direct_prev_history: 211 | return cls.direct_prev_history(buffer) 212 | 213 | num = re.findall(KeyMapping.prev_n_history, text) 214 | if num: 215 | num = num[0] 216 | return cls.prev_n_history(num) 217 | 218 | num = re.findall(KeyMapping.echo_prev_history, text) 219 | if num: 220 | num = num[0] 221 | return cls.echo_prev_history(num) 222 | 223 | like_text = re.findall(KeyMapping.last_like_history, text) 224 | if like_text: 225 | like_text = like_text[0] 226 | return cls.last_like_history(like_text) 227 | 228 | return text 229 | 230 | @classmethod 231 | def _get_history(cls, buffer, arrow): 232 | """ 233 | 得到指定的历史记录。 234 | arrow为 1 代表下一条历史记录,为 0 代表上一条历史记录 235 | """ 236 | length = len(History.history) - 1 237 | arrow = 'prev' if arrow else 'next' 238 | if arrow == 'prev': 239 | if buffer._history_index < length: 240 | buffer._history_index += 1 241 | return History.history[length - buffer._history_index] 242 | else: 243 | return History.history[length] 244 | elif arrow == 'next': 245 | if buffer._history_index > 1: 246 | buffer._history_index -= 1 247 | return History.history[length - buffer._history_index] 248 | else: 249 | return History.history[0] 250 | 251 | @classmethod 252 | def direct_prev_history(cls, buffer): 253 | """ 254 | 直接执行上一条历史记录 255 | """ 256 | return cls._get_history(buffer, 1) 257 | 258 | @classmethod 259 | def prev_n_history(cls, n): 260 | """ 261 | 执行上第n条历史记录 262 | """ 263 | index = len(History.history) - int(n) 264 | return History.history[index] 265 | 266 | @classmethod 267 | def echo_prev_history(cls, n): 268 | """ 269 | 打印上第n条历史记录不执行 270 | """ 271 | index = len(History.history) - int(n) 272 | print(History.history[index]) 273 | return '' 274 | 275 | @classmethod 276 | def last_like_history(cls, text): 277 | """ 278 | 执行最近一条包含text的历史记录 279 | """ 280 | index = len(History.history) - 1 281 | while index >= 0: 282 | if text in History.history[index]: 283 | print(History.history[index]) 284 | return History.history[index] 285 | 286 | index -= 1 287 | 288 | return '' 289 | 290 | 291 | def show_history(buffer, arrow): 292 | """ 293 | 展示历史记录,arrow代表方向 294 | """ 295 | history = LineInput._get_history(buffer, arrow) 296 | 297 | buffer.content = list(history) 298 | buffer.offseted = buffer.offset = 0 299 | return True 300 | -------------------------------------------------------------------------------- /pysh/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arianxx/Pysh/d44f7303bc4ce3764a300830d361115200ad3602/pysh/contrib/__init__.py -------------------------------------------------------------------------------- /pysh/contrib/control.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 这里定义具体的各个符号和语句的行为,并由parser.Handler.run进行调用 4 | """ 5 | import os 6 | import re 7 | from functools import partial 8 | 9 | from .keyword import Symbol, Keyword 10 | from .parser import Parser, Handler 11 | from ..command import pysh, exit 12 | from ..manage.env import Variable, EnvVariable 13 | from ..manage.middleware import StdoutRedirection, StdinRedirection 14 | 15 | 16 | class SymbolAction: 17 | """ 18 | 注册符号行为的类。 19 | no_binding_action收集注册的、尚未绑定实例的类。 20 | """ 21 | no_binding_action = {} 22 | 23 | def __init__(self, handler): 24 | """ 25 | 实例化时绑定parser.Handle的实例,并将自身与收集的行为函数绑定,以便能够像原始方法那样调用。 26 | """ 27 | self.handler = handler 28 | self.action = {} 29 | for name in self.no_binding_action: 30 | self.action[name] = partial(self.no_binding_action[name], self) 31 | 32 | def __getattr__(self, name): 33 | """ 34 | 使访问注册的方法能够像访问原始方法一样。 35 | :param name: 注册的方法名 36 | :return: 37 | """ 38 | if name in self.action: 39 | return self.action[name] 40 | else: 41 | raise AttributeError( 42 | "{cls.__name__} object has no attribute {name}".format(cls=type(self), name=name) 43 | ) 44 | 45 | @classmethod 46 | def register(cls, func): 47 | name = func.__name__ 48 | cls.no_binding_action.update( 49 | { 50 | name: func 51 | } 52 | ) 53 | 54 | 55 | class KeywordAction: 56 | """ 57 | 注册关键字行为的类。 58 | 与SymbolAction类似 59 | """ 60 | no_binding_action = {} 61 | 62 | def __init__(self, handler): 63 | self.handler = handler 64 | self.action = {} 65 | for name in self.no_binding_action: 66 | self.action[name] = partial(self.no_binding_action[name], self) 67 | 68 | def __getattr__(self, name): 69 | if name in self.action: 70 | return self.action[name] 71 | else: 72 | raise AttributeError( 73 | "{cls.__name__} object has no attribute {name}".format(cls=type(self), name=name) 74 | ) 75 | 76 | @classmethod 77 | def register(cls, func): 78 | name = func.__name__ 79 | cls.no_binding_action.update( 80 | { 81 | name: func 82 | } 83 | ) 84 | 85 | 86 | # 以下注册符号行为 87 | 88 | @SymbolAction.register 89 | def semicolon(self, tokens): 90 | """ 91 | 分割命令。 92 | """ 93 | if Symbol.mapping['semicolon'].char in tokens: 94 | tokens_list = Symbol.mapping['semicolon'].cls.handle(tokens) 95 | self.handler.tokens = None 96 | for tokens in tokens_list: 97 | Handler(tokens).run() 98 | 99 | return 100 | 101 | 102 | @SymbolAction.register 103 | def backend(self, tokens): 104 | """ 105 | 是否后台执行 106 | """ 107 | backend = False 108 | if tokens and tokens[-1] == Symbol.mapping['backend'].char: 109 | self.handler.tokens = Symbol.mapping['backend'].cls.handle(tokens) 110 | backend = True 111 | 112 | return backend 113 | 114 | 115 | @SymbolAction.register 116 | def double_and(self, tokens): 117 | """ 118 | 逻辑与 119 | """ 120 | if Symbol.mapping['double_and'].char in tokens: 121 | tokens_list = Symbol.mapping['double_and'].cls.handle(tokens) 122 | self.handler.tokens = None 123 | for tokens in tokens_list: 124 | if Handler(tokens).run(): 125 | continue 126 | else: 127 | break 128 | return 129 | 130 | 131 | @SymbolAction.register 132 | def dollars(self, tokens): 133 | """ 134 | 当前shell id 135 | """ 136 | cls = Symbol.mapping['dollars'].cls 137 | self.handler.tokens = cls.handle(tokens) 138 | return 139 | 140 | 141 | @SymbolAction.register 142 | def wave_line(self, tokens): 143 | """ 144 | 用户目录 145 | """ 146 | cls = Symbol.mapping['wave_line'].cls 147 | self.handler.tokens = cls.handle(tokens) 148 | return 149 | 150 | 151 | @SymbolAction.register 152 | def equal(self, tokens): 153 | """ 154 | 变量赋值 155 | """ 156 | cls = Symbol.mapping['equal'].cls 157 | vr_mapping, self.handler.tokens = cls.handle(tokens) 158 | Variable.variable.update(vr_mapping) 159 | return 160 | 161 | 162 | @SymbolAction.register 163 | def dollar(self, tokens): 164 | """ 165 | 变量展开 166 | """ 167 | cls = Symbol.mapping['dollar'].cls 168 | self.handler.tokens = cls.handle(tokens) 169 | return 170 | 171 | 172 | @SymbolAction.register 173 | def right_angel(self, tokens): 174 | """ 175 | 原来设想的输入输出重定向阶段,应该在分发应用那里,使用装饰器实现来着。 176 | 可是,后来感觉这样解析参数、传来传去的太麻烦,还是就这样吧…… 177 | """ 178 | cls = Symbol.mapping['right_angel'].cls 179 | if cls.char in tokens: 180 | file_path, self.handler.tokens = cls.handle(tokens) 181 | try: 182 | # 覆盖文件 183 | sr = StdoutRedirection(file_path, file_override=True) 184 | except FileNotFoundError as e: 185 | print(e) 186 | self.handler.tokens = None 187 | except PermissionError as e: 188 | print(e) 189 | self.handler.tokens = None 190 | else: 191 | with sr.context(): 192 | Handler(self.handler.tokens).run() 193 | self.handler.tokens = None 194 | return 195 | 196 | 197 | @SymbolAction.register 198 | def double_right_angel(self, tokens): 199 | """ 200 | 重定向 201 | """ 202 | cls = Symbol.mapping['double_right_angel'].cls 203 | if cls.char in tokens: 204 | file_path, self.handler.tokens = cls.handle(tokens) 205 | try: 206 | # 不覆盖文件 207 | sr = StdoutRedirection(file_path, file_override=False) 208 | except FileNotFoundError as e: 209 | print(e) 210 | self.handler.tokens = None 211 | except PermissionError as e: 212 | print(e) 213 | self.handler.tokens = None 214 | else: 215 | with sr.context(): 216 | Handler(self.handler.tokens).run() 217 | self.handler.tokens = None 218 | return 219 | 220 | 221 | @SymbolAction.register 222 | def left_angel(self, tokens): 223 | """ 224 | 重定向输入流 225 | """ 226 | cls = Symbol.mapping['left_angel'].cls 227 | if cls.char in tokens: 228 | file_path, self.handler.tokens = cls.handle(tokens) 229 | try: 230 | si = StdinRedirection(file_path=file_path) 231 | except FileNotFoundError as e: 232 | print(e) 233 | self.handler.tokens = None 234 | except PermissionError as e: 235 | print(e) 236 | self.handler.tokens = None 237 | else: 238 | try: 239 | with si.context(): 240 | Handler(self.handler.tokens).run() 241 | except UnicodeDecodeError as e: 242 | # 指定一个非文本文件,读取时会弹出此错误 243 | print(e) 244 | finally: 245 | self.handler.tokens = None 246 | return 247 | 248 | 249 | @SymbolAction.register 250 | def double_left_angel(self, tokens): 251 | """ 252 | 标记式重定向输入流 253 | """ 254 | cls = Symbol.mapping['double_left_angel'].cls 255 | if cls.char in tokens: 256 | tag, tokens = cls.handle(tokens) 257 | 258 | if tag: 259 | pipe = [] 260 | while True: 261 | token = input('...') 262 | if token == tag: 263 | break 264 | else: 265 | pipe.append(token + '\n') 266 | si = StdinRedirection(source=pipe) 267 | 268 | try: 269 | with si.context(): 270 | Handler(self.handler.tokens).run() 271 | except TypeError as e: 272 | print(e) 273 | finally: 274 | self.handler.tokens = None 275 | return 276 | 277 | 278 | @SymbolAction.register 279 | def back_quote(self, tokens): 280 | """ 281 | 命令替代 282 | """ 283 | cls = Symbol.mapping['back_quote'].cls 284 | if cls.char in tokens: 285 | sub_tokens = cls.handle(tokens) 286 | if sub_tokens: 287 | for token in sub_tokens: 288 | sr = StdoutRedirection() 289 | with sr.context() as out: 290 | Handler(token).run() 291 | 292 | raw_token = ''.join(out.pipe) 293 | self.handler.raw_token = re.sub('`.*`', 294 | raw_token, 295 | self.handler.raw_token, 1) 296 | Parser(self.handler.raw_token).run() 297 | self.handler.tokens = None 298 | return 299 | 300 | 301 | @SymbolAction.register 302 | def pipe(self, tokens): 303 | """ 304 | 前一个命令的输出作为下一个命令的输入 305 | """ 306 | cls = Symbol.mapping['pipe'].cls 307 | if cls.char in tokens: 308 | tokens = cls.handle(tokens) 309 | for index, token in enumerate(tokens): 310 | if index == 0: 311 | sr = StdoutRedirection() 312 | with sr.context() as sr: 313 | Handler(token).run() 314 | elif index == len(tokens) - 1: 315 | si = StdinRedirection(source=sr.pipe) 316 | with si.context(): 317 | Handler(token).run() 318 | else: 319 | si = StdinRedirection(source=sr.pipe) 320 | sr = StdoutRedirection() 321 | with si.context(), sr.context() as sr: 322 | Handler(token).run() 323 | 324 | self.handler.tokens = None 325 | return 326 | 327 | 328 | # 以下注册关键字行为 329 | 330 | 331 | @KeywordAction.register 332 | def daemon(self, tokens): 333 | """ 334 | 守护模式 335 | """ 336 | daemon_map = Keyword.mapping['daemon'] 337 | daemon = False 338 | if tokens and tokens[-1] == daemon_map.words: 339 | self.handler.tokens = daemon_map.cls.handle(tokens) 340 | daemon = True 341 | 342 | return daemon 343 | 344 | 345 | @KeywordAction.register 346 | def join(self, tokens): 347 | """ 348 | 阻塞模式 349 | """ 350 | join_map = Keyword.mapping['join'] 351 | join = False 352 | if join_map.words in tokens: 353 | self.handler.tokens = join_map.cls.handle(tokens) 354 | join = True 355 | 356 | return join 357 | 358 | 359 | @KeywordAction.register 360 | def export(self, tokens): 361 | """ 362 | 设置环境变量 363 | """ 364 | export_map = Keyword.mapping['export'] 365 | if tokens and export_map.cls.words == tokens[0]: 366 | vr_dict = export_map.cls.handle(tokens)[0] 367 | self.handler.tokens = None 368 | 369 | for key, value in vr_dict.items(): 370 | EnvVariable.set_env_variable(key, value) 371 | 372 | return True 373 | 374 | 375 | @KeywordAction.register 376 | def exec(self, tokens): 377 | """ 378 | exec命令 379 | """ 380 | cls = Keyword.mapping['exec'] 381 | if tokens and tokens[0] == cls.words: 382 | tokens.pop(0) 383 | if len(tokens) >= 1: 384 | if tokens[0] == '>': 385 | tokens.pop(0) 386 | file_path = tokens[0] 387 | exec_shell(file_path) 388 | raise exit.ShellExit 389 | else: 390 | Handler(tokens).run() 391 | self.handler.tokens = None 392 | raise exit.ShellExit 393 | 394 | self.handler.tokens = tokens 395 | return 396 | 397 | 398 | # 辅助函数 399 | def exec_shell(file_path): 400 | while True: 401 | path = os.path.abspath(os.path.curdir).strip('C:').replace("\\", '/') 402 | line_slogan = pysh.pysh.line_symbol + path + '$' 403 | raw_command = input(line_slogan).strip() 404 | 405 | sr = StdoutRedirection(file_path, False) 406 | 407 | with sr.context(): 408 | if not raw_command: 409 | continue 410 | else: 411 | parser = Parser(raw_command) 412 | 413 | try: 414 | parser.run() 415 | except EOFError: 416 | raise exit.ShellExit 417 | except exit.ShellExit: 418 | break 419 | 420 | return True 421 | -------------------------------------------------------------------------------- /pysh/contrib/control_flow.py: -------------------------------------------------------------------------------- 1 | """ 2 | 实现控制流的模块。 3 | 拦截待分发的命令,解析关键词,根据不同逻辑做出相应调度 4 | """ 5 | 6 | class ControlFlow: 7 | storage = {} 8 | 9 | @classmethod 10 | def register(cls, control_cls): 11 | cls.storage.update({ 12 | control_cls.__name__:control_cls, 13 | }) 14 | return control_cls 15 | 16 | 17 | @ControlFlow.register 18 | class WhileControl: 19 | keyword = 'while' 20 | 21 | def __init__(self): 22 | pass 23 | -------------------------------------------------------------------------------- /pysh/contrib/keyword.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """" 3 | 注册特殊符号或关键字。对tokens做一些与符号本身相关必要的处理(如清洗),具体的调用逻辑交给parser.Handle, 4 | 复杂的行为交给control.SymbolAction/control.KeywordAction。 5 | 6 | 类应该按照功能命名而非符号。然而自从开始的几个类名没留意后,惯性的力量是巨大的…… 7 | """ 8 | import os 9 | import re 10 | from collections import namedtuple, OrderedDict 11 | 12 | from ..manage.env import Processing, Variable, EnvVariable 13 | 14 | symbol_mapping = namedtuple('symbol', ['char', 'name', 'derc', 'cls']) 15 | keyword_mapping = namedtuple('keyword', ['words', 'name', 'derc', 'cls']) 16 | 17 | handle_state = namedtuple('state', ['state', 'tokens', 'others']) 18 | 19 | 20 | class Symbol: 21 | """ 22 | 注册特殊符号 23 | """ 24 | mapping = OrderedDict() 25 | 26 | @classmethod 27 | def register(cls, mapping_cls): 28 | """ 29 | 注册特殊符号。 30 | 31 | :param mapping_cls: 遇到特殊符号时将要回调的类。 32 | :return: 返回原类 33 | """ 34 | cls.mapping[mapping_cls.name] = symbol_mapping( 35 | mapping_cls.char, 36 | mapping_cls.name, 37 | mapping_cls.derc, 38 | mapping_cls, 39 | ) 40 | 41 | return mapping_cls 42 | 43 | @classmethod 44 | def registered_symbol(cls): 45 | return OrderedDict(cls.mapping) 46 | 47 | 48 | class Keyword: 49 | """ 50 | 注册关键字 51 | """ 52 | mapping = OrderedDict() 53 | 54 | @classmethod 55 | def register(cls, mapping_cls): 56 | """ 57 | 注册关键字。 58 | 59 | :param mapping_cls: 映射关键字行为的类。 60 | :return: 返回原类 61 | """ 62 | cls.mapping[mapping_cls.name] = keyword_mapping( 63 | mapping_cls.words, 64 | mapping_cls.name, 65 | mapping_cls.derc, 66 | mapping_cls, 67 | ) 68 | 69 | return mapping_cls 70 | 71 | @classmethod 72 | def registered_keyword(cls): 73 | return OrderedDict(cls.mapping) 74 | 75 | 76 | # 以下为符号注册 77 | 78 | 79 | @Symbol.register 80 | class Semicolon: 81 | """ 82 | 执行多条分明的符号 83 | """ 84 | char = ';' 85 | name = 'semicolon' 86 | derc = '分割多条命令' 87 | 88 | @classmethod 89 | def handle(cls, tokens): 90 | """ 91 | 返回经cls.chars分割后的tokens列表。 92 | 93 | :param tokens: 等待分割的tokens 94 | :return: 分割后的列表 95 | """ 96 | token_list = [] 97 | 98 | def split(tokens): 99 | nonlocal token_list 100 | if cls.char in tokens: 101 | flag = tokens.index(cls.char) 102 | token_list.append(tokens[0:flag]) 103 | tokens = tokens[flag + 1:] 104 | split(tokens) 105 | else: 106 | if tokens: 107 | token_list.append(tokens) 108 | return 109 | 110 | split(tokens) 111 | 112 | return token_list 113 | 114 | 115 | @Symbol.register 116 | class Backend: 117 | """ 118 | 末尾有这个符号即启动子进程后台执行。 119 | """ 120 | char = '&' 121 | name = 'backend' 122 | derc = '启动多进程' 123 | 124 | @classmethod 125 | def handle(cls, tokens): 126 | """ 127 | 简单的去掉符号并返回,逻辑交给parser中的handler。 128 | :param tokens: 129 | :return: 130 | """ 131 | tokens = tokens[:-1] 132 | return tokens 133 | 134 | 135 | @Symbol.register 136 | class DoubleAnd: 137 | """ 138 | 逻辑与,只有第一个命令成功返回才执行第二个命令。 139 | """ 140 | char = '&&' 141 | name = 'double_and' 142 | derc = '逻辑与' 143 | 144 | @classmethod 145 | def handle(cls, tokens): 146 | """ 147 | 像semicolon那样分割列表 148 | :param tokens: 149 | :return: 150 | """ 151 | tokens = [ 152 | token if token != cls.char else Semicolon.char 153 | for token in tokens 154 | ] 155 | 156 | return Semicolon.handle(tokens) 157 | 158 | 159 | @Symbol.register 160 | class Dollars: 161 | """ 162 | 替换为当前shell id. 163 | """ 164 | char = '$$' 165 | name = 'dollars' 166 | derc = '当前shell id' 167 | 168 | @classmethod 169 | def handle(cls, tokens): 170 | symbol = cls.char 171 | shell_id = get_shell_id() 172 | tokens = list(map( 173 | lambda token: token.replace(symbol, str(shell_id)) 174 | if cls.char in token 175 | else token, 176 | tokens 177 | )) 178 | return tokens 179 | 180 | 181 | @Symbol.register 182 | class WaveLine: 183 | """ 184 | 替换为用户目录。 185 | """ 186 | char = '~' 187 | name = 'wave_line' 188 | derc = '用户目录' 189 | # win用户目录储存在USERPROFILE环境变量里,linux为HOME 190 | catalog = os.environ.get('USERPROFILE', None) or os.environ.get('HOME', None) 191 | 192 | @classmethod 193 | def handle(cls, tokens): 194 | tokens = list(map( 195 | lambda token: token if token != cls.char 196 | else cls.catalog, 197 | tokens 198 | )) 199 | return tokens 200 | 201 | 202 | @Symbol.register 203 | class Equal: 204 | """ 205 | 设置变量的符号 206 | """ 207 | char = '=' 208 | name = 'equal' 209 | derc = '变量赋值' 210 | 211 | @classmethod 212 | def handle(cls, tokens): 213 | re_str = r'^[a-zA-Z][a-zA-Z1-9]*=.+$' 214 | variable_mapping = {} 215 | for token in list(tokens): 216 | # 如果都是设置变量的语句,就记录 217 | if re.match(re_str, token): 218 | vr_pair = token.split('=') 219 | variable_mapping.update({vr_pair[0]: vr_pair[1]}) 220 | tokens.pop(tokens.index(token)) 221 | else: 222 | # 如果后面有其它语句,前面的设置变量的语句就无效 223 | # 模仿bash的行为 224 | variable_mapping.clear() 225 | break 226 | return (variable_mapping, tokens) 227 | 228 | 229 | @Symbol.register 230 | class Dollar: 231 | """ 232 | 读取变量 233 | """ 234 | char = '$' 235 | name = 'dollar' 236 | derc = '变量读取声明' 237 | 238 | @classmethod 239 | def handle(cls, tokens): 240 | tokens = list(map( 241 | variable_replace, 242 | tokens 243 | )) 244 | return tokens 245 | 246 | 247 | @Symbol.register 248 | class BackQuote: 249 | """ 250 | 反引号。用于命令替代 251 | """ 252 | char = '`' 253 | name = 'back_quote' 254 | derc = '命令替代' 255 | 256 | @classmethod 257 | def handle(cls, tokens): 258 | sub_tokens = [] 259 | get_sub_tokens(sub_tokens, tokens) 260 | return sub_tokens 261 | 262 | 263 | @Symbol.register 264 | class RightAngel: 265 | """ 266 | 重定向输入符号,覆盖文件 267 | """ 268 | char = '>' 269 | name = 'right_angel' 270 | derc = '覆盖式重定向输出' 271 | 272 | @classmethod 273 | def handle(cls, tokens): 274 | index = tokens.index(cls.char) 275 | try: 276 | tag = tokens[index + 1] 277 | except IndexError: 278 | tag = '' 279 | tokens.pop(index) 280 | else: 281 | tokens.pop(index) 282 | tokens.pop(index) 283 | 284 | return tag, tokens 285 | 286 | 287 | @Symbol.register 288 | class DoubleRightAngel: 289 | """ 290 | 重定向输出,文件存在不覆盖 291 | """ 292 | char = '>>' 293 | name = 'double_right_angel' 294 | derc = '非覆盖式重定向输出' 295 | 296 | @classmethod 297 | def handle(cls, tokens): 298 | index = tokens.index(cls.char) 299 | try: 300 | tag = tokens[index + 1] 301 | except IndexError: 302 | tag = '' 303 | tokens.pop(index) 304 | else: 305 | tokens.pop(index) 306 | tokens.pop(index) 307 | 308 | return tag, tokens 309 | 310 | 311 | @Symbol.register 312 | class LeftAngel: 313 | """ 314 | 将文件内容作为命令输入 315 | """ 316 | char = '<' 317 | name = 'left_angel' 318 | derc = '重定向输入' 319 | 320 | @classmethod 321 | def handle(cls, tokens): 322 | index = tokens.index(cls.char) 323 | try: 324 | tag = tokens[index + 1] 325 | except IndexError: 326 | tag = '' 327 | tokens.pop(index) 328 | else: 329 | tokens.pop(index) 330 | tokens.pop(index) 331 | 332 | return tag, tokens 333 | 334 | 335 | @Symbol.register 336 | class DoubleLeftAngel: 337 | """ 338 | 标记式重定向输入。 339 | 将指定标记之间的内容作为输入流。 340 | """ 341 | char = '<<' 342 | name = 'double_left_angel' 343 | derc = '标记式重定向输入' 344 | 345 | @classmethod 346 | def handle(cls, tokens): 347 | index = tokens.index(cls.char) 348 | try: 349 | tag = tokens[index + 1] 350 | except IndexError: 351 | tag = '' 352 | tokens.pop(index) 353 | else: 354 | tokens.pop(index) 355 | tokens.pop(index) 356 | 357 | return tag, tokens 358 | 359 | 360 | @Symbol.register 361 | class Pipe: 362 | """ 363 | 管道功能 364 | """ 365 | char = '|' 366 | name = 'pipe' 367 | derc = '前一个命令的输出,作为下一个命令的输入' 368 | 369 | @classmethod 370 | def handle(cls, tokens): 371 | if cls.char in tokens: 372 | index = [index 373 | for index, value in enumerate(tokens) 374 | if value == cls.char] 375 | return split_tokens(tokens, index) 376 | 377 | 378 | # 以下为关键字注册 379 | 380 | @Keyword.register 381 | class Daemon: 382 | """ 383 | 是否以守护进程方式启动子进程 384 | """ 385 | words = '--daemon' 386 | name = 'daemon' 387 | derc = '进程守护模式' 388 | 389 | @classmethod 390 | def handle(cls, tokens): 391 | tokens = tokens[:-1] 392 | return tokens 393 | 394 | 395 | @Keyword.register 396 | class Join: 397 | """ 398 | 子进程是否阻塞 399 | """ 400 | words = '--join' 401 | name = 'join' 402 | derc = '进程阻塞' 403 | 404 | @classmethod 405 | def handle(cls, tokens): 406 | tokens = list(filter( 407 | lambda token: True if token != cls.words else False, 408 | tokens 409 | )) 410 | return tokens 411 | 412 | 413 | @Keyword.register 414 | class Export: 415 | """ 416 | 设置环境变量 417 | """ 418 | words = 'export' 419 | name = 'export' 420 | derc = '设置环境变量' 421 | 422 | @classmethod 423 | def handle(cls, tokens): 424 | tokens = tokens[1:] 425 | return Equal.handle(tokens) 426 | 427 | 428 | @Keyword.register 429 | class Exec: 430 | """ 431 | 执行单条命令,或整体重定向shell输出 432 | """ 433 | words = 'exec' 434 | name = 'exec' 435 | derc = '执行单条命令/重定向shell输出' 436 | 437 | @classmethod 438 | def handle(cls, tokens): 439 | return tokens 440 | 441 | 442 | # 以下为辅助函数 443 | 444 | 445 | def get_shell_id(): 446 | """ 447 | 得到当前shell id 448 | """ 449 | processing = Processing.get_records() 450 | shell_processing = list(filter( 451 | lambda id: True if processing[id]['NAME'] == 'pysh' else False, 452 | processing 453 | )) 454 | return list(shell_processing)[-1] 455 | 456 | 457 | def variable_replace(token): 458 | """ 459 | 替换命令里面的变量为值。 460 | """ 461 | re_str = r'(?P\$[a-zA-Z][a-zA-Z1-9]*)' 462 | vr_declares = re.findall(re_str, token) 463 | for vr_declare in vr_declares: 464 | vr = Variable.variable.get(vr_declare[1:], False) \ 465 | or EnvVariable.variable.get(vr_declare[1:], '') 466 | token = token.replace(vr_declare, vr) 467 | 468 | return token 469 | 470 | 471 | def get_sub_tokens(sub_tokens, tokens): 472 | """ 473 | 递归清洗tokens,得到所有子命令 474 | 475 | :param sub_tokens: 收集子命令的列表 476 | :param tokens: 原token 477 | :return: sub_tokens 478 | """ 479 | sub_token = [] 480 | try: 481 | bq_1 = tokens.index(BackQuote.char) 482 | except ValueError: 483 | return sub_tokens 484 | else: 485 | find = False 486 | for token in tokens[bq_1 + 1:]: 487 | if token != BackQuote.char: 488 | sub_token.append(token) 489 | else: 490 | find = True 491 | break 492 | else: 493 | sub_token.clear() 494 | 495 | if find: 496 | tokens.pop(bq_1) 497 | bq_2 = tokens.index(BackQuote.char) 498 | sub_tokens.append(sub_token) 499 | try: 500 | get_sub_tokens(sub_tokens, tokens[bq_2 + 1:]) 501 | except IndexError: 502 | return sub_tokens 503 | else: 504 | return sub_tokens 505 | 506 | 507 | def split_tokens(tokens, index): 508 | """ 509 | 用于管道功能,按管道出现位置分割命令 510 | 511 | :param tokens: 初步分割后的命令列表 512 | :param index: 管道在列表中出现的位置列表 513 | :return: 分割后的列表 514 | """ 515 | result = [[]] 516 | result[-1].extend(tokens[0:index[0]]) 517 | for i, _ in enumerate(index): 518 | result.append([]) 519 | if i < len(index) - 1: 520 | try: 521 | result[-1].extend( 522 | tokens[index[i] + 1:index[i + 1]] 523 | ) 524 | except IndexError: 525 | result.pop(-1) 526 | break 527 | result[-1].extend( 528 | tokens[index[i] + 1:] 529 | ) 530 | return result 531 | -------------------------------------------------------------------------------- /pysh/contrib/parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 解析输入的命令。如果命令有特殊符号,执行特殊符号指定的操作。如果命令有控制语句,将解析之后的命令列表交给控制语句。 4 | """ 5 | from shlex import shlex 6 | 7 | from ..manage.dispatch import dispatch 8 | 9 | 10 | class Parser: 11 | def __init__(self, token): 12 | """ 13 | 对输入的命令做初步分割处理,然后委派给后续类。 14 | 15 | :param token: 输入的命令 16 | """ 17 | self.token = token 18 | 19 | self.shlex = shlex(self.token, punctuation_chars=True) 20 | self.shlex.wordchars += '$.\//:' 21 | 22 | try: 23 | self.tokens = list(self.shlex) 24 | except ValueError as e: 25 | print(e) 26 | return 27 | 28 | def run(self): 29 | Handler(self).run() 30 | 31 | 32 | class Handler: 33 | """ 34 | 对初步分割之后的命令做进一步处理,并执行 35 | """ 36 | _tokens = None 37 | 38 | def __init__(self, parser): 39 | try: 40 | tokens = parser.tokens 41 | self.raw_token = parser.token 42 | except AttributeError: 43 | # 传入Parser的实例对Handler进行初始化是后来为命令替代改变的特性 44 | # 原来是直接传入分割后的tokens 45 | # 所以加入try以适应旧的调用方式 46 | try: 47 | tokens = parser 48 | self.raw_token = ' '.join(parser) 49 | except TypeError as e: 50 | print(e) 51 | return 52 | 53 | tokens = list(filter(lambda token: token, tokens)) 54 | self.tokens = list(map(lambda token: token.strip(), tokens)) 55 | 56 | @property 57 | def tokens(self): 58 | return self._tokens 59 | 60 | @tokens.setter 61 | def tokens(self, tokens): 62 | """ 63 | tokens是一个特殊属性,在tokens发生改变时,同步修改command和args. 64 | 65 | :param tokens: 变更的tokens 66 | :return: None 67 | """ 68 | self.args = None 69 | self.command = None 70 | 71 | self._tokens = tokens 72 | 73 | if self.tokens: 74 | self.command = self.tokens[0] 75 | if len(self.tokens) > 1: 76 | self.args = self.tokens[1:] 77 | 78 | def _unfold_literal(self): 79 | """ 80 | 展开字面量 81 | """ 82 | if self.tokens: 83 | # 展开shell pid 84 | self.sa.dollars(self.tokens) 85 | # 展开用户目录 86 | self.sa.wave_line(self.tokens) 87 | # 设置变量 88 | self.sa.equal(self.tokens) 89 | # 展开变量 90 | self.sa.dollar(self.tokens) 91 | # 环境变量 92 | self.ka.export(self.tokens) 93 | 94 | def _backend(self): 95 | """ 96 | 后台启动相关判断 97 | """ 98 | # 是否后台启动 99 | backend = self.sa.backend(self.tokens) 100 | daemon = False 101 | join = False 102 | if backend: 103 | # 是否守护模式、阻塞 104 | daemon = self.ka.daemon(self.tokens) 105 | join = self.ka.join(self.tokens) 106 | 107 | self.backend = backend 108 | self.daemon = daemon 109 | self.join = join 110 | 111 | def _redirect(self): 112 | """ 113 | 重定向、替换相关逻辑 114 | """ 115 | if self.tokens: 116 | # 管道 117 | self.sa.pipe(self.tokens) 118 | 119 | if self.tokens: 120 | # 重定向输入 121 | self.sa.left_angel(self.tokens) 122 | 123 | if self.tokens: 124 | # 标记式重定向输入 125 | self.sa.double_left_angel(self.tokens) 126 | 127 | if self.tokens: 128 | # 重定向输出,覆盖文件 129 | self.sa.right_angel(self.tokens) 130 | 131 | if self.tokens: 132 | # 重定向输出,不覆盖 133 | self.sa.double_right_angel(self.tokens) 134 | 135 | if self.tokens: 136 | # 命令替换 137 | self.sa.back_quote(self.tokens) 138 | 139 | def _division(self): 140 | """ 141 | 分割语句相关的判断 142 | """ 143 | if self.tokens: 144 | # 分号分割语句 145 | self.sa.semicolon(self.tokens) 146 | 147 | if self.tokens: 148 | # 逻辑与 149 | self.sa.double_and(self.tokens) 150 | 151 | def _dispatch(self): 152 | """ 153 | 分发命令 154 | """ 155 | if self.tokens: 156 | # 如果没有tokens,说明命令在解析中已经递归执行完毕,直接返回。 157 | if self.args: 158 | rv = dispatch.dispatch(self.command, *self.args, backend=self.backend, daemon=self.daemon, join=self.join) 159 | else: 160 | rv = dispatch.dispatch(self.command, backend=self.backend, daemon=self.daemon, join=self.join) 161 | else: 162 | rv = True 163 | 164 | return rv 165 | 166 | def run(self): 167 | """ 168 | 控制判断符号、关键字的具体逻辑。常在control具体命令中递归执行。 169 | 170 | :return: None 171 | """ 172 | # 放在这里避免循环导入 173 | from .control import SymbolAction, KeywordAction 174 | self.sa = SymbolAction(self) 175 | self.ka = KeywordAction(self) 176 | 177 | if self.tokens: 178 | # exec语句 179 | self.ka.exec(self.tokens) 180 | 181 | self._division() 182 | 183 | self._unfold_literal() 184 | 185 | self._backend() 186 | 187 | self._redirect() 188 | 189 | rv = self._dispatch() 190 | 191 | return rv 192 | -------------------------------------------------------------------------------- /pysh/contrib/tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | 存放一些不适合放在别处的工具性质的类或函数 3 | """ 4 | 5 | 6 | class _Getch: 7 | """ 8 | 用于行编辑过程。 9 | 读取单个字符。可读取任意字符,包括控制字符。并且兼容win和linux平台。 10 | 参考自stack overflow: 11 | https://stackoverflow.com/questions/510357/python-read-a-single-character-from-the-user/20865751 12 | """ 13 | 14 | def __init__(self): 15 | self.getch = self._GetchLinux 16 | 17 | def __call__(self): 18 | self._getch() 19 | 20 | try: 21 | return self.char.decode() 22 | except AttributeError: 23 | return self.char 24 | 25 | def _getch(self): 26 | self.char = self.getch() 27 | 28 | def _GetchWindows(self): 29 | import msvcrt 30 | return msvcrt.getch() 31 | 32 | def _GetchLinux(self): 33 | try: 34 | import sys, tty, termios 35 | except ModuleNotFoundError: 36 | return self._GetchWindows() 37 | fd = sys.stdin.fileno() 38 | old_settings = termios.tcgetattr(fd) 39 | try: 40 | tty.setraw(sys.stdin.fileno()) 41 | ch = sys.stdin.read(1) 42 | finally: 43 | termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) 44 | return ch 45 | 46 | 47 | class LineEndError(ValueError): 48 | pass 49 | 50 | 51 | getch = _Getch() 52 | -------------------------------------------------------------------------------- /pysh/manage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arianxx/Pysh/d44f7303bc4ce3764a300830d361115200ad3602/pysh/manage/__init__.py -------------------------------------------------------------------------------- /pysh/manage/dispatch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Dispatch接受解析之后的参数,将参数分发给应用执行,并执行环境需要的其它操作。 4 | """ 5 | import os 6 | import subprocess 7 | import sys 8 | from multiprocessing import Process 9 | 10 | from .env import Application, Processing, History, Variable, EnvVariable 11 | from .middleware import StdinRedirection 12 | 13 | apps = Application.app 14 | 15 | 16 | class Task(Process): 17 | """ 18 | 包装真正的命令。 19 | """ 20 | 21 | def __init__(self, app, *args): 22 | super().__init__() 23 | self.app = app 24 | self.args = args 25 | self.name = self.app.__class__.__name__ 26 | self.backend = False 27 | self.is_join = False 28 | 29 | if type(self.app) != str: 30 | # 不是字符串说明是内部注册的命令类 31 | self._task = self.app(*self.args) 32 | 33 | # 绑定当前环境到应用,以便多进程下能够访问 34 | self._task.env = { 35 | 'app': Application.app, 36 | 'process': Processing.process, 37 | 'history': History.history, 38 | 'variable': Variable.variable, 39 | } 40 | else: 41 | # 否则是外部命令路径 42 | self._task = None 43 | 44 | def run(self): 45 | if self.backend: 46 | # 如果是后台程序,强制打开输入流 47 | sys.stdin = os.fdopen(0) 48 | 49 | if self._task: 50 | Application.app = self._task.env['app'] 51 | Processing.process = self._task.env['process'] 52 | History.history = self._task.env['history'] 53 | 54 | if self._task: 55 | self._task.env.update( 56 | Application=Application, 57 | Processing=Processing, 58 | History=History, 59 | Variable=Variable, 60 | ) 61 | 62 | Processing.record(self) 63 | 64 | try: 65 | if self._task: 66 | # 内部命令,调用注册的方法 67 | rv = self._task.handler() 68 | else: 69 | # 外部命令打开子进程 70 | rv = subprocess.call([self.app] + [*self.args], 71 | stdin=sys.stdin, 72 | stdout=sys.stdout, 73 | stderr=sys.stderr) 74 | except EOFError as e: 75 | # 多进程可能会发生io错误 76 | print(e) 77 | rv = False 78 | except Exception: 79 | # 其它异常向上冒泡 80 | raise 81 | finally: 82 | # 清理记录 83 | Processing.remove(self.id) 84 | 85 | return rv 86 | 87 | 88 | class Script: 89 | """ 90 | 如果传入一个文件路径,尝试使用shebang或内置的命令解析这个文件 91 | """ 92 | 93 | def __init__(self, path): 94 | self.path = path.strip() 95 | 96 | def _read(self): 97 | """ 98 | 尝试读取文件 99 | """ 100 | try: 101 | file = open(self.path, 'rt') 102 | except FileNotFoundError as e: 103 | print(e) 104 | raise e 105 | except PermissionError as e: 106 | print(e) 107 | raise e 108 | 109 | self.file = file 110 | 111 | try: 112 | self.lines = self.file.readlines() 113 | except UnicodeDecodeError as e: 114 | print(e) 115 | raise e 116 | finally: 117 | self.file.close() 118 | 119 | return 120 | 121 | def _dispatch(self): 122 | """ 123 | 如果有shebang则用shebang对应的程序运行。 124 | 否则自身运行 125 | """ 126 | if not self.lines: 127 | raise ValueError("没有输入") 128 | 129 | shebang = None 130 | for line in self.lines: 131 | if line.startswith('#'): 132 | if line.startswith('#!'): 133 | shebang = line.strip('#!').strip() 134 | else: 135 | continue 136 | else: 137 | break 138 | 139 | if shebang: 140 | try: 141 | subprocess.call([shebang, self.path]) 142 | return True 143 | except FileNotFoundError as e: 144 | print(e) 145 | return False 146 | else: 147 | return self._parse() 148 | 149 | def _parse(self): 150 | """ 151 | 重定向输入到文件,一行一行解析 152 | """ 153 | from ..contrib.parser import Parser 154 | 155 | tokens = self.lines[1:] 156 | si = StdinRedirection(source=tokens) 157 | rv = True 158 | with si.context(): 159 | while True: 160 | # 虽然是while True,tokens读尽时,上下文管理器弹出StopIteration,捕获后会退出循环 161 | token = input() 162 | try: 163 | Parser(token).run() 164 | except Exception as e: 165 | print(e) 166 | rv = False 167 | break 168 | return rv 169 | 170 | def run(self): 171 | try: 172 | self._read() 173 | except Exception as e: 174 | print(e) 175 | return False 176 | 177 | return self._dispatch() 178 | 179 | 180 | class Dispatch: 181 | def __init__(self): 182 | pass 183 | 184 | def _thread(self, app, *args, daemon=False, join=False): 185 | """ 186 | 后台命令就新建一个进程。 187 | 188 | :param app: 要启动的app类 189 | :param args: 命令的参数 190 | :return: None 191 | """ 192 | task = Task(app, *args) 193 | task.backend = True 194 | if join: 195 | task.is_join = True 196 | 197 | # 守护进程 198 | # 如果进程以守护进程的方式启动,在父进程结束时,守护进程也立即结束。 199 | # 并且,以守护进程方式启动,会导致输入混乱。如果以守护进程启动新pysh 200 | # 会导致两个shell交替相应输入 201 | task.daemon = daemon 202 | 203 | try: 204 | task.start() 205 | except AssertionError as e: 206 | # 父进程以守护模式启动,就不能再创建子进程。 207 | # 会弹出: 208 | # AssertionError: daemonic processes are not allowed to have children 209 | # 虽然我并不明白为什么不能创建 210 | # 总之先忽略它吧…… 211 | print(e) 212 | 213 | # 子进程是否阻塞 214 | # 在pysh中启动一个新pysh,应该使其阻塞,否则会产生混乱 215 | if join: 216 | task.join() 217 | 218 | # 启动子进程后在父进程也记录子进程的id 219 | Processing.record(task) 220 | return True 221 | 222 | def _front(self, app, *args): 223 | """ 224 | 前台程序直接手动启动。 225 | 226 | :param app: 要启动的app类 227 | :param args: 命令的参数 228 | :return: None 229 | """ 230 | task = Task(app, *args) 231 | return task.run() 232 | 233 | def _script(self, file_path): 234 | """ 235 | 尝试解析脚本 236 | """ 237 | return Script(file_path).run() 238 | 239 | @History.history_recorder 240 | def dispatch(self, command, *args, backend, daemon, join): 241 | # 先搜索注册的应用,没有就搜寻环境PATH变量 242 | app = apps.get(command, None) or EnvVariable.search_path(command) 243 | 244 | if app: 245 | if backend: 246 | # rv = return_value 247 | rv = self._thread(app, *args, daemon=daemon, join=join) 248 | else: 249 | rv = self._front(app, *args) 250 | elif os.path.isfile(command): 251 | # 如果是文件路径,尝试按照shebang运行或自身解析 252 | rv = self._script(command) 253 | else: 254 | print('{}: command not found'.format(command)) 255 | rv = False 256 | 257 | return rv 258 | 259 | 260 | dispatch = Dispatch() 261 | -------------------------------------------------------------------------------- /pysh/manage/env.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 处理shell一切环境信息,包括记录注册的应用以及从外部环境读取变量等 4 | """ 5 | import os 6 | import sys 7 | from collections import deque, OrderedDict 8 | from datetime import datetime 9 | from functools import wraps 10 | 11 | 12 | class Application: 13 | app = OrderedDict() 14 | 15 | @staticmethod 16 | def __new__(cls, *args, **kwargs): 17 | # Application不必要有多个实例 18 | if not getattr(cls, '_instance', None): 19 | cls._instance = super().__new__(cls, *args, **kwargs) 20 | 21 | try: 22 | import readline 23 | from .middleware import Completer 24 | except ImportError: 25 | # Win下没有readline模块 26 | pass 27 | else: 28 | # 后来在行编辑中自己实现了简单的命令补全 29 | readline.parse_and_bind("tab: complete") 30 | readline.set_completer(Completer.search_symbol) 31 | 32 | return cls._instance 33 | 34 | def register(self, cls): 35 | # 将注册的应用收集到app字典中 36 | app_name = cls.__name__ 37 | self.app[app_name] = cls 38 | 39 | return cls 40 | 41 | @classmethod 42 | def registered_app(cls): 43 | return dict(cls.app) 44 | 45 | class Processing: 46 | """ 47 | 记录没有退出的命令的id。 48 | """ 49 | process = {} 50 | 51 | @classmethod 52 | def record(cls, task): 53 | """ 54 | 记录命令实例。 55 | """ 56 | if not cls.process: 57 | task.id = 0 58 | pid = task.id 59 | else: 60 | index = 0 61 | while True: 62 | if index not in cls.process: 63 | pid = index 64 | break 65 | index += 1 66 | task.id = pid 67 | 68 | cls.process[pid] = {} 69 | cls.process[pid]['NAME'] = task._task.__class__.__name__ 70 | cls.process[pid]['TIME'] = datetime.now() 71 | cls.process[pid]['instance'] = task 72 | return True 73 | 74 | @classmethod 75 | def remove(cls, id): 76 | """ 77 | 退出时执行此方法删除记录。 78 | 79 | :param id: 命令记录在process字典中的pid值。 80 | :return: 成功与否。 81 | """ 82 | id = id 83 | if id in cls.process.keys(): 84 | cls.process.pop(id) 85 | return True 86 | return False 87 | 88 | @classmethod 89 | def get_records(cls): 90 | """ 91 | 每次访问,清理掉无效的记录 92 | 93 | :return: 记录 94 | """ 95 | for id in list(cls.process): 96 | try: 97 | if cls.process[id]['instance'].backend and not cls.process[id]['instance'].is_alive(): 98 | cls.process.pop(id) 99 | except AssertionError as e: 100 | # 多个pysh进程下,某些情况会弹出这个错误 101 | # AssertionError: can only test a child process 102 | # 经常在exit的时候弹出 103 | # 并不知道这是什么原因…… 104 | print(e) 105 | 106 | return cls.process 107 | 108 | 109 | class History: 110 | """ 111 | 记录命令执行的记录。 112 | """ 113 | history = deque(maxlen=100) 114 | 115 | @classmethod 116 | def history_recorder(cls, func): 117 | """ 118 | 给dispatch.Dispatch.dispatch函数装饰,记录执行的命令。 119 | 120 | :param func: 待装饰的函数 121 | :return: 装饰器 122 | """ 123 | 124 | @wraps(func) 125 | def decorator(self, command, *args, backend=False, daemon=False, join=False): 126 | args = args or [] 127 | cls.history.append(' '.join([command] + list(args))) 128 | return func(self, command, *args, backend=backend, daemon=daemon, join=join) 129 | 130 | return decorator 131 | 132 | @classmethod 133 | def clear(cls): 134 | """ 135 | 清空历史命令 136 | :return: None 137 | """ 138 | 139 | cls.history.clear() 140 | 141 | @classmethod 142 | def maxlen(cls): 143 | return cls.history.maxlen 144 | 145 | @classmethod 146 | def new_maxlen(cls, value): 147 | """ 148 | 可以修改最大可容纳的历史命令数量。 149 | 150 | :param value: 可以记录的最大命令数 151 | :return: None 152 | """ 153 | new_history = deque(maxlen=int(value)) 154 | new_history.extend(cls.history.copy()) 155 | cls.history = new_history 156 | 157 | 158 | class Variable: 159 | """ 160 | 储存变量 161 | """ 162 | variable = OrderedDict() 163 | 164 | @classmethod 165 | def add(cls, name, value): 166 | cls.variable[name] = value 167 | 168 | @classmethod 169 | def get_vr(cls, name): 170 | return cls.variable[name] 171 | 172 | @classmethod 173 | def remove(cls, name): 174 | if cls.variable.get(name) is not None: 175 | cls.variable.pop(name) 176 | 177 | @classmethod 178 | def clear(cls): 179 | cls.variable.clear() 180 | 181 | 182 | class EnvVariable: 183 | """ 184 | 环境变量,以及外部PATH变量 185 | """ 186 | env_file_path = '/.env' 187 | variable = OrderedDict() 188 | cached = {} 189 | PATH = None 190 | 191 | @classmethod 192 | def set_env_variable(cls, name, value): 193 | cls.variable.update({name: value}) 194 | with open(cls.env_file_path, 'wt') as file: 195 | for key, value in cls.variable.items(): 196 | file.write(str(key) + '=' + str(value) + '\n') 197 | return True 198 | 199 | @classmethod 200 | def remove_env_variable(cls, name): 201 | try: 202 | cls.variable.pop(name) 203 | except KeyError as e: 204 | print(e) 205 | else: 206 | with open(cls.env_file_path, 'wt') as file: 207 | for key, value in cls.variable.items(): 208 | file.write(str(key) + '=' + str(value) + '\n') 209 | return True 210 | 211 | @classmethod 212 | def search_path(cls, name): 213 | if name in cls.cached: 214 | return cls.cached[name] 215 | 216 | for dir in cls.PATH: 217 | path = os.path.join(dir, name) 218 | if os.path.isfile(path): 219 | cls.cached_path(name, path) 220 | return path 221 | path = os.path.join(dir, name + '.exe') 222 | if os.path.isfile(path): 223 | cls.cached_path(name, path) 224 | return path 225 | else: 226 | return False 227 | 228 | @classmethod 229 | def cached_path(cls, name, path): 230 | cls.cached.update({name: path}) 231 | return True 232 | 233 | @classmethod 234 | def clear_cached(cls): 235 | cls.cached.clear() 236 | return True 237 | 238 | 239 | # 以下辅助函数 240 | 241 | def get_env_path(): 242 | PATH = os.environ['PATH'] 243 | sep = ';' if sys.platform == 'win32' else ':' 244 | return PATH.split(sep) 245 | 246 | def get_env_variable(EnvVariable): 247 | """ 248 | 从文件中读取环境变量。 249 | 250 | :param EnvVariable: EnvVariable类 251 | :return: 读取的变量名和值的字典 252 | """ 253 | path = EnvVariable.env_file_path 254 | try: 255 | with open(path, 'rt') as file: 256 | lines = file.readlines() 257 | variable = {} 258 | for line in lines: 259 | try: 260 | name, value = line.strip().split('=') 261 | name, value = name.strip(), value.strip() 262 | except Exception: 263 | pass 264 | else: 265 | variable.update({name: value}) 266 | except FileNotFoundError as e: 267 | variable = {} 268 | pass 269 | return variable 270 | 271 | 272 | if not EnvVariable.variable: 273 | EnvVariable.variable = get_env_variable(EnvVariable) 274 | 275 | if not EnvVariable.PATH: 276 | EnvVariable.PATH = get_env_path() -------------------------------------------------------------------------------- /pysh/manage/middleware.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import contextlib 3 | import itertools 4 | import os 5 | import sys 6 | from itertools import chain 7 | 8 | from ..manage.env import Application, Variable, EnvVariable, History 9 | 10 | 11 | class StdoutRedirection(): 12 | """ 13 | 重定向输出 14 | """ 15 | 16 | def __init__(self, file_path=None, file_override=True): 17 | """ 18 | 如果提供了文件目录,就尝试打开文件,稍后将会将输出重定向到文件。 19 | 否则将输出重定向到内部列表pipe。 20 | """ 21 | self.file = None 22 | self.tmp_file = None 23 | self.pipe = [] 24 | if file_path: 25 | try: 26 | if file_override: 27 | self.file = open(file_path, 'wt') 28 | else: 29 | self.file = open(file_path, 'at') 30 | except FileNotFoundError as e: 31 | """ 32 | 只是特别标注出来这里可能弹出的错误。 33 | 错误向上冒泡,具体处理措施交由调用者完成。 34 | """ 35 | raise e 36 | except PermissionError as e: 37 | raise e 38 | else: 39 | for num in itertools.count(): 40 | try: 41 | self.tmp_file = open('stdouttemp' + str(num) + '.txt', 'wt') 42 | except FileExistsError: 43 | continue 44 | else: 45 | break 46 | 47 | @contextlib.contextmanager 48 | def context(self): 49 | """ 50 | 重定向输出的上下文管理器 51 | 52 | contextlib.contextmanager装饰器自动将协程转换为上下文管理器 53 | yield前为进入with时执行,yield为with语句返回值,yield后退出with时执行 54 | """ 55 | origin_stdout = sys.stdout 56 | 57 | if self.file: 58 | sys.stdout = self.file 59 | else: 60 | sys.stdout = self.tmp_file 61 | 62 | try: 63 | yield self 64 | except Exception as e: 65 | raise e 66 | finally: 67 | # 不论弹出什么异常,都先还原输出流 68 | sys.stdout.flush() 69 | sys.stdout.close() 70 | sys.stdout = origin_stdout 71 | 72 | if self.tmp_file: 73 | with open(self.tmp_file.name, 'rt') as file: 74 | self.pipe = file.readlines() 75 | 76 | os.remove(self.tmp_file.name) 77 | 78 | 79 | class StdinRedirection(): 80 | """ 81 | 重定向输入 82 | """ 83 | 84 | def __init__(self, file_path=None, source=None): 85 | """ 86 | 如果有文件路径,将输入重定向到文件。否则将输入重定向到source。 87 | source应该是一个实现了__iter__方法的可迭代对象 88 | """ 89 | self.file = None 90 | self.tmp_file = None 91 | self.source = source 92 | 93 | if file_path: 94 | try: 95 | self.file = open(file_path, 'rt') 96 | except FileNotFoundError as e: 97 | raise e 98 | else: 99 | self._from_source() 100 | 101 | def _from_source(self): 102 | for num in itertools.count(): 103 | try: 104 | self.tmp_file = open('stdintemp' + str(num) + '.txt', 'wt') 105 | except FileExistsError: 106 | continue 107 | else: 108 | break 109 | 110 | self._to_temp_file(self.source) 111 | self.tmp_file.close() 112 | self.tmp_file = open(self.tmp_file.name, 'rt') 113 | return 114 | 115 | def _to_temp_file(self, source): 116 | for line in source: 117 | if type(line) != str: 118 | self._to_temp_file(line) 119 | else: 120 | self.tmp_file.write(line) 121 | 122 | return 123 | 124 | @contextlib.contextmanager 125 | def context(self): 126 | origin_stdin = sys.stdin 127 | 128 | if self.file: 129 | sys.stdin = self.file 130 | else: 131 | sys.stdin = self.tmp_file 132 | 133 | try: 134 | yield self 135 | except StopIteration: 136 | # _source迭代完毕 137 | pass 138 | except Exception as e: 139 | raise e 140 | finally: 141 | # 还原输入流 142 | sys.stdin.flush() 143 | sys.stdin.close() 144 | sys.stdin = origin_stdin 145 | 146 | if self.tmp_file: 147 | os.remove(self.tmp_file.name) 148 | 149 | 150 | class Completer: 151 | @classmethod 152 | def search_symbol(cls, text, state): 153 | """ 154 | 用于行编辑,编辑tab键自动补全功能。 155 | """ 156 | names = [name for name in chain( 157 | History.history, 158 | Application.app, 159 | Variable.variable, 160 | EnvVariable.variable 161 | ) if name.startswith(text)] 162 | try: 163 | return names[state] 164 | except IndexError: 165 | return False 166 | --------------------------------------------------------------------------------