├── .github └── workflows │ └── pythonapp.yml ├── .gitignore ├── CHANGELOG ├── LICENSE ├── MANIFEST.in ├── README.md ├── _config.yml ├── build-package.sh ├── completions ├── jtbl_bash_completion.sh └── jtbl_zsh_completion.sh ├── install.sh ├── jtbl ├── __init__.py ├── __main__.py └── cli.py ├── man └── jtbl.1 ├── pypi-upload.sh ├── requirements.txt ├── runtests.sh ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── test_get_json.py └── test_make_table.py /.github/workflows/pythonapp.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | paths: 6 | - "**/*.py" 7 | pull_request: 8 | paths: 9 | - "**/*.py" 10 | 11 | jobs: 12 | build: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [macos-latest, ubuntu-latest, windows-latest] 17 | python-version: ["3.7", "3.8", "3.9", "3.10"] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v1 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install -r requirements.txt 29 | - name: Test with unittest 30 | run: | 31 | python -m unittest discover tests 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | dist/ 4 | build/ 5 | *.egg-info/ 6 | .github/ 7 | .vscode/settings.json 8 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | jtbl changelog 2 | 3 | 20231210 v1.6.0 4 | - Add long options 5 | - Add DocuWiki table option 6 | - Add Bash and Zsh completions 7 | 8 | 20231022 v1.5.3 9 | - Add `-f` option for fancy table output 10 | 11 | 20230522 v1.5.2 12 | - Fix output for empty array 13 | 14 | 20230228 v1.5.1 15 | - Preserve long float formatting 16 | 17 | 20230114 v1.5.0 18 | - Be more tolerant of loading JSON Lines with blank lines and whitespace 19 | 20 | 20221006 v1.4.0 21 | - Add CSV (`-c`) table output option 22 | - Add HTML (`-H`) table output option 23 | - Preserve column order when wrapping or truncation is required 24 | - Bump tabulate library to v0.8.10 25 | 26 | 20220731 v1.3.2 27 | - Add `__main__.py` for `python -m jtbl` use cases 28 | 29 | 20220404 v1.3.1 30 | - Add quiet option to suppress errors 31 | - Add man page to source package 32 | 33 | 20220215 v1.3.0 34 | - Add -m markdown table output option 35 | - Add man page 36 | 37 | 20211223 v1.2.3 38 | - Remove item header from rotate view if there is only one item 39 | - Add separator line under item header in rotate view 40 | 41 | 20211222 v1.2.2 42 | - Use nicer grid lines for tables 43 | 44 | 20211222 v1.2.1 45 | - Remove borders around rotated items 46 | 47 | 20211221 v1.2.0 48 | - Add rotate option to print seperate key/value tables for each row 49 | 50 | 20210712 v1.1.7 51 | - Include CHANGELOG in source distribution (no other code changes) 52 | 53 | 20200614 v1.1.6 54 | - Move handling of blank input to make_table() 55 | 56 | 20200614 v1.1.5 57 | - Improve handling of blank input 58 | 59 | 20200612 v1.1.4 60 | - Handle blank input gracefully 61 | 62 | 20200509 v1.1.3 63 | - Remove tests from package 64 | 65 | 20200409 v1.1.2 66 | - fix break on pipe error 67 | 68 | 20200318 v1.1.1 69 | - Add --cols option to manually set the terminal width 70 | 71 | 20200318 v1.1.0 72 | - Refactored code for unit tests 73 | - Added unit tests 74 | 75 | 20200317 v1.0.0 76 | - Add nowrap option 77 | - Refactored wrap into a function 78 | - Improved error messages 79 | 80 | 20200311 v0.5.4 81 | - Fix to display False values 82 | 83 | 20200311 v0.5.3 84 | - Improve error messages 85 | 86 | 20200310 v0.5.2 87 | - Scale value tweak 88 | - Error message update 89 | 90 | 20200309 v0.5.1 91 | - Wrap or truncate row headers with no values 92 | 93 | 20200309 v0.5.0 94 | - Improved auto scaling 95 | - Improved error handling 96 | - Fixed a bug where some data was being corrupted during autoscale wrapping 97 | 98 | 20200308 v0.1.7 99 | - Catch exception when the user passes an Array with no objects 100 | 101 | 20200308 v0.1.6 102 | - Fix dict error when iterating and changing size 103 | 104 | 20200308 v0.1.5 105 | - Add version info 106 | - Add help 107 | - Add truncate option 108 | - More scaler improvements 109 | - Gracefully handle CTRL-C interrupt 110 | 111 | 20200307 v0.1.4 112 | - Slight improvement to auto scaler 113 | 114 | 20200306 v0.1.3 115 | - Added rudimentary auto scaler (can be improved) 116 | 117 | 20200306 v0.1.2 118 | - Nicer error messages on parse errors 119 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kelly Brazil 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include man/jtbl.1 2 | include CHANGELOG -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Tests](https://github.com/kellyjonbrazil/jtbl/workflows/Tests/badge.svg?branch=master)](https://github.com/kellyjonbrazil/jtbl/actions) 2 | [![Pypi](https://img.shields.io/pypi/v/jtbl.svg)](https://pypi.org/project/jtbl/) 3 | 4 | # jtbl 5 | A simple cli tool to print JSON data as a table in the terminal. 6 | 7 | `jtbl` accepts piped JSON data from `stdin` and outputs a text table representation to `stdout`. e.g: 8 | ``` 9 | $ cat cities.json | jtbl 10 | LatD LatM LatS NS LonD LonM LonS EW City State 11 | ------ ------ ------ ---- ------ ------ ------ ---- ----------------- ------- 12 | 41 5 59 N 80 39 0 W Youngstown OH 13 | 42 52 48 N 97 23 23 W Yankton SD 14 | 46 35 59 N 120 30 36 W Yakima WA 15 | 42 16 12 N 71 48 0 W Worcester MA 16 | 43 37 48 N 89 46 11 W Wisconsin Dells WI 17 | 36 5 59 N 80 15 0 W Winston-Salem NC 18 | 49 52 48 N 97 9 0 W Winnipeg MB 19 | ``` 20 | 21 | `jtbl` expects a JSON array of JSON objects or [JSON Lines](http://jsonlines.org/). 22 | 23 | It can be useful to JSONify command line output with `jc`, filter through a tool like `jq`, and present in `jtbl`: 24 | ``` 25 | $ jc ifconfig | jq -c '.[] | {name, type, ipv4_addr, ipv4_mask}'| jtbl 26 | name type ipv4_addr ipv4_mask 27 | ------- -------------- -------------- ------------- 28 | docker0 Ethernet 172.17.0.1 255.255.0.0 29 | ens33 Ethernet 192.168.71.146 255.255.255.0 30 | lo Local Loopback 127.0.0.1 255.0.0.0 31 | ``` 32 | 33 | ## Installation 34 | You can install `jtbl` via `pip`, via OS Package Repositories, MSI installer for Windows, or by downloading the correct binary for your architecture and running it anywhere on your filesystem. 35 | 36 | ### Pip (macOS, linux, unix, Windows) 37 | For the most up-to-date version and the most cross-platform option, use `pip` or `pip3` to download and install `jtbl` directly from [PyPi](https://pypi.org/project/jtbl/): 38 | 39 | [![Pypi](https://img.shields.io/pypi/v/jtbl.svg)](https://pypi.org/project/jtbl/) 40 | ```bash 41 | pip3 install jtbl 42 | ``` 43 | 44 | ### OS Packages 45 | 46 | [![Packaging status](https://repology.org/badge/vertical-allrepos/jtbl.svg)](https://repology.org/project/jtbl/versions) 47 | 48 | See [Releases](https://github.com/kellyjonbrazil/jtbl/releases) on Github for MSI packages and binaries. 49 | 50 | ## Usage 51 | Just pipe JSON data to `jtbl`. (e.g. `cat` a JSON file, `jc`, `jq`, `aws` cli, `kubectl`, etc.) 52 | ``` 53 | $ | jtbl [OPTIONS] 54 | ``` 55 | ### Options 56 | - `--cols=n` manually configure the terminal width 57 | - `-c`, `--csv` CSV table output 58 | - `-d`, `--dokuwiki` Dokuwiki table output 59 | - `-f`, `--fancy` fancy table output 60 | - `-h`, `--help` prints help information 61 | - `-H`, `--html` HTML table output 62 | - `-m`, `--markdown` markdown table output 63 | - `-n`, `--no-wrap` no data wrapping if too long for the terminal width (overrides `--cols` and `-t`) 64 | - `-q`, `--quiet` don't print error messages to STDERR 65 | - `-r`, `--rotate` rotate the data (each row turns into a table of key/value pairs) 66 | - `-t`, `--truncate` truncate data instead of wrapping if too long for the terminal width 67 | - `-v`, `--version` prints version information 68 | 69 | ## Compatible JSON Formats 70 | `jtbl` works best with a shallow array of JSON objects. Each object should have a few elements that will be turned into table columns. Fortunately, this is how many APIs present their data. 71 | 72 | **JSON Array Example** 73 | ``` 74 | [ 75 | { 76 | "unit": "proc-sys-fs-binfmt_misc.automount", 77 | "load": "loaded", 78 | "active": "active", 79 | "sub": "waiting", 80 | "description": "Arbitrary Executable File Formats File System Automount Point" 81 | }, 82 | { 83 | "unit": "sys-devices-pci0000:00-0000:00:07.1-ata2-host2-target2:0:0-2:0:0:0-block-sr0.device", 84 | "load": "loaded", 85 | "active": "active", 86 | "sub": "plugged", 87 | "description": "VMware_Virtual_IDE_CDROM_Drive" 88 | }, 89 | ... 90 | ] 91 | ``` 92 | 93 | `jtbl` can also work with [JSON Lines](http://jsonlines.org/) format with similar features. 94 | 95 | **JSON Lines Example** 96 | ``` 97 | {"name": "docker0", type": "Ethernet", "ipv4_addr": "172.17.0.1", "ipv4_mask": "255.255.0.0"} 98 | {"name": "ens33", "type": "Ethernet", "ipv4_addr": "192.168.71.146", "ipv4_mask": "255.255.255.0"} 99 | {"name": "lo", "type": "Local Loopback", "ipv4_addr": "127.0.0.1", "ipv4_mask": "255.0.0.0"} 100 | ... 101 | ``` 102 | 103 | ## Filtering the JSON Input 104 | If there are too many elements, or the data in the elements are too large, the table may not fit in the terminal screen. In this case you can use a JSON filter like `jq` or `jello` to send `jtbl` only the elements you are interested in: 105 | 106 | ### `jq` Array Method 107 | The following example uses `jq` to filter and format the filtered elements into a proper JSON array. 108 | ``` 109 | $ cat /etc/passwd | jc --passwd | jq '[.[] | {username, shell}]' 110 | [ 111 | { 112 | "username": "root", 113 | "shell": "/bin/bash" 114 | }, 115 | { 116 | "username": "bin", 117 | "shell": "/sbin/nologin" 118 | }, 119 | { 120 | "username": "daemon", 121 | "shell": "/sbin/nologin" 122 | }, 123 | ... 124 | ] 125 | ``` 126 | *(Notice the square brackets around the filter)* 127 | 128 | ### `jq` Slurp Method 129 | The following example uses `jq` to filter and 'slurp' the filtered elements into a proper JSON array. 130 | ``` 131 | $ cat /etc/passwd | jc --passwd | jq '.[] | {username, shell}' | jq -s 132 | [ 133 | { 134 | "username": "root", 135 | "shell": "/bin/bash" 136 | }, 137 | { 138 | "username": "bin", 139 | "shell": "/sbin/nologin" 140 | }, 141 | { 142 | "username": "daemon", 143 | "shell": "/sbin/nologin" 144 | }, 145 | ... 146 | ] 147 | ``` 148 | *(Notice the `jq -s` at the end)* 149 | 150 | ### `jq` JSON Lines Method 151 | The following example will send the data in JSON Lines format, which `jtbl` can understand: 152 | ``` 153 | $ cat /etc/passwd | jc --passwd | jq -c '.[] | {username, shell}' 154 | {"username":"root","shell":"/bin/bash"} 155 | {"username":"bin","shell":"/sbin/nologin"} 156 | {"username":"daemon","shell":"/sbin/nologin"} 157 | ... 158 | ``` 159 | *(Notice the `-c` option being used)* 160 | 161 | ### `jello` List Comprehension Method 162 | If you prefer python list and dictionary syntax to filter JSON data, you can use `jello`: 163 | ``` 164 | $ cat /etc/passwd | jc --passwd | jello '[{"username": x.username, "shell": x.shell} for x in _]' 165 | [ 166 | { 167 | "username": "root", 168 | "shell": "/bin/bash" 169 | }, 170 | { 171 | "username": "bin", 172 | "shell": "/sbin/nologin" 173 | }, 174 | { 175 | "username": "daemon", 176 | "shell": "/sbin/nologin" 177 | }, 178 | ... 179 | ] 180 | ``` 181 | 182 | When piping any of these to `jtbl` you get the following result: 183 | ``` 184 | $ cat /etc/passwd | jc --passwd | jello '[{"username": x.username, "shell": x.shell} for x in _]' | jtbl 185 | username shell 186 | --------------- -------------- 187 | root /bin/bash 188 | bin /sbin/nologin 189 | daemon /sbin/nologin 190 | ... 191 | ``` 192 | 193 | ## Working with Deeper JSON Structures 194 | `jtbl` will happily dump deeply nested JSON structures into a table, but usually this is not what you are looking for. 195 | ``` 196 | $ jc dig www.cnn.com | jtbl 197 | ╒══════════╤══════════╤═══════╤════════════╤═════════════╤══════════════╤══════════════╤══════════════╤══════════════╤════════════╤════════════╤══════════════╤════════════╤════════════╤════════╤══════════════╤══════════════╕ 198 | │ opcode │ status │ id │ flags │ query_num │ answer_num │ authority_ │ additional │ opt_pseudo │ question │ answer │ query_time │ server │ when │ rcvd │ when_epoch │ when_epoch │ 199 | │ │ │ │ │ │ │ num │ _num │ section │ │ │ │ │ │ │ │ _utc │ 200 | ╞══════════╪══════════╪═══════╪════════════╪═════════════╪══════════════╪══════════════╪══════════════╪══════════════╪════════════╪════════════╪══════════════╪════════════╪════════════╪════════╪══════════════╪══════════════╡ 201 | │ QUERY │ NOERROR │ 36494 │ ['qr', 'rd │ 1 │ 4 │ 0 │ 1 │ {'edns': { │ {'name': ' │ [{'name': │ 47 │ 2600:1700: │ Wed Dec 22 │ 100 │ 1640200072 │ │ 202 | │ │ │ │ ', 'ra'] │ │ │ │ │ 'version': │ cnn.com.', │ 'cnn.com.' │ │ bab0:d40:: │ 11:07:52 │ │ │ │ 203 | │ │ │ │ │ │ │ │ │ 0, 'flags │ 'class': │ , 'class': │ │ 1#53(2600: │ PST 2021 │ │ │ │ 204 | │ │ │ │ │ │ │ │ │ ': [], 'ud │ 'IN', 'typ │ 'IN', 'ty │ │ 1700:bab0: │ │ │ │ │ 205 | │ │ │ │ │ │ │ │ │ p': 4096}} │ e': 'A'} │ pe': 'A', │ │ d40::1) │ │ │ │ │ 206 | │ │ │ │ │ │ │ │ │ │ │ 'ttl': 60, │ │ │ │ │ │ │ 207 | │ │ │ │ │ │ │ │ │ │ │ 'data': ' │ │ │ │ │ │ │ 208 | │ │ │ │ │ │ │ │ │ │ │ 151.101.12 │ │ │ │ │ │ │ 209 | │ │ │ │ │ │ │ │ │ │ │ 9.67'}, {' │ │ │ │ │ │ │ 210 | │ │ │ │ │ │ │ │ │ │ │ name': 'cn │ │ │ │ │ │ │ 211 | │ │ │ │ │ │ │ │ │ │ │ n.com.', ' │ │ │ │ │ │ │ 212 | │ │ │ │ │ │ │ │ │ │ │ class': 'I │ │ │ │ │ │ │ 213 | │ │ │ │ │ │ │ │ │ │ │ ... │ │ │ │ │ │ │ 214 | ╘══════════╧══════════╧═══════╧════════════╧═════════════╧══════════════╧══════════════╧══════════════╧══════════════╧════════════╧════════════╧══════════════╧════════════╧════════════╧════════╧══════════════╧══════════════╛ 215 | ``` 216 | 217 | ## Diving Deeper into the JSON with `jq` or `jello`: 218 | To get to the data you are interested in you can use a JSON filter like `jq` or `jello` to dive deeper. 219 | 220 | Using `jq`: 221 | ``` 222 | $ jc dig www.cnn.com | jq '.[0].answer' 223 | ``` 224 | or with `jello`: 225 | ``` 226 | $ jc dig www.cnn.com | jello '_[0].answer' 227 | ``` 228 | Both will produce the following output: 229 | ``` 230 | [ 231 | { 232 | "name": "www.cnn.com.", 233 | "class": "IN", 234 | "type": "CNAME", 235 | "ttl": 90, 236 | "data": "turner-tls.map.fastly.net." 237 | }, 238 | { 239 | "name": "turner-tls.map.fastly.net.", 240 | "class": "IN", 241 | "type": "A", 242 | "ttl": 20, 243 | "data": "151.101.1.67" 244 | } 245 | ... 246 | ] 247 | ``` 248 | 249 | This will produce the following table in `jtbl` 250 | ``` 251 | $ jc dig www.cnn.com | jello '_[0].answer' | jtbl 252 | name class type ttl data 253 | -------------------------- ------- ------ ----- -------------------------- 254 | www.cnn.com. IN CNAME 11 turner-tls.map.fastly.net. 255 | turner-tls.map.fastly.net. IN A 23 151.101.129.67 256 | turner-tls.map.fastly.net. IN A 23 151.101.1.67 257 | turner-tls.map.fastly.net. IN A 23 151.101.65.67 258 | turner-tls.map.fastly.net. IN A 23 151.101.193.67 259 | 260 | ``` 261 | 262 | ## Column Width 263 | `jtbl` will attempt to shrink columns to a sane size if it detects the output is wider than the terminal width. The `--cols` option will override the automatic terminal width detection. 264 | 265 | You can use the `-t` option to truncate the rows instead of wrapping when the terminal width is too small for all of the data. 266 | 267 | The `-n` option disables wrapping and overrides the `--cols` and `-t` options. 268 | 269 | This can be useful to present a nicely non-wrapped table of infinite width in combination with `less -S`: 270 | ``` 271 | $ jc ps aux | jtbl -n | less -S 272 | user pid vsz rss tt stat started time command 273 | ------------------ ----- --------- ------ ---- ------ --------- --------- --------------------------------------------------- 274 | joeuser 34029 4277364 24800 s000 S+ 9:28AM 0:00.27 /usr/local/Cellar/python/3.7.6_1/Frameworks/Python.... 275 | joeuser 34030 4283136 17104 s000 S+ 9:28AM 0:00.20 /usr/local/Cellar/python/3.7.6_1/Frameworks/Python.... 276 | joeuser 481 5728568 189328 S 17Apr20 21:46.52 /Applications/Utilities/Terminal.app/Contents/MacOS... 277 | joeuser 45827 6089084 693768 S Wed01PM 84:54.87 /Applications/Microsoft Teams.app/Contents/Framewor... 278 | joeuser 1493 9338824 911600 S 17Apr20 143:27.08 /Applications/Microsoft Outlook.app/Contents/MacOS/... 279 | joeuser 45822 5851524 163840 S Wed01PM 38:48.83 /Applications/Microsoft Teams.app/Contents/MacOS/Te... 280 | ``` -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /build-package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # build jc PIP package 3 | # to install locally, run: pip3 install jc-x.x.tar.gz 4 | 5 | python3 setup.py sdist bdist_wheel 6 | -------------------------------------------------------------------------------- /completions/jtbl_bash_completion.sh: -------------------------------------------------------------------------------- 1 | _jtbl() 2 | { 3 | OPTIONS=(--cols -c --csv -d --dokuwiki -f --fancy -h --help -H --html -m --markdown -n --no-wrap -q --quiet -r --rotate -t --truncate -v --version) 4 | MOD_OPTIONS=(--cols -n --no-wrap -q --quiet -t --truncate) 5 | 6 | COMPREPLY=() 7 | _get_comp_words_by_ref cur prev words cword 8 | 9 | if [ "${#words[@]}" != "2" ]; then 10 | COMPREPLY=($(compgen -W "${MOD_OPTIONS[*]}" -- "${cur}")) 11 | return 0 12 | else 13 | COMPREPLY=($(compgen -W "${OPTIONS[*]}" -- "${cur}")) 14 | return 0 15 | fi 16 | } && 17 | complete -F _jtbl jtbl -------------------------------------------------------------------------------- /completions/jtbl_zsh_completion.sh: -------------------------------------------------------------------------------- 1 | #compdef jtbl 2 | 3 | _jtbl() { 4 | jtbl_options_describe=( 5 | '--cols:manually configure the terminal width' 6 | '-c:CSV table output' 7 | '--csv:CSV table output' 8 | '-d:DokuWiki table output' 9 | '--dokuwiki:DokuWiki table output' 10 | '-f:fancy table output' 11 | '--fancy:fancy table output' 12 | '-h:help' 13 | '--help:help' 14 | '-H:HTML table output' 15 | '--html:HTML table output' 16 | '-m:markdown table output' 17 | '--markdown:markdown table output' 18 | '-n:do not try to wrap if too wide for the terminal' 19 | '--no-wrap:do not try to wrap if too wide for the terminal' 20 | "-q:quiet - don't print error messages" 21 | "--quiet:quiet - don't print error messages" 22 | '-r:rotate table output' 23 | '--rotate:rotate table output' 24 | '-t:truncate data if too wide for the terminal' 25 | '--truncate:truncate data if too wide for the terminal' 26 | '-v:version info' 27 | '--version:version info' 28 | ) 29 | 30 | _describe 'commands' jtbl_options_describe 31 | return 0 32 | } 33 | 34 | _jtbl -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | pip3 install --upgrade --user -e . -------------------------------------------------------------------------------- /jtbl/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kellyjonbrazil/jtbl/0018aaddf5a76cc03a761ef01b065ff2183f9d17/jtbl/__init__.py -------------------------------------------------------------------------------- /jtbl/__main__.py: -------------------------------------------------------------------------------- 1 | import jtbl.cli 2 | 3 | jtbl.cli.main() 4 | -------------------------------------------------------------------------------- /jtbl/cli.py: -------------------------------------------------------------------------------- 1 | import io 2 | import sys 3 | import signal 4 | import textwrap 5 | import csv 6 | import json 7 | import tabulate 8 | import shutil 9 | 10 | __version__ = '1.6.0' 11 | SUCCESS, ERROR = True, False 12 | 13 | # START add DokuWiki table format 14 | dokuwiki_format = { 15 | "dokuwiki": tabulate.TableFormat( 16 | lineabove=tabulate.Line("|", "-", "|", "|"), 17 | linebelowheader=tabulate.Line("|", "-", "|", "|"), 18 | linebetweenrows=None, 19 | linebelow=None, 20 | headerrow=tabulate.DataRow("^", "^", "^"), 21 | datarow=tabulate.DataRow("|", "|", "|"), 22 | padding=1, 23 | with_header_hide=["lineabove", "linebelowheader"], 24 | ) 25 | } 26 | tabulate._table_formats.update(dokuwiki_format) # type: ignore 27 | # END add DokuWiki table format 28 | 29 | def ctrlc(signum, frame): 30 | """exit with error on SIGINT""" 31 | sys.exit(1) 32 | 33 | 34 | def get_stdin(): 35 | """return STDIN data""" 36 | if sys.stdin.isatty(): 37 | return None 38 | else: 39 | return sys.stdin.read() 40 | 41 | 42 | def helptext(): 43 | print_error(textwrap.dedent('''\ 44 | jtbl: Converts JSON and JSON Lines to a table 45 | 46 | Usage: | jtbl [OPTIONS] 47 | 48 | --cols=n manually configure the terminal width 49 | -c, --csv CSV table output 50 | -d, --dokuwiki DokuWiki table output 51 | -f, --fancy fancy table output 52 | -h, --help help 53 | -H, --html HTML table output 54 | -m, --markdown markdown table output 55 | -n, --no-wrap do not try to wrap if too wide for the terminal 56 | -q, --quiet quiet - don't print error messages 57 | -r, --rotate rotate table output 58 | -t, --truncate truncate data if too wide for the terminal 59 | -v, --version version info 60 | ''')) 61 | 62 | 63 | def print_error(message, quiet=False): 64 | """print error messages to STDERR and quit with error code""" 65 | if not quiet: 66 | print(message, file=sys.stderr) 67 | sys.exit(1) 68 | 69 | 70 | def wrap(data, columns, table_format, truncate): 71 | """ 72 | Wrap or truncate the data to fit the terminal width. 73 | 74 | Returns a tuple of (data, table_format) 75 | data (list) a modified list of dictionies with wrapped or truncated string values. 76 | wrapping is achieved by inserting \n characters into the value strings. 77 | 78 | table_format (string) 'simple' (for truncation) or 'fancy_grid' (for wrapping) 79 | """ 80 | # find the length of the keys (headers) and longest values 81 | data_width = {} 82 | for entry in data: 83 | for k, v in entry.items(): 84 | if k in data_width: 85 | if len(str(v)) > data_width[k]: 86 | data_width[k] = len(str(v)) 87 | else: 88 | data_width[k] = len(str(v)) 89 | 90 | # highest_value calculations are only approximate since there can be left and right justification 91 | num_of_headers = len(data_width.keys()) 92 | combined_total_list = [] 93 | for k, v in data_width.items(): 94 | highest_value = max(len(k) + 4, v + 2) 95 | combined_total_list.append(highest_value) 96 | 97 | total_width = sum(combined_total_list) 98 | 99 | new_table = [] 100 | if total_width > columns: 101 | # Find the best wrap_width based on the terminal size 102 | sorted_list = sorted(combined_total_list, reverse=True) 103 | wrap_width = sorted_list[0] 104 | scale = 2.5 if truncate else 4.5 105 | 106 | while wrap_width > 4 and total_width >= (columns - (num_of_headers * scale)): 107 | sorted_list = sorted(sorted_list, reverse=True) 108 | sorted_list[0] -= 1 109 | total_width = sum(sorted_list) 110 | wrap_width = sorted_list[0] 111 | 112 | # truncate or wrap every wrap_width chars for all field values 113 | for entry in data: 114 | add_keys = {} 115 | for k, v in entry.items(): 116 | if v is None: 117 | v = '' 118 | 119 | if truncate: 120 | new_key = str(k)[0:wrap_width] 121 | new_value = str(v)[0:wrap_width] 122 | add_keys[new_key] = new_value 123 | 124 | else: 125 | table_format = 'fancy_grid' 126 | new_key = '\n'.join([str(k)[i:i + wrap_width] for i in range(0, len(str(k)), wrap_width)]) 127 | new_value = '\n'.join([str(v)[i:i + wrap_width] for i in range(0, len(str(v)), wrap_width)]) 128 | add_keys[new_key] = new_value 129 | 130 | new_table.append(add_keys) 131 | 132 | return (new_table or data, table_format) 133 | 134 | 135 | def get_json(json_data, columns=None): 136 | """Accepts JSON or JSON Lines and returns a tuple of 137 | (success/error, list of dictionaries) 138 | """ 139 | if not json_data or json_data.isspace(): 140 | return (ERROR, 'jtbl: Missing piped data\n') 141 | 142 | try: 143 | data = json.loads(json_data) 144 | if type(data) is not list: 145 | data_list = [] 146 | data_list.append(data) 147 | data = data_list 148 | 149 | return SUCCESS, data 150 | 151 | except Exception: 152 | # if json.loads fails, assume the data is formatted as json lines and parse 153 | data = json_data.splitlines() 154 | data_list = [] 155 | for i, jsonline in enumerate(data): 156 | try: 157 | if jsonline.strip(): 158 | entry = json.loads(jsonline) 159 | data_list.append(entry) 160 | except Exception as e: 161 | # can't parse the data. Throw a nice message and quit 162 | return (ERROR, textwrap.dedent(f'''\ 163 | jtbl: Exception - {e} 164 | Cannot parse line {i + 1} (Not JSON or JSON Lines data): 165 | {str(jsonline)[0:columns - 8]} 166 | ''')) 167 | return SUCCESS, data_list 168 | 169 | 170 | def check_data(data=None, columns=0): 171 | """Return (SUCCESS, data) if data can be processed. (ERROR, msg) if not""" 172 | # only process if there is data 173 | if data: 174 | try: 175 | if not isinstance(data[0], dict): 176 | data = json.dumps(data) 177 | return (ERROR, textwrap.dedent(f'''\ 178 | jtbl: Cannot represent this part of the JSON Object as a table. 179 | (Could be an Element, an Array, or Null data instead of an Object): 180 | {str(data)[0:columns - 8]} 181 | ''')) 182 | 183 | except Exception: 184 | # can't parse the data. Throw a nice message and quit 185 | return (ERROR, textwrap.dedent(f'''\ 186 | jtbl: Cannot parse the data (Not JSON or JSON Lines data): 187 | {str(data)[0:columns - 8]} 188 | ''')) 189 | 190 | return SUCCESS, data 191 | 192 | if data == []: 193 | return SUCCESS, '' 194 | 195 | return ERROR, data 196 | 197 | 198 | def get_headers(data): 199 | """scan the data and return a dictionary of all of the headers in order""" 200 | headers = [] 201 | 202 | if isinstance(data, dict): 203 | headers.append(data.keys()) 204 | 205 | elif isinstance(data, list): 206 | for row in data: 207 | if isinstance(row, dict): 208 | headers.extend(row.keys()) 209 | 210 | # preserve field order by using dict.fromkeys() 211 | header_dict = dict.fromkeys(headers) 212 | 213 | return header_dict 214 | 215 | 216 | def make_rotate_table( 217 | data=None, 218 | truncate=False, 219 | nowrap=False, 220 | columns=None, 221 | table_format='simple', 222 | rotate=False 223 | ): 224 | """generates a rotated table""" 225 | table = '' 226 | for idx, row in enumerate(data): 227 | rotated_data = [] 228 | for k, v in row.items(): 229 | rotated_data.append({'key': k, 'value': v}) 230 | 231 | succeeded, result = make_table( 232 | data=rotated_data, 233 | truncate=truncate, 234 | nowrap=nowrap, 235 | columns=columns, 236 | table_format=table_format, 237 | rotate=rotate 238 | ) 239 | 240 | if succeeded: 241 | if len(data) > 1: 242 | table += f'item: {idx}\n' 243 | table += '─' * columns + '\n' 244 | table += result + '\n\n' 245 | 246 | return (SUCCESS, table[:-1]) 247 | 248 | 249 | def make_csv_table(data=None): 250 | """generate csv table""" 251 | buffer = io.StringIO() 252 | headers = get_headers(data) 253 | 254 | writer = csv.DictWriter( 255 | buffer, 256 | headers, 257 | restval='', 258 | extrasaction='raise', 259 | dialect='excel' 260 | ) 261 | 262 | writer.writeheader() 263 | 264 | if isinstance(data, dict): 265 | data = [data] 266 | 267 | if isinstance(data, list): 268 | for row in data: 269 | writer.writerow(row) 270 | 271 | return (SUCCESS, buffer.getvalue()) 272 | 273 | 274 | def make_table( 275 | data=None, 276 | truncate=False, 277 | nowrap=False, 278 | columns=None, 279 | table_format='simple', 280 | rotate=False 281 | ): 282 | """Generate simple or fancy table""" 283 | if not nowrap: 284 | data, table_format = wrap( 285 | data=data, 286 | columns=columns, 287 | table_format=table_format, 288 | truncate=truncate 289 | ) 290 | 291 | headers = 'keys' 292 | if rotate: 293 | table_format = 'plain' 294 | headers = '' 295 | 296 | return (SUCCESS, tabulate.tabulate(data, headers=headers, tablefmt=table_format, floatfmt='')) 297 | 298 | 299 | def main(): 300 | # break on ctrl-c keyboard interrupt 301 | signal.signal(signal.SIGINT, ctrlc) 302 | 303 | # break on pipe error. need try/except for windows compatibility 304 | try: 305 | signal.signal(signal.SIGPIPE, signal.SIG_DFL) 306 | except AttributeError: 307 | pass 308 | 309 | stdin = get_stdin() 310 | 311 | options = [] 312 | long_options = {} 313 | for arg in sys.argv: 314 | if arg.startswith('-') and not arg.startswith('--'): 315 | options.extend(arg[1:]) 316 | 317 | if arg.startswith('--'): 318 | if '=' in arg: 319 | try: 320 | k, v = arg[2:].split('=') 321 | long_options[k] = int(v) 322 | except Exception: 323 | helptext() 324 | else: 325 | long_options[arg[2:]] = None 326 | 327 | csv = 'c' in options or 'csv' in long_options 328 | dokuwiki = 'd' in options or 'dokuwiki' in long_options 329 | html = 'H' in options or 'html' in long_options 330 | markdown = 'm' in options or 'markdown' in long_options 331 | fancy_grid = 'f' in options or 'fancy' in long_options 332 | nowrap = 'n' in options or 'no-wrap' in long_options 333 | quiet = 'q' in options or 'quiet' in long_options 334 | rotate = 'r' in options or 'rotate' in long_options 335 | truncate = 't' in options or 'truncate' in long_options 336 | version_info = 'v' in options or 'version' in long_options 337 | helpme = 'h' in options or 'help' in long_options 338 | 339 | if markdown: 340 | tbl_fmt = 'github' 341 | elif dokuwiki: 342 | tbl_fmt = 'dokuwiki' 343 | elif html: 344 | tbl_fmt = 'html' 345 | elif fancy_grid: 346 | tbl_fmt = 'fancy_grid' 347 | else: 348 | tbl_fmt = 'simple' 349 | 350 | if not rotate and (markdown or dokuwiki or html or csv): 351 | nowrap = True 352 | 353 | columns = None 354 | if 'cols' in long_options: 355 | columns = long_options['cols'] 356 | 357 | if columns is None: 358 | columns = shutil.get_terminal_size().columns 359 | 360 | if version_info: 361 | print_error(f'jtbl: version {__version__}\n') 362 | 363 | if helpme: 364 | helptext() 365 | 366 | succeeded, json_data = get_json(stdin, columns=columns) 367 | if not succeeded: 368 | print_error(json_data, quiet=quiet) 369 | 370 | succeeded, json_data = check_data(json_data, columns=columns) 371 | if not succeeded: 372 | print_error(json_data, quiet=quiet) 373 | 374 | # Make and print the tables 375 | if rotate: 376 | succeeded, result = make_rotate_table( 377 | data=json_data, 378 | truncate=truncate, 379 | nowrap=nowrap, 380 | columns=columns, 381 | rotate=True 382 | ) 383 | 384 | if succeeded: 385 | print(result) 386 | else: 387 | print_error(result, quiet=quiet) 388 | 389 | elif csv: 390 | succeeded, result = make_csv_table(data=json_data) 391 | 392 | if succeeded: 393 | print(result) 394 | else: 395 | print_error(result, quiet=quiet) 396 | 397 | else: 398 | succeeded, result = make_table( 399 | data=json_data, 400 | truncate=truncate, 401 | nowrap=nowrap, 402 | columns=columns, 403 | table_format=tbl_fmt 404 | ) 405 | 406 | if succeeded: 407 | print(result) 408 | else: 409 | print_error(result, quiet=quiet) 410 | 411 | 412 | if __name__ == '__main__': 413 | main() 414 | -------------------------------------------------------------------------------- /man/jtbl.1: -------------------------------------------------------------------------------- 1 | .TH jtbl 1 2023-11-24 1.6.0 "JTBL - JSON tables in the terminal" 2 | .SH NAME 3 | jtbl \- Print JSON and JSON Lines data as a table in the terminal 4 | .SH SYNOPSIS 5 | .PP 6 | 7 | A simple cli tool to print JSON data as a table in the terminal. 8 | 9 | jtbl accepts piped JSON or JSON Lines data from stdin and outputs a text table 10 | representation to stdout. 11 | .PP 12 | 13 | .SH USAGE 14 | 15 | cat data.json | jtbl [OPTIONS] 16 | 17 | .fi 18 | .PP 19 | 20 | .SS Options 21 | \fB--cols=n\fP manually configure the terminal width 22 | 23 | \fB-c\fP, \fB--csv\fP CSV table output 24 | 25 | \fB-d\fP, \fB--dokuwiki\fP DokuWiki table output 26 | 27 | \fB-f\fP, \fB--fancy\fP fancy table output 28 | 29 | \fB-h\fP, \fB--help\fP help 30 | 31 | \fB-H\fP, \fB--html\fP HTML table output 32 | 33 | \fB-m\fP, \fB--markdown\fP markdown table output 34 | 35 | \fB-n\fP, \fB--no-wrap\fP do not try to wrap if too wide for the terminal 36 | 37 | \fB-q\fP, \fB--quiet\fP quiet - don't print error messages 38 | 39 | \fB-r\fP, \fB--rotate\fP rotate table output 40 | 41 | \fB-t\fP, \fB--truncate\fP truncate data if too wide for the terminal 42 | 43 | \fB-v\fP, \fB--version\fP version info 44 | 45 | .SS Example 46 | .na 47 | .nf 48 | $ echo '[ 49 | > { 50 | > "column1": "foo", 51 | > "column2": 123, 52 | > "column3": true, 53 | > "column4": null 54 | > }, 55 | > { 56 | > "column1": "bar", 57 | > "column2": 456, 58 | > "column3": false, 59 | > "column4": null 60 | > } 61 | > ]' | jtbl 62 | column1 column2 column3 column4 63 | --------- --------- --------- --------- 64 | foo 123 True 65 | bar 456 False 66 | 67 | .SH AUTHOR 68 | Kelly Brazil (kellyjonbrazil@gmail.com) 69 | 70 | https://github.com/kellyjonbrazil/jtbl 71 | 72 | .SH COPYRIGHT 73 | Copyright (c) 2020-2023 Kelly Brazil 74 | 75 | License: MIT License 76 | -------------------------------------------------------------------------------- /pypi-upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | twine upload dist/* -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tabulate>=0.8.10 2 | -------------------------------------------------------------------------------- /runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python3 -m unittest -v 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_file = LICENSE -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open('README.md', 'r') as f: 4 | long_description = f.read() 5 | 6 | setuptools.setup( 7 | name='jtbl', 8 | version='1.6.0', 9 | author='Kelly Brazil', 10 | author_email='kellyjonbrazil@gmail.com', 11 | description='A simple cli tool to print JSON and JSON Lines data as a table in the terminal.', 12 | install_requires=[ 13 | 'tabulate>=0.8.6' 14 | ], 15 | license='MIT', 16 | long_description=long_description, 17 | long_description_content_type='text/markdown', 18 | python_requires='>=3.6', 19 | url='https://github.com/kellyjonbrazil/jtbl', 20 | packages=setuptools.find_packages(exclude=['*.tests', '*.tests.*', 'tests.*', 'tests']), 21 | entry_points={ 22 | 'console_scripts': [ 23 | 'jtbl=jtbl.cli:main' 24 | ] 25 | }, 26 | classifiers=[ 27 | 'Programming Language :: Python :: 3', 28 | 'License :: OSI Approved :: MIT License', 29 | 'Operating System :: OS Independent', 30 | 'Topic :: Utilities' 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kellyjonbrazil/jtbl/0018aaddf5a76cc03a761ef01b065ff2183f9d17/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_get_json.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import textwrap 3 | import jtbl.cli 4 | 5 | 6 | class MyTests(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.SUCCESS, self.ERROR = True, False 10 | self.columns = 80 11 | 12 | def test_no_piped_data(self): 13 | stdin = None 14 | expected = textwrap.dedent('''\ 15 | jtbl: Missing piped data 16 | ''') 17 | 18 | self.assertEqual(jtbl.cli.get_json(stdin, columns=self.columns), (self.ERROR, expected)) 19 | 20 | def test_null_data(self): 21 | stdin = '' 22 | expected = textwrap.dedent('''\ 23 | jtbl: Missing piped data 24 | ''') 25 | 26 | self.assertEqual(jtbl.cli.get_json(stdin, columns=self.columns), (self.ERROR, expected)) 27 | 28 | def test_hello_string(self): 29 | stdin = 'hello' 30 | expected = textwrap.dedent('''\ 31 | jtbl: Exception - Expecting value: line 1 column 1 (char 0) 32 | Cannot parse line 1 (Not JSON or JSON Lines data): 33 | hello 34 | ''') 35 | 36 | self.assertEqual(jtbl.cli.get_json(stdin, columns=self.columns), (self.ERROR, expected)) 37 | 38 | def test_array_input(self): 39 | stdin = '["value1", "value2", "value3"]' 40 | expected = ["value1", "value2", "value3"] 41 | 42 | self.assertEqual(jtbl.cli.get_json(stdin, columns=self.columns), (self.SUCCESS, expected)) 43 | 44 | def test_deep_nest(self): 45 | stdin = '{"this":{"is":{"a":{"deeply":{"nested":{"structure":"value1","item2":"value2"}}}}}}' 46 | expected = [{"this":{"is":{"a":{"deeply":{"nested":{"structure":"value1","item2":"value2"}}}}}}] 47 | 48 | self.assertEqual(jtbl.cli.get_json(stdin, columns=self.columns), (self.SUCCESS, expected)) 49 | 50 | def test_jc_dig(self): 51 | stdin = '[{"id": 55658, "opcode": "QUERY", "status": "NOERROR", "flags": ["qr", "rd", "ra"], "query_num": 1, "answer_num": 5, "authority_num": 0, "additional_num": 1, "question": {"name": "www.cnn.com.", "class": "IN", "type": "A"}, "answer": [{"name": "www.cnn.com.", "class": "IN", "type": "CNAME", "ttl": 147, "data": "turner-tls.map.fastly.net."}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.1.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.65.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.129.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.193.67"}], "query_time": 44, "server": "2600", "when": "Wed Mar 18 12:20:59 PDT 2020", "rcvd": 143}]' 52 | expected = [{"id": 55658, "opcode": "QUERY", "status": "NOERROR", "flags": ["qr", "rd", "ra"], "query_num": 1, "answer_num": 5, "authority_num": 0, "additional_num": 1, "question": {"name": "www.cnn.com.", "class": "IN", "type": "A"}, "answer": [{"name": "www.cnn.com.", "class": "IN", "type": "CNAME", "ttl": 147, "data": "turner-tls.map.fastly.net."}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.1.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.65.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.129.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.193.67"}], "query_time": 44, "server": "2600", "when": "Wed Mar 18 12:20:59 PDT 2020", "rcvd": 143}] 53 | self.assertEqual(jtbl.cli.get_json(stdin, columns=self.columns), (self.SUCCESS, expected)) 54 | 55 | def test_json_lines(self): 56 | """test JSON Lines data""" 57 | stdin = textwrap.dedent('''\ 58 | {"name":"lo0","type":null,"ipv4_addr":"127.0.0.1","ipv4_mask":"255.0.0.0"} 59 | {"name":"gif0","type":null,"ipv4_addr":null,"ipv4_mask":null} 60 | {"name":"stf0","type":null,"ipv4_addr":null,"ipv4_mask":null} 61 | {"name":"XHC0","type":null,"ipv4_addr":null,"ipv4_mask":null} 62 | {"name":"XHC20","type":null,"ipv4_addr":null,"ipv4_mask":null} 63 | {"name":"VHC128","type":null,"ipv4_addr":null,"ipv4_mask":null} 64 | {"name":"XHC1","type":null,"ipv4_addr":null,"ipv4_mask":null} 65 | {"name":"en5","type":null,"ipv4_addr":null,"ipv4_mask":null} 66 | {"name":"ap1","type":null,"ipv4_addr":null,"ipv4_mask":null} 67 | {"name":"en0","type":null,"ipv4_addr":"192.168.1.221","ipv4_mask":"255.255.255.0"} 68 | {"name":"p2p0","type":null,"ipv4_addr":null,"ipv4_mask":null} 69 | {"name":"awdl0","type":null,"ipv4_addr":null,"ipv4_mask":null} 70 | {"name":"en1","type":null,"ipv4_addr":null,"ipv4_mask":null} 71 | {"name":"en2","type":null,"ipv4_addr":null,"ipv4_mask":null} 72 | {"name":"en3","type":null,"ipv4_addr":null,"ipv4_mask":null} 73 | {"name":"en4","type":null,"ipv4_addr":null,"ipv4_mask":null} 74 | {"name":"bridge0","type":null,"ipv4_addr":null,"ipv4_mask":null} 75 | {"name":"utun0","type":null,"ipv4_addr":null,"ipv4_mask":null} 76 | {"name":"utun1","type":null,"ipv4_addr":null,"ipv4_mask":null} 77 | {"name":"utun2","type":null,"ipv4_addr":null,"ipv4_mask":null} 78 | {"name":"utun3","type":null,"ipv4_addr":null,"ipv4_mask":null} 79 | {"name":"utun4","type":null,"ipv4_addr":null,"ipv4_mask":null} 80 | {"name":"vmnet1","type":null,"ipv4_addr":"192.168.101.1","ipv4_mask":"255.255.255.0"} 81 | {"name":"vmnet8","type":null,"ipv4_addr":"192.168.71.1","ipv4_mask":"255.255.255.0"}''') 82 | expected = [ 83 | {"name":"lo0","type":None,"ipv4_addr":"127.0.0.1","ipv4_mask":"255.0.0.0"}, 84 | {"name":"gif0","type":None,"ipv4_addr":None,"ipv4_mask":None}, 85 | {"name":"stf0","type":None,"ipv4_addr":None,"ipv4_mask":None}, 86 | {"name":"XHC0","type":None,"ipv4_addr":None,"ipv4_mask":None}, 87 | {"name":"XHC20","type":None,"ipv4_addr":None,"ipv4_mask":None}, 88 | {"name":"VHC128","type":None,"ipv4_addr":None,"ipv4_mask":None}, 89 | {"name":"XHC1","type":None,"ipv4_addr":None,"ipv4_mask":None}, 90 | {"name":"en5","type":None,"ipv4_addr":None,"ipv4_mask":None}, 91 | {"name":"ap1","type":None,"ipv4_addr":None,"ipv4_mask":None}, 92 | {"name":"en0","type":None,"ipv4_addr":"192.168.1.221","ipv4_mask":"255.255.255.0"}, 93 | {"name":"p2p0","type":None,"ipv4_addr":None,"ipv4_mask":None}, 94 | {"name":"awdl0","type":None,"ipv4_addr":None,"ipv4_mask":None}, 95 | {"name":"en1","type":None,"ipv4_addr":None,"ipv4_mask":None}, 96 | {"name":"en2","type":None,"ipv4_addr":None,"ipv4_mask":None}, 97 | {"name":"en3","type":None,"ipv4_addr":None,"ipv4_mask":None}, 98 | {"name":"en4","type":None,"ipv4_addr":None,"ipv4_mask":None}, 99 | {"name":"bridge0","type":None,"ipv4_addr":None,"ipv4_mask":None}, 100 | {"name":"utun0","type":None,"ipv4_addr":None,"ipv4_mask":None}, 101 | {"name":"utun1","type":None,"ipv4_addr":None,"ipv4_mask":None}, 102 | {"name":"utun2","type":None,"ipv4_addr":None,"ipv4_mask":None}, 103 | {"name":"utun3","type":None,"ipv4_addr":None,"ipv4_mask":None}, 104 | {"name":"utun4","type":None,"ipv4_addr":None,"ipv4_mask":None}, 105 | {"name":"vmnet1","type":None,"ipv4_addr":"192.168.101.1","ipv4_mask":"255.255.255.0"}, 106 | {"name":"vmnet8","type":None,"ipv4_addr":"192.168.71.1","ipv4_mask":"255.255.255.0"} 107 | ] 108 | 109 | self.assertEqual(jtbl.cli.get_json(stdin, columns=self.columns), (self.SUCCESS, expected)) 110 | 111 | 112 | if __name__ == '__main__': 113 | unittest.main() 114 | -------------------------------------------------------------------------------- /tests/test_make_table.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import textwrap 3 | import jtbl.cli 4 | 5 | 6 | class MyTests(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.SUCCESS, self.ERROR = True, False 10 | self.columns = 80 11 | 12 | def test_empty_dict(self): 13 | stdin = {} 14 | expected = '' 15 | self.assertEqual(jtbl.cli.make_table(data=stdin, columns=self.columns), (self.SUCCESS, expected)) 16 | 17 | def test_empty_list(self): 18 | stdin = [] 19 | expected = '' 20 | self.assertEqual(jtbl.cli.make_table(data=stdin, columns=self.columns), (self.SUCCESS, expected)) 21 | 22 | def test_simple_key_value(self): 23 | stdin = [{"key": "value"}] 24 | expected = textwrap.dedent('''\ 25 | key 26 | ----- 27 | value''') 28 | 29 | self.assertEqual(jtbl.cli.make_table(data=stdin, columns=self.columns), (self.SUCCESS, expected)) 30 | 31 | def test_multi_key_value(self): 32 | stdin = [{"key1": "value1", "key2": "value1"}, {"key1": "value2", "key2": "value2"}] 33 | expected = textwrap.dedent('''\ 34 | key1 key2 35 | ------ ------ 36 | value1 value1 37 | value2 value2''') 38 | 39 | self.assertEqual(jtbl.cli.make_table(data=stdin, columns=self.columns), (self.SUCCESS, expected)) 40 | 41 | def test_null_string(self): 42 | stdin = [None] 43 | self.assertRaises(AttributeError, jtbl.cli.make_table, data=stdin, columns=self.columns) 44 | 45 | def test_array_input(self): 46 | stdin = ["value1", "value2", "value3"] 47 | self.assertRaises(AttributeError, jtbl.cli.make_table, data=stdin, columns=self.columns) 48 | 49 | def test_deep_nest(self): 50 | stdin = [{"this":{"is":{"a":{"deeply":{"nested":{"structure":"value1","item2":"value2"}}}}}}] 51 | expected = textwrap.dedent('''\ 52 | this 53 | --------------------------------------------------------------------------------- 54 | {'is': {'a': {'deeply': {'nested': {'structure': 'value1', 'item2': 'value2'}}}}}''') 55 | 56 | self.assertEqual(jtbl.cli.make_table(data=stdin, columns=100), (self.SUCCESS, expected)) 57 | 58 | def test_jc_dig(self): 59 | stdin = [{"id": 55658, "opcode": "QUERY", "status": "NOERROR", "flags": ["qr", "rd", "ra"], "query_num": 1, "answer_num": 5, "authority_num": 0, "additional_num": 1, "question": {"name": "www.cnn.com.", "class": "IN", "type": "A"}, "answer": [{"name": "www.cnn.com.", "class": "IN", "type": "CNAME", "ttl": 147, "data": "turner-tls.map.fastly.net."}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.1.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.65.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.129.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.193.67"}], "query_time": 44, "server": "2600", "when": "Wed Mar 18 12:20:59 PDT 2020", "rcvd": 143}] 60 | expected = textwrap.dedent('''\ 61 | ╒══════╤════════╤════════╤════════╤════════╤════════╤════════╤════════╤════════╤════════╤════════╤════════╤════════╤════════╕ 62 | │ id │ opco │ stat │ flag │ quer │ answ │ auth │ addi │ ques │ answ │ quer │ serv │ when │ rcvd │ 63 | │ │ de │ us │ s │ y_nu │ er_n │ orit │ tion │ tion │ er │ y_ti │ er │ │ │ 64 | │ │ │ │ │ m │ um │ y_nu │ al_n │ │ │ me │ │ │ │ 65 | │ │ │ │ │ │ │ m │ um │ │ │ │ │ │ │ 66 | ╞══════╪════════╪════════╪════════╪════════╪════════╪════════╪════════╪════════╪════════╪════════╪════════╪════════╪════════╡ 67 | │ 5565 │ QUER │ NOER │ ['qr │ 1 │ 5 │ 0 │ 1 │ {'na │ [{'n │ 44 │ 2600 │ Wed │ 143 │ 68 | │ 8 │ Y │ ROR │ ', ' │ │ │ │ │ me': │ ame' │ │ │ Mar │ │ 69 | │ │ │ │ rd', │ │ │ │ │ 'ww │ : 'w │ │ │ 18 1 │ │ 70 | │ │ │ │ 'ra │ │ │ │ │ w.cn │ ww.c │ │ │ 2:20 │ │ 71 | │ │ │ │ '] │ │ │ │ │ n.co │ nn.c │ │ │ :59 │ │ 72 | │ │ │ │ │ │ │ │ │ m.', │ om.' │ │ │ PDT │ │ 73 | │ │ │ │ │ │ │ │ │ 'cl │ , 'c │ │ │ 2020 │ │ 74 | │ │ │ │ │ │ │ │ │ ass' │ lass │ │ │ │ │ 75 | │ │ │ │ │ │ │ │ │ : 'I │ ': ' │ │ │ │ │ 76 | │ │ │ │ │ │ │ │ │ N', │ IN', │ │ │ │ │ 77 | │ │ │ │ │ │ │ │ │ 'typ │ 'ty │ │ │ │ │ 78 | │ │ │ │ │ │ │ │ │ e': │ pe': │ │ │ │ │ 79 | │ │ │ │ │ │ │ │ │ 'A'} │ 'CN │ │ │ │ │ 80 | │ │ │ │ │ │ │ │ │ │ AME' │ │ │ │ │ 81 | │ │ │ │ │ │ │ │ │ │ , 't │ │ │ │ │ 82 | │ │ │ │ │ │ │ │ │ │ tl': │ │ │ │ │ 83 | │ │ │ │ │ │ │ │ │ │ 147 │ │ │ │ │ 84 | │ │ │ │ │ │ │ │ │ │ , 'd │ │ │ │ │ 85 | │ │ │ │ │ │ │ │ │ │ ata' │ │ │ │ │ 86 | │ │ │ │ │ │ │ │ │ │ : 't │ │ │ │ │ 87 | │ │ │ │ │ │ │ │ │ │ urne │ │ │ │ │ 88 | │ │ │ │ │ │ │ │ │ │ r-tl │ │ │ │ │ 89 | │ │ │ │ │ │ │ │ │ │ s.ma │ │ │ │ │ 90 | │ │ │ │ │ │ │ │ │ │ p.fa │ │ │ │ │ 91 | │ │ │ │ │ │ │ │ │ │ stly │ │ │ │ │ 92 | │ │ │ │ │ │ │ │ │ │ .net │ │ │ │ │ 93 | │ │ │ │ │ │ │ │ │ │ .'}, │ │ │ │ │ 94 | │ │ │ │ │ │ │ │ │ │ {'n │ │ │ │ │ 95 | │ │ │ │ │ │ │ │ │ │ ame' │ │ │ │ │ 96 | │ │ │ │ │ │ │ │ │ │ : 't │ │ │ │ │ 97 | │ │ │ │ │ │ │ │ │ │ urne │ │ │ │ │ 98 | │ │ │ │ │ │ │ │ │ │ r-tl │ │ │ │ │ 99 | │ │ │ │ │ │ │ │ │ │ s.ma │ │ │ │ │ 100 | │ │ │ │ │ │ │ │ │ │ p.fa │ │ │ │ │ 101 | │ │ │ │ │ │ │ │ │ │ stly │ │ │ │ │ 102 | │ │ │ │ │ │ │ │ │ │ .net │ │ │ │ │ 103 | │ │ │ │ │ │ │ │ │ │ .', │ │ │ │ │ 104 | │ │ │ │ │ │ │ │ │ │ 'cla │ │ │ │ │ 105 | │ │ │ │ │ │ │ │ │ │ ss': │ │ │ │ │ 106 | │ │ │ │ │ │ │ │ │ │ 'IN │ │ │ │ │ 107 | │ │ │ │ │ │ │ │ │ │ ', ' │ │ │ │ │ 108 | │ │ │ │ │ │ │ │ │ │ type │ │ │ │ │ 109 | │ │ │ │ │ │ │ │ │ │ ': ' │ │ │ │ │ 110 | │ │ │ │ │ │ │ │ │ │ A', │ │ │ │ │ 111 | │ │ │ │ │ │ │ │ │ │ 'ttl │ │ │ │ │ 112 | │ │ │ │ │ │ │ │ │ │ ': 5 │ │ │ │ │ 113 | │ │ │ │ │ │ │ │ │ │ , 'd │ │ │ │ │ 114 | │ │ │ │ │ │ │ │ │ │ ata' │ │ │ │ │ 115 | │ │ │ │ │ │ │ │ │ │ : '1 │ │ │ │ │ 116 | │ │ │ │ │ │ │ │ │ │ 51.1 │ │ │ │ │ 117 | │ │ │ │ │ │ │ │ │ │ 01.1 │ │ │ │ │ 118 | │ │ │ │ │ │ │ │ │ │ .67' │ │ │ │ │ 119 | │ │ │ │ │ │ │ │ │ │ }, { │ │ │ │ │ 120 | │ │ │ │ │ │ │ │ │ │ 'nam │ │ │ │ │ 121 | │ │ │ │ │ │ │ │ │ │ e': │ │ │ │ │ 122 | │ │ │ │ │ │ │ │ │ │ 'tur │ │ │ │ │ 123 | │ │ │ │ │ │ │ │ │ │ ner- │ │ │ │ │ 124 | │ │ │ │ │ │ │ │ │ │ tls. │ │ │ │ │ 125 | │ │ │ │ │ │ │ │ │ │ map. │ │ │ │ │ 126 | │ │ │ │ │ │ │ │ │ │ fast │ │ │ │ │ 127 | │ │ │ │ │ │ │ │ │ │ ly.n │ │ │ │ │ 128 | │ │ │ │ │ │ │ │ │ │ et.' │ │ │ │ │ 129 | │ │ │ │ │ │ │ │ │ │ , 'c │ │ │ │ │ 130 | │ │ │ │ │ │ │ │ │ │ lass │ │ │ │ │ 131 | │ │ │ │ │ │ │ │ │ │ ': ' │ │ │ │ │ 132 | │ │ │ │ │ │ │ │ │ │ IN', │ │ │ │ │ 133 | │ │ │ │ │ │ │ │ │ │ 'ty │ │ │ │ │ 134 | │ │ │ │ │ │ │ │ │ │ pe': │ │ │ │ │ 135 | │ │ │ │ │ │ │ │ │ │ 'A' │ │ │ │ │ 136 | │ │ │ │ │ │ │ │ │ │ , 't │ │ │ │ │ 137 | │ │ │ │ │ │ │ │ │ │ tl': │ │ │ │ │ 138 | │ │ │ │ │ │ │ │ │ │ 5, │ │ │ │ │ 139 | │ │ │ │ │ │ │ │ │ │ 'dat │ │ │ │ │ 140 | │ │ │ │ │ │ │ │ │ │ a': │ │ │ │ │ 141 | │ │ │ │ │ │ │ │ │ │ '151 │ │ │ │ │ 142 | │ │ │ │ │ │ │ │ │ │ .101 │ │ │ │ │ 143 | │ │ │ │ │ │ │ │ │ │ .65. │ │ │ │ │ 144 | │ │ │ │ │ │ │ │ │ │ 67'} │ │ │ │ │ 145 | │ │ │ │ │ │ │ │ │ │ , {' │ │ │ │ │ 146 | │ │ │ │ │ │ │ │ │ │ name │ │ │ │ │ 147 | │ │ │ │ │ │ │ │ │ │ ': ' │ │ │ │ │ 148 | │ │ │ │ │ │ │ │ │ │ turn │ │ │ │ │ 149 | │ │ │ │ │ │ │ │ │ │ er-t │ │ │ │ │ 150 | │ │ │ │ │ │ │ │ │ │ ls.m │ │ │ │ │ 151 | │ │ │ │ │ │ │ │ │ │ ap.f │ │ │ │ │ 152 | │ │ │ │ │ │ │ │ │ │ astl │ │ │ │ │ 153 | │ │ │ │ │ │ │ │ │ │ y.ne │ │ │ │ │ 154 | │ │ │ │ │ │ │ │ │ │ t.', │ │ │ │ │ 155 | │ │ │ │ │ │ │ │ │ │ 'cl │ │ │ │ │ 156 | │ │ │ │ │ │ │ │ │ │ ass' │ │ │ │ │ 157 | │ │ │ │ │ │ │ │ │ │ : 'I │ │ │ │ │ 158 | │ │ │ │ │ │ │ │ │ │ N', │ │ │ │ │ 159 | │ │ │ │ │ │ │ │ │ │ 'typ │ │ │ │ │ 160 | │ │ │ │ │ │ │ │ │ │ e': │ │ │ │ │ 161 | │ │ │ │ │ │ │ │ │ │ 'A', │ │ │ │ │ 162 | │ │ │ │ │ │ │ │ │ │ 'tt │ │ │ │ │ 163 | │ │ │ │ │ │ │ │ │ │ l': │ │ │ │ │ 164 | │ │ │ │ │ │ │ │ │ │ 5, ' │ │ │ │ │ 165 | │ │ │ │ │ │ │ │ │ │ data │ │ │ │ │ 166 | │ │ │ │ │ │ │ │ │ │ ': ' │ │ │ │ │ 167 | │ │ │ │ │ │ │ │ │ │ 151. │ │ │ │ │ 168 | │ │ │ │ │ │ │ │ │ │ 101. │ │ │ │ │ 169 | │ │ │ │ │ │ │ │ │ │ 129. │ │ │ │ │ 170 | │ │ │ │ │ │ │ │ │ │ 67'} │ │ │ │ │ 171 | │ │ │ │ │ │ │ │ │ │ , {' │ │ │ │ │ 172 | │ │ │ │ │ │ │ │ │ │ name │ │ │ │ │ 173 | │ │ │ │ │ │ │ │ │ │ ': ' │ │ │ │ │ 174 | │ │ │ │ │ │ │ │ │ │ turn │ │ │ │ │ 175 | │ │ │ │ │ │ │ │ │ │ er-t │ │ │ │ │ 176 | │ │ │ │ │ │ │ │ │ │ ls.m │ │ │ │ │ 177 | │ │ │ │ │ │ │ │ │ │ ap.f │ │ │ │ │ 178 | │ │ │ │ │ │ │ │ │ │ astl │ │ │ │ │ 179 | │ │ │ │ │ │ │ │ │ │ y.ne │ │ │ │ │ 180 | │ │ │ │ │ │ │ │ │ │ t.', │ │ │ │ │ 181 | │ │ │ │ │ │ │ │ │ │ 'cl │ │ │ │ │ 182 | │ │ │ │ │ │ │ │ │ │ ass' │ │ │ │ │ 183 | │ │ │ │ │ │ │ │ │ │ : 'I │ │ │ │ │ 184 | │ │ │ │ │ │ │ │ │ │ N', │ │ │ │ │ 185 | │ │ │ │ │ │ │ │ │ │ 'typ │ │ │ │ │ 186 | │ │ │ │ │ │ │ │ │ │ e': │ │ │ │ │ 187 | │ │ │ │ │ │ │ │ │ │ 'A', │ │ │ │ │ 188 | │ │ │ │ │ │ │ │ │ │ 'tt │ │ │ │ │ 189 | │ │ │ │ │ │ │ │ │ │ l': │ │ │ │ │ 190 | │ │ │ │ │ │ │ │ │ │ 5, ' │ │ │ │ │ 191 | │ │ │ │ │ │ │ │ │ │ data │ │ │ │ │ 192 | │ │ │ │ │ │ │ │ │ │ ': ' │ │ │ │ │ 193 | │ │ │ │ │ │ │ │ │ │ 151. │ │ │ │ │ 194 | │ │ │ │ │ │ │ │ │ │ 101. │ │ │ │ │ 195 | │ │ │ │ │ │ │ │ │ │ 193. │ │ │ │ │ 196 | │ │ │ │ │ │ │ │ │ │ 67'} │ │ │ │ │ 197 | │ │ │ │ │ │ │ │ │ │ ] │ │ │ │ │ 198 | ╘══════╧════════╧════════╧════════╧════════╧════════╧════════╧════════╧════════╧════════╧════════╧════════╧════════╧════════╛''') 199 | 200 | self.assertEqual(jtbl.cli.make_table(data=stdin, columns=80), (self.SUCCESS, expected)) 201 | 202 | def test_jc_dig_150cols(self): 203 | stdin = [{"id": 55658, "opcode": "QUERY", "status": "NOERROR", "flags": ["qr", "rd", "ra"], "query_num": 1, "answer_num": 5, "authority_num": 0, "additional_num": 1, "question": {"name": "www.cnn.com.", "class": "IN", "type": "A"}, "answer": [{"name": "www.cnn.com.", "class": "IN", "type": "CNAME", "ttl": 147, "data": "turner-tls.map.fastly.net."}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.1.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.65.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.129.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.193.67"}], "query_time": 44, "server": "2600", "when": "Wed Mar 18 12:20:59 PDT 2020", "rcvd": 143}] 204 | expected = textwrap.dedent('''\ 205 | ╒═══════╤══════════╤══════════╤═════════╤══════════╤══════════╤══════════╤══════════╤══════════╤══════════╤══════════╤══════════╤════════╤════════╕ 206 | │ id │ opcode │ status │ flags │ query_ │ answer │ author │ additi │ questi │ answer │ query_ │ server │ when │ rcvd │ 207 | │ │ │ │ │ num │ _num │ ity_nu │ onal_n │ on │ │ time │ │ │ │ 208 | │ │ │ │ │ │ │ m │ um │ │ │ │ │ │ │ 209 | ╞═══════╪══════════╪══════════╪═════════╪══════════╪══════════╪══════════╪══════════╪══════════╪══════════╪══════════╪══════════╪════════╪════════╡ 210 | │ 55658 │ QUERY │ NOERRO │ ['qr', │ 1 │ 5 │ 0 │ 1 │ {'name │ [{'nam │ 44 │ 2600 │ Wed Ma │ 143 │ 211 | │ │ │ R │ 'rd', │ │ │ │ │ ': 'ww │ e': 'w │ │ │ r 18 1 │ │ 212 | │ │ │ │ 'ra'] │ │ │ │ │ w.cnn. │ ww.cnn │ │ │ 2:20:5 │ │ 213 | │ │ │ │ │ │ │ │ │ com.', │ .com.' │ │ │ 9 PDT │ │ 214 | │ │ │ │ │ │ │ │ │ 'clas │ , 'cla │ │ │ 2020 │ │ 215 | │ │ │ │ │ │ │ │ │ s': 'I │ ss': ' │ │ │ │ │ 216 | │ │ │ │ │ │ │ │ │ N', 't │ IN', ' │ │ │ │ │ 217 | │ │ │ │ │ │ │ │ │ ype': │ type': │ │ │ │ │ 218 | │ │ │ │ │ │ │ │ │ 'A'} │ 'CNAM │ │ │ │ │ 219 | │ │ │ │ │ │ │ │ │ │ E', 't │ │ │ │ │ 220 | │ │ │ │ │ │ │ │ │ │ tl': 1 │ │ │ │ │ 221 | │ │ │ │ │ │ │ │ │ │ 47, 'd │ │ │ │ │ 222 | │ │ │ │ │ │ │ │ │ │ ata': │ │ │ │ │ 223 | │ │ │ │ │ │ │ │ │ │ 'turne │ │ │ │ │ 224 | │ │ │ │ │ │ │ │ │ │ r-tls. │ │ │ │ │ 225 | │ │ │ │ │ │ │ │ │ │ map.fa │ │ │ │ │ 226 | │ │ │ │ │ │ │ │ │ │ stly.n │ │ │ │ │ 227 | │ │ │ │ │ │ │ │ │ │ et.'}, │ │ │ │ │ 228 | │ │ │ │ │ │ │ │ │ │ {'nam │ │ │ │ │ 229 | │ │ │ │ │ │ │ │ │ │ e': 't │ │ │ │ │ 230 | │ │ │ │ │ │ │ │ │ │ urner- │ │ │ │ │ 231 | │ │ │ │ │ │ │ │ │ │ tls.ma │ │ │ │ │ 232 | │ │ │ │ │ │ │ │ │ │ p.fast │ │ │ │ │ 233 | │ │ │ │ │ │ │ │ │ │ ly.net │ │ │ │ │ 234 | │ │ │ │ │ │ │ │ │ │ .', 'c │ │ │ │ │ 235 | │ │ │ │ │ │ │ │ │ │ lass': │ │ │ │ │ 236 | │ │ │ │ │ │ │ │ │ │ 'IN', │ │ │ │ │ 237 | │ │ │ │ │ │ │ │ │ │ 'type │ │ │ │ │ 238 | │ │ │ │ │ │ │ │ │ │ ': 'A' │ │ │ │ │ 239 | │ │ │ │ │ │ │ │ │ │ , 'ttl │ │ │ │ │ 240 | │ │ │ │ │ │ │ │ │ │ ': 5, │ │ │ │ │ 241 | │ │ │ │ │ │ │ │ │ │ 'data' │ │ │ │ │ 242 | │ │ │ │ │ │ │ │ │ │ : '151 │ │ │ │ │ 243 | │ │ │ │ │ │ │ │ │ │ .101.1 │ │ │ │ │ 244 | │ │ │ │ │ │ │ │ │ │ .67'}, │ │ │ │ │ 245 | │ │ │ │ │ │ │ │ │ │ {'nam │ │ │ │ │ 246 | │ │ │ │ │ │ │ │ │ │ e': 't │ │ │ │ │ 247 | │ │ │ │ │ │ │ │ │ │ urner- │ │ │ │ │ 248 | │ │ │ │ │ │ │ │ │ │ tls.ma │ │ │ │ │ 249 | │ │ │ │ │ │ │ │ │ │ p.fast │ │ │ │ │ 250 | │ │ │ │ │ │ │ │ │ │ ly.net │ │ │ │ │ 251 | │ │ │ │ │ │ │ │ │ │ .', 'c │ │ │ │ │ 252 | │ │ │ │ │ │ │ │ │ │ lass': │ │ │ │ │ 253 | │ │ │ │ │ │ │ │ │ │ 'IN', │ │ │ │ │ 254 | │ │ │ │ │ │ │ │ │ │ 'type │ │ │ │ │ 255 | │ │ │ │ │ │ │ │ │ │ ': 'A' │ │ │ │ │ 256 | │ │ │ │ │ │ │ │ │ │ , 'ttl │ │ │ │ │ 257 | │ │ │ │ │ │ │ │ │ │ ': 5, │ │ │ │ │ 258 | │ │ │ │ │ │ │ │ │ │ 'data' │ │ │ │ │ 259 | │ │ │ │ │ │ │ │ │ │ : '151 │ │ │ │ │ 260 | │ │ │ │ │ │ │ │ │ │ .101.6 │ │ │ │ │ 261 | │ │ │ │ │ │ │ │ │ │ 5.67'} │ │ │ │ │ 262 | │ │ │ │ │ │ │ │ │ │ , {'na │ │ │ │ │ 263 | │ │ │ │ │ │ │ │ │ │ me': ' │ │ │ │ │ 264 | │ │ │ │ │ │ │ │ │ │ turner │ │ │ │ │ 265 | │ │ │ │ │ │ │ │ │ │ -tls.m │ │ │ │ │ 266 | │ │ │ │ │ │ │ │ │ │ ap.fas │ │ │ │ │ 267 | │ │ │ │ │ │ │ │ │ │ tly.ne │ │ │ │ │ 268 | │ │ │ │ │ │ │ │ │ │ t.', ' │ │ │ │ │ 269 | │ │ │ │ │ │ │ │ │ │ class' │ │ │ │ │ 270 | │ │ │ │ │ │ │ │ │ │ : 'IN' │ │ │ │ │ 271 | │ │ │ │ │ │ │ │ │ │ , 'typ │ │ │ │ │ 272 | │ │ │ │ │ │ │ │ │ │ e': 'A │ │ │ │ │ 273 | │ │ │ │ │ │ │ │ │ │ ', 'tt │ │ │ │ │ 274 | │ │ │ │ │ │ │ │ │ │ l': 5, │ │ │ │ │ 275 | │ │ │ │ │ │ │ │ │ │ 'data │ │ │ │ │ 276 | │ │ │ │ │ │ │ │ │ │ ': '15 │ │ │ │ │ 277 | │ │ │ │ │ │ │ │ │ │ 1.101. │ │ │ │ │ 278 | │ │ │ │ │ │ │ │ │ │ 129.67 │ │ │ │ │ 279 | │ │ │ │ │ │ │ │ │ │ '}, {' │ │ │ │ │ 280 | │ │ │ │ │ │ │ │ │ │ name': │ │ │ │ │ 281 | │ │ │ │ │ │ │ │ │ │ 'turn │ │ │ │ │ 282 | │ │ │ │ │ │ │ │ │ │ er-tls │ │ │ │ │ 283 | │ │ │ │ │ │ │ │ │ │ .map.f │ │ │ │ │ 284 | │ │ │ │ │ │ │ │ │ │ astly. │ │ │ │ │ 285 | │ │ │ │ │ │ │ │ │ │ net.', │ │ │ │ │ 286 | │ │ │ │ │ │ │ │ │ │ 'clas │ │ │ │ │ 287 | │ │ │ │ │ │ │ │ │ │ s': 'I │ │ │ │ │ 288 | │ │ │ │ │ │ │ │ │ │ N', 't │ │ │ │ │ 289 | │ │ │ │ │ │ │ │ │ │ ype': │ │ │ │ │ 290 | │ │ │ │ │ │ │ │ │ │ 'A', ' │ │ │ │ │ 291 | │ │ │ │ │ │ │ │ │ │ ttl': │ │ │ │ │ 292 | │ │ │ │ │ │ │ │ │ │ 5, 'da │ │ │ │ │ 293 | │ │ │ │ │ │ │ │ │ │ ta': ' │ │ │ │ │ 294 | │ │ │ │ │ │ │ │ │ │ 151.10 │ │ │ │ │ 295 | │ │ │ │ │ │ │ │ │ │ 1.193. │ │ │ │ │ 296 | │ │ │ │ │ │ │ │ │ │ 67'}] │ │ │ │ │ 297 | ╘═══════╧══════════╧══════════╧═════════╧══════════╧══════════╧══════════╧══════════╧══════════╧══════════╧══════════╧══════════╧════════╧════════╛''') 298 | 299 | self.assertEqual(jtbl.cli.make_table(data=stdin, columns=150), (self.SUCCESS, expected)) 300 | 301 | def test_jc_dig_150cols_t(self): 302 | stdin = [{"id": 55658, "opcode": "QUERY", "status": "NOERROR", "flags": ["qr", "rd", "ra"], "query_num": 1, "answer_num": 5, "authority_num": 0, "additional_num": 1, "question": {"name": "www.cnn.com.", "class": "IN", "type": "A"}, "answer": [{"name": "www.cnn.com.", "class": "IN", "type": "CNAME", "ttl": 147, "data": "turner-tls.map.fastly.net."}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.1.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.65.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.129.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.193.67"}], "query_time": 44, "server": "2600", "when": "Wed Mar 18 12:20:59 PDT 2020", "rcvd": 143}] 303 | expected = textwrap.dedent('''\ 304 | id opcode status flags query_nu answer_n authorit addition question answer query_ti server when rcvd 305 | ----- -------- -------- -------- ---------- ---------- ---------- ---------- ---------- -------- ---------- -------- ------- ------ 306 | 55658 QUERY NOERROR ['qr', ' 1 5 0 1 {'name': [{'name' 44 2600 Wed Mar 143''') 307 | 308 | self.assertEqual(jtbl.cli.make_table(data=stdin, truncate=True, columns=150), (self.SUCCESS, expected)) 309 | 310 | def test_jc_dig_nowrap(self): 311 | stdin = [{"id": 55658, "opcode": "QUERY", "status": "NOERROR", "flags": ["qr", "rd", "ra"], "query_num": 1, "answer_num": 5, "authority_num": 0, "additional_num": 1, "question": {"name": "www.cnn.com.", "class": "IN", "type": "A"}, "answer": [{"name": "www.cnn.com.", "class": "IN", "type": "CNAME", "ttl": 147, "data": "turner-tls.map.fastly.net."}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.1.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.65.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.129.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.193.67"}], "query_time": 44, "server": "2600", "when": "Wed Mar 18 12:20:59 PDT 2020", "rcvd": 143}] 312 | expected = textwrap.dedent('''\ 313 | id opcode status flags query_num answer_num authority_num additional_num question answer query_time server when rcvd 314 | ----- -------- -------- ------------------ ----------- ------------ --------------- ---------------- ---------------------------------------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------ -------- ---------------------------- ------ 315 | 55658 QUERY NOERROR ['qr', 'rd', 'ra'] 1 5 0 1 {'name': 'www.cnn.com.', 'class': 'IN', 'type': 'A'} [{'name': 'www.cnn.com.', 'class': 'IN', 'type': 'CNAME', 'ttl': 147, 'data': 'turner-tls.map.fastly.net.'}, {'name': 'turner-tls.map.fastly.net.', 'class': 'IN', 'type': 'A', 'ttl': 5, 'data': '151.101.1.67'}, {'name': 'turner-tls.map.fastly.net.', 'class': 'IN', 'type': 'A', 'ttl': 5, 'data': '151.101.65.67'}, {'name': 'turner-tls.map.fastly.net.', 'class': 'IN', 'type': 'A', 'ttl': 5, 'data': '151.101.129.67'}, {'name': 'turner-tls.map.fastly.net.', 'class': 'IN', 'type': 'A', 'ttl': 5, 'data': '151.101.193.67'}] 44 2600 Wed Mar 18 12:20:59 PDT 2020 143''') 316 | 317 | self.assertEqual(jtbl.cli.make_table(data=stdin, nowrap=True, columns=150), (self.SUCCESS, expected)) 318 | 319 | def test_jc_dig_nowrap_t_cols_80(self): 320 | """test that nowrap overrides both truncate and columns""" 321 | stdin = [{"id": 55658, "opcode": "QUERY", "status": "NOERROR", "flags": ["qr", "rd", "ra"], "query_num": 1, "answer_num": 5, "authority_num": 0, "additional_num": 1, "question": {"name": "www.cnn.com.", "class": "IN", "type": "A"}, "answer": [{"name": "www.cnn.com.", "class": "IN", "type": "CNAME", "ttl": 147, "data": "turner-tls.map.fastly.net."}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.1.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.65.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.129.67"}, {"name": "turner-tls.map.fastly.net.", "class": "IN", "type": "A", "ttl": 5, "data": "151.101.193.67"}], "query_time": 44, "server": "2600", "when": "Wed Mar 18 12:20:59 PDT 2020", "rcvd": 143}] 322 | expected = textwrap.dedent('''\ 323 | id opcode status flags query_num answer_num authority_num additional_num question answer query_time server when rcvd 324 | ----- -------- -------- ------------------ ----------- ------------ --------------- ---------------- ---------------------------------------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------ -------- ---------------------------- ------ 325 | 55658 QUERY NOERROR ['qr', 'rd', 'ra'] 1 5 0 1 {'name': 'www.cnn.com.', 'class': 'IN', 'type': 'A'} [{'name': 'www.cnn.com.', 'class': 'IN', 'type': 'CNAME', 'ttl': 147, 'data': 'turner-tls.map.fastly.net.'}, {'name': 'turner-tls.map.fastly.net.', 'class': 'IN', 'type': 'A', 'ttl': 5, 'data': '151.101.1.67'}, {'name': 'turner-tls.map.fastly.net.', 'class': 'IN', 'type': 'A', 'ttl': 5, 'data': '151.101.65.67'}, {'name': 'turner-tls.map.fastly.net.', 'class': 'IN', 'type': 'A', 'ttl': 5, 'data': '151.101.129.67'}, {'name': 'turner-tls.map.fastly.net.', 'class': 'IN', 'type': 'A', 'ttl': 5, 'data': '151.101.193.67'}] 44 2600 Wed Mar 18 12:20:59 PDT 2020 143''') 326 | 327 | self.assertEqual(jtbl.cli.make_table(data=stdin, nowrap=True, columns=80, truncate=True), (self.SUCCESS, expected)) 328 | 329 | def test_jc_dig_answer(self): 330 | stdin = [{"name":"www.cnn.com.","class":"IN","type":"CNAME","ttl":147,"data":"turner-tls.map.fastly.net."},{"name":"turner-tls.map.fastly.net.","class":"IN","type":"A","ttl":5,"data":"151.101.1.67"},{"name":"turner-tls.map.fastly.net.","class":"IN","type":"A","ttl":5,"data":"151.101.65.67"},{"name":"turner-tls.map.fastly.net.","class":"IN","type":"A","ttl":5,"data":"151.101.129.67"},{"name":"turner-tls.map.fastly.net.","class":"IN","type":"A","ttl":5,"data":"151.101.193.67"}] 331 | expected = textwrap.dedent('''\ 332 | name class type ttl data 333 | -------------------------- ------- ------ ----- -------------------------- 334 | www.cnn.com. IN CNAME 147 turner-tls.map.fastly.net. 335 | turner-tls.map.fastly.net. IN A 5 151.101.1.67 336 | turner-tls.map.fastly.net. IN A 5 151.101.65.67 337 | turner-tls.map.fastly.net. IN A 5 151.101.129.67 338 | turner-tls.map.fastly.net. IN A 5 151.101.193.67''') 339 | 340 | self.assertEqual(jtbl.cli.make_table(data=stdin, columns=80), (self.SUCCESS, expected)) 341 | 342 | def test_float_format(self): 343 | stdin = [{"a": 1000000, "b": 1000000.1}] 344 | expected = ' a b\n------- ---------\n1000000 1000000.1' 345 | 346 | self.assertEqual(jtbl.cli.make_table(data=stdin, columns=80), (self.SUCCESS, expected)) 347 | 348 | def test_json_lines(self): 349 | """test JSON Lines data""" 350 | stdin = [ 351 | {"name":"lo0","type":None,"ipv4_addr":"127.0.0.1","ipv4_mask":"255.0.0.0"}, 352 | {"name":"gif0","type":None,"ipv4_addr":None,"ipv4_mask":None}, 353 | {"name":"stf0","type":None,"ipv4_addr":None,"ipv4_mask":None}, 354 | {"name":"XHC0","type":None,"ipv4_addr":None,"ipv4_mask":None}, 355 | {"name":"XHC20","type":None,"ipv4_addr":None,"ipv4_mask":None}, 356 | {"name":"VHC128","type":None,"ipv4_addr":None,"ipv4_mask":None}, 357 | {"name":"XHC1","type":None,"ipv4_addr":None,"ipv4_mask":None}, 358 | {"name":"en5","type":None,"ipv4_addr":None,"ipv4_mask":None}, 359 | {"name":"ap1","type":None,"ipv4_addr":None,"ipv4_mask":None}, 360 | {"name":"en0","type":None,"ipv4_addr":"192.168.1.221","ipv4_mask":"255.255.255.0"}, 361 | {"name":"p2p0","type":None,"ipv4_addr":None,"ipv4_mask":None}, 362 | {"name":"awdl0","type":None,"ipv4_addr":None,"ipv4_mask":None}, 363 | {"name":"en1","type":None,"ipv4_addr":None,"ipv4_mask":None}, 364 | {"name":"en2","type":None,"ipv4_addr":None,"ipv4_mask":None}, 365 | {"name":"en3","type":None,"ipv4_addr":None,"ipv4_mask":None}, 366 | {"name":"en4","type":None,"ipv4_addr":None,"ipv4_mask":None}, 367 | {"name":"bridge0","type":None,"ipv4_addr":None,"ipv4_mask":None}, 368 | {"name":"utun0","type":None,"ipv4_addr":None,"ipv4_mask":None}, 369 | {"name":"utun1","type":None,"ipv4_addr":None,"ipv4_mask":None}, 370 | {"name":"utun2","type":None,"ipv4_addr":None,"ipv4_mask":None}, 371 | {"name":"utun3","type":None,"ipv4_addr":None,"ipv4_mask":None}, 372 | {"name":"utun4","type":None,"ipv4_addr":None,"ipv4_mask":None}, 373 | {"name":"vmnet1","type":None,"ipv4_addr":"192.168.101.1","ipv4_mask":"255.255.255.0"}, 374 | {"name":"vmnet8","type":None,"ipv4_addr":"192.168.71.1","ipv4_mask":"255.255.255.0"} 375 | ] 376 | expected = textwrap.dedent('''\ 377 | name type ipv4_addr ipv4_mask 378 | ------- ------ ------------- ------------- 379 | lo0 127.0.0.1 255.0.0.0 380 | gif0 381 | stf0 382 | XHC0 383 | XHC20 384 | VHC128 385 | XHC1 386 | en5 387 | ap1 388 | en0 192.168.1.221 255.255.255.0 389 | p2p0 390 | awdl0 391 | en1 392 | en2 393 | en3 394 | en4 395 | bridge0 396 | utun0 397 | utun1 398 | utun2 399 | utun3 400 | utun4 401 | vmnet1 192.168.101.1 255.255.255.0 402 | vmnet8 192.168.71.1 255.255.255.0''') 403 | 404 | self.assertEqual(jtbl.cli.make_table(data=stdin, columns=self.columns), (self.SUCCESS, expected)) 405 | 406 | def test_markdown(self): 407 | """test markdown output""" 408 | stdin = [ 409 | { 410 | "column1": "data", 411 | "column2": 123, 412 | "column3": True, 413 | "column4": None 414 | }, 415 | { 416 | "column1": "This is a long string that should not be truncated by the markdown table format. Lines should not be wrapped for markdown.", 417 | "column2": 123, 418 | "column3": True, 419 | "column4": None 420 | } 421 | ] 422 | 423 | expected = textwrap.dedent('''\ 424 | | column1 | column2 | column3 | column4 | 425 | |----------------------------------------------------------------------------------------------------------------------------|-----------|-----------|-----------| 426 | | data | 123 | True | | 427 | | This is a long string that should not be truncated by the markdown table format. Lines should not be wrapped for markdown. | 123 | True | |''') 428 | 429 | self.assertEqual(jtbl.cli.make_table(data=stdin, columns=self.columns, nowrap=True, table_format='github'), (self.SUCCESS, expected)) 430 | 431 | 432 | def test_dokuwiki(self): 433 | """test DokuWiki markdown output""" 434 | stdin = [ 435 | { 436 | "column1": "data", 437 | "column2": 123, 438 | "column3": True, 439 | "column4": None 440 | }, 441 | { 442 | "column1": "This is a long string that should not be truncated by the markdown table format. Lines should not be wrapped for markdown.", 443 | "column2": 123, 444 | "column3": True, 445 | "column4": None 446 | } 447 | ] 448 | 449 | expected = textwrap.dedent('''\ 450 | ^ column1 ^ column2 ^ column3 ^ column4 ^ 451 | | data | 123 | True | | 452 | | This is a long string that should not be truncated by the markdown table format. Lines should not be wrapped for markdown. | 123 | True | |''') 453 | self.assertEqual(jtbl.cli.make_table(data=stdin, columns=self.columns, nowrap=True, table_format='dokuwiki'), (self.SUCCESS, expected)) 454 | 455 | 456 | def test_add_remove_fields(self): 457 | """test with added and missing fields""" 458 | stdin = [{"foo this is a very long long key":"this is a very very long string yes it is"},{"foo this is a very long long key":"medium length string","bar this is another very long string":"now is the time for all good men to come to the aide of their party"},{"baz is yet another long key name":"hello there how are you doing today? I am fine, thank you.","bar this is another very long string":"short string"}] 459 | expected = textwrap.dedent('''\ 460 | ╒════════════════════════════════╤════════════════════════════════╤════════════════════════════════╕ 461 | │ foo this is a very long long │ bar this is another very lon │ baz is yet another long key │ 462 | │ key │ g string │ name │ 463 | ╞════════════════════════════════╪════════════════════════════════╪════════════════════════════════╡ 464 | │ this is a very very long str │ │ │ 465 | │ ing yes it is │ │ │ 466 | ├────────────────────────────────┼────────────────────────────────┼────────────────────────────────┤ 467 | │ medium length string │ now is the time for all good │ │ 468 | │ │ men to come to the aide of │ │ 469 | │ │ their party │ │ 470 | ├────────────────────────────────┼────────────────────────────────┼────────────────────────────────┤ 471 | │ │ short string │ hello there how are you doin │ 472 | │ │ │ g today? I am fine, thank yo │ 473 | │ │ │ u. │ 474 | ╘════════════════════════════════╧════════════════════════════════╧════════════════════════════════╛''') 475 | 476 | self.assertEqual(jtbl.cli.make_table(data=stdin, columns=100), (self.SUCCESS, expected)) 477 | 478 | 479 | def test_csv(self): 480 | """test csv output""" 481 | stdin = [{"LatD":"41","LatM":"5","LatS":"59","NS":"N","LonD":"80","LonM":"39","LonS":"0","EW":"W","City":"Youngstown","State":"OH"},{"LatD":"42","LatM":"52","LatS":"48","NS":"N","LonD":"97","LonM":"23","LonS":"23","EW":"W","City":"Yankton","State":"SD"},{"LatD":"46","LatM":"35","LatS":"59","NS":"N","LonD":"120","LonM":"30","LonS":"36","EW":"W","City":"Yakima","State":"WA"},{"LatD":"42","LatM":"16","LatS":"12","NS":"N","LonD":"71","LonM":"48","LonS":"0","EW":"W","City":"Worcester","State":"MA"},{"LatD":"43","LatM":"37","LatS":"48","NS":"N","LonD":"89","LonM":"46","LonS":"11","EW":"W","City":"Wisconsin Dells","State":"WI"},{"LatD":"36","LatM":"5","LatS":"59","NS":"N","LonD":"80","LonM":"15","LonS":"0","EW":"W","City":"Winston-Salem","State":"NC"},{"LatD":"49","LatM":"52","LatS":"48","NS":"N","LonD":"97","LonM":"9","LonS":"0","EW":"W","City":"Winnipeg","State":"MB"},{"LatD":"39","LatM":"11","LatS":"23","NS":"N","LonD":"78","LonM":"9","LonS":"36","EW":"W","City":"Winchester","State":"VA"},{"LatD":"34","LatM":"14","LatS":"24","NS":"N","LonD":"77","LonM":"55","LonS":"11","EW":"W","City":"Wilmington","State":"NC"}] 482 | expected = textwrap.dedent('''\ 483 | LatD,LatM,LatS,NS,LonD,LonM,LonS,EW,City,State\r 484 | 41,5,59,N,80,39,0,W,Youngstown,OH\r 485 | 42,52,48,N,97,23,23,W,Yankton,SD\r 486 | 46,35,59,N,120,30,36,W,Yakima,WA\r 487 | 42,16,12,N,71,48,0,W,Worcester,MA\r 488 | 43,37,48,N,89,46,11,W,Wisconsin Dells,WI\r 489 | 36,5,59,N,80,15,0,W,Winston-Salem,NC\r 490 | 49,52,48,N,97,9,0,W,Winnipeg,MB\r 491 | 39,11,23,N,78,9,36,W,Winchester,VA\r 492 | 34,14,24,N,77,55,11,W,Wilmington,NC\r 493 | ''') 494 | 495 | self.assertEqual(jtbl.cli.make_csv_table(data=stdin), (self.SUCCESS, expected)) 496 | 497 | 498 | def test_html(self): 499 | """test html output""" 500 | stdin = [{"LatD":"41","LatM":"5","LatS":"59","NS":"N","LonD":"80","LonM":"39","LonS":"0","EW":"W","City":"Youngstown","State":"OH"},{"LatD":"42","LatM":"52","LatS":"48","NS":"N","LonD":"97","LonM":"23","LonS":"23","EW":"W","City":"Yankton","State":"SD"},{"LatD":"46","LatM":"35","LatS":"59","NS":"N","LonD":"120","LonM":"30","LonS":"36","EW":"W","City":"Yakima","State":"WA"},{"LatD":"42","LatM":"16","LatS":"12","NS":"N","LonD":"71","LonM":"48","LonS":"0","EW":"W","City":"Worcester","State":"MA"},{"LatD":"43","LatM":"37","LatS":"48","NS":"N","LonD":"89","LonM":"46","LonS":"11","EW":"W","City":"Wisconsin Dells","State":"WI"},{"LatD":"36","LatM":"5","LatS":"59","NS":"N","LonD":"80","LonM":"15","LonS":"0","EW":"W","City":"Winston-Salem","State":"NC"},{"LatD":"49","LatM":"52","LatS":"48","NS":"N","LonD":"97","LonM":"9","LonS":"0","EW":"W","City":"Winnipeg","State":"MB"},{"LatD":"39","LatM":"11","LatS":"23","NS":"N","LonD":"78","LonM":"9","LonS":"36","EW":"W","City":"Winchester","State":"VA"},{"LatD":"34","LatM":"14","LatS":"24","NS":"N","LonD":"77","LonM":"55","LonS":"11","EW":"W","City":"Wilmington","State":"NC"}] 501 | 502 | expected = textwrap.dedent('''\ 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 |
LatD LatM LatSNS LonD LonM LonSEW City State
41 5 59N 80 39 0W Youngstown OH
42 52 48N 97 23 23W Yankton SD
46 35 59N 120 30 36W Yakima WA
42 16 12N 71 48 0W Worcester MA
43 37 48N 89 46 11W Wisconsin DellsWI
36 5 59N 80 15 0W Winston-Salem NC
49 52 48N 97 9 0W Winnipeg MB
39 11 23N 78 9 36W Winchester VA
34 14 24N 77 55 11W Wilmington NC
''') 519 | 520 | self.assertEqual(jtbl.cli.make_table(data=stdin, columns=self.columns, nowrap=True, table_format='html'), (self.SUCCESS, expected)) 521 | 522 | def test_fancy(self): 523 | """test fancy output with -f""" 524 | stdin = [{"name":"www.cnn.com.","class":"IN","type":"CNAME","ttl":147,"data":"turner-tls.map.fastly.net."},{"name":"turner-tls.map.fastly.net.","class":"IN","type":"A","ttl":5,"data":"151.101.1.67"},{"name":"turner-tls.map.fastly.net.","class":"IN","type":"A","ttl":5,"data":"151.101.65.67"},{"name":"turner-tls.map.fastly.net.","class":"IN","type":"A","ttl":5,"data":"151.101.129.67"},{"name":"turner-tls.map.fastly.net.","class":"IN","type":"A","ttl":5,"data":"151.101.193.67"}] 525 | expected = textwrap.dedent('''\ 526 | ╒════════════════════════════╤═════════╤════════╤═══════╤════════════════════════════╕ 527 | │ name │ class │ type │ ttl │ data │ 528 | ╞════════════════════════════╪═════════╪════════╪═══════╪════════════════════════════╡ 529 | │ www.cnn.com. │ IN │ CNAME │ 147 │ turner-tls.map.fastly.net. │ 530 | ├────────────────────────────┼─────────┼────────┼───────┼────────────────────────────┤ 531 | │ turner-tls.map.fastly.net. │ IN │ A │ 5 │ 151.101.1.67 │ 532 | ├────────────────────────────┼─────────┼────────┼───────┼────────────────────────────┤ 533 | │ turner-tls.map.fastly.net. │ IN │ A │ 5 │ 151.101.65.67 │ 534 | ├────────────────────────────┼─────────┼────────┼───────┼────────────────────────────┤ 535 | │ turner-tls.map.fastly.net. │ IN │ A │ 5 │ 151.101.129.67 │ 536 | ├────────────────────────────┼─────────┼────────┼───────┼────────────────────────────┤ 537 | │ turner-tls.map.fastly.net. │ IN │ A │ 5 │ 151.101.193.67 │ 538 | ╘════════════════════════════╧═════════╧════════╧═══════╧════════════════════════════╛''') 539 | self.assertEqual(jtbl.cli.make_table(data=stdin, columns=self.columns, nowrap=True, table_format='fancy_grid'), (self.SUCCESS, expected)) 540 | 541 | def test_rotate(self): 542 | """test html output""" 543 | stdin = [{"LatD":"41","LatM":"5","LatS":"59","NS":"N","LonD":"80","LonM":"39","LonS":"0","EW":"W","City":"Youngstown","State":"OH"},{"LatD":"42","LatM":"52","LatS":"48","NS":"N","LonD":"97","LonM":"23","LonS":"23","EW":"W","City":"Yankton","State":"SD"},{"LatD":"46","LatM":"35","LatS":"59","NS":"N","LonD":"120","LonM":"30","LonS":"36","EW":"W","City":"Yakima","State":"WA"},{"LatD":"42","LatM":"16","LatS":"12","NS":"N","LonD":"71","LonM":"48","LonS":"0","EW":"W","City":"Worcester","State":"MA"},{"LatD":"43","LatM":"37","LatS":"48","NS":"N","LonD":"89","LonM":"46","LonS":"11","EW":"W","City":"Wisconsin Dells","State":"WI"},{"LatD":"36","LatM":"5","LatS":"59","NS":"N","LonD":"80","LonM":"15","LonS":"0","EW":"W","City":"Winston-Salem","State":"NC"},{"LatD":"49","LatM":"52","LatS":"48","NS":"N","LonD":"97","LonM":"9","LonS":"0","EW":"W","City":"Winnipeg","State":"MB"},{"LatD":"39","LatM":"11","LatS":"23","NS":"N","LonD":"78","LonM":"9","LonS":"36","EW":"W","City":"Winchester","State":"VA"},{"LatD":"34","LatM":"14","LatS":"24","NS":"N","LonD":"77","LonM":"55","LonS":"11","EW":"W","City":"Wilmington","State":"NC"}] 544 | 545 | expected = textwrap.dedent('''\ 546 | item: 0 547 | ──────────────────────────────────────────────────────────────────────────────── 548 | LatD 41 549 | LatM 5 550 | LatS 59 551 | NS N 552 | LonD 80 553 | LonM 39 554 | LonS 0 555 | EW W 556 | City Youngstown 557 | State OH 558 | 559 | item: 1 560 | ──────────────────────────────────────────────────────────────────────────────── 561 | LatD 42 562 | LatM 52 563 | LatS 48 564 | NS N 565 | LonD 97 566 | LonM 23 567 | LonS 23 568 | EW W 569 | City Yankton 570 | State SD 571 | 572 | item: 2 573 | ──────────────────────────────────────────────────────────────────────────────── 574 | LatD 46 575 | LatM 35 576 | LatS 59 577 | NS N 578 | LonD 120 579 | LonM 30 580 | LonS 36 581 | EW W 582 | City Yakima 583 | State WA 584 | 585 | item: 3 586 | ──────────────────────────────────────────────────────────────────────────────── 587 | LatD 42 588 | LatM 16 589 | LatS 12 590 | NS N 591 | LonD 71 592 | LonM 48 593 | LonS 0 594 | EW W 595 | City Worcester 596 | State MA 597 | 598 | item: 4 599 | ──────────────────────────────────────────────────────────────────────────────── 600 | LatD 43 601 | LatM 37 602 | LatS 48 603 | NS N 604 | LonD 89 605 | LonM 46 606 | LonS 11 607 | EW W 608 | City Wisconsin Dells 609 | State WI 610 | 611 | item: 5 612 | ──────────────────────────────────────────────────────────────────────────────── 613 | LatD 36 614 | LatM 5 615 | LatS 59 616 | NS N 617 | LonD 80 618 | LonM 15 619 | LonS 0 620 | EW W 621 | City Winston-Salem 622 | State NC 623 | 624 | item: 6 625 | ──────────────────────────────────────────────────────────────────────────────── 626 | LatD 49 627 | LatM 52 628 | LatS 48 629 | NS N 630 | LonD 97 631 | LonM 9 632 | LonS 0 633 | EW W 634 | City Winnipeg 635 | State MB 636 | 637 | item: 7 638 | ──────────────────────────────────────────────────────────────────────────────── 639 | LatD 39 640 | LatM 11 641 | LatS 23 642 | NS N 643 | LonD 78 644 | LonM 9 645 | LonS 36 646 | EW W 647 | City Winchester 648 | State VA 649 | 650 | item: 8 651 | ──────────────────────────────────────────────────────────────────────────────── 652 | LatD 34 653 | LatM 14 654 | LatS 24 655 | NS N 656 | LonD 77 657 | LonM 55 658 | LonS 11 659 | EW W 660 | City Wilmington 661 | State NC 662 | ''') 663 | 664 | self.assertEqual(jtbl.cli.make_rotate_table(data=stdin, columns=self.columns, nowrap=True, rotate=True), (self.SUCCESS, expected)) 665 | 666 | 667 | def test_rotate_single_item(self): 668 | """test html output""" 669 | stdin = [{"LatD":"41","LatM":"5","LatS":"59","NS":"N","LonD":"80","LonM":"39","LonS":"0","EW":"W","City":"Youngstown","State":"OH"}] 670 | 671 | expected = textwrap.dedent('''\ 672 | LatD 41 673 | LatM 5 674 | LatS 59 675 | NS N 676 | LonD 80 677 | LonM 39 678 | LonS 0 679 | EW W 680 | City Youngstown 681 | State OH 682 | ''') 683 | 684 | self.assertEqual(jtbl.cli.make_rotate_table(data=stdin, columns=self.columns, nowrap=True, rotate=True), (self.SUCCESS, expected)) 685 | 686 | 687 | if __name__ == '__main__': 688 | unittest.main() 689 | --------------------------------------------------------------------------------