feat: add --json flag to read, dump, and scan commands#5
Conversation
Closes #1 Outputs results as JSON instead of Rich tables when --json is passed, enabling piping into jq or other tools: modbus read 192.168.1.10 40001 -c 5 --json | jq '.registers[].value'
There was a problem hiding this comment.
Pull request overview
This PR adds a --json flag to the read, dump, and scan commands of modbus-cli, enabling structured JSON output for piping into tools like jq. It also adds --float, --byte-order, and --word-order options to the read command for decoding register pairs as 32-bit IEEE 754 floats (not mentioned in the PR description).
Changes:
- Added
--jsonflag toread,dump, andscancommands that outputs structured JSON instead of Rich tables - Added
--floatdecoding support with--byte-orderand--word-orderoptions to thereadcommand - Added
_decode_floatshelper function for IEEE 754 float decoding from register pairs
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| if as_json: | ||
| print(__import__("json").dumps({"host": host, "range": scan_range, "register": register, "devices": [{"slave_id": sid, "register_value": val} for sid, val in found]}, indent=2)) | ||
| return |
There was a problem hiding this comment.
When --json is used, Rich progress bars, spinner output, and "Found slave" messages are still printed to the console (lines 355-392) before the JSON is emitted. This pollutes stdout/stderr and makes the output unsuitable for piping into jq or other tools, which is the stated purpose of the --json flag. The JSON check should be moved earlier, or Rich output should be suppressed (e.g., by redirecting the console to stderr or skipping the progress display) when as_json is true.
| if as_json: | ||
| registers = [{"address": start_address + i, "raw": val, "value": _format_value(val, fmt)} for i, val in enumerate(all_values)] | ||
| print(__import__("json").dumps({"host": host, "type": detected_start, "slave": slave, "registers": registers}, indent=2)) | ||
| return |
There was a problem hiding this comment.
Similar to the scan command issue: when --json is used, Rich output (connection header at line 539, progress bars at lines 544-557) is still printed to the console before the JSON is emitted. This contaminates the JSON output and breaks piping. The Rich output should be suppressed or redirected to stderr when as_json is true.
| from rich.table import Table | ||
| from rich.panel import Panel |
There was a problem hiding this comment.
Table and Panel are already imported at the top of the file (lines 9-10). These local re-imports are unnecessary.
| from rich.table import Table | |
| from rich.panel import Panel |
| pairs = [{"address": address + i * 2, "value": floats[i]} for i in range(len(floats))] | ||
| print(__import__("json").dumps({"host": host, "type": detected_type, "slave": slave, "float_values": pairs}, indent=2)) |
There was a problem hiding this comment.
IEEE 754 special values (NaN, Infinity) can appear in Modbus register data. Python's json.dumps will serialize these as bare NaN/Infinity tokens, which are not valid JSON and will cause downstream JSON parsers (like jq) to fail. Consider using json.dumps(..., allow_nan=False) with error handling, or converting these values to null or string representations before serialization.
| floats = _decode_floats(list(values), byte_order=byte_order, word_order=word_order) | ||
| if as_json: | ||
| pairs = [{"address": address + i * 2, "value": floats[i]} for i in range(len(floats))] | ||
| print(__import__("json").dumps({"host": host, "type": detected_type, "slave": slave, "float_values": pairs}, indent=2)) |
There was a problem hiding this comment.
json is already imported at the top of the file (line 3). Using __import__("json") here is unnecessary and inconsistent. Just use json.dumps(...) directly.
| addr_display = address + i if not reg_type else raw_address + i | ||
| int_val = int(val) | ||
| registers.append({"address": addr_display, "raw": int_val, "value": _format_value(int_val, fmt) if not isinstance(val, bool) else str(int_val)}) | ||
| print(__import__("json").dumps({"host": host, "type": detected_type, "slave": slave, "registers": registers}, indent=2)) |
There was a problem hiding this comment.
Same issue: json is already imported at the top of the file. Use json.dumps(...) instead of __import__("json").dumps(...). This pattern is repeated in all three commands (read, scan, dump).
|
|
||
| if as_float: | ||
| if len(values) % 2 != 0: | ||
| from .theme import error_panel |
There was a problem hiding this comment.
error_panel is already imported at the top of the file (line 14). This local re-import is unnecessary.
| from .theme import error_panel |
| @click.option("--float", "as_float", is_flag=True, default=False, help="Decode register pairs as 32-bit IEEE 754 floats.") | ||
| @click.option("--byte-order", default="BE", type=click.Choice(["BE", "LE"]), help="Byte order for float decoding (default: BE).") | ||
| @click.option("--word-order", default="BE", type=click.Choice(["BE", "LE"]), help="Word order for float decoding (default: BE).") |
There was a problem hiding this comment.
The --float, --byte-order, and --word-order options are not mentioned in the PR description or issue #1. These are unrelated features bundled into a PR that's supposed to only add --json support. Consider splitting these into a separate PR for cleaner review and history.
| if as_json: | ||
| registers = [] | ||
| for i, val in enumerate(values): | ||
| addr_display = address + i if not reg_type else raw_address + i | ||
| int_val = int(val) | ||
| registers.append({"address": addr_display, "raw": int_val, "value": _format_value(int_val, fmt) if not isinstance(val, bool) else str(int_val)}) | ||
| print(__import__("json").dumps({"host": host, "type": detected_type, "slave": slave, "registers": registers}, indent=2)) | ||
| return |
There was a problem hiding this comment.
When --json is used with the read command, Rich output is still written to stdout before the JSON (e.g., console.print() at line 171, connection_header() at line 176, console.status() at lines 173 and 178). This contaminates the JSON output when piping. Consider suppressing Rich output or redirecting it to stderr when as_json is true.
Summary
--jsonflag to theread,dump, andscancommands--jsonis passed, outputs structured JSON instead of Rich tablesjqor other toolsUsage
JSON Schema (read)
{ "host": "192.168.1.10", "type": "holding", "slave": 1, "registers": [ {"address": 40001, "raw": 237, "value": "237"} ] }Closes #1