Skip to content

Commit 2eaef10

Browse files
LemonBoybrammool
authored andcommitted
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 #10327)
1 parent e7d6dbc commit 2eaef10

File tree

16 files changed

+360
-133
lines changed

16 files changed

+360
-133
lines changed

runtime/doc/eval.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1523,6 +1523,25 @@ to be doubled. These two commands are equivalent: >
15231523
if a =~ '\s*'
15241524
15251525
1526+
interpolated-string *interp-string* *E256*
1527+
--------------------
1528+
$"string" interpolated string constant *expr-$quote*
1529+
$'string' interpolated literal string constant *expr-$'*
1530+
1531+
Interpolated strings are an extension of the |string| and |literal-string|,
1532+
allowing the inclusion of Vim script expressions (see |expr1|). Any
1533+
expression returning a value can be enclosed between curly braces. The value
1534+
is converted to a string. All the text and results of the expressions
1535+
are concatenated to make a new string.
1536+
1537+
To include an opening brace '{' or closing brace '}' in the string content
1538+
double it.
1539+
1540+
Examples: >
1541+
let your_name = input("What's your name? ")
1542+
echo $"Hello, {your_name}!"
1543+
echo $"The square root of 9 is {sqrt(9)}"
1544+
15261545
option *expr-option* *E112* *E113*
15271546
------
15281547
&option option value, local value if possible

src/errors.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3268,4 +3268,8 @@ EXTERN char e_illegal_map_mode_string_str[]
32683268
EXTERN char e_channel_job_feature_not_available[]
32693269
INIT(= N_("E1277: Channel and job feature is not available"));
32703270
# endif
3271+
EXTERN char e_stray_closing_curly_str[]
3272+
INIT(= N_("E1278: Stray '}' without a matching '{': %s"));
3273+
EXTERN char e_missing_close_curly_str[]
3274+
INIT(= N_("E1279: Missing '}': %s"));
32713275
#endif

