Skip to content

Commit efbfa86

Browse files
yegappanbrammool
authored andcommitted
patch 8.2.4770: cannot easily mix expression and heredoc
Problem: Cannot easily mix expression and heredoc. Solution: Support in heredoc. (Yegappan Lakshmanan, closes #10138)
1 parent 68aaff4 commit efbfa86

File tree

6 files changed

+314
-24
lines changed

6 files changed

+314
-24
lines changed

runtime/doc/eval.txt

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3224,14 +3224,32 @@ declarations and assignments do not use a command. |vim9-declaration|
32243224

32253225
*:let=<<* *:let-heredoc*
32263226
*E990* *E991* *E172* *E221* *E1145*
3227-
:let {var-name} =<< [trim] {endmarker}
3227+
:let {var-name} =<< [trim] [eval] {endmarker}
32283228
text...
32293229
text...
32303230
{endmarker}
32313231
Set internal variable {var-name} to a |List|
32323232
containing the lines of text bounded by the string
3233-
{endmarker}. The lines of text is used as a
3234-
|literal-string|.
3233+
{endmarker}.
3234+
3235+
If "eval" is not specified, then each line of text is
3236+
used as a |literal-string|. If "eval" is specified,
3237+
then any Vim expression in the form ``={expr}`` is
3238+
evaluated and the result replaces the expression.
3239+
Example where $HOME is expanded: >
3240+
let lines =<< trim eval END
3241+
some text
3242+
See the file `=$HOME`/.vimrc
3243+
more text
3244+
END
3245+
< There can be multiple Vim expressions in a single line
3246+
but an expression cannot span multiple lines. If any
3247+
expression evaluation fails, then the assignment fails.
3248+
once the "`=" has been found {expr} and a backtick
3249+
must follow. {expr} cannot be empty.
3250+
Currenty, in a compiled function {expr} is evaluated
3251+
when compiling the function, THIS WILL CHANGE.
3252+
32353253
{endmarker} must not contain white space.
32363254
{endmarker} cannot start with a lower case character.
32373255
The last line should end only with the {endmarker}
@@ -3281,6 +3299,13 @@ text...
32813299
1 2 3 4
32823300
5 6 7 8
32833301
DATA
3302+
3303+
let code =<< trim eval CODE
3304+
let v = `=10 + 20`
3305+
let h = "`=$HOME`"
3306+
let s = "`=Str1()` abc `=Str2()`"
3307+
let n = `=MyFunc(3, 4)`
3308+
CODE
32843309
<
32853310
*E121*
32863311
:let {var-name} .. List the value of variable {var-name}. Multiple

src/evalvars.c

Lines changed: 120 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,66 @@ list_script_vars(int *first)
602602
"s:", FALSE, first);
603603
}
604604

605+
/*
606+
* Evaluate all the Vim expressions (`=expr`) in string "str" and return the
607+
* resulting string. The caller must free the returned string.
608+
*/
609+
static char_u *
610+
eval_all_expr_in_str(char_u *str)
611+
{
612+
garray_T ga;
613+
char_u *s;
614+
char_u *p;
615+
char_u save_c;
616+
char_u *exprval;
617+
int status;
618+
619+
ga_init2(&ga, 1, 80);
620+
p = str;
621+
622+
// Look for `=expr`, evaluate the expression and replace `=expr` with the
623+
// result.
624+
while (*p != NUL)
625+
{
626+
s = p;
627+
while (*p != NUL && (*p != '`' || p[1] != '='))
628+
p++;
629+
ga_concat_len(&ga, s, p - s);
630+
if (*p == NUL)
631+
break; // no backtick expression found
632+
633+
s = p;
634+
p += 2; // skip `=
635+
636+
status = *p == NUL ? OK : skip_expr(&p, NULL);
637+
if (status == FAIL || *p != '`')
638+
{
639+
// invalid expression or missing ending backtick
640+
if (status != FAIL)
641+
emsg(_(e_missing_backtick));
642+
vim_free(ga.ga_data);
643+
return NULL;
644+
}
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)
652+
{
653+
// expression evaluation failed
654+
vim_free(ga.ga_data);
655+
return NULL;
656+
}
657+
ga_concat(&ga, exprval);
658+
vim_free(exprval);
659+
}
660+
ga_append(&ga, NUL);
661+
662+
return ga.ga_data;
663+
}
664+
605665
/*
606666
* Get a list of lines from a HERE document. The here document is a list of
607667
* lines surrounded by a marker.
@@ -619,7 +679,7 @@ list_script_vars(int *first)
619679
* tcl, mzscheme), script_get is set to TRUE. In this case, if the marker is
620680
* missing, then '.' is accepted as a marker.
621681
*
622-
* Returns a List with {lines} or NULL.
682+
* Returns a List with {lines} or NULL on failure.
623683
*/
624684
list_T *
625685
heredoc_get(exarg_T *eap, char_u *cmd, int script_get)
@@ -628,11 +688,14 @@ heredoc_get(exarg_T *eap, char_u *cmd, int script_get)
628688
char_u *marker;
629689
list_T *l;
630690
char_u *p;
691+
char_u *str;
631692
int marker_indent_len = 0;
632693
int text_indent_len = 0;
633694
char_u *text_indent = NULL;
634695
char_u dot[] = ".";
635696
int comment_char = in_vim9script() ? '#' : '"';
697+
int evalstr = FALSE;
698+
int eval_failed = FALSE;
636699

