Skip to content

Commit

Permalink
rc tools menu: work around ARG_MAX limit
Browse files Browse the repository at this point in the history
when "menu" arguments exceed ~200k bytes, I get "execve failed:
Argument list too long" (even though ARG_MAX is 2MB).  This is because
the shell process is passed the arguments to menu.

Reproduce with

	evaluate-commands %exp{
		menu asdf %sh{dd 2>/dev/null if=/dev/zero bs=1000 count=200 | sed s/././g}
	}

I hit this with rust-analyzer which can send ~70 code actions to
select from, each with a lengthy JSON object, so the :menu invocation
can sometimes reach the effective limit. It can also become slow
(0.5 seconds), maybe because we fork multiple times per argument.

Fix this by passing arguments through $kak_response_fifo.
The on-accept and on-change callbacks have the same problem (sh -c
string too long), so move them to temporary files.  This means we
can get rid of some escaping.

Note that there is currently no dedicated way to stop Kakoune from
passing "$@" to the shell process, so define an extra command that
doesn't take args.

Since we serialize arguments into a single string (using "echo
-quoting"), we need to deserialize them before doing anything else.
We currently don't have an off-the-shelf solution for this.  Perhaps
this is an argument for "echo -quoting json" (which has been suggested
before I believe).
  • Loading branch information
krobelus committed Dec 16, 2023
1 parent 83fb65a commit ac84184
Showing 1 changed file with 132 additions and 73 deletions.
205 changes: 132 additions & 73 deletions rc/tools/menu.kak
Original file line number Diff line number Diff line change
Expand Up @@ -7,79 +7,138 @@ define-command menu -params 1.. -docstring %{
-auto-single instantly validate if only one item is available
-select-cmds each item specify an additional command to run when selected
} %{
evaluate-commands -save-regs a %{
set-register a %arg{@}
menu-impl
}
}
define-command -hidden menu-impl %{
evaluate-commands %sh{
auto_single=false
select_cmds=false
stride=2
on_abort=
while true
do
case "$1" in
(-auto-single) auto_single=true ;;
(-select-cmds) select_cmds=true; stride=3 ;;
(-on-abort) on_abort="$2"; shift ;;
(-markup) ;; # no longer supported
(*) break ;;
esac
shift
done
if [ $(( $# % $stride )) -ne 0 ]; then
echo fail "wrong argument count"
exit
fi
if $auto_single && [ $# -eq $stride ]; then
printf %s "$2"
exit
fi
shellquote() {
printf "'%s'" "$(printf %s "$1" | sed "s/'/'\\\\''/g; s/§/§§/g; $2")"
}
cases=
select_cases=
completion=
nl=$(printf '\n.'); nl=${nl%.}
while [ $# -gt 0 ]; do
title=$1
command=$2
completion="${completion}${title}${nl}"
cases="${cases}
($(shellquote "$title" s/¶/¶¶/g))
printf '%s\\n' $(shellquote "$command" s/¶/¶¶/g)
;;"
if $select_cmds; then
select_command=$3
select_cases="${select_cases}
($(shellquote "$title" s/¶/¶¶/g))
printf '%s\\n' $(shellquote "$select_command" s/¶/¶¶/g)
;;"
fi
shift $stride
done
printf "\
prompt '' %%§
evaluate-commands %%sh¶
case \"\$kak_text\" in \
%s
(*) echo fail -- no such item: \"'\$(printf %%s \"\$kak_text\" | sed \"s/'/''/g\")'\" ;;
esac
§" "$cases"
if $select_cmds; then
printf " \
-on-change %%§
evaluate-commands %%sh¶
case \"\$kak_text\" in \
%s
(*) : ;;
esac
§" "$select_cases"
fi
if [ -n "$on_abort" ]; then
printf " -on-abort '%s'" "$(printf %s "$on_abort" | sed "s/'/''/g")"
fi
printf ' -menu -shell-script-candidates %%§
printf %%s %s
§\n' "$(shellquote "$completion")"
echo >$kak_command_fifo "echo -to-file $kak_response_fifo -quoting kakoune -- %reg{a}"
perl < $kak_response_fifo -we '
use strict;
my $Q = "'\''";
my @args = ();
{
my $arg = undef;
my $prev_is_quote = 0;
my $state = "before-arg";
while (not eof(STDIN)) {
my $c = getc(STDIN);
if ($state eq "before-arg") {
($c eq $Q) or die "bad char: $c";
$state = "in-arg";
$arg = "";
} elsif ($state eq "in-arg") {
if ($prev_is_quote) {
$prev_is_quote = 0;
if ($c eq $Q) {
$arg .= $Q;
next;
}
($c eq " ") or die "bad char: $c";
push @args, $arg;
$state = "before-arg";
next;
} elsif ($c eq $Q) {
$prev_is_quote = 1;
next;
}
$arg .= $c;
}
}
($state eq "in-arg") or die "expected $Q as last char";
push @args, $arg;
}
my $auto_single = 0;
my $select_cmds = 0;
my $on_abort = "";
while (defined $args[0] && $args[0] =~ m/^-/) {
if ($args[0] eq "-auto-single") {
$auto_single = 1;
}
if ($args[0] eq "-select-cmds") {
$select_cmds = 1;
}
if ($args[0] eq "-on-abort") {
if (not defined $args[1]) {
print "fail %{menu: missing argument to -on-abort}";
exit;
}
$on_abort = $args[1];
}
shift @args;
if ($args[0] eq "--") {
last;
}
}
my $stride = 2 + $select_cmds;
if (scalar @args == 0 or scalar @args % $stride != 0) {
print "fail %{menu: wrong argument count}";
exit;
}
if ($auto_single && scalar @args == $stride) {
print $args[$0];
exit;
}
sub shellquote {
my $arg = shift;
$arg =~ s/$Q/$Q\\$Q$Q/g;
return "$Q$arg$Q";
}
sub kakquote {
my $arg = shift;
$arg =~ s/$Q/$Q$Q/g;
return "$Q$arg$Q";
}
my $accept_cases = "";
my $select_cases = "";
my $completions = "";
sub case_clause {
my $name = shellquote shift;
my $command = shellquote shift;
return "($name)\n"
. " printf \"%s\n\" $command ;;\n";
}
for (my $i = 0; $i < scalar @args; $i += $stride) {
my $name = $args[$i];
my $command = $args[$i+1];
$accept_cases .= case_clause $name, $command;
$select_cases .= case_clause $name, $args[$i+2] if $select_cmds;
$completions .= "$name\n";
}
use File::Temp qw(tempdir);
my $tmpdir = tempdir;
sub put {
my $name = shift;
my $contents = shift;
my $filename = "$tmpdir/$name";
open my $fh, ">", "$filename" or die "failed to open $filename: $!";
print $fh $contents or die "write: $!";
close $fh or die "close: $!";
return $filename;
};
my $on_accept = put "on-accept",
"case \"\$kak_text\" in\n" .
"$accept_cases" .
"(*) echo fail -- no such item: \"$Q\$(printf %s \"\$kak_text\" | sed \"s/$Q/$Q$Q/g\")$Q\";\n" .
"esac\n";
my $on_change = put "on-change",
"case \"\$kak_text\" in\n" .
"$select_cases" .
"esac\n";
my $shell_script_candidates = put "shell-script-candidates", $completions;
print "prompt %{} %{ evaluate-commands %sh{. $on_accept kak_text; rm -r $tmpdir} }";
print " -on-abort " . kakquote "nop %sh{rm -r $tmpdir}; $on_abort";
if ($select_cmds) {
print " -on-change %{ evaluate-commands %sh{. $on_change kak_text} }";
}
print " -menu -shell-script-candidates %{cat $shell_script_candidates}";
' ||
echo 'fail menu: encountered an error, see *debug* buffer';
}
}

0 comments on commit ac84184

Please sign in to comment.