diff --git a/README.rst b/README.rst index f706b85..34d47b2 100644 --- a/README.rst +++ b/README.rst @@ -40,6 +40,36 @@ Requirements Usage ===== +CLI Tool +-------- + +py-multiaddr provides a command line interface to inspect multiaddrs and decode them into structured JSON. + +.. code-block:: bash + + $ multiaddr /ip4/1.2.3.4/tcp/80 + { + "string": "/ip4/1.2.3.4/tcp/80", + "packed": "0x0401020304060050", + "packedSize": 8, + "components": [ + { + "protocol": "ip4", + "code": 4, + "value": "1.2.3.4", + "rawValue": "0x01020304" + }, + { + "protocol": "tcp", + "code": 6, + "value": "80", + "rawValue": "0x0050" + } + ] + } + +You can also pass a hex-encoded multiaddr prefixed with `0x`, e.g. `multiaddr 0x0401020304060050`. Use the `-c` or `--compact` flag to output on a single line. + Simple ------ diff --git a/multiaddr/cli.py b/multiaddr/cli.py new file mode 100644 index 0000000..c647250 --- /dev/null +++ b/multiaddr/cli.py @@ -0,0 +1,63 @@ +import argparse +import json +import sys +from typing import Any + +from . import __version__ +from .multiaddr import Multiaddr +from .transforms import bytes_iter + + +def main() -> None: + parser = argparse.ArgumentParser(description="Inspect multiaddrs") + parser.add_argument("addr", help="Multiaddr string or hex bytes (0x...)") + parser.add_argument("-c", "--compact", action="store_true", help="Output compact JSON") + parser.add_argument( + "--version", action="version", version=f"%(prog)s {__version__}" + ) + + args = parser.parse_args() + + try: + if args.addr.startswith("0x"): + addr_bytes = bytes.fromhex(args.addr[2:]) + addr = Multiaddr(addr_bytes) + else: + addr = Multiaddr(args.addr) + except Exception as e: + print(f"Error parsing multiaddr: {e}", file=sys.stderr) + sys.exit(1) + + try: + components = [] + for _, proto, codec, value_bytes in bytes_iter(addr.to_bytes()): + component: dict[str, Any] = { + "protocol": proto.name, + "code": proto.code, + } + if codec.SIZE != 0: + component["value"] = codec.to_string(proto, value_bytes) + component["rawValue"] = "0x" + value_bytes.hex() + else: + component["value"] = None + component["rawValue"] = None + components.append(component) + + except Exception as e: + print(f"Error decoding multiaddr components: {e}", file=sys.stderr) + sys.exit(1) + + indent = None if args.compact else 2 + + output = { + "string": str(addr), + "packed": "0x" + addr.to_bytes().hex(), + "packedSize": len(addr.to_bytes()), + "components": components, + } + + print(json.dumps(output, indent=indent)) + + +if __name__ == "__main__": + main() diff --git a/newsfragments/119.feature b/newsfragments/119.feature new file mode 100644 index 0000000..72f986b --- /dev/null +++ b/newsfragments/119.feature @@ -0,0 +1 @@ +Add a command-line interface (CLI) to decode multiaddrs into structured JSON. diff --git a/pyproject.toml b/pyproject.toml index c1e8143..81a184c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,9 @@ dependencies = [ Homepage = "https://github.com/multiformats/py-multiaddr" Download = "https://github.com/multiformats/py-multiaddr/tarball/0.2.0" +[project.scripts] +multiaddr = "multiaddr.cli:main" + [project.optional-dependencies] dev = [ "Sphinx>=5.0.0", diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..34d3640 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,59 @@ +import json +import sys +from unittest.mock import patch + +import pytest + +from multiaddr import cli + + +def test_cli_string(capsys): + with patch.object(sys, "argv", ["multiaddr", "/ip4/1.2.3.4/tcp/80"]): + cli.main() + + out, _ = capsys.readouterr() + data = json.loads(out) + + assert data["string"] == "/ip4/1.2.3.4/tcp/80" + assert data["packed"] == "0x0401020304060050" + assert data["packedSize"] == 8 + assert len(data["components"]) == 2 + + assert data["components"][0]["protocol"] == "ip4" + assert data["components"][0]["value"] == "1.2.3.4" + assert data["components"][0]["rawValue"] == "0x01020304" + + assert data["components"][1]["protocol"] == "tcp" + assert data["components"][1]["value"] == "80" + assert data["components"][1]["rawValue"] == "0x0050" + + +def test_cli_hex(capsys): + with patch.object(sys, "argv", ["multiaddr", "0x0401020304060050"]): + cli.main() + + out, _ = capsys.readouterr() + data = json.loads(out) + + assert data["string"] == "/ip4/1.2.3.4/tcp/80" + assert data["packed"] == "0x0401020304060050" + + +def test_cli_compact(capsys): + with patch.object(sys, "argv", ["multiaddr", "-c", "/ip4/1.2.3.4"]): + cli.main() + + out, _ = capsys.readouterr() + assert "\n" not in out.strip() + data = json.loads(out) + assert data["string"] == "/ip4/1.2.3.4" + + +def test_cli_invalid_input(capsys): + with patch.object(sys, "argv", ["multiaddr", "/invalid/input"]): + with pytest.raises(SystemExit) as e: + cli.main() + assert e.value.code == 1 + + _, err = capsys.readouterr() + assert "Error parsing multiaddr:" in err