Skip to content

Commit

Permalink
Account for existing backslashes in completion
Browse files Browse the repository at this point in the history
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
  • Loading branch information
magicant committed Apr 14, 2024
1 parent 76694b7 commit a47afee
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 21 deletions.
3 changes: 3 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ History of Yash
- [line-editing] Fixed the spurious error message printed when
completing after `git config alias.` with the nounset shell option
enabled.
- [line-editing] Completion no longer inserts a redundant backslash
to escape a character included in the completed word
when the cursor follows another backslash.

## Yash 2.56.1 (2024-03-20)

Expand Down
3 changes: 3 additions & 0 deletions NEWS.ja
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Yash 更新履歴

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

## Yash 2.56.1 (2024-03-20)

Expand Down
56 changes: 41 additions & 15 deletions lineedit/complete.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* Yash: yet another shell */
/* complete.c: command line completion */
/* (C) 2007-2022 magicant */
/* (C) 2007-2024 magicant */

/* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand Down Expand Up @@ -486,10 +486,12 @@ void print_context_info(const le_context_T *ctxt)
{
const char *s DUMMY_INIT(NULL);
switch (ctxt->quote) {
case QUOTE_NONE: s = "none"; break;
case QUOTE_NORMAL: s = "normal"; break;
case QUOTE_SINGLE: s = "single"; break;
case QUOTE_DOUBLE: s = "double"; break;
case QUOTE_NONE: s = "none"; break;
case QUOTE_NORMAL: s = "normal"; break;
case QUOTE_NORMAL_ESCAPED: s = "normal escaped"; break;
case QUOTE_SINGLE: s = "single"; break;
case QUOTE_DOUBLE: s = "double"; break;
case QUOTE_DOUBLE_ESCAPED: s = "double escaped"; break;
}
le_compdebug("quote type: %s", s);
switch (ctxt->type & CTXT_MASK) {
Expand Down Expand Up @@ -1177,13 +1179,9 @@ size_t get_common_prefix_length(void)
* up after this function. */
void update_main_buffer(bool subst, bool finish)
{
const le_candidate_T *cand;
xwcsbuf_T buf;
size_t srclen;
size_t substindex;
le_quote_T quotetype;

wb_init(&buf);
if (subst) {
srclen = 0;
substindex = ctxt->srcindex;
Expand All @@ -1192,8 +1190,29 @@ void update_main_buffer(bool subst, bool finish)
srclen = wcslen(ctxt->src);
substindex = ctxt->origindex;
quotetype = ctxt->quote;

switch (quotetype) {
case QUOTE_NONE:
case QUOTE_NORMAL:
case QUOTE_SINGLE:
case QUOTE_DOUBLE:
break;
case QUOTE_NORMAL_ESCAPED:
case QUOTE_DOUBLE_ESCAPED:
// Get the backslash preceding the cursor removed
assert(substindex > 0);
substindex--;
assert(le_main_buffer.contents[substindex] == '\\');
break;
}
}

// Quote the substring of the candidate to be inserted
const le_candidate_T *cand;
xwcsbuf_T quoted;
wb_init(&quoted);
if (le_selected_candidate_index >= le_candidates.length) {
// No candidate selected. Quote the longest common prefix.
size_t cpl = get_common_prefix_length();
assert(srclen <= cpl);
cand = le_candidates.contents[0];
Expand All @@ -1202,21 +1221,24 @@ void update_main_buffer(bool subst, bool finish)
wchar_t value[valuelen + 1];
wcsncpy(value, cand->origvalue + srclen, valuelen);
value[valuelen] = L'\0';
quote(&buf, value, quotetype);
quote(&quoted, value, quotetype);
} else {
// Quote the selected candidate.
cand = le_candidates.contents[le_selected_candidate_index];
assert(srclen <= wcslen(cand->origvalue));
if (cand->origvalue[0] == L'\0' && quotetype == QUOTE_NORMAL)
wb_cat(&buf, L"\"\"");
wb_cat(&quoted, L"\"\"");
else
quote(&buf, cand->origvalue + srclen, quotetype);
quote(&quoted, cand->origvalue + srclen, quotetype);
}

// Insert the quoted candidate to the main buffer
assert(le_main_index >= substindex);
wb_replace_force(&le_main_buffer,
substindex, le_main_index - substindex,
buf.contents, buf.length);
le_main_index = substindex + buf.length;
wb_destroy(&buf);
quoted.contents, quoted.length);
le_main_index = substindex + quoted.length;
wb_destroy(&quoted);

