xref: /reactos/base/applications/cmdutils/find/find.c (revision 15524349)
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
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
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 
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         FindString(stdin, NULL, argv[iSearchedStringIndex]);
341     }
342 
343     return iReturnValue;
344 }
345