diff --git a/README.rst b/README.rst index b321ff4..949db61 100644 --- a/README.rst +++ b/README.rst @@ -43,18 +43,19 @@ This is the available functionality: .. code:: - usage: inlinehashes [-h] [-a {sha256,sha384,sha512}] [-f] [-t {all,scripts,styles}] source + usage: inlinehashes [-h] [-a {sha256,sha384,sha512}] [-o {table,json,plain}] [-t {all,script-src,style-src}] source - positional arguments: + positional arguments: source URL or local HTML file to check - optional arguments: + options: -h, --help show this help message and exit -a {sha256,sha384,sha512}, --alg {sha256,sha384,sha512} - Hash algorithm to use (default: sha256) - -f, --full Include full content in the output - -t {all,scripts,styles}, --target {all,scripts,styles} - Target inline content to look for + Hash algorithm to use (default: sha256) + -o {table,json,plain}, --output {table,json,plain} + Format used to write the output (default: table) + -t {all,script-src,style-src}, --target {all,script-src,style-src} + Target inline content to look for (default: all) Here is an example of the output: diff --git a/inlinehashes/app.py b/inlinehashes/app.py index 368c3e4..6dc9096 100644 --- a/inlinehashes/app.py +++ b/inlinehashes/app.py @@ -10,17 +10,19 @@ from typing import List from urllib.error import URLError from urllib.request import Request, urlopen +from rich import box +from rich.console import Console +from rich.syntax import Syntax +from rich.table import Table + import inlinehashes -def build_output( - inlines: List[inlinehashes.lib.Inline], alg: str, full: bool = False -) -> str: +def build_json_output(inlines: List[inlinehashes.lib.Inline], alg: str) -> Syntax: """Build a JSON output from a list of Inline objects.""" - snippet = "content" if full else "short_content" out = [ { - "content": getattr(i, snippet), + "content": i.short_content, "hash": getattr(i, alg), "directive": i.directive, "line": i.line, @@ -28,7 +30,32 @@ def build_output( } for i in inlines ] - return json.dumps(out, indent=2) + return Syntax(json.dumps(out, indent=2), "JSON", theme="ansi_dark") + + +def build_plain_output(inlines: List[inlinehashes.lib.Inline], alg: str) -> str: + """Build a simple output of an inline per line.""" + return "\n".join( + [ + f"[magenta]{i.directive}[/magenta] [cyan]{i.line}[/cyan] " + f"[green]{i.position}[/green] [default]{getattr(i, alg)}[/default]" + for i in inlines + ] + ) + + +def build_table_output(inlines: List[inlinehashes.lib.Inline], alg: str) -> Table: + """Build a table to output the inlines in a nicer way.""" + table = Table(box=box.HORIZONTALS) + table.add_column("Directive", style="magenta") + table.add_column("Line", justify="right", style="cyan") + table.add_column("Position", justify="right", style="green") + table.add_column("Hash") + + for i in inlines: + table.add_row(i.directive, str(i.line), str(i.position), getattr(i, alg)) + + return table def run_cli() -> None: @@ -43,21 +70,24 @@ def run_cli() -> None: choices=["sha256", "sha384", "sha512"], ) parser.add_argument( - "-f", - "--full", - help="Include full content in the output", - action="store_true", + "-o", + "--output", + help="Format used to write the output (default: table)", + default="table", + choices=["table", "json", "plain"], ) parser.add_argument( "-t", "--target", - help="Target inline content to look for", + help="Target inline content to look for (default: all)", default="all", choices=["all", "script-src", "style-src"], ) args = parser.parse_args() path = args.source target = args.target + output_format = args.output + console = Console() try: if path.startswith("http://") or path.startswith("https://"): @@ -71,13 +101,19 @@ def run_cli() -> None: with open(path, "r") as f: content = f.read() except (URLError, OSError) as error: - print(error) - print(f"Failed to get source: {path}") + console.print(error) + console.print(f"Failed to get source: {path}") exit(1) inlines = inlinehashes.parse(content, target) - out = build_output(inlines, args.alg, bool(args.full)) - print(out) + if output_format == "json": + out = build_json_output(inlines, args.alg) + elif output_format == "plain": + out = build_plain_output(inlines, args.alg) + else: + out = build_table_output(inlines, args.alg) + + console.print(out) if __name__ == "__main__": diff --git a/poetry.lock b/poetry.lock index 0887b93..d3c8821 100644 --- a/poetry.lock +++ b/poetry.lock @@ -111,6 +111,43 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "markdown-it-py" +version = "2.1.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "markdown-it-py-2.1.0.tar.gz", hash = "sha256:cf7e59fed14b5ae17c0006eff14a2d9a00ed5f3a846148153899a0224e2c07da"}, + {file = "markdown_it_py-2.1.0-py3-none-any.whl", hash = "sha256:93de681e5c021a432c63147656fe21790bc01231e0cd2da73626f1aa3ac0fe27"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark (>=3.2,<4.0)"] +code-style = ["pre-commit (==2.6)"] +compare = ["commonmark (>=0.9.1,<0.10.0)", "markdown (>=3.3.6,<3.4.0)", "mistletoe (>=0.8.1,<0.9.0)", "mistune (>=2.0.2,<2.1.0)", "panflute (>=2.1.3,<2.2.0)"] +linkify = ["linkify-it-py (>=1.0,<2.0)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "mypy" version = "0.991" @@ -229,6 +266,21 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pygments" +version = "2.14.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, + {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, +] + +[package.extras] +plugins = ["importlib-metadata"] + [[package]] name = "pytest" version = "7.2.1" @@ -251,6 +303,25 @@ pluggy = ">=0.12,<2.0" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +[[package]] +name = "rich" +version = "13.2.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.2.0-py3-none-any.whl", hash = "sha256:7c963f0d03819221e9ac561e1bc866e3f95a02248c1234daa48954e6d381c003"}, + {file = "rich-13.2.0.tar.gz", hash = "sha256:f1a00cdd3eebf999a15d85ec498bfe0b1a77efe9b34f645768a54132ef444ac5"}, +] + +[package.dependencies] +markdown-it-py = ">=2.1.0,<3.0.0" +pygments = ">=2.6.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] + [[package]] name = "ruff" version = "0.0.229" @@ -304,4 +375,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "81f9d3306e76f9f7c39291bd7972216d852f39aba794b43afe2b9a0d4a7a2829" +content-hash = "ded8c6b34befe59dd43fd80d0a9027a54c17a97bc734445fb5978235f17cc0ca" diff --git a/pyproject.toml b/pyproject.toml index 9ccafb9..dc89a7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.11" beautifulsoup4 = "^4.10.0" +rich = "^13.2.0" [tool.poetry.dev-dependencies] pytest = "^7.2.1"