bpx fakes a zsh-like hook function system for interactive bash shells. It works with the bind builtin command to modify the readline buffer. See Examples and GIFS.
bpx works best in bash >=4.4 (PS0) and has been tested with the emacs line editing mode in an interactive instance, that was not running in an Emacs shell buffer. Please let me know, how to do it with the vi mode (which is, btw, also the POSIX line editing mode). bash must run without its --noediting option, of course.
Feel free to open an issue or put in a pull request on https://github.com/D630/bpx. See also the comments in the script.
bpx suppresses bash from using the secondary prompt, when more input is needed to complete the command line. Instead, bpx runs its own emulation of Readline's edit-and-execute-command. In order to use your preferred editor, modify the function __bpx_edit, that is:
function __bpx_edit {
command vim -f \
'+set ft=sh' \
"+call cursor(1,$READLINE_POINT+1)" \
"${1?}" < /dev/tty > /dev/tty;
};
To download the very latest source code:
git clone https://github.com/D630/bpx
In order to use the latest tagged version, do also something like this:
cd -- ./bpx
git checkout $(git describe --abbrev=0 --tags)
Just put the shell procedure/script bpx.bash elsewhere on your PATH and
then execute it with .
or source
in your configuraton file for interactive
bash sessions (usually .bashrc).
Bash is able to handle signals and other conditions (trap), to invoke the function command_not_found_handle, and to expand special variables at special times (prompting). But it has no "real" hook mechanism like you can see working in tcsh's special aliases, in zsh's hook functions or fish's event handling.
Because of that, the usual way to emulate zsh-like preexec and precmd hook functions is to set a trap on DEBUG and a special value in PROMPT_COMMAND respectively. But the DEBUG trap has many pitfalls, and it aims badly the real bahviour of zsh: preexec functions in zsh are executed after the command line (aka. command list or parse tree) has been read and is about to be executed; they are not beeing executed before each command/pipeline execution of the list. Actually, the second and third parameters of its functions don't even obtain the whole expansion; alias expansion in command and process substitutions, for example, is not performed. That is: one command line, one zsh like preexec hook!
The variable PS0, that was introcuced in bash 4.4, can be a replacement for the DEBUG trap, if the user is satisfied with running commands in command subtitution (a corresponding PRE_PROMPT_COMMAND is missing). The alternative (but still hacky) way used in bpx is to involve Readline by using the bind builtin command to work with the readline buffer. The preexec hook is then plugged into a wrapper around the Readline command accept-line. As a by-product, we are able to define some additional hooks:
Order | Hook | Method | Description |
---|---|---|---|
1 | preread | bind | Executed in a "keyseq:shell-command" binding before the readline-function accept-line is invoked. READLINE_{LINE,POINT} may be modified |
2 | preexec | bind | Executed in a "keyseq:shell-command" binding before the readline-function accept-line is invoked. READLINE_{LINE,POINT} may not be read and modified anymore |
3 | debug | trap | Works with DEBUG and is executed for every command in the command list; BASH_COMMAND available (not recommended) |
4 | prompt | variable | Works with the PROMPT_COMMAND variable and is executed before each prompting of the primary prompt (PS1) |
5 | postread | bind | Executed in a "keyseq:shell-command" binding after the readline-function accept-line has been invoked. READLINE_{LINE,POINT} of the next command line may be modified |
Similar to zsh, those hook functions execute function names in an array, which
has the same name as the hook + "_functions" appended: preread_functions=(foo bar)
. bpx takes care to redirect to stderr and to pass the correct status
codes to them ($?). Your settings to history expansion will also be
respected.
bpx sets some global variables for internal purposes:
Variable | Description |
---|---|
bpx_var | Integer attributed indexed array variable |
"[0] | Is one when command line isn't valid |
"[1] | Is one when complete command line has been read |
"[2] | Number of BASH_COMMAND in a command line (debug) |
"[3] | Last status code |
As part of the wrapper, bpx binds a number of key sequences
(__bpx_set_binds), which will be running after entering the key
sequence \C-x\C-x1
. Simply use this key or bind it to a key or key sequence
of your choice:
bind 'C-j: "\C-x\C-x1"'
Then make sure bpx has sane internal variables, when the next hook takes place:
# set bpx_var to 0
PS1='${_[ bpx_var=0, 1 ]}\u@\h \w \$ '
Before preread, preexec, and postread hook functions are about to be executed, the wrapper provides some functions and variables for them:
Name | Object | Description |
---|---|---|
__bpx_command_line | Normal function | The body contains the full command line with aliases expanded outside of command and process substitutions |
__bpx_set_rl1 | Normal function | Assigns to rl1 |
__bpx_set_rl2 | Normal function | Assigns to rl2; implies __bpx_set_rl1 |
__bpx_read_again | Normal function | Forces rereading and editing of the command line from start |
__bpx_read_abort | Normal function | Stops the editing and execution of the current command line |
__bpx_read_accept | Normal function | Undoes _bpx_read{again,abort} |
rl0 | Normal scalar variable | Holds the the full command line without alias expanded |
rl1 | Normal indexed array | Contains the body of __bpx_command_line |
rl2 | Normal indexed array | Contains the body of __bpx_command_line, but each index only points to one word (+ operator) |
Thats is, if the typed command is
$ for i in 1; do ls; done;
then
$ printf '%s\n' "$rl0"
for i in 1; do ls; done;
$ declare -fp __bpx_command_line
__bpx_command_line ()
{
for i in 1;
do
/bin/ls -h --color=auto;
done
}
$ __bpx_set_rl2
$ printf '%s\n' "${rl1[@]}"
for i in 1;
do
/bin/ls -h --color=auto;
done
$ printf '%s\n' "${rl2[@]}"
for
i
in
1;
do
/bin/ls
-h
--color=auto;
done
If you really desire debug, set the DEBUG trap like trap __bpx_hook_debug DEBUG
. Then play around with the following settings
shopt -s extdebug
set +o functrace
set +o errtrace
and reset bpx_var[2] to zero
PS1='${_[ bpx_var[2]=0, 1 ]}\u@\h \w \$ '
If command history is active, the parameter histcmd holds the output of
HISTTIMEFORMAT= history 1
with the history number removed, otherwise it's the
empty string.
prompt can be used, for example, like so: PROMPT_COMMAND=__bpx_hook_prompt
.
If you define PROMPT_COMMAND in a different way, make sure prompt functions
have access to the ? parameter to work properly. Everything else belonging to
PROMPT_COMMAND can be executed by doing:
function my_stuff {
...
}
prompt_functions=(my_stuff)
If you started the session with debug and preread/preexec/postread and you want to disable preread/preexec/postread in the same session, do this: rebind/unbind your key or key sequence if wanted and unset bpx_var[3] like
unset -v bpx_var[3]
The test configuration makes use of all hook mechanisms and extdebug. Run bash like
env -i \
HOME=$HOME \
INPUTRC=/dev/null \
TERM=$TERM \
HISTFILE=/tmp/bash_history~ \
bash --rcfile bpx.bash -i
and execute the test file
. test.bash
bpx has been written and tested in GNU bash 5.0-6 on Debian GNU/Linux bullseye/sid (5.5.0-2-amd64 x86-64).
bpx needs also the following programs/packages:
- GNU coreutils 8.30: cat, chmod, rm
- GNU diffutils 3.7: cmp
Optional is (see above):
- Vi IMproved 8.2 (Included patches: 1-510)
GNU GPLv3