Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

zap-to-char in Bash/readline implementieren?

2 views
Skip to first unread message

Tim Landscheidt

unread,
Oct 17, 2023, 7:39:21 AM10/17/23
to
Hi,

ein Emacs-Befehl, den ich in Bash 5.2.15 immer schmerzlich
vermisse, ist zap-to-char (M-z), der ein Zeichen liest und
dann den Text zwischen dem Cursor und dem nächsten (gegebe-
nenfalls dem n-ten) Vorkommen dieses Zeichens einschließlich
löscht.

Ich hatte in der Vergangenheit schon einmal die Bash-Doku-
mentation dahingehend untersucht, ob die readline-Bibliothek
vielleicht einen solchen Befehl bietet, er aber lediglich
von Bash nicht mit einer Taste assoziiert wird, aber leider
ist er wohl kein elementarer Teil dieser.

Heute stöbere ich noch einmal durch die Man-Page und stoße
auf die Shell-Variablen „READLINE_ARGUMENT“,
„READLINE_LINE“, „READLINE_MARK“ und „READLINE_POINT“ und
deren Verweise auf „bind -x“:

| […]
| -x keyseq:shell-command
| Cause shell-command to be executed
| whenever keyseq is entered. When
| shell-command is executed, the shell
| sets the READLINE_LINE variable to
| the contents of the readline line
| buffer and the READLINE_POINT and
| READLINE_MARK variables to the cur‐
| rent location of the insertion point
| and the saved insertion point (the
| mark), respectively. The shell as‐
| signs any numeric argument the user
| supplied to the READLINE_ARGUMENT
| variable. If there was no argument,
| that variable is not set. If the ex‐
| ecuted command changes the value of
| any of READLINE_LINE, READLINE_POINT,
| or READLINE_MARK, those new values
| will be reflected in the editing
| state.
| […]

Das hört sich ja an, als könnte man zap-to-char damit prin-
zipiell in Bash implementieren. Ist jemand so etwas schon
einmal über den Weg gelaufen? Mein Google-Fu ist zu
schlecht.

TIA,
Tim

Christian Weisgerber

unread,
Oct 17, 2023, 10:30:07 AM10/17/23
to
On 2023-10-17, Tim Landscheidt <t...@tim-landscheidt.de> wrote:

> ein Emacs-Befehl, den ich in Bash 5.2.15 immer schmerzlich
> vermisse, ist zap-to-char (M-z), der ein Zeichen liest und
> dann den Text zwischen dem Cursor und dem nächsten (gegebe-
> nenfalls dem n-ten) Vorkommen dieses Zeichens einschließlich
> löscht.

Im vi-Modus geht das ja mit df<Zeichen>. Man sollte meinen,
entsprechende Editier-Primitive seien vorhanden, aber...

*schaut in die libreadline-Sourcen*

... der vi-Modus wird mit eigenen, nicht weiter dokumentierten und
wenig allgemeintauglichen Funktionen realisiert.

Im Emacs-Modus gibt es immerhin character-search (C-]). Mir
ist es aber nicht gelungen, damit ein passendes Makro für
~/.inputrc zu bauen. Ich kenne mich damit aber auch nicht
weiter aus.

> Heute stöbere ich noch einmal durch die Man-Page und stoße
> auf die Shell-Variablen „READLINE_ARGUMENT“,
> „READLINE_LINE“, „READLINE_MARK“ und „READLINE_POINT“ und
> deren Verweise auf „bind -x“:
>
> Das hört sich ja an, als könnte man zap-to-char damit prin-
> zipiell in Bash implementieren.

Ein Problem ist, dass das Zeichen, bis zu dem gelöscht werden soll,
irgendwie eingelesen werden muss. Ich habe mal schnell sowas
gebastelt:

