├── MANIFEST.in ├── bin └── jpterm ├── LICENSE ├── CHANGELOG.rst ├── setup.py ├── README.rst └── jpterm.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /bin/jpterm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import jpterm 3 | 4 | 5 | if __name__ == '__main__': 6 | jpterm.main() 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 James Saryerwinnie 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 0.2.2 2 | ===== 3 | 4 | * Use OrderedDict to retain key order from expression 5 | in output. 6 | 7 | 0.2.1 8 | ===== 9 | 10 | * Add an ``-o|--output-file`` option that allows you to 11 | control where the final output is written. 12 | * Relax pygments version constraint. 13 | 14 | 0.2.0 15 | ===== 16 | 17 | * Backwards incompatible: Ctrl-p has been changed to now 18 | be an output mode toggle. The output mode tells jpterm 19 | what to print (if anything) when it exits. You can now 20 | no longer save and print multiple expressions. 21 | * You can now exit jpterm using ctrl-c in addition to 22 | F5. This fixes issues for people that were unable to 23 | use F5 previously. 24 | 25 | 0.1.0 26 | ===== 27 | 28 | * Add support for reading the input JSON document from stdin. 29 | This also changes the -i option to a positional argument. 30 | * Rename main executable from jmespath-term to jpterm. 31 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | import io 5 | 6 | from setuptools import setup, find_packages 7 | 8 | 9 | requires = [ 10 | 'jmespath>=0.8.0,<=1.0.0', 11 | 'Pygments>=2.0,<3.0', 12 | 'urwid==1.2.2' 13 | ] 14 | 15 | 16 | setup( 17 | name='jmespath-terminal', 18 | version='0.2.2', 19 | description='JMESPath Terminal', 20 | long_description=io.open('README.rst', encoding='utf-8').read(), 21 | author='James Saryerwinnie', 22 | author_email='js@jamesls.com', 23 | url='https://github.com/jmespath/jmespath.terminal', 24 | scripts=['bin/jpterm'], 25 | py_modules=['jpterm'], 26 | install_requires=requires, 27 | classifiers=( 28 | 'Development Status :: 4 - Beta', 29 | 'Intended Audience :: Developers', 30 | 'Natural Language :: English', 31 | 'License :: OSI Approved :: Apache Software License', 32 | 'Programming Language :: Python', 33 | 'Programming Language :: Python :: 2.6', 34 | 'Programming Language :: Python :: 2.7', 35 | 'Programming Language :: Python :: 3', 36 | 'Programming Language :: Python :: 3.3', 37 | ), 38 | ) 39 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | JMESPath Terminal 3 | ================= 4 | 5 | JMESPath, in your terminal! 6 | 7 | .. image:: https://cloud.githubusercontent.com/assets/368057/5158769/6546e58a-72fe-11e4-8ceb-ba866777983e.gif 8 | 9 | 10 | Overview 11 | ======== 12 | 13 | JMESPath is an expression language for manipulating JSON documents. If you've 14 | never heard of JMESPath before, you write a JMESPath expression that when 15 | applied to an input JSON document will produces an output JSON document based 16 | on the expression you've provided. 17 | 18 | You can check out the `JMESPath site 19 | `__ for more information. 20 | 21 | One of the best ways to learn the JMESPath language is to experiment 22 | by creating your own JMESPath expressions. The JMESPath Terminal 23 | makes it easy to see the results of your JMESPath expressions immediately 24 | as you type. 25 | 26 | 27 | Getting Started 28 | =============== 29 | 30 | You can install the JMESPath Terminal via pip:: 31 | 32 | $ pip install jmespath-terminal 33 | 34 | There will then be a ``jpterm`` program you can run:: 35 | 36 | $ jpterm 37 | 38 | With no arguments specified, a sample JSON document is used as 39 | input. 40 | 41 | You can also specify an initial JSON document to use 42 | by specifying the JSON file as a positional argument:: 43 | 44 | $ jpterm /tmp/somejsondoc.json 45 | 46 | You can also pipe an input JSON document into the 47 | ``jpterm`` command: 48 | 49 | .. image:: https://cloud.githubusercontent.com/assets/368057/5158770/6a6afb6e-72fe-11e4-8be3-893edf21920e.gif 50 | 51 | Output 52 | ------ 53 | 54 | When the ``jpterm`` program exits (via ``F5`` or ``Ctrl-c``), ``jpterm`` may 55 | write content to stdout depending on the output mode. There are three output 56 | modes: 57 | 58 | * result - Whatever is in the "JMESPath result" pane (the right hand side) will 59 | be printed to stdout. 60 | * expression - Whatever is in the "JMESPath expression" pane is printed to 61 | stdout. 62 | * nothing - Nothing is written to stdout when exiting. 63 | 64 | The default mode is "result", which means that by default, whatever is in the 65 | result pane will be printed to stdout when ``jpterm`` exits. You can switch 66 | output modes using ``Ctrl-p``, which will cycle through the three modes above. 67 | You can also specify what mode to use when starting the ``jpterm`` command 68 | using the ``-m/--output-mode`` command line option. 69 | 70 | Keyboard Shortcuts 71 | ------------------ 72 | 73 | ``F5 or Ctrl + c`` 74 | | Quit the program. 75 | ``Ctrl + p`` 76 | | Output mode toggle. Toggle between outputting the current result, 77 | | expression, or nothing. This is discussed in the "Output" section above. 78 | ``Ctrl + ]`` 79 | | Clear the current expression. 80 | 81 | Mouse Clicks 82 | ------------ 83 | 84 | NOTE: These features are dependent on terminal support. (The Terminal.app 85 | included in Mac OS X does not support this, but `iTerm2 `_ 86 | does.) 87 | 88 | One feature of the Urwid Python package (which jmespath-terminal is built on) 89 | is that mouse clicks are recognized. This allows you to click to switch focus 90 | on either the Input or Result window (and of course back to the Expression) and 91 | scroll it. 92 | 93 | This can make it difficult to select text for copying/pasting. Many Linux 94 | terminals will allow you to select the text with a ``Shift + click/drag`` and 95 | copy it with ``Shift + Ctrl + c``. In iTerm2 selections can be made with 96 | ``Opt/Alt + click/drag``. 97 | 98 | Working on JMESPath Terminal 99 | ============================ 100 | 101 | If you like to work on jmespath-terminal to add new features, 102 | you can first create and activate a new virtual environment:: 103 | 104 | $ virtualenv venv 105 | $ . venv/bin/activate 106 | 107 | Then install the module:: 108 | 109 | $ pip install -e . 110 | 111 | You'll now be able to modify the ``jpterm.py`` module and see 112 | your changes reflected when you run the ``jpterm`` command. 113 | 114 | Beta Status 115 | =========== 116 | 117 | Until jmespath-terminal reaches version 1.0, some of the command line options 118 | and semantics may change. There will be a CHANGELOG.rst that will outline any 119 | changes that occur for each new version. 120 | -------------------------------------------------------------------------------- /jpterm.py: -------------------------------------------------------------------------------- 1 | """JMESPath text terminal.""" 2 | import os 3 | import sys 4 | import json 5 | import argparse 6 | 7 | import urwid 8 | import jmespath 9 | import pygments.lexers 10 | import collections 11 | 12 | 13 | __version__ = '0.2.2' 14 | 15 | 16 | SAMPLE_JSON = { 17 | 'a': 'foo', 18 | 'b': 2, 19 | 'c': { 20 | 'd': 'baz', 21 | 'e': [1, 2, 3] 22 | }, 23 | "d": True, 24 | "e": None, 25 | "f": 1.1 26 | } 27 | OUTPUT_MODES = [ 28 | 'result', 29 | 'expression', 30 | 'quiet', 31 | ] 32 | 33 | 34 | class ConsoleJSONFormatter(object): 35 | # We only need to worry about the tokens that can come 36 | # from lexing JSON. 37 | TOKEN_TYPES = { 38 | # For the values of JSON strings. 39 | 'Token.Literal.String.Double': urwid.AttrSpec('dark green', 'default'), 40 | 'Token.Literal.Number.Integer': urwid.AttrSpec('dark blue', 'default'), 41 | 'Token.Literal.Number.Float': urwid.AttrSpec('dark blue', 'default'), 42 | # null, true, false 43 | 'Token.Keyword.Constant': urwid.AttrSpec('light blue', 'default'), 44 | 'Token.Punctuation': urwid.AttrSpec('light blue', 'default'), 45 | 'Token.Text': urwid.AttrSpec('white', 'default'), 46 | # Key names in a hash. 47 | 'Token.Name.Tag': urwid.AttrSpec('white', 'default'), 48 | 49 | } 50 | # Used when the token name is not in the list above. 51 | DEFAULT_COLOR = urwid.AttrSpec('light blue', 'default'), 52 | 53 | def generate_colors(self, tokens): 54 | types = self.TOKEN_TYPES 55 | default = self.DEFAULT_COLOR 56 | for token_type, token_string in tokens: 57 | yield types.get(str(token_type), default), token_string 58 | 59 | 60 | class JMESPathDisplay(object): 61 | 62 | PALETTE = [ 63 | ('input expr', 'black,bold', 'light gray'), 64 | ('bigtext', 'white', 'black'), 65 | ] 66 | 67 | def __init__(self, input_data, output_mode='result'): 68 | self.view = None 69 | self.parsed_json = input_data 70 | self.lexer = pygments.lexers.get_lexer_by_name('json') 71 | self.formatter = ConsoleJSONFormatter() 72 | self.output_mode = output_mode 73 | self.last_result = None 74 | self.last_expression = None 75 | 76 | def _create_colorized_json(self, json_string): 77 | tokens = self.lexer.get_tokens(json_string) 78 | markup = list(self.formatter.generate_colors(tokens)) 79 | return markup 80 | 81 | def _get_font_instance(self): 82 | return urwid.get_all_fonts()[-2][1]() 83 | 84 | def _create_view(self): 85 | self.input_expr = urwid.Edit(('input expr', "JMESPath Expression: ")) 86 | 87 | sb = urwid.BigText("JMESPath", self._get_font_instance()) 88 | sb = urwid.Padding(sb, 'center', None) 89 | sb = urwid.AttrWrap(sb, 'bigtext') 90 | sb = urwid.Filler(sb, 'top', None, 5) 91 | self.status_bar = urwid.BoxAdapter(sb, 5) 92 | 93 | div = urwid.Divider() 94 | self.header = urwid.Pile( 95 | [self.status_bar, div, 96 | urwid.AttrMap(self.input_expr, 'input expr'), div], 97 | focus_item=2) 98 | urwid.connect_signal(self.input_expr, 'change', self._on_edit) 99 | 100 | self.input_json = urwid.Text( 101 | self._create_colorized_json(json.dumps(self.parsed_json, 102 | indent=2)) 103 | ) 104 | self.input_json_list = [div, self.input_json] 105 | self.left_content = urwid.ListBox(self.input_json_list) 106 | self.left_content = urwid.LineBox(self.left_content, 107 | title='Input JSON') 108 | 109 | self.jmespath_result = urwid.Text("") 110 | self.jmespath_result_list = [div, self.jmespath_result] 111 | self.right_content = urwid.ListBox(self.jmespath_result_list) 112 | self.right_content = urwid.LineBox(self.right_content, 113 | title='JMESPath Result') 114 | 115 | self.content = urwid.Columns([self.left_content, self.right_content]) 116 | 117 | self.footer = urwid.Text("Status: ") 118 | self.view = urwid.Frame(body=self.content, header=self.header, 119 | footer=self.footer, focus_part='header') 120 | 121 | def _on_edit(self, widget, text): 122 | self.last_expression = text 123 | if not text: 124 | # If a user has hit backspace until there's no expression 125 | # left, we can exit early and just clear the result text 126 | # panel. 127 | self.jmespath_result.set_text('') 128 | return 129 | try: 130 | options = jmespath.Options(dict_cls=collections.OrderedDict) 131 | result = jmespath.compile(text).search(self.parsed_json, options) 132 | self.footer.set_text("Status: success") 133 | except Exception: 134 | pass 135 | else: 136 | if result is not None: 137 | self.last_result = result 138 | result_markup = self._create_colorized_json( 139 | json.dumps(result, indent=2)) 140 | self.jmespath_result.set_text(result_markup) 141 | 142 | def main(self, screen=None): 143 | self._create_view() 144 | self.loop = urwid.MainLoop(self.view, self.PALETTE, 145 | unhandled_input=self.unhandled_input, 146 | screen=screen) 147 | self.loop.screen.set_terminal_properties(colors=256) 148 | self.loop.run() 149 | 150 | def unhandled_input(self, key): 151 | if key == 'f5': 152 | raise urwid.ExitMainLoop() 153 | elif key == 'ctrl ]': 154 | # Keystroke to quickly empty out the 155 | # currently entered expression. Avoids 156 | # having to hold backspace to delete 157 | # the current expression current expression. 158 | self.input_expr.edit_text = '' 159 | self.jmespath_result.set_text('') 160 | elif key == 'ctrl p': 161 | new_mode = OUTPUT_MODES[ 162 | (OUTPUT_MODES.index(self.output_mode) + 1) % len(OUTPUT_MODES)] 163 | self.output_mode = new_mode 164 | self.footer.set_text("Status: output mode set to %s" % new_mode) 165 | 166 | def display_output(self, filename): 167 | if self.output_mode == 'result' and \ 168 | self.last_result is not None: 169 | result = json.dumps(self.last_result, indent=2) 170 | elif self.output_mode == 'expression' and \ 171 | self.last_expression is not None: 172 | result = self.last_expression 173 | else: 174 | # If the output_mode is 'quiet' then we don't need to print anything. 175 | return 176 | if filename is not None: 177 | with open(filename, 'w') as f: 178 | f.write(result) 179 | else: 180 | sys.stdout.write(result) 181 | 182 | 183 | def _load_input_json(filename): 184 | if filename is not None: 185 | with open(filename) as f: 186 | input_json = json.load(f) 187 | elif not os.isatty(sys.stdin.fileno()): 188 | # If stdin is a pipe, we need read the JSON from 189 | # stdin and then reset stdin this back to the controlling tty. 190 | input_json = json.loads(sys.stdin.read()) 191 | sys.stdin = open(os.ctermid(), 'r') 192 | else: 193 | # If the user didn't provide a filename, 194 | # we want to be helpful so we'll use a sample 195 | # document so they can still try out the 196 | # JMESPath Terminal. 197 | input_json = SAMPLE_JSON 198 | return input_json 199 | 200 | 201 | def main(): 202 | parser = argparse.ArgumentParser(description=__doc__) 203 | parser.add_argument('input-json', nargs='?', 204 | help='The initial input JSON file to use. ' 205 | 'If this value is not provided, a sample ' 206 | 'JSON document will be provided.') 207 | parser.add_argument('-m', '--output-mode', 208 | choices=OUTPUT_MODES, 209 | default='result', 210 | help="Specify what's printed to stdout " 211 | "when jpterm exits. This can also be changed " 212 | "when jpterm is running using Ctrl-o") 213 | parser.add_argument('-o', '--output-file', 214 | help="By default, the output is printed " 215 | "to stdout when jpterm exits. You can " 216 | "instead direct the output to a file using " 217 | "the -o/--ouput-file option.") 218 | parser.add_argument('--version', action='version', 219 | version='jmespath-term %s' % __version__) 220 | 221 | args = parser.parse_args() 222 | try: 223 | input_json = _load_input_json(getattr(args, 'input-json', None)) 224 | except ValueError as e: 225 | sys.stderr.write("Unable to load the input JSON: %s\n\n" % e) 226 | return 1 227 | 228 | screen = urwid.raw_display.Screen() 229 | display = JMESPathDisplay(input_json, args.output_mode) 230 | try: 231 | display.main(screen=screen) 232 | except KeyboardInterrupt: 233 | pass 234 | display.display_output(args.output_file) 235 | return 0 236 | 237 | 238 | if __name__ == '__main__': 239 | sys.exit(main()) 240 | --------------------------------------------------------------------------------