help-bash
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Emacs' zap-to-char as a bindable shell function


From: Tim Landscheidt
Subject: Emacs' zap-to-char as a bindable shell function
Date: Tue, 24 Oct 2023 10:00:17 +0000
User-agent: Gnus/5.13 (Gnus v5.13) Emacs/28.3 (gnu/linux)

Hi,

I use zap-to-char (M-z) in Emacs quite frequently and thus
miss it dearly in Bash/readline (though probably not as much
as transient-mark-mode :-)).

Recently, I stumbled upon the section for "bind -x" in the
man page for the first time and thought that it should be
possible to approximate Emacs' behaviour that way, and,
voilĂ , I present to you the results of my labour.

One additional take-away from that exercise for me was the
realization that "bind -x" functions have full control over
the input line, i. e. with Bash/readline one can not only
bind a key to a "macro" where one has to hope that the bound
keys "work" at that specific location, but that a bound
shell function can take context into consideration and then
Do The Right Thing™.

If someone wants to clean up the code and use it for docu-
mentation, you are welcome to (CC0).

| # 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 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

Tim



reply via email to

[Prev in Thread] Current Thread [Next in Thread]