Simon Tatham's pty filtering tools

This page describes a collection of Unix tools for filtering the input and output of a shell session. Written by Simon Tatham.

This is one of my unfinished, and not very polished, pieces of software. Documentation is sparse; the collection of tools is quite eclectic and random and not all of them will be useful to anyone else; too many of the tools have too-generic names which wouldn't go well in the /usr/bin of a serious distro. At the moment I'm mostly considering it to be symbiosisware. But I put it up on the web anyway in case it's useful to anyone: either the specific filtering tools I've already written, or the framework that makes it easy (ish) to write extra ones.

General idea

There are many tools like these

There already exist Unix tools that run within a terminal session, and wrap a program in a second pseudo-terminal. Some examples written by other people include the standard script command (records your session output to a log file); ttyrec (the same, but with timestamps, so you can replay it in real time using a tool such as IPBT); and luit (translates character sets, so that you can run a program expecting one character set in a terminal that speaks another).

But these ones are mine

This is my own personal collection of pty filtering tools of this kind. I wrote the general framework around 2002, making it easy to replace just the part that processes the input and/or output. I've been adding tools to the collection ever since, whenever I find a thing that would be useful.

Some of them replicate the functionality of other people's tools. All the tools I mention above have equivalents in this collection, because I very slightly prefer my versions for my own use (and because once I had the framework it doesn't need a lot of effort to write another tool). That's one of the ways in which this collection is symbiosisware. But others are more unusual.

Common features

All these tools have a set of common features. By default each one makes a pseudo-terminal and runs a shell in it. You can use it to wrap a command other than a shell by writing -- on the command line followed by the command you want to run; there are common options to swap out the pty for a pair of pipes, or even an AF_UNIX socketpair.

Some tools have additional options of their own to control their filtering behaviour, but all of them support these standard ones.

The filtering tools

nullfilter

The very simplest tool of all: don't do anything to the input or the output, just pass it all through unmodified.

This sounds pointless, but I find it useful surprisingly often! Some example uses:

noinput

Almost as simple: throw away all terminal input. I've used this when giving demos of a terminal-based application to an in-person audience, to avoid the audience accidentally (or on purpose) interrupting the demo by prodding the keyboard.

(Of course, that meant I couldn't interact with the demo myself either. But that's OK, because it was a ttyrec recorded ahead of time, and played on loop by IPBT.)

noprogress

Deletes any terminal output that ends in a carriage-return character without a following line feed. The idea is that some tools (the build tool ninja, in particular) will print interim progress reports by sending some text and then a CR, to move the cursor back to the same line, so that each progress report overwrites the previous one. Those progress reports are removed by this filter, so that you only get ninja's "permanent" output – the stuff that persists on the screen after it finishes running.

I mentioned above that one of my uses for nullfilter is to run something colourful inside it and then pipe to less -R. One common example of "something colourful" for me is a build tool like ninja which in turn runs compile commands that produce coloured error messages. But nullfilter -- ninja | less -R looks ugly because of those overprinted progress reports. Solution: use this filter instead, noprogress -- ninja | less -R.

recolour

Intercepts SGR escape sequences in the terminal output – the ones that turn on and off terminal attributes like bold and underline, and also select the foreground and background colour. Then rewrite the set of attributes, and re-emit a different SGR sequence.

In principle, this could be used for lots of special effects: converting between bold and brighter colours, getting rid of blinking text if you find it annoying, etc. My actual aim is to use it for remapping the foreground and background colours to improve readability, especially for people with anomalous or impaired colour vision who find that the more usual tools for that (such as terminal programs that enforce a minimum contrast) aren't suited to their own vision.

As of 2025-05-10 this is new and experimental, and the only way to choose what remapping rules you want is to edit the source code and recompile. If you run it unmodified, you get a demonstration remapping which just permutes the red, green and blue channels, to prove it's doing something.

