Skip to content

Commit a47afee

Browse files
committed
Account for existing backslashes in completion
The previous implementation assumed that there was no backslash escape preceding the cursor when performing command line completion. This caused a redundant backslash to be inserted when the next character in the completed word needed escaping. After this commit, the shell checks for an escape before the current position and removes it when inserting the completion, leaving the correct number of backslashes. Fixes #47
1 parent 76694b7 commit a47afee

File tree

5 files changed

+61
-21
lines changed

5 files changed

+61
-21
lines changed

NEWS

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ History of Yash
55
- [line-editing] Fixed the spurious error message printed when
66
completing after `git config alias.` with the nounset shell option
77
enabled.
8+
- [line-editing] Completion no longer inserts a redundant backslash
9+
to escape a character included in the completed word
10+
when the cursor follows another backslash.
811

912
## Yash 2.56.1 (2024-03-20)
1013

NEWS.ja

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ Yash 更新履歴
44

55
- [行編集] nounset オプション有効時に `git config alias.` に続けて
66
補完をしようとするとエラーが出るのを修正
7+
- [行編集] カーソルがバックスラッシュの直後にある時に補完をすると
8+
補完する単語に含まれるエスケープが必要な文字に対して
9+
余計なバックスラッシュが挿入されるのを修正
710

811
## Yash 2.56.1 (2024-03-20)
912

lineedit/complete.c

+41-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* Yash: yet another shell */
22
/* complete.c: command line completion */
3-
/* (C) 2007-2022 magicant */
3+
/* (C) 2007-2024 magicant */
44

55
/* This program is free software: you can redistribute it and/or modify
66
* it under the terms of the GNU General Public License as published by
@@ -486,10 +486,12 @@ void print_context_info(const le_context_T *ctxt)
486486
{
487487
const char *s DUMMY_INIT(NULL);
488488
switch (ctxt->quote) {
489-
case QUOTE_NONE: s = "none"; break;
490-
case QUOTE_NORMAL: s = "normal"; break;
491-
case QUOTE_SINGLE: s = "single"; break;
492-
case QUOTE_DOUBLE: s = "double"; break;
489+
case QUOTE_NONE: s = "none"; break;
490+
case QUOTE_NORMAL: s = "normal"; break;
491+
case QUOTE_NORMAL_ESCAPED: s = "normal escaped"; break;
492+
case QUOTE_SINGLE: s = "single"; break;
493+
case QUOTE_DOUBLE: s = "double"; break;
494+
case QUOTE_DOUBLE_ESCAPED: s = "double escaped"; break;
493495
}
494496
le_compdebug("quote type: %s", s);
495497
switch (ctxt->type & CTXT_MASK) {
@@ -1177,13 +1179,9 @@ size_t get_common_prefix_length(void)
11771179
* up after this function. */
11781180
void update_main_buffer(bool subst, bool finish)
11791181
{
1180-
const le_candidate_T *cand;
1181-
xwcsbuf_T buf;
11821182
size_t srclen;
11831183
size_t substindex;
11841184
le_quote_T quotetype;
1185-
1186-
wb_init(&buf);
11871185
if (subst) {
11881186
srclen = 0;
11891187
substindex = ctxt->srcindex;
@@ -1192,8 +1190,29 @@ void update_main_buffer(bool subst, bool finish)
11921190
srclen = wcslen(ctxt->src);
11931191
substindex = ctxt->origindex;
11941192
quotetype = ctxt->quote;
1193+
1194+
switch (quotetype) {
1195+
case QUOTE_NONE:
1196+
case QUOTE_NORMAL:
1197+
case QUOTE_SINGLE:
1198+
case QUOTE_DOUBLE:
1199+
break;
1200+
case QUOTE_NORMAL_ESCAPED:
1201+
case QUOTE_DOUBLE_ESCAPED:
1202+
// Get the backslash preceding the cursor removed
1203+
assert(substindex > 0);
1204+
substindex--;
1205+
assert(le_main_buffer.contents[substindex] == '\\');
1206+
break;
1207+
}
11951208
}
1209+
1210+
// Quote the substring of the candidate to be inserted
1211+
const le_candidate_T *cand;
1212+
xwcsbuf_T quoted;
1213+
wb_init(&quoted);
11961214
if (le_selected_candidate_index >= le_candidates.length) {
1215+
// No candidate selected. Quote the longest common prefix.
11971216
size_t cpl = get_common_prefix_length();
11981217
assert(srclen <= cpl);
11991218
cand = le_candidates.contents[0];
@@ -1202,21 +1221,24 @@ void update_main_buffer(bool subst, bool finish)
12021221
wchar_t value[valuelen + 1];
12031222
wcsncpy(value, cand->origvalue + srclen, valuelen);
12041223
value[valuelen] = L'\0';
1205-
quote(&buf, value, quotetype);
1224+
quote(&quoted, value, quotetype);
12061225
} else {
1226+
// Quote the selected candidate.
12071227
cand = le_candidates.contents[le_selected_candidate_index];
12081228
assert(srclen <= wcslen(cand->origvalue));
12091229
if (cand->origvalue[0] == L'\0' && quotetype == QUOTE_NORMAL)
1210-
wb_cat(&buf, L"\"\"");
1230+
wb_cat(&quoted, L"\"\"");
12111231
else
1212-
quote(&buf, cand->origvalue + srclen, quotetype);
1232+
quote(&quoted, cand->origvalue + srclen, quotetype);
12131233
}
1234+
1235+
// Insert the quoted candidate to the main buffer
12141236
assert(le_main_index >= substindex);
12151237
wb_replace_force(&le_main_buffer,
12161238
substindex, le_main_index - substindex,
1217-
buf.contents, buf.length);
1218-
le_main_index = substindex + buf.length;
1219-
wb_destroy(&buf);
1239+
quoted.contents, quoted.length);
1240+
le_main_index = substindex + quoted.length;
1241+
wb_destroy(&quoted);
12201242

