Conversation
…n a non-paged colored tty
…e-space: pre;'> wrapper; disable pagers in PTY (GIT_PAGER/PAGER=cat); refresh --help, docs, and man page; add tests
Thorsrud22
left a comment
There was a problem hiding this comment.
Thanks for this. Running a command inside a PTY is a genuinely useful addition, and the happy path works: ansi2html --partial -- printf '\033[31mRED\033[0m' gives <span class="ansi31">RED</span>. I checked the branch out and exercised it on macOS; here is what I found, roughly in order of severity.
Blocking
-
import ptyat module level breaksimport ansi2htmlon Windows.ptyimportstty, which doesfrom termios import *, andtermiosis Unix-only. Simulating a platform withouttermios(sys.modules['termios'] = None):mainimports fine, this branch raisesImportError. ansi2html is a library with ~2M downloads/month, so an import-time failure hits every consumer on Windows even if they never use the CLI. Fix is easy: importpty,selectandsubprocessinside the passthrough branch (or a helper function) so stdin mode keeps working everywhere. -
The "first non-option token starts the command" heuristic breaks every option that takes a separate value. On this branch, all of these fail with
expected one argumentwhile working onmain:ansi2html -s solarized --partial < in.txt ansi2html -t Foo --partial < in.txt ansi2html -f 12 --partial < in.txt ansi2html --input-encoding utf-8 --partial < in.txtOnly the
--scheme=solarizedform survives. Rather than re-implementing option parsing, let argparse do it: acommandpositional withnargs=argparse.REMAINDER(orparse_known_args) handles--and knows which options consume a value. Alternatively require the explicit--and drop the heuristic entirely. -
The branch conflicts with
main(19 commits behind, GitHub reportsCONFLICTING). The.pre-commit-config.yamlhunk is already onmainand should drop out on rebase.
Should fix
- Child exit status is discarded.
ansi2html sh -c 'exit 3'exits 0. Propagatingproc.returncodeviasys.exitwould make this usable in scripts. - A child that reads stdin hangs forever. The child's stdin is the PTY slave, and nothing ever writes to the master, so
ansi2html catnever returns.stdin=subprocess.DEVNULLseems right for an output-capture tool. --helpnow prints a doubled usage block.usage=main.__doc__renders asusage: \n Usage:\n ansi2html ..., and the examples appear again inepilog. Droppingusage=and keeping the examples only inepilogfixes both.- Non-ASCII hyphen in help text. "pseudo‑terminal" in the description (and README) uses U+2011, which can raise on a non-UTF-8 stdout. A plain
-is safer. LESS=-Ris set but not documented anywhere (PR body,--help, man page). Either document it alongsideGIT_PAGER/PAGERor drop it.
Tests
tests/helpers.pypops everyansi2html*module fromsys.modulesand prependssrc/tosys.path. tox already installs the package in develop mode, so this isn't needed, and it can make two different module objects coexist so a patch on one doesn't affect the other.from ansi2html import converteris enough. Thescriptparameter ofrun_with_fake_ptyis also unused.- The fake PTY is an
os.pipe, so the actualpty.openpty+select+ EIO/EOF loop, which is the risky part and differs between Linux and macOS, is never exercised. One real-PTY test (skipped on non-Unix) would cover it.
Scope suggestion
--standalone/-S and the man-page entries for the existing -W, -L, -s, -t options are independent of PTY passthrough and look ready. Splitting them into a small separate PR would let them land now regardless of how the passthrough discussion goes.
Happy to re-test once it's rebased.
| import argparse | ||
| import io | ||
| import os | ||
| import pty |
There was a problem hiding this comment.
pty -> tty -> from termios import *, and termios doesn't exist on Windows, so this makes import ansi2html itself fail there (verified by blocking termios in sys.modules: main imports, this branch raises ImportError). Suggest importing pty, select and subprocess inside the passthrough branch so stdin mode keeps working on every platform.
| # Treat the first non-option token as the start of the command | ||
| split_at = None | ||
| for i, tok in enumerate(argv): | ||
| if not tok.startswith("-"): |
There was a problem hiding this comment.
This treats the value of any option as the start of the command. On this branch ansi2html -s solarized --partial < in.txt fails with argument -s/--scheme: expected one argument (same for -t, -f, --input-encoding); all work on main. Letting argparse own this, e.g. a command positional with nargs=argparse.REMAINDER, avoids re-implementing option parsing and still supports --.
| " to avoid hanging pagers; set them explicitly to override." | ||
| ), | ||
| formatter_class=argparse.RawTextHelpFormatter, | ||
| usage=main.__doc__, |
There was a problem hiding this comment.
With usage= set to the docstring, --help now prints usage: followed by a second Usage: heading, and the examples repeat in epilog. Dropping usage= and keeping examples only in epilog gives a clean help page.
| parser = argparse.ArgumentParser( | ||
| description=( | ||
| "Convert text with ANSI color codes to HTML/LaTeX.\n\n" | ||
| "Can also run a command inside a PTY (pseudo‑terminal) and convert\n" |
There was a problem hiding this comment.
The hyphen in "pseudo‑terminal" is U+2011 (non-breaking hyphen), here and in the README. A plain ASCII - avoids encoding errors on a non-UTF-8 stdout.
| env.setdefault("GIT_PAGER", "cat") | ||
| env.setdefault("PAGER", "cat") | ||
| # Keep color passthrough if a pager is still used for some reason | ||
| env.setdefault("LESS", "-R") |
There was a problem hiding this comment.
LESS=-R isn't mentioned in the PR description, --help, or the man page, unlike GIT_PAGER/PAGER. Either document it with the others or drop it.
| try: | ||
| with subprocess.Popen( | ||
| cmd_args, | ||
| stdin=slave_fd, |
There was a problem hiding this comment.
With stdin attached to the PTY slave and nothing writing to the master, a child that reads stdin blocks forever: ansi2html cat never returns. stdin=subprocess.DEVNULL seems like the right default for an output-capture tool.
| except OSError: | ||
| pass | ||
|
|
||
| ansi_text = b"".join(chunks).decode(opts.input_encoding, "replace") |
There was a problem hiding this comment.
The child's exit status is dropped here: ansi2html sh -c 'exit 3' exits 0. Propagating proc.returncode (e.g. sys.exit(proc.returncode) after printing) would make this usable in scripts and CI.
| from unittest.mock import patch | ||
|
|
||
|
|
||
| def import_local_converter() -> Any: |
There was a problem hiding this comment.
tox runs with usedevelop = True, so the installed ansi2html already is src/. Popping modules from sys.modules and re-importing can leave two distinct ansi2html.converter module objects alive, so a patch applied to one won't be seen by code holding the other. A plain from ansi2html import converter should be enough.
| return importlib.import_module("ansi2html.converter") | ||
|
|
||
|
|
||
| def run_with_fake_pty(conv: Any, argv: Iterable[str], script: str) -> None: |
CLI passthrough (PTY), standalone snippets, and pager safeguards
Summary
--standalone(-S) to emit a minimal inline HTML snippet wrapped in<code>.GIT_PAGER=cat,PAGER=cat) to prevent hangs; users can override.--help, README/docs, and man page; add targeted tests.Why
Capturing realistic colorized output from CLIs (e.g.,
git) usually requires a TTY. This PR makes it easy to run such commands, capture their ANSI-colored output, and convert it to HTMLreliably—without getting stuck behind pagers.
What’s Changed
TERM=xterm-256color.--standalone(-S): like--inlinebut wrapped in<code style='white-space: pre;'>…</code>for convenient embedding.GIT_PAGER=catandPAGER=catby default to avoid hangs.Usage
Run a command and convert its colored output:
ansi2html git log -p > git-log.htmlSeparate ansi2html options from the child command:
ansi2html --inline -- git log -p > inline-git-log.html
Produce a minimal embeddable snippet:
echo $'\e[31mRED\e[0m' | ansi2html --standalone
short form:
echo $'\e[31mRED\e[0m' | ansi2html -S
Behavior Notes
Docs & Tests
Backwards Compatibility
Commits
Checklist