637700
if (eap->getline == NULL)
638701
{
@@ -642,21 +705,36 @@ heredoc_get(exarg_T *eap, char_u *cmd, int script_get)
642705

643706
// Check for the optional 'trim' word before the marker
644707
cmd = skipwhite(cmd);
645-
if (STRNCMP(cmd, "trim", 4) == 0 && (cmd[4] == NUL || VIM_ISWHITE(cmd[4])))
708+
709+
while (TRUE)
646710
{
647-
cmd = skipwhite(cmd + 4);
711+
if (STRNCMP(cmd, "trim", 4) == 0
712+
&& (cmd[4] == NUL || VIM_ISWHITE(cmd[4])))
713+
{
714+
cmd = skipwhite(cmd + 4);
715+
716+
// Trim the indentation from all the lines in the here document.
717+
// The amount of indentation trimmed is the same as the indentation
718+
// of the first line after the :let command line. To find the end
719+
// marker the indent of the :let command line is trimmed.
720+
p = *eap->cmdlinep;
721+
while (VIM_ISWHITE(*p))
722+
{
723+
p++;
724+
marker_indent_len++;
725+
}
726+
text_indent_len = -1;
648727

649-
// Trim the indentation from all the lines in the here document.
650-
// The amount of indentation trimmed is the same as the indentation of
651-
// the first line after the :let command line. To find the end marker
652-
// the indent of the :let command line is trimmed.
653-
p = *eap->cmdlinep;
654-
while (VIM_ISWHITE(*p))
728+
continue;
729+
}
730+
if (STRNCMP(cmd, "eval", 4) == 0
731+
&& (cmd[4] == NUL || VIM_ISWHITE(cmd[4])))
655732
{
656-
p++;
657-
marker_indent_len++;
733+
cmd = skipwhite(cmd + 4);
734+
evalstr = TRUE;
735+
continue;
658736
}
659-
text_indent_len = -1;
737+
break;
660738
}
661739

662740
// The marker is the next word.
@@ -716,6 +794,14 @@ heredoc_get(exarg_T *eap, char_u *cmd, int script_get)
716794
break;
717795
}
718796

797+
// If expression evaluation failed in the heredoc, then skip till the
798+
// end marker.
799+
if (eval_failed)
800+
{
801+
vim_free(theline);
802+
continue;
803+
}
804+
719805
if (text_indent_len == -1 && *theline != NUL)
720806
{
721807
// set the text indent from the first line.
@@ -734,12 +820,33 @@ heredoc_get(exarg_T *eap, char_u *cmd, int script_get)
734820
if (theline[ti] != text_indent[ti])
735821
break;
736822

737-
if (list_append_string(l, theline + ti, -1) == FAIL)
823+
str = theline + ti;
824+
if (evalstr)
825+
{
826+
str = eval_all_expr_in_str(str);
827+
if (str == NULL)
828+
{
829+
// expression evaluation failed
830+
vim_free(theline);
831+
eval_failed = TRUE;
832+
continue;
833+
}
834+
vim_free(theline);
835+
theline = str;
836+
}
837+
838+
if (list_append_string(l, str, -1) == FAIL)
738839
break;
739840
vim_free(theline);
740841
}
741842
vim_free(text_indent);
742843

