Eliminate the wildcard_expand_internal function

This commit is contained in:
ridiculousfish 2015-08-03 17:17:15 -07:00
parent 602e9cebd9
commit b68410d619

View file

@ -265,7 +265,7 @@ static bool wildcard_complete_internal(const wchar_t *str,
/* Locate the next wildcard character position, e.g. ANY_CHAR or ANY_STRING */
size_t next_wc_char_pos = wildcard_find(wc);
/* Maybe we have no more wildcards at all. */
/* Maybe we have no more wildcards at all. This includes the empty string. */
if (next_wc_char_pos == wcstring::npos)
{
string_fuzzy_match_t match = string_fuzzy_match_string(wc, str);
@ -399,25 +399,17 @@ bool wildcard_match(const wcstring &str, const wcstring &wc, bool leading_dots_f
}
/**
Creates a path from the specified directory and filename.
*/
static wcstring make_path(const wcstring &base_dir, const wcstring &name)
{
return base_dir + name;
}
/**
Obtain a description string for the file specified by the filename.
The returned value is a string constant and should not be free'd.
\param filename The file for which to find a description string
\param lstat_res The result of calling lstat on the file
\param lbuf The struct buf output of calling lstat on the file
\param stat_res The result of calling stat on the file
\param buf The struct buf output of calling stat on the file
\param err The errno value after a failed stat call on the file.
*/
Obtain a description string for the file specified by the filename.
The returned value is a string constant and should not be free'd.
\param filename The file for which to find a description string
\param lstat_res The result of calling lstat on the file
\param lbuf The struct buf output of calling lstat on the file
\param stat_res The result of calling stat on the file
\param buf The struct buf output of calling stat on the file
\param err The errno value after a failed stat call on the file.
*/
static wcstring file_get_desc(const wcstring &filename,
int lstat_res,
@ -426,7 +418,7 @@ static wcstring file_get_desc(const wcstring &filename,
struct stat buf,
int err)
{
if (!lstat_res)
{
if (S_ISLNK(lbuf.st_mode))
@ -439,29 +431,27 @@ static wcstring file_get_desc(const wcstring &filename,
}
else
{
if ((buf.st_mode & S_IXUSR) ||
(buf.st_mode & S_IXGRP) ||
(buf.st_mode & S_IXOTH))
if (buf.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))
{
if (waccess(filename, X_OK) == 0)
{
/*
Weird group permissions and other such
issues make it non-trivial to find out
if we can actually execute a file using
the result from stat. It is much safer
to use the access function, since it
tells us exactly what we want to know.
*/
Weird group permissions and other such
issues make it non-trivial to find out
if we can actually execute a file using
the result from stat. It is much safer
to use the access function, since it
tells us exactly what we want to know.
*/
return COMPLETE_EXEC_LINK_DESC;
}
}
}
return COMPLETE_SYMLINK_DESC;
}
else
{
@ -471,18 +461,18 @@ static wcstring file_get_desc(const wcstring &filename,
{
return COMPLETE_ROTTEN_SYMLINK_DESC;
}
case ELOOP:
{
return COMPLETE_LOOP_SYMLINK_DESC;
}
}
/*
On unknown errors we do nothing. The file will be
given the default 'File' description or one based on the suffix.
*/
On unknown errors we do nothing. The file will be
given the default 'File' description or one based on the suffix.
*/
}
}
else if (S_ISCHR(buf.st_mode))
{
@ -506,182 +496,121 @@ static wcstring file_get_desc(const wcstring &filename,
}
else
{
if ((buf.st_mode & S_IXUSR) ||
(buf.st_mode & S_IXGRP) ||
(buf.st_mode & S_IXOTH))
if (buf.st_mode & (S_IXUSR | S_IXGRP | S_IXGRP))
{
if (waccess(filename, X_OK) == 0)
{
/*
Weird group permissions and other such issues
make it non-trivial to find out if we can
actually execute a file using the result from
stat. It is much safer to use the access
function, since it tells us exactly what we want
to know.
*/
Weird group permissions and other such issues
make it non-trivial to find out if we can
actually execute a file using the result from
stat. It is much safer to use the access
function, since it tells us exactly what we want
to know.
*/
return COMPLETE_EXEC_DESC;
}
}
}
}
return COMPLETE_FILE_DESC ;
}
/**
Add the specified filename if it matches the specified wildcard.
If the filename matches, first get the description of the specified
filename. If this is a regular file, append the filesize to the
description.
\param list the list to add he completion to
\param fullname the full filename of the file
\param completion the completion part of the file name
\param wc the wildcard to match against
\param is_cmd whether we are performing command completion
*/
static void wildcard_completion_allocate(std::vector<completion_t> *list,
const wcstring &fullname,
const wcstring &completion,
const wchar_t *wc,
expand_flags_t expand_flags)
{
assert(list != NULL);
struct stat buf, lbuf;
wcstring sb;
wcstring munged_completion;
int flags = 0;
int stat_res, lstat_res;
int stat_errno=0;
long long sz;
/*
If the file is a symlink, we need to stat both the file itself
_and_ the destination file. But we try to avoid this with
non-symlinks by first doing an lstat, and if the file is not a
link we copy the results over to the regular stat buffer.
*/
if ((lstat_res = lwstat(fullname, &lbuf)))
/** Test if the given file is an executable (if EXECUTABLES_ONLY) or directory (if DIRECTORIES_ONLY).
If it matches, call wildcard_complete() with some description that we make up.
Note that the filename came from a readdir() call, so we know it exists.
*/
static bool wildcard_test_flags_then_complete(const wcstring &filepath,
const wcstring &filename,
const wchar_t *wc,
expand_flags_t expand_flags,
std::vector<completion_t> *out)
{
struct stat lstat_buf = {}, stat_buf = {};
int stat_res = -1;
int stat_errno = 0;
int lstat_res = lwstat(filepath, &lstat_buf);
if (lstat_res < 0)
{
/* lstat failed! */
sz=-1;
stat_res = lstat_res;
/* lstat failed */
}
else
{
if (S_ISLNK(lbuf.st_mode))
if (S_ISLNK(lstat_buf.st_mode))
{
if ((stat_res = wstat(fullname, &buf)))
stat_res = wstat(filepath, &stat_buf);
if (stat_res < 0)
{
sz=-1;
/*
In order to differentiate between e.g. rotten symlinks
and symlink loops, we also need to know the error status of wstat.
*/
stat_errno = errno;
}
else
{
sz = (long long)buf.st_size;
}
/*
In order to differentiate between e.g. rotten symlinks
and symlink loops, we also need to know the error status of wstat.
*/
stat_errno = errno;
}
else
{
stat_buf = lstat_buf;
stat_res = lstat_res;
memcpy(&buf, &lbuf, sizeof(struct stat));
sz = (long long)buf.st_size;
}
}
const long long file_size = stat_res == 0 ? stat_buf.st_size : 0;
const bool is_directory = stat_res == 0 && S_ISDIR(stat_buf.st_mode);
const bool is_executable = stat_res == 0 && S_ISREG(stat_buf.st_mode);
if (expand_flags & DIRECTORIES_ONLY)
{
if (!is_directory)
{
return false;
}
}
if (expand_flags & EXECUTABLES_ONLY)
{
if (!is_executable || waccess(filepath, X_OK) != 0)
{
return false;
}
}
/* Compute the description */
bool wants_desc = !(expand_flags & EXPAND_NO_DESCRIPTIONS);
wcstring desc;
if (wants_desc)
desc = file_get_desc(fullname, lstat_res, lbuf, stat_res, buf, stat_errno);
if (sz >= 0 && S_ISDIR(buf.st_mode))
{
flags |= COMPLETE_NO_SPACE;
munged_completion = completion;
munged_completion.push_back(L'/');
if (wants_desc)
sb.append(desc);
desc = file_get_desc(filepath, lstat_res, lstat_buf, stat_res, stat_buf, stat_errno);
if (file_size >= 0)
{
if (!desc.empty())
desc.append(L", ");
desc.append(format_size(file_size));
}
}
/* Append a / if this is a directory */
if (is_directory)
{
return wildcard_complete(filename + L'/', wc, desc.c_str(), NULL, out, expand_flags, COMPLETE_NO_SPACE);
}
else
{
if (wants_desc)
{
if (! desc.empty())
{
sb.append(desc);
sb.append(L", ");
}
sb.append(format_size(sz));
}
return wildcard_complete(filename, wc, desc.c_str(), NULL, out, expand_flags, 0);
}
const wcstring &completion_to_use = munged_completion.empty() ? completion : munged_completion;
wildcard_complete(completion_to_use, wc, sb.c_str(), NULL, list, expand_flags, flags);
}
/**
Test if the file specified by the given filename matches the
expansion flags specified. flags can be a combination of
EXECUTABLES_ONLY and DIRECTORIES_ONLY.
*/
static bool test_flags(const wcstring &filename, expand_flags_t flags)
{
if (flags & DIRECTORIES_ONLY)
{
struct stat buf;
if (wstat(filename, &buf) == -1)
{
return false;
}
if (!S_ISDIR(buf.st_mode))
{
return false;
}
}
if (flags & EXECUTABLES_ONLY)
{
if (waccess(filename, X_OK) != 0)
return false;
struct stat buf;
if (wstat(filename, &buf) == -1)
{
return false;
}
if (!S_ISREG(buf.st_mode))
{
return false;
}
}
return true;
}
/** Appends a completion to the completion list, if the string is missing from the set. */
static void insert_completion_if_missing(const wcstring &str, std::vector<completion_t> *out, std::set<wcstring> *completion_set)
{
if (completion_set->insert(str).second)
append_completion(out, str);
}
class wildcard_expander_t
{
/* The original string we are expanding */
const wcstring original_base;
/* Original wildcard we are expanding. */
const wchar_t * const original_wildcard;
/* the set of items we have resolved, used to avoid duplication */
std::set<wcstring> completion_set;
@ -735,7 +664,13 @@ class wildcard_expander_t
public:
wildcard_expander_t(expand_flags_t f, std::vector<completion_t> *r) : flags(f), resolved(r), did_interrupt(false), did_add(false)
wildcard_expander_t(const wcstring &orig_base, const wchar_t *orig_wc, expand_flags_t f, std::vector<completion_t> *r) :
original_base(orig_base),
original_wildcard(orig_wc),
flags(f),
resolved(r),
did_interrupt(false),
did_add(false)
{
assert(resolved != NULL);
@ -789,10 +724,9 @@ void wildcard_expander_t::expand_trailing_slash(const wcstring &base_dir)
{
if (! next.empty() && next.at(0) != L'.')
{
const wcstring abs_path = base_dir + next;
if (test_flags(abs_path, this->flags))
const wcstring filepath = base_dir + next;
if (wildcard_test_flags_then_complete(filepath, next, L"", flags, this->resolved))
{
wildcard_completion_allocate(this->resolved, abs_path, next, L"", flags);
this->did_add = true;
}
}
@ -846,10 +780,9 @@ void wildcard_expander_t::expand_last_segment(const wcstring &base_dir, DIR *bas
std::vector<completion_t> local_matches;
if (wildcard_complete(name_str, wc.c_str(), L"", NULL, &local_matches, flags & EXPAND_FUZZY_MATCH, 0))
{
const wcstring abs_path = base_dir + name_str;
if (test_flags(abs_path, flags))
const wcstring filepath = base_dir + name_str;
if (wildcard_test_flags_then_complete(filepath, name_str, wc.c_str(), flags, this->resolved))
{
wildcard_completion_allocate(this->resolved, abs_path, name_str, wc.c_str(), flags);
this->did_add = true;
}
}
@ -965,325 +898,6 @@ void wildcard_expander_t::expand(const wcstring &base_dir, const wchar_t *wc)
}
}
/**
The real implementation of wildcard expansion is in this
function. Other functions are just wrappers around this one.
This function traverses the relevant directory tree looking for
matches, and recurses when needed to handle wildcrards spanning
multiple components and recursive wildcards.
Because this function calls itself recursively with substrings,
it's important that the parameters be raw pointers instead of wcstring,
which would be too expensive to construct for all substrings.
Args:
wc: the wildcard string itself, e.g. foo* (where * is acutally ANY_CHAR)
base_dir: the "working directory" against which the wildcard is to be resolved
flags: flags controlling expansion
out: resolved items get inserted into here
completion_set: the set of items we have resolved, used to avoid duplication
visited_files: the set of file IDs we have visited, used to avoid symlink loops
Returns:
1 if matches were found, 0 if not, -1 on abort (control-C)
*/
static int wildcard_expand_internal(const wchar_t *wc,
const wchar_t * const base_dir,
expand_flags_t flags,
std::vector<completion_t> *out,
std::set<wcstring> &completion_set,
std::set<file_id_t> &visited_files)
{
/* Variables for traversing a directory */
DIR *dir;
/* The result returned */
int res = 0;
// debug( 3, L"WILDCARD_EXPAND %ls in %ls", wc, base_dir );
if (is_main_thread() ? reader_interrupted() : reader_thread_job_is_stale())
{
return -1;
}
if (!wc || !base_dir)
{
debug(2, L"Got null string on line %d of file %s", __LINE__, __FILE__);
return 0;
}
const size_t base_dir_len = wcslen(base_dir);
const size_t wc_len = wcslen(wc);
if (flags & EXPAND_FOR_COMPLETIONS)
{
/*
Avoid excessive number of returned matches for wc ending with a *
*/
if (wc_len > 0 && (wc[wc_len-1]==ANY_STRING))
{
wchar_t * foo = wcsdup(wc);
foo[wc_len-1]=0;
int res = wildcard_expand_internal(foo, base_dir, flags, out, completion_set, visited_files);
free(foo);
return res;
}
}
/* Determine if we are the last segment */
const wchar_t * const next_slash = wcschr(wc,L'/');
const bool is_last_segment = (next_slash == NULL);
const size_t wc_segment_len = next_slash ? next_slash - wc : wc_len;
const wcstring wc_segment = wcstring(wc, wc_segment_len);
/* Maybe this segment has no wildcards at all. If this is not the last segment, and it has no wildcards, then we don't need to match against the directory contents, and in fact we don't want to match since we may not be able to read it anyways (#2099). Don't even open the directory! */
const bool segment_has_wildcards = wildcard_has(wc_segment, true /* internal, i.e. look for ANY_CHAR instead of ? */);
if (! segment_has_wildcards && ! is_last_segment)
{
wcstring new_base_dir = make_path(base_dir, wc_segment);
new_base_dir.push_back(L'/');
/* Skip multiple separators */
assert(next_slash != NULL);
const wchar_t *new_wc = next_slash;
while (*new_wc==L'/')
{
new_wc++;
}
/* Early out! */
return wildcard_expand_internal(new_wc, new_base_dir.c_str(), flags, out, completion_set, visited_files);
}
/* Test for recursive match string in current segment */
const bool is_recursive = (wc_segment.find(ANY_STRING_RECURSIVE) != wcstring::npos);
const wchar_t *base_dir_or_cwd = (base_dir[0] == L'\0') ? L"." : base_dir;
if (!(dir = wopendir(base_dir_or_cwd)))
{
return 0;
}
/*
Is this segment of the wildcard the last?
*/
if (is_last_segment)
{
/*
Wildcard segment is the last segment,
Insert all matching files/directories
*/
if (wc[0]=='\0')
{
/*
The last wildcard segment is empty. Insert everything if
completing, the directory itself otherwise.
*/
if (flags & EXPAND_FOR_COMPLETIONS)
{
wcstring next;
while (wreaddir(dir, next))
{
if (next[0] != L'.')
{
wcstring long_name = make_path(base_dir, next);
if (test_flags(long_name.c_str(), flags))
{
wildcard_completion_allocate(out, long_name, next, L"", flags);
}
}
}
}
else
{
res = 1;
insert_completion_if_missing(base_dir, out, &completion_set);
}
}
else
{
/* This is the last wildcard segment, and it is not empty. Match files/directories. */
wcstring name_str;
while (wreaddir(dir, name_str))
{
if (flags & EXPAND_FOR_COMPLETIONS)
{
const wcstring long_name = make_path(base_dir, name_str);
/* Test for matches before stating file, so as to minimize the number of calls to the much slower stat function. The only expand flag we care about is EXPAND_FUZZY_MATCH; we have no complete flags. */
std::vector<completion_t> test;
if (wildcard_complete(name_str, wc, L"", NULL, &test, flags & EXPAND_FUZZY_MATCH, 0))
{
if (test_flags(long_name.c_str(), flags))
{
wildcard_completion_allocate(out, long_name, name_str, wc, flags);
}
}
}
else
{
if (wildcard_match(name_str, wc, true /* skip files with leading dots */))
{
const wcstring long_name = make_path(base_dir, name_str);
int skip = 0;
if (is_recursive)
{
/*
In recursive mode, we are only
interested in adding files -directories
will be added in the next pass.
*/
struct stat buf;
if (!wstat(long_name, &buf))
{
skip = S_ISDIR(buf.st_mode);
}
}
if (! skip)
{
insert_completion_if_missing(long_name, out, &completion_set);
}
res = 1;
}
}
}
}
}
if ((! is_last_segment) || is_recursive)
{
/*
Wilcard segment is not the last segment. Recursively call
wildcard_expand for all matching subdirectories.
*/
/*
In recursive mode, we look through the directory twice. If
so, this rewind is needed.
*/
rewinddir(dir);
/* new_dir is a scratch area containing the full path to a file/directory we are iterating over */
wcstring new_dir = base_dir;
wcstring name_str;
while (wreaddir(dir, name_str))
{
/*
Test if the file/directory name matches the whole
wildcard element, i.e. regular matching.
*/
bool whole_match = wildcard_match(name_str, wc_segment, true /* ignore leading dots */);
bool partial_match = false;
/*
If we are doing recursive matching, also check if this
directory matches the part up to the recusrive
wildcard, if so, then we can search all subdirectories
for matches.
*/
if (is_recursive)
{
const wchar_t *end = wcschr(wc, ANY_STRING_RECURSIVE);
wchar_t *wc_sub = wcsndup(wc, end-wc+1);
partial_match = wildcard_match(name_str, wc_sub, true /* ignore leading dots */);
free(wc_sub);
}
if (whole_match || partial_match)
{
struct stat buf;
int new_res;
// new_dir is base_dir + some other path components
// Replace everything after base_dir with the new path component
new_dir.replace(base_dir_len, wcstring::npos, name_str);
int stat_res = wstat(new_dir, &buf);
if (!stat_res)
{
// Insert a "file ID" into visited_files
// If the insertion fails, we've already visited this file (i.e. a symlink loop)
// If we're not recursive, insert anyways (in case we loop back around in a future recursive segment), but continue on; the idea being that literal path components should still work
const file_id_t file_id = file_id_t::file_id_from_stat(&buf);
if (S_ISDIR(buf.st_mode) && (visited_files.insert(file_id).second || ! is_recursive))
{
new_dir.push_back(L'/');
/*
Regular matching
*/
if (whole_match)
{
const wchar_t *new_wc = L"";
if (next_slash)
{
new_wc=next_slash+1;
/*
Accept multiple '/' as a single directory separator
*/
while (*new_wc==L'/')
{
new_wc++;
}
}
new_res = wildcard_expand_internal(new_wc,
new_dir.c_str(),
flags,
out,
completion_set,
visited_files);
if (new_res == -1)
{
res = -1;
break;
}
res |= new_res;
}
/*
Recursive matching
*/
if (partial_match)
{
new_res = wildcard_expand_internal(wcschr(wc, ANY_STRING_RECURSIVE),
new_dir.c_str(),
flags,
out,
completion_set,
visited_files);
if (new_res == -1)
{
res = -1;
break;
}
res |= new_res;
}
}
}
}
}
}
closedir(dir);
return res;
}
static int wildcard_expand(const wchar_t *wc,
const wcstring &base_dir,
@ -1293,7 +907,7 @@ static int wildcard_expand(const wchar_t *wc,
assert(out != NULL);
size_t c = out->size();
wildcard_expander_t expander(flags, out);
wildcard_expander_t expander(base_dir, wc, flags, out);
expander.expand(base_dir, wc);
if (flags & EXPAND_FOR_COMPLETIONS)