src/eval.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3769,8 +3769,12 @@ eval7(
37693769

37703770
/*
37713771
* Environment variable: $VAR.
3772+
* Interpolated string: $"string" or $'string'.
37723773
*/
3773-
case '$': ret = eval_env_var(arg, rettv, evaluate);
3774+
case '$': if ((*arg)[1] == '"' || (*arg)[1] == '\'')
3775+
ret = eval_interp_string(arg, rettv, evaluate);
3776+
else
3777+
ret = eval_env_var(arg, rettv, evaluate);
37743778
break;
37753779

37763780
/*

src/evalvars.c

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -603,59 +603,88 @@ list_script_vars(int *first)
603603
}
604604

605605
/*
606-
* Evaluate all the Vim expressions (`=expr`) in string "str" and return the
606+
* Evaluate all the Vim expressions ({expr}) in string "str" and return the
607607
* resulting string. The caller must free the returned string.
608608
*/
609-
static char_u *
609+
char_u *
610610
eval_all_expr_in_str(char_u *str)
611611
{
612612
garray_T ga;
613-
char_u *s;
614613
char_u *p;
615614
char_u save_c;
616-
char_u *exprval;
617-
int status;
615+
char_u *expr_val;
618616

619617
ga_init2(&ga, 1, 80);
620618
p = str;
621619

622-
// Look for `=expr`, evaluate the expression and replace `=expr` with the
623-
// result.
624620
while (*p != NUL)
625621
{
626-
s = p;
627-
while (*p != NUL && (*p != '`' || p[1] != '='))
628-
p++;
629-
ga_concat_len(&ga, s, p - s);
622+
char_u *lit_start;
623+
char_u *block_start;
624+
char_u *block_end;
625+
int escaped_brace = FALSE;
626+
627+
// Look for a block start.
628+
lit_start = p;
629+
while (*p != '{' && *p != '}' && *p != NUL)
630+
++p;
631+
632+
if (*p != NUL && *p == p[1])
633+
{
634+
// Escaped brace, unescape and continue.
635+
// Include the brace in the literal string.
636+
++p;
637+
escaped_brace = TRUE;
638+
}
639+
else if (*p == '}')
640+
{
641+
semsg(_(e_stray_closing_curly_str), str);
642+
ga_clear(&ga);
643+
return NULL;
644+
}
645+
646+
// Append the literal part.
647+
ga_concat_len(&ga, lit_start, (size_t)(p - lit_start));
648+
630649
if (*p == NUL)
631-
break; // no backtick expression found
650+
break;
632651

633-
s = p;
634-
p += 2; // skip `=
652+
if (escaped_brace)
653+
{
654+
// Skip the second brace.
655+
++p;
656+
continue;
657+
}
635658

636-
status = *p == NUL ? OK : skip_expr(&p, NULL);
637-
if (status == FAIL || *p != '`')
659+
// Skip the opening {.
660+
block_start = ++p;
661+
block_end = block_start;
662+
if (*block_start != NUL && skip_expr(&block_end, NULL) == FAIL)
638663
{
639-
// invalid expression or missing ending backtick
640-
if (status != FAIL)
641-
emsg(_(e_missing_backtick));
642-
vim_free(ga.ga_data);
664+
ga_clear(&ga);
643665
return NULL;
644666
}
645-
s += 2; // skip `=
646-
save_c = *p;
647-
*p = NUL;
648-
exprval = eval_to_string(s, TRUE);
649-
*p = save_c;
650-
p++;
651-
if (exprval == NULL)
667+
block_end = skipwhite(block_end);
668+
// The block must be closed by a }.
669+
if (*block_end != '}')
652670
{
653-
// expression evaluation failed
654-
vim_free(ga.ga_data);
671+
semsg(_(e_missing_close_curly_str), str);
672+
ga_clear(&ga);
655673
return NULL;
656674
}
657-
ga_concat(&ga, exprval);
658-
vim_free(exprval);
675+
save_c = *block_end;
676+
*block_end = NUL;
677+
expr_val = eval_to_string(block_start, TRUE);
678+
*block_end = save_c;
679+
if (expr_val == NULL)
680+
{
681+
ga_clear(&ga);
682+
return NULL;
683+
}
684+
ga_concat(&ga, expr_val);
685+
vim_free(expr_val);
686+
687+
p = block_end + 1;
659688
}
660689
ga_append(&ga, NUL);
661690

@@ -825,7 +854,7 @@ heredoc_get(exarg_T *eap, char_u *cmd, int script_get, int vim9compile)
825854
str = theline + ti;
826855
if (vim9compile)
827856
{
828-
if (compile_heredoc_string(str, evalstr, cctx) == FAIL)
857+
if (compile_all_expr_in_str(str, evalstr, cctx) == FAIL)
829858
{
830859
vim_free(theline);
831860
vim_free(text_indent);

src/proto/evalvars.pro

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,6 @@ void set_callback(callback_T *dest, callback_T *src);
105105
void copy_callback(callback_T *dest, callback_T *src);
106106
void expand_autload_callback(callback_T *cb);
107107
void free_callback(callback_T *callback);
108+
char_u *eval_all_expr_in_str(char_u *str);
109+
108110
/* vim: set ft=c : */

src/proto/typval.pro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ int eval_string(char_u **arg, typval_T *rettv, int evaluate);
7272
int eval_lit_string(char_u **arg, typval_T *rettv, int evaluate);
7373
char_u *tv2string(typval_T *tv, char_u **tofree, char_u *numbuf, int copyID);
7474
int eval_env_var(char_u **arg, typval_T *rettv, int evaluate);
75+
int eval_interp_string(char_u **arg, typval_T *rettv, int evaluate);
7576
linenr_T tv_get_lnum(typval_T *argvars);
7677
linenr_T tv_get_lnum_buf(typval_T *argvars, buf_T *buf);
7778
buf_T *tv_get_buf(typval_T *tv, int curtab_only);

src/proto/vim9compile.pro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ int may_get_next_line(char_u *whitep, char_u **arg, cctx_T *cctx);
1616
int may_get_next_line_error(char_u *whitep, char_u **arg, cctx_T *cctx);
1717
void fill_exarg_from_cctx(exarg_T *eap, cctx_T *cctx);
1818
int func_needs_compiling(ufunc_T *ufunc, compiletype_T compile_type);
19-
int compile_heredoc_string(char_u *str, int evalstr, cctx_T *cctx);
19+
int compile_all_expr_in_str(char_u *str, int evalstr, cctx_T *cctx);
2020
int assignment_len(char_u *p, int *heredoc);
2121
void vim9_declare_error(char_u *name);
2222
int get_var_dest(char_u *name, assign_dest_T *dest, cmdidx_T cmdidx, int *option_scope, int *vimvaridx, type_T **type, cctx_T *cctx);

src/testdir/test_debugger.vim

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,15 +377,15 @@ func Test_Debugger_breakadd_expr()
377377
let expected =<< eval trim END
378378
Oldval = "10"
379379
Newval = "11"
380-
`=fnamemodify('Xtest.vim', ':p')`
380+
{fnamemodify('Xtest.vim', ':p')}
381381
line 1: let g:Xtest_var += 1
382382
END
383383
call RunDbgCmd(buf, ':source %', expected)
384384
call RunDbgCmd(buf, 'cont')
385385
let expected =<< eval trim END
386386
Oldval = "11"
387387
Newval = "12"
388-
`=fnamemodify('Xtest.vim', ':p')`
388+
{fnamemodify('Xtest.vim', ':p')}
389389
line 1: let g:Xtest_var += 1
390390
END
391391
call RunDbgCmd(buf, ':source %', expected)

src/testdir/test_expr.vim

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,4 +890,60 @@ func Test_float_compare()
890890
call v9.CheckLegacyAndVim9Success(lines)
891891
endfunc
892892

893+
func Test_string_interp()
894+
let lines =<< trim END
895+
call assert_equal('', $"")
896+
call assert_equal('foobar', $"foobar")
897+
#" Escaping rules.
898+
call assert_equal('"foo"{bar}', $"\"foo\"{{bar}}")
899+
call assert_equal('"foo"{bar}', $'"foo"{{bar}}')
900+
call assert_equal('foobar', $"{\"foo\"}" .. $'{''bar''}')
901+
#" Whitespace before/after the expression.
902+
call assert_equal('3', $"{ 1 + 2 }")
903+
#" String conversion.
904+
call assert_equal('hello from ' .. v:version, $"hello from {v:version}")
905+
call assert_equal('hello from ' .. v:version, $'hello from {v:version}')
906+
#" Paper over a small difference between VimScript behaviour.
907+
call assert_equal(string(v:true), $"{v:true}")
908+
call assert_equal('(1+1=2)', $"(1+1={1 + 1})")
909+
#" Hex-escaped opening brace: char2nr('{') == 0x7b
910+
call assert_equal('esc123ape', $"esc\x7b123}ape")
911+
call assert_equal('me{}me', $"me{\x7b}\x7dme")
912+
VAR var1 = "sun"
913+
VAR var2 = "shine"
914+
call assert_equal('sunshine', $"{var1}{var2}")
915+
call assert_equal('sunsunsun', $"{var1->repeat(3)}")
916+
#" Multibyte strings.
917+
call assert_equal('say ハロー・ワールド', $"say {'ハロー・ワールド'}")
918+
#" Nested.
919+
call assert_equal('foobarbaz', $"foo{$\"{'bar'}\"}baz")
920+
#" Do not evaluate blocks when the expr is skipped.
921+
VAR tmp = 0
922+
if v:false
923+
echo "${ LET tmp += 1 }"
924+
endif
925+
call assert_equal(0, tmp)
926+
927+
#" Stray closing brace.
928+
call assert_fails('echo $"moo}"', 'E1278:')
929+
#" Undefined variable in expansion.
930+
call assert_fails('echo $"{moo}"', 'E121:')
931+
#" Empty blocks are rejected.
932+
call assert_fails('echo $"{}"', 'E15:')
933+
call assert_fails('echo $"{ }"', 'E15:')
934+
END
935+
call v9.CheckLegacyAndVim9Success(lines)
936+
937+
let lines =<< trim END
938+
call assert_equal('5', $"{({x -> x + 1})(4)}")
939+
END
940+
call v9.CheckLegacySuccess(lines)
941+
942+
let lines =<< trim END
943+
call assert_equal('5', $"{((x) => x + 1)(4)}")
944+
call assert_fails('echo $"{ # foo }"', 'E1279:')
945+
END
946+
call v9.CheckDefAndScriptSuccess(lines)
947+
endfunc
948+
893949
" vim: shiftwidth=2 sts=2 expandtab

0 commit comments

Comments
 (0)