844+
if (eval_failed)
845+
{
846+
// expression evaluation in the heredoc failed
847+
list_free(l);
848+
return NULL;
849+
}
743850
return l;
744851
}
745852

src/testdir/test_let.vim

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
" Tests for the :let command.
22

3+
import './vim9.vim' as v9
4+
35
func Test_let()
46
" Test to not autoload when assigning. It causes internal error.
57
set runtimepath+=./sautest
@@ -379,7 +381,8 @@ END
379381
call assert_equal(['Text', 'with', 'indent'], text)
380382
endfunc
381383

382-
" Test for the setting a variable using the heredoc syntax
384+
" Test for the setting a variable using the heredoc syntax.
385+
" Keep near the end, this messes up highlighting.
383386
func Test_let_heredoc()
384387
let var1 =<< END
385388
Some sample text
@@ -495,4 +498,96 @@ END
495498
call assert_equal([' x', ' \y', ' z'], [a, b, c])
496499
endfunc
497500

501+
" Test for evaluating Vim expressions in a heredoc using `=expr`
502+
" Keep near the end, this messes up highlighting.
503+
func Test_let_heredoc_eval()
504+
let str = ''
505+
let code =<< trim eval END
506+
let a = `=5 + 10`
507+
let b = `=min([10, 6])` + `=max([4, 6])`
508+
`=str`
509+
let c = "abc`=str`d"
510+
END
511+
call assert_equal(['let a = 15', 'let b = 6 + 6', '', 'let c = "abcd"'], code)
512+
let $TESTVAR = "Hello"
513+
let code =<< eval trim END
514+
let s = "`=$TESTVAR`"
515+
END
516+
call assert_equal(['let s = "Hello"'], code)
517+
let code =<< eval END
518+
let s = "`=$TESTVAR`"
519+
END
520+
call assert_equal([' let s = "Hello"'], code)
521+
let a = 10
522+
let data =<< eval END
523+
`=a`
524+
END
525+
call assert_equal(['10'], data)
526+
let x = 'X'
527+
let code =<< eval trim END
528+
let a = `abc`
529+
let b = `=x`
530+
let c = `
531+
END
532+
call assert_equal(['let a = `abc`', 'let b = X', 'let c = `'], code)
533+
let code = 'xxx'
534+
let code =<< eval trim END
535+
let n = `=5 +
536+
6`
537+
END
538+
call assert_equal('xxx', code)
539+
let code =<< eval trim END
540+
let n = `=min([1, 2]` + `=max([3, 4])`
541+
END
542+
call assert_equal('xxx', code)
543+
544+
let lines =<< trim LINES
545+
let text =<< eval trim END
546+
let b = `=
547+
END
548+
LINES
549+
call v9.CheckScriptFailure(lines, 'E1083:')
550+
551+
let lines =<< trim LINES
552+
let text =<< eval trim END
553+
let b = `=abc
554+
END
555+
LINES
556+
call v9.CheckScriptFailure(lines, 'E1083:')
557+
558+
let lines =<< trim LINES
559+
let text =<< eval trim END
560+
let b = `=`
561+
END
562+
LINES
563+
call v9.CheckScriptFailure(lines, 'E15:')
564+
565+
" Test for sourcing a script containing a heredoc with invalid expression.
566+
" Variable assignment should fail, if expression evaluation fails
567+
new
568+
let g:Xvar = 'test'
569+
let g:b = 10
570+
let lines =<< trim END
571+
let Xvar =<< eval CODE
572+
let a = 1
573+
let b = `=5+`
574+
let c = 2
575+
CODE
576+
let g:Count += 1
577+
END
578+
call setline(1, lines)
579+
let g:Count = 0
580+
call assert_fails('source', 'E15:')
581+
call assert_equal(1, g:Count)
582+
call setline(3, 'let b = `=abc`')
583+
call assert_fails('source', 'E121:')
584+
call assert_equal(2, g:Count)
585+
call setline(3, 'let b = `=abc` + `=min([9, 4])` + 2')
586+
call assert_fails('source', 'E121:')
587+
call assert_equal(3, g:Count)
588+
call assert_equal('test', g:Xvar)
589+
call assert_equal(10, g:b)
590+
bw!
591+
endfunc
592+
498593
" vim: shiftwidth=2 sts=2 expandtab

0 commit comments

Comments
 (0)