12211243
if (le_selected_candidate_index >= le_candidates.length)
12221244
return;
@@ -1227,11 +1249,13 @@ void update_main_buffer(bool subst, bool finish)
12271249
switch (quotetype) {
12281250
case QUOTE_NONE:
12291251
case QUOTE_NORMAL:
1252+
case QUOTE_NORMAL_ESCAPED:
12301253
break;
12311254
case QUOTE_SINGLE:
12321255
insert_to_main_buffer(L'\'');
12331256
break;
12341257
case QUOTE_DOUBLE:
1258+
case QUOTE_DOUBLE_ESCAPED:
12351259
insert_to_main_buffer(L'"');
12361260
break;
12371261
}
@@ -1331,6 +1355,7 @@ void quote(xwcsbuf_T *restrict buf,
13311355
wb_cat(buf, s);
13321356
return;
13331357
case QUOTE_NORMAL:
1358+
case QUOTE_NORMAL_ESCAPED:
13341359
for (size_t i = 0; s[i] != L'\0'; i++) {
13351360
if (s[i] == L'\n') {
13361361
wb_ncat_force(buf, L"'\n'", 3);
@@ -1350,6 +1375,7 @@ void quote(xwcsbuf_T *restrict buf,
13501375
}
13511376
return;
13521377
case QUOTE_DOUBLE:
1378+
case QUOTE_DOUBLE_ESCAPED:
13531379
for (size_t i = 0; s[i] != L'\0'; i++) {
13541380
if (wcschr(CHARS_ESCAPABLE, s[i]) != NULL)
13551381
wb_wccat(buf, L'\\');

lineedit/complete.h

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* Yash: yet another shell */
22
/* complete.h: command line completion */
3-
/* (C) 2007-2022 magicant */
3+
/* (C) 2007-2024 magicant */
44

55
/* This program is free software: you can redistribute it and/or modify
66
* it under the terms of the GNU General Public License as published by
@@ -64,8 +64,10 @@ typedef struct le_candidate_T {
6464
typedef enum le_quote_T {
6565
QUOTE_NONE, // no quotation required
6666
QUOTE_NORMAL, // not in single/double quotation; backslashes can be used
67+
QUOTE_NORMAL_ESCAPED, // like QUOTE_NORMAL, but the cursor is backslashed
6768
QUOTE_SINGLE, // in single quotation
6869
QUOTE_DOUBLE, // in double quotation
70+
QUOTE_DOUBLE_ESCAPED, // like QUOTE_DOUBLE, but the cursor is backslashed
6971
} le_quote_T;
7072
typedef enum le_contexttype_T {
7173
CTXT_NORMAL, // normal word

lineedit/compparse.c

+11-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* Yash: yet another shell */
22
/* compparse.c: simple parser for command line completion */
3-
/* (C) 2007-2020 magicant */
3+
/* (C) 2007-2024 magicant */
44

55
/* This program is free software: you can redistribute it and/or modify
66
* it under the terms of the GNU General Public License as published by
@@ -618,7 +618,8 @@ wordunit_T *cparse_word(
618618
return NULL;
619619

620620
wordunit_T *first = NULL, **lastp = &first;
621-
bool indq = false; /* in double quotes? */
621+
bool indq = false; /* in double quotes? */
622+
bool escaped = false; /* after a backslash? */
622623
size_t startindex = INDEX;
623624
size_t srcindex = le_main_index - (LEN - INDEX);
624625

@@ -638,11 +639,14 @@ wordunit_T *cparse_word(
638639
case L'\0':
639640
goto done;
640641
case L'\\':
641-
if (BUF[INDEX + 1] != L'\0') {
642+
if (BUF[INDEX + 1] == L'\0') {
643+
escaped = true;
644+
INDEX += 1;
645+
goto done;
646+
} else {
642647
INDEX += 2;
643648
continue;
644649
}
645-
break;
646650
case L'$':
647651
MAKE_WORDUNIT_STRING;
648652
wu = cparse_special_word_unit(
@@ -701,7 +705,9 @@ wordunit_T *cparse_word(
701705
assert(first != NULL);
702706
return first;
703707
} else {
704-
pi->ctxt->quote = indq ? QUOTE_DOUBLE : QUOTE_NORMAL;
708+
pi->ctxt->quote = indq ?
709+
escaped ? QUOTE_DOUBLE_ESCAPED : QUOTE_DOUBLE :
710+
escaped ? QUOTE_NORMAL_ESCAPED : QUOTE_NORMAL;
705711
goto finish;
706712
}
707713

0 commit comments

Comments
 (0)