Mercurial > vim
changeset 28813:3626ca6a20ea v8.2.4930
patch 8.2.4930: interpolated string expression requires escaping
Commit: https://github.com/vim/vim/commit/0abc2871c105882ed1c1effb9a7757fad8a395bd
Author: Bram Moolenaar <Bram@vim.org>
Date: Tue May 10 13:24:30 2022 +0100
patch 8.2.4930: interpolated string expression requires escaping
Problem: Interpolated string expression requires escaping.
Solution: Do not require escaping in the expression.
author | Bram Moolenaar <Bram@vim.org> |
---|---|
date | Tue, 10 May 2022 14:30:04 +0200 |
parents | 483371d05cd5 |
children | 748050f8ed41 |
files | src/alloc.c src/dict.c src/eval.c src/evalvars.c src/proto/alloc.pro src/proto/evalvars.pro src/proto/typval.pro src/proto/vim9compile.pro src/testdir/test_expr.vim src/testdir/test_let.vim src/typval.c src/version.c src/vim9compile.c src/vim9expr.c src/vim9instr.c |
diffstat | 15 files changed, 288 insertions(+), 123 deletions(-) [+] |
line wrap: on
line diff
--- a/src/alloc.c +++ b/src/alloc.c @@ -832,7 +832,7 @@ ga_add_string(garray_T *gap, char_u *p) /* * Concatenate a string to a growarray which contains bytes. - * When "s" is NULL does not do anything. + * When "s" is NULL memory allocation fails does not do anything. * Note: Does NOT copy the NUL at the end! */ void @@ -869,14 +869,14 @@ ga_concat_len(garray_T *gap, char_u *s, /* * Append one byte to a growarray which contains bytes. */ - void + int ga_append(garray_T *gap, int c) { - if (ga_grow(gap, 1) == OK) - { - *((char *)gap->ga_data + gap->ga_len) = c; - ++gap->ga_len; - } + if (ga_grow(gap, 1) == FAIL) + return FAIL; + *((char *)gap->ga_data + gap->ga_len) = c; + ++gap->ga_len; + return OK; } #if (defined(UNIX) && !defined(USE_SYSTEM)) || defined(MSWIN) \
--- a/src/dict.c +++ b/src/dict.c @@ -866,13 +866,13 @@ get_literal_key(char_u **arg) if (**arg == '\'') { - if (eval_lit_string(arg, &rettv, TRUE) == FAIL) + if (eval_lit_string(arg, &rettv, TRUE, FALSE) == FAIL) return NULL; key = rettv.vval.v_string; } else if (**arg == '"') { - if (eval_string(arg, &rettv, TRUE) == FAIL) + if (eval_string(arg, &rettv, TRUE, FALSE) == FAIL) return NULL; key = rettv.vval.v_string; }
--- a/src/eval.c +++ b/src/eval.c @@ -3726,13 +3726,13 @@ eval7( /* * String constant: "string". */ - case '"': ret = eval_string(arg, rettv, evaluate); + case '"': ret = eval_string(arg, rettv, evaluate, FALSE); break; /* * Literal string constant: 'str''ing'. */ - case '\'': ret = eval_lit_string(arg, rettv, evaluate); + case '\'': ret = eval_lit_string(arg, rettv, evaluate, FALSE); break; /*
--- a/src/evalvars.c +++ b/src/evalvars.c @@ -603,16 +603,52 @@ list_script_vars(int *first) } /* - * 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_u * +eval_one_expr_in_str(char_u *p, garray_T *gap) +{ + char_u *block_start = skipwhite(p + 1); // skip the opening { + char_u *block_end = block_start; + char_u *expr_val; + + 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; + expr_val = eval_to_string(block_start, TRUE); + *block_end = '}'; + if (expr_val == NULL) + return NULL; + ga_concat(gap, expr_val); + vim_free(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_u * eval_all_expr_in_str(char_u *str) { garray_T ga; char_u *p; - char_u save_c; - char_u *expr_val; ga_init2(&ga, 1, 80); p = str; @@ -620,8 +656,6 @@ eval_all_expr_in_str(char_u *str) while (*p != NUL) { char_u *lit_start; - char_u *block_start; - char_u *block_end; int escaped_brace = FALSE; // Look for a block start. @@ -656,35 +690,13 @@ eval_all_expr_in_str(char_u *str) continue; } - // Skip the opening {. - block_start = ++p; - block_end = block_start; - if (*block_start != NUL && skip_expr(&block_end, NULL) == FAIL) + // Evaluate the expression and append the result. + p = eval_one_expr_in_str(p, &ga); + if (p == NULL) { 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); - ga_clear(&ga); - return NULL; - } - save_c = *block_end; - *block_end = NUL; - 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); - vim_free(expr_val); - - p = block_end + 1; } ga_append(&ga, NUL);
--- a/src/proto/alloc.pro +++ b/src/proto/alloc.pro @@ -19,13 +19,13 @@ int ga_copy_strings(garray_T *from, garr void ga_init(garray_T *gap); void ga_init2(garray_T *gap, size_t itemsize, int growsize); int ga_grow(garray_T *gap, int n); -int ga_grow_id(garray_T *gap, int n, alloc_id_T id UNUSED); +int ga_grow_id(garray_T *gap, int n, alloc_id_T id); int ga_grow_inner(garray_T *gap, int n); char_u *ga_concat_strings(garray_T *gap, char *sep); int ga_copy_string(garray_T *gap, char_u *p); int ga_add_string(garray_T *gap, char_u *p); void ga_concat(garray_T *gap, char_u *s); void ga_concat_len(garray_T *gap, char_u *s, size_t len); -void ga_append(garray_T *gap, int c); +int ga_append(garray_T *gap, int c); void append_ga_line(garray_T *gap); /* vim: set ft=c : */
--- a/src/proto/evalvars.pro +++ b/src/proto/evalvars.pro @@ -13,6 +13,8 @@ list_T *eval_spell_expr(char_u *badword, int get_spellword(list_T *list, char_u **pp); void prepare_vimvar(int idx, typval_T *save_tv); void restore_vimvar(int idx, typval_T *save_tv); +char_u *eval_one_expr_in_str(char_u *p, garray_T *gap); +char_u *eval_all_expr_in_str(char_u *str); list_T *heredoc_get(exarg_T *eap, char_u *cmd, int script_get, int vim9compile); void ex_var(exarg_T *eap); void ex_let(exarg_T *eap); @@ -105,6 +107,4 @@ void set_callback(callback_T *dest, call void copy_callback(callback_T *dest, callback_T *src); void expand_autload_callback(callback_T *cb); void free_callback(callback_T *callback); -char_u *eval_all_expr_in_str(char_u *str); - /* vim: set ft=c : */
--- a/src/proto/typval.pro +++ b/src/proto/typval.pro @@ -68,11 +68,11 @@ int tv_islocked(typval_T *tv); int tv_equal(typval_T *tv1, typval_T *tv2, int ic, int recursive); int eval_option(char_u **arg, typval_T *rettv, int evaluate); int eval_number(char_u **arg, typval_T *rettv, int evaluate, int want_string); -int eval_string(char_u **arg, typval_T *rettv, int evaluate); -int eval_lit_string(char_u **arg, typval_T *rettv, int evaluate); +int eval_string(char_u **arg, typval_T *rettv, int evaluate, int interpolate); +int eval_lit_string(char_u **arg, typval_T *rettv, int evaluate, int interpolate); +int eval_interp_string(char_u **arg, typval_T *rettv, int evaluate); char_u *tv2string(typval_T *tv, char_u **tofree, char_u *numbuf, int copyID); int eval_env_var(char_u **arg, typval_T *rettv, int evaluate); -int eval_interp_string(char_u **arg, typval_T *rettv, int evaluate); linenr_T tv_get_lnum(typval_T *argvars); linenr_T tv_get_lnum_buf(typval_T *argvars, buf_T *buf); buf_T *tv_get_buf(typval_T *tv, int curtab_only);
--- a/src/proto/vim9compile.pro +++ b/src/proto/vim9compile.pro @@ -16,6 +16,7 @@ int may_get_next_line(char_u *whitep, ch int may_get_next_line_error(char_u *whitep, char_u **arg, cctx_T *cctx); void fill_exarg_from_cctx(exarg_T *eap, cctx_T *cctx); int func_needs_compiling(ufunc_T *ufunc, compiletype_T compile_type); +char_u *compile_one_expr_in_str(char_u *p, cctx_T *cctx); int compile_all_expr_in_str(char_u *str, int evalstr, cctx_T *cctx); int assignment_len(char_u *p, int *heredoc); void vim9_declare_error(char_u *name);
--- a/src/testdir/test_expr.vim +++ b/src/testdir/test_expr.vim @@ -897,7 +897,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. @@ -907,8 +907,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}") @@ -916,7 +916,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
--- a/src/testdir/test_let.vim +++ b/src/testdir/test_let.vim @@ -387,9 +387,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.
--- a/src/typval.c +++ b/src/typval.c @@ -2065,19 +2065,23 @@ eval_number( } /* - * 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. */ int -eval_string(char_u **arg, typval_T *rettv, int evaluate) +eval_string(char_u **arg, typval_T *rettv, int evaluate, int interpolate) { char_u *p; char_u *end; - int extra = 0; + int extra = interpolate ? 1 : 0; + int off = interpolate ? 0 : 1; int len; // 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) { @@ -2088,9 +2092,21 @@ eval_string(char_u **arg, typval_T *rett 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(_(e_missing_double_quote_str), *arg); return FAIL; @@ -2099,7 +2115,7 @@ eval_string(char_u **arg, typval_T *rett // If only parsing, set *arg and return here if (!evaluate) { - *arg = p + 1; + *arg = p + off; return OK; } @@ -2112,7 +2128,7 @@ eval_string(char_u **arg, typval_T *rett return FAIL; end = rettv->vval.v_string; - for (p = *arg + 1; *p != NUL && *p != '"'; ) + for (p = *arg + off; *p != NUL && *p != '"'; ) { if (*p == '\\') { @@ -2192,15 +2208,23 @@ eval_string(char_u **arg, typval_T *rett } // FALLTHROUGH - default: MB_COPY_CHAR(p, end); + default: MB_COPY_CHAR(p, end); break; } } else + { + if (interpolate && (*p == '{' || *p == '}')) + { + if (*p == '{' && p[1] != '{') // start of expression + break; + ++p; // reduce "{{" to "{" and "}}" to "}" + } MB_COPY_CHAR(p, end); + } } *end = NUL; - if (*p != NUL) // just in case + if (*p == '"' && !interpolate) ++p; *arg = p; @@ -2209,17 +2233,20 @@ eval_string(char_u **arg, typval_T *rett /* * Allocate a variable for a 'str''ing' constant. - * Return OK or FAIL. + * When "interpolate" is TRUE reduce "{{" to "{" and stop at a single "{". + * Return OK when a "rettv" was set to the string. + * Return FAIL on error, "rettv" is not set. */ int -eval_lit_string(char_u **arg, typval_T *rettv, int evaluate) +eval_lit_string(char_u **arg, typval_T *rettv, int evaluate, int interpolate) { char_u *p; char_u *str; - int reduce = 0; + int reduce = interpolate ? -1 : 0; + 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 == '\'') { @@ -2228,9 +2255,29 @@ eval_lit_string(char_u **arg, typval_T * ++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(_(e_missing_single_quote_str), *arg); return FAIL; @@ -2239,18 +2286,19 @@ eval_lit_string(char_u **arg, typval_T * // 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. str = alloc((p - *arg) - reduce); if (str == NULL) return FAIL; rettv->v_type = VAR_STRING; rettv->vval.v_string = str; - for (p = *arg + 1; *p != NUL; ) + for (p = *arg + off; *p != NUL; ) { if (*p == '\'') { @@ -2258,38 +2306,82 @@ eval_lit_string(char_u **arg, typval_T * break; ++p; } + else if (interpolate && (*p == '{' || *p == '}')) + { + if (*p == '{' && p[1] != '{') + break; + ++p; + } MB_COPY_CHAR(p, str); } *str = NUL; - *arg = p + 1; + *arg = p + off; return OK; } +/* + * Evaluate a single or double quoted string possibly containing expressions. + * "arg" points to the '$'. The result is put in "rettv". + * Returns OK or FAIL. + */ int eval_interp_string(char_u **arg, typval_T *rettv, int evaluate) { typval_T tv; - int ret; + int ret = OK; + int quote; + garray_T ga; + char_u *p; + + ga_init2(&ga, 1, 80); + + // *arg is on the '$' character, move it to the first string character. + ++*arg; + quote = **arg; + ++*arg; - // *arg is on the '$' character. - (*arg)++; + for (;;) + { + // 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); + clear_tv(&tv); + } + + if (**arg != '{') + { + // found terminating quote + ++*arg; + break; + } + p = eval_one_expr_in_str(*arg, &ga); + if (p == NULL) + { + ret = FAIL; + break; + } + *arg = p; + } rettv->v_type = VAR_STRING; - - if (**arg == '"') - ret = eval_string(arg, &tv, evaluate); - else - ret = eval_lit_string(arg, &tv, evaluate); + if (ret == FAIL || !evaluate || ga_append(&ga, NUL) == FAIL) + { + ga_clear(&ga); + rettv->vval.v_string = NULL; + return ret; + } - if (ret == FAIL || !evaluate) - return ret; - - rettv->vval.v_string = eval_all_expr_in_str(tv.vval.v_string); - - clear_tv(&tv); - - return rettv->vval.v_string != NULL ? OK : FAIL; + rettv->vval.v_string = ga.ga_data; + return OK; } /*
--- a/src/version.c +++ b/src/version.c @@ -747,6 +747,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 4930, +/**/ 4929, /**/ 4928,
--- a/src/vim9compile.c +++ b/src/vim9compile.c @@ -969,6 +969,36 @@ theend: } /* + * Compile one Vim expression {expr} in string "p". + * "p" points to the opening "{". + * Return a pointer to the character after "}", NULL for an error. + */ + char_u * +compile_one_expr_in_str(char_u *p, cctx_T *cctx) +{ + char_u *block_start; + char_u *block_end; + + // Skip the opening {. + block_start = skipwhite(p + 1); + block_end = block_start; + if (*block_start != NUL && skip_expr(&block_end, NULL) == FAIL) + return NULL; + block_end = skipwhite(block_end); + // The block must be closed by a }. + if (*block_end != '}') + { + semsg(_(e_missing_close_curly_str), p); + return NULL; + } + if (compile_expr0(&block_start, cctx) == FAIL) + return NULL; + may_generate_2STRING(-1, TRUE, cctx); + + return block_end + 1; +} + +/* * Compile a string "str" (either containing a literal string or a mix of * literal strings and Vim expressions of the form `{expr}`). This is used * when compiling a heredoc assignment to a variable or an interpolated string @@ -997,8 +1027,6 @@ compile_all_expr_in_str(char_u *str, int while (*p != NUL) { char_u *lit_start; - char_u *block_start; - char_u *block_end; int escaped_brace = FALSE; // Look for a block start. @@ -1038,28 +1066,14 @@ compile_all_expr_in_str(char_u *str, int continue; } - // Skip the opening {. - block_start = skipwhite(p + 1); - block_end = block_start; - if (*block_start != NUL && skip_expr(&block_end, NULL) == FAIL) + p = compile_one_expr_in_str(p, cctx); + if (p == NULL) return FAIL; - block_end = skipwhite(block_end); - // The block must be closed by a }. - if (*block_end != '}') - { - semsg(_(e_missing_close_curly_str), str); - return FAIL; - } - if (compile_expr0(&block_start, cctx) == FAIL) - return FAIL; - may_generate_2STRING(-1, TRUE, cctx); ++count; - - p = block_end + 1; } // Small optimization, if there's only a single piece skip the ISN_CONCAT. - if (count != 1) + if (count > 1) return generate_CONCAT(cctx, count); return OK;
--- a/src/vim9expr.c +++ b/src/vim9expr.c @@ -762,9 +762,9 @@ compile_call( argvars[0].v_type = VAR_UNKNOWN; if (*s == '"') - (void)eval_string(&s, &argvars[0], TRUE); + (void)eval_string(&s, &argvars[0], TRUE, FALSE); else if (*s == '\'') - (void)eval_lit_string(&s, &argvars[0], TRUE); + (void)eval_lit_string(&s, &argvars[0], TRUE, FALSE); s = skipwhite(s); if (*s == ')' && argvars[0].v_type == VAR_STRING && ((is_has && !dynamic_feature(argvars[0].vval.v_string)) @@ -1375,30 +1375,73 @@ compile_get_env(char_u **arg, cctx_T *cc } /* - * Compile "$"string"" or "$'string'". + * Compile $"string" or $'string'. */ static int compile_interp_string(char_u **arg, cctx_T *cctx) { typval_T tv; int ret; + int quote; int evaluate = cctx->ctx_skip != SKIP_YES; + int count = 0; + char_u *p; - // *arg is on the '$' character. - (*arg)++; + // *arg is on the '$' character, move it to the first string character. + ++*arg; + quote = **arg; + ++*arg; - if (**arg == '"') - ret = eval_string(arg, &tv, evaluate); - else - ret = eval_lit_string(arg, &tv, evaluate); + for (;;) + { + // 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) + { + if ((tv.vval.v_string != NULL && *tv.vval.v_string != NUL) + || (**arg != '{' && count == 0)) + { + // generate non-empty string or empty string if it's the only + // one + if (generate_PUSHS(cctx, &tv.vval.v_string) == FAIL) + return FAIL; + tv.vval.v_string = NULL; // don't free it now + ++count; + } + clear_tv(&tv); + } + + if (**arg != '{') + { + // found terminating quote + ++*arg; + break; + } + + p = compile_one_expr_in_str(*arg, cctx); + if (p == NULL) + { + ret = FAIL; + break; + } + ++count; + *arg = p; + } if (ret == FAIL || !evaluate) return ret; - ret = compile_all_expr_in_str(tv.vval.v_string, TRUE, cctx); - clear_tv(&tv); + // Small optimization, if there's only a single piece skip the ISN_CONCAT. + if (count > 1) + return generate_CONCAT(cctx, count); - return ret; + return OK; } /* @@ -2161,14 +2204,14 @@ compile_expr8( /* * String constant: "string". */ - case '"': if (eval_string(arg, rettv, TRUE) == FAIL) + case '"': if (eval_string(arg, rettv, TRUE, FALSE) == FAIL) return FAIL; break; /* * Literal string constant: 'str''ing'. */ - case '\'': if (eval_lit_string(arg, rettv, TRUE) == FAIL) + case '\'': if (eval_lit_string(arg, rettv, TRUE, FALSE) == FAIL) return FAIL; break;
--- a/src/vim9instr.c +++ b/src/vim9instr.c @@ -726,6 +726,8 @@ generate_PUSHF(cctx_T *cctx, float_T fnu /* * Generate an ISN_PUSHS instruction. * Consumes "*str". When freed *str is set to NULL, unless "str" is NULL. + * Note that if "str" is used in the instruction OK is returned and "*str" is + * not set to NULL. */ int generate_PUSHS(cctx_T *cctx, char_u **str)