use lib $ENV{RLWRAP_FILTERDIR};
use RlwrapFilter;
$filter = new RlwrapFilter;
$filter -> output_handler(sub {s/apple/orange/; $_}); # re-write output
$filter -> prompt_handler(\&pimp_the_prompt); # change prompt
$filter -> history_handler(sub {s/with password \w+/with password ****/; $_}); # keep passwords out of history
$filter -> run;
rlwrap (1) (<http://utopia.knoware.nl/~hlub/uck/rlwrap>) is a tiny utility that sits between the user and any console command, in order to bestow readline capabilities (line editing, history recall) to commands that don't have them.
Since version 0.32, rlwrap can use filters to script almost every aspect of rlwrap's interaction with the user: changing the history, re-writing output and input, calling a pager or computing completion word lists from the current input.
RlwrapFilter makes it very simple to write rlwrap filters in perl. A filter only needs to instantiate a RlwrapFilter object, change a few of its default handlers and then call its 'run' method.
Handlers are user-defined callbacks that get called from the 'run' method with a message (i.e. the un-filtered input, output, prompt) as their first argument. For convenience, $_ is set to the same value. They should return the re-written message text. They get called in a fixed cyclic order: prompt, completion, history, input, echo, output, prompt, ... etc ad infinitum. Rlwrap may always skip a handler when in direct mode, on the other hand, completion and output handlers may get called more than once in succession. If a handler is left undefined, the result is as if the message text were returned unaltered.
It is important to note that the filter, and hence all its handlers, are bypassed when command is in direct mode, i.e. when it asks for single keystrokes (and also, for security reasons, when it doesn't echo, e.g. when asking for a password). If you don't want this to happen, use rlwrap -a to force rlwrap to remain in readline mode and to apply the filter to all of command's in- and output. This will make editors and pagers (which respond to single keystrokes) unusable, unless you use rlwrap's -N option (linux only)
The getters/setters for the respective handlers are listed below:
myhandler("She played for A", "A", "Arsenal", "Arendal", "Anderlecht")
it could then return a list of stronger clubs: ("Ajax", "AZ67", "Arnhem")
If you use rlwrap in --multi-line mode, additional echo lines will have to be handled by the output handler
If you want to handle command's entire output in one go, you can specify an output handler that returns an empty string, and then use $filter -> cumulative_output in your prompt handler to send the re-written output "out-of-band" just before the prompt:
$filter -> output_handler(sub {""});
$filter -> prompt_handler(
sub{ $filter -> send_output_oob(mysub($filter -> cumulative_output));
"Hi there > "
});
Note that when rlwrap is run in --multi-line mode the echo handler will still only handle the first echo line. The remainder will generally be echoed back preceded by a continuation prompt; it is up to the output handler what to do with it.
When necessary (i.e. when rlwrap is in "impatient mode") the prompt is removed from $filter->cumulative_output by the time the prompt handler is called.
rlwrap communicates with a filter through messages consisting of a tag byte (TAG_OUTPUT, TAG_PROMPT etc. - to inform the filter of what is being sent), an unsigned 32-bit integer containing the length of the message, the message text and an extra newline. For every message sent, rlwrap expects, and waits for an answer message with the same tag. Sending back a different (in-band) tag is an error and instantly kills rlwrap, though filters may precede their answer message with "out-of-band" messages to output text (TAG_OUTPUT_OUT_OF_BAND), report errors (TAG_ERROR), and to manipulate the completion word list (TAG_ADD_TO_COMPLETION_LIST and TAG_REMOVE_FROM_COMPLETION_LIST) Out-of-band messages are not serviced by rlwrap until right after it has sent the next in-band message - the communication with the filter is synchronous and driven by rlwrap.
Messages are received and sent via two pipes. STDIN, STDOUT and STDERR are still connected to the user's terminal, and you can read and write them directly, though this may mess up the screen and confuse the user unless you are careful. A filter can even communicate with the rlwrapped command behind rlwrap's back (cf the cloak_and_dagger() method)
The protocol uses the following tags (tags > 128 are out-of-band)
TAG_INPUT 0 TAG_OUTPUT 1 TAG_HISTORY 2 TAG_COMPLETION 3 TAG_PROMPT 4 TAG_IGNORE 251 TAG_ADD_TO_COMPLETION_LIST 252 TAG_REMOVE_FROM_COMPLETION_LIST 253 TAG_OUTPUT_OUT_OF_BAND 254 TAG_ERROR 255
To see how this works, you can eavesdrop on the protocol using the 'logger' filter.
The constants TAG_INPUT, ... are exported by the RlwrapFilter.pm module.
As STDIN is still connected to the users teminal, one might expect the filter to receive SIGINT, SIGTERM, SIGTSTP directly from the terminal driver if the user presses CTRL-C, CTRL-Z etc Normally, we don't want this - it would confuse rlwrap, and the user (who thinks she is talking straight to the rlwapped command) probably meant those signals to be sent to the command itself. For this reason the filter starts with all signals blocked.
Filters that interact with the users terminal (e.g. to run a pager) should unblock signals like SIGTERM, SIGWINCH.
The filter is started by rlwrap after command, and stays alive as long as rlwrap runs. Filter methods are immediately usable. When command exits, the filter stays around for a little longer in order to process command's last words. As calling the cwd and cloak_and_dagger methods at that time will make the filter die with an error, it may be advisable to wrap those calls in eval{}
If a filter calls die() it will send an (out-of-band) TAG_ERROR message to rlwrap before exiting. rlwrap will then report the message and exit (just after its next in-band message - out-of-band messages are not always processed immediately)
die() within an eval() sets $@ as usual.
Before calling a filter, rlwrap sets the following environment variables:
RLWRAP_FILTERDIR directory where RlwrapFilter.pm and most filters live (set by B<rlwrap>, can be
overridden by the user before calling rlwrap)
PATH rlwrap automatically adds $RLWRAP_FILTERDIR to the front of filter's PATH
RLWRAP_VERSION rlwrap version (e.g. "0.35")
RLWRAP_COMMAND_PID process ID of the rlwrapped command
RLWRAP_COMMAND_LINE command line of the rlwrapped command
RLWRAP_IMPATIENT whether rlwrap is in "impatient mode" (cf B<rlwrap (1)>). In impatient mode,
the candidate prompt is filtered through the output handler (and displayed before
being overwritten by the cooked prompt).
RLWRAP_INPUT_PIPE_FD File descriptor of input pipe. For internal use only
RLWRAP_OUTPUT_PIPE_FD File descriptor of output pipe. For internal use only
RLWRAP_MASTER_PTY_FD File descriptor of I<command>'s pty.
While RlwrapFilter.pm makes it easy to write simple filters, debugging them can be a problem. A couple of useful tricks:
When running a filter, the in- and outgoing messages can be logged by the logger filter, using a pipeline:
rlwrap -z 'pipeline logger incoming : my_filter : logger outgoing' command
When called by rlwrap, filters get their input from $RLWRAP_INPUT_PIPE_FD and write their output to $RLWRAP_OUTPUT_PIPE_FD, and expect and write messages consisting of a tag byte, a 32-bit length and the message proper. This is not terribly useful when running a filter directly from the command line (outside rlwrap), even if we set the RLWRAP_*_FD ourselves.
Therefore, when run directly from the command line, a filter expects input messages on its standard input of the form
TAG_PROMPT myprompt >
(i.a. a tag name, one space and a message followed by a newline) and it will respond in the same way on its standard output
rlwrap (1), readline (3)