diff options
Diffstat (limited to 'src/nvim/os/shell.c')
| -rw-r--r-- | src/nvim/os/shell.c | 498 | 
1 files changed, 498 insertions, 0 deletions
| diff --git a/src/nvim/os/shell.c b/src/nvim/os/shell.c index d1de18d5b3..ecca88f656 100644 --- a/src/nvim/os/shell.c +++ b/src/nvim/os/shell.c @@ -9,13 +9,17 @@  #include <uv.h>  #include "nvim/ascii.h" +#include "nvim/fileio.h"  #include "nvim/lib/kvec.h"  #include "nvim/log.h"  #include "nvim/event/loop.h"  #include "nvim/event/libuv_process.h"  #include "nvim/event/rstream.h" +#include "nvim/ex_cmds.h" +#include "nvim/misc1.h"  #include "nvim/os/shell.h"  #include "nvim/os/signal.h" +#include "nvim/path.h"  #include "nvim/types.h"  #include "nvim/main.h"  #include "nvim/vim.h" @@ -32,6 +36,8 @@  #define NS_1_SECOND         1000000000U     // 1 second, in nanoseconds  #define OUT_DATA_THRESHOLD  1024 * 10U      // 10KB, "a few screenfuls" of data. +#define SHELL_SPECIAL (char_u *)"\t \"&'$;<>()\\|" +  typedef struct {    char *data;    size_t cap, len; @@ -41,6 +47,498 @@ typedef struct {  # include "os/shell.c.generated.h"  #endif +static void save_patterns(int num_pat, char_u **pat, int *num_file, +                          char_u ***file) +{ +  *file = xmalloc((size_t)num_pat * sizeof(char_u *)); +  for (int i = 0; i < num_pat; i++) { +    char_u *s = vim_strsave(pat[i]); +    // Be compatible with expand_filename(): halve the number of +    // backslashes. +    backslash_halve(s); +    (*file)[i] = s; +  } +  *num_file = num_pat; +} + +static bool have_wildcard(int num, char_u **file) +{ +  for (int i = 0; i < num; i++) { +    if (path_has_wildcard(file[i])) { +      return true; +    } +  } +  return false; +} + +static bool have_dollars(int num, char_u **file) +{ +  for (int i = 0; i < num; i++) { +    if (vim_strchr(file[i], '$') != NULL) { +      return true; +    } +  } +  return false; +} + +/// Performs wildcard pattern matching using the shell. +/// +/// @param      num_pat  is the number of input patterns. +/// @param      pat      is an array of pointers to input patterns. +/// @param[out] num_file is pointer to number of matched file names. +///                      Set to the number of pointers in *file. +/// @param[out] file     is pointer to array of pointers to matched file names. +///                      Memory pointed to by the initial value of *file will +///                      not be freed. +///                      Set to NULL if FAIL is returned. Otherwise points to +///                      allocated memory. +/// @param      flags    is a combination of EW_* flags used in +///                      expand_wildcards(). +///                      If matching fails but EW_NOTFOUND is set in flags or +///                      there are no wildcards, the patterns from pat are +///                      copied into *file. +/// +/// @returns             OK for success or FAIL for error. +int os_expand_wildcards(int num_pat, char_u **pat, int *num_file, +                         char_u ***file, int flags) FUNC_ATTR_NONNULL_ARG(3) +  FUNC_ATTR_NONNULL_ARG(4) +{ +  int i; +  size_t len; +  char_u      *p; +  bool dir; +  char_u *extra_shell_arg = NULL; +  ShellOpts shellopts = kShellOptExpand | kShellOptSilent; +  int j; +  char_u      *tempname; +  char_u      *command; +  FILE        *fd; +  char_u      *buffer; +#define STYLE_ECHO      0       /* use "echo", the default */ +#define STYLE_GLOB      1       /* use "glob", for csh */ +#define STYLE_VIMGLOB   2       /* use "vimglob", for Posix sh */ +#define STYLE_PRINT     3       /* use "print -N", for zsh */ +#define STYLE_BT        4       /* `cmd` expansion, execute the pattern +                                 * directly */ +  int shell_style = STYLE_ECHO; +  int check_spaces; +  static bool did_find_nul = false; +  bool ampersent = false; +  // vimglob() function to define for Posix shell +  static char *sh_vimglob_func = +    "vimglob() { while [ $# -ge 1 ]; do echo \"$1\"; shift; done }; vimglob >"; + +  bool is_fish_shell = +#if defined(UNIX) +    STRNCMP(invocation_path_tail(p_sh, NULL), "fish", 4) == 0; +#else +    false; +#endif + +  *num_file = 0;        // default: no files found +  *file = NULL; + +  // If there are no wildcards, just copy the names to allocated memory. +  // Saves a lot of time, because we don't have to start a new shell. +  if (!have_wildcard(num_pat, pat)) { +    save_patterns(num_pat, pat, num_file, file); +    return OK; +  } + +  // Don't allow any shell command in the sandbox. +  if (sandbox != 0 && check_secure()) { +    return FAIL; +  } + +  // Don't allow the use of backticks in secure and restricted mode. +  if (secure || restricted) { +    for (i = 0; i < num_pat; i++) { +      if (vim_strchr(pat[i], '`') != NULL +          && (check_restricted() || check_secure())) { +        return FAIL; +      } +    } +  } + +  // get a name for the temp file +  if ((tempname = vim_tempname()) == NULL) { +    EMSG(_(e_notmp)); +    return FAIL; +  } + +  // Let the shell expand the patterns and write the result into the temp +  // file. +  // STYLE_BT:         NL separated +  //       If expanding `cmd` execute it directly. +  // STYLE_GLOB:       NUL separated +  //       If we use *csh, "glob" will work better than "echo". +  // STYLE_PRINT:      NL or NUL separated +  //       If we use *zsh, "print -N" will work better than "glob". +  // STYLE_VIMGLOB:    NL separated +  //       If we use *sh*, we define "vimglob()". +  // STYLE_ECHO:       space separated. +  //       A shell we don't know, stay safe and use "echo". +  if (num_pat == 1 && *pat[0] == '`' +      && (len = STRLEN(pat[0])) > 2 +      && *(pat[0] + len - 1) == '`') { +    shell_style = STYLE_BT; +  } else if ((len = STRLEN(p_sh)) >= 3) { +    if (STRCMP(p_sh + len - 3, "csh") == 0) { +      shell_style = STYLE_GLOB; +    } else if (STRCMP(p_sh + len - 3, "zsh") == 0) { +      shell_style = STYLE_PRINT; +    } +  } +  if (shell_style == STYLE_ECHO && strstr((char *)path_tail(p_sh), +          "sh") != NULL) +    shell_style = STYLE_VIMGLOB; + +  // Compute the length of the command.  We need 2 extra bytes: for the +  // optional '&' and for the NUL. +  // Worst case: "unset nonomatch; print -N >" plus two is 29 +  len = STRLEN(tempname) + 29; +  if (shell_style == STYLE_VIMGLOB) +    len += STRLEN(sh_vimglob_func); + +  for (i = 0; i < num_pat; i++) { +    // Count the length of the patterns in the same way as they are put in +    // "command" below. +    len++;                              // add space +    for (j = 0; pat[i][j] != NUL; j++) { +      if (vim_strchr(SHELL_SPECIAL, pat[i][j]) != NULL) { +        len++;                  // may add a backslash +      } +      len++; +    } +  } + +  if (is_fish_shell) { +    len += sizeof("egin;"" end") - 1; +  } + +  command = xmalloc(len); + +  // Build the shell command: +  // - Set $nonomatch depending on EW_NOTFOUND (hopefully the shell +  //    recognizes this). +  // - Add the shell command to print the expanded names. +  // - Add the temp file name. +  // - Add the file name patterns. +  if (shell_style == STYLE_BT) { +    // change `command; command& ` to (command; command ) +    if (is_fish_shell) { +      STRCPY(command, "begin; "); +    } else { +      STRCPY(command, "("); +    } +    STRCAT(command, pat[0] + 1);                // exclude first backtick +    p = command + STRLEN(command) - 1; +    if (is_fish_shell) { +      *p-- = ';'; +      STRCAT(command, " end"); +    } else { +      *p-- = ')';                                 // remove last backtick +    } +    while (p > command && ascii_iswhite(*p)) { +      p--; +    } +    if (*p == '&') {                            // remove trailing '&' +      ampersent = true; +      *p = ' '; +    } +    STRCAT(command, ">"); +  } else { +    if (flags & EW_NOTFOUND) +      STRCPY(command, "set nonomatch; "); +    else +      STRCPY(command, "unset nonomatch; "); +    if (shell_style == STYLE_GLOB) +      STRCAT(command, "glob >"); +    else if (shell_style == STYLE_PRINT) +      STRCAT(command, "print -N >"); +    else if (shell_style == STYLE_VIMGLOB) +      STRCAT(command, sh_vimglob_func); +    else +      STRCAT(command, "echo >"); +  } + +  STRCAT(command, tempname); + +  if (shell_style != STYLE_BT) { +    for (i = 0; i < num_pat; i++) { +      // Put a backslash before special +      // characters, except inside ``. +      bool intick = false; + +      p = command + STRLEN(command); +      *p++ = ' '; +      for (j = 0; pat[i][j] != NUL; j++) { +        if (pat[i][j] == '`') { +          intick = !intick; +        } else if (pat[i][j] == '\\' && pat[i][j + 1] != NUL) { +          // Remove a backslash, take char literally.  But keep +          // backslash inside backticks, before a special character +          // and before a backtick. +          if (intick +              || vim_strchr(SHELL_SPECIAL, pat[i][j + 1]) != NULL +              || pat[i][j + 1] == '`') { +            *p++ = '\\'; +          } +          j++; +        } else if (!intick +                   && ((flags & EW_KEEPDOLLAR) == 0 || pat[i][j] != '$') +                   && vim_strchr(SHELL_SPECIAL, pat[i][j]) != NULL) { +          // Put a backslash before a special character, but not +          // when inside ``. And not for $var when EW_KEEPDOLLAR is +          // set. +          *p++ = '\\'; +        } + +        // Copy one character. +        *p++ = pat[i][j]; +      } +      *p = NUL; +    } +  } + +  if (flags & EW_SILENT) { +    shellopts |= kShellOptHideMess; +  } + +  if (ampersent) { +    STRCAT(command, "&");               // put the '&' after the redirection +  } + +  // Using zsh -G: If a pattern has no matches, it is just deleted from +  // the argument list, otherwise zsh gives an error message and doesn't +  // expand any other pattern. +  if (shell_style == STYLE_PRINT) { +    extra_shell_arg = (char_u *)"-G";       // Use zsh NULL_GLOB option + +  // If we use -f then shell variables set in .cshrc won't get expanded. +  // vi can do it, so we will too, but it is only necessary if there is a "$" +  // in one of the patterns, otherwise we can still use the fast option. +  } else if (shell_style == STYLE_GLOB && !have_dollars(num_pat, pat)) { +    extra_shell_arg = (char_u *)"-f";           // Use csh fast option +  } + +  // execute the shell command +  i = call_shell( +      command, +      shellopts, +      extra_shell_arg +      ); + +  // When running in the background, give it some time to create the temp +  // file, but don't wait for it to finish. +  if (ampersent) { +    os_delay(10L, true); +  } + +  xfree(command); + +  if (i) {                         // os_call_shell() failed +    os_remove((char *)tempname); +    xfree(tempname); +    // With interactive completion, the error message is not printed. +    if (!(flags & EW_SILENT)) { +      msg_putchar('\n');                // clear bottom line quickly +      cmdline_row = Rows - 1;           // continue on last line +      MSG(_(e_wildexpand)); +      msg_start();                    // don't overwrite this message +    } + +    // If a `cmd` expansion failed, don't list `cmd` as a match, even when +    // EW_NOTFOUND is given +    if (shell_style == STYLE_BT) { +      return FAIL; +    } +    goto notfound; +  } + +  // read the names from the file into memory +  fd = fopen((char *)tempname, READBIN); +  if (fd == NULL) { +    // Something went wrong, perhaps a file name with a special char. +    if (!(flags & EW_SILENT)) { +      MSG(_(e_wildexpand)); +      msg_start();                      // don't overwrite this message +    } +    xfree(tempname); +    goto notfound; +  } +  int fseek_res = fseek(fd, 0L, SEEK_END); +  if (fseek_res < 0) { +    xfree(tempname); +    fclose(fd); +    return FAIL; +  } +  int64_t templen = ftell(fd);        // get size of temp file +  if (templen < 0) { +    xfree(tempname); +    fclose(fd); +    return FAIL; +  } +#if SIZEOF_LONG_LONG > SIZEOF_SIZE_T +  assert(templen <= (long long)SIZE_MAX); +#endif +  len = (size_t)templen; +  fseek(fd, 0L, SEEK_SET); +  buffer = xmalloc(len + 1); +  // fread() doesn't terminate buffer with NUL; +  // appropriate termination (not always NUL) is done below. +  size_t readlen = fread((char *)buffer, 1, len, fd); +  fclose(fd); +  os_remove((char *)tempname); +  if (readlen != len) { +    // unexpected read error +    EMSG2(_(e_notread), tempname); +    xfree(tempname); +    xfree(buffer); +    return FAIL; +  } +  xfree(tempname); + +  // file names are separated with Space +  if (shell_style == STYLE_ECHO) { +    buffer[len] = '\n';                 // make sure the buffer ends in NL +    p = buffer; +    for (i = 0; *p != '\n'; i++) {      // count number of entries +      while (*p != ' ' && *p != '\n') { +        p++; +      } +      p = skipwhite(p);                 // skip to next entry +    } +  // file names are separated with NL +  } else if (shell_style == STYLE_BT || shell_style == STYLE_VIMGLOB) { +    buffer[len] = NUL;                  // make sure the buffer ends in NUL +    p = buffer; +    for (i = 0; *p != NUL; i++) {       // count number of entries +      while (*p != '\n' && *p != NUL) { +        p++; +      } +      if (*p != NUL) { +        p++; +      } +      p = skipwhite(p);                 // skip leading white space +    } +  // file names are separated with NUL +  } else { +    // Some versions of zsh use spaces instead of NULs to separate +    // results.  Only do this when there is no NUL before the end of the +    // buffer, otherwise we would never be able to use file names with +    // embedded spaces when zsh does use NULs. +    // When we found a NUL once, we know zsh is OK, set did_find_nul and +    // don't check for spaces again. +    check_spaces = false; +    if (shell_style == STYLE_PRINT && !did_find_nul) { +      // If there is a NUL, set did_find_nul, else set check_spaces +      buffer[len] = NUL; +      if (len && (int)STRLEN(buffer) < (int)len) +        did_find_nul = true; +      else +        check_spaces = true; +    } + +    // Make sure the buffer ends with a NUL.  For STYLE_PRINT there +    // already is one, for STYLE_GLOB it needs to be added. +    if (len && buffer[len - 1] == NUL) { +      len--; +    } else { +      buffer[len] = NUL; +    } +    i = 0; +    for (p = buffer; p < buffer + len; p++) { +      if (*p == NUL || (*p == ' ' && check_spaces)) {       // count entry +        i++; +        *p = NUL; +      } +    } +    if (len) { +      i++;                              // count last entry +    } +  } +  assert(buffer[len] == NUL || buffer[len] == '\n'); + +  if (i == 0) { +    // Can happen when using /bin/sh and typing ":e $NO_SUCH_VAR^I". +    // /bin/sh will happily expand it to nothing rather than returning an +    // error; and hey, it's good to check anyway -- webb. +    xfree(buffer); +    goto notfound; +  } +  *num_file = i; +  *file = xmalloc(sizeof(char_u *) * (size_t)i); + +  // Isolate the individual file names. +  p = buffer; +  for (i = 0; i < *num_file; ++i) { +    (*file)[i] = p; +    // Space or NL separates +    if (shell_style == STYLE_ECHO || shell_style == STYLE_BT +        || shell_style == STYLE_VIMGLOB) { +      while (!(shell_style == STYLE_ECHO && *p == ' ') +             && *p != '\n' && *p != NUL) { +        p++; +      } +      if (p == buffer + len) {                  // last entry +        *p = NUL; +      } else { +        *p++ = NUL; +        p = skipwhite(p);                       // skip to next entry +      } +    } else {          // NUL separates +      while (*p && p < buffer + len) {          // skip entry +        p++; +      } +      p++;                                      // skip NUL +    } +  } + +  // Move the file names to allocated memory. +  for (j = 0, i = 0; i < *num_file; i++) { +    // Require the files to exist. Helps when using /bin/sh +    if (!(flags & EW_NOTFOUND) && !os_path_exists((*file)[i])) { +      continue; +    } + +    // check if this entry should be included +    dir = (os_isdir((*file)[i])); +    if ((dir && !(flags & EW_DIR)) || (!dir && !(flags & EW_FILE))) +      continue; + +    // Skip files that are not executable if we check for that. +    if (!dir && (flags & EW_EXEC) +        && !os_can_exe((char *)(*file)[i], NULL, !(flags & EW_SHELLCMD))) { +      continue; +    } + +    p = xmalloc(STRLEN((*file)[i]) + 1 + dir); +    STRCPY(p, (*file)[i]); +    if (dir) { +      add_pathsep((char *)p);             // add '/' to a directory name +    } +    (*file)[j++] = p; +  } +  xfree(buffer); +  *num_file = j; + +  if (*num_file == 0) {     // rejected all entries +    XFREE_CLEAR(*file); +    goto notfound; +  } + +  return OK; + +notfound: +  if (flags & EW_NOTFOUND) { +    save_patterns(num_pat, pat, num_file, file); +    return OK; +  } +  return FAIL; + +} +  /// Builds the argument vector for running the user-configured 'shell' (p_sh)  /// with an optional command prefixed by 'shellcmdflag' (p_shcf). E.g.:  /// | 
