Kakoune needs a real scripting language

I don’t see how using a fifo would be much slower than implementing it directly in kakoune. Maybe it’s a problem with connect, whose implementation is a bit slow because it calls many small processes?

@scr The generic command is :send.

If I recall well my OS classes, writting and reading a file is a pretty slow operation, compared to printing to stdout. I might be wrong, it was long time ago :stuck_out_tongue:

This would be great! Is there a GitHub issue for this?

My understanding is that printing to stdout is orders of magnitude slower than writing to a file because in order to print to stdout, you have to write to a file, and that file somehow interacts with I/O. Whenever you start doing things with I/O things start getting significantly slower.

Yep. #3782 #3783

stdout is just the standard output, it can be a standard file or a tty or whatever.

Writing to the disk might be slow, but a fifo is a special type of file. Fifos are controlled by the kernel and are a way of transmitting data between processes. In fact, the output is not stored in disk because if the process outputs a lot of data it gets blocked until the reader reads some of it and makes space in the buffer of the fifo. What’s more, when you pipe the output of one command to another, you are using something very similar. A fifo file is just a special type of pipe that has name and can be addressed by a path like a lot of things in linux, because everything is a file.

That said, maybe the implementation for named fifos is a bit different than pipes or maybe the command is slow for other reasons (like chaining multiple commands instead of a single one). But man fifo says that it has no contents in the filesystem.

5 Likes

I’m trying out this approach to call a python file now:

def -override python -params .. %{
    eval %sh{
        pyfile="$1"
        shift
        args=$(printf '%q ' "$@")
        vars=$(grep -o 'kak_\w*' "$pyfile" | sort -u | tr '\n' ' ')
        printf %s "eval %sh{python $pyfile $args # $vars}"
    }
}

For an example, save this to example.py:

import os, sys, shlex, json

def quote(s):
    return "'" + s.replace("'", "''") + "'"

print('info --', quote(json.dumps({
    'argv': sys.argv,
    'selections': shlex.split(os.environ['kak_quoted_selections'])
}, indent=2)))

Now, calling (in kakoune) python example.py %val{window_range} will eval %sh{python example.py 0\ 0\ 25\ 94 # kak_quoted_selections }. Here the comment contains kak_quoted_selections so that this environment variable is passed from kakoune to the %sh-block, and it was addedbecause it appears in the python source file. This gets evaluated and we get the expected info box:

The python function can of course be called from a defined command:

def example -params .. %{
    python example.py %val{window_range} %arg{@}
}

I find it pretty convenient. Please share your thoughts about it.

%q is only supported in bash, but a portable version of the same thing isn’t much more complex:

multi_sed_quoter() {
    for each; do
        printf "'"
        printf "%s" "$each" | sed "s/'/''/g"
        printf "' "
    done
}

# ...

args=$(multi_sed_quoter "$@")

I like the trick with extracting the Kakoune variable names from the script, and making sure they’ll be available in the environment! It’s pretty simple, but I can’t think of a way to break it, even with malicious input.

It might be nice to see a variant that lets you specify the Python script inline, like:

python arg1 arg2 arg3 %{
    import os, sys, shlex, json

    #... etc.
}

You could even bundle a kak.py with your plugin, and add your plugin’s path to $PYTHONPATH so scripts could import kak to get helpers like:

import os, shlex

def quote(x):
    if isinstance(x, str):
        return "'" + x.replace("'", "''") + "'"
    elif isinstance(x, bytes):
        return b"'" + x.replace(b"'", b"''") + b"'"
    elif hasattr(x, "__iter__"):
        return " ".join(kakquote(each) for each in x)
    else:
        return kakquote(str(x))

def get(x):
    shlex.split(os.environ.get(x, ""))

That’s just brainstorming, though - what you have now is already useful, and it’s hard to justify cluttering up such a simple implementation with extra features.

Thanks for your input! :slight_smile:

That’s a bummer that prinf %q is not posix. Calling sed gives a noticeable overhead:

$ set -- a $'b\n' 'c"'"'d"
$ time for each; { printf "'"; printf %s "$each" | sed "s/'/''/g"; printf "' "; }
'a' 'b
' 'c"''d'
real    0m0.074s
user    0m0.058s
sys 0m0.024s
$ time printf "%q " "$@"
a $'b\n' c\"\'d
real    0m0.009s
user    0m0.006s
sys 0m0.005s

We can instead do a little dance and define a new command and call it immediately: Edit: simplified this in the next post

def -override python -params .. %{
    eval %sh{
        pyfile="$1"
        vars=$(grep -o 'kak_\w*' "$pyfile" | sort -u | tr '\n' ' ')
        args='"$@"'
        printf '%s\n' "def -hidden -override python-eval -params .. %{eval %sh{python $args # $vars}}"
        printf '%s\n' "python-eval %arg{@}"
    }
}

If we want to support inline scripts one way is to fall back to python’s -c, but then the expansions will be in the second argument. This is one way to work with it:

def -override python -params .. %{
    eval %sh{
        pyfile="$1"
        vars=$(
            [ "$pyfile" = "-c" ] && printf %s "$2" || cat "$pyfile" | 
            grep -o 'kak_\w*' | sort -u | tr '\n' ' '
        )
        args='"$@"'
        printf '%s\n' "def -hidden -override python-eval -params .. %{eval %sh{python $args # $vars}}"
        printf '%s\n' "python-eval %arg{@}"
    }
}

Now, as an example, python -c 'import os; print("info", repr(os.environ["kak_window_range"]))' works. And indeed, this can be supplemented with a support library like you suggest with kak-quoting and shlex-splitting.

Turns out you don’t need to do the little define-dance to retain the arguments for the second %sh-block. So this works:

def -override python -params .. %{
    eval %sh{
        pyfile="$1"
        vars=$(
            [ "$pyfile" = "-c" ] && printf %s "$2" || cat "$pyfile" | 
            grep -o 'kak_\w*' | sort -u | tr '\n' ' '
        )
        args='"$@"'
        printf %s "eval %sh{python $args # $vars}"
    }
}
1 Like

That’s clever - avoiding the quoting by generating eval %sh{python "$@"}.
I love the fact that many “plugins” need very little Kakoune-specific code.

That’s more or less also what Luar does =)

