├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── azureshell ├── __init__.py ├── azureshell.conf ├── azureshell.py ├── cache.py ├── compat.py ├── completer.py ├── config.py ├── index.py ├── index_generator.py ├── lexer.py ├── logger.py └── utils.py ├── img └── azure-shell-console.gif ├── register.py ├── scripts ├── build_run.sh └── get-styles.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.5 2 | * add register.py and separate 'READM.md to rst conversion' logic from setup.py 3 | 4 | ## 0.2.4 5 | * fixup bug in index_geenrator.py- [Issue #2](https://github.com/yokawasa/azure-shell/issues/2) 6 | 7 | ## 0.2.3 8 | * fixup bug - [Issue #1](https://github.com/yokawasa/azure-shell/issues/1) 9 | 10 | ## 0.2.1 + 0.2.2 11 | * [NOT Recommended] Please skip this version as it still has index loading issue while tried to fixup [Issue #1](https://github.com/yokawasa/azure-shell/issues/1) 12 | 13 | ## 0.2.0 14 | * Added Index Generator executable (azure-shell-index-generator) 15 | 16 | ## 0.1.0 17 | * Inital Release 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # azure-shell 2 | 3 | An interactive Azure CLI 2.0 command line interface. 4 | 5 | [Note] Microsoft official Azure CLI 2.0 shell can be found at [Azure/azure-cli-shell](https://github.com/Azure/azure-cli-shell) 6 | 7 | ![](https://github.com/yokawasa/azure-shell/raw/master/img/azure-shell-console.gif) 8 | 9 | ## Features 10 | 11 | * Auto-completion of Azure CLI group, subgroups, commands, and parameters 12 | * Syntax highlighting 13 | * Command history 14 | 15 | ## Supported Environments 16 | 17 | * Python versions: 2.7, 3.3, 3.4, 3.5, 3.5, 3.6 and maybe more 18 | * OS: Mac, Ubuntu, CentOS, Bash-on-Windows, or any platform where azure-cli can be installed 19 | 20 | ## Prerequisites 21 | 22 | You need Azure CLI 2.0 installed as prerequisites for azure-shell. Please refer to [Install Azure CLI 2.0](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) and install it if not yet installed 23 | 24 | ## Installation 25 | 26 | The azure-shell requires python and pip to install. You can install the azure-shell using pip: 27 | 28 | ``` 29 | pip install azure-shell 30 | ``` 31 | 32 | If you've already have azure-shell installed and want to upgrade to the latest version, you can upgrade like this: 33 | 34 | ``` 35 | pip install --upgrade azure-shell 36 | ``` 37 | 38 | ## Usage 39 | 40 | Once you've installed the azure-shell, you can run azure-shell by simply typing azure-shell: 41 | 42 | ``` 43 | azure-shell 44 | ``` 45 | 46 | You can exit the azure-shell by typing either exit or quit: 47 | 48 | ``` 49 | azure> exit 50 | ``` 51 | 52 | Basically you can run azure-shell without making any configurations but you can give options to azure-shell to change its default behabior: 53 | 54 | ``` 55 | azure-shell --help 56 | 57 | Usage: azure-shell [-h] [--version] [--basedir BASEDIR] [--config CONFIG] 58 | [--index INDEX] 59 | 60 | An interactive Azure CLI command line interface 61 | 62 | optional arguments: 63 | -h, --help show this help message and exit 64 | --version show program's version number and exit 65 | --basedir BASEDIR Azure Shell base dir path ($HOME/.azureshell by default) 66 | --config CONFIG Azure Shell config file path 67 | ($HOME/.azureshell/azureshell.conf by default) 68 | --index INDEX Azure Shell index file to load ($HOME/.azureshel/cli- 69 | index-.json) 70 | ``` 71 | 72 | ## Azure Shell Index Generator 73 | 74 | You can generate an index for azure-shell using azure-shell-index-generator command. Please be noted that it will take time before all data generation works are done 75 | 76 | ``` 77 | azure-shell-index-generator --output ~/.azureshell/cli-index.json 78 | ``` 79 | 80 | Basically you don't need to generate the index by yourself as azure-shell automatically downloads an index from its repository and load it for commands and parameters completion in startup time. But you also can give azure-shell your index using --index option. 81 | 82 | ``` 83 | azure-shell --index ~/.azureshell/cli-index.json 84 | ``` 85 | 86 | ## Contributing 87 | 88 | Bug reports and pull requests are welcome on GitHub at https://github.com/yokawasa/azure-shell 89 | 90 | ## More Information 91 | 92 | * [Get started with Azure CLI 2.0](https://docs.microsoft.com/en-us/cli/azure/get-started-with-azure-cli) 93 | * [Install Azure CLI 2.0](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) 94 | 95 | -------------------------------------------------------------------------------- /azureshell/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import print_function, unicode_literals 4 | 5 | import os 6 | import sys 7 | import argparse 8 | 9 | from . import compat 10 | from .azureshell import AzureShell 11 | from .completer import AzureShellCompleter 12 | from .index import AzureShellIndex, AzureShellIndexException 13 | from .config import AzureShellConfig 14 | from .cache import AzureShellCache 15 | from .utils import get_cli_version, find_executable_path, AS_ERR, AZURE_SHELL_MINIMUM_AZURE_CLI_VERSION 16 | from .logger import init_logger 17 | 18 | __version__ = '0.2.5' 19 | _DEFAULT_AZURE_SHELL_BASE_DIR = '{}/.azureshell'.format(os.environ['HOME']) 20 | 21 | def main(): 22 | 23 | parser = argparse.ArgumentParser(description='An interactive Azure CLI command line interface') 24 | parser.add_argument( 25 | '--version', action='version', version=__version__) 26 | parser.add_argument( 27 | '--basedir', 28 | help='Azure Shell base dir path ($HOME/.azureshell by default)') 29 | parser.add_argument( 30 | '--config', 31 | help='Azure Shell config file path ($HOME/.azureshell/azureshell.conf by default)') 32 | parser.add_argument( 33 | '--index', 34 | help='Azure Shell index file to load ($HOME/.azureshel/cli-index-.json)') 35 | args = parser.parse_args() 36 | 37 | ## az executable command path check 38 | if not find_executable_path('az'): 39 | AS_ERR("[ERROR] NO azure cli (az) executable command found!") 40 | AS_ERR("Please install Azure CLI 2.0 and set its executable dir to PATH") 41 | AS_ERR("See https://github.com/Azure/azure-cli") 42 | sys.exit(1) 43 | 44 | azure_cli_version = str(utils.get_cli_version()) 45 | ## Check minimum azure-cli version 46 | if azure_cli_version < AZURE_SHELL_MINIMUM_AZURE_CLI_VERSION: 47 | AS_ERR("[ERROR] Azure CLI 2.0 minimum version failure!") 48 | AS_ERR("Minimum azure-cli version required: {} (Your version: {})".format( 49 | AZURE_SHELL_MINIMUM_AZURE_CLI_VERSION, azure_cli_version)) 50 | AS_ERR("Please install the latest azure-cli and set its executable dir to PATH") 51 | AS_ERR("See https://github.com/Azure/azure-cli") 52 | sys.exit(1) 53 | 54 | print("azure-shell version:{}".format(__version__)) 55 | #print("AzureCLI version:{}".format(azure_cli_version)) 56 | 57 | base_dir = args.basedir if args.basedir else _DEFAULT_AZURE_SHELL_BASE_DIR 58 | config_file = args.config if args.config else '{}/azureshell.conf'.format(base_dir) 59 | config = None 60 | ## Check if config file exists 61 | if not os.path.exists(config_file): 62 | AS_ERR("[WARNING] No config file found:{}".format(config_file)) 63 | AS_ERR("Creating an default config file :{}".format(config_file)) 64 | AzureShellConfig.makedefaultconfig(config_file) 65 | config = AzureShellConfig(config_file) 66 | init_logger('azureshell', config.log_file, config.log_level) 67 | 68 | index = AzureShellIndex(base_dir) 69 | index_file = '' 70 | index_version = '' 71 | if args.index: 72 | index_file = args.index 73 | # Check if specified index file exists and exit if the file does not exist 74 | if not os.path.exists(index_file): 75 | AS_ERR("[ERROR] index file doesn't exist: {}".format(index_file)) 76 | sys.exit(1) 77 | else: 78 | # Check if default index file exists and download the file if not exist on local 79 | index_version = index.get_local_version() 80 | index_file = index.get_index_file(index_version) 81 | if not index_version or \ 82 | not os.path.exists(index_file) or index_version < azure_cli_version: 83 | index = AzureShellIndex(base_dir) 84 | index_version = index.download_index_from_repository(azure_cli_version) 85 | index_file = index.get_index_file(index_version) 86 | 87 | AzureShellCache.Instance().set('base_dir', base_dir) 88 | AzureShellCache.Instance().set('index_file', index_file) 89 | AzureShellCache.Instance().set('config_file',config_file) 90 | 91 | index_data = {} 92 | try: 93 | index_data = AzureShellIndex.load_index(index_file) 94 | except AzureShellIndexException as e: 95 | AS_ERR("[ERROR] index file loading failure: {}".format(index_file)) 96 | AS_ERR(str(e)) 97 | sys.exit(1) 98 | 99 | completer = AzureShellCompleter(index_data) 100 | azureshell = AzureShell(config,completer) 101 | azureshell.run() 102 | 103 | if __name__ == '__main__': 104 | main() 105 | -------------------------------------------------------------------------------- /azureshell/azureshell.conf: -------------------------------------------------------------------------------- 1 | [azureshell] 2 | 3 | # Pygments: highlighter style 4 | # run ./scripts/get-styles.py 5 | # 'manni', 'igor', 'lovelace', 'xcode', 'vim', 'autumn', 'abap', 'vs', 6 | # 'rrt', 'native', 'perldoc', 'borland', 'arduino', 'tango', 'emacs', 'friendly' 7 | # To disable themes, set highlighter_style = none 8 | highlighter_style = vim 9 | 10 | # log_file location. 11 | log_file = ~/.azureshell/azureshell.log 12 | # 13 | # # Default log level. Possible values: "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG". 14 | log_level = INFO 15 | -------------------------------------------------------------------------------- /azureshell/azureshell.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | import os 5 | import sys 6 | import subprocess 7 | import logging 8 | 9 | from prompt_toolkit.document import Document 10 | from prompt_toolkit.shortcuts import create_eventloop,create_default_layout 11 | from prompt_toolkit.layout.processors import HighlightMatchingBracketProcessor, ConditionalProcessor 12 | from prompt_toolkit.filters import Always, HasFocus, IsDone 13 | from prompt_toolkit.enums import DEFAULT_BUFFER 14 | from prompt_toolkit.buffer import Buffer 15 | from prompt_toolkit.interface import CommandLineInterface, Application, AbortAction, AcceptAction 16 | from prompt_toolkit.auto_suggest import AutoSuggestFromHistory 17 | from prompt_toolkit.history import InMemoryHistory, FileHistory 18 | from prompt_toolkit.key_binding.manager import KeyBindingManager 19 | from pygments.token import Token 20 | from pygments.util import ClassNotFound 21 | from pygments.styles import get_style_by_name 22 | from prompt_toolkit.styles import default_style_extensions, style_from_dict 23 | 24 | from .cache import AzureShellCache 25 | 26 | logger = logging.getLogger('azureshell.azureshell') 27 | 28 | class InputInterrupt(Exception): 29 | pass 30 | 31 | class AzureShell(object): 32 | 33 | def __init__(self, config, completer): 34 | self._cli = None 35 | self._env = os.environ.copy() 36 | self.history = InMemoryHistory() 37 | self.file_history = FileHistory( 38 | "{}/history".format(AzureShellCache.Instance().get('base_dir'))) 39 | self._config = config 40 | self.completer = completer 41 | 42 | def run(self): 43 | while True: 44 | try: 45 | document = self.get_cli().run(reset_current_buffer=True) 46 | text = document.text 47 | except InputInterrupt: 48 | pass 49 | except (KeyboardInterrupt, EOFError): 50 | logger.debug("breaking loop due to KeyboardInterrupt or EOFError") 51 | break 52 | else: 53 | if text.startswith('exit') or text.startswith('quit'): 54 | sys.exit(0) 55 | 56 | if text.startswith('az'): 57 | full_cmd = text 58 | self.history.append(full_cmd) 59 | else: 60 | full_cmd = text[0:] 61 | 62 | logger.debug("Execute subprocess command:{}".format(full_cmd)) 63 | self.get_cli().request_redraw() 64 | p = subprocess.Popen(full_cmd, shell=True, env=self._env) 65 | p.communicate() 66 | 67 | def on_input_timeout(self, cli): 68 | document = cli.current_buffer.document 69 | text = document.text 70 | logger.debug("on_input_timeout document:{}".format(document)) 71 | # Add 'az' to current buffer if no text typed in 72 | #if not document.text: 73 | # cli.current_buffer.document = Document(u'az', 2) 74 | cli.request_redraw() 75 | 76 | def get_cli(self): 77 | if self._cli is None: 78 | self._cli = self.create_cli() 79 | return self._cli 80 | 81 | def create_cli(self): 82 | ## KeyBindings configuration 83 | key_binding = KeyBindingManager( 84 | enable_search=True, 85 | enable_abort_and_exit_bindings=True, 86 | enable_system_bindings=True, 87 | enable_auto_suggest_bindings=True, 88 | enable_open_in_editor=False) 89 | 90 | ## Buffer configuration 91 | default_buffer= Buffer( 92 | history=self.file_history, 93 | auto_suggest=AutoSuggestFromHistory(), 94 | enable_history_search=True, 95 | completer=self.completer, 96 | complete_while_typing=Always(), 97 | accept_action=AcceptAction.RETURN_DOCUMENT) 98 | 99 | ## Style configuration 100 | try: 101 | style = get_style_by_name(self._config.highlighter_style) 102 | except ClassNotFound: 103 | style = get_style_by_name('native') 104 | 105 | styles = {} 106 | styles.update(style.styles) 107 | styles.update(default_style_extensions) 108 | styles.update({ 109 | Token.Menu.Completions.Completion: 'bg:#003fff #ffffff', 110 | Token.Menu.Completions.Completion.Current: 'bg:#5ab300 #000000', 111 | Token.Menu.Completions.Meta.Current: 'bg:#5ab300 #ffffff', 112 | Token.Menu.Completions.Meta: 'bg:#ffffff #000000', 113 | Token.Scrollbar: 'bg:#003fff', 114 | Token.Scrollbar.Button: 'bg:#003333', 115 | }) 116 | prompt_style = style_from_dict(styles) 117 | 118 | ## Application 119 | application = Application( 120 | layout=self.create_cli_layout(), 121 | mouse_support=False, 122 | style=prompt_style, 123 | buffer=default_buffer, 124 | on_abort=AbortAction.RETRY, 125 | on_exit=AbortAction.RAISE_EXCEPTION, 126 | on_input_timeout=self.on_input_timeout, 127 | key_bindings_registry=key_binding.registry, 128 | ) 129 | 130 | cli = CommandLineInterface(application=application, 131 | eventloop=create_eventloop()) 132 | return cli 133 | 134 | def create_cli_layout(self): 135 | from .lexer import AzureShellLexer 136 | lexer = AzureShellLexer 137 | return create_default_layout ( 138 | message =u'azure> ', 139 | reserve_space_for_menu=8, 140 | lexer=lexer, 141 | extra_input_processors=[ 142 | ConditionalProcessor( 143 | processor=HighlightMatchingBracketProcessor(chars='[](){}'), 144 | filter=HasFocus(DEFAULT_BUFFER) & ~IsDone()) 145 | ] 146 | ) 147 | -------------------------------------------------------------------------------- /azureshell/cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | class AzureShellCache: 4 | 5 | __inst = None 6 | __cache = {} 7 | 8 | @staticmethod 9 | def Instance(): 10 | if AzureShellCache.__inst == None: 11 | AzureShellCache() 12 | return AzureShellCache.__inst 13 | 14 | def __init__(self): 15 | if AzureShellCache.__inst != None: 16 | raise Exception("This must not be called!!") 17 | AzureShellCache.__inst = self 18 | 19 | def set(self, k, v): 20 | self.__cache[k] = v 21 | 22 | def get(self, k): 23 | return self.__cache.get(k) 24 | -------------------------------------------------------------------------------- /azureshell/compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | 5 | if sys.version_info[0] == 3: 6 | ## Python3 7 | text_type = str 8 | import urllib.request as urllib2 9 | import configparser 10 | else: 11 | ## Python2 12 | text_type = unicode 13 | import urllib2 14 | import ConfigParser as configparser 15 | -------------------------------------------------------------------------------- /azureshell/completer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | import os 5 | import logging 6 | from prompt_toolkit.completion import Completer, Completion 7 | 8 | logger = logging.getLogger('azureshell.completer') 9 | 10 | class AzureShellCompleter(Completer): 11 | 12 | def __init__(self, index_data): 13 | self._index = index_data 14 | self._root_name = 'az' 15 | self._current_name = '' 16 | self._current = self._index[self._root_name] 17 | self._last_position = 0 18 | self._current_line = '' 19 | self._last_option = '' 20 | self.cmd_path = [self._current_name] 21 | 22 | @property 23 | def last_option(self): 24 | return self._last_option 25 | 26 | @property 27 | def current_command(self): 28 | return u' '.join(self.cmd_path) 29 | 30 | def get_completions(self, document, complete_event): 31 | cursor = document.text_before_cursor 32 | stripped_cursor = cursor.strip() 33 | logger.debug("get_completions cursor:{}".format(cursor)) 34 | 35 | # Skip _autocomplete if user typed in NON 'az' command 36 | if stripped_cursor and not stripped_cursor.startswith('az'): 37 | # Skip except the case that stripped_cursor is 'a' 38 | if not stripped_cursor =='a': 39 | return 40 | 41 | completions = self._autocomplete(cursor) 42 | prompt_completions = [] 43 | word_before_cursor = '' 44 | if stripped_cursor: 45 | word_before_cursor = stripped_cursor.split()[-1] 46 | for completion in completions: 47 | arg_tree = self._current.get('argument_tree', {}) 48 | if completion.startswith('--') and completion in arg_tree: 49 | meta = arg_tree[completion] 50 | if meta['required']: 51 | display_text = '%s (required)' % completion 52 | else: 53 | display_text = completion 54 | display_meta = arg_tree[completion].get('help','') 55 | elif completion == 'az': 56 | display_text = completion 57 | display_meta = '' 58 | else: 59 | cmd_tree = self._current.get('command_tree',{}) 60 | display_text = completion 61 | display_meta = cmd_tree[completion].get('summary','') 62 | if cursor and cursor[-1] == ' ': 63 | location = 0 64 | else: 65 | location = -len(word_before_cursor) 66 | # Converting multiple options like '--help -h' into single 67 | # like '--help' for usability 68 | normalized_completion = completion.split()[0] 69 | prompt_completions.append ( 70 | Completion(normalized_completion, location, 71 | display=display_text, display_meta=display_meta) 72 | ) 73 | 74 | for c in prompt_completions: 75 | yield c 76 | 77 | def _autocomplete(self, line): 78 | current_length = len(line) 79 | self._current_line = line 80 | logger.debug("_autocomplete cur_line:{} (cur_len:{}, last_pos:{})".format(self._current_line, current_length, self._last_position)) 81 | if current_length == 1 and self._last_position > 1: 82 | self._reset() 83 | elif current_length < self._last_position: 84 | # Hit backspace, and handling backspace 85 | return self._handle_backspace() 86 | elif not line: 87 | return [] 88 | #elif current_length != self._last_position + 1: 89 | # return self._complete_from_full_parse() 90 | 91 | # Only update the _last_position after the cases above were verified 92 | self._last_position = len(line) 93 | 94 | # Autocomplete with 'az' if line is a single space or 'a' 95 | # (assuming that user hits a space on a new line or typed in 'a') 96 | stripped_line = line.strip() 97 | if line and ( not stripped_line or stripped_line == 'a' ): 98 | return ['az'] 99 | 100 | # Skip if it's only space ' ' 101 | words = line.split() 102 | if (len(words) < 1): 103 | return [] 104 | # Now you have at least single word in the line like 105 | # azure> az 106 | # azure> az vm 107 | # azure> az vm list .. 108 | last_word = words[-1] 109 | logger.debug("_autocomplete last_word: {}".format(last_word)) 110 | if last_word in self._current.get('argument_tree', {}): 111 | # The last thing we completed was an argument, record this as self.last_arg 112 | self._last_option = last_word 113 | if line[-1] == ' ': 114 | # this is the case like: 115 | # 'az vm ' or 'az vm --debug ' 116 | # 1. 'az vm ' 117 | # Proceed next command completions if the command has childres 118 | # 2. 'az vm --debug ' ( current command is 'az vm' ) 119 | # Process the current command completion 120 | if not last_word.startswith('-'): 121 | next_command = self._current['command_tree'].get(last_word) 122 | if next_command is not None: 123 | self._current = next_command 124 | self._current_name = last_word 125 | self.cmd_path.append(self._current_name) 126 | return self._current['commands'][:] 127 | 128 | elif last_word.startswith('-'): 129 | all_args = self._current['arguments'] 130 | # Select args with Forward matching 131 | return [arg for arg in sorted(all_args) if arg.startswith(last_word)] 132 | 133 | logger.debug("_autocomplete dump: {}".format([cmd for cmd in sorted(self._current['commands']) if cmd.startswith(last_word)])) 134 | # Select commands with Forward matching 135 | return [cmd for cmd in sorted(self._current['commands']) if cmd.startswith(last_word)] 136 | 137 | def _reset(self): 138 | self._current_name = self._root_name 139 | self._current = self._index[self._root_name] 140 | self._last_position = 0 141 | self._last_option = '' 142 | self.cmd_path = [self._current_name] 143 | 144 | def _handle_backspace(self): 145 | # reseting and starting from the beginning 146 | self._reset() 147 | line = self._current_line 148 | for i in range(1, len(self._current_line)): 149 | self._autocomplete(line[:i]) 150 | return self._autocomplete(line) 151 | -------------------------------------------------------------------------------- /azureshell/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | from .compat import configparser 5 | 6 | _DEFAULT_AZURE_SHELL_CONFIG_HIGHLIGHTER_STYLE = 'vim' 7 | _DEFAULT_AZURE_SHELL_CONFIG_LOG_FILE = '~/azureshell.log' 8 | _DEFAULT_AZURE_SHELL_CONFIG_LOG_LEVEL = 'INFO' 9 | 10 | class AzureShellConfig(object): 11 | 12 | def __init__(self, config_file=None): 13 | self._config_file = config_file 14 | self._config_parser = configparser.SafeConfigParser() 15 | 16 | if self._config_file: 17 | self._config_parser.read(config_file) 18 | 19 | @staticmethod 20 | def makedefaultconfig(config_file): 21 | dir_name = os.path.dirname(config_file) 22 | if not os.path.isdir(dir_name): 23 | os.makedirs(dir_name) 24 | with open(config_file, "w") as f: 25 | f.write("[azureshell]\n# Pygments: highlighter style\n# run ./scripts/get-styles.py\n# 'manni', 'igor', 'lovelace', 'xcode', 'vim', 'autumn', 'abap', 'vs', 'rrt', 'native', 'perldoc', 'borland', 'arduino', 'tango', 'emacs', 'friendly', 'monokai', 'paraiso-dark'\n# To disable themes, set highlighter_style = none\nhighlighter_style = vim\n\n# log_file location\nlog_file = ~/.azureshell/azureshell.log\n\n# Default log level. Possible values: 'CRITICAL', 'ERROR', 'WARNING', 'INFO' and 'DEBUG'\nlog_level = DEBUG") 26 | 27 | def _get_string(self, section, key, default_val): 28 | v ='' 29 | if not self._config_file: 30 | return default_val 31 | 32 | try: 33 | v = self._config_parser.get(section, key) 34 | except configparser.NoOptionError as e: 35 | v = default_val 36 | return v 37 | 38 | @property 39 | def highlighter_style(self): 40 | return self._get_string('azureshell','highlighter_style',_DEFAULT_AZURE_SHELL_CONFIG_HIGHLIGHTER_STYLE) 41 | 42 | @property 43 | def log_file(self): 44 | return self._get_string('azureshell','log_file',_DEFAULT_AZURE_SHELL_CONFIG_LOG_FILE) 45 | 46 | @property 47 | def log_level(self): 48 | return self._get_string('azureshell','log_level',_DEFAULT_AZURE_SHELL_CONFIG_LOG_LEVEL) 49 | -------------------------------------------------------------------------------- /azureshell/index.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import json 5 | import logging 6 | from . import utils 7 | from .compat import urllib2 8 | 9 | logger = logging.getLogger('azureshell.index') 10 | 11 | _AZURE_SHELL_REMOTE_URL_BASE = "https://azureshellrepo.blob.core.windows.net/index" 12 | _AZURE_SHELL_AVAILABLE_INDEX_FILE = "index.available" 13 | _AZURE_SHELL_LOCAL_INDEX_VERSION_FILE = "index.local" 14 | 15 | class AzureShellIndexException(Exception): 16 | pass 17 | 18 | class AzureShellIndex(object): 19 | 20 | def __init__(self, index_base_dir): 21 | self._index_base_dir = index_base_dir 22 | self._version_file = "{}/{}".format(index_base_dir, _AZURE_SHELL_LOCAL_INDEX_VERSION_FILE) 23 | 24 | def get_index_file(self, index_version): 25 | return "{}/cli-index.{}.json".format(self._index_base_dir, index_version) 26 | 27 | def get_local_version(self): 28 | if not os.path.exists(self._version_file): 29 | return None 30 | f = open(self._version_file) 31 | v = f.readline().strip() 32 | f.close 33 | return v 34 | 35 | def set_local_version(self,index_version): 36 | f = open(self._version_file,"w") 37 | f.writelines(str(index_version)) 38 | f.close 39 | 40 | def download_index_from_repository(self, index_version ): 41 | # Check available list and get the best version 42 | version_to_download = AzureShellIndex.get_best_index_version_in_repository(index_version) 43 | local_index_version = self.get_local_version() 44 | # Download index if needed 45 | if not local_index_version or version_to_download > local_index_version: 46 | if not os.path.isdir(self._index_base_dir): 47 | os.makedirs(self._index_base_dir) 48 | remote_url = "{}/cli-index.{}.json".format( 49 | _AZURE_SHELL_REMOTE_URL_BASE,version_to_download) 50 | logger.debug("Downloading...:{}".format(remote_url)) 51 | f = urllib2.urlopen(remote_url) 52 | data = f.read() 53 | local_index_file = "{}/cli-index.{}.json".format(self._index_base_dir, version_to_download) 54 | with open(local_index_file, "wb") as code: 55 | code.write(data) 56 | 57 | ## Update local index version 58 | self.set_local_version(version_to_download) 59 | return version_to_download 60 | 61 | def load(self, index_version): 62 | index_file = "{}/cli-index.{}.json".format(self._index_base_dir, index_version) 63 | return AzureShellIndex.load_index(index_file) 64 | 65 | 66 | ### TODO: error handling 67 | @staticmethod 68 | def get_best_index_version_in_repository( index_version ): 69 | # Check available list and get the best version 70 | remote_url = "{}/{}".format( 71 | _AZURE_SHELL_REMOTE_URL_BASE, _AZURE_SHELL_AVAILABLE_INDEX_FILE) 72 | logger.debug("Reading...:{}".format(remote_url)) 73 | f = urllib2.urlopen(remote_url) 74 | data = f.read().decode('utf-8') 75 | versions=data.splitlines() 76 | versions.sort(reverse=True) 77 | ## check if versions contains myversion 78 | my_nearest_version = index_version 79 | if not index_version in versions: 80 | ## get my nearest available version if not versions contains index_version 81 | for i in versions: 82 | if index_version > i: 83 | my_nearest_version = i 84 | break 85 | return my_nearest_version 86 | 87 | @staticmethod 88 | def load_index(index_file): 89 | if not os.path.exists(index_file): 90 | estr="index file does not exist:{}".format(index_file) 91 | logger.error(estr) 92 | raise AzureShellIndexException(estr) 93 | 94 | data = {} 95 | try: 96 | with open(index_file, 'r') as f: 97 | data = json.load(f) 98 | except Exception as e: 99 | logger.error(str(e)) 100 | raise AzureShellIndexException(str(e)) 101 | return data 102 | 103 | 104 | def _parse_command(name, command_obj,completions): 105 | if not command_obj: 106 | return 107 | if 'arguments' in command_obj: 108 | args = command_obj['arguments'] 109 | for arg in args: 110 | options = arg.split() 111 | completions['args'].update(options) 112 | if 'commands' in command_obj and 'command_tree' in command_obj: 113 | subcommand_names = command_obj['commands'] 114 | completions['commands'].extend(subcommand_names) 115 | cmd_tree = command_obj['command_tree'] 116 | for subcommand_name in cmd_tree.keys(): 117 | subcommand_obj = cmd_tree[subcommand_name] 118 | _parse_command(subcommand_name, subcommand_obj,completions) 119 | 120 | def get_completions_commands_and_arguments( index_data = {} ): 121 | completions = { 122 | 'commands': [], 123 | 'args': set() 124 | } 125 | index_root = index_data['az'] 126 | completions['commands'] = index_root['commands'] 127 | for command_name in completions['commands']: 128 | command_obj = index_root['command_tree'].get(command_name) 129 | _parse_command(command_name, command_obj, completions) 130 | return completions 131 | -------------------------------------------------------------------------------- /azureshell/index_generator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | import getpass 6 | import yaml 7 | import json 8 | import argparse 9 | import subprocess 10 | import logging 11 | from .utils import get_cli_version, find_executable_path, get_azurecli_modules_path, AZURE_SHELL_MINIMUM_AZURE_CLI_VERSION 12 | 13 | logger = logging.getLogger('index.generator') 14 | 15 | _AZURE_SHELL_INDEX_GEN_USER = getpass.getuser() 16 | _AZURE_SHELL_INDEX_GEN_USER_HOME = os.environ['HOME'] 17 | 18 | """ 19 | Data Structure for Azure Shell Index 20 | """ 21 | def new_index_command_tree(): 22 | return {'arguments': [], 23 | 'argument_tree': {}, 24 | 'commands': [], 25 | 'command_tree': {}, 26 | 'summary': '' 27 | } 28 | 29 | """ 30 | Data Structure for internal parsing Azure CLI Groups & Commands 31 | groups_map 32 | group_name1 => group1 33 | -name 34 | -summary 35 | -subgroups = [] 36 | -commands =[] 37 | group_name2 => group2 38 | ... 39 | commands_map 40 | cmd1 => cmd_object1 41 | -summary 42 | -arguments : [] 43 | -argument_tree : {} 44 | arg1 => arg_object1 {} 45 | -required 46 | -options 47 | -help 48 | arg2 => arg_object2 49 | arg3 => arg_object3 50 | -example 51 | -command 52 | -group 53 | cmd2 => cmd_object2 54 | ... 55 | """ 56 | def new_parsing_group(): 57 | return { 58 | 'name': '', 59 | 'commands': [], 60 | 'subgroups': [], 61 | 'summary': '', 62 | } 63 | def new_parsing_command(): 64 | return { 65 | 'summary': '', 66 | 'arguments': [], 67 | 'argument_tree': {}, 68 | 'example': '', 69 | 'command': '', 70 | 'group': '' 71 | } 72 | def new_parsing_argument(): 73 | return { 74 | 'required': False, 75 | 'options': '', 76 | 'help': '' 77 | } 78 | 79 | def dump_cmd_object(c): 80 | logger.debug("cmd dump: command:{} summary:{} arguments:{} example:{} group:{}".format( 81 | c['command'], c['summary'], "|".join(c['arguments']), c['example'], c['group'])) 82 | 83 | def dump_group_object(g): 84 | logger.debug("group dump:{} subgroup:{} summary:{} cmdlist={}".format( 85 | g['name'], "|".join(g['subgroups']), g['summary'], "|".join(g['commands']))) 86 | 87 | 88 | 89 | def convert_help_column(c): 90 | c = c.replace(_AZURE_SHELL_INDEX_GEN_USER_HOME, '') 91 | c = c.replace(_AZURE_SHELL_INDEX_GEN_USER, '') 92 | return c 93 | 94 | IN_BLOCK_TYPE_NONE = 0 95 | IN_BLOCK_TYPE_COMMAND = 1 96 | IN_BLOCK_TYPE_ARGUMENTS = 2 97 | IN_BLOCK_TYPE_EXAMPLES = 3 98 | 99 | def capture_cmd(cmd): 100 | logger.debug("Capturing command: {}".format(cmd)) 101 | cmd_string = 'az {} -h'.format(cmd) 102 | proc = subprocess.Popen(cmd_string, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 103 | cmd_object= new_parsing_command() 104 | 105 | in_block_type = IN_BLOCK_TYPE_NONE 106 | cur_argument = '' 107 | while True: 108 | line = proc.stdout.readline() 109 | l = line.strip() 110 | if l =='Command': 111 | in_block_type = IN_BLOCK_TYPE_COMMAND 112 | elif l.find("Arguments") >= 0: 113 | in_block_type = IN_BLOCK_TYPE_ARGUMENTS 114 | elif l.find("Examples") >= 0: 115 | in_block_type = IN_BLOCK_TYPE_EXAMPLES 116 | elif l: 117 | if in_block_type == IN_BLOCK_TYPE_COMMAND: 118 | colon_pos = l.find(':') 119 | if colon_pos > 0: 120 | l = l[colon_pos+1:].lstrip() 121 | cmd_object['summary'] = cmd_object['summary'] + l 122 | elif in_block_type ==IN_BLOCK_TYPE_ARGUMENTS: 123 | ### options 124 | if l[0:2] == '--': 125 | arg_object = new_parsing_argument() 126 | colon_pos = l.find(':') 127 | options_column = l[0:colon_pos].rstrip() 128 | help_column = l[colon_pos+1:].strip() 129 | required_pos = options_column.find('[Required]') 130 | options = options_column 131 | is_required=False 132 | if required_pos > 0: 133 | arg_object['required'] = True 134 | options = options_column[0:required_pos].rstrip() 135 | arg_object['options'] = options 136 | arg_object['help'] = convert_help_column(help_column) 137 | l = cmd_object['arguments'] 138 | l.append(options) 139 | cmd_object['arguments'] = l 140 | cmd_object['argument_tree'][options]=arg_object 141 | cmd_object['group']= get_group_name_from_command(cmd) 142 | cmd_object['command']=cmd 143 | cur_argument = options 144 | else: 145 | arg_object = cmd_object['argument_tree'][cur_argument] 146 | arg_object['help'] = convert_help_column("{} {}".format(arg_object['help'],l)) 147 | cmd_object['argument_tree'][cur_argument] = arg_object 148 | elif in_block_type ==IN_BLOCK_TYPE_EXAMPLES: 149 | cmd_object['example'] = "{}\n{}".format(cmd_object['example'],l) 150 | else: 151 | in_block_type = IN_BLOCK_TYPE_NONE ## reset 152 | logger.error("[Warning] OTHER::cmd:{} :: {}".format(cmd_string, l)) 153 | 154 | # Break the loop when 155 | # - buffer is empty and 156 | # - Popen.poll returns not None (=process terminate) 157 | if not line and proc.poll() is not None: 158 | break 159 | dump_cmd_object(cmd_object) 160 | return cmd_object 161 | 162 | def get_group_name_from_command(cmd): 163 | group_name = " ".join(cmd.split()[:-1]) 164 | if not group_name: 165 | group_name = 'az' 166 | return group_name 167 | 168 | def get_parent_name_from_group_name(group_name): 169 | parent = " ".join(group_name.split()[:-1]) 170 | if not parent and group_name != 'az': 171 | parent = 'az' 172 | return parent 173 | 174 | ## Create Group & SubGroup Tree 175 | def get_groups(cmd_table): 176 | from azure.cli.core.help_files import helps 177 | groups_map = {} 178 | ## Populate group and group summary from helps 179 | for cmd in helps: 180 | diction_help = yaml.load(helps[cmd]) 181 | if diction_help.has_key('type') and diction_help['type'] != 'group': 182 | continue 183 | group_name = cmd 184 | group = new_parsing_group() 185 | if diction_help.has_key('short-summary'): 186 | group['name'] = group_name 187 | group['summary'] = diction_help['short-summary'] 188 | groups_map[group_name] = group 189 | ## Populate group from cmd table 190 | for cmd in cmd_table: 191 | group_name = get_group_name_from_command(cmd) 192 | if not groups_map.has_key(group_name): 193 | group = new_parsing_group() 194 | group_cmd_list = [] 195 | group_cmd_list.append(cmd) 196 | group['name'] = group_name 197 | group['commands'].append(cmd) 198 | groups_map[group_name] = group 199 | else: 200 | groups_map[group_name]['commands'].append(cmd) 201 | return groups_map 202 | 203 | def LEAF_NODE(node): 204 | return node.split()[-1] 205 | 206 | def LEAF_NODES(nodes): 207 | l=[] 208 | for node in nodes: 209 | l.append(node.split()[-1]) 210 | return l 211 | 212 | def populate_group_command_tree(group, groups_map,cmds_map): 213 | ## Inrease the limit just in case recursive func calling use up recursionlimit 214 | #import sys 215 | #sys.setrecursionlimit(10000) 216 | subgroups = [] 217 | for subgroup_name in group['subgroups']: 218 | if groups_map.has_key(subgroup_name): 219 | subgroup = groups_map[subgroup_name] 220 | populate_group_command_tree(subgroup, groups_map, cmds_map) 221 | subgroups.append(subgroup) 222 | group['subgroups']=subgroups 223 | cmd_list = [] 224 | for cmd_name in group['commands']: 225 | if cmds_map.has_key(cmd_name): 226 | cmd_list.append(cmds_map[cmd_name]) 227 | group['commands']=cmd_list 228 | 229 | def group_to_index_tree(index_tree, group_name, groups_map,cmds_map): 230 | index_tree['arguments'] = [] 231 | index_tree['argument_tree'] = {} 232 | index_tree['summary'] = groups_map[group_name]['summary'] 233 | 234 | child_index_tree_dict = [] 235 | leaf_command_list = [] 236 | # group.commands -> index_tree.commands 237 | leaf_command_list = LEAF_NODES(groups_map[group_name]['commands']) 238 | # group.commands -> index_tree.command_tree 239 | child_index_tree_dict = {} 240 | for cmd_name in groups_map[group_name]['commands']: 241 | child_index_tree = new_index_command_tree() 242 | command_to_index_tree(child_index_tree, cmd_name, cmds_map) 243 | child_index_tree_dict[LEAF_NODE(cmd_name)] = child_index_tree 244 | # group.subgroups -> index_tree.commands + index_tree.command_tree 245 | for subgroup_name in groups_map[group_name]['subgroups']: 246 | leaf_command_list.append(LEAF_NODE(subgroup_name)) 247 | child_index_tree = new_index_command_tree() 248 | group_to_index_tree(child_index_tree, subgroup_name, groups_map,cmds_map) 249 | child_index_tree_dict[LEAF_NODE(subgroup_name)] = child_index_tree 250 | 251 | index_tree['commands'] = leaf_command_list 252 | index_tree['command_tree'] = child_index_tree_dict 253 | 254 | def command_to_index_tree(index_tree, command_name, cmds_map): 255 | if cmds_map.has_key(command_name): 256 | index_tree['arguments'] = cmds_map[command_name]['arguments'] 257 | index_tree['argument_tree'] = cmds_map[command_name]['argument_tree'] 258 | index_tree['commands'] = [] 259 | index_tree['command_tree'] = {} 260 | index_tree['summary'] = cmds_map[command_name]['summary'] 261 | 262 | ## 263 | ## Main entrypoint for Azure Shell Index Generator 264 | ## 265 | def main(): 266 | parser = argparse.ArgumentParser(description='Azure Shell CLI Index Generator') 267 | parser.add_argument( 268 | '-o','--output', 269 | help='Azure Shell index file output path (ex. /tmp/cli-index.json)') 270 | parser.add_argument( 271 | '--verbose', action='store_true', 272 | help='Increase verbosity') 273 | args = parser.parse_args() 274 | 275 | ## Logging config 276 | handler = logging.StreamHandler(sys.stdout) 277 | if args.verbose: 278 | handler.setLevel(logging.DEBUG) 279 | logger.setLevel(logging.DEBUG) 280 | else: 281 | handler.setLevel(logging.INFO) 282 | logger.setLevel(logging.INFO) 283 | logger.addHandler(handler) 284 | 285 | # Output file validation and config 286 | if not args.output: 287 | logger.error('[ERROR] No output file specified with -o or --output param!') 288 | sys.exit(1) 289 | output_file = os.path.abspath(args.output) 290 | if not os.path.isdir(os.path.dirname(output_file)): 291 | logger.error('[ERROR] No directory exists for output file:{}'.format(output_file)) 292 | sys.exit(1) 293 | 294 | ## az executable command path check 295 | if not find_executable_path('az'): 296 | logger.error("[ERROR] NO azure cli (az) executable command found!") 297 | logger.error("Please install Azure CLI 2.0 and set its executable dir to PATH") 298 | logger.error("See https://github.com/Azure/azure-cli") 299 | sys.exit(1) 300 | 301 | azure_cli_version = get_cli_version() 302 | ## Check minimum azure-cli version 303 | if azure_cli_version < AZURE_SHELL_MINIMUM_AZURE_CLI_VERSION: 304 | logger.error("[ERROR] Azure CLI 2.0 minimum version failure!") 305 | logger.error("Minimum azure-cli version required: {} (Your version: {})".format( 306 | AZURE_SHELL_MINIMUM_AZURE_CLI_VERSION, azure_cli_version)) 307 | logger.error("Please install the latest azure-cli and set its executable dir to PATH") 308 | logger.error("See https://github.com/Azure/azure-cli") 309 | sys.exit(1) 310 | 311 | ## Import Azure CLI core modules 312 | module_import_trial_count = 0 313 | while True: 314 | try: 315 | from azure.cli.core.help_files import helps 316 | from azure.cli.core.application import APPLICATION, Configuration 317 | break 318 | except ImportError: 319 | if module_import_trial_count > 0: 320 | logger.error("[ERROR] azure.cli.core module import failure!") 321 | sys.exit(1) 322 | ## Getting AzureCLI modules path and append it to current path list 323 | azurecli_modules_path = get_azurecli_modules_path() 324 | if azurecli_modules_path: 325 | sys.path.append(azurecli_modules_path) 326 | module_import_trial_count = module_import_trial_count + 1 327 | 328 | ## Get command table via acure.cli.core 329 | azure_cli_version = get_cli_version() 330 | cmd_table = {} 331 | if azure_cli_version < '2.0.3': # External I/F has been changed since azure-cli-2.0.3 332 | cmd_table = APPLICATION.configuration.get_command_table() 333 | else: 334 | config = Configuration() 335 | cmd_table = config.get_command_table() 336 | 337 | groups_map = {} 338 | cmds_map = {} 339 | groups_map = get_groups(cmd_table) 340 | ## Populate subgroups for each groups 341 | for group_name in groups_map.keys(): 342 | parent_name = get_parent_name_from_group_name(group_name) 343 | if parent_name: 344 | groups_map[parent_name]['subgroups'].append(group_name) 345 | 346 | logger.info("Start generating index...") 347 | logger.info("Output index file: {}".format(output_file)) 348 | 349 | ## Get command list 350 | cmd_list = cmd_table.keys() 351 | for cmd in cmd_list: 352 | try: 353 | cmds_map[cmd] = capture_cmd(cmd) 354 | except: 355 | pass 356 | 357 | ## Create Json Tree from root 'az' group 358 | index_tree = new_index_command_tree() 359 | group_to_index_tree(index_tree, 'az', groups_map,cmds_map) 360 | root_tree = {'az': index_tree} 361 | result_json = json.dumps(root_tree) 362 | 363 | ## Write json to your specified output file 364 | with open(output_file, "w") as f: 365 | f.write(result_json) 366 | -------------------------------------------------------------------------------- /azureshell/lexer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pygments.lexer import words 4 | from pygments.lexer import RegexLexer 5 | from pygments.token import Text, Keyword, Literal, Name, Operator 6 | from .index import AzureShellIndex, get_completions_commands_and_arguments 7 | from .cache import AzureShellCache 8 | 9 | class AzureShellLexer(RegexLexer): 10 | index_data = {} 11 | index_data = AzureShellIndex.load_index( AzureShellCache.Instance().get('index_file')) 12 | completions = get_completions_commands_and_arguments(index_data) 13 | 14 | tokens = { 15 | 'root': [ 16 | (words( tuple(['az']), prefix=r'\b', suffix=r'\b'), Literal.String), 17 | (words( tuple(completions['commands']), prefix=r'\b', suffix=r'\b'), Name.Class), 18 | (words( tuple(list(completions['args'])), prefix=r'', suffix=r'\b'), Keyword.Declaration), 19 | # Everything else 20 | (r'.*\n', Text), 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /azureshell/logger.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | _DEFAULT_AZURE_SHELL_LOG_LEVEL = 'INFO' 5 | _DEFAULT_AZURE_SHELL_LOG_FORMAT = '%(asctime)s %(levelname)s %(message)s' 6 | 7 | def init_logger(name, log_file, 8 | log_level = _DEFAULT_AZURE_SHELL_LOG_LEVEL, 9 | log_format = _DEFAULT_AZURE_SHELL_LOG_FORMAT): 10 | 11 | logger = logging.getLogger(name) 12 | handler = logging.FileHandler(os.path.expanduser(log_file)) 13 | formatter = logging.Formatter(log_format) 14 | handler.setFormatter(formatter) 15 | logger.addHandler(handler) 16 | switcher = { 17 | 'CRITICAL': logging.CRITICAL, 18 | 'ERROR': logging.ERROR, 19 | 'WARNING': logging.WARNING, 20 | 'INFO': logging.INFO, 21 | 'DEBUG': logging.DEBUG 22 | } 23 | logger.setLevel(switcher.get(log_level.upper(),logging.INFO)) 24 | -------------------------------------------------------------------------------- /azureshell/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | import re 6 | import subprocess 7 | from . import compat 8 | from .cache import AzureShellCache 9 | 10 | AZURE_SHELL_MINIMUM_AZURE_CLI_VERSION = '2.0.0' 11 | 12 | def AS_ERR(s): 13 | sys.stderr.write("{}\n".format(s)) 14 | 15 | def find_executable_path(executable): 16 | path = os.environ['PATH'] 17 | pathlist = path.split(os.pathsep) 18 | if os.path.isfile(executable): 19 | return executable 20 | else: 21 | for path in pathlist: 22 | f = os.path.join(path, executable) 23 | if os.path.isfile(f): 24 | return f 25 | return '' 26 | 27 | def get_cli_version(): 28 | v = AzureShellCache.Instance().get('azure_cli_version') 29 | if v: 30 | return v 31 | cmd_string = 'az --version' 32 | proc = subprocess.Popen(cmd_string,shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 33 | while True: 34 | line = proc.stdout.readline() 35 | l = line.strip().decode('utf-8') 36 | if l.startswith('azure-cli'): 37 | r = re.compile("([a-z-]*) \((.*)\)") 38 | o = r.findall(l) 39 | if len(o) == 1 and len(o[0]) ==2: 40 | v = o[0][1] 41 | AzureShellCache.Instance().set('azure_cli_version',v) 42 | break 43 | if not line and proc.poll() is not None: 44 | break 45 | return v 46 | 47 | 48 | def get_azurecli_modules_path(): 49 | ## Getting a path for python executable taht AzureCLI leverage 50 | executable = find_executable_path('az') 51 | python_path = None 52 | f = open(executable) 53 | l = f.readline().strip() 54 | while l: 55 | if not l.startswith('#') and l.find('python'): 56 | cols = l.split() 57 | if len(cols) > 1: 58 | python_path = cols[0] 59 | break 60 | l = f.readline().strip() 61 | f.close 62 | if not python_path: 63 | return None 64 | ## Getting python module paths that Azure CLI configures 65 | azurecli_modules_path =None 66 | cmd_string="{} < /dev/null || cwd=`(cd "$cwd" && pwd)` 5 | cd ${cwd}/.. 6 | sudo rm -rf dist build 7 | python setup.py sdist 8 | sudo python setup.py install 9 | cd ${cwd} 10 | azure-shell 11 | -------------------------------------------------------------------------------- /scripts/get-styles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from pygments.styles import get_all_styles 5 | styles = list(get_all_styles()) 6 | print "All available styles :" 7 | print styles 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import re 3 | import os 4 | import ast 5 | 6 | try: 7 | from setuptools import setup, find_packages 8 | except ImportError: 9 | from distutils.core import setup, find_packages 10 | 11 | requires = [ 12 | 'prompt-toolkit>=1.0.0,<1.1.0', 13 | 'configparser>=3.5.0', 14 | 'Pygments>=2.1.3,<3.0.0', 15 | 'pyyaml', 16 | ] 17 | 18 | long_description = 'An interactive Azure CLI 2.0 command line interface' 19 | if os.path.exists('README.txt'): 20 | long_description = open('README.txt', encoding='utf-8').read() 21 | 22 | with open('azureshell/__init__.py', 'r') as fd: 23 | version = re.search( 24 | r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', 25 | fd.read(), re.MULTILINE).group(1) 26 | 27 | setup( 28 | description='An interactive Azure CLI 2.0 command line interface', 29 | long_description=long_description, 30 | author='Yoichi Kawasaki', 31 | author_email='yoichi.kawasaki@outlook.com', 32 | name='azure-shell', 33 | version=version, 34 | url='https://github.com/yokawasa/azure-shell', 35 | download_url='https://pypi.python.org/pypi/azure-shell', 36 | packages=find_packages(exclude=['tests*']), 37 | include_package_data=True, 38 | package_data={'azureshell': ['azureshell.conf']}, 39 | install_requires=requires, 40 | entry_points={ 41 | 'console_scripts': [ 42 | 'azure-shell = azureshell:main', 43 | 'azure-shell-index-generator = azureshell.index_generator:main', 44 | ] 45 | }, 46 | license="Apache License 2.0", 47 | platforms='any', 48 | classifiers=[ 49 | 'Environment :: Console', 50 | 'Intended Audience :: Developers', 51 | 'Intended Audience :: System Administrators', 52 | 'Natural Language :: English', 53 | 'License :: OSI Approved :: Apache Software License', 54 | 'Programming Language :: Python', 55 | 'Programming Language :: Python :: 2.7', 56 | 'Programming Language :: Python :: 3', 57 | 'Programming Language :: Python :: 3.3', 58 | 'Programming Language :: Python :: 3.4', 59 | 'Programming Language :: Python :: 3.5', 60 | 'Programming Language :: Python :: 3.6', 61 | ], 62 | keywords='azure azure-shell, shell, azure-cli', 63 | ) 64 | --------------------------------------------------------------------------------