win: executable(): full path without extension

Absolute path is considered executable even *without* an extension.
This commit is contained in:
Justin M. Keyes
2017-02-03 16:56:31 +01:00
parent cd5b131575
commit 67fbbdb1b5
3 changed files with 89 additions and 73 deletions

View File

@@ -219,37 +219,42 @@ int os_exepath(char *buffer, size_t *size)
bool os_can_exe(const char_u *name, char_u **abspath, bool use_path) bool os_can_exe(const char_u *name, char_u **abspath, bool use_path)
FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_NONNULL_ARG(1)
{ {
// when use_path is false or if it's an absolute or relative path don't bool no_path = !use_path || path_is_absolute_path(name);
// need to use $PATH. #ifndef WIN32
if (!use_path || path_is_absolute_path(name) // If the filename is "qualified" (relative or absolute) do not check $PATH.
|| (name[0] == '.' no_path |= (name[0] == '.'
&& (name[1] == '/' || (name[1] == '.' && name[2] == '/')))) { && (name[1] == '/' || (name[1] == '.' && name[2] == '/')));
#endif
if (no_path) {
#ifdef WIN32 #ifdef WIN32
bool ok = is_executable(name); const char *pathext = os_getenv("PATHEXT");
if (!pathext) {
pathext = ".com;.exe;.bat;.cmd";
}
bool ok = is_executable((char *)name) || is_executable_ext((char *)name,
pathext);
#else #else
// Must have path separator, cannot execute files in the current directory. // Must have path separator, cannot execute files in the current directory.
bool ok = gettail_dir(name) != name && is_executable(name); bool ok = gettail_dir(name) != name && is_executable((char *)name);
#endif #endif
if (ok) { if (ok) {
if (abspath != NULL) { if (abspath != NULL) {
*abspath = save_absolute_path(name); *abspath = save_absolute_path(name);
} }
return true; return true;
} }
return false; return false;
} }
return is_executable_in_path(name, abspath); return is_executable_in_path(name, abspath);
} }
// Return true if "name" is an executable file, false if not or it doesn't /// Returns true if `name` is an executable file.
// exist. static bool is_executable(const char *name)
static bool is_executable(const char_u *name)
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_ALL
{ {
int32_t mode = os_getperm(name); int32_t mode = os_getperm((char_u *)name);
if (mode < 0) { if (mode < 0) {
return false; return false;
@@ -264,6 +269,37 @@ static bool is_executable(const char_u *name)
#endif #endif
} }
#ifdef WIN32
/// Appends file extensions from `pathext` to `name` and returns true if any
/// such combination is executable.
static bool is_executable_ext(char *name, const char *pathext)
FUNC_ATTR_NONNULL_ALL
{
xstrlcpy((char *)NameBuff, name, sizeof(NameBuff));
char *buf_end = xstrchrnul((char *)NameBuff, '\0');
for (const char *ext = pathext; *ext; ext++) {
// Skip the extension if there is no suffix after a '.'.
if (ext[0] == '.' && (ext[1] == '\0' || ext[1] == ENV_SEPCHAR)) {
ext++;
continue;
}
const char *ext_end = xstrchrnul(ext, ENV_SEPCHAR);
STRLCPY(buf_end, ext, ext_end - ext + 1);
if (is_executable((char *)NameBuff)) {
return true;
}
if (*ext_end != ENV_SEPCHAR) {
break;
}
ext = ext_end;
}
return false;
}
#endif
/// Checks if a file is inside the `$PATH` and is executable. /// Checks if a file is inside the `$PATH` and is executable.
/// ///
/// @param[in] name The name of the executable. /// @param[in] name The name of the executable.
@@ -294,11 +330,10 @@ static bool is_executable_in_path(const char_u *name, char_u **abspath)
if (!pathext) { if (!pathext) {
pathext = ".com;.exe;.bat;.cmd"; pathext = ".com;.exe;.bat;.cmd";
} }
buf_len += strlen(pathext); buf_len += strlen(pathext);
#endif #endif
char_u *buf = xmalloc(buf_len); char *buf = xmalloc(buf_len);
// Walk through all entries in $PATH to check if "name" exists there and // Walk through all entries in $PATH to check if "name" exists there and
// is an executable file. // is an executable file.
@@ -307,50 +342,24 @@ static bool is_executable_in_path(const char_u *name, char_u **abspath)
for (;; ) { for (;; ) {
char *e = xstrchrnul(p, ENV_SEPCHAR); char *e = xstrchrnul(p, ENV_SEPCHAR);
// Glue the directory from $PATH with `name` and save into buf. // Combine the $PATH segment with `name`.
STRLCPY(buf, p, e - p + 1); STRLCPY(buf, p, e - p + 1);
append_path((char *)buf, (char *)name, buf_len); append_path(buf, (char *)name, buf_len);
if (is_executable(buf)) { #ifdef WIN32
// Check if the caller asked for a copy of the path. bool ok = is_executable(buf) || is_executable_ext(buf, pathext);
if (abspath != NULL) { #else
*abspath = save_absolute_path(buf); bool ok = is_executable(buf);
#endif
if (ok) {
if (abspath != NULL) { // Caller asked for a copy of the path.
*abspath = save_absolute_path((char_u *)buf);
} }
rv = true; rv = true;
goto end; goto end;
} }
#ifdef WIN32
// Try appending file extensions from $PATHEXT to the name.
char *buf_end = xstrchrnul((char *)buf, '\0');
for (const char *ext = pathext; *ext; ext++) {
// Skip the extension if there is no suffix after a '.'.
if (ext[0] == '.' && (ext[1] == '\0' || ext[1] == ENV_SEPCHAR)) {
*ext++;
continue;
}
const char *ext_end = xstrchrnul(ext, ENV_SEPCHAR);
STRLCPY(buf_end, ext, ext_end - ext + 1);
if (is_executable(buf)) {
// Check if the caller asked for a copy of the path.
if (abspath != NULL) {
*abspath = save_absolute_path(buf);
}
rv = true;
goto end;
}
if (*ext_end != ENV_SEPCHAR) {
break;
}
ext = ext_end;
}
#endif
if (*e != ENV_SEPCHAR) { if (*e != ENV_SEPCHAR) {
// End of $PATH without finding any executable called name. // End of $PATH without finding any executable called name.
goto end; goto end;

View File

@@ -995,12 +995,10 @@ static void uniquefy_paths(garray_T *gap, char_u *pattern)
ga_remove_duplicate_strings(gap); ga_remove_duplicate_strings(gap);
} }
/* /// Return the end of the directory name, on the first path
* Return the end of the directory name, on the first path /// separator:
* separator: /// "/path/file", "/path/dir/", "/path//dir", "/file"
* "/path/file", "/path/dir/", "/path//dir", "/file" /// ^ ^ ^ ^
* ^ ^ ^ ^
*/
char_u *gettail_dir(const char_u *fname) char_u *gettail_dir(const char_u *fname)
{ {
const char_u *dir_end = fname; const char_u *dir_end = fname;
@@ -2131,17 +2129,12 @@ int append_path(char *path, const char *to_append, size_t max_len)
size_t current_length = strlen(path); size_t current_length = strlen(path);
size_t to_append_length = strlen(to_append); size_t to_append_length = strlen(to_append);
// Do not append empty strings. // Do not append empty string or a dot.
if (to_append_length == 0) { if (to_append_length == 0 || strcmp(to_append, ".") == 0) {
return OK; return OK;
} }
// Do not append a dot. // Combine the path segments, separated by a slash.
if (STRCMP(to_append, ".") == 0) {
return OK;
}
// Glue both paths with a slash.
if (current_length > 0 && !vim_ispathsep_nocolon(path[current_length-1])) { if (current_length > 0 && !vim_ispathsep_nocolon(path[current_length-1])) {
current_length += 1; // Count the trailing slash. current_length += 1; // Count the trailing slash.
@@ -2150,7 +2143,7 @@ int append_path(char *path, const char *to_append, size_t max_len)
return FAIL; return FAIL;
} }
STRCAT(path, PATHSEPSTR); xstrlcat(path, PATHSEPSTR, max_len);
} }
// +1 for the NUL at the end. // +1 for the NUL at the end.
@@ -2158,7 +2151,7 @@ int append_path(char *path, const char *to_append, size_t max_len)
return FAIL; return FAIL;
} }
STRCAT(path, to_append); xstrlcat(path, to_append, max_len);
return OK; return OK;
} }

