1 /*
2 * PROJECT: ReactOS Find Command
3 * LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later)
4 * PURPOSE: Prints all lines of a file that contain a string.
5 * COPYRIGHT: Copyright 1994-2002 Jim Hall (jhall@freedos.org)
6 * Copyright 2019 Paweł Cholewa (DaMcpg@protonmail.com)
7 * Copyright 2019 Hermes Belusca-Maito
8 */
9
10 #include <stdio.h>
11 #include <stdlib.h>
12
13 #include <windef.h>
14 #include <winbase.h>
15 #include <winnls.h>
16 #include <winuser.h>
17
18 #include <conutils.h>
19 #include <strsafe.h>
20
21 #include "resource.h"
22
23 #define FIND_LINE_BUFFER_SIZE 4096
24
25 static BOOL bInvertSearch = FALSE;
26 static BOOL bCountLines = FALSE;
27 static BOOL bDisplayLineNumbers = FALSE;
28 static BOOL bIgnoreCase = FALSE;
29 static BOOL bDoNotSkipOfflineFiles = FALSE;
30
31 /**
32 * @name StrStrCase
33 * @implemented
34 *
35 * Locates a substring inside a NULL-terminated wide string.
36 *
37 * @param[in] pszStr
38 * The NULL-terminated string to be scanned.
39 *
40 * @param[in] pszSearch
41 * The NULL-terminated string to search for.
42 *
43 * @param[in] bIgnoreCase
44 * TRUE if case has to be ignored, FALSE otherwise.
45 *
46 * @return
47 * Returns a pointer to the first occurrence of pszSearch in pszStr,
48 * or NULL if pszSearch does not appear in pszStr. If pszSearch points
49 * to a string of zero length, the function returns pszStr.
50 */
51 static PWSTR
StrStrCase(IN PCWSTR pszStr,IN PCWSTR pszSearch,IN BOOL bIgnoreCase)52 StrStrCase(
53 IN PCWSTR pszStr,
54 IN PCWSTR pszSearch,
55 IN BOOL bIgnoreCase)
56 {
57 if (bIgnoreCase)
58 {
59 LCID LocaleId;
60 INT i, cch1, cch2;
61
62 LocaleId = GetThreadLocale();
63
64 cch1 = wcslen(pszStr);
65 cch2 = wcslen(pszSearch);
66
67 if (cch2 == 0)
68 return (PWSTR)pszStr;
69
70 for (i = 0; i <= cch1 - cch2; ++i)
71 {
72 if (CompareStringW(LocaleId /* LOCALE_SYSTEM_DEFAULT */,
73 NORM_IGNORECASE /* | NORM_LINGUISTIC_CASING */,
74 pszStr + i, cch2, pszSearch, cch2) == CSTR_EQUAL)
75 {
76 return (PWSTR)(pszStr + i);
77 }
78 }
79 return NULL;
80 }
81 else
82 {
83 return wcsstr(pszStr, pszSearch);
84 }
85 }
86
87 /**
88 * @name FindString
89 * @implemented
90 *
91 * Prints all lines of the stream that contain a string.
92 *
93 * @param[in] pStream
94 * The stream to read from.
95 *
96 * @param[in] pszFilePath
97 * The file name to print out. Can be NULL.
98 *
99 * @param[in] pszSearchString
100 * The NULL-terminated string to search for.
101 *
102 * @return
103 * 0 if the string was found at least once, 1 otherwise.
104 */
105 static int
FindString(IN FILE * pStream,IN PCWSTR pszFilePath OPTIONAL,IN PCWSTR pszSearchString)106 FindString(
107 IN FILE* pStream,
108 IN PCWSTR pszFilePath OPTIONAL,
109 IN PCWSTR pszSearchString)
110 {
111 LONG lLineCount = 0;
112 LONG lLineNumber = 0;
113 BOOL bSubstringFound;
114 int iReturnValue = 1;
115 WCHAR szLineBuffer[FIND_LINE_BUFFER_SIZE];
116
117 if (pszFilePath != NULL)
118 {
119 /* Print the file's header */
120 ConPrintf(StdOut, L"\n---------- %s%s",
121 pszFilePath, bCountLines ? L": " : L"\n");
122 }
123
124 /* Loop through every line in the file */
125 // FIXME: What if the string we search for crosses the boundary of our szLineBuffer ?
126 while (fgetws(szLineBuffer, _countof(szLineBuffer), pStream) != NULL)
127 {
128 ++lLineNumber;
129
130 bSubstringFound = (StrStrCase(szLineBuffer, pszSearchString, bIgnoreCase) != NULL);
131
132 /* Check if this line can be counted */
133 if (bSubstringFound != bInvertSearch)
134 {
135 iReturnValue = 0;
136
137 if (bCountLines)
138 {
139 ++lLineCount;
140 }
141 else
142 {
143 /* Display the line number if needed */
144 if (bDisplayLineNumbers)
145 {
146 ConPrintf(StdOut, L"[%ld]", lLineNumber);
147 }
148 ConPrintf(StdOut, L"%s", szLineBuffer);
149 }
150 }
151 }
152
153 if (bCountLines)
154 {
155 /* Print the matching line count */
156 ConPrintf(StdOut, L"%ld\n", lLineCount);
157 }
158 #if 0
159 else if (pszFilePath != NULL && iReturnValue == 0)
160 {
161 /* Print a newline for formatting */
162 ConPrintf(StdOut, L"\n");
163 }
164 #endif
165
166 return iReturnValue;
167 }
168
wmain(int argc,WCHAR * argv[])169 int wmain(int argc, WCHAR* argv[])
170 {
171 int i;
172 int iReturnValue = 2;
173 int iSearchedStringIndex = -1;
174 BOOL bFoundFileParameter = FALSE;
175 HANDLE hFindFile;
176 WIN32_FIND_DATAW FindData;
177 FILE* pOpenedFile;
178 PWCHAR ptr;
179 WCHAR szFullFilePath[MAX_PATH];
180
181 /* Initialize the Console Standard Streams */
182 ConInitStdStreams();
183
184 if (argc == 1)
185 {
186 /* If no argument were provided by the user, display program usage and exit */
187 ConResPuts(StdOut, IDS_USAGE);
188 return 0;
189 }
190
191 /* Parse the command line arguments */
192 for (i = 1; i < argc; ++i)
193 {
194 /* Check if this argument contains a switch */
195 if (wcslen(argv[i]) == 2 && argv[i][0] == L'/')
196 {
197 switch (towupper(argv[i][1]))
198 {
199 case L'?':
200 ConResPuts(StdOut, IDS_USAGE);
201 return 0;
202 case L'V':
203 bInvertSearch = TRUE;
204 break;
205 case L'C':
206 bCountLines = TRUE;
207 break;
208 case L'N':
209 bDisplayLineNumbers = TRUE;
210 break;
211 case L'I':
212 bIgnoreCase = TRUE;
213 break;
214 default:
215 /* Report invalid switch error */
216 ConResPuts(StdErr, IDS_INVALID_SWITCH);
217 return 2;
218 }
219 }
220 else if (wcslen(argv[i]) > 2 && argv[i][0] == L'/')
221 {
222 /* Check if this parameter is /OFF or /OFFLINE */
223 if (_wcsicmp(argv[i], L"/off") == 0 || _wcsicmp(argv[i], L"/offline") == 0)
224 {
225 bDoNotSkipOfflineFiles = TRUE;
226 }
227 else
228 {
229 /* Report invalid switch error */
230 ConResPuts(StdErr, IDS_INVALID_SWITCH);
231 return 2;
232 }
233 }
234 else
235 {
236 if (iSearchedStringIndex == -1)
237 {
238 iSearchedStringIndex = i;
239 }
240 else
241 {
242 /* There's a file specified in the parameters, no need to read from stdin */
243 bFoundFileParameter = TRUE;
244 }
245 }
246 }
247
248 if (iSearchedStringIndex == -1)
249 {
250 /* User didn't provide the string to search for, display program usage and exit */
251 ConResPuts(StdErr, IDS_USAGE);
252 return 2;
253 }
254
255 if (bFoundFileParameter)
256 {
257 /* After the command line arguments were parsed, iterate through them again to get the filenames */
258 for (i = 1; i < argc; ++i)
259 {
260 /* If the value is a switch or the searched string, continue */
261 if ((wcslen(argv[i]) > 0 && argv[i][0] == L'/') || i == iSearchedStringIndex)
262 {
263 continue;
264 }
265
266 hFindFile = FindFirstFileW(argv[i], &FindData);
267 if (hFindFile == INVALID_HANDLE_VALUE)
268 {
269 ConResPrintf(StdErr, IDS_NO_SUCH_FILE, argv[i]);
270 continue;
271 }
272
273 do
274 {
275 /* Check if the file contains offline attribute and should be skipped */
276 if ((FindData.dwFileAttributes & FILE_ATTRIBUTE_OFFLINE) && !bDoNotSkipOfflineFiles)
277 {
278 continue;
279 }
280
281 /* Skip directory */
282 // FIXME: Implement recursivity?
283 if (FindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
284 {
285 continue;
286 }
287
288 /*
289 * Build the full file path from the file specification pattern.
290 *
291 * Note that we could use GetFullPathName() instead, however
292 * we want to keep compatibility with Windows' find.exe utility
293 * that does not use this function as it keeps the file name
294 * directly based on the pattern.
295 */
296 ptr = wcsrchr(argv[i], L'\\'); // Check for last directory.
297 if (!ptr)
298 ptr = wcsrchr(argv[i], L':'); // Check for drive.
299 if (ptr)
300 {
301 /* The pattern contains a drive or directory part: keep it and concatenate the full file name */
302 StringCchCopyNW(szFullFilePath, _countof(szFullFilePath),
303 argv[i], ptr + 1 - argv[i]);
304 StringCchCatW(szFullFilePath, _countof(szFullFilePath),
305 FindData.cFileName);
306 }
307 else
308 {
309 /* The pattern does not contain any drive or directory part: just copy the full file name */
310 StringCchCopyW(szFullFilePath, _countof(szFullFilePath),
311 FindData.cFileName);
312 }
313
314 // FIXME: Windows' find.exe supports searching inside binary files.
315 pOpenedFile = _wfopen(szFullFilePath, L"r");
316 if (pOpenedFile == NULL)
317 {
318 ConResPrintf(StdErr, IDS_CANNOT_OPEN, szFullFilePath);
319 continue;
320 }
321
322 /* NOTE: Convert the file path to uppercase for formatting */
323 if (FindString(pOpenedFile, _wcsupr(szFullFilePath), argv[iSearchedStringIndex]) == 0)
324 {
325 iReturnValue = 0;
326 }
327 else if (iReturnValue != 0)
328 {
329 iReturnValue = 1;
330 }
331
332 fclose(pOpenedFile);
333 } while (FindNextFileW(hFindFile, &FindData));
334
335 FindClose(hFindFile);
336 }
337 }
338 else
339 {
340 iReturnValue = FindString(stdin, NULL, argv[iSearchedStringIndex]);
341 }
342
343 return iReturnValue;
344 }
345