Branching on a boolean option -- without calling the shell

In Kakoune scripts, branching on the value of a boolean option, myvar, may be done by calling the shell inside a try ... catch construction:

try %{
    evaluate-commands %sh{
        if [ "$kak_opt_myvar" = false ]; then
            printf %s\\n fail
        fi
    }
    commands_1
} \
catch %{
    commands_2
}

which looks bloated and contorted to me. (One could use a wrapper to make the code look simpler, but then the wrapper needs to be defined elsewhere.)

Recently I have been using a trick to avoid calling the shell and inflating the code. The trick is to change the type of the myvar option to str instead of boolean, setting myvar to nop when I want it to be true and to nay when I want it to be false.

Now the branching looks like this:

try %{
    eval %opt(myvar)
    commands_1
} \
catch %{
    commands_2
}

This is Kakoune’s eval, not the shell’s. When myvar is “true”, eval %opt(myvar) goes through and we execute commands_1. When myvar is “false”, eval %opt(myvar) fails (because there is no such command as nay) and we execute commands_2.

But perhaps most of you, professional coders, are already using this sort of hack? In any case, I like the look of the resulting code. You are free to decide whether this is more clever than stupid or vice-versa :slight_smile:!

7 Likes

No, at leas I’m not thought about it! Nice trick!

I was using this custom command:

require-module kak
add-highlighter shared/kakrc/code/if_else regex \b(if|else)\b 0:keyword