zap_to_char()
{
local head tail char
head=${READLINE_LINE:0:$READLINE_POINT}
tail=${READLINE_LINE:$READLINE_POINT}
read -rN1 char
tail=${tail#*${char}}
READLINE_LINE=$head$tail
}
bind -x '"\ez":zap_to_char'

Leider wird vor der Ausführung der Funktion die Zeile im Terminal
gelöscht und erst anschließend wieder angezeigt, das read also auf
einer leeren Zeile ausgeführt. Außerdem ist der gelöschte Teil dann
natürlich nicht im Kill-Buffer wie bei M-z in Emacs.

Ich denke, am sinnvollsten wäre es, zap-to-char direkt in libreadline
zu implementieren. Wahrscheinlich kann man den Code im Wesentlichen
von anderen Funktionen kopieren.

--
Christian "naddy" Weisgerber na...@mips.inka.de

Tim Landscheidt

unread,
Oct 23, 2023, 9:43:28 AM10/23/23
to
Christian Weisgerber <na...@mips.inka.de> wrote:

> […]

>> Heute stöbere ich noch einmal durch die Man-Page und stoße
>> auf die Shell-Variablen „READLINE_ARGUMENT“,
>> „READLINE_LINE“, „READLINE_MARK“ und „READLINE_POINT“ und
>> deren Verweise auf „bind -x“:

>> Das hört sich ja an, als könnte man zap-to-char damit prin-
>> zipiell in Bash implementieren.

> Ein Problem ist, dass das Zeichen, bis zu dem gelöscht werden soll,
> irgendwie eingelesen werden muss. Ich habe mal schnell sowas
> gebastelt:

> zap_to_char()
> {
> local head tail char
> head=${READLINE_LINE:0:$READLINE_POINT}
> tail=${READLINE_LINE:$READLINE_POINT}
> read -rN1 char
> tail=${tail#*${char}}
> READLINE_LINE=$head$tail
> }
> bind -x '"\ez":zap_to_char'

> Leider wird vor der Ausführung der Funktion die Zeile im Terminal
> gelöscht und erst anschließend wieder angezeigt, das read also auf
> einer leeren Zeile ausgeführt. Außerdem ist der gelöschte Teil dann
> natürlich nicht im Kill-Buffer wie bei M-z in Emacs.

> Ich denke, am sinnvollsten wäre es, zap-to-char direkt in libreadline
> zu implementieren. Wahrscheinlich kann man den Code im Wesentlichen
> von anderen Funktionen kopieren.

Eine Implementation in libreadline wäre sicherlich optimal.
Der Vorteil von „bind -x“ ist natürlich, dass die Funktiona-
lität /jetzt/ verfügbar wird und nicht in ein paar Jah-
ren :-). Außerdem fand ich es interessant, ein Beispiel für
„bind -x“ zu entwickeln, das einen realen Nutzen hat.

Ich habe Deine und Stefans Inspirationen (danke!) daher wei-
terverarbeitet. Ich wollte Emacs’ Verhalten hinsichtlich des
universalen Argumentes möglichst identisch nachbauen, und,
auch wenn libreadline leider keinen transient-mark-mode
kennt und ich daher praktisch nie die Mark auf der Befehls-
zeile benutze, auch deren Positionierung beachten:

| # zap_to_char ARG CHAR.
| function zap_to_char {
| # Default to deleting to the first occurence of CHAR.
| local arg="${READLINE_ARGUMENT:-1}"

| # Read character to zap to.
| local ch
| read -d '' -rsn 1 ch

| local line_before_point="${READLINE_LINE:0:${READLINE_POINT}}"
| local line_after_point="${READLINE_LINE:${READLINE_POINT}}"
| while [ "$arg" -gt 0 ]; do
| local new_line_after_point="${line_after_point#*${ch}}"
| # If CHAR cannot be found ARG times in the current
| # line after point, fail.
| if [ "$line_after_point" = "$new_line_after_point" ]; then
| return
| fi
| line_after_point="$new_line_after_point"
| arg=$(("$arg" - 1))
| done

| # Reposition mark.
| # If the mark was not after the point, do nothing.
| if [ "$READLINE_MARK" -gt "$READLINE_POINT" ]; then
| # Otherwise, the mark gets moved left as many
| # characters as the line after the point shrunk by,
| # but no further left than the point.
| READLINE_MARK=$((READLINE_MARK - (${#READLINE_LINE} - READLINE_POINT - ${#line_after_point})))
| if [ "$READLINE_MARK" -lt "$READLINE_POINT" ]; then
| READLINE_MARK="${READLINE_POINT}"
| fi
| fi

| # Set new line.
| READLINE_LINE="${line_before_point}${line_after_point}"
| }

| bind -x '"\ez":zap_to_char'

| # Test one zap_to_char call.
| # test_zap_to_char-1 INPUT ARG CHAR EXPOUT.
| # The characters "#" and "!" are used to denote the position
| # of the mark and the point, respectively. If both the mark
| # and the point are at the same position, the order is "#!".
| function test_zap_to_char-1 {
| local READLINE_LINE="$(printf %s "$1" | sed -e 's/[#!]//g;')"
| local READLINE_POINT="$(printf %s "$1" | sed -e 's/^\([^#!]*\)\(#\([^#!]*\)\)\?!.*$/\1\3/;' | wc -c)"
| local READLINE_MARK="$(printf %s "$1" | sed -e 's/^\([^#!]*\)\(!\([^#!]*\)\)\?#.*$/\1\3/;' | wc -c)"
| local READLINE_ARGUMENT="$2"
| if [ "$READLINE_ARGUMENT" = 'x' ]; then
| unset READLINE_ARGUMENT
| fi
| zap_to_char < <(echo -nE "$3")
| local actual_output="${READLINE_LINE:0:${READLINE_POINT}}!${READLINE_LINE:${READLINE_POINT}}"
| if [ $READLINE_MARK -le $READLINE_POINT ]; then
| actual_output="${actual_output:0:$READLINE_MARK}#${actual_output:$READLINE_MARK}"
| else
| actual_output="${actual_output:0:$READLINE_MARK + 1}#${actual_output:$READLINE_MARK + 1}"
| fi

| if [ "$actual_output" = "$4" ]; then
| echo ok
| else
| echo "not ok: Expected \"$4\", got \"$actual_output\"."
| fi
| }

| # Test suite for zap_to_char.
| function test_zap_to_char {
| # Basic functionality.
| test_zap_to_char-1 'ABC#!DEFGHIJKLMNOPQRSTUVWXYZ' x 'J' 'ABC#!KLMNOPQRSTUVWXYZ'
| test_zap_to_char-1 'ABC#!DEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ' 2 'J' 'ABC#!KLMNOPQRSTUVWXYZ'
| # Character cannot be found after point.
| test_zap_to_char-1 'ABC#!DEFGHIJKLMNOPQRSTUVWXYZ' x 'j' 'ABC#!DEFGHIJKLMNOPQRSTUVWXYZ'
| # Character cannot be found not enough times after point.
| test_zap_to_char-1 'ABC#!DEFGHIJKLMNOPQRSTUVWXYZ' 2 'J' 'ABC#!DEFGHIJKLMNOPQRSTUVWXYZ'
| # Preserve mark.
| test_zap_to_char-1 'ABC#DE!FGHIJKLMNOPQRSTUVWXYZ' x 'J' 'ABC#DE!KLMNOPQRSTUVWXYZ'
| test_zap_to_char-1 'ABCDE!FG#HIJKLMNOPQRSTUVWXYZ' x 'J' 'ABCDE#!KLMNOPQRSTUVWXYZ'
| test_zap_to_char-1 'ABCDE!FGHIJKLM#NOPQRSTUVWXYZ' x 'J' 'ABCDE!KLM#NOPQRSTUVWXYZ'
| }

| test_zap_to_char

Was ich in jedem Fall mitgenommen habe: Man kann in der Bash
Tasten nicht nur auf libreadline-Primitive konfigurieren
oder kontextlose Eingabekombinationen, die hoffentlich tun,
was sie sollen, sondern hat die Möglichkeit, die Befehlszei-
le komplett zu manipulieren mit allen Mitteln, die in Shell-
Funktionen zu Verfügung stehen.

Besten Dank noch einmal,
Tim
0 new messages