Hi, @danr !

What if, instead of calling an external file, your script executes the last argument of the command (called example above) as a python code? That way, we could get something similar to the lua command of the Luar plugin. It would be nice if we could use the same ideas to bring scripting Kakoune in other languages besides Lua.

1 Like

Hi!

I to put the file in the first argument since it was easy to refer to using "$1" to grep for kak_ expansions. I use these to imprint them into the next %sh-block, this way you can get their values accessing them as environment variables in the target language (python here, but lua should work too). The point of my exercise was to automatically get these values instead of having to pass them to the command just to have to match them up with command line arguments.

Putting the file in the first argument matches up how python is invoked from the shell making the %sh-block simple and the api familiar. I updated it to also support python -c PROGRAM_STRING (this flag is from the command line api of python) by grepping in this program string.

Here is a simpler one. It works for any program, not just python:

def sh -params .. %{
    eval eval "%%sh{""$@"" # %sh{printf '%s ' ""$@"" | grep -o 'kak_\w\+' | tr '\n' ' '}}"
}

sh python -c %{if 1:
    import os, sys, shlex, json

    def quote(s): return "'" + s.replace("'", "''") + "'"

    print('info --', quote(json.dumps({
        'buffile': os.environ["kak_buffile"],
        'bufname': os.environ["kak_bufname"],
        'argv': sys.argv,
        'selections': shlex.split(os.environ['kak_quoted_selections'])
    }, indent=2)))
} %val{selections}
3 Likes

So I did eventually get around to implementing some basic control flow commands in kakoune. My first attempt got side tracked by trying to pointless abstract over the command structure when a simpler implementation would easily suffice. So I wrote a basic if, for and return command over the weekend. It’s literally the only cpp I’ve ever written so don’t expect anything too impressive, it’s mainly just a proof of concept and for personal entertainment. link to repo

But here are some examples of what you can write with it

word-select.kak alternate implementation

map global normal w ': word-select "%val{selections_desc}" "<a-:>" "w" "/" <ret>'
map global normal W ': word-select "%val{selections_desc}" "<a-:>" "w" "/" <ret>'
define-command -hidden -override word-select -params 4 %{
    try %{
        execute-keys '<a-i>' %arg{3} %arg{2}
    }
    if %arg{1} equals %val{selections_desc} %{
        execute-keys %arg{4} \w <ret> <a-i> %arg{3} %arg{2}
    }
}

iterating over a str-list

declare option str-list thing1 thing2 thing3
for item in list %{
    echo -debug "@@@%val{item}@@@"
}

iterating over regex matches

for '[^/]+' in %val{buffile} %{
    echo -debug "path segment: %val{for}"
}

adding comments plus whitespace (double hyphen)

execute-keys k <a-x> H
if '^(\h*--\h*).*' matches "%val{selection}" %{
    exec -draft "I%val{if_capture_1}<esc>"
}

return can be used to end a block, e.g. when writing try with selection based gaurds so you don’t have to worry about the different branches overlapping

define-command ... %{
    try %{  }
    try %{  }
    try %{  }
}

I’ve personally found it quite nice for readability in the few small scripts I wrote to test the implementation but I don’t think small changes like this can really have a significant impact on how you write scripts.

4 Likes

God I would really like this. Some basic control flow would be nice.

1 Like

Other attempts at implementing control flow


I wrote this horrible hack of for implementation in luar just for fun:

provide-module control-flow %¼
	require-module luar
	define-command for -params 4.. %{
		lua %arg{@} %{
    		local elm_name = table.remove(arg, 1)
    		table.remove(arg,1) -- drop 'in'
    		local body = table.remove(arg, #arg)
    		for _,elm in ipairs(arg) do
    			local b = string.gsub(body, "%%val{" .. elm_name .. "}",elm)
				kak.evaluate_commands(b)
    		end
		}
	}

	define-command test-control-flow %{
        for buffer in %val{buflist} %{
        	echo -debug %val{buffer}
        }
	}
¼
1 Like

Don’t know if you already hear about execline. I use it extensively in container under alpine with the s6 supervision suite, and it’s minimalism remind me a lot about kakoune.

Having a script in one command line with each command replacing the previous one with chain loading until there is no arguments left is beautiful.

As it’s really small and portable (25kb), it would ne nice to have %execlineb{ as a lightweight embedded %sh{ alternative. It should also be faster and less resources hungry when the script is reduced to calling a command or two or use pipe, fifo, redirections, …

2 Likes

I strongly recommend the enlightened reading of Why not just use /bin/sh? (funny that the page name is dieshdiedie.html).

also %exec{ would be less verbose, and better match what the same sh command do and what execlineb do at every step.

2 Likes