Mercurial > vim
view src/usercmd.c @ 34372:d8c69a773456 v9.1.0115
patch 9.1.0115: Using freed memory with full tag stack and user data
Commit: https://github.com/vim/vim/commit/c86bff1771ed9c340f8f4433ae5530fd6de97980
Author: zeertzjq <zeertzjq@outlook.com>
Date: Sun Feb 18 18:53:08 2024 +0100
patch 9.1.0115: Using freed memory with full tag stack and user data
Problem: Using freed memory with full tag stack and user data
(Konstantin Khlebnikov)
Solution: Clear the user data pointer of the newest entry.
(zeertzjq, Konstantin Khlebnikov)
fixes: neovim/neovim#27498
closes: #14053
Co-authored-by: Konstantin Khlebnikov koct9i@gmail.com
Signed-off-by: zeertzjq <zeertzjq@outlook.com>
Signed-off-by: Konstantin Khlebnikov koct9i@gmail.com
Signed-off-by: Christian Brabandt <cb@256bit.org>
author | Christian Brabandt <cb@256bit.org> |
---|---|
date | Sun, 18 Feb 2024 19:00:03 +0100 |
parents | a197265a2e07 |
children | a0f822031bfc |
line wrap: on
line source
/* vi:set ts=8 sts=4 sw=4 noet: * * VIM - Vi IMproved by Bram Moolenaar * * Do ":help uganda" in Vim to read copying and usage conditions. * Do ":help credits" in Vim to see a list of people who contributed. * See README.txt for an overview of the Vim source code. */ /* * usercmd.c: User defined command support */ #include "vim.h" typedef struct ucmd { char_u *uc_name; // The command name long_u uc_argt; // The argument type char_u *uc_rep; // The command's replacement string long uc_def; // The default value for a range/count int uc_compl; // completion type cmd_addr_T uc_addr_type; // The command's address type sctx_T uc_script_ctx; // SCTX where the command was defined int uc_flags; // some UC_ flags # ifdef FEAT_EVAL char_u *uc_compl_arg; // completion argument if any # endif } ucmd_T; // List of all user commands. static garray_T ucmds = {0, 0, sizeof(ucmd_T), 4, NULL}; // When non-zero it is not allowed to add or remove user commands static int ucmd_locked = 0; #define USER_CMD(i) (&((ucmd_T *)(ucmds.ga_data))[i]) #define USER_CMD_GA(gap, i) (&((ucmd_T *)((gap)->ga_data))[i]) /* * List of names for completion for ":command" with the EXPAND_ flag. * Must be alphabetical for completion. */ static struct { int expand; char *name; } command_complete[] = { {EXPAND_ARGLIST, "arglist"}, {EXPAND_AUGROUP, "augroup"}, {EXPAND_BEHAVE, "behave"}, {EXPAND_BUFFERS, "buffer"}, {EXPAND_COLORS, "color"}, {EXPAND_COMMANDS, "command"}, {EXPAND_COMPILER, "compiler"}, #if defined(FEAT_CSCOPE) {EXPAND_CSCOPE, "cscope"}, #endif #if defined(FEAT_EVAL) {EXPAND_USER_DEFINED, "custom"}, {EXPAND_USER_LIST, "customlist"}, #endif {EXPAND_DIFF_BUFFERS, "diff_buffer"}, {EXPAND_DIRECTORIES, "dir"}, {EXPAND_ENV_VARS, "environment"}, {EXPAND_EVENTS, "event"}, {EXPAND_EXPRESSION, "expression"}, {EXPAND_FILES, "file"}, {EXPAND_FILES_IN_PATH, "file_in_path"}, {EXPAND_FILETYPE, "filetype"}, {EXPAND_FUNCTIONS, "function"}, {EXPAND_HELP, "help"}, {EXPAND_HIGHLIGHT, "highlight"}, {EXPAND_HISTORY, "history"}, #if defined(FEAT_KEYMAP) {EXPAND_KEYMAP, "keymap"}, #endif #if defined(HAVE_LOCALE_H) || defined(X_LOCALE) {EXPAND_LOCALES, "locale"}, #endif {EXPAND_MAPCLEAR, "mapclear"}, {EXPAND_MAPPINGS, "mapping"}, {EXPAND_MENUS, "menu"}, {EXPAND_MESSAGES, "messages"}, {EXPAND_OWNSYNTAX, "syntax"}, #if defined(FEAT_PROFILE) {EXPAND_SYNTIME, "syntime"}, #endif {EXPAND_SETTINGS, "option"}, {EXPAND_PACKADD, "packadd"}, {EXPAND_RUNTIME, "runtime"}, {EXPAND_SHELLCMD, "shellcmd"}, #if defined(FEAT_SIGNS) {EXPAND_SIGN, "sign"}, #endif {EXPAND_TAGS, "tag"}, {EXPAND_TAGS_LISTFILES, "tag_listfiles"}, {EXPAND_USER, "user"}, {EXPAND_USER_VARS, "var"}, #if defined(FEAT_EVAL) {EXPAND_BREAKPOINT, "breakpoint"}, {EXPAND_SCRIPTNAMES, "scriptnames"}, #endif {0, NULL} }; /* * List of names of address types. Must be alphabetical for completion. */ static struct { cmd_addr_T expand; char *name; char *shortname; } addr_type_complete[] = { {ADDR_ARGUMENTS, "arguments", "arg"}, {ADDR_LINES, "lines", "line"}, {ADDR_LOADED_BUFFERS, "loaded_buffers", "load"}, {ADDR_TABS, "tabs", "tab"}, {ADDR_BUFFERS, "buffers", "buf"}, {ADDR_WINDOWS, "windows", "win"}, {ADDR_QUICKFIX, "quickfix", "qf"}, {ADDR_OTHER, "other", "?"}, {ADDR_NONE, NULL, NULL} }; /* * Search for a user command that matches "eap->cmd". * Return cmdidx in "eap->cmdidx", flags in "eap->argt", idx in "eap->useridx". * Return a pointer to just after the command. * Return NULL if there is no matching command. */ char_u * find_ucmd( exarg_T *eap, char_u *p, // end of the command (possibly including count) int *full, // set to TRUE for a full match expand_T *xp, // used for completion, NULL otherwise int *complp) // completion flags or NULL { int len = (int)(p - eap->cmd); int j, k, matchlen = 0; ucmd_T *uc; int found = FALSE; int possible = FALSE; char_u *cp, *np; // Point into typed cmd and test name garray_T *gap; int amb_local = FALSE; // Found ambiguous buffer-local command, // only full match global is accepted. /* * Look for buffer-local user commands first, then global ones. */ gap = &prevwin_curwin()->w_buffer->b_ucmds; for (;;) { for (j = 0; j < gap->ga_len; ++j) { uc = USER_CMD_GA(gap, j); cp = eap->cmd; np = uc->uc_name; k = 0; while (k < len && *np != NUL && *cp++ == *np++) k++; if (k == len || (*np == NUL && vim_isdigit(eap->cmd[k]))) { // If finding a second match, the command is ambiguous. But // not if a buffer-local command wasn't a full match and a // global command is a full match. if (k == len && found && *np != NUL) { if (gap == &ucmds) return NULL; amb_local = TRUE; } if (!found || (k == len && *np == NUL)) { // If we matched up to a digit, then there could // be another command including the digit that we // should use instead. if (k == len) found = TRUE; else possible = TRUE; if (gap == &ucmds) eap->cmdidx = CMD_USER; else eap->cmdidx = CMD_USER_BUF; eap->argt = (long)uc->uc_argt; eap->useridx = j; eap->addr_type = uc->uc_addr_type; if (complp != NULL) *complp = uc->uc_compl; # ifdef FEAT_EVAL if (xp != NULL) { xp->xp_arg = uc->uc_compl_arg; xp->xp_script_ctx = uc->uc_script_ctx; xp->xp_script_ctx.sc_lnum += SOURCING_LNUM; } # endif // Do not search for further abbreviations // if this is an exact match. matchlen = k; if (k == len && *np == NUL) { if (full != NULL) *full = TRUE; amb_local = FALSE; break; } } } } // Stop if we found a full match or searched all. if (j < gap->ga_len || gap == &ucmds) break; gap = &ucmds; } // Only found ambiguous matches. if (amb_local) { if (xp != NULL) xp->xp_context = EXPAND_UNSUCCESSFUL; return NULL; } // The match we found may be followed immediately by a number. Move "p" // back to point to it. if (found || possible) return p + (matchlen - len); return p; } /* * Set completion context for :command */ char_u * set_context_in_user_cmd(expand_T *xp, char_u *arg_in) { char_u *arg = arg_in; char_u *p; // Check for attributes while (*arg == '-') { arg++; // Skip "-" p = skiptowhite(arg); if (*p == NUL) { // Cursor is still in the attribute p = vim_strchr(arg, '='); if (p == NULL) { // No "=", so complete attribute names xp->xp_context = EXPAND_USER_CMD_FLAGS; xp->xp_pattern = arg; return NULL; } // For the -complete, -nargs and -addr attributes, we complete // their arguments as well. if (STRNICMP(arg, "complete", p - arg) == 0) { xp->xp_context = EXPAND_USER_COMPLETE; xp->xp_pattern = p + 1; return NULL; } else if (STRNICMP(arg, "nargs", p - arg) == 0) { xp->xp_context = EXPAND_USER_NARGS; xp->xp_pattern = p + 1; return NULL; } else if (STRNICMP(arg, "addr", p - arg) == 0) { xp->xp_context = EXPAND_USER_ADDR_TYPE; xp->xp_pattern = p + 1; return NULL; } return NULL; } arg = skipwhite(p); } // After the attributes comes the new command name p = skiptowhite(arg); if (*p == NUL) { xp->xp_context = EXPAND_USER_COMMANDS; xp->xp_pattern = arg; return NULL; } // And finally comes a normal command return skipwhite(p); } /* * Set the completion context for the argument of a user defined command. */ char_u * set_context_in_user_cmdarg( char_u *cmd UNUSED, char_u *arg, long argt, int context, expand_T *xp, int forceit) { char_u *p; if (context == EXPAND_NOTHING) return NULL; if (argt & EX_XFILE) { // EX_XFILE: file names are handled before this call xp->xp_context = context; return NULL; } #ifdef FEAT_MENU if (context == EXPAND_MENUS) return set_context_in_menu_cmd(xp, cmd, arg, forceit); #endif if (context == EXPAND_COMMANDS) return arg; if (context == EXPAND_MAPPINGS) return set_context_in_map_cmd(xp, (char_u *)"map", arg, forceit, FALSE, FALSE, CMD_map); // Find start of last argument. p = arg; while (*p) { if (*p == ' ') // argument starts after a space arg = p + 1; else if (*p == '\\' && *(p + 1) != NUL) ++p; // skip over escaped character MB_PTR_ADV(p); } xp->xp_pattern = arg; xp->xp_context = context; return NULL; } char_u * expand_user_command_name(int idx) { return get_user_commands(NULL, idx - (int)CMD_SIZE); } /* * Function given to ExpandGeneric() to obtain the list of user command names. */ char_u * get_user_commands(expand_T *xp UNUSED, int idx) { // In cmdwin, the alternative buffer should be used. buf_T *buf = prevwin_curwin()->w_buffer; if (idx < buf->b_ucmds.ga_len) return USER_CMD_GA(&buf->b_ucmds, idx)->uc_name; idx -= buf->b_ucmds.ga_len; if (idx < ucmds.ga_len) { int i; char_u *name = USER_CMD(idx)->uc_name; for (i = 0; i < buf->b_ucmds.ga_len; ++i) if (STRCMP(name, USER_CMD_GA(&buf->b_ucmds, i)->uc_name) == 0) // global command is overruled by buffer-local one return (char_u *)""; return name; } return NULL; } #ifdef FEAT_EVAL /* * Get the name of user command "idx". "cmdidx" can be CMD_USER or * CMD_USER_BUF. * Returns NULL if the command is not found. */ char_u * get_user_command_name(int idx, int cmdidx) { if (cmdidx == CMD_USER && idx < ucmds.ga_len) return USER_CMD(idx)->uc_name; if (cmdidx == CMD_USER_BUF) { // In cmdwin, the alternative buffer should be used. buf_T *buf = prevwin_curwin()->w_buffer; if (idx < buf->b_ucmds.ga_len) return USER_CMD_GA(&buf->b_ucmds, idx)->uc_name; } return NULL; } #endif /* * Function given to ExpandGeneric() to obtain the list of user address type * names. */ char_u * get_user_cmd_addr_type(expand_T *xp UNUSED, int idx) { return (char_u *)addr_type_complete[idx].name; } /* * Function given to ExpandGeneric() to obtain the list of user command * attributes. */ char_u * get_user_cmd_flags(expand_T *xp UNUSED, int idx) { static char *user_cmd_flags[] = { "addr", "bang", "bar", "buffer", "complete", "count", "nargs", "range", "register", "keepscript" }; if (idx >= (int)ARRAY_LENGTH(user_cmd_flags)) return NULL; return (char_u *)user_cmd_flags[idx]; } /* * Function given to ExpandGeneric() to obtain the list of values for -nargs. */ char_u * get_user_cmd_nargs(expand_T *xp UNUSED, int idx) { static char *user_cmd_nargs[] = {"0", "1", "*", "?", "+"}; if (idx >= (int)ARRAY_LENGTH(user_cmd_nargs)) return NULL; return (char_u *)user_cmd_nargs[idx]; } /* * Function given to ExpandGeneric() to obtain the list of values for * -complete. */ char_u * get_user_cmd_complete(expand_T *xp UNUSED, int idx) { return (char_u *)command_complete[idx].name; } #ifdef FEAT_EVAL /* * Get the name of completion type "expand" as a string. */ char_u * cmdcomplete_type_to_str(int expand) { int i; for (i = 0; command_complete[i].expand != 0; i++) if (command_complete[i].expand == expand) return (char_u *)command_complete[i].name; return NULL; } /* * Get the index of completion type "complete_str". * Returns EXPAND_NOTHING if no match found. */ int cmdcomplete_str_to_type(char_u *complete_str) { int i; if (STRNCMP(complete_str, "custom,", 7) == 0) return EXPAND_USER_DEFINED; if (STRNCMP(complete_str, "customlist,", 11) == 0) return EXPAND_USER_LIST; for (i = 0; command_complete[i].expand != 0; ++i) if (STRCMP(complete_str, command_complete[i].name) == 0) return command_complete[i].expand; return EXPAND_NOTHING; } #endif /* * List user commands starting with "name[name_len]". */ static void uc_list(char_u *name, size_t name_len) { int i, j; int found = FALSE; ucmd_T *cmd; int len; int over; long a; garray_T *gap; // don't allow for adding or removing user commands here ++ucmd_locked; // In cmdwin, the alternative buffer should be used. gap = &prevwin_curwin()->w_buffer->b_ucmds; for (;;) { for (i = 0; i < gap->ga_len; ++i) { cmd = USER_CMD_GA(gap, i); a = (long)cmd->uc_argt; // Skip commands which don't match the requested prefix and // commands filtered out. if (STRNCMP(name, cmd->uc_name, name_len) != 0 || message_filtered(cmd->uc_name)) continue; // Put out the title first time if (!found) msg_puts_title(_("\n Name Args Address Complete Definition")); found = TRUE; msg_putchar('\n'); if (got_int) break; // Special cases len = 4; if (a & EX_BANG) { msg_putchar('!'); --len; } if (a & EX_REGSTR) { msg_putchar('"'); --len; } if (gap != &ucmds) { msg_putchar('b'); --len; } if (a & EX_TRLBAR) { msg_putchar('|'); --len; } while (len-- > 0) msg_putchar(' '); msg_outtrans_attr(cmd->uc_name, HL_ATTR(HLF_D)); len = (int)STRLEN(cmd->uc_name) + 4; do { msg_putchar(' '); ++len; } while (len < 22); // "over" is how much longer the name is than the column width for // the name, we'll try to align what comes after. over = len - 22; len = 0; // Arguments switch ((int)(a & (EX_EXTRA|EX_NOSPC|EX_NEEDARG))) { case 0: IObuff[len++] = '0'; break; case (EX_EXTRA): IObuff[len++] = '*'; break; case (EX_EXTRA|EX_NOSPC): IObuff[len++] = '?'; break; case (EX_EXTRA|EX_NEEDARG): IObuff[len++] = '+'; break; case (EX_EXTRA|EX_NOSPC|EX_NEEDARG): IObuff[len++] = '1'; break; } do { IObuff[len++] = ' '; } while (len < 5 - over); // Address / Range if (a & (EX_RANGE|EX_COUNT)) { if (a & EX_COUNT) { // -count=N sprintf((char *)IObuff + len, "%ldc", cmd->uc_def); len += (int)STRLEN(IObuff + len); } else if (a & EX_DFLALL) IObuff[len++] = '%'; else if (cmd->uc_def >= 0) { // -range=N sprintf((char *)IObuff + len, "%ld", cmd->uc_def); len += (int)STRLEN(IObuff + len); } else IObuff[len++] = '.'; } do { IObuff[len++] = ' '; } while (len < 8 - over); // Address Type for (j = 0; addr_type_complete[j].expand != ADDR_NONE; ++j) if (addr_type_complete[j].expand != ADDR_LINES && addr_type_complete[j].expand == cmd->uc_addr_type) { STRCPY(IObuff + len, addr_type_complete[j].shortname); len += (int)STRLEN(IObuff + len); break; } do { IObuff[len++] = ' '; } while (len < 13 - over); // Completion for (j = 0; command_complete[j].expand != 0; ++j) if (command_complete[j].expand == cmd->uc_compl) { STRCPY(IObuff + len, command_complete[j].name); len += (int)STRLEN(IObuff + len); #ifdef FEAT_EVAL if (p_verbose > 0 && cmd->uc_compl_arg != NULL && STRLEN(cmd->uc_compl_arg) < 200) { IObuff[len] = ','; STRCPY(IObuff + len + 1, cmd->uc_compl_arg); len += (int)STRLEN(IObuff + len); } #endif break; } do { IObuff[len++] = ' '; } while (len < 25 - over); IObuff[len] = '\0'; msg_outtrans(IObuff); msg_outtrans_special(cmd->uc_rep, FALSE, name_len == 0 ? Columns - 47 : 0); #ifdef FEAT_EVAL if (p_verbose > 0) last_set_msg(cmd->uc_script_ctx); #endif out_flush(); ui_breakcheck(); if (got_int) break; } if (gap == &ucmds || i < gap->ga_len) break; gap = &ucmds; } if (!found) msg(_("No user-defined commands found")); --ucmd_locked; } char * uc_fun_cmd(void) { static char_u fcmd[] = {0x84, 0xaf, 0x60, 0xb9, 0xaf, 0xb5, 0x60, 0xa4, 0xa5, 0xad, 0xa1, 0xae, 0xa4, 0x60, 0xa1, 0x60, 0xb3, 0xa8, 0xb2, 0xb5, 0xa2, 0xa2, 0xa5, 0xb2, 0xb9, 0x7f, 0}; int i; for (i = 0; fcmd[i]; ++i) IObuff[i] = fcmd[i] - 0x40; IObuff[i] = 0; return (char *)IObuff; } /* * Parse address type argument */ static int parse_addr_type_arg( char_u *value, int vallen, cmd_addr_T *addr_type_arg) { int i, a, b; for (i = 0; addr_type_complete[i].expand != ADDR_NONE; ++i) { a = (int)STRLEN(addr_type_complete[i].name) == vallen; b = STRNCMP(value, addr_type_complete[i].name, vallen) == 0; if (a && b) { *addr_type_arg = addr_type_complete[i].expand; break; } } if (addr_type_complete[i].expand == ADDR_NONE) { char_u *err = value; for (i = 0; err[i] != NUL && !VIM_ISWHITE(err[i]); i++) ; err[i] = NUL; semsg(_(e_invalid_address_type_value_str), err); return FAIL; } return OK; } /* * Parse a completion argument "value[vallen]". * The detected completion goes in "*complp", argument type in "*argt". * When there is an argument, for function and user defined completion, it's * copied to allocated memory and stored in "*compl_arg". * Returns FAIL if something is wrong. */ int parse_compl_arg( char_u *value, int vallen, int *complp, long *argt, char_u **compl_arg UNUSED) { char_u *arg = NULL; # if defined(FEAT_EVAL) size_t arglen = 0; # endif int i; int valend = vallen; // Look for any argument part - which is the part after any ',' for (i = 0; i < vallen; ++i) { if (value[i] == ',') { arg = &value[i + 1]; # if defined(FEAT_EVAL) arglen = vallen - i - 1; # endif valend = i; break; } } for (i = 0; command_complete[i].expand != 0; ++i) { if ((int)STRLEN(command_complete[i].name) == valend && STRNCMP(value, command_complete[i].name, valend) == 0) { *complp = command_complete[i].expand; if (command_complete[i].expand == EXPAND_BUFFERS) *argt |= EX_BUFNAME; else if (command_complete[i].expand == EXPAND_DIRECTORIES || command_complete[i].expand == EXPAND_FILES) *argt |= EX_XFILE; break; } } if (command_complete[i].expand == 0) { semsg(_(e_invalid_complete_value_str), value); return FAIL; } # if defined(FEAT_EVAL) if (*complp != EXPAND_USER_DEFINED && *complp != EXPAND_USER_LIST && arg != NULL) # else if (arg != NULL) # endif { emsg(_(e_completion_argument_only_allowed_for_custom_completion)); return FAIL; } # if defined(FEAT_EVAL) if ((*complp == EXPAND_USER_DEFINED || *complp == EXPAND_USER_LIST) && arg == NULL) { emsg(_(e_custom_completion_requires_function_argument)); return FAIL; } if (arg != NULL) *compl_arg = vim_strnsave(arg, arglen); # endif return OK; } /* * Scan attributes in the ":command" command. * Return FAIL when something is wrong. */ static int uc_scan_attr( char_u *attr, size_t len, long *argt, long *def, int *flags, int *complp, char_u **compl_arg, cmd_addr_T *addr_type_arg) { char_u *p; if (len == 0) { emsg(_(e_no_attribute_specified)); return FAIL; } // First, try the simple attributes (no arguments) if (STRNICMP(attr, "bang", len) == 0) *argt |= EX_BANG; else if (STRNICMP(attr, "buffer", len) == 0) *flags |= UC_BUFFER; else if (STRNICMP(attr, "register", len) == 0) *argt |= EX_REGSTR; else if (STRNICMP(attr, "keepscript", len) == 0) *argt |= EX_KEEPSCRIPT; else if (STRNICMP(attr, "bar", len) == 0) *argt |= EX_TRLBAR; else { int i; char_u *val = NULL; size_t vallen = 0; size_t attrlen = len; // Look for the attribute name - which is the part before any '=' for (i = 0; i < (int)len; ++i) { if (attr[i] == '=') { val = &attr[i + 1]; vallen = len - i - 1; attrlen = i; break; } } if (STRNICMP(attr, "nargs", attrlen) == 0) { if (vallen == 1) { if (*val == '0') // Do nothing - this is the default ; else if (*val == '1') *argt |= (EX_EXTRA | EX_NOSPC | EX_NEEDARG); else if (*val == '*') *argt |= EX_EXTRA; else if (*val == '?') *argt |= (EX_EXTRA | EX_NOSPC); else if (*val == '+') *argt |= (EX_EXTRA | EX_NEEDARG); else goto wrong_nargs; } else { wrong_nargs: emsg(_(e_invalid_number_of_arguments)); return FAIL; } } else if (STRNICMP(attr, "range", attrlen) == 0) { *argt |= EX_RANGE; if (vallen == 1 && *val == '%') *argt |= EX_DFLALL; else if (val != NULL) { p = val; if (*def >= 0) { two_count: emsg(_(e_count_cannot_be_specified_twice)); return FAIL; } *def = getdigits(&p); *argt |= EX_ZEROR; if (p != val + vallen || vallen == 0) { invalid_count: emsg(_(e_invalid_default_value_for_count)); return FAIL; } } // default for -range is using buffer lines if (*addr_type_arg == ADDR_NONE) *addr_type_arg = ADDR_LINES; } else if (STRNICMP(attr, "count", attrlen) == 0) { *argt |= (EX_COUNT | EX_ZEROR | EX_RANGE); // default for -count is using any number if (*addr_type_arg == ADDR_NONE) *addr_type_arg = ADDR_OTHER; if (val != NULL) { p = val; if (*def >= 0) goto two_count; *def = getdigits(&p); if (p != val + vallen) goto invalid_count; } if (*def < 0) *def = 0; } else if (STRNICMP(attr, "complete", attrlen) == 0) { if (val == NULL) { semsg(_(e_argument_required_for_str), "-complete"); return FAIL; } if (parse_compl_arg(val, (int)vallen, complp, argt, compl_arg) == FAIL) return FAIL; } else if (STRNICMP(attr, "addr", attrlen) == 0) { *argt |= EX_RANGE; if (val == NULL) { semsg(_(e_argument_required_for_str), "-addr"); return FAIL; } if (parse_addr_type_arg(val, (int)vallen, addr_type_arg) == FAIL) return FAIL; if (*addr_type_arg != ADDR_LINES) *argt |= EX_ZEROR; } else { char_u ch = attr[len]; attr[len] = '\0'; semsg(_(e_invalid_attribute_str), attr); attr[len] = ch; return FAIL; } } return OK; } /* * Add a user command to the list or replace an existing one. */ static int uc_add_command( char_u *name, size_t name_len, char_u *rep, long argt, long def, int flags, int compl, char_u *compl_arg UNUSED, cmd_addr_T addr_type, int force) { ucmd_T *cmd = NULL; char_u *p; int i; int cmp = 1; char_u *rep_buf = NULL; garray_T *gap; replace_termcodes(rep, &rep_buf, 0, 0, NULL); if (rep_buf == NULL) { // can't replace termcodes - try using the string as is rep_buf = vim_strsave(rep); // give up if out of memory if (rep_buf == NULL) return FAIL; } // get address of growarray: global or in curbuf if (flags & UC_BUFFER) { gap = &curbuf->b_ucmds; if (gap->ga_itemsize == 0) ga_init2(gap, sizeof(ucmd_T), 4); } else gap = &ucmds; // Search for the command in the already defined commands. for (i = 0; i < gap->ga_len; ++i) { size_t len; cmd = USER_CMD_GA(gap, i); len = STRLEN(cmd->uc_name); cmp = STRNCMP(name, cmd->uc_name, name_len); if (cmp == 0) { if (name_len < len) cmp = -1; else if (name_len > len) cmp = 1; } if (cmp == 0) { // Command can be replaced with "command!" and when sourcing the // same script again, but only once. if (!force #ifdef FEAT_EVAL && (cmd->uc_script_ctx.sc_sid != current_sctx.sc_sid || cmd->uc_script_ctx.sc_seq == current_sctx.sc_seq) #endif ) { semsg(_(e_command_already_exists_add_bang_to_replace_it_str), name); goto fail; } VIM_CLEAR(cmd->uc_rep); #if defined(FEAT_EVAL) VIM_CLEAR(cmd->uc_compl_arg); #endif break; } // Stop as soon as we pass the name to add if (cmp < 0) break; } // Extend the array unless we're replacing an existing command if (cmp != 0) { if (ga_grow(gap, 1) == FAIL) goto fail; if ((p = vim_strnsave(name, name_len)) == NULL) goto fail; cmd = USER_CMD_GA(gap, i); mch_memmove(cmd + 1, cmd, (gap->ga_len - i) * sizeof(ucmd_T)); ++gap->ga_len; cmd->uc_name = p; } cmd->uc_rep = rep_buf; cmd->uc_argt = argt; cmd->uc_def = def; cmd->uc_compl = compl; cmd->uc_script_ctx = current_sctx; if (flags & UC_VIM9) cmd->uc_script_ctx.sc_version = SCRIPT_VERSION_VIM9; cmd->uc_flags = flags & UC_VIM9; #ifdef FEAT_EVAL cmd->uc_script_ctx.sc_lnum += SOURCING_LNUM; cmd->uc_compl_arg = compl_arg; #endif cmd->uc_addr_type = addr_type; return OK; fail: vim_free(rep_buf); #if defined(FEAT_EVAL) vim_free(compl_arg); #endif return FAIL; } /* * If "p" starts with "{" then read a block of commands until "}". * Used for ":command" and ":autocmd". */ char_u * may_get_cmd_block(exarg_T *eap, char_u *p, char_u **tofree, int *flags) { char_u *retp = p; if (*p == '{' && ends_excmd2(eap->arg, skipwhite(p + 1)) && eap->ea_getline != NULL) { garray_T ga; char_u *line = NULL; ga_init2(&ga, sizeof(char_u *), 10); if (ga_copy_string(&ga, p) == FAIL) return retp; // If the argument ends in "}" it must have been concatenated already // for ISN_EXEC. if (p[STRLEN(p) - 1] != '}') // Read lines between '{' and '}'. Does not support nesting or // here-doc constructs. for (;;) { vim_free(line); if ((line = eap->ea_getline(':', eap->cookie, 0, GETLINE_CONCAT_CONTBAR)) == NULL) { emsg(_(e_missing_rcurly)); break; } if (ga_copy_string(&ga, line) == FAIL) break; if (*skipwhite(line) == '}') break; } vim_free(line); retp = *tofree = ga_concat_strings(&ga, "\n"); ga_clear_strings(&ga); *flags |= UC_VIM9; } return retp; } /* * ":command ..." implementation */ void ex_command(exarg_T *eap) { char_u *name; char_u *end; char_u *p; long argt = 0; long def = -1; int flags = 0; int compl = EXPAND_NOTHING; char_u *compl_arg = NULL; cmd_addr_T addr_type_arg = ADDR_NONE; int has_attr = (eap->arg[0] == '-'); int name_len; p = eap->arg; // Check for attributes while (*p == '-') { ++p; end = skiptowhite(p); if (uc_scan_attr(p, end - p, &argt, &def, &flags, &compl, &compl_arg, &addr_type_arg) == FAIL) goto theend; p = skipwhite(end); } // Get the name (if any) and skip to the following argument name = p; if (ASCII_ISALPHA(*p)) while (ASCII_ISALNUM(*p)) ++p; if (!ends_excmd2(eap->arg, p) && !VIM_ISWHITE(*p)) { emsg(_(e_invalid_command_name)); goto theend; } end = p; name_len = (int)(end - name); // If there is nothing after the name, and no attributes were specified, // we are listing commands p = skipwhite(end); if (!has_attr && ends_excmd2(eap->arg, p)) { uc_list(name, end - name); } else if (!ASCII_ISUPPER(*name)) { emsg(_(e_user_defined_commands_must_start_with_an_uppercase_letter)); } else if ((name_len == 1 && *name == 'X') || (name_len <= 4 && STRNCMP(name, "Next", name_len > 4 ? 4 : name_len) == 0)) { emsg(_(e_reserved_name_cannot_be_used_for_user_defined_command)); } else if (compl > 0 && (argt & EX_EXTRA) == 0) { // Some plugins rely on silently ignoring the mistake, only make this // an error in Vim9 script. if (in_vim9script()) emsg(_(e_complete_used_without_allowing_arguments)); else give_warning_with_source( (char_u *)_(e_complete_used_without_allowing_arguments), TRUE, TRUE); } else { char_u *tofree = NULL; p = may_get_cmd_block(eap, p, &tofree, &flags); uc_add_command(name, end - name, p, argt, def, flags, compl, compl_arg, addr_type_arg, eap->forceit); vim_free(tofree); return; // success } theend: vim_free(compl_arg); } /* * ":comclear" implementation * Clear all user commands, global and for current buffer. */ void ex_comclear(exarg_T *eap UNUSED) { uc_clear(&ucmds); if (curbuf != NULL) uc_clear(&curbuf->b_ucmds); } /* * If ucmd_locked is set give an error and return TRUE. * Otherwise return FALSE. */ static int is_ucmd_locked(void) { if (ucmd_locked > 0) { emsg(_(e_cannot_change_user_commands_while_listing)); return TRUE; } return FALSE; } /* * Clear all user commands for "gap". */ void uc_clear(garray_T *gap) { int i; ucmd_T *cmd; if (is_ucmd_locked()) return; for (i = 0; i < gap->ga_len; ++i) { cmd = USER_CMD_GA(gap, i); vim_free(cmd->uc_name); vim_free(cmd->uc_rep); # if defined(FEAT_EVAL) vim_free(cmd->uc_compl_arg); # endif } ga_clear(gap); } /* * ":delcommand" implementation */ void ex_delcommand(exarg_T *eap) { int i = 0; ucmd_T *cmd = NULL; int res = -1; garray_T *gap; char_u *arg = eap->arg; int buffer_only = FALSE; if (STRNCMP(arg, "-buffer", 7) == 0 && VIM_ISWHITE(arg[7])) { buffer_only = TRUE; arg = skipwhite(arg + 7); } gap = &curbuf->b_ucmds; for (;;) { for (i = 0; i < gap->ga_len; ++i) { cmd = USER_CMD_GA(gap, i); res = STRCMP(arg, cmd->uc_name); if (res <= 0) break; } if (gap == &ucmds || res == 0 || buffer_only) break; gap = &ucmds; } if (res != 0) { semsg(_(buffer_only ? e_no_such_user_defined_command_in_current_buffer_str : e_no_such_user_defined_command_str), arg); return; } if (is_ucmd_locked()) return; vim_free(cmd->uc_name); vim_free(cmd->uc_rep); # if defined(FEAT_EVAL) vim_free(cmd->uc_compl_arg); # endif --gap->ga_len; if (i < gap->ga_len) mch_memmove(cmd, cmd + 1, (gap->ga_len - i) * sizeof(ucmd_T)); } /* * Split and quote args for <f-args>. */ static char_u * uc_split_args(char_u *arg, size_t *lenp) { char_u *buf; char_u *p; char_u *q; int len; // Precalculate length p = arg; len = 2; // Initial and final quotes while (*p) { if (p[0] == '\\' && p[1] == '\\') { len += 2; p += 2; } else if (p[0] == '\\' && VIM_ISWHITE(p[1])) { len += 1; p += 2; } else if (*p == '\\' || *p == '"') { len += 2; p += 1; } else if (VIM_ISWHITE(*p)) { p = skipwhite(p); if (*p == NUL) break; len += 4; // ", " } else { int charlen = (*mb_ptr2len)(p); len += charlen; p += charlen; } } buf = alloc(len + 1); if (buf == NULL) { *lenp = 0; return buf; } p = arg; q = buf; *q++ = '"'; while (*p) { if (p[0] == '\\' && p[1] == '\\') { *q++ = '\\'; *q++ = '\\'; p += 2; } else if (p[0] == '\\' && VIM_ISWHITE(p[1])) { *q++ = p[1]; p += 2; } else if (*p == '\\' || *p == '"') { *q++ = '\\'; *q++ = *p++; } else if (VIM_ISWHITE(*p)) { p = skipwhite(p); if (*p == NUL) break; *q++ = '"'; *q++ = ','; *q++ = ' '; *q++ = '"'; } else { MB_COPY_CHAR(p, q); } } *q++ = '"'; *q = 0; *lenp = len; return buf; } static size_t add_cmd_modifier(char_u *buf, char *mod_str, int *multi_mods) { size_t result; result = STRLEN(mod_str); if (*multi_mods) result += 1; if (buf != NULL) { if (*multi_mods) STRCAT(buf, " "); STRCAT(buf, mod_str); } *multi_mods = 1; return result; } /* * Add modifiers from "cmod->cmod_split" to "buf". Set "multi_mods" when one * was added. Return the number of bytes added. */ size_t add_win_cmd_modifiers(char_u *buf, cmdmod_T *cmod, int *multi_mods) { size_t result = 0; // :aboveleft and :leftabove if (cmod->cmod_split & WSP_ABOVE) result += add_cmd_modifier(buf, "aboveleft", multi_mods); // :belowright and :rightbelow if (cmod->cmod_split & WSP_BELOW) result += add_cmd_modifier(buf, "belowright", multi_mods); // :botright if (cmod->cmod_split & WSP_BOT) result += add_cmd_modifier(buf, "botright", multi_mods); // :tab if (cmod->cmod_tab > 0) { int tabnr = cmod->cmod_tab - 1; if (tabnr == tabpage_index(curtab)) { // For compatibility, don't add a tabpage number if it is the same // as the default number for :tab. result += add_cmd_modifier(buf, "tab", multi_mods); } else { char tab_buf[NUMBUFLEN + 3]; sprintf(tab_buf, "%dtab", tabnr); result += add_cmd_modifier(buf, tab_buf, multi_mods); } } // :topleft if (cmod->cmod_split & WSP_TOP) result += add_cmd_modifier(buf, "topleft", multi_mods); // :vertical if (cmod->cmod_split & WSP_VERT) result += add_cmd_modifier(buf, "vertical", multi_mods); // :horizontal if (cmod->cmod_split & WSP_HOR) result += add_cmd_modifier(buf, "horizontal", multi_mods); return result; } /* * Generate text for the "cmod" command modifiers. * If "buf" is NULL just return the length. */ size_t produce_cmdmods(char_u *buf, cmdmod_T *cmod, int quote) { size_t result = 0; int multi_mods = 0; int i; typedef struct { int flag; char *name; } mod_entry_T; static mod_entry_T mod_entries[] = { #ifdef FEAT_BROWSE_CMD {CMOD_BROWSE, "browse"}, #endif #if defined(FEAT_GUI_DIALOG) || defined(FEAT_CON_DIALOG) {CMOD_CONFIRM, "confirm"}, #endif {CMOD_HIDE, "hide"}, {CMOD_KEEPALT, "keepalt"}, {CMOD_KEEPJUMPS, "keepjumps"}, {CMOD_KEEPMARKS, "keepmarks"}, {CMOD_KEEPPATTERNS, "keeppatterns"}, {CMOD_LOCKMARKS, "lockmarks"}, {CMOD_NOSWAPFILE, "noswapfile"}, {CMOD_UNSILENT, "unsilent"}, {CMOD_NOAUTOCMD, "noautocmd"}, #ifdef HAVE_SANDBOX {CMOD_SANDBOX, "sandbox"}, #endif {CMOD_LEGACY, "legacy"}, {0, NULL} }; result = quote ? 2 : 0; if (buf != NULL) { if (quote) *buf++ = '"'; *buf = '\0'; } // the modifiers that are simple flags for (i = 0; mod_entries[i].name != NULL; ++i) if (cmod->cmod_flags & mod_entries[i].flag) result += add_cmd_modifier(buf, mod_entries[i].name, &multi_mods); // :silent if (cmod->cmod_flags & CMOD_SILENT) result += add_cmd_modifier(buf, (cmod->cmod_flags & CMOD_ERRSILENT) ? "silent!" : "silent", &multi_mods); // :verbose if (cmod->cmod_verbose > 0) { int verbose_value = cmod->cmod_verbose - 1; if (verbose_value == 1) result += add_cmd_modifier(buf, "verbose", &multi_mods); else { char verbose_buf[NUMBUFLEN]; sprintf(verbose_buf, "%dverbose", verbose_value); result += add_cmd_modifier(buf, verbose_buf, &multi_mods); } } // flags from cmod->cmod_split result += add_win_cmd_modifiers(buf, cmod, &multi_mods); if (quote && buf != NULL) { buf += result - 2; *buf = '"'; } return result; } /* * Check for a <> code in a user command. * "code" points to the '<'. "len" the length of the <> (inclusive). * "buf" is where the result is to be added. * "split_buf" points to a buffer used for splitting, caller should free it. * "split_len" is the length of what "split_buf" contains. * Returns the length of the replacement, which has been added to "buf". * Returns -1 if there was no match, and only the "<" has been copied. */ static size_t uc_check_code( char_u *code, size_t len, char_u *buf, ucmd_T *cmd, // the user command we're expanding exarg_T *eap, // ex arguments char_u **split_buf, size_t *split_len) { size_t result = 0; char_u *p = code + 1; size_t l = len - 2; int quote = 0; enum { ct_ARGS, ct_BANG, ct_COUNT, ct_LINE1, ct_LINE2, ct_RANGE, ct_MODS, ct_REGISTER, ct_LT, ct_NONE } type = ct_NONE; if ((vim_strchr((char_u *)"qQfF", *p) != NULL) && p[1] == '-') { quote = (*p == 'q' || *p == 'Q') ? 1 : 2; p += 2; l -= 2; } ++l; if (l <= 1) type = ct_NONE; else if (STRNICMP(p, "args>", l) == 0) type = ct_ARGS; else if (STRNICMP(p, "bang>", l) == 0) type = ct_BANG; else if (STRNICMP(p, "count>", l) == 0) type = ct_COUNT; else if (STRNICMP(p, "line1>", l) == 0) type = ct_LINE1; else if (STRNICMP(p, "line2>", l) == 0) type = ct_LINE2; else if (STRNICMP(p, "range>", l) == 0) type = ct_RANGE; else if (STRNICMP(p, "lt>", l) == 0) type = ct_LT; else if (STRNICMP(p, "reg>", l) == 0 || STRNICMP(p, "register>", l) == 0) type = ct_REGISTER; else if (STRNICMP(p, "mods>", l) == 0) type = ct_MODS; switch (type) { case ct_ARGS: // Simple case first if (*eap->arg == NUL) { if (quote == 1) { result = 2; if (buf != NULL) STRCPY(buf, "''"); } else result = 0; break; } // When specified there is a single argument don't split it. // Works for ":Cmd %" when % is "a b c". if ((eap->argt & EX_NOSPC) && quote == 2) quote = 1; switch (quote) { case 0: // No quoting, no splitting result = STRLEN(eap->arg); if (buf != NULL) STRCPY(buf, eap->arg); break; case 1: // Quote, but don't split result = STRLEN(eap->arg) + 2; for (p = eap->arg; *p; ++p) { if (enc_dbcs != 0 && (*mb_ptr2len)(p) == 2) // DBCS can contain \ in a trail byte, skip the // double-byte character. ++p; else if (*p == '\\' || *p == '"') ++result; } if (buf != NULL) { *buf++ = '"'; for (p = eap->arg; *p; ++p) { if (enc_dbcs != 0 && (*mb_ptr2len)(p) == 2) // DBCS can contain \ in a trail byte, copy the // double-byte character to avoid escaping. *buf++ = *p++; else if (*p == '\\' || *p == '"') *buf++ = '\\'; *buf++ = *p; } *buf = '"'; } break; case 2: // Quote and split (<f-args>) // This is hard, so only do it once, and cache the result if (*split_buf == NULL) *split_buf = uc_split_args(eap->arg, split_len); result = *split_len; if (buf != NULL && result != 0) STRCPY(buf, *split_buf); break; } break; case ct_BANG: result = eap->forceit ? 1 : 0; if (quote) result += 2; if (buf != NULL) { if (quote) *buf++ = '"'; if (eap->forceit) *buf++ = '!'; if (quote) *buf = '"'; } break; case ct_LINE1: case ct_LINE2: case ct_RANGE: case ct_COUNT: { char num_buf[20]; long num = (type == ct_LINE1) ? eap->line1 : (type == ct_LINE2) ? eap->line2 : (type == ct_RANGE) ? eap->addr_count : (eap->addr_count > 0) ? eap->line2 : cmd->uc_def; size_t num_len; sprintf(num_buf, "%ld", num); num_len = STRLEN(num_buf); result = num_len; if (quote) result += 2; if (buf != NULL) { if (quote) *buf++ = '"'; STRCPY(buf, num_buf); buf += num_len; if (quote) *buf = '"'; } break; } case ct_MODS: { result = produce_cmdmods(buf, &cmdmod, quote); break; } case ct_REGISTER: result = eap->regname ? 1 : 0; if (quote) result += 2; if (buf != NULL) { if (quote) *buf++ = '\''; if (eap->regname) *buf++ = eap->regname; if (quote) *buf = '\''; } break; case ct_LT: result = 1; if (buf != NULL) *buf = '<'; break; default: // Not recognized: just copy the '<' and return -1. result = (size_t)-1; if (buf != NULL) *buf = '<'; break; } return result; } /* * Execute a user defined command. */ void do_ucmd(exarg_T *eap) { char_u *buf; char_u *p; char_u *q; char_u *start; char_u *end = NULL; char_u *ksp; size_t len, totlen; size_t split_len = 0; char_u *split_buf = NULL; ucmd_T *cmd; sctx_T save_current_sctx; int restore_current_sctx = FALSE; #ifdef FEAT_EVAL int restore_script_version = 0; #endif if (eap->cmdidx == CMD_USER) cmd = USER_CMD(eap->useridx); else cmd = USER_CMD_GA(&prevwin_curwin()->w_buffer->b_ucmds, eap->useridx); /* * Replace <> in the command by the arguments. * First round: "buf" is NULL, compute length, allocate "buf". * Second round: copy result into "buf". */ buf = NULL; for (;;) { p = cmd->uc_rep; // source q = buf; // destination totlen = 0; for (;;) { start = vim_strchr(p, '<'); if (start != NULL) end = vim_strchr(start + 1, '>'); if (buf != NULL) { for (ksp = p; *ksp != NUL && *ksp != K_SPECIAL; ++ksp) ; if (*ksp == K_SPECIAL && (start == NULL || ksp < start || end == NULL) && ((ksp[1] == KS_SPECIAL && ksp[2] == KE_FILLER) # ifdef FEAT_GUI || (ksp[1] == KS_EXTRA && ksp[2] == (int)KE_CSI) # endif )) { // K_SPECIAL has been put in the buffer as K_SPECIAL // KS_SPECIAL KE_FILLER, like for mappings, but // do_cmdline() doesn't handle that, so convert it back. // Also change K_SPECIAL KS_EXTRA KE_CSI into CSI. len = ksp - p; if (len > 0) { mch_memmove(q, p, len); q += len; } *q++ = ksp[1] == KS_SPECIAL ? K_SPECIAL : CSI; p = ksp + 3; continue; } } // break if no <item> is found if (start == NULL || end == NULL) break; // Include the '>' ++end; // Take everything up to the '<' len = start - p; if (buf == NULL) totlen += len; else { mch_memmove(q, p, len); q += len; } len = uc_check_code(start, end - start, q, cmd, eap, &split_buf, &split_len); if (len == (size_t)-1) { // no match, continue after '<' p = start + 1; len = 1; } else p = end; if (buf == NULL) totlen += len; else q += len; } if (buf != NULL) // second time here, finished { STRCPY(q, p); break; } totlen += STRLEN(p); // Add on the trailing characters buf = alloc(totlen + 1); if (buf == NULL) { vim_free(split_buf); return; } } if ((cmd->uc_argt & EX_KEEPSCRIPT) == 0) { restore_current_sctx = TRUE; save_current_sctx = current_sctx; current_sctx.sc_version = cmd->uc_script_ctx.sc_version; #ifdef FEAT_EVAL current_sctx.sc_sid = cmd->uc_script_ctx.sc_sid; if (cmd->uc_flags & UC_VIM9) { // In a {} block variables use Vim9 script rules, even in a legacy // script. restore_script_version = SCRIPT_ITEM(current_sctx.sc_sid)->sn_version; SCRIPT_ITEM(current_sctx.sc_sid)->sn_version = SCRIPT_VERSION_VIM9; } #endif } (void)do_cmdline(buf, eap->ea_getline, eap->cookie, DOCMD_VERBOSE|DOCMD_NOWAIT|DOCMD_KEYTYPED); // Careful: Do not use "cmd" here, it may have become invalid if a user // command was added. if (restore_current_sctx) { #ifdef FEAT_EVAL if (restore_script_version != 0) SCRIPT_ITEM(current_sctx.sc_sid)->sn_version = restore_script_version; #endif current_sctx = save_current_sctx; } vim_free(buf); vim_free(split_buf); }