-*tagsrch.txt* For Vim version 9.2. Last change: 2026 May 31
+*tagsrch.txt* For Vim version 9.2. Last change: 2026 Jul 24
VIM REFERENCE MANUAL by Bram Moolenaar
either relative to the current directory or a full
path.
cmd Ex command used to locate the tag in the file. This
- can be either an Ex search pattern or a line number.
+ can be a line number, a search pattern or any other
+ Ex command, like in a tags file (see
+ |tags-file-format|). A search pattern must start and
+ end with "/" or "?". An invalid value results in
+ |E987|.
Note that the format is similar to that of |taglist()|, which makes it possible
to use its output to generate the result.
The following fields are optional:
}
#ifdef FEAT_EVAL
+// Classification of a "cmd" value returned by a tagfunc.
+typedef enum {
+ TAGCMD_INVALID, // cannot be stored in a tag line
+ TAGCMD_ADDRESS, // line number or /pat/ or ?pat? (no "|" needed)
+ TAGCMD_GENERIC // any other Ex command: needs a "|" terminator
+} tagcmd_T;
+
+/*
+ * Classify "cmd" from a tagfunc result (see tagcmd_T), like a tags file
+ * address, to decide whether a "|" terminator must be appended before storing
+ * it in a tag line.
+ */
+ static tagcmd_T
+tagfunc_cmd_kind(char_u *cmd)
+{
+ if (VIM_ISDIGIT(*cmd))
+ return *skipdigits(cmd) == NUL ? TAGCMD_ADDRESS : TAGCMD_INVALID;
+ if (*cmd == '/' || *cmd == '?')
+ {
+ // A Tab inside the pattern is fine (Universal Ctags emits one for a
+ // tab-indented line); only trailing content after the closing
+ // delimiter, which would break the tag line fields, is rejected.
+ char_u *end = skip_regexp(cmd + 1, *cmd, FALSE);
+
+ return (*end == *cmd && end[1] == NUL)
+ ? TAGCMD_ADDRESS : TAGCMD_INVALID;
+ }
+ if (vim_strpbrk(cmd, (char_u *)"\t\r\n") != NULL)
+ return TAGCMD_INVALID;
+ return TAGCMD_GENERIC;
+}
+
/*
* Call the user-defined function to generate a list of tags used by
* find_tags().
break;
}
+ tagcmd_T cmdkind = tagfunc_cmd_kind(res_cmd);
+ if (cmdkind == TAGCMD_INVALID)
+ {
+ emsg(_(e_invalid_return_value_from_tagfunc));
+ break;
+ }
+ if (cmdkind == TAGCMD_GENERIC)
+ len += 3; // need space for "|;\""
+
if (name_only)
mfp = vim_strsave(res_name);
else
STRCPY(p, res_cmd);
p += STRLEN(p);
- if (has_extra)
+ if (cmdkind == TAGCMD_GENERIC)
+ *p++ = '|'; // terminate the command
+
+ if (cmdkind == TAGCMD_GENERIC || has_extra)
{
STRCPY(p, ";\"");
p += STRLEN(p);
str++;
if (find_extra(&str) == OK)
{
- pbuf_end = str;
+ // Drop a trailing "|" that terminates a generic Ex command, so it
+ // is not executed as an empty command separator.
+ if (str > pbuf && str[-1] == '|')
+ pbuf_end = str - 1;
+ else
+ pbuf_end = str;
*pbuf_end = NUL;
}
}
set tagfunc=
endfunc
+" The 'cmd' field can be a line number, a search pattern or any other Ex
+" command, like in a tags file. A value that would break the tag line (an
+" unterminated pattern, trailing junk, or a <Tab> outside a pattern) is
+" rejected with E987 instead of silently corrupting the result.
+func Test_tagfunc_invalid_cmd()
+ func InvalidCmd(pat, flags, info)
+ return [{'name': 'a', 'filename': 'Xtest', 'cmd': a:pat, 'kind': 'f'}]
+ endfunc
+ set tagfunc=InvalidCmd
+
+ " unterminated search pattern
+ call assert_fails("call taglist('/foo')", 'E987:')
+ " trailing content after the closing delimiter of a pattern
+ call assert_fails('call taglist("/foo/\tbar")', 'E987:')
+ " a line number with trailing junk
+ call assert_fails("call taglist('3G5')", 'E987:')
+ " a <Tab> in a generic command would break the tag line fields
+ call assert_fails('call taglist("call\tcursor(3, 5)")', 'E987:')
+
+ " valid forms are accepted and returned unchanged: line number, delimited
+ " patterns, and a Tab inside a pattern (Universal Ctags emits one like this
+ " for a tab-indented line)
+ call assert_equal('42', taglist('42')[0].cmd)
+ call assert_equal('/foo/', taglist('/foo/')[0].cmd)
+ call assert_equal('?foo?', taglist('?foo?')[0].cmd)
+ call assert_equal("/^\tint foo$/", taglist("/^\tint foo$/")[0].cmd)
+
+ " a generic command round-trips with its kind field intact: before the fix
+ " it swallowed the ';"<Tab>kind' part, so kind came back empty (#20781)
+ let t = taglist('call cursor(3, 5)')[0]
+ call assert_equal('call cursor(3, 5)', t.cmd)
+ call assert_equal('f', t.kind)
+
+ set tagfunc&
+ delfunc InvalidCmd
+endfunc
+
+" A tagfunc 'cmd' is executed on a tag jump. A search pattern must reach the
+" matched column (no regression from the do_search() path), and a line number
+" or a generic Ex command must position the cursor as written.
+func Test_tagfunc_cmd_jump()
+ call writefile(['line one', 'line two', 'foo bar baz'], 'Xtagcmd', 'D')
+ func JumpCmd(pat, flags, info)
+ return [{'name': 'a', 'filename': 'Xtagcmd', 'cmd': s:cmd, 'kind': 'f'}]
+ endfunc
+ set tagfunc=JumpCmd
+
+ " '5|' / cursor col 5 / the 'b' of 'bar' all land on [3, 5]
+ for cmd in ['/bar/', 'call cursor(3, 5)', 'normal! 3G5|']
+ let s:cmd = cmd
+ enew
+ tag a
+ call assert_equal([3, 5], [line('.'), col('.')], 'cmd: ' .. cmd)
+ bw!
+ endfor
+
+ set tagfunc&
+ delfunc JumpCmd
+endfunc
+
+" A generic Ex command from a tagfunc runs under 'secure' and the sandbox, just
+" like one from a tags file, so it cannot delete a file or run a shell.
+func Test_tagfunc_cmd_secure()
+ call writefile(['one', 'two', 'three'], 'Xtagcmd', 'D')
+ call writefile(['keep me'], 'Xvictim', 'D')
+ func EvilTagFunc(pat, flags, info)
+ return [{'name': 'a', 'filename': 'Xtagcmd',
+ \ 'cmd': "call delete('Xvictim')", 'kind': 'f'}]
+ endfunc
+ set tagfunc=EvilTagFunc
+
+ enew
+ call assert_fails('tag a', 'E12:')
+ call assert_true(filereadable('Xvictim'))
+
+ bw!
+ set tagfunc&
+ delfunc EvilTagFunc
+endfunc
+
" vim: shiftwidth=2 sts=2 expandtab