(I don't yet know if this tool will have long-term usefulness, or if it will only be useful for initial experimentation and then some useful colour remapping rules will end up being done some other way. So I haven't put in any serious effort to allow user-defined remapping rules without recompilation, like embedding a language interpreter or a DSL or having a plugin system, or even a set of built-in remapping options. I'm waiting to see what if anything might be needed.)

Note that by default this tool works by converting all colour-setting control sequences into ones that specify 24-bit RGB colour. So if your terminal doesn't support 24-bit colour, then it won't work at all. Apparently this is true for the default Mac Terminal.app (at least, as of version 2.14 on macOS 15.4.1); iTerm works better.

asciify

Filter all non-ASCII characters out of the terminal input. There's one particular very old application I interact with which will outright crash if it receives any input byte with the high bit set.

This tool tries to translate some characters into good ASCII approximations. In particular, Unicode typographical quotes and dashes are converted back into ASCII ones, so if you paste English text out of a nicely typographed web page into your terminal, it will come out with a sensible ASCII rendition of the same punctuation. The range of Unicode characters handled nicely in this way is quite small, and anything too difficult will just generate a fallback such as <U+1234>. But that's still good enough to fulfill the primary purpose of the tool: not crashing the wrapped application.

nhfilter

Convert input sequences of the form ESC x (which PuTTY will generate if you press Alt+x, for any letter key x) into the byte value that is the character 'x' with the high bit set.

I used to use this for playing Nethack, which expects that high-bit-set representation of Alt+letter keystrokes. PuTTY doesn't have a mode built in to make Alt set the high bit, because it's so difficult to work out what should happen in all the hard cases, and I was sure users would complain about edge cases. (What if the character value has bit 7 set already? What if it's a Unicode code point above 255?) But this tool has a much more limited scope: it's only for making Nethack work properly, so I know it only needs to handle that small set of sequences.

idlewrapper

Terminate the subprocess and exit if there's been no input or output for a specified length of time.

deidle

Prevent a session from being disconnected by idlewrapper or anything like it, by manufacturing extra input periodically if there hasn't been any other input. You'd probably only use this to wrap an application for which there's a keystroke you know is always safe to send.

parity

Strip the high bit off all terminal output, in the expectation that it was a serial-line parity scheme that something failed to take off already before writing text to the terminal. Also, optionally, put a parity bit back on to terminal input.

I can't even remember what I wanted this for in the first place. I wrote it as recently as 2016, and I assume serial ports must have been involved somewhere, but I have no idea what I actually needed to fix in this area. The commit message doesn't remind me what application I was wrapping!

record

My own tool similar to both script and ttyrec: it records a copy of the terminal output to a file, in a format of your choice: plain text (like script), or the ttyrec format that includes timestamps, or the similar nh-recorder format.

I wrote my own one of these instead of just using script and/or ttyrec for several reasons:

csfilter

My own tool similar to luit: you tell it what character set the outer terminal speaks (default UTF-8) and what character set you want the inner terminal to work in, and it does character-set translation in both directions. So if you have an old-fashioned program that expects some single-byte charset like ISO 8859-1 or even CP437, this kind of thing is the easiest way to get it to work, because you can wrap just that application instead of reconfiguring your whole terminal.

My version of this tool uses my own character-set translation library (a spinoff of PuTTY's), which supports a few lesser-known character sets that I've occasionally wanted to use. Also I find it convenient that the outer and inner character sets are specified by name on the command line, instead of luit's approach of deducing them from environment variables like LC_CTYPE. These aren't very good reasons to do a huge amount of work writing a new version of the same program from scratch – but since I already had this filter framework and the character set library, it wasn't a huge amount of work to glue them together, and seemed just about worth it to me.

Bonus tool: linktty

There's one other tool built from the same code base as all of these filters. It's different from the rest, because it's not a filter that runs a subprocess in a pty. It just reuses some of the infrastructure of the filters to do a different job.

linktty creates two pseudo-terminals, puts them into raw mode, and copies data between them in both directions. So if two unrelated processes each opens one of the two ptys for both reading and writing, then they can talk to each other, as if they were speaking via a serial line.

I use this for communicating between gdb and gdbserver, when I need to debug a full-screen terminal program (so that it's convenient to run the debugger in a separate terminal from the program itself). You tell gdbserver to use one of the ptys, and gdb to use the other, and each of them behaves as if it were doing serial-port debugging to a remote machine, but really they're talking to each other.

(The usual way people do this is by making gdbserver listen on a network port, and telling gdb to connect to it. But this is a huge security hazard, because the network connection has no authentication, so anyone else can connect to the same port and then tell gdbserver to run arbitrary code under your user id! For debugging on the same machine you can restrict the network socket to localhost, but that still doesn't take account of the fact that Unix machines are multi-user. An AF_UNIX socket would be the right way to fix this, but as far as I know gdb doesn't support that. This security issue is known – even documented in the gdb manual – but that seems to be all they've done about it, because they just say "don't run this while connected to any public network", which rather overlooks the fact that these days pretty much everything is connected to a public network one way or another.)

For convenience, linktty creates a symlink to each terminal, so that you can give each of gdb and gdbserver a fixed pathname, even if the names of the actual ptys differ between runs. For example, you might run commands like these (in three separate shell sessions):

linktty /home/username/tty-for-gdb /home/username/tty-for-gdbserver
gdbserver /home/username/tty-for-gdbserver program-to-debug [args]
gdb -ex 'target extended-remote /home/username/tty-for-gdb'

and then you don't have to worry about the real names of the pty devices, you just use the symlinks. The next time you run these commands, linktty will probably get a different pair of ptys, and none of these processes will care.

Where to find them

These tools have a public git repository, but no release archives (or numbered releases at all). To download the code, run

git clone https://git.tartarus.org/simon/filter.git

Alternatively, you can browse the repository on the web.

If you want to use this setup to write a filter of your own, the easiest thing to do is to make a copy of nulltrans.c, and start from there. That source file contains comments describing the API expected by the framework, and you can look at all the existing filter programs for examples of each feature in use.

Feedback

I'm not promising much maintenance of this set of tools, but if you do find a bug, you're welcome to report it to anakin@pobox.com.

You might find it helpful to read this article before reporting a bug.


(comments to anakin@pobox.com)
(thanks to chiark for hosting this page)
(last modified on Tue May 13 16:09:56 2025)