define-command -docstring "if <condition> <expression> [else [if <condition>] <expression>]: if statement that accepts shell-valid condition string" \
if -params 2.. %{ evaluate-commands %sh{
    while [ true ]; do
        condition="[ $1 ]"
        if [ -n "$3" ] && [ "$3" != "else" ]; then
            printf "%s\n" "fail %{if: unknown operator '$3'}"
        elif [ $# -eq 3 ]; then
            printf "%s\n" "fail %{if: wrong argument count}"
        elif eval $condition; then
            [ -n "${2##*&*}" ] && arg="$2" || arg="$(printf '%s' "$2" | sed 's/&/&&/g')"
            printf "%s\n" "evaluate-commands %& $arg &"
        elif [ $# -eq 4 ]; then
            [ -n "${4##*&*}" ] && arg="$4" || arg="$(printf '%s' "$4" | sed 's/&/&&/g')"
            printf "%s\n" "evaluate-commands %& $arg &"
        elif [ $# -gt 4 ]; then
            if [ "$4" = "if" ]; then
                shift 4
                continue
            else
                printf "%s\n" "fail %{if: wrong argument count}"
            fi
        fi
        exit
    done
}}

That allows me to chain if else ifelse blocks as in C-like languages, but I mostly use it for single if else, like this, to branch my configuration depending on where I run Kakoune:

plug "andreyorst/base16-gruvbox.kak" domain gitlab.com theme %{
    if %[ -n "${PATH##*termux*}" ] %{
        colorscheme base16-gruvbox-dark-soft
    } else %{
        colorscheme base16-gruvbox-dark-hard
    }
}

The problem is that I mostly want to use shell conditionals as those are far more versatile compared to Kakoune ones, but your example can be used to make some code that uses try catch approach more appealing. I wonder if this can be turned into function, as in my example

I forgot to say that a similar trick can be used to define a procedure (= command) with two variants. Instead of declaring two different commands, declare a single command along these lines:

define-command -params 1 my-procedure %{
    try %{
       eval %arg(1)
       commands_1
    } \
    catch %{
        commands_2
    }
    shared_commands ...
}

Calling the procedure with either nop or nay as an argument value will execute one variant or the other.

One thing that I don’t get with your trick is how do you set the option to the desired value, don’t you need a branch at that point? Let’s say, how would you check if your current selection is 5 characters long?

Not necessarily. Setting the option to “true” or “false” (nop or nay) may be done in a separate command that you may call during your editing session (e.g., “do you want the script to use feature X [yes|no]?”). The option-setting command may also be called by a hook that fires when opening a buffer of a specific filetype.

Even when setting the option to “true” or “false” requires a branch, this branch (A) may occur earlier in the script, in a part separate from the later branch or command (B). For example, you may set the option to “nop” or “nay” depending on the result of an <a-k>... test done on the current selection. Then you may need to call other commands before finally calling (B) with the value of the previously set option.

Of course, my trick is superflous if you just need to do some test and execute some action immediately, depending on the result of the test. The trick is useful only when option-setting is done separately from the final branch – and especially useful if several procedures depend on the previously set option at different points in time.

Nice trick, hadn’t thought of this!

You can allow the variable to have the value true and false by defining true:

def true nop

Now eval true does not fail, as desired.

4 Likes

Nice! I’ve done something similar with setting option values as commands and running eval on the option.

With how many posts of this sort that have popped up, I’m becoming more and more convinced that conditional control flow should be properly included in kakscript. Though, I guess the percentage of kak files that use things like this is still pretty small.

That or, once mrsh is ready, have a compile option to integrate the shell into Kakoune itself.

1 Like

Normal user configs probably don’t need conditionals very often, but a ton of plugins depend on conditionals. Adding it to kakscript would be a nice quality of life improvement, and could improve performance as well.

Checking if an int is a specific value without shell:

def check-0-0 nop
try %{
	eval "check-0-%opt{indentwidth}"
	echo "indentwidth is 0"
} catch %{
	echo "indentwidth is different from/greater than 0"
}

I know a few plugins that could benefit from this.

4 Likes

Kakoune primarily operates on buffers, so you can use a scratch buffer.

edit -scratch
set-register '"' %opt{my_option}
execute-keys R
set-register / my-regex
execute-keys <a-k> <ret>

It is nice, but a bit verbose, and ultimately, we do not care about the buffer thing.

In a recent version of Kakoune, @mawww introduced user hooks, allowing us to create our own hooks. As a bonus, we can leverage branching without opening a shell or a scratch buffer.

define-command if -params 3 -docstring 'if <value> <regex> <commands>' %{
  hook -group if global User %arg{2} %arg{3}
  trigger-user-hook %arg{1}
  remove-hooks global if
}

Example

if %opt{my_plugin_enabled} true %{
  echo Tchou!
}

I realized and used this trick in snippets.kak.

4 Likes

I assume one could also test for string equality: check-value-%opt{…}

1 Like

I thought about this too but isn’t it so that the program flow will be “incorrect”?

if %opt{my_plugin_enabled} true %{
  echo Tchou!
}
echo Ouch!

This will output:

Ouch!
Tchou!

Which is reversed from what you would expect from an if.

2 Likes

I haven’t thought about it, it’s a good catch. :+1:

A slight variant I used today when writing a prompt command because I wanted different behaviour when %val{text} was empty.

define-command if-empty -params 2 %{
    try %{
        eval "nop%arg{1}"
        eval %arg{2}
    }
}

define-command if-not-empty -params 2 %{
    try %{
        eval "nop%arg{1}"
    } catch %arg{2}
}

Here’s the usage in a case insensitive select command. Without it you end up selecting everything when the prompty is empty. I included the entire snippet in case anyone wants to use this but fair warning, it segfaults on the last official release, works fine if you build from latest master.

declare-option str-list nocase_initial_selections
define-command -hidden nocase-restore %{
    select %opt{nocase_initial_selections}
}
define-command nocase-select %{
    set-option window nocase_initial_selections %val{selections_desc}
    prompt -on-abort nocase-restore -on-change nocase-select-impl select: nop
}
define-command -hidden nocase-select-impl %{
    select %opt{nocase_initial_selections}
    if-not-empty %val{text} %{
        try %{ exec <esc> s '(?i)' %val{text} <ret> }
        prompt -on-abort nocase-restore -on-change nocase-select-impl -init "%val{text}" select: nop
    }
}

be careful not to search for ;q! though. this method has a few unfourtunate edge cases now that I think about it…

Here’s the lambda calculus approach:

define-command true -params 2 %{ eval %arg{1} }
define-command false -params 2 %{ eval %arg{2} }
define-command if -params 3 %{ eval -verbatim %arg{1} %arg{2} %arg{3} }

if true %{echo -debug 1} %{echo -debug 2}

if false %{echo -debug 1} %{echo -debug 2}

Turns out you can define true and false as functions in kakscript.

true is a function that takes two braches, and executes the first
false is a function that takes two branches, and executes the seconf

if is a function that takes thre arguments, first is truef or falsef and the branches. It then passes the branches to the test function, which chooses the branch.

I gave it a bit of testing and it seems to work for complex branches.

You can use %sh{} as a test as a bonus:

if %sh{ [ "a" = "b" ] && echo true || echo false } %{
    echo -debug "that's true"
} %{
    echo -debug "that's false"
}

This prints that's false in the debug buffer.

Single branches can be acheived with when and unless:

define-command when -params 3 %{ eval -verbatim %arg{1} %arg{2} nop }
define-command unless -params 3 %{ eval -verbatim %arg{1} nop %arg{2} }
2 Likes

I am now convinced that this is the answer to this problem. I’ve did a bit more testing, and it works really well

Yeah, this is probably the right approach. The only thing that concerns me here is the eval, because I don’t fully understand it. If there’s an eval, there must be always be a related escape.

For sh, eval-escaping is simple: apostrophe-escape all strings (' -> '\'' then surround). What is the equivalent escaping rule for kakscript's eval? And can it be done programatically without %sh?

TBH I don’t understand it either. I really wanted for it to work, so perhaps that’s why it works :smiley:

@occivink suggested to use --verbatim escaping to be more portable, but I’m not expert here, and I don’t know why --verbatim is needed in if but is not needed in true or false for example, and adding it there breaks stuff.

However note, that Kakoune’s eval has nothing to do with shell’s eval. It’s just a short way of writing evaluate-commands. So maybe escaping with --verbatim once is sufficient.

Try this:

def -override -params .. echo-args %{ nop -- %sh{
  env printf '- |%s|\n' "$@" 1>&2
}}
def -override -params .. eval-echo-args %{
  eval echo-args %arg{@}
}

eval echo-args '''a ''''b'''
echo-args 'a ''b'

It’s the only way I got them to match. IOW, if you want eval ... to have the same effect as ..., each argument must be surrounded in triple quotes, and inside the surround each quote must be quadrupled.

Can this escaping be done in pure kakscript, no sh? Can I define a command that takes a list of arguments and recursively builds up a string with those rules? Because if I could, a lot of things become possible by switching to continuation-passing style.

I don’t qute understand why this is needed? You provide balanced strings to if which are evaled by kakoune, without shell (%{this is a string}), so those are already escaped by you. This escaping is preserved with --verbatim, as far as I understand this.

I’ve ported my configuration to use this if instead of my previous implementation Branching on a boolean option -- without calling the shell - #2 by andreyorst, and it works without need to escape anything.