[CMDUTILS][WHERE] Implement WHERE command (#3642)

WHERE is a Windows command that finds the file location from a executable file name. This PR implements it in ReactOS. CORE-17443
This commit is contained in:
Katayama Hirofumi MZ 2021-05-11 14:37:49 +09:00 committed by GitHub
parent 04e9251612
commit 55060911e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 639 additions and 0 deletions

View file

@ -25,6 +25,7 @@ add_subdirectory(taskkill)
add_subdirectory(tasklist)
add_subdirectory(timeout)
add_subdirectory(tree)
add_subdirectory(where)
add_subdirectory(whoami)
add_subdirectory(wmic)
add_subdirectory(wscript)

View file

@ -0,0 +1,7 @@
include_directories(${REACTOS_SOURCE_DIR}/sdk/lib/conutils)
add_executable(where where.c where.rc)
set_module_type(where win32cui UNICODE)
target_link_libraries(where conutils ${PSEH_LIB})
add_importlibs(where msvcrt kernel32)
add_cd_file(TARGET where DESTINATION reactos/system32 FOR all)

View file

@ -0,0 +1,52 @@
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
STRINGTABLE
BEGIN
IDS_USAGE "Usage: WHERE [options] pattern...\n\
\n\
Description:\n\
Shows the location of the file(s) specified by the pattern(s).\n\
By default, this tool searches by using the pattern(s) and the paths\n\
of the PATH environment variable.\n\
\n\
Options:\n\
/F Displays all matched file(s) in double quotes.\n\
/Q Quiet mode. Doesn't show any files and messages.\n\
/R dir Starts searching from the specified directory and recursively\n\
performs the search.\n\
/T Shows the file size and last modified date of all matched\n\
files.\n\
pattern Specifies the pattern to search files. Wildcards * and ? can\n\
be used. ""$env:pattern"" and ""path:pattern"" formats can also\n\
be used, where ""env"" is an environment variable and\n\
the search is done in the paths of the ""env"" environment\n\
variable. Don't use these formats with /R. The search is also\n\
performed by adding the extension of the PATHEXT variable to\n\
the pattern.\n\
/? Displays this message.\n\
\n\
NOTE: This tool returns an error level of 0 if the search was successful,\n\
1 if the file was not found, and 2 if there was an error.\n\
\n\
Example:\n\
WHERE myfile*.exe\n\
WHERE /F /T mspaint\n\
WHERE $WINDIR:notepad myfile???\n\
WHERE C:\\ReactOS;C:\\ReactOS\\system32:exp*.exe\n\
WHERE /R ""C:\\Program Files"" *.dll\n"
IDS_BAD_ARG "ERROR: Invalid argument - '%ls'.\n"
IDS_NOT_FOUND "INFO: Could not find files for the given pattern(s).\n"
IDS_FILE_INFO "%10I64u %-12ls %-12ls %ls\n"
IDS_WANT_VALUE "ERROR: Value is needed for '%ls'.\n"
IDS_TYPE_HELP "Type ""WHERE /?"" for usage help.\n"
IDS_ENVPAT_WITH_R "ERROR: ""$env:pattern"" cannot be used with /R.\n"
IDS_PATHPAT_WITH_R "ERROR: ""path:pattern"" format cannot be used with /R.\n"
IDS_BAD_PATHPAT "ERROR: Invalid pattern is specified in ""path:pattern"".\n"
IDS_OUTOFMEMORY "ERROR: Out of memory.\n"
IDS_BAD_ENVVAR "ERROR: Environment variable ""%ls"" is not found.\n"
IDS_CANT_FOUND "ERROR: The system could not find the file specified.\n"
IDS_BAD_DIR "ERROR: Invalid directory is specified.\n"
IDS_BAD_NAME "ERROR: The filename, directory name or volume label syntax is wrong.\n"
IDS_TOO_MANY "ERROR: '%ls' option is not allowed more than '%u' time(s).\n"
END

View file

@ -0,0 +1,15 @@
#define IDS_USAGE 100
#define IDS_BAD_ARG 101
#define IDS_NOT_FOUND 103
#define IDS_FILE_INFO 104
#define IDS_WANT_VALUE 106
#define IDS_TYPE_HELP 107
#define IDS_ENVPAT_WITH_R 108
#define IDS_PATHPAT_WITH_R 109
#define IDS_BAD_PATHPAT 110
#define IDS_OUTOFMEMORY 111
#define IDS_BAD_ENVVAR 112
#define IDS_CANT_FOUND 113
#define IDS_BAD_DIR 114
#define IDS_BAD_NAME 115
#define IDS_TOO_MANY 116

View file

@ -0,0 +1,66 @@
/*
* PROJECT: ReactOS WHERE command
* LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later)
* PURPOSE: Providing string list
* COPYRIGHT: Copyright 2021 Katayama Hirofumi MZ <katayama.hirofumi.mz@gmail.com>
*/
#pragma once
#define str_clone _wcsdup
typedef struct strlist_t
{
LPWSTR *ppsz;
unsigned int count;
} strlist_t;
#define strlist_default { NULL, 0 }
static inline void strlist_init(strlist_t *plist)
{
plist->ppsz = NULL;
plist->count = 0;
}
static inline LPWSTR strlist_get_at(strlist_t *plist, unsigned int i)
{
return plist->ppsz[i];
}
static int strlist_add(strlist_t *plist, LPCWSTR psz)
{
LPWSTR *ppsz, clone = str_clone(psz);
if (!clone)
return 0;
ppsz = (LPWSTR *)realloc(plist->ppsz, (plist->count + 1) * sizeof(LPWSTR));
if (!ppsz)
{
free(clone);
return 0;
}
plist->ppsz = ppsz;
plist->ppsz[plist->count] = clone;
++(plist->count);
return 1;
}
static void strlist_destroy(strlist_t *plist)
{
unsigned int i;
for (i = 0; i < plist->count; ++i)
free(plist->ppsz[i]);
plist->count = 0;
free(plist->ppsz);
plist->ppsz = NULL;
}
static inline int strlist_find_i(strlist_t *plist, LPCWSTR psz)
{
unsigned int i;
for (i = 0; i < plist->count; ++i)
{
if (_wcsicmp(plist->ppsz[i], psz) == 0)
return i;
}
return -1;
}

View file

@ -0,0 +1,486 @@
/*
* PROJECT: ReactOS WHERE command
* LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later)
* PURPOSE: Search executable files
* COPYRIGHT: Copyright 2021 Katayama Hirofumi MZ <katayama.hirofumi.mz@gmail.com>
*/
#include <stdlib.h>
#include <windef.h>
#include <winbase.h>
#include <winnls.h>
#include <strsafe.h>
#include <conutils.h>
#include "strlist.h" // strlist_...
#include "resource.h"
#define FLAG_HELP (1 << 0) // "/?"
#define FLAG_R (1 << 1) // recursive directory
#define FLAG_Q (1 << 2) // quiet mode
#define FLAG_F (1 << 3) // double quote
#define FLAG_T (1 << 4) // detailed info
static DWORD s_dwFlags = 0;
static LPWSTR s_pszRecursiveDir = NULL;
static strlist_t s_patterns = strlist_default;
static strlist_t s_results = strlist_default;
static strlist_t s_pathext = strlist_default;
// is it either "." or ".."?
#define IS_DOTS(pch) \
(*(pch) == L'.' && ((pch)[1] == 0 || ((pch)[1] == L'.' && (pch)[2] == 0)))
#define DEFAULT_PATHEXT L".com;.exe;.bat;.cmd"
typedef enum WRET // return code of WHERE command
{
WRET_SUCCESS = 0,
WRET_NOT_FOUND = 1,
WRET_ERROR = 2
} WRET;
static VOID WhereError(UINT nID)
{
if (!(s_dwFlags & FLAG_Q)) // not quiet mode?
ConResPuts(StdErr, nID);
}
typedef BOOL (CALLBACK *WHERE_CALLBACK)(LPCWSTR pattern, LPCWSTR path, PWIN32_FIND_DATAW data);
static BOOL
WhereSearchGeneric(LPCWSTR pattern, LPWSTR path, size_t path_len, BOOL bDir,
WHERE_CALLBACK callback)
{
LPWSTR pch;
size_t cch;
BOOL ret;
WIN32_FIND_DATAW data;
HANDLE hFind = FindFirstFileExW(path, FindExInfoStandard, &data, FindExSearchNameMatch,
NULL, 0);
if (hFind == INVALID_HANDLE_VALUE)
return TRUE; // not found
pch = wcsrchr(path, L'\\') + 1;
cch = path_len - (pch - path);
do
{
if (bDir != !!(data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
continue;
if (bDir && IS_DOTS(data.cFileName))
continue; // ignore "." and ".."
if (data.dwFileAttributes & FILE_ATTRIBUTE_VIRTUAL)
continue; // ignore virtual
StringCchCopyW(pch, cch, data.cFileName); // build full path
ret = callback(pattern, path, &data);
if (!ret) // out of memory
break;
} while (FindNextFileW(hFind, &data));
FindClose(hFind);
return ret;
}
static BOOL CALLBACK WherePrintPath(LPCWSTR pattern, LPCWSTR path, PWIN32_FIND_DATAW data)
{
WCHAR szPath[MAX_PATH + 2], szDate[32], szTime[32];
LARGE_INTEGER FileSize;
FILETIME ftLocal;
SYSTEMTIME st;
if (strlist_find_i(&s_results, path) >= 0)
return TRUE; // already exists
if (!strlist_add(&s_results, path))
return FALSE; // out of memory
if (s_dwFlags & FLAG_Q) // quiet mode?
return TRUE;
if (s_dwFlags & FLAG_T) // print detailed info
{
// convert date/time
FileTimeToLocalFileTime(&data->ftLastWriteTime, &ftLocal);
FileTimeToSystemTime(&ftLocal, &st);
// get date/time strings
GetDateFormatW(LOCALE_USER_DEFAULT, 0, &st, NULL, szDate, _countof(szDate));
GetTimeFormatW(LOCALE_USER_DEFAULT, 0, &st, NULL, szTime, _countof(szTime));
// set size
FileSize.LowPart = data->nFileSizeLow;
FileSize.HighPart = data->nFileSizeHigh;
// print
if (s_dwFlags & FLAG_F) // double quote
StringCchPrintfW(szPath, _countof(szPath), L"\"%s\"", path);
else
StringCchCopyW(szPath, _countof(szPath), path);
ConResPrintf(StdOut, IDS_FILE_INFO, FileSize.QuadPart, szDate, szTime, szPath);
}
else // print path only
{
if (s_dwFlags & FLAG_F) // double quote
ConPrintf(StdOut, L"\"%ls\"\n", path);
else
ConPrintf(StdOut, L"%ls\n", path);
}
return TRUE; // success
}
static BOOL WhereSearchFiles(LPCWSTR pattern, LPCWSTR dir)
{
INT iExt;
size_t cch;
WCHAR szPath[MAX_PATH];
StringCchCopyW(szPath, _countof(szPath), dir);
StringCchCatW(szPath, _countof(szPath), L"\\");
StringCchCatW(szPath, _countof(szPath), pattern);
cch = wcslen(szPath);
for (iExt = 0; iExt < s_pathext.count; ++iExt)
{
szPath[cch] = 0; // cut off extension
// append extension
StringCchCatW(szPath, _countof(szPath), strlist_get_at(&s_pathext, iExt));
if (!WhereSearchGeneric(pattern, szPath, _countof(szPath), FALSE, WherePrintPath))
return FALSE;
}
return TRUE;
}
static BOOL WhereSearchRecursive(LPCWSTR pattern, LPCWSTR dir);
static BOOL CALLBACK
WhereSearchRecursiveCallback(LPCWSTR pattern, LPCWSTR path, PWIN32_FIND_DATAW data)
{
return WhereSearchRecursive(pattern, path);
}
// FIXME: Too slow. Optimize for speed.
static BOOL WhereSearchRecursive(LPCWSTR pattern, LPCWSTR dir)
{
WCHAR szPath[MAX_PATH];
if (!WhereSearchFiles(pattern, dir))
return FALSE; // out of memory
// build path with wildcard
StringCchCopyW(szPath, _countof(szPath), dir);
StringCchCatW(szPath, _countof(szPath), L"\\*");
return WhereSearchGeneric(pattern, szPath, _countof(szPath), TRUE,
WhereSearchRecursiveCallback);
}
static BOOL WhereSearch(LPCWSTR pattern, strlist_t *dirlist)
{
UINT iDir;
for (iDir = 0; iDir < dirlist->count; ++iDir)
{
if (!WhereSearchFiles(pattern, strlist_get_at(dirlist, iDir)))
return FALSE;
}
return TRUE;
}
static BOOL WhereGetVariable(LPCWSTR name, LPWSTR *value)
{
DWORD cch = GetEnvironmentVariableW(name, NULL, 0);
if (cch == 0) // variable not found
{
*value = NULL;
if (!(s_dwFlags & FLAG_Q)) // not quiet mode?
ConResPrintf(StdErr, IDS_BAD_ENVVAR, name);
return TRUE; // it is error, but continue the task
}
*value = malloc(cch * sizeof(WCHAR));
if (!*value || !GetEnvironmentVariableW(name, *value, cch))
{
free(*value);
*value = NULL;
return FALSE; // error
}
return TRUE;
}
static BOOL WhereDoOption(DWORD flag, LPCWSTR option)
{
if (s_dwFlags & flag)
{
ConResPrintf(StdErr, IDS_TOO_MANY, option, 1);
ConResPuts(StdErr, IDS_TYPE_HELP);
return FALSE;
}
s_dwFlags |= flag;
return TRUE;
}
static BOOL WhereParseCommandLine(INT argc, WCHAR** argv)
{
INT iArg;
for (iArg = 1; iArg < argc; ++iArg)
{
LPWSTR arg = argv[iArg];
if (arg[0] == L'/' || arg[0] == L'-')
{
if (arg[2] == 0)
{
switch (towupper(arg[1]))
{
case L'?':
if (!WhereDoOption(FLAG_HELP, L"/?"))
return FALSE;
continue;
case L'F':
if (!WhereDoOption(FLAG_F, L"/F"))
return FALSE;
continue;
case L'Q':
if (!WhereDoOption(FLAG_Q, L"/Q"))
return FALSE;
continue;
case L'T':
if (!WhereDoOption(FLAG_T, L"/T"))
return FALSE;
continue;
case L'R':
{
if (!WhereDoOption(FLAG_R, L"/R"))
return FALSE;
if (iArg + 1 < argc)
{
++iArg;
s_pszRecursiveDir = argv[iArg];
continue;
}
ConResPrintf(StdErr, IDS_WANT_VALUE, L"/R");
ConResPuts(StdErr, IDS_TYPE_HELP);
return FALSE;
}
}
}
ConResPrintf(StdErr, IDS_BAD_ARG, argv[iArg]);
ConResPuts(StdErr, IDS_TYPE_HELP);
return FALSE;
}
else // pattern?
{
if (!strlist_add(&s_patterns, argv[iArg])) // append pattern
{
ConResPuts(StdErr, IDS_OUTOFMEMORY);
return FALSE;
}
}
}
return TRUE; // success
}
static BOOL WhereGetPathExt(strlist_t *ext_list)
{
BOOL ret = TRUE;
LPWSTR pszPathExt, ext;
DWORD cchPathExt = GetEnvironmentVariableW(L"PATHEXT", NULL, 0);
pszPathExt = (cchPathExt ? malloc(cchPathExt * sizeof(WCHAR)) : str_clone(DEFAULT_PATHEXT));
if (!pszPathExt)
return FALSE; // out of memory
if (cchPathExt)
GetEnvironmentVariableW(L"PATHEXT", pszPathExt, cchPathExt);
if (!strlist_add(ext_list, L"")) // add empty extension for normal search
{
strlist_destroy(ext_list);
free(pszPathExt);
return FALSE;
}
for (ext = wcstok(pszPathExt, L";"); ext; ext = wcstok(NULL, L";")) // for all extensions
{
if (!strlist_add(ext_list, ext)) // add extension to ext_list
{
strlist_destroy(ext_list);
ret = FALSE;
break;
}
}
free(pszPathExt);
return ret;
}
static BOOL WhereFindByDirs(LPCWSTR pattern, LPWSTR dirs)
{
BOOL ret;
size_t cch;
WCHAR szPath[MAX_PATH];
LPWSTR dir, pch;
strlist_t dirlist = strlist_default;
GetCurrentDirectoryW(_countof(szPath), szPath);
if (!strlist_add(&dirlist, szPath))
return FALSE; // out of memory
for (dir = wcstok(dirs, L";"); dir; dir = wcstok(NULL, L";"))
{
if (*dir == L'"') // began from '"'
{
pch = wcschr(++dir, L'"'); // find '"'
if (*pch)
*pch = 0; // cut off
}
if (*dir != '\\' && dir[1] != L':')
continue; // relative path
cch = wcslen(dir);
if (cch > 0 && dir[cch - 1] == L'\\')
dir[cch - 1] = 0; // remove trailing backslash
if (!strlist_add(&dirlist, dir))
{
strlist_destroy(&dirlist);
return FALSE; // out of memory
}
}
ret = WhereSearch(pattern, &dirlist);
strlist_destroy(&dirlist);
return ret;
}
static BOOL WhereFindByVar(LPCWSTR pattern, LPCWSTR name)
{
LPWSTR value;
BOOL ret = WhereGetVariable(name, &value);
if (ret && value)
ret = WhereFindByDirs(pattern, value);
free(value);
return ret;
}
static BOOL WhereIsRecursiveDirOK(LPCWSTR name)
{
if (wcschr(name, L';') != NULL)
{
WhereError(IDS_BAD_NAME);
return FALSE;
}
else
{
DWORD attrs = GetFileAttributesW(name);
if (attrs == INVALID_FILE_ATTRIBUTES) // file not found
{
WhereError(IDS_CANT_FOUND);
return FALSE;
}
if (!(attrs & FILE_ATTRIBUTE_DIRECTORY))
{
WhereError(IDS_BAD_DIR);
return FALSE;
}
return TRUE;
}
}
static BOOL WhereDoPattern(LPWSTR pattern)
{
BOOL ret;
LPWSTR pch = wcsrchr(pattern, L':');
if (pch)
{
*pch++ = 0;
if (pattern[0] == L'$') // $env:pattern
{
if (s_dwFlags & FLAG_R) // recursive?
{
WhereError(IDS_ENVPAT_WITH_R);
return FALSE;
}
ret = WhereFindByVar(pch, pattern + 1);
}
else // path:pattern
{
if (s_dwFlags & FLAG_R) // recursive?
{
WhereError(IDS_PATHPAT_WITH_R);
return FALSE;
}
if (wcschr(pch, L'\\') != NULL) // found '\\'?
{
WhereError(IDS_BAD_PATHPAT);
return FALSE;
}
ret = WhereFindByDirs(pch, pattern);
}
}
else if (s_pszRecursiveDir) // recursive
{
WCHAR szPath[MAX_PATH];
if (!WhereIsRecursiveDirOK(s_pszRecursiveDir))
return FALSE;
GetFullPathNameW(s_pszRecursiveDir, _countof(szPath), szPath, NULL);
ret = WhereSearchRecursive(pattern, szPath);
}
else // otherwise
{
ret = WhereFindByVar(pattern, L"PATH");
}
if (!ret)
WhereError(IDS_OUTOFMEMORY);
return ret;
}
INT wmain(INT argc, WCHAR **argv)
{
typedef BOOL (WINAPI *FN_DISABLE_WOW)(PVOID *);
HANDLE hKernel32 = GetModuleHandleA("kernel32");
FN_DISABLE_WOW DisableWOW =
(FN_DISABLE_WOW)GetProcAddress(hKernel32, "Wow64DisableWow64FsRedirection");
DWORD iPattern;
WRET ret = WRET_ERROR;
PVOID dummy;
ConInitStdStreams(); // Initialize the Console Standard Streams
if (!WhereParseCommandLine(argc, argv))
goto quit;
if ((s_dwFlags & FLAG_HELP) || !s_patterns.count)
{
ConResPuts(StdOut, IDS_USAGE);
goto quit;
}
if (DisableWOW)
DisableWOW(&dummy);
if (!WhereGetPathExt(&s_pathext))
{
WhereError(IDS_OUTOFMEMORY);
goto quit;
}
ret = WRET_SUCCESS;
for (iPattern = 0; iPattern < s_patterns.count; ++iPattern)
{
if (!WhereDoPattern(strlist_get_at(&s_patterns, iPattern)))
{
ret = WRET_ERROR;
goto quit;
}
}
if (!s_results.count)
{
WhereError(IDS_NOT_FOUND);
ret = WRET_NOT_FOUND;
}
quit:
strlist_destroy(&s_results);
strlist_destroy(&s_patterns);
strlist_destroy(&s_pathext);
return ret;
}

View file

@ -0,0 +1,12 @@
#include <windef.h>
#include "resource.h"
#define REACTOS_STR_FILE_DESCRIPTION "ReactOS WHERE Command"
#define REACTOS_STR_INTERNAL_NAME "where"
#define REACTOS_STR_ORIGINAL_FILENAME "where.exe"
#include <reactos/version.rc>
#pragma code_page(65001) /* UTF-8 */
#ifdef LANGUAGE_EN_US
#include "lang/en-US.rc"
#endif