View File

@@ -20,14 +20,8 @@ describe('executable()', function()
-- Windows: siblings are in Nvim's "pseudo-$PATH". -- Windows: siblings are in Nvim's "pseudo-$PATH".
local expected = iswin() and 1 or 0 local expected = iswin() and 1 or 0
if iswin() then if iswin() then
print('XXXXXXXXXXXXXXXXXXXXXXXXX')
print(helpers.eval('$PATH'))
print('XXXXXXXXXXXXXXXXXXXXXXXXX')
-- $PATH on AppVeyor CI might be oversized, redefine it to a minimal one. -- $PATH on AppVeyor CI might be oversized, redefine it to a minimal one.
clear({env={PATH=[[C:\Windows\system32;C:\Windows]]}}) clear({env={PATH=[[C:\Windows\system32;C:\Windows]]}})
print(helpers.eval('$PATH'))
print('XXXXXXXXXXXXXXXXXXXXXXXXX')
print(helpers.eval("echo glob(fnamemodify(exepath(v:progpath), ':h').'/*')"))
eq('arg1=lemon;arg2=sky;arg3=tree;', eq('arg1=lemon;arg2=sky;arg3=tree;',
call('system', sibling_exe..' lemon sky tree')) call('system', sibling_exe..' lemon sky tree'))
end end
@@ -103,6 +97,26 @@ describe('executable() (Windows)', function()
eq(0, call('executable', '.\\test_executable_zzz')) eq(0, call('executable', '.\\test_executable_zzz'))
end) end)
it('full path with extension', function()
-- Some executable we can expect in the test env.
local exe = 'printargs-test'
local exedir = helpers.eval("fnamemodify(v:progpath, ':h')")
local exepath = exedir..'/'..exe..'.exe'
eq(1, call('executable', exepath))
eq('arg1=lemon;arg2=sky;arg3=tree;',
call('system', exepath..' lemon sky tree'))
end)
it('full path without extension', function()
-- Some executable we can expect in the test env.
local exe = 'printargs-test'
local exedir = helpers.eval("fnamemodify(v:progpath, ':h')")
local exepath = exedir..'/'..exe
eq('arg1=lemon;arg2=sky;arg3=tree;',
call('system', exepath..' lemon sky tree'))
eq(1, call('executable', exepath))
end)
it('respects $PATHEXT when trying extensions on a filename', function() it('respects $PATHEXT when trying extensions on a filename', function()
clear({env={PATHEXT='.zzz'}}) clear({env={PATHEXT='.zzz'}})
for _,ext in ipairs(exts) do for _,ext in ipairs(exts) do