diff options
Diffstat (limited to 'findcmd.c')
-rw-r--r-- | findcmd.c | 696 |
1 files changed, 696 insertions, 0 deletions
diff --git a/findcmd.c b/findcmd.c new file mode 100644 index 0000000..95f231e --- /dev/null +++ b/findcmd.c @@ -0,0 +1,696 @@ +/* findcmd.c -- Functions to search for commands by name. */ + +/* Copyright (C) 1997-2022 Free Software Foundation, Inc. + + This file is part of GNU Bash, the Bourne Again SHell. + + Bash is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bash is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bash. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "config.h" + +#include <stdio.h> +#include "chartypes.h" +#include "bashtypes.h" +#if !defined (_MINIX) && defined (HAVE_SYS_FILE_H) +# include <sys/file.h> +#endif +#include "filecntl.h" +#include "posixstat.h" + +#if defined (HAVE_UNISTD_H) +# include <unistd.h> +#endif +#include <errno.h> + +#include "bashansi.h" + +#include "memalloc.h" +#include "shell.h" +#include "execute_cmd.h" +#include "flags.h" +#include "hashlib.h" +#include "pathexp.h" +#include "hashcmd.h" +#include "findcmd.h" /* matching prototypes and declarations */ + +#include <glob/strmatch.h> + +#if !defined (errno) +extern int errno; +#endif + +/* Static functions defined and used in this file. */ +static char *_find_user_command_internal PARAMS((const char *, int)); +static char *find_user_command_internal PARAMS((const char *, int)); +static char *find_user_command_in_path PARAMS((const char *, char *, int, int *)); +static char *find_in_path_element PARAMS((const char *, char *, int, int, struct stat *, int *)); +static char *find_absolute_program PARAMS((const char *, int)); + +static char *get_next_path_element PARAMS((char *, int *)); + +/* The file name which we would try to execute, except that it isn't + possible to execute it. This is the first file that matches the + name that we are looking for while we are searching $PATH for a + suitable one to execute. If we cannot find a suitable executable + file, then we use this one. */ +static char *file_to_lose_on; + +/* Non-zero if we should stat every command found in the hash table to + make sure it still exists. */ +int check_hashed_filenames = CHECKHASH_DEFAULT; + +/* DOT_FOUND_IN_SEARCH becomes non-zero when find_user_command () + encounters a `.' as the directory pathname while scanning the + list of possible pathnames; i.e., if `.' comes before the directory + containing the file of interest. */ +int dot_found_in_search = 0; + +/* Set up EXECIGNORE; a blacklist of patterns that executable files should not + match. */ +static struct ignorevar execignore = +{ + "EXECIGNORE", + NULL, + 0, + NULL, + NULL +}; + +void +setup_exec_ignore (varname) + char *varname; +{ + setup_ignore_patterns (&execignore); +} + +static int +exec_name_should_ignore (name) + const char *name; +{ + struct ign *p; + + for (p = execignore.ignores; p && p->val; p++) + if (strmatch (p->val, (char *)name, FNMATCH_EXTFLAG|FNM_CASEFOLD) != FNM_NOMATCH) + return 1; + return 0; +} + +/* Return some flags based on information about this file. + The EXISTS bit is non-zero if the file is found. + The EXECABLE bit is non-zero the file is executable. + Zero is returned if the file is not found. */ +int +file_status (name) + const char *name; +{ + struct stat finfo; + int r; + + /* Determine whether this file exists or not. */ + if (stat (name, &finfo) < 0) + return (0); + + /* If the file is a directory, then it is not "executable" in the + sense of the shell. */ + if (S_ISDIR (finfo.st_mode)) + return (FS_EXISTS|FS_DIRECTORY); + + r = FS_EXISTS; + +#if defined (HAVE_EACCESS) + /* Use eaccess(2) if we have it to take things like ACLs and other + file access mechanisms into account. eaccess uses the effective + user and group IDs, not the real ones. We could use sh_eaccess, + but we don't want any special treatment for /dev/fd. */ + if (exec_name_should_ignore (name) == 0 && eaccess (name, X_OK) == 0) + r |= FS_EXECABLE; + if (eaccess (name, R_OK) == 0) + r |= FS_READABLE; + + return r; +#elif defined (AFS) + /* We have to use access(2) to determine access because AFS does not + support Unix file system semantics. This may produce wrong + answers for non-AFS files when ruid != euid. I hate AFS. */ + if (exec_name_should_ignore (name) == 0 && access (name, X_OK) == 0) + r |= FS_EXECABLE; + if (access (name, R_OK) == 0) + r |= FS_READABLE; + + return r; +#else /* !HAVE_EACCESS && !AFS */ + + /* Find out if the file is actually executable. By definition, the + only other criteria is that the file has an execute bit set that + we can use. The same with whether or not a file is readable. */ + + /* Root only requires execute permission for any of owner, group or + others to be able to exec a file, and can read any file. */ + if (current_user.euid == (uid_t)0) + { + r |= FS_READABLE; + if (exec_name_should_ignore (name) == 0 && (finfo.st_mode & S_IXUGO)) + r |= FS_EXECABLE; + return r; + } + + /* If we are the owner of the file, the owner bits apply. */ + if (current_user.euid == finfo.st_uid) + { + if (exec_name_should_ignore (name) == 0 && (finfo.st_mode & S_IXUSR)) + r |= FS_EXECABLE; + if (finfo.st_mode & S_IRUSR) + r |= FS_READABLE; + } + + /* If we are in the owning group, the group permissions apply. */ + else if (group_member (finfo.st_gid)) + { + if (exec_name_should_ignore (name) == 0 && (finfo.st_mode & S_IXGRP)) + r |= FS_EXECABLE; + if (finfo.st_mode & S_IRGRP) + r |= FS_READABLE; + } + + /* Else we check whether `others' have permission to execute the file */ + else + { + if (exec_name_should_ignore (name) == 0 && finfo.st_mode & S_IXOTH) + r |= FS_EXECABLE; + if (finfo.st_mode & S_IROTH) + r |= FS_READABLE; + } + + return r; +#endif /* !AFS */ +} + +/* Return non-zero if FILE exists and is executable. + Note that this function is the definition of what an + executable file is; do not change this unless YOU know + what an executable file is. */ +int +executable_file (file) + const char *file; +{ + int s; + + s = file_status (file); +#if defined (EISDIR) + if (s & FS_DIRECTORY) + errno = EISDIR; /* let's see if we can improve error messages */ +#endif + return ((s & FS_EXECABLE) && ((s & FS_DIRECTORY) == 0)); +} + +int +is_directory (file) + const char *file; +{ + return (file_status (file) & FS_DIRECTORY); +} + +int +executable_or_directory (file) + const char *file; +{ + int s; + + s = file_status (file); + return ((s & FS_EXECABLE) || (s & FS_DIRECTORY)); +} + +/* Locate the executable file referenced by NAME, searching along + the contents of the shell PATH variable. Return a new string + which is the full pathname to the file, or NULL if the file + couldn't be found. If a file is found that isn't executable, + and that is the only match, then return that. */ +char * +find_user_command (name) + const char *name; +{ + return (find_user_command_internal (name, FS_EXEC_PREFERRED|FS_NODIRS)); +} + +/* Locate the file referenced by NAME, searching along the contents + of the shell PATH variable. Return a new string which is the full + pathname to the file, or NULL if the file couldn't be found. This + returns the first readable file found; designed to be used to look + for shell scripts or files to source. */ +char * +find_path_file (name) + const char *name; +{ + return (find_user_command_internal (name, FS_READABLE)); +} + +static char * +_find_user_command_internal (name, flags) + const char *name; + int flags; +{ + char *path_list, *cmd; + SHELL_VAR *var; + + /* Search for the value of PATH in both the temporary environments and + in the regular list of variables. */ + if (var = find_variable_tempenv ("PATH")) /* XXX could be array? */ + path_list = value_cell (var); + else + path_list = (char *)NULL; + + if (path_list == 0 || *path_list == '\0') + return (savestring (name)); + + cmd = find_user_command_in_path (name, path_list, flags, (int *)0); + + return (cmd); +} + +static char * +find_user_command_internal (name, flags) + const char *name; + int flags; +{ +#ifdef __WIN32__ + char *res, *dotexe; + + dotexe = (char *)xmalloc (strlen (name) + 5); + strcpy (dotexe, name); + strcat (dotexe, ".exe"); + res = _find_user_command_internal (dotexe, flags); + free (dotexe); + if (res == 0) + res = _find_user_command_internal (name, flags); + return res; +#else + return (_find_user_command_internal (name, flags)); +#endif +} + +/* Return the next element from PATH_LIST, a colon separated list of + paths. PATH_INDEX_POINTER is the address of an index into PATH_LIST; + the index is modified by this function. + Return the next element of PATH_LIST or NULL if there are no more. */ +static char * +get_next_path_element (path_list, path_index_pointer) + char *path_list; + int *path_index_pointer; +{ + char *path; + + path = extract_colon_unit (path_list, path_index_pointer); + + if (path == 0) + return (path); + + if (*path == '\0') + { + free (path); + path = savestring ("."); + } + + return (path); +} + +/* Look for PATHNAME in $PATH. Returns either the hashed command + corresponding to PATHNAME or the first instance of PATHNAME found + in $PATH. If (FLAGS&CMDSRCH_HASH) is non-zero, insert the instance of + PATHNAME found in $PATH into the command hash table. + If (FLAGS&CMDSRCH_STDPATH) is non-zero, we are running in a `command -p' + environment and should use the Posix standard path. + Returns a newly-allocated string. */ +char * +search_for_command (pathname, flags) + const char *pathname; + int flags; +{ + char *hashed_file, *command, *path_list; + int temp_path, st; + SHELL_VAR *path; + + hashed_file = command = (char *)NULL; + + /* If PATH is in the temporary environment for this command, don't use the + hash table to search for the full pathname. */ + path = find_variable_tempenv ("PATH"); + temp_path = path && tempvar_p (path); + + /* Don't waste time trying to find hashed data for a pathname + that is already completely specified or if we're using a command- + specific value for PATH. */ + if (temp_path == 0 && (flags & CMDSRCH_STDPATH) == 0 && absolute_program (pathname) == 0) + hashed_file = phash_search (pathname); + + /* If a command found in the hash table no longer exists, we need to + look for it in $PATH. Thank you Posix.2. This forces us to stat + every command found in the hash table. */ + + if (hashed_file && (posixly_correct || check_hashed_filenames)) + { + st = file_status (hashed_file); + if ((st & (FS_EXISTS|FS_EXECABLE)) != (FS_EXISTS|FS_EXECABLE)) + { + phash_remove (pathname); + free (hashed_file); + hashed_file = (char *)NULL; + } + } + + if (hashed_file) + command = hashed_file; + else if (absolute_program (pathname)) + /* A command containing a slash is not looked up in PATH or saved in + the hash table. */ + command = savestring (pathname); + else + { + if (flags & CMDSRCH_STDPATH) + path_list = conf_standard_path (); + else if (temp_path || path) + path_list = value_cell (path); + else + path_list = 0; + + command = find_user_command_in_path (pathname, path_list, FS_EXEC_PREFERRED|FS_NODIRS, &st); + + if (command && hashing_enabled && temp_path == 0 && (flags & CMDSRCH_HASH)) + { + /* If we found the full pathname the same as the command name, the + command probably doesn't exist. Don't put it into the hash + table unless it's an executable file in the current directory. */ + if (STREQ (command, pathname)) + { + if (st & FS_EXECABLE) + phash_insert ((char *)pathname, command, dot_found_in_search, 1); + } + /* If we're in posix mode, don't add files without the execute bit + to the hash table. */ + else if (posixly_correct || check_hashed_filenames) + { + if (st & FS_EXECABLE) + phash_insert ((char *)pathname, command, dot_found_in_search, 1); + } + else + phash_insert ((char *)pathname, command, dot_found_in_search, 1); + } + + if (flags & CMDSRCH_STDPATH) + free (path_list); + } + + return (command); +} + +char * +user_command_matches (name, flags, state) + const char *name; + int flags, state; +{ + register int i; + int path_index, name_len; + char *path_list, *path_element, *match; + struct stat dotinfo; + static char **match_list = NULL; + static int match_list_size = 0; + static int match_index = 0; + + if (state == 0) + { + /* Create the list of matches. */ + if (match_list == 0) + { + match_list_size = 5; + match_list = strvec_create (match_list_size); + } + + /* Clear out the old match list. */ + for (i = 0; i < match_list_size; i++) + match_list[i] = 0; + + /* We haven't found any files yet. */ + match_index = 0; + + if (absolute_program (name)) + { + match_list[0] = find_absolute_program (name, flags); + match_list[1] = (char *)NULL; + path_list = (char *)NULL; + } + else + { + name_len = strlen (name); + file_to_lose_on = (char *)NULL; + dot_found_in_search = 0; + if (stat (".", &dotinfo) < 0) + dotinfo.st_dev = dotinfo.st_ino = 0; /* so same_file won't match */ + path_list = get_string_value ("PATH"); + path_index = 0; + } + + while (path_list && path_list[path_index]) + { + path_element = get_next_path_element (path_list, &path_index); + + if (path_element == 0) + break; + + match = find_in_path_element (name, path_element, flags, name_len, &dotinfo, (int *)0); + free (path_element); + + if (match == 0) + continue; + + if (match_index + 1 == match_list_size) + { + match_list_size += 10; + match_list = strvec_resize (match_list, (match_list_size + 1)); + } + + match_list[match_index++] = match; + match_list[match_index] = (char *)NULL; + FREE (file_to_lose_on); + file_to_lose_on = (char *)NULL; + } + + /* We haven't returned any strings yet. */ + match_index = 0; + } + + match = match_list[match_index]; + + if (match) + match_index++; + + return (match); +} + +static char * +find_absolute_program (name, flags) + const char *name; + int flags; +{ + int st; + + st = file_status (name); + + /* If the file doesn't exist, quit now. */ + if ((st & FS_EXISTS) == 0) + return ((char *)NULL); + + /* If we only care about whether the file exists or not, return + this filename. Otherwise, maybe we care about whether this + file is executable. If it is, and that is what we want, return it. */ + if ((flags & FS_EXISTS) || ((flags & FS_EXEC_ONLY) && (st & FS_EXECABLE))) + return (savestring (name)); + + return (NULL); +} + +static char * +find_in_path_element (name, path, flags, name_len, dotinfop, rflagsp) + const char *name; + char *path; + int flags, name_len; + struct stat *dotinfop; + int *rflagsp; +{ + int status; + char *full_path, *xpath; + + xpath = (posixly_correct == 0 && *path == '~') ? bash_tilde_expand (path, 0) : path; + + /* Remember the location of "." in the path, in all its forms + (as long as they begin with a `.', e.g. `./.') */ + /* We could also do this or something similar for all relative pathnames + found while searching PATH. */ + if (dot_found_in_search == 0 && *xpath == '.') + dot_found_in_search = same_file (".", xpath, dotinfop, (struct stat *)NULL); + + full_path = sh_makepath (xpath, name, 0); + + status = file_status (full_path); + + if (xpath != path) + free (xpath); + + if (rflagsp) + *rflagsp = status; + + if ((status & FS_EXISTS) == 0) + { + free (full_path); + return ((char *)NULL); + } + + /* The file exists. If the caller simply wants the first file, here it is. */ + if (flags & FS_EXISTS) + return (full_path); + + /* If we have a readable file, and the caller wants a readable file, this + is it. */ + if ((flags & FS_READABLE) && (status & FS_READABLE)) + return (full_path); + + /* If the file is executable, then it satisfies the cases of + EXEC_ONLY and EXEC_PREFERRED. Return this file unconditionally. */ + if ((status & FS_EXECABLE) && (flags & (FS_EXEC_ONLY|FS_EXEC_PREFERRED)) && + (((flags & FS_NODIRS) == 0) || ((status & FS_DIRECTORY) == 0))) + { + FREE (file_to_lose_on); + file_to_lose_on = (char *)NULL; + return (full_path); + } + + /* The file is not executable, but it does exist. If we prefer + an executable, then remember this one if it is the first one + we have found. */ + if ((flags & FS_EXEC_PREFERRED) && file_to_lose_on == 0 && exec_name_should_ignore (full_path) == 0) + file_to_lose_on = savestring (full_path); + + /* If we want only executable files, or we don't want directories and + this file is a directory, or we want a readable file and this file + isn't readable, fail. */ + if ((flags & (FS_EXEC_ONLY|FS_EXEC_PREFERRED)) || + ((flags & FS_NODIRS) && (status & FS_DIRECTORY)) || + ((flags & FS_READABLE) && (status & FS_READABLE) == 0)) + { + free (full_path); + return ((char *)NULL); + } + else + return (full_path); +} + +/* This does the dirty work for find_user_command_internal () and + user_command_matches (). + NAME is the name of the file to search for. + PATH_LIST is a colon separated list of directories to search. + FLAGS contains bit fields which control the files which are eligible. + Some values are: + FS_EXEC_ONLY: The file must be an executable to be found. + FS_EXEC_PREFERRED: If we can't find an executable, then the + the first file matching NAME will do. + FS_EXISTS: The first file found will do. + FS_NODIRS: Don't find any directories. +*/ +static char * +find_user_command_in_path (name, path_list, flags, rflagsp) + const char *name; + char *path_list; + int flags, *rflagsp; +{ + char *full_path, *path; + int path_index, name_len, rflags; + struct stat dotinfo; + + /* We haven't started looking, so we certainly haven't seen + a `.' as the directory path yet. */ + dot_found_in_search = 0; + + if (rflagsp) + *rflagsp = 0; + + if (absolute_program (name)) + { + full_path = find_absolute_program (name, flags); + return (full_path); + } + + if (path_list == 0 || *path_list == '\0') + return (savestring (name)); /* XXX */ + + file_to_lose_on = (char *)NULL; + name_len = strlen (name); + if (stat (".", &dotinfo) < 0) + dotinfo.st_dev = dotinfo.st_ino = 0; + path_index = 0; + + while (path_list[path_index]) + { + /* Allow the user to interrupt out of a lengthy path search. */ + QUIT; + + path = get_next_path_element (path_list, &path_index); + if (path == 0) + break; + + /* Side effects: sets dot_found_in_search, possibly sets + file_to_lose_on. */ + full_path = find_in_path_element (name, path, flags, name_len, &dotinfo, &rflags); + free (path); + + /* We use the file status flag bits to check whether full_path is a + directory, which we reject here. */ + if (full_path && (rflags & FS_DIRECTORY)) + { + free (full_path); + continue; + } + + if (full_path) + { + if (rflagsp) + *rflagsp = rflags; + FREE (file_to_lose_on); + return (full_path); + } + } + + /* We didn't find exactly what the user was looking for. Return + the contents of FILE_TO_LOSE_ON which is NULL when the search + required an executable, or non-NULL if a file was found and the + search would accept a non-executable as a last resort. If the + caller specified FS_NODIRS, and file_to_lose_on is a directory, + return NULL. */ + if (file_to_lose_on && (flags & FS_NODIRS) && file_isdir (file_to_lose_on)) + { + free (file_to_lose_on); + file_to_lose_on = (char *)NULL; + } + + return (file_to_lose_on); +} + +/* External interface to find a command given a $PATH. Separate from + find_user_command_in_path to allow future customization. */ +char * +find_in_path (name, path_list, flags) + const char *name; + char *path_list; + int flags; +{ + return (find_user_command_in_path (name, path_list, flags, (int *)0)); +} |