Skip to content

Commit 70f110a

Browse files
committed
rc tools menu: work around ARG_MAX limit
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).
1 parent b0119b0 commit 70f110a

File tree

1 file changed

+134
-73
lines changed

1 file changed

+134
-73
lines changed

rc/tools/menu.kak

Lines changed: 134 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -7,79 +7,140 @@ define-command menu -params 1.. -docstring %{
77
-auto-single instantly validate if only one item is available
88
-select-cmds each item specify an additional command to run when selected
99
} %{
10+
evaluate-commands -save-regs a %{
11+
set-register a %arg{@}
12+
menu-impl
13+
}
14+
}
15+
define-command -hidden menu-impl %{
1016
evaluate-commands %sh{
11-
auto_single=false
12-
select_cmds=false
13-
stride=2
14-
on_abort=
15-
while true
16-
do
17-
case "$1" in
18-
(-auto-single) auto_single=true ;;
19-
(-select-cmds) select_cmds=true; stride=3 ;;
20-
(-on-abort) on_abort="$2"; shift ;;
21-
(-markup) ;; # no longer supported
22-
(*) break ;;
23-
esac
24-
shift
25-
done
26-
if [ $(( $# % $stride )) -ne 0 ]; then
27-
echo fail "wrong argument count"
28-
exit
29-
fi
30-
if $auto_single && [ $# -eq $stride ]; then
31-
printf %s "$2"
32-
exit
33-
fi
34-
shellquote() {
35-
printf "'%s'" "$(printf %s "$1" | sed "s/'/'\\\\''/g; s/§/§§/g; $2")"
36-
}
37-
cases=
38-
select_cases=
39-
completion=
40-
nl=$(printf '\n.'); nl=${nl%.}
41-
while [ $# -gt 0 ]; do
42-
title=$1
43-
command=$2
44-
completion="${completion}${title}${nl}"
45-
cases="${cases}
46-
($(shellquote "$title" s/¶/¶¶/g))
47-
printf '%s\\n' $(shellquote "$command" s/¶/¶¶/g)
48-
;;"
49-
if $select_cmds; then
50-
select_command=$3
51-
select_cases="${select_cases}
52-
($(shellquote "$title" s/¶/¶¶/g))
53-
printf '%s\\n' $(shellquote "$select_command" s/¶/¶¶/g)
54-
;;"
55-
fi
56-
shift $stride
57-
done
58-
printf "\
59-
prompt '' %%§
60-
evaluate-commands %%sh¶
61-
case \"\$kak_text\" in \
62-
%s
63-
(*) echo fail -- no such item: \"'\$(printf %%s \"\$kak_text\" | sed \"s/'/''/g\")'\" ;;
64-
esac
65-
66-
§" "$cases"
67-
if $select_cmds; then
68-
printf " \
69-
-on-change %%§
70-
evaluate-commands %%sh¶
71-
case \"\$kak_text\" in \
72-
%s
73-
(*) : ;;
74-
esac
75-
76-
§" "$select_cases"
77-
fi
78-
if [ -n "$on_abort" ]; then
79-
printf " -on-abort '%s'" "$(printf %s "$on_abort" | sed "s/'/''/g")"
80-
fi
81-
printf ' -menu -shell-script-candidates %%§
82-
printf %%s %s
83-
§\n' "$(shellquote "$completion")"
17+
echo >$kak_command_fifo "echo -to-file $kak_response_fifo -quoting kakoune -- %reg{a}"
18+
perl < $kak_response_fifo -we '
19+
use strict;
20+
my $Q = "'\''";
21+
my @args = ();
22+
{
23+
my $arg = undef;
24+
my $prev_is_quote = 0;
25+
my $state = "before-arg";
26+
while (not eof(STDIN)) {
27+
my $c = getc(STDIN);
28+
if ($state eq "before-arg") {
29+
($c eq $Q) or die "bad char: $c";
30+
$state = "in-arg";
31+
$arg = "";
32+
} elsif ($state eq "in-arg") {
33+
if ($prev_is_quote) {
34+
$prev_is_quote = 0;
35+
if ($c eq $Q) {
36+
$arg .= $Q;
37+
next;
38+
}
39+
($c eq " ") or die "bad char: $c";
40+
push @args, $arg;
41+
$state = "before-arg";
42+
next;
43+
} elsif ($c eq $Q) {
44+
$prev_is_quote = 1;
45+
next;
46+
}
47+
$arg .= $c;
48+
}
49+
}
50+
($state eq "in-arg") or die "expected $Q as last char";
51+
push @args, $arg;
52+
}
53+
54+
my $auto_single = 0;
55+
my $select_cmds = 0;
56+
my $on_abort = "";
57+
while (defined $args[0] && $args[0] =~ m/^-/) {
58+
if ($args[0] eq "--") {
59+
shift @args;
60+
last;
61+
}
62+
if ($args[0] eq "-auto-single") {
63+
$auto_single = 1;
64+
}
65+
if ($args[0] eq "-select-cmds") {
66+
$select_cmds = 1;
67+
}
68+
if ($args[0] eq "-on-abort") {
69+
if (not defined $args[1]) {
70+
print "fail %{menu: missing argument to -on-abort}";
71+
exit;
72+
}
73+
$on_abort = $args[1];
74+
shift @args;
75+
}
76+
shift @args;
77+
}
78+
my $stride = 2 + $select_cmds;
79+
if (scalar @args == 0 or scalar @args % $stride != 0) {
80+
print "fail %{menu: wrong argument count}";
81+
exit;
82+
}
83+
if ($auto_single && scalar @args == $stride) {
84+
print $args[$0];
85+
exit;
86+
}
87+
88+
sub shellquote {
89+
my $arg = shift;
90+
$arg =~ s/$Q/$Q\\$Q$Q/g;
91+
return "$Q$arg$Q";
92+
}
93+
sub kakquote {
94+
my $arg = shift;
95+
$arg =~ s/$Q/$Q$Q/g;
96+
return "$Q$arg$Q";
97+
}
98+
99+
my $accept_cases = "";
100+
my $select_cases = "";
101+
my $completions = "";
102+
sub case_clause {
103+
my $name = shellquote shift;
104+
my $command = shellquote shift;
105+
return "($name)\n"
106+
. " printf \"%s\n\" $command ;;\n";
107+
}
108+
for (my $i = 0; $i < scalar @args; $i += $stride) {
109+
my $name = $args[$i];
110+
my $command = $args[$i+1];
111+
$accept_cases .= case_clause $name, $command;
112+
$select_cases .= case_clause $name, $args[$i+2] if $select_cmds;
113+
$completions .= "$name\n";
114+
}
115+
use File::Temp qw(tempdir);
116+
my $tmpdir = tempdir;
117+
sub put {
118+
my $name = shift;
119+
my $contents = shift;
120+
my $filename = "$tmpdir/$name";
121+
open my $fh, ">", "$filename" or die "failed to open $filename: $!";
122+
print $fh $contents or die "write: $!";
123+
close $fh or die "close: $!";
124+
return $filename;
125+
};
126+
my $on_accept = put "on-accept",
127+
"case \"\$kak_text\" in\n" .
128+
"$accept_cases" .
129+
"(*) echo fail -- no such item: \"$Q\$(printf %s \"\$kak_text\" | sed \"s/$Q/$Q$Q/g\")$Q\";\n" .
130+
"esac\n";
131+
my $on_change = put "on-change",
132+
"case \"\$kak_text\" in\n" .
133+
"$select_cases" .
134+
"esac\n";
135+
my $shell_script_candidates = put "shell-script-candidates", $completions;
136+
137+
print "prompt %{} %{ evaluate-commands %sh{. $on_accept kak_text; rm -r $tmpdir} }";
138+
print " -on-abort " . kakquote "nop %sh{rm -r $tmpdir}; $on_abort";
139+
if ($select_cmds) {
140+
print " -on-change %{ evaluate-commands %sh{. $on_change kak_text} }";
141+
}
142+
print " -menu -shell-script-candidates %{cat $shell_script_candidates}";
143+
' ||
144+
echo 'fail menu: encountered an error, see *debug* buffer';
84145
}
85146
}

0 commit comments

Comments
 (0)