if (le_selected_candidate_index >= le_candidates.length)
return;
Expand All @@ -1227,11 +1249,13 @@ void update_main_buffer(bool subst, bool finish)
switch (quotetype) {
case QUOTE_NONE:
case QUOTE_NORMAL:
case QUOTE_NORMAL_ESCAPED:
break;
case QUOTE_SINGLE:
insert_to_main_buffer(L'\'');
break;
case QUOTE_DOUBLE:
case QUOTE_DOUBLE_ESCAPED:
insert_to_main_buffer(L'"');
break;
}
Expand Down Expand Up @@ -1331,6 +1355,7 @@ void quote(xwcsbuf_T *restrict buf,
wb_cat(buf, s);
return;
case QUOTE_NORMAL:
case QUOTE_NORMAL_ESCAPED:
for (size_t i = 0; s[i] != L'\0'; i++) {
if (s[i] == L'\n') {
wb_ncat_force(buf, L"'\n'", 3);
Expand All @@ -1350,6 +1375,7 @@ void quote(xwcsbuf_T *restrict buf,
}
return;
case QUOTE_DOUBLE:
case QUOTE_DOUBLE_ESCAPED:
for (size_t i = 0; s[i] != L'\0'; i++) {
if (wcschr(CHARS_ESCAPABLE, s[i]) != NULL)
wb_wccat(buf, L'\\');
Expand Down
4 changes: 3 additions & 1 deletion lineedit/complete.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* Yash: yet another shell */
/* complete.h: command line completion */
/* (C) 2007-2022 magicant */
/* (C) 2007-2024 magicant */

/* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand Down Expand Up @@ -64,8 +64,10 @@ typedef struct le_candidate_T {
typedef enum le_quote_T {
QUOTE_NONE, // no quotation required
QUOTE_NORMAL, // not in single/double quotation; backslashes can be used
QUOTE_NORMAL_ESCAPED, // like QUOTE_NORMAL, but the cursor is backslashed
QUOTE_SINGLE, // in single quotation
QUOTE_DOUBLE, // in double quotation
QUOTE_DOUBLE_ESCAPED, // like QUOTE_DOUBLE, but the cursor is backslashed
} le_quote_T;
typedef enum le_contexttype_T {
CTXT_NORMAL, // normal word
Expand Down
16 changes: 11 additions & 5 deletions lineedit/compparse.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* Yash: yet another shell */
/* compparse.c: simple parser for command line completion */
/* (C) 2007-2020 magicant */
/* (C) 2007-2024 magicant */

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

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

Expand All @@ -638,11 +639,14 @@ wordunit_T *cparse_word(
case L'\0':
goto done;
case L'\\':
if (BUF[INDEX + 1] != L'\0') {
if (BUF[INDEX + 1] == L'\0') {
escaped = true;
INDEX += 1;
goto done;
} else {
INDEX += 2;
continue;
}
break;
case L'$':
MAKE_WORDUNIT_STRING;
wu = cparse_special_word_unit(
Expand Down Expand Up @@ -701,7 +705,9 @@ wordunit_T *cparse_word(
assert(first != NULL);
return first;
} else {
pi->ctxt->quote = indq ? QUOTE_DOUBLE : QUOTE_NORMAL;
pi->ctxt->quote = indq ?
escaped ? QUOTE_DOUBLE_ESCAPED : QUOTE_DOUBLE :
escaped ? QUOTE_NORMAL_ESCAPED : QUOTE_NORMAL;
goto finish;
}

Expand Down

0 comments on commit a47afee

Please sign in to comment.