From 3ad8c08acc506555667a070cf83c410ac9334f1e Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 14 Apr 2023 19:45:54 +0800 Subject: vim-patch:8.2.4770: cannot easily mix expression and heredoc Problem: Cannot easily mix expression and heredoc. Solution: Support in heredoc. (Yegappan Lakshmanan, closes vim/vim#10138) https://github.com/vim/vim/commit/efbfa867a146fcd93fdec2435597aa4ae7f1325c Co-authored-by: Yegappan Lakshmanan --- runtime/doc/eval.txt | 29 ++++++++-- src/nvim/eval/userfunc.c | 17 ++++-- src/nvim/eval/vars.c | 119 ++++++++++++++++++++++++++++++++++++------ test/old/testdir/test_let.vim | 97 +++++++++++++++++++++++++++++++++- 4 files changed, 239 insertions(+), 23 deletions(-) diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index 351690f4df..c8eea03f5f 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -2540,14 +2540,30 @@ This does NOT work: > *:let=<<* *:let-heredoc* *E990* *E991* *E172* *E221* *E1145* -:let {var-name} =<< [trim] {endmarker} +:let {var-name} =<< [trim] [eval] {endmarker} text... text... {endmarker} Set internal variable {var-name} to a |List| containing the lines of text bounded by the string - {endmarker}. The lines of text is used as a - |literal-string|. + {endmarker}. + + If "eval" is not specified, then each line of text is + used as a |literal-string|. If "eval" is specified, + then any Vim expression in the form ``={expr}`` is + evaluated and the result replaces the expression. + Example where $HOME is expanded: > + let lines =<< trim eval END + some text + See the file `=$HOME`/.vimrc + more text + END +< There can be multiple Vim expressions in a single line + but an expression cannot span multiple lines. If any + expression evaluation fails, then the assignment fails. + once the "`=" has been found {expr} and a backtick + must follow. {expr} cannot be empty. + {endmarker} must not contain white space. {endmarker} cannot start with a lower case character. The last line should end only with the {endmarker} @@ -2597,6 +2613,13 @@ text... 1 2 3 4 5 6 7 8 DATA + + let code =<< trim eval CODE + let v = `=10 + 20` + let h = "`=$HOME`" + let s = "`=Str1()` abc `=Str2()`" + let n = `=MyFunc(3, 4)` + CODE < *E121* :let {var-name} .. List the value of variable {var-name}. Multiple diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c index 67c73924c8..67b1e53a35 100644 --- a/src/nvim/eval/userfunc.c +++ b/src/nvim/eval/userfunc.c @@ -2483,10 +2483,19 @@ void ex_function(exarg_T *eap) && (!ASCII_ISALNUM(p[2]) || (p[2] == 't' && !ASCII_ISALNUM(p[3]))))) { p = skipwhite(arg + 3); - if (strncmp(p, "trim", 4) == 0) { - // Ignore leading white space. - p = skipwhite(p + 4); - heredoc_trimmed = xstrnsave(theline, (size_t)(skipwhite(theline) - theline)); + while (true) { + if (strncmp(p, "trim", 4) == 0) { + // Ignore leading white space. + p = skipwhite(p + 4); + heredoc_trimmed = xstrnsave(theline, (size_t)(skipwhite(theline) - theline)); + continue; + } + if (strncmp(p, "eval", 4) == 0) { + // Ignore leading white space. + p = skipwhite(p + 4); + continue; + } + break; } skip_until = xstrnsave(p, (size_t)(skiptowhite(p) - p)); do_concat = false; diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c index b8a8f39437..3905cf82a6 100644 --- a/src/nvim/eval/vars.c +++ b/src/nvim/eval/vars.c @@ -53,6 +53,56 @@ static const char *e_letunexp = N_("E18: Unexpected characters in :let"); static const char *e_lock_unlock = N_("E940: Cannot lock or unlock variable %s"); +/// Evaluate all the Vim expressions (`=expr`) in string "str" and return the +/// resulting string. The caller must free the returned string. +static char *eval_all_expr_in_str(char *str) +{ + garray_T ga; + ga_init(&ga, 1, 80); + char *p = str; + + // Look for `=expr`, evaluate the expression and replace `=expr` with the + // result. + while (*p != NUL) { + char *s = p; + while (*p != NUL && (*p != '`' || p[1] != '=')) { + p++; + } + ga_concat_len(&ga, s, (size_t)(p - s)); + if (*p == NUL) { + break; // no backtick expression found + } + s = p; + p += 2; // skip `= + + int status = *p == NUL ? OK : skip_expr(&p, NULL); + if (status == FAIL || *p != '`') { + // invalid expression or missing ending backtick + if (status != FAIL) { + emsg(_("E1083: Missing backtick")); + } + xfree(ga.ga_data); + return NULL; + } + s += 2; // skip `= + char save_c = *p; + *p = NUL; + char *exprval = eval_to_string(s, true); + *p = save_c; + p++; + if (exprval == NULL) { + // expression evaluation failed + xfree(ga.ga_data); + return NULL; + } + ga_concat(&ga, exprval); + xfree(exprval); + } + ga_append(&ga, NUL); + + return ga.ga_data; +} + /// Get a list of lines from a HERE document. The here document is a list of /// lines surrounded by a marker. /// cmd << {marker} @@ -65,7 +115,7 @@ static const char *e_lock_unlock = N_("E940: Cannot lock or unlock variable %s") /// marker, then the leading indentation before the lines (matching the /// indentation in the 'cmd' line) is stripped. /// -/// @return a List with {lines} or NULL. +/// @return a List with {lines} or NULL on failure. static list_T *heredoc_get(exarg_T *eap, char *cmd) { char *marker; @@ -81,20 +131,33 @@ static list_T *heredoc_get(exarg_T *eap, char *cmd) // Check for the optional 'trim' word before the marker cmd = skipwhite(cmd); - if (strncmp(cmd, "trim", 4) == 0 - && (cmd[4] == NUL || ascii_iswhite(cmd[4]))) { - cmd = skipwhite(cmd + 4); - - // Trim the indentation from all the lines in the here document. - // The amount of indentation trimmed is the same as the indentation of - // the first line after the :let command line. To find the end marker - // the indent of the :let command line is trimmed. - p = *eap->cmdlinep; - while (ascii_iswhite(*p)) { - p++; - marker_indent_len++; + bool evalstr = false; + bool eval_failed = false; + while (true) { + if (strncmp(cmd, "trim", 4) == 0 + && (cmd[4] == NUL || ascii_iswhite(cmd[4]))) { + cmd = skipwhite(cmd + 4); + + // Trim the indentation from all the lines in the here document. + // The amount of indentation trimmed is the same as the indentation + // of the first line after the :let command line. To find the end + // marker the indent of the :let command line is trimmed. + p = *eap->cmdlinep; + while (ascii_iswhite(*p)) { + p++; + marker_indent_len++; + } + text_indent_len = -1; + + continue; + } + if (strncmp(cmd, "eval", 4) == 0 + && (cmd[4] == NUL || ascii_iswhite(cmd[4]))) { + cmd = skipwhite(cmd + 4); + evalstr = true; + continue; } - text_indent_len = -1; + break; } // The marker is the next word. @@ -136,6 +199,14 @@ static list_T *heredoc_get(exarg_T *eap, char *cmd) xfree(theline); break; } + + // If expression evaluation failed in the heredoc, then skip till the + // end marker. + if (eval_failed) { + xfree(theline); + continue; + } + if (text_indent_len == -1 && *theline != NUL) { // set the text indent from the first line. p = theline; @@ -155,11 +226,29 @@ static list_T *heredoc_get(exarg_T *eap, char *cmd) } } - tv_list_append_string(l, theline + ti, -1); + char *str = theline + ti; + if (evalstr) { + str = eval_all_expr_in_str(str); + if (str == NULL) { + // expression evaluation failed + xfree(theline); + eval_failed = true; + continue; + } + xfree(theline); + theline = str; + } + + tv_list_append_string(l, str, -1); xfree(theline); } xfree(text_indent); + if (eval_failed) { + // expression evaluation in the heredoc failed + tv_list_free(l); + return NULL; + } return l; } diff --git a/test/old/testdir/test_let.vim b/test/old/testdir/test_let.vim index 6fbf305624..fe14709e71 100644 --- a/test/old/testdir/test_let.vim +++ b/test/old/testdir/test_let.vim @@ -1,5 +1,7 @@ " Tests for the :let command. +source vim9.vim + func Test_let() " Test to not autoload when assigning. It causes internal error. set runtimepath+=./sautest @@ -385,7 +387,8 @@ END call assert_equal(['Text', 'with', 'indent'], text) endfunc -" Test for the setting a variable using the heredoc syntax +" Test for the setting a variable using the heredoc syntax. +" Keep near the end, this messes up highlighting. func Test_let_heredoc() let var1 =<< END Some sample text @@ -493,4 +496,96 @@ END call assert_equal([' x', ' \y', ' z'], [a, b, c]) endfunc +" Test for evaluating Vim expressions in a heredoc using `=expr` +" Keep near the end, this messes up highlighting. +func Test_let_heredoc_eval() + let str = '' + let code =<< trim eval END + let a = `=5 + 10` + let b = `=min([10, 6])` + `=max([4, 6])` + `=str` + let c = "abc`=str`d" + END + call assert_equal(['let a = 15', 'let b = 6 + 6', '', 'let c = "abcd"'], code) + let $TESTVAR = "Hello" + let code =<< eval trim END + let s = "`=$TESTVAR`" + END + call assert_equal(['let s = "Hello"'], code) + let code =<< eval END + let s = "`=$TESTVAR`" +END + call assert_equal([' let s = "Hello"'], code) + let a = 10 + let data =<< eval END +`=a` +END + call assert_equal(['10'], data) + let x = 'X' + let code =<< eval trim END + let a = `abc` + let b = `=x` + let c = ` + END + call assert_equal(['let a = `abc`', 'let b = X', 'let c = `'], code) + let code = 'xxx' + let code =<< eval trim END + let n = `=5 + + 6` + END + call assert_equal('xxx', code) + let code =<< eval trim END + let n = `=min([1, 2]` + `=max([3, 4])` + END + call assert_equal('xxx', code) + + let lines =<< trim LINES + let text =<< eval trim END + let b = `= + END + LINES + call CheckScriptFailure(lines, 'E1083:') + + let lines =<< trim LINES + let text =<< eval trim END + let b = `=abc + END + LINES + call CheckScriptFailure(lines, 'E1083:') + + let lines =<< trim LINES + let text =<< eval trim END + let b = `=` + END + LINES + call CheckScriptFailure(lines, 'E15:') + + " Test for sourcing a script containing a heredoc with invalid expression. + " Variable assignment should fail, if expression evaluation fails + new + let g:Xvar = 'test' + let g:b = 10 + let lines =<< trim END + let Xvar =<< eval CODE + let a = 1 + let b = `=5+` + let c = 2 + CODE + let g:Count += 1 + END + call setline(1, lines) + let g:Count = 0 + call assert_fails('source', 'E15:') + call assert_equal(1, g:Count) + call setline(3, 'let b = `=abc`') + call assert_fails('source', 'E121:') + call assert_equal(2, g:Count) + call setline(3, 'let b = `=abc` + `=min([9, 4])` + 2') + call assert_fails('source', 'E121:') + call assert_equal(3, g:Count) + call assert_equal('test', g:Xvar) + call assert_equal(10, g:b) + bw! +endfunc + " vim: shiftwidth=2 sts=2 expandtab -- cgit From 3c16e75ae194f728c703032084a8f6dd0833a563 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 14 Apr 2023 21:00:08 +0800 Subject: vim-patch:8.2.4783: Coverity warns for leaking memory Problem: Coverity warns for leaking memory. Solution: Use another strategy freeing "theline". https://github.com/vim/vim/commit/42ccb8d74700506936567b0eb6d11def5e25e1dd Co-authored-by: Bram Moolenaar --- src/nvim/eval/vars.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c index 3905cf82a6..048b5ee2aa 100644 --- a/src/nvim/eval/vars.c +++ b/src/nvim/eval/vars.c @@ -178,12 +178,14 @@ static list_T *heredoc_get(exarg_T *eap, char *cmd) return NULL; } + char *theline = NULL; list_T *l = tv_list_alloc(0); for (;;) { int mi = 0; int ti = 0; - char *theline = eap->getline(NUL, eap->cookie, 0, false); + xfree(theline); + theline = eap->getline(NUL, eap->cookie, 0, false); if (theline == NULL) { semsg(_("E990: Missing end marker '%s'"), marker); break; @@ -196,14 +198,12 @@ static list_T *heredoc_get(exarg_T *eap, char *cmd) mi = marker_indent_len; } if (strcmp(marker, theline + mi) == 0) { - xfree(theline); break; } // If expression evaluation failed in the heredoc, then skip till the // end marker. if (eval_failed) { - xfree(theline); continue; } @@ -231,7 +231,6 @@ static list_T *heredoc_get(exarg_T *eap, char *cmd) str = eval_all_expr_in_str(str); if (str == NULL) { // expression evaluation failed - xfree(theline); eval_failed = true; continue; } @@ -240,8 +239,8 @@ static list_T *heredoc_get(exarg_T *eap, char *cmd) } tv_list_append_string(l, str, -1); - xfree(theline); } + xfree(theline); xfree(text_indent); if (eval_failed) { -- cgit From 2cf8f01e7d0469b592bacecd5f224b4fe3149a62 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 14 Apr 2023 21:06:15 +0800 Subject: vim-patch:8.2.4840: heredoc expression evaluated even when skipping Problem: Heredoc expression evaluated even when skipping. Solution: Don't evaluate when "skip" is set. (closes vim/vim#10306) https://github.com/vim/vim/commit/05c7f5d3d03440da6f69604f8c06c4e3d90d2a26 Co-authored-by: Bram Moolenaar --- src/nvim/eval/vars.c | 2 +- test/old/testdir/test_let.vim | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c index 048b5ee2aa..a8d1e01152 100644 --- a/src/nvim/eval/vars.c +++ b/src/nvim/eval/vars.c @@ -227,7 +227,7 @@ static list_T *heredoc_get(exarg_T *eap, char *cmd) } char *str = theline + ti; - if (evalstr) { + if (evalstr && !eap->skip) { str = eval_all_expr_in_str(str); if (str == NULL) { // expression evaluation failed diff --git a/test/old/testdir/test_let.vim b/test/old/testdir/test_let.vim index fe14709e71..915bba2314 100644 --- a/test/old/testdir/test_let.vim +++ b/test/old/testdir/test_let.vim @@ -507,20 +507,24 @@ func Test_let_heredoc_eval() let c = "abc`=str`d" END call assert_equal(['let a = 15', 'let b = 6 + 6', '', 'let c = "abcd"'], code) + let $TESTVAR = "Hello" let code =<< eval trim END let s = "`=$TESTVAR`" END call assert_equal(['let s = "Hello"'], code) + let code =<< eval END let s = "`=$TESTVAR`" END call assert_equal([' let s = "Hello"'], code) + let a = 10 let data =<< eval END `=a` END call assert_equal(['10'], data) + let x = 'X' let code =<< eval trim END let a = `abc` @@ -528,12 +532,14 @@ END let c = ` END call assert_equal(['let a = `abc`', 'let b = X', 'let c = `'], code) + let code = 'xxx' let code =<< eval trim END let n = `=5 + 6` END call assert_equal('xxx', code) + let code =<< eval trim END let n = `=min([1, 2]` + `=max([3, 4])` END @@ -560,6 +566,13 @@ END LINES call CheckScriptFailure(lines, 'E15:') + " skipped heredoc + if 0 + let msg =<< trim eval END + n is: `=n` + END + endif + " Test for sourcing a script containing a heredoc with invalid expression. " Variable assignment should fail, if expression evaluation fails new -- cgit From bacb5021d4eff33c67eb659fb01125b2abcacd79 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 14 Apr 2023 21:08:00 +0800 Subject: vim-patch:8.2.4883: string interpolation only works in heredoc Problem: String interpolation only works in heredoc. Solution: Support interpolated strings. Use syntax for heredoc consistent with strings, similar to C#. (closes vim/vim#10327) https://github.com/vim/vim/commit/2eaef106e4a7fc9dc74a7e672b5f550ec1f9786e Cherry-pick Test_Debugger_breakadd_expr() from Vim. Co-authored-by: LemonBoy --- runtime/doc/eval.txt | 20 ++++++++++ src/nvim/eval.c | 33 +++++++++++++++- src/nvim/eval/vars.c | 81 ++++++++++++++++++++++++-------------- src/nvim/globals.h | 5 +++ test/old/testdir/test_debugger.vim | 33 ++++++++++++++++ test/old/testdir/test_expr.vim | 56 ++++++++++++++++++++++++++ test/old/testdir/test_let.vim | 59 ++++++++++++++++----------- 7 files changed, 233 insertions(+), 54 deletions(-) diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index c8eea03f5f..518a190d3c 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -1406,6 +1406,26 @@ to be doubled. These two commands are equivalent: > if a =~ '\s*' +------------------------------------------------------------------------------ +interpolated-string *interp-string* + +$"string" interpolated string constant *expr-$quote* +$'string' interpolated literal string constant *expr-$'* + +Interpolated strings are an extension of the |string| and |literal-string|, +allowing the inclusion of Vim script expressions (see |expr1|). Any +expression returning a value can be enclosed between curly braces. The value +is converted to a string. All the text and results of the expressions +are concatenated to make a new string. + +To include an opening brace '{' or closing brace '}' in the string content +double it. + +Examples: > + let your_name = input("What's your name? ") + echo $"Hello, {your_name}!" + echo $"The square root of 9 is {sqrt(9)}" + ------------------------------------------------------------------------------ option *expr-option* *E112* *E113* diff --git a/src/nvim/eval.c b/src/nvim/eval.c index b240c36977..f8a9326703 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -3100,8 +3100,13 @@ static int eval7(char **arg, typval_T *rettv, evalarg_T *const evalarg, bool wan ret = eval_option((const char **)arg, rettv, evaluate); break; // Environment variable: $VAR. + // Interpolated string: $"string" or $'string'. case '$': - ret = eval_env_var(arg, rettv, evaluate); + if ((*arg)[1] == '"' || (*arg)[1] == '\'') { + ret = eval_interp_string(arg, rettv, evaluate); + } else { + ret = eval_env_var(arg, rettv, evaluate); + } break; // Register contents: @r. @@ -4053,6 +4058,32 @@ static int eval_lit_string(char **arg, typval_T *rettv, int evaluate) return OK; } +int eval_interp_string(char **arg, typval_T *rettv, int evaluate) +{ + // *arg is on the '$' character. + (*arg)++; + + rettv->v_type = VAR_STRING; + + typval_T tv; + int ret; + if (**arg == '"') { + ret = eval_string(arg, &tv, evaluate); + } else { + ret = eval_lit_string(arg, &tv, evaluate); + } + + if (ret == FAIL || !evaluate) { + return ret; + } + + rettv->vval.v_string = eval_all_expr_in_str(tv.vval.v_string); + + tv_clear(&tv); + + return rettv->vval.v_string != NULL ? OK : FAIL; +} + /// @return the function name of the partial. char *partial_name(partial_T *pt) FUNC_ATTR_PURE diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c index a8d1e01152..b86c49fd98 100644 --- a/src/nvim/eval/vars.c +++ b/src/nvim/eval/vars.c @@ -53,50 +53,73 @@ static const char *e_letunexp = N_("E18: Unexpected characters in :let"); static const char *e_lock_unlock = N_("E940: Cannot lock or unlock variable %s"); -/// Evaluate all the Vim expressions (`=expr`) in string "str" and return the +/// Evaluate all the Vim expressions ({expr}) in string "str" and return the /// resulting string. The caller must free the returned string. -static char *eval_all_expr_in_str(char *str) +char *eval_all_expr_in_str(char *str) { garray_T ga; ga_init(&ga, 1, 80); char *p = str; - // Look for `=expr`, evaluate the expression and replace `=expr` with the - // result. while (*p != NUL) { - char *s = p; - while (*p != NUL && (*p != '`' || p[1] != '=')) { - p++; + bool escaped_brace = false; + + // Look for a block start. + char *lit_start = p; + while (*p != '{' && *p != '}' && *p != NUL) { + ++p; + } + + if (*p != NUL && *p == p[1]) { + // Escaped brace, unescape and continue. + // Include the brace in the literal string. + ++p; + escaped_brace = true; + } else if (*p == '}') { + semsg(_(e_stray_closing_curly_str), str); + ga_clear(&ga); + return NULL; } - ga_concat_len(&ga, s, (size_t)(p - s)); + + // Append the literal part. + ga_concat_len(&ga, lit_start, (size_t)(p - lit_start)); + if (*p == NUL) { - break; // no backtick expression found + break; } - s = p; - p += 2; // skip `= - int status = *p == NUL ? OK : skip_expr(&p, NULL); - if (status == FAIL || *p != '`') { - // invalid expression or missing ending backtick - if (status != FAIL) { - emsg(_("E1083: Missing backtick")); - } - xfree(ga.ga_data); + if (escaped_brace) { + // Skip the second brace. + ++p; + continue; + } + + // Skip the opening {. + char *block_start = ++p; + char *block_end = block_start; + if (*block_start != NUL && skip_expr(&block_end, NULL) == FAIL) { + ga_clear(&ga); return NULL; } - s += 2; // skip `= - char save_c = *p; - *p = NUL; - char *exprval = eval_to_string(s, true); - *p = save_c; - p++; - if (exprval == NULL) { - // expression evaluation failed - xfree(ga.ga_data); + block_end = skipwhite(block_end); + // The block must be closed by a }. + if (*block_end != '}') { + semsg(_(e_missing_close_curly_str), str); + ga_clear(&ga); return NULL; } - ga_concat(&ga, exprval); - xfree(exprval); + char save_c = *block_end; + *block_end = NUL; + char *expr_val = eval_to_string(block_start, true); + *block_end = save_c; + if (expr_val == NULL) { + ga_clear(&ga); + return NULL; + } + ga_concat(&ga, expr_val); + xfree(expr_val); + + p = block_end + 1; } ga_append(&ga, NUL); diff --git a/src/nvim/globals.h b/src/nvim/globals.h index e406d93494..11888a5df8 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -1024,6 +1024,11 @@ EXTERN const char e_highlight_group_name_too_long[] INIT(= N_("E1249: Highlight EXTERN const char e_invalid_line_number_nr[] INIT(= N_("E966: Invalid line number: %ld")); +EXTERN char e_stray_closing_curly_str[] +INIT(= N_("E1278: Stray '}' without a matching '{': %s")); +EXTERN char e_missing_close_curly_str[] +INIT(= N_("E1279: Missing '}': %s")); + EXTERN const char e_undobang_cannot_redo_or_move_branch[] INIT(= N_("E5767: Cannot use :undo! to redo or move to a different undo branch")); diff --git a/test/old/testdir/test_debugger.vim b/test/old/testdir/test_debugger.vim index f5177c8fb2..18616e8717 100644 --- a/test/old/testdir/test_debugger.vim +++ b/test/old/testdir/test_debugger.vim @@ -348,6 +348,39 @@ func Test_Debugger_breakadd() call assert_fails('breakadd file Xtest.vim /\)/', 'E55:') endfunc +" Test for expression breakpoint set using ":breakadd expr " +func Test_Debugger_breakadd_expr() + CheckRunVimInTerminal + let lines =<< trim END + let g:Xtest_var += 1 + END + call writefile(lines, 'Xtest.vim') + + " Start Vim in a terminal + let buf = RunVimInTerminal('Xtest.vim', {}) + call RunDbgCmd(buf, ':let g:Xtest_var = 10') + call RunDbgCmd(buf, ':breakadd expr g:Xtest_var') + call RunDbgCmd(buf, ':source %') + let expected =<< eval trim END + Oldval = "10" + Newval = "11" + {fnamemodify('Xtest.vim', ':p')} + line 1: let g:Xtest_var += 1 + END + call RunDbgCmd(buf, ':source %', expected) + call RunDbgCmd(buf, 'cont') + let expected =<< eval trim END + Oldval = "11" + Newval = "12" + {fnamemodify('Xtest.vim', ':p')} + line 1: let g:Xtest_var += 1 + END + call RunDbgCmd(buf, ':source %', expected) + + call StopVimInTerminal(buf) + call delete('Xtest.vim') +endfunc + func Test_Backtrace_Through_Source() CheckRunVimInTerminal CheckCWD diff --git a/test/old/testdir/test_expr.vim b/test/old/testdir/test_expr.vim index c8b7fde100..86e720a1ae 100644 --- a/test/old/testdir/test_expr.vim +++ b/test/old/testdir/test_expr.vim @@ -848,4 +848,60 @@ func Test_float_compare() call CheckLegacyAndVim9Success(lines) endfunc +func Test_string_interp() + let lines =<< trim END + call assert_equal('', $"") + call assert_equal('foobar', $"foobar") + #" Escaping rules. + call assert_equal('"foo"{bar}', $"\"foo\"{{bar}}") + call assert_equal('"foo"{bar}', $'"foo"{{bar}}') + call assert_equal('foobar', $"{\"foo\"}" .. $'{''bar''}') + #" Whitespace before/after the expression. + call assert_equal('3', $"{ 1 + 2 }") + #" String conversion. + call assert_equal('hello from ' .. v:version, $"hello from {v:version}") + call assert_equal('hello from ' .. v:version, $'hello from {v:version}') + #" Paper over a small difference between VimScript behaviour. + call assert_equal(string(v:true), $"{v:true}") + call assert_equal('(1+1=2)', $"(1+1={1 + 1})") + #" Hex-escaped opening brace: char2nr('{') == 0x7b + call assert_equal('esc123ape', $"esc\x7b123}ape") + call assert_equal('me{}me', $"me{\x7b}\x7dme") + VAR var1 = "sun" + VAR var2 = "shine" + call assert_equal('sunshine', $"{var1}{var2}") + call assert_equal('sunsunsun', $"{var1->repeat(3)}") + #" Multibyte strings. + call assert_equal('say ハロー・ワールド', $"say {'ハロー・ワールド'}") + #" Nested. + call assert_equal('foobarbaz', $"foo{$\"{'bar'}\"}baz") + #" Do not evaluate blocks when the expr is skipped. + VAR tmp = 0 + if v:false + echo "${ LET tmp += 1 }" + endif + call assert_equal(0, tmp) + + #" Stray closing brace. + call assert_fails('echo $"moo}"', 'E1278:') + #" Undefined variable in expansion. + call assert_fails('echo $"{moo}"', 'E121:') + #" Empty blocks are rejected. + call assert_fails('echo $"{}"', 'E15:') + call assert_fails('echo $"{ }"', 'E15:') + END + call CheckLegacyAndVim9Success(lines) + + let lines =<< trim END + call assert_equal('5', $"{({x -> x + 1})(4)}") + END + call CheckLegacySuccess(lines) + + let lines =<< trim END + call assert_equal('5', $"{((x) => x + 1)(4)}") + call assert_fails('echo $"{ # foo }"', 'E1279:') + END + call CheckDefAndScriptSuccess(lines) +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/test/old/testdir/test_let.vim b/test/old/testdir/test_let.vim index 915bba2314..8f7121935e 100644 --- a/test/old/testdir/test_let.vim +++ b/test/old/testdir/test_let.vim @@ -387,6 +387,17 @@ END call assert_equal(['Text', 'with', 'indent'], text) endfunc +func Test_let_interpolated() + call assert_equal('{text}', $'{{text}}') + call assert_equal('{{text}}', $'{{{{text}}}}') + let text = 'text' + call assert_equal('text{{', $'{text .. "{{"}') + call assert_equal('text{{', $"{text .. '{{'}") + " FIXME: should not need to escape quotes in the expression + call assert_equal('text{{', $'{text .. ''{{''}') + call assert_equal('text{{', $"{text .. \"{{\"}") +endfunc + " Test for the setting a variable using the heredoc syntax. " Keep near the end, this messes up highlighting. func Test_let_heredoc() @@ -496,72 +507,72 @@ END call assert_equal([' x', ' \y', ' z'], [a, b, c]) endfunc -" Test for evaluating Vim expressions in a heredoc using `=expr` +" Test for evaluating Vim expressions in a heredoc using {expr} " Keep near the end, this messes up highlighting. func Test_let_heredoc_eval() let str = '' let code =<< trim eval END - let a = `=5 + 10` - let b = `=min([10, 6])` + `=max([4, 6])` - `=str` - let c = "abc`=str`d" + let a = {5 + 10} + let b = {min([10, 6])} + {max([4, 6])} + {str} + let c = "abc{str}d" END call assert_equal(['let a = 15', 'let b = 6 + 6', '', 'let c = "abcd"'], code) let $TESTVAR = "Hello" let code =<< eval trim END - let s = "`=$TESTVAR`" + let s = "{$TESTVAR}" END call assert_equal(['let s = "Hello"'], code) let code =<< eval END - let s = "`=$TESTVAR`" + let s = "{$TESTVAR}" END call assert_equal([' let s = "Hello"'], code) let a = 10 let data =<< eval END -`=a` +{a} END call assert_equal(['10'], data) let x = 'X' let code =<< eval trim END - let a = `abc` - let b = `=x` - let c = ` + let a = {{abc}} + let b = {x} + let c = {{ END - call assert_equal(['let a = `abc`', 'let b = X', 'let c = `'], code) + call assert_equal(['let a = {abc}', 'let b = X', 'let c = {'], code) let code = 'xxx' let code =<< eval trim END - let n = `=5 + - 6` + let n = {5 + + 6} END call assert_equal('xxx', code) let code =<< eval trim END - let n = `=min([1, 2]` + `=max([3, 4])` + let n = {min([1, 2]} + {max([3, 4])} END call assert_equal('xxx', code) let lines =<< trim LINES let text =<< eval trim END - let b = `= + let b = { END LINES - call CheckScriptFailure(lines, 'E1083:') + call CheckScriptFailure(lines, 'E1279:') let lines =<< trim LINES let text =<< eval trim END - let b = `=abc + let b = {abc END LINES - call CheckScriptFailure(lines, 'E1083:') + call CheckScriptFailure(lines, 'E1279:') let lines =<< trim LINES let text =<< eval trim END - let b = `=` + let b = {} END LINES call CheckScriptFailure(lines, 'E15:') @@ -569,7 +580,7 @@ END " skipped heredoc if 0 let msg =<< trim eval END - n is: `=n` + n is: {n} END endif @@ -581,7 +592,7 @@ END let lines =<< trim END let Xvar =<< eval CODE let a = 1 - let b = `=5+` + let b = {5+} let c = 2 CODE let g:Count += 1 @@ -590,10 +601,10 @@ END let g:Count = 0 call assert_fails('source', 'E15:') call assert_equal(1, g:Count) - call setline(3, 'let b = `=abc`') + call setline(3, 'let b = {abc}') call assert_fails('source', 'E121:') call assert_equal(2, g:Count) - call setline(3, 'let b = `=abc` + `=min([9, 4])` + 2') + call setline(3, 'let b = {abc} + {min([9, 4])} + 2') call assert_fails('source', 'E121:') call assert_equal(3, g:Count) call assert_equal('test', g:Xvar) -- cgit From ef9af89da753235c64cbd8b7d700c686bc94dad7 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sat, 15 Apr 2023 17:51:39 +0800 Subject: vim-patch:8.2.4930: interpolated string expression requires escaping Problem: Interpolated string expression requires escaping. Solution: Do not require escaping in the expression. https://github.com/vim/vim/commit/0abc2871c105882ed1c1effb9a7757fad8a395bd Co-authored-by: Bram Moolenaar --- src/nvim/eval.c | 144 ++++++++++++++++++++++++++++++----------- src/nvim/eval/vars.c | 70 ++++++++++++-------- test/old/testdir/test_expr.vim | 8 +-- test/old/testdir/test_let.vim | 5 +- 4 files changed, 156 insertions(+), 71 deletions(-) diff --git a/src/nvim/eval.c b/src/nvim/eval.c index f8a9326703..da345e4b53 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -3063,12 +3063,12 @@ static int eval7(char **arg, typval_T *rettv, evalarg_T *const evalarg, bool wan // String constant: "string". case '"': - ret = eval_string(arg, rettv, evaluate); + ret = eval_string(arg, rettv, evaluate, false); break; // Literal string constant: 'str''ing'. case '\'': - ret = eval_lit_string(arg, rettv, evaluate); + ret = eval_lit_string(arg, rettv, evaluate, false); break; // List: [expr, expr] @@ -3868,16 +3868,20 @@ static int eval_number(char **arg, typval_T *rettv, bool evaluate, bool want_str return OK; } -/// Allocate a variable for a string constant. +/// Evaluate a string constant and put the result in "rettv". +/// "*arg" points to the double quote or to after it when "interpolate" is true. +/// When "interpolate" is true reduce "{{" to "{", reduce "}}" to "}" and stop +/// at a single "{". /// /// @return OK or FAIL. -static int eval_string(char **arg, typval_T *rettv, int evaluate) +static int eval_string(char **arg, typval_T *rettv, bool evaluate, bool interpolate) { char *p; - unsigned int extra = 0; + unsigned int extra = interpolate ? 1 : 0; + const int off = interpolate ? 0 : 1; // Find the end of the string, skipping backslashed characters. - for (p = *arg + 1; *p != NUL && *p != '"'; MB_PTR_ADV(p)) { + for (p = *arg + off; *p != NUL && *p != '"'; MB_PTR_ADV(p)) { if (*p == '\\' && p[1] != NUL) { p++; // A "\" form occupies at least 4 characters, and produces up @@ -3886,17 +3890,27 @@ static int eval_string(char **arg, typval_T *rettv, int evaluate) if (*p == '<') { extra += 5; } + } else if (interpolate && (*p == '{' || *p == '}')) { + if (*p == '{' && p[1] != '{') { // start of expression + break; + } + p++; + if (p[-1] == '}' && *p != '}') { // single '}' is an error + semsg(_(e_stray_closing_curly_str), *arg); + return FAIL; + } + extra--; // "{{" becomes "{", "}}" becomes "}" } } - if (*p != '"') { + if (*p != '"' && !(interpolate && *p == '{')) { semsg(_("E114: Missing quote: %s"), *arg); return FAIL; } // If only parsing, set *arg and return here if (!evaluate) { - *arg = p + 1; + *arg = p + off; return OK; } @@ -3907,7 +3921,7 @@ static int eval_string(char **arg, typval_T *rettv, int evaluate) rettv->vval.v_string = xmalloc((size_t)len); char *end = rettv->vval.v_string; - for (p = *arg + 1; *p != NUL && *p != '"';) { + for (p = *arg + off; *p != NUL && *p != '"';) { if (*p == '\\') { switch (*++p) { case 'b': @@ -3996,11 +4010,17 @@ static int eval_string(char **arg, typval_T *rettv, int evaluate) break; } } else { + if (interpolate && (*p == '{' || *p == '}')) { + if (*p == '{' && p[1] != '{') { // start of expression + break; + } + p++; // reduce "{{" to "{" and "}}" to "}" + } mb_copy_char((const char **)&p, &end); } } *end = NUL; - if (*p != NUL) { // just in case + if (*p == '"' && !interpolate) { p++; } *arg = p; @@ -4009,79 +4029,131 @@ static int eval_string(char **arg, typval_T *rettv, int evaluate) } /// Allocate a variable for a 'str''ing' constant. +/// When "interpolate" is true reduce "{{" to "{" and stop at a single "{". /// -/// @return OK or FAIL. -static int eval_lit_string(char **arg, typval_T *rettv, int evaluate) +/// @return OK when a "rettv" was set to the string. +/// FAIL on error, "rettv" is not set. +static int eval_lit_string(char **arg, typval_T *rettv, bool evaluate, bool interpolate) { char *p; - int reduce = 0; + int reduce = interpolate ? -1 : 0; + const int off = interpolate ? 0 : 1; // Find the end of the string, skipping ''. - for (p = *arg + 1; *p != NUL; MB_PTR_ADV(p)) { + for (p = *arg + off; *p != NUL; MB_PTR_ADV(p)) { if (*p == '\'') { if (p[1] != '\'') { break; } reduce++; p++; + } else if (interpolate) { + if (*p == '{') { + if (p[1] != '{') { + break; + } + p++; + reduce++; + } else if (*p == '}') { + p++; + if (*p != '}') { + semsg(_(e_stray_closing_curly_str), *arg); + return FAIL; + } + reduce++; + } } } - if (*p != '\'') { + if (*p != '\'' && !(interpolate && *p == '{')) { semsg(_("E115: Missing quote: %s"), *arg); return FAIL; } // If only parsing return after setting "*arg" if (!evaluate) { - *arg = p + 1; + *arg = p + off; return OK; } - // Copy the string into allocated memory, handling '' to ' reduction. + // Copy the string into allocated memory, handling '' to ' reduction and + // any expressions. char *str = xmalloc((size_t)((p - *arg) - reduce)); rettv->v_type = VAR_STRING; rettv->vval.v_string = str; - for (p = *arg + 1; *p != NUL;) { + for (p = *arg + off; *p != NUL;) { if (*p == '\'') { if (p[1] != '\'') { break; } p++; + } else if (interpolate && (*p == '{' || *p == '}')) { + if (*p == '{' && p[1] != '{') { + break; + } + p++; } mb_copy_char((const char **)&p, &str); } *str = NUL; - *arg = p + 1; + *arg = p + off; return OK; } -int eval_interp_string(char **arg, typval_T *rettv, int evaluate) +/// Evaluate a single or double quoted string possibly containing expressions. +/// "arg" points to the '$'. The result is put in "rettv". +/// +/// @return OK or FAIL. +int eval_interp_string(char **arg, typval_T *rettv, bool evaluate) { - // *arg is on the '$' character. + int ret = OK; + + garray_T ga; + ga_init(&ga, 1, 80); + + // *arg is on the '$' character, move it to the first string character. + (*arg)++; + const int quote = (uint8_t)(**arg); (*arg)++; - rettv->v_type = VAR_STRING; + for (;;) { + typval_T tv; + // Get the string up to the matching quote or to a single '{'. + // "arg" is advanced to either the quote or the '{'. + if (quote == '"') { + ret = eval_string(arg, &tv, evaluate, true); + } else { + ret = eval_lit_string(arg, &tv, evaluate, true); + } + if (ret == FAIL) { + break; + } + if (evaluate) { + ga_concat(&ga, tv.vval.v_string); + tv_clear(&tv); + } - typval_T tv; - int ret; - if (**arg == '"') { - ret = eval_string(arg, &tv, evaluate); - } else { - ret = eval_lit_string(arg, &tv, evaluate); + if (**arg != '{') { + // found terminating quote + (*arg)++; + break; + } + char *p = eval_one_expr_in_str(*arg, &ga); + if (p == NULL) { + ret = FAIL; + break; + } + *arg = p; } - if (ret == FAIL || !evaluate) { - return ret; + rettv->v_type = VAR_STRING; + if (ret != FAIL && evaluate) { + ga_append(&ga, NUL); } - - rettv->vval.v_string = eval_all_expr_in_str(tv.vval.v_string); - - tv_clear(&tv); - - return rettv->vval.v_string != NULL ? OK : FAIL; + rettv->vval.v_string = ga.ga_data; + return OK; } /// @return the function name of the partial. diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c index b86c49fd98..7ae7b5a57a 100644 --- a/src/nvim/eval/vars.c +++ b/src/nvim/eval/vars.c @@ -53,8 +53,42 @@ static const char *e_letunexp = N_("E18: Unexpected characters in :let"); static const char *e_lock_unlock = N_("E940: Cannot lock or unlock variable %s"); -/// Evaluate all the Vim expressions ({expr}) in string "str" and return the -/// resulting string. The caller must free the returned string. +/// Evaluate one Vim expression {expr} in string "p" and append the +/// resulting string to "gap". "p" points to the opening "{". +/// Return a pointer to the character after "}", NULL for an error. +char *eval_one_expr_in_str(char *p, garray_T *gap) +{ + char *block_start = skipwhite(p + 1); // skip the opening { + char *block_end = block_start; + + if (*block_start == NUL) { + semsg(_(e_missing_close_curly_str), p); + return NULL; + } + if (skip_expr(&block_end, NULL) == FAIL) { + return NULL; + } + block_end = skipwhite(block_end); + if (*block_end != '}') { + semsg(_(e_missing_close_curly_str), p); + return NULL; + } + *block_end = NUL; + char *expr_val = eval_to_string(block_start, true); + *block_end = '}'; + if (expr_val == NULL) { + return NULL; + } + ga_concat(gap, expr_val); + xfree(expr_val); + + return block_end + 1; +} + +/// Evaluate all the Vim expressions {expr} in "str" and return the resulting +/// string in allocated memory. "{{" is reduced to "{" and "}}" to "}". +/// Used for a heredoc assignment. +/// Returns NULL for an error. char *eval_all_expr_in_str(char *str) { garray_T ga; @@ -67,13 +101,13 @@ char *eval_all_expr_in_str(char *str) // Look for a block start. char *lit_start = p; while (*p != '{' && *p != '}' && *p != NUL) { - ++p; + p++; } if (*p != NUL && *p == p[1]) { // Escaped brace, unescape and continue. // Include the brace in the literal string. - ++p; + p++; escaped_brace = true; } else if (*p == '}') { semsg(_(e_stray_closing_curly_str), str); @@ -90,36 +124,16 @@ char *eval_all_expr_in_str(char *str) if (escaped_brace) { // Skip the second brace. - ++p; + p++; continue; } - // Skip the opening {. - char *block_start = ++p; - char *block_end = block_start; - if (*block_start != NUL && skip_expr(&block_end, NULL) == FAIL) { - ga_clear(&ga); - return NULL; - } - block_end = skipwhite(block_end); - // The block must be closed by a }. - if (*block_end != '}') { - semsg(_(e_missing_close_curly_str), str); + // Evaluate the expression and append the result. + p = eval_one_expr_in_str(p, &ga); + if (p == NULL) { ga_clear(&ga); return NULL; } - char save_c = *block_end; - *block_end = NUL; - char *expr_val = eval_to_string(block_start, true); - *block_end = save_c; - if (expr_val == NULL) { - ga_clear(&ga); - return NULL; - } - ga_concat(&ga, expr_val); - xfree(expr_val); - - p = block_end + 1; } ga_append(&ga, NUL); diff --git a/test/old/testdir/test_expr.vim b/test/old/testdir/test_expr.vim index 86e720a1ae..dee7266bb5 100644 --- a/test/old/testdir/test_expr.vim +++ b/test/old/testdir/test_expr.vim @@ -855,7 +855,7 @@ func Test_string_interp() #" Escaping rules. call assert_equal('"foo"{bar}', $"\"foo\"{{bar}}") call assert_equal('"foo"{bar}', $'"foo"{{bar}}') - call assert_equal('foobar', $"{\"foo\"}" .. $'{''bar''}') + call assert_equal('foobar', $"{"foo"}" .. $'{'bar'}') #" Whitespace before/after the expression. call assert_equal('3', $"{ 1 + 2 }") #" String conversion. @@ -865,8 +865,8 @@ func Test_string_interp() call assert_equal(string(v:true), $"{v:true}") call assert_equal('(1+1=2)', $"(1+1={1 + 1})") #" Hex-escaped opening brace: char2nr('{') == 0x7b - call assert_equal('esc123ape', $"esc\x7b123}ape") - call assert_equal('me{}me', $"me{\x7b}\x7dme") + call assert_equal('esc123ape', $"esc{123}ape") + call assert_equal('me{}me', $"me{"\x7b"}\x7dme") VAR var1 = "sun" VAR var2 = "shine" call assert_equal('sunshine', $"{var1}{var2}") @@ -874,7 +874,7 @@ func Test_string_interp() #" Multibyte strings. call assert_equal('say ハロー・ワールド', $"say {'ハロー・ワールド'}") #" Nested. - call assert_equal('foobarbaz', $"foo{$\"{'bar'}\"}baz") + call assert_equal('foobarbaz', $"foo{$"{'bar'}"}baz") #" Do not evaluate blocks when the expr is skipped. VAR tmp = 0 if v:false diff --git a/test/old/testdir/test_let.vim b/test/old/testdir/test_let.vim index 8f7121935e..0d84164274 100644 --- a/test/old/testdir/test_let.vim +++ b/test/old/testdir/test_let.vim @@ -393,9 +393,8 @@ func Test_let_interpolated() let text = 'text' call assert_equal('text{{', $'{text .. "{{"}') call assert_equal('text{{', $"{text .. '{{'}") - " FIXME: should not need to escape quotes in the expression - call assert_equal('text{{', $'{text .. ''{{''}') - call assert_equal('text{{', $"{text .. \"{{\"}") + call assert_equal('text{{', $'{text .. '{{'}') + call assert_equal('text{{', $"{text .. "{{"}") endfunc " Test for the setting a variable using the heredoc syntax. -- cgit From 29efd54e0284727a7dde5608e5eedaef9c00c65f Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sat, 15 Apr 2023 18:19:47 +0800 Subject: vim-patch:8.2.4934: string interpolation fails when not evaluating Problem: String interpolation fails when not evaluating. Solution: Skip the expression when not evaluating. (closes vim/vim#10398) https://github.com/vim/vim/commit/70c41241c2701f26a99085e433925a206ca265a3 Co-authored-by: Bram Moolenaar --- runtime/syntax/modula3.vim | 4 +--- src/nvim/eval.c | 2 +- src/nvim/eval/vars.c | 21 ++++++++++++--------- test/old/testdir/test_scriptnames.vim | 4 +--- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/runtime/syntax/modula3.vim b/runtime/syntax/modula3.vim index 390a1a90ff..67243db600 100644 --- a/runtime/syntax/modula3.vim +++ b/runtime/syntax/modula3.vim @@ -84,9 +84,7 @@ syn case ignore let s:digits = "0123456789ABCDEF" for s:radix in range(2, 16) - " Nvim does not support interpolated strings yet. - " exe $'syn match modula3Integer "\<{s:radix}_[{s:digits[:s:radix - 1]}]\+L\=\>"' - exe 'syn match modula3Integer "\<' .. s:radix .. '_[' .. s:digits[:s:radix - 1] .. ']\+L\=\>"' + exe $'syn match modula3Integer "\<{s:radix}_[{s:digits[:s:radix - 1]}]\+L\=\>"' endfor unlet s:digits s:radix diff --git a/src/nvim/eval.c b/src/nvim/eval.c index da345e4b53..289489a182 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -4140,7 +4140,7 @@ int eval_interp_string(char **arg, typval_T *rettv, bool evaluate) (*arg)++; break; } - char *p = eval_one_expr_in_str(*arg, &ga); + char *p = eval_one_expr_in_str(*arg, &ga, evaluate); if (p == NULL) { ret = FAIL; break; diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c index 7ae7b5a57a..d3ca2624eb 100644 --- a/src/nvim/eval/vars.c +++ b/src/nvim/eval/vars.c @@ -55,8 +55,9 @@ static const char *e_lock_unlock = N_("E940: Cannot lock or unlock variable %s") /// Evaluate one Vim expression {expr} in string "p" and append the /// resulting string to "gap". "p" points to the opening "{". +/// When "evaluate" is false only skip over the expression. /// Return a pointer to the character after "}", NULL for an error. -char *eval_one_expr_in_str(char *p, garray_T *gap) +char *eval_one_expr_in_str(char *p, garray_T *gap, bool evaluate) { char *block_start = skipwhite(p + 1); // skip the opening { char *block_end = block_start; @@ -73,14 +74,16 @@ char *eval_one_expr_in_str(char *p, garray_T *gap) semsg(_(e_missing_close_curly_str), p); return NULL; } - *block_end = NUL; - char *expr_val = eval_to_string(block_start, true); - *block_end = '}'; - if (expr_val == NULL) { - return NULL; + if (evaluate) { + *block_end = NUL; + char *expr_val = eval_to_string(block_start, true); + *block_end = '}'; + if (expr_val == NULL) { + return NULL; + } + ga_concat(gap, expr_val); + xfree(expr_val); } - ga_concat(gap, expr_val); - xfree(expr_val); return block_end + 1; } @@ -129,7 +132,7 @@ char *eval_all_expr_in_str(char *str) } // Evaluate the expression and append the result. - p = eval_one_expr_in_str(p, &ga); + p = eval_one_expr_in_str(p, &ga, true); if (p == NULL) { ga_clear(&ga); return NULL; diff --git a/test/old/testdir/test_scriptnames.vim b/test/old/testdir/test_scriptnames.vim index d804684722..06ae305ab7 100644 --- a/test/old/testdir/test_scriptnames.vim +++ b/test/old/testdir/test_scriptnames.vim @@ -35,9 +35,7 @@ func Test_getscriptinfo() source Xscript let l = getscriptinfo() call assert_match('Xscript$', l[-1].name) - " Nvim does not support interpolated strings yet. - " call assert_equal(g:loaded_script_id, $"{l[-1].sid}_") - call assert_equal(g:loaded_script_id, '' . l[-1].sid . '_') + call assert_equal(g:loaded_script_id, $"{l[-1].sid}_") call delete('Xscript') endfunc -- cgit From c6ebcd523dc91c8b1d52a8c9a61aca3d3a59250b Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sat, 15 Apr 2023 18:22:44 +0800 Subject: vim-patch:9.0.0104: going beyond allocated memory when evaluating string constant Problem: Going beyond allocated memory when evaluating string constant. Solution: Properly skip over form. https://github.com/vim/vim/commit/1e56bda9048a9625bce6e660938c834c5c15b07d Co-authored-by: Bram Moolenaar --- src/nvim/eval.c | 17 ++++++++++++++++- test/old/testdir/test_eval_stuff.vim | 5 +++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 289489a182..a30f9146c5 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -3877,6 +3877,7 @@ static int eval_number(char **arg, typval_T *rettv, bool evaluate, bool want_str static int eval_string(char **arg, typval_T *rettv, bool evaluate, bool interpolate) { char *p; + const char *const arg_end = *arg + strlen(*arg); unsigned int extra = interpolate ? 1 : 0; const int off = interpolate ? 0 : 1; @@ -3888,7 +3889,20 @@ static int eval_string(char **arg, typval_T *rettv, bool evaluate, bool interpol // to 9 characters (6 for the char and 3 for a modifier): // reserve space for 5 extra. if (*p == '<') { + int modifiers = 0; + int flags = FSK_KEYCODE | FSK_IN_STRING; + extra += 5; + + // Skip to the '>' to avoid using '{' inside for string + // interpolation. + if (p[1] != '*') { + flags |= FSK_SIMPLIFY; + } + if (find_special_key((const char **)&p, (size_t)(arg_end - p), + &modifiers, flags, NULL) != 0) { + p--; // leave "p" on the ">" + } } } else if (interpolate && (*p == '{' || *p == '}')) { if (*p == '{' && p[1] != '{') { // start of expression @@ -3994,7 +4008,8 @@ static int eval_string(char **arg, typval_T *rettv, bool evaluate, bool interpol if (p[1] != '*') { flags |= FSK_SIMPLIFY; } - extra = trans_special((const char **)&p, strlen(p), end, flags, false, NULL); + extra = trans_special((const char **)&p, (size_t)(arg_end - p), + end, flags, false, NULL); if (extra != 0) { end += extra; if (end >= rettv->vval.v_string + len) { diff --git a/test/old/testdir/test_eval_stuff.vim b/test/old/testdir/test_eval_stuff.vim index 90e3942c4d..7acc91c17b 100644 --- a/test/old/testdir/test_eval_stuff.vim +++ b/test/old/testdir/test_eval_stuff.vim @@ -407,4 +407,9 @@ func Test_modified_char_no_escape_special() nunmap endfunc +func Test_eval_string_in_special_key() + " this was using the '{' inside <> as the start of an interpolated string + silent! echo 0{1-$"\n|nö% +endfunc + " vim: shiftwidth=2 sts=2 expandtab -- cgit From f2a9097d764cf61b9479d7633a9738077f75f43c Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sat, 15 Apr 2023 19:02:02 +0800 Subject: vim-patch:partial:d899e5112079 Update runtime files https://github.com/vim/vim/commit/d899e51120798d3fb5420abb1f19dddf3f014d05 Co-authored-by: Bram Moolenaar --- runtime/doc/eval.txt | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index 518a190d3c..072d894aff 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -2569,20 +2569,20 @@ text... {endmarker}. If "eval" is not specified, then each line of text is - used as a |literal-string|. If "eval" is specified, - then any Vim expression in the form ``={expr}`` is - evaluated and the result replaces the expression. + used as a |literal-string|, except that single quotes + doe not need to be doubled. + If "eval" is specified, then any Vim expression in the + form {expr} is evaluated and the result replaces the + expression, like with |interp-string|. Example where $HOME is expanded: > let lines =<< trim eval END some text - See the file `=$HOME`/.vimrc + See the file {$HOME}/.vimrc more text END < There can be multiple Vim expressions in a single line but an expression cannot span multiple lines. If any expression evaluation fails, then the assignment fails. - once the "`=" has been found {expr} and a backtick - must follow. {expr} cannot be empty. {endmarker} must not contain white space. {endmarker} cannot start with a lower case character. @@ -2635,10 +2635,10 @@ text... DATA let code =<< trim eval CODE - let v = `=10 + 20` - let h = "`=$HOME`" - let s = "`=Str1()` abc `=Str2()`" - let n = `=MyFunc(3, 4)` + let v = {10 + 20} + let h = "{$HOME}" + let s = "{Str1()} abc {Str2()}" + let n = {MyFunc(3, 4)} CODE < *E121* -- cgit From 9f1d33307270e7d013896aea6042b73d091078f5 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sat, 15 Apr 2023 19:04:55 +0800 Subject: vim-patch:3f32a5f1601a Update runtime files and translations https://github.com/vim/vim/commit/3f32a5f1601ab2b0eba0caad00d4c26fb86a02a2 Co-authored-by: Bram Moolenaar --- runtime/doc/eval.txt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index 072d894aff..dd9137649d 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -1417,14 +1417,22 @@ allowing the inclusion of Vim script expressions (see |expr1|). Any expression returning a value can be enclosed between curly braces. The value is converted to a string. All the text and results of the expressions are concatenated to make a new string. - + *E1278* To include an opening brace '{' or closing brace '}' in the string content -double it. +double it. For double quoted strings using a backslash also works. A single +closing brace '}' will result in an error. Examples: > let your_name = input("What's your name? ") +< What's your name? Peter ~ +> + echo echo $"Hello, {your_name}!" - echo $"The square root of 9 is {sqrt(9)}" +< Hello, Peter! ~ +> + echo $"The square root of {{9}} is {sqrt(9)}" +< The square root of {9} is 3.0 ~ + ------------------------------------------------------------------------------ option *expr-option* *E112* *E113* -- cgit From 57221e0d11d1c24bc2abada7559a1d20c5090b62 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sat, 15 Apr 2023 19:11:29 +0800 Subject: vim-patch:b59ae59a5870 Update runtime files https://github.com/vim/vim/commit/b59ae59a58706e454ef8c78276f021b1f58466e7 Co-authored-by: Bram Moolenaar --- runtime/doc/eval.txt | 6 +++--- runtime/doc/options.txt | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index dd9137649d..1ff6e3c360 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -1407,7 +1407,7 @@ to be doubled. These two commands are equivalent: > ------------------------------------------------------------------------------ -interpolated-string *interp-string* +interpolated-string *$quote* *interpolated-string* $"string" interpolated string constant *expr-$quote* $'string' interpolated literal string constant *expr-$'* @@ -2578,10 +2578,10 @@ text... If "eval" is not specified, then each line of text is used as a |literal-string|, except that single quotes - doe not need to be doubled. + does not need to be doubled. If "eval" is specified, then any Vim expression in the form {expr} is evaluated and the result replaces the - expression, like with |interp-string|. + expression, like with |interpolated-string|. Example where $HOME is expanded: > let lines =<< trim eval END some text diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index b4cad51990..dcaf37af20 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -3332,8 +3332,8 @@ A jump table for the options with a short description can be found at |Q_op|. If the expression starts with s: or ||, then it is replaced with the script ID (|local-function|). Example: > - set includeexpr=s:MyIncludeExpr(v:fname) - set includeexpr=SomeIncludeExpr(v:fname) + setlocal includeexpr=s:MyIncludeExpr(v:fname) + setlocal includeexpr=SomeIncludeExpr(v:fname) < The expression will be evaluated in the |sandbox| when set from a modeline, see |sandbox-option|. -- cgit