RlwrapFilter - Perl class for rlwrap filters
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) (<
https://github.com/hanslub42/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.
Filters can be combined in a pipeline using the special
pipeline filter.
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.
There is also a Python 3 module
rlwrapfilter.py, distributed together
with
rlwrap, that provides more or less the same API as its
perl
counterpart.
- $f = new RlwrapFilter
- $f = RlwrapFilter -> new(prompt_handler => sub
{"Hi! > "}, minimal_rlwrap_version => "0.35",
...)
- Return a new RlwrapFilter object.
Handlers are user-defined callbacks that specify one or more of an RlwrapFilter
object's handler methods (handle_input, handle_prompt) They get called from
the 'run' method in response to a message sent from
rlwrap. Messages
consist of a tag indicating which handler should be called (e.g. TAG_INPUT,
TAG_HISTORY) and the message text. Usually, a filter overrides only one or at
most two methods.
In many cases (e.g. TAG_INPUT, TAG_OUTPUT, TAG_PROMPT) the message text is a
simple string. Their handlers are called with the message text (i.e. the
un-filtered input, output, prompt) as their only argument. For convenience, $_
is set to the same value. They should return the re-written message text.
Some handlers (those for TAG_COMPLETION and TAG_HOTKEY) are a little more
complex: their message text (accessible via $_) is a tab-separated list of
fields; they get called with multiple arguments and are evaluated in list
context.
The message handlers are 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 (in
fact,
rlwrap knows when this is the case and won't even bother to send
the message)
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:
- $handler = $f -> prompt_handler, $f ->
prompt_handler(\&handler)
- The prompt handler re-writes prompts and gets called when
rlwrap decides it is time to "cook" the prompt, by default some
40 ms after the last output has arrived. Of course, rlwrap cannot
read the mind of command, so what looks like a prompt to
rlwrap may actually be the beginning of an output line that took
command a little longer to formulate. If this is a problem, specify
a longer "cooking" time with rlwrap's -w option, use the
prompts_are_never_empty method or "reject" the prompt
(cf. the prompt_rejected method)
- $handler = $f -> completion_handler, $f ->
completion_handler(\&handler)
- The completion handler gets called with three arguments:
the entire input line, the prefix (partial word to complete), and rlwrap's
own completion list. It should return a (possibly revised) list of
completions. As an example, suppose the user has typed "She played
for A<TAB>". The handler will be called like this:
myhandler("She played for A", "A", "Arsenal", "Arendal", "Anderlecht")
it could then return a list of stronger clubs: ("Ajax",
"AZ67", "Arnhem")
- $handler = $f -> history_handler, $f ->
history_handler(\&handler)
- Every input line is submitted to this handler, the return
value is put in rlwrap's history. Returning an empty or undefined value
will keep the input line out of the history.
- $handler = $f -> hotkey_handler, $f ->
hotkey_handler(\&handler)
- If, while editing an input line, the user presses a key
that is bound to "rlwrap_hotkey" in .inputrc, the handler
is called with five arguments: the hotkey, the prefix (i.e. the part of
the current input line before the cursor), the remaining part of the input
line (postfix), the history as one string ("line 1\nline 2\n...line
N", and the history position. It has to return a similar list, except
that the first element will be printed in the "echo area" if it
is changed from its original value.
Example: if the current input line is "pea soup" (with the
cursor on the space), and the user presses CTRL+P, which happens to be
bound to "rlwrap-hotkey" in .inputrc, the handler is
called like this:
my_handler("\0x10", "pea", " soup", "tomato soup\nasparagus..", 12) # 16 = CTRL-P
If you prefer peanut soup, the handler should return
("Mmmm!", "peanut", " soup", "asparagus..", 11)
after which the input line will be "peanut soup" (with the cursor
again on the space), the echo area will display "Mmmm!", and any
reference to inferior soups will have been purged from the history.
If the returned input line ends with a newline rlwrap will
immediately accept the result.
- $handler = $f -> input_handler, $f ->
input_handler(\&handler)
- Every input line (which may consist of multiple
\n-separated lines, when using bracketed paste) is submitted to this
handler, The handler's return value is written to command's pty
(pseudo-terminal).
- $handler = $f -> echo_handler, $f ->
echo_handler(\&handler)
- The first line of output that is read back from
command's pty is the echo'ed input line. If your input handler
alters the input line, it is the altered input that will be echo'ed back.
If you don't want to confuse the user, use an echo handler that returns
your original input.
If you use rlwrap in --multi-line mode, additional echo lines will have to
be handled by the output handler
- $handler = $f -> output_handler, $f ->
output_handler(\&handler)
- All command output after the echo line is submitted
to the output handler (including newlines). This handler may get called
many times in succession, dependent on the size of command's
write() calls, and the whims of your system's scheduler. Therefore
your handler should be prepared to rewrite your output in
"chunks", where you even don't have the guarantee that the
chunks contain entire unbroken lines.
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.
- $handler = $f -> signal_handler, $f ->
signal_handler(\&handler)
- As rlwrap is transparent to signals, signals get
passed on to command. This handler gets called (as handler($signo))
for signals SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGCONT, SIGUSR1, SIGUSR2,
and SIGWINCH, before the signal is delivered. It receives (and should
return) $signo as a string. The returned signal is delivered to
command; return "0" to ignore the signal altogether.
Output can be written out-of-band (to rlwrap) or cloak_and_dagger
(to command, see below)
- $handler = $f -> message_handler, $f ->
message_handler(\&handler)
- This handler gets called (as handler($message, $tag)) for
every incoming message, and every tag (including out-of-band tags), before
all other handlers. Its return value is ignored, but it may be useful for
logging and debugging purposes. The $tag is an integer that can be
converted to a tag name by the 'tag2name' method
- $f -> help_text("Usage...")
- Set the help text for this filter. It will be displayed by
rlwrap -z <filter>. The second line of the help text is used by
"rlwrap -z listing"; it should be a short description of what
the filter does.
- $f -> minimal_rlwrap_version("x.yy")
- Die unless rlwrap is version x.yy or newer
- $dir = $f -> cwd
- return the name of command's current working
directory. This uses the /proc filesystem, and may only work on newer
linux systems (on older linux and on Solaris, it will return something
like "/proc/12345/cwd", useful to find the contents of
command's working directory, but not its name)
- $text = $f -> cumulative_output
- return the current cumulative output. All (untreated)
output gets appended to the cumulative output after the output_handler has
been called. The cumulative output starts with a fresh slate with every
OUTPUT message that directly follows an INPUT message (ignoring
out-of-band messages and rejected prompts)
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.
- $tag = $f -> previous_tag
- The tag of the last preceding in-band message. A tag is an
integer between 0 and 255, its name can be found with the following
method:
- $name = $f -> tag2name($tag)
- Convert the tag (an integer) to its name (e.g.
"TAG_PROMPT")
- $name = $f -> name2tag($tag)
- Convert a valid tag name like "TAG_PROMPT" to a
tag (an integer)
- $f -> send_output_oob($text)
- Make rlwrap display $text. $text is sent
"out-of-band" : rlwrap will not see it until just after
it has sent the next message to the filter
- $f -> send_ignore_oob($text)
- Send an out-of-band TAG_IGNORE message to rlwrap.
rlwrap will silently discard it, but it can be useful when
debugging filters
- $f -> tweak_readline_oob($readline_function,
@parameters)
- Send a specially formatted out-of-band message in order to
tweak readline (i.e. to make rlwrap call a readline function
or set a readline variable). See the GNU Readline
documentation for details.
At this moment, the following tweaks are recognised:
$filter -> tweak_readline_oob("rl_variable_bind", $rl_variable_name, $value);
# ... only for bindable readline variables like those in .inputrc
$filter -> tweak_readline_oob("rl_completer_word_break_characters", $chars);
$filter -> tweak_readline_oob("rl_completer_quote_characters", $chars);
$filter -> tweak_readline_oob("rl_filename_completion_desired", "0" or "1");
The parameters should not contain "::" (two consecutive colons).
This method can be called at any moment, even before $filter ->
run
- $f -> add_to_completion_list(@words)
- $f -> remove_from_completion_list(@words)
- Permanently add or remove the words in @words to/from
rlwrap's completion list.
- $f -> cloak_and_dagger($question, $prompt,
$timeout);
- Send $question to command's input and read back
everything that comes back until $prompt is seen at
"end-of-chunk", or no new chunks arrive for $timeout seconds,
whichever comes first. Return the response (without the final $prompt).
rlwrap remains completely unaware of this conversation.
- $f -> cloak_and_dagger_verbose($verbosity)
- If $verbosity evaluates to a true value, make rlwrap print
all questions sent to command by the "cloak_and_dagger"
method, and command's responses. By default, $verbosity = 0;
setting it to 1 will mess up the screen but greatly facilitate the
(otherwise rather tricky) use of "cloak_and_dagger"
- $self -> prompt_rejected
- A special text ("_THIS_CANNOT_BE_A_PROMPT_") to
be returned by a prompt handler to "reject" the prompt. This
will make rlwrap skip cooking the prompt. $self->previous_tag and
$self->cumulative_output will not be touched.
- $text = $f -> prompts_are_never_empty($val)
- If $val evaluates to a true value, automatically reject
empty prompts.
- $f -> command_line
- In scalar context: the rlwrapped command and its arguments
as a string ("command -v blah") in list context: the same as a
list ("command", "-v", "blah")
- $f -> running_under_rlwrap
- Whether the filter is run by rlwrap, or directly
from the command line
- $f -> run
- Start an event loop that reads rlwrap's messages from the
input pipe, calls the appropriate handlers and writes the result to the
output pipe. This method never returns.
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_HOTKEY 5
TAG_SIGNAL 6
TAG_WHAT_ARE_YOUR_INTERESTS 127
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.
TAG_WHAT_ARE_YOUR_INTERESTS is only ever used internally, to prevent the
exchange of messages that won't be handled by the filter anyway. It will be
seen by the general message handler, and therefore show up (exactly once, at
program start) in the output of e.g. the
logger filter.
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 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 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 command's pty.
RLWRAP_BREAK_CHARS The characters rlwrap considers word-breaking (cf. the --break-chars option in rlwrap (1))
RLWRAP_DEBUG The value of the --debug (-d) option given to rlwrap
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. The message
will not contain the final newline) and it will respond in the same way on its
standard output. Of course,
rlwrap can help with the tedious typing of
tag names:
rlwrap -f tagnames filter_to_be_debugged
Because
rlwrap cannot put TABs and newlines in input lines, filters will
convert '\t' and '\n' into TAB and newline when run directly from the command
line.
rlwrap (1),
readline (3)