# HG changeset patch # User Bram Moolenaar # Date 1590857104 -7200 # Node ID 89b0f161e6a6f43ebf4e8dafc23ca98ed852236f # Parent 3b819401b347b3e8adc57fd51446d53767308a88 patch 8.2.0850: MS-Windows: exepath() works different from cmd.exe Commit: https://github.com/vim/vim/commit/95da136142628e06425f9d9eb2d1ca56a9e48feb Author: Bram Moolenaar Date: Sat May 30 18:37:55 2020 +0200 patch 8.2.0850: MS-Windows: exepath() works different from cmd.exe Problem: MS-Windows: exepath() works different from cmd.exe. Solution: Make exepath() work better on MS-Windows. (closes https://github.com/vim/vim/issues/6115) diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -4034,7 +4034,7 @@ executable({expr}) *executable()* On MS-Windows the ".exe", ".bat", etc. can optionally be included. Then the extensions in $PATHEXT are tried. Thus if "foo.exe" does not exist, "foo.exe.bat" can be found. If - $PATHEXT is not set then ".exe;.com;.bat;.cmd" is used. A dot + $PATHEXT is not set then ".com;.exe;.bat;.cmd" is used. A dot by itself can be used in $PATHEXT to try using the name without an extension. When 'shell' looks like a Unix shell, then the name is also tried without adding an extension. diff --git a/src/os_win32.c b/src/os_win32.c --- a/src/os_win32.c +++ b/src/os_win32.c @@ -2080,57 +2080,200 @@ theend: #endif /* - * If "use_path" is TRUE: Return TRUE if "name" is in $PATH. - * If "use_path" is FALSE: Return TRUE if "name" exists. + * Return TRUE if "name" is an executable file, FALSE if not or it doesn't exist. * When returning TRUE and "path" is not NULL save the path and set "*path" to * the allocated memory. * TODO: Should somehow check if it's really executable. */ static int -executable_exists(char *name, char_u **path, int use_path) -{ - WCHAR *p; - WCHAR fnamew[_MAX_PATH]; - WCHAR *dumw; - WCHAR *wcurpath, *wnewpath; - long n; - - if (!use_path) - { - if (mch_getperm((char_u *)name) != -1 && !mch_isdir((char_u *)name)) - { - if (path != NULL) - { - if (mch_isFullName((char_u *)name)) - *path = vim_strsave((char_u *)name); - else - *path = FullName_save((char_u *)name, FALSE); - } - return TRUE; - } +executable_file(char *name, char_u **path) +{ + if (mch_getperm((char_u *)name) != -1 && !mch_isdir((char_u *)name)) + { + if (path != NULL) + *path = FullName_save((char_u *)name, FALSE); + return TRUE; + } + return FALSE; +} + +/* + * If "use_path" is TRUE: Return TRUE if "name" is in $PATH. + * If "use_path" is FALSE: Return TRUE if "name" exists. + * If "use_pathext" is TRUE search "name" with extensions in $PATHEXT. + * When returning TRUE and "path" is not NULL save the path and set "*path" to + * the allocated memory. + */ + static int +executable_exists(char *name, char_u **path, int use_path, int use_pathext) +{ + // WinNT and later can use _MAX_PATH wide characters for a pathname, which + // means that the maximum pathname is _MAX_PATH * 3 bytes when 'enc' is + // UTF-8. + char_u buf[_MAX_PATH * 3]; + size_t len = STRLEN(name); + size_t tmplen; + char_u *p, *e, *e2; + char_u *pathbuf = NULL; + char_u *pathext = NULL; + char_u *pathextbuf = NULL; + int noext = FALSE; + int retval = FALSE; + + if (len >= sizeof(buf)) // safety check return FALSE; - } - - p = enc_to_utf16((char_u *)name, NULL); - if (p == NULL) - return FALSE; - - wcurpath = _wgetenv(L"PATH"); - wnewpath = ALLOC_MULT(WCHAR, wcslen(wcurpath) + 3); - if (wnewpath == NULL) - return FALSE; - wcscpy(wnewpath, L".;"); - wcscat(wnewpath, wcurpath); - n = (long)SearchPathW(wnewpath, p, NULL, _MAX_PATH, fnamew, &dumw); - vim_free(wnewpath); - vim_free(p); - if (n == 0) - return FALSE; - if (GetFileAttributesW(fnamew) & FILE_ATTRIBUTE_DIRECTORY) - return FALSE; - if (path != NULL) - *path = utf16_to_enc(fnamew, NULL); - return TRUE; + + // Using the name directly when a Unix-shell like 'shell'. + if (strstr((char *)gettail(p_sh), "sh") != NULL) + noext = TRUE; + + if (use_pathext) + { + pathext = mch_getenv("PATHEXT"); + if (pathext == NULL) + pathext = (char_u *)".com;.exe;.bat;.cmd"; + + if (noext == FALSE) + { + /* + * Loop over all extensions in $PATHEXT. + * Check "name" ends with extension. + */ + p = pathext; + while (*p) + { + if (p[0] == ';' + || (p[0] == '.' && (p[1] == NUL || p[1] == ';'))) + { + // Skip empty or single ".". + ++p; + continue; + } + e = vim_strchr(p, ';'); + if (e == NULL) + e = p + STRLEN(p); + tmplen = e - p; + + if (_strnicoll(name + len - tmplen, (char *)p, tmplen) == 0) + { + noext = TRUE; + break; + } + + p = e; + } + } + } + + // Prepend single "." to pathext, it's means no extension added. + if (pathext == NULL) + pathext = (char_u *)"."; + else if (noext == TRUE) + { + if (pathextbuf == NULL) + pathextbuf = alloc(STRLEN(pathext) + 3); + if (pathextbuf == NULL) + { + retval = FALSE; + goto theend; + } + STRCPY(pathextbuf, ".;"); + STRCAT(pathextbuf, pathext); + pathext = pathextbuf; + } + + // Use $PATH when "use_path" is TRUE and "name" is basename. + if (use_path && gettail((char_u *)name) == (char_u *)name) + { + p = mch_getenv("PATH"); + if (p != NULL) + { + pathbuf = alloc(STRLEN(p) + 3); + if (pathbuf == NULL) + { + retval = FALSE; + goto theend; + } + STRCPY(pathbuf, ".;"); + STRCAT(pathbuf, p); + } + } + + /* + * Walk through all entries in $PATH to check if "name" exists there and + * is an executable file. + */ + p = (pathbuf != NULL) ? pathbuf : (char_u *)"."; + while (*p) + { + if (*p == ';') // Skip empty entry + { + ++p; + continue; + } + e = vim_strchr(p, ';'); + if (e == NULL) + e = p + STRLEN(p); + + if (e - p + len + 2 > sizeof(buf)) + { + retval = FALSE; + goto theend; + } + // A single "." that means current dir. + if (e - p == 1 && *p == '.') + STRCPY(buf, name); + else + { + vim_strncpy(buf, p, e - p); + add_pathsep(buf); + STRCAT(buf, name); + } + tmplen = STRLEN(buf); + + /* + * Loop over all extensions in $PATHEXT. + * Check "name" with extension added. + */ + p = pathext; + while (*p) + { + if (*p == ';') + { + // Skip empty entry + ++p; + continue; + } + e2 = vim_strchr(p, (int)';'); + if (e2 == NULL) + e2 = p + STRLEN(p); + + if (!(p[0] == '.' && (p[1] == NUL || p[1] == ';'))) + { + // Not a single "." that means no extension is added. + if (e2 - p + tmplen + 1 > sizeof(buf)) + { + retval = FALSE; + goto theend; + } + vim_strncpy(buf + tmplen, p, e2 - p); + } + if (executable_file((char *)buf, path)) + { + retval = TRUE; + goto theend; + } + + p = e2; + } + + p = e; + } + +theend: + free(pathextbuf); + free(pathbuf); + return retval; } #if (defined(__MINGW32__) && __MSVCRT_VERSION__ >= 0x800) || \ @@ -2210,7 +2353,7 @@ mch_init_g(void) vimrun_path = (char *)vim_strsave(vimrun_location); s_dont_use_vimrun = FALSE; } - else if (executable_exists("vimrun.exe", NULL, TRUE)) + else if (executable_exists("vimrun.exe", NULL, TRUE, FALSE)) s_dont_use_vimrun = FALSE; // Don't give the warning for a missing vimrun.exe right now, but only @@ -2224,7 +2367,7 @@ mch_init_g(void) * If "finstr.exe" doesn't exist, use "grep -n" for 'grepprg'. * Otherwise the default "findstr /n" is used. */ - if (!executable_exists("findstr.exe", NULL, TRUE)) + if (!executable_exists("findstr.exe", NULL, TRUE, FALSE)) set_option_value((char_u *)"grepprg", 0, (char_u *)"grep -n", 0); # ifdef FEAT_CLIPBOARD @@ -3306,69 +3449,7 @@ mch_writable(char_u *name) int mch_can_exe(char_u *name, char_u **path, int use_path) { - // WinNT and later can use _MAX_PATH wide characters for a pathname, which - // means that the maximum pathname is _MAX_PATH * 3 bytes when 'enc' is - // UTF-8. - char_u buf[_MAX_PATH * 3]; - int len = (int)STRLEN(name); - char_u *p, *saved; - - if (len >= sizeof(buf)) // safety check - return FALSE; - - // Try using the name directly when a Unix-shell like 'shell'. - if (strstr((char *)gettail(p_sh), "sh") != NULL) - if (executable_exists((char *)name, path, use_path)) - return TRUE; - - /* - * Loop over all extensions in $PATHEXT. - */ - p = mch_getenv("PATHEXT"); - if (p == NULL) - p = (char_u *)".com;.exe;.bat;.cmd"; - saved = vim_strsave(p); - if (saved == NULL) - return FALSE; - p = saved; - while (*p) - { - char_u *tmp = vim_strchr(p, ';'); - - if (tmp != NULL) - *tmp = NUL; - if (_stricoll((char *)name + len - STRLEN(p), (char *)p) == 0 - && executable_exists((char *)name, path, use_path)) - { - vim_free(saved); - return TRUE; - } - if (tmp == NULL) - break; - p = tmp + 1; - } - vim_free(saved); - - vim_strncpy(buf, name, sizeof(buf) - 1); - p = mch_getenv("PATHEXT"); - if (p == NULL) - p = (char_u *)".com;.exe;.bat;.cmd"; - while (*p) - { - if (p[0] == '.' && (p[1] == NUL || p[1] == ';')) - { - // A single "." means no extension is added. - buf[len] = NUL; - ++p; - if (*p) - ++p; - } - else - copy_option_part(&p, buf + len, sizeof(buf) - len, ";"); - if (executable_exists((char *)buf, path, use_path)) - return TRUE; - } - return FALSE; + return executable_exists((char *)name, path, TRUE, TRUE); } /* diff --git a/src/testdir/test_functions.vim b/src/testdir/test_functions.vim --- a/src/testdir/test_functions.vim +++ b/src/testdir/test_functions.vim @@ -1187,6 +1187,30 @@ func Test_Executable() call assert_equal(0, executable('notepad.exe.exe')) call assert_equal(0, executable('shell32.dll')) call assert_equal(0, executable('win.ini')) + + " get "notepad" path and remove the leading drive and sep. (ex. 'C:\') + let notepadcmd = exepath('notepad.exe') + let driveroot = notepadcmd[:2] + let notepadcmd = notepadcmd[3:] + new + " check that the relative path works in / + execute 'lcd' driveroot + call assert_equal(1, executable(notepadcmd)) + call assert_equal(driveroot .. notepadcmd, notepadcmd->exepath()) + bwipe + + " create "notepad.bat" + call mkdir('Xdir') + let notepadbat = fnamemodify('Xdir/notepad.bat', ':p') + call writefile([], notepadbat) + new + " check that the path and the pathext order is valid + lcd Xdir + let [pathext, $PATHEXT] = [$PATHEXT, '.com;.exe;.bat;.cmd'] + call assert_equal(notepadbat, exepath('notepad')) + let $PATHEXT = pathext + bwipe + eval 'Xdir'->delete('rf') elseif has('unix') call assert_equal(1, 'cat'->executable()) call assert_equal(0, executable('nodogshere')) diff --git a/src/version.c b/src/version.c --- 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 */ /**/ + 850, +/**/ 849, /**/ 848,