xref: /reactos/base/shell/cmd/internal.c (revision d029a626)
1 /*
2  *  INTERNAL.C - command.com internal commands.
3  *
4  *
5  *  History:
6  *
7  *  17/08/94 (Tim Norman)
8  *    started.
9  *
10  *  08/08/95 (Matt Rains)
11  *    i have cleaned up the source code. changes now bring this source into
12  *    guidelines for recommended programming practice.
13  *
14  *  cd()
15  *    started.
16  *
17  *  dir()
18  *    i have added support for file attributes to the DIR() function. the
19  *    routine adds "d" (directory) and "r" (read only) output. files with the
20  *    system attribute have the filename converted to lowercase. files with
21  *    the hidden attribute are not displayed.
22  *
23  *    i have added support for directorys. now if the directory attribute is
24  *    detected the file size if replaced with the string "<dir>".
25  *
26  *  ver()
27  *    started.
28  *
29  *  md()
30  *    started.
31  *
32  *  rd()
33  *    started.
34  *
35  *  del()
36  *    started.
37  *
38  *  does not support wildcard selection.
39  *
40  *  todo: add delete directory support.
41  *        add recursive directory delete support.
42  *
43  *  ren()
44  *    started.
45  *
46  *  does not support wildcard selection.
47  *
48  *    todo: add rename directory support.
49  *
50  *  a general structure has been used for the cd, rd and md commands. this
51  *  will be better in the long run. it is too hard to maintain such diverse
52  *  functions when you are involved in a group project like this.
53  *
54  *  12/14/95 (Tim Norman)
55  *    fixed DIR so that it will stick \*.* if a directory is specified and
56  *    that it will stick on .* if a file with no extension is specified or
57  *    *.* if it ends in a \
58  *
59  *  1/6/96 (Tim Norman)
60  *    added an isatty call to DIR so it won't prompt for keypresses unless
61  *    stdin and stdout are the console.
62  *
63  *    changed parameters to be mutually consistent to make calling the
64  *    functions easier
65  *
66  *  rem()
67  *    started.
68  *
69  *  doskey()
70  *    started.
71  *
72  *    01/22/96 (Oliver Mueller)
73  *        error messages are now handled by perror.
74  *
75  *    02/05/96 (Tim Norman)
76  *        converted all functions to accept first/rest parameters
77  *
78  *    07/26/96 (Tim Norman)
79  *        changed return values to int instead of void
80  *
81  *        path() started.
82  *
83  *    12/23/96 (Aaron Kaufman)
84  *        rewrote dir() to mimic MS-DOS's dir
85  *
86  *    01/28/97 (Tim Norman)
87  *        cleaned up Aaron's DIR code
88  *
89  *    06/13/97 (Tim Norman)
90  *        moved DIR code to dir.c
91  *        re-implemented Aaron's DIR code
92  *
93  *    06/14/97 (Steffan Kaiser)
94  *        ctrl-break handling
95  *        bug fixes
96  *
97  *    27-Jul-1998 (John P Price <linux-guru@gcfl.net>)
98  *        added config.h include
99  *
100  *    03-Dec-1998 (Eric Kohl)
101  *        Replaced DOS calls by Win32 calls.
102  *
103  *    08-Dec-1998 (Eric Kohl)
104  *        Added help texts ("/?").
105  *
106  *    18-Dec-1998 (Eric Kohl)
107  *        Added support for quoted arguments (cd "program files").
108  *
109  *    07-Jan-1999 (Eric Kohl)
110  *        Clean up.
111  *
112  *    26-Jan-1999 (Eric Kohl)
113  *        Replaced remaining CRT io functions by Win32 io functions.
114  *        Unicode safe!
115  *
116  *    30-Jan-1999 (Eric Kohl)
117  *        Added "cd -" feature. Changes to the previous directory.
118  *
119  *    15-Mar-1999 (Eric Kohl)
120  *        Fixed bug in "cd -" feature. If the previous directory was a root
121  *        directory, it was ignored.
122  *
123  *    23-Feb-2001 (Carl Nettelblad <cnettel@hem.passagen.se>)
124  *        Improved chdir/cd command.
125  *
126  *    02-Apr-2004 (Magnus Olsen <magnus@greatlord.com>)
127  *        Remove all hard code string so they can be
128  *        translate to other langues.
129  *
130  *    19-Jul-2005 (Brandon Turner <turnerb7@msu.edu>)
131  *        Rewrite the CD, it working as Windows 2000 CMD
132  *
133  *    19-Jul-2005 (Magnus Olsen <magnus@greatlord.com>)
134  *        Add SetRootPath and GetRootPath
135  *
136  *    14-Jul-2007 (Pierre Schweitzer <heis_spiter@hotmail.com>)
137  *        Added commands help display to help command (ex. : "help cmd")
138  */
139 
140 #include "precomp.h"
141 
142 #ifdef INCLUDE_CMD_CHDIR
143 
144 /*
145  * Helper function for getting the current path from drive
146  * without changing the drive. Return code: 0 = ok, 1 = fail.
147  * 'InPath' can have any size; if the two first letters are
148  * not a drive with ':' it will get the current path on
149  * the current drive exactly as GetCurrentDirectory() does.
150  */
151 INT
GetRootPath(IN LPCTSTR InPath,OUT LPTSTR OutPath,IN INT size)152 GetRootPath(
153     IN LPCTSTR InPath,
154     OUT LPTSTR OutPath,
155     IN INT size)
156 {
157     if (InPath[0] && InPath[1] == _T(':'))
158     {
159         INT t = 0;
160 
161         if ((InPath[0] >= _T('0')) && (InPath[0] <= _T('9')))
162         {
163             t = (InPath[0] - _T('0')) + 28;
164         }
165         else if ((InPath[0] >= _T('a')) && (InPath[0] <= _T('z')))
166         {
167             t = (InPath[0] - _T('a')) + 1;
168         }
169         else if ((InPath[0] >= _T('A')) && (InPath[0] <= _T('Z')))
170         {
171             t = (InPath[0] - _T('A')) + 1;
172         }
173 
174         return (_tgetdcwd(t, OutPath, size) == NULL);
175     }
176 
177     /* Get current directory */
178     return !GetCurrentDirectory(size, OutPath);
179 }
180 
181 
SetRootPath(TCHAR * oldpath,TCHAR * InPath)182 BOOL SetRootPath(TCHAR *oldpath, TCHAR *InPath)
183 {
184     DWORD dwLastError;
185     TCHAR OutPath[MAX_PATH];
186     TCHAR OutPathTemp[MAX_PATH];
187 
188     StripQuotes(InPath);
189 
190     /* Retrieve the full path name from the (possibly relative) InPath */
191     if (GetFullPathName(InPath, ARRAYSIZE(OutPathTemp), OutPathTemp, NULL) == 0)
192     {
193         dwLastError = GetLastError();
194         goto Fail;
195     }
196 
197     if (bEnableExtensions)
198     {
199         /*
200          * Convert the full path to its correct case, and
201          * resolve any wilcard present as well in the path
202          * and retrieve the first result.
203          * Example: c:\windows\SYSTEM32 => C:\WINDOWS\System32
204          * Example: C:\WINDOWS\S* => C:\WINDOWS\System,
205          * or C:\WINDOWS\System32, depending on the user's OS.
206          */
207         GetPathCase(OutPathTemp, OutPath);
208     }
209     else
210     {
211         _tcscpy(OutPath, OutPathTemp);
212     }
213 
214     /* Use _tchdir(), since unlike SetCurrentDirectory() it updates
215      * the current-directory-on-drive environment variables. */
216     if (_tchdir(OutPath) != 0)
217     {
218         dwLastError = GetLastError();
219         if (dwLastError == ERROR_FILE_NOT_FOUND)
220             dwLastError = ERROR_PATH_NOT_FOUND;
221         goto Fail;
222     }
223 
224     /* Keep the original drive in ordinary CD/CHDIR (without /D switch) */
225     if (oldpath != NULL && _tcsncicmp(OutPath, oldpath, 2) != 0)
226         SetCurrentDirectory(oldpath);
227 
228     return TRUE;
229 
230 Fail:
231     ConErrFormatMessage(dwLastError);
232     nErrorLevel = 1;
233     return FALSE;
234 }
235 
236 
237 /*
238  * CD / CHDIR
239  */
cmd_chdir(LPTSTR param)240 INT cmd_chdir(LPTSTR param)
241 {
242     BOOL bChangeDrive = FALSE;
243     LPTSTR tmp;
244     TCHAR szCurrent[MAX_PATH];
245 
246     /* Filter out special cases first */
247 
248     /* Print help */
249     if (!_tcsncmp(param, _T("/?"), 2))
250     {
251         ConOutResPaging(TRUE, STRING_CD_HELP);
252         return 0;
253     }
254 
255     //
256     // FIXME: Use the split() tokenizer if bEnableExtensions == FALSE,
257     // so as to cut the parameter at the first separator (space, ',', ';'):
258     // - When bEnableExtensions == FALSE, doing
259     //   CD system32;winsxs
260     //   will go into system32, (but: CD "system32;winsxs" will fail as below), while
261     // - When bEnableExtensions == TRUE, it will fail because the "system32;winsxs"
262     //   directory does not exist.
263     //
264 
265     /* Remove extra quotes */
266     StripQuotes(param);
267 
268     if (bEnableExtensions)
269     {
270         /* Strip trailing whitespace */
271         tmp = param + _tcslen(param) - 1;
272         while (tmp > param && _istspace(*tmp))
273             --tmp;
274         *(tmp + 1) = _T('\0');
275     }
276 
277     /* Reset the error level */
278     nErrorLevel = 0;
279 
280     /* Print the current directory on a disk */
281     if (_tcslen(param) == 2 && param[1] == _T(':'))
282     {
283         if (GetRootPath(param, szCurrent, ARRAYSIZE(szCurrent)))
284         {
285             error_invalid_drive();
286             return 1;
287         }
288         ConOutPrintf(_T("%s\n"), szCurrent);
289         return 0;
290     }
291 
292     /* Get the current directory */
293     GetCurrentDirectory(ARRAYSIZE(szCurrent), szCurrent);
294     if (param[0] == _T('\0'))
295     {
296         ConOutPrintf(_T("%s\n"), szCurrent);
297         return 0;
298     }
299 
300     /* If the input string is prefixed with the /D switch, change the drive */
301     if (!_tcsncicmp(param, _T("/D"), 2))
302     {
303         bChangeDrive = TRUE;
304         param += 2;
305         while (_istspace(*param))
306             ++param;
307     }
308 
309     if (!SetRootPath(bChangeDrive ? NULL : szCurrent, param))
310     {
311         nErrorLevel = 1;
312         return 1;
313     }
314 
315     return 0;
316 }
317 
318 #endif
319 
320 #ifdef INCLUDE_CMD_MKDIR
321 
322 /* Helper function for mkdir to make directories in a path.
323 Don't use the api to decrease dependence on libs */
324 BOOL
MakeFullPath(TCHAR * DirPath)325 MakeFullPath(TCHAR * DirPath)
326 {
327     TCHAR path[MAX_PATH];
328     TCHAR *p = DirPath;
329     INT_PTR  n;
330 
331     if (CreateDirectory(DirPath, NULL))
332         return TRUE;
333     else if (GetLastError() != ERROR_PATH_NOT_FOUND)
334         return FALSE;
335 
336     /* got ERROR_PATH_NOT_FOUND, so try building it up one component at a time */
337     if (p[0] && p[1] == _T(':'))
338         p += 2;
339     while (*p == _T('\\'))
340         p++; /* skip drive root */
341     do
342     {
343         p = _tcschr(p, _T('\\'));
344         n = p ? p++ - DirPath : _tcslen(DirPath);
345         _tcsncpy(path, DirPath, n);
346         path[n] = _T('\0');
347         if ( !CreateDirectory(path, NULL) &&
348             (GetLastError() != ERROR_ALREADY_EXISTS))
349         {
350             return FALSE;
351         }
352     } while (p != NULL);
353 
354     return TRUE;
355 }
356 
357 /*
358  * MD / MKDIR
359  */
cmd_mkdir(LPTSTR param)360 INT cmd_mkdir (LPTSTR param)
361 {
362     LPTSTR *p;
363     INT argc, i;
364     DWORD dwLastError;
365 
366     if (!_tcsncmp (param, _T("/?"), 2))
367     {
368         ConOutResPaging(TRUE,STRING_MKDIR_HELP);
369         return 0;
370     }
371 
372     p = split (param, &argc, FALSE, FALSE);
373     if (argc == 0)
374     {
375         ConErrResPuts(STRING_ERROR_REQ_PARAM_MISSING);
376         freep(p);
377         nErrorLevel = 1;
378         return 1;
379     }
380 
381     nErrorLevel = 0;
382     for (i = 0; i < argc; i++)
383     {
384         if (!MakeFullPath(p[i]))
385         {
386             dwLastError = GetLastError();
387             switch (dwLastError)
388             {
389             case ERROR_PATH_NOT_FOUND:
390                 ConErrResPuts(STRING_MD_ERROR2);
391                 break;
392 
393             case ERROR_FILE_EXISTS:
394             case ERROR_ALREADY_EXISTS:
395                 ConErrResPrintf(STRING_MD_ERROR, p[i]);
396                 break;
397 
398             default:
399                 ErrorMessage(GetLastError(), NULL);
400             }
401             nErrorLevel = 1;
402         }
403     }
404 
405     freep (p);
406     return nErrorLevel;
407 }
408 #endif
409 
410 #ifdef INCLUDE_CMD_RMDIR
411 /*
412  * RD / RMDIR
413  */
DeleteFolder(LPTSTR Directory)414 BOOL DeleteFolder(LPTSTR Directory)
415 {
416     LPTSTR pFileName;
417     HANDLE hFile;
418     WIN32_FIND_DATA f;
419     DWORD dwAttribs;
420     TCHAR szFullPath[MAX_PATH];
421 
422     _tcscpy(szFullPath, Directory);
423     pFileName = &szFullPath[_tcslen(szFullPath)];
424     /*
425      * Append a path separator if we don't have one already, and if this a drive root
426      * path is not specified (paths like "C:" mean the current directory on drive C:).
427      */
428     if (*szFullPath && *(pFileName - 1) != _T(':') && *(pFileName - 1) != _T('\\'))
429         *pFileName++ = _T('\\');
430     _tcscpy(pFileName, _T("*"));
431 
432     hFile = FindFirstFile(szFullPath, &f);
433     if (hFile != INVALID_HANDLE_VALUE)
434     {
435         do
436         {
437             /* Check Breaker */
438             if (bCtrlBreak)
439                 break;
440 
441             if (!_tcscmp(f.cFileName, _T(".")) ||
442                 !_tcscmp(f.cFileName, _T("..")))
443             {
444                 continue;
445             }
446 
447             _tcscpy(pFileName, f.cFileName);
448 
449             dwAttribs = f.dwFileAttributes;
450 
451             if (dwAttribs & FILE_ATTRIBUTE_DIRECTORY)
452             {
453                 if (!DeleteFolder(szFullPath))
454                 {
455                     /* Couldn't delete the file, print out the error */
456                     ErrorMessage(GetLastError(), szFullPath);
457 
458                     /* Continue deleting files/subfolders */
459                 }
460             }
461             else
462             {
463                 /* Force file deletion even if it's read-only */
464                 if (dwAttribs & FILE_ATTRIBUTE_READONLY)
465                     SetFileAttributes(szFullPath, dwAttribs & ~FILE_ATTRIBUTE_READONLY);
466 
467                 if (!DeleteFile(szFullPath))
468                 {
469                     /* Couldn't delete the file, print out the error */
470                     ErrorMessage(GetLastError(), szFullPath);
471 
472                     /* Restore file attributes */
473                     SetFileAttributes(szFullPath, dwAttribs);
474 
475                     /* Continue deleting files/subfolders */
476                 }
477             }
478 
479         } while (FindNextFile(hFile, &f));
480         FindClose(hFile);
481     }
482 
483     /* Ignore directory deletion if the user pressed Ctrl-C */
484     if (bCtrlBreak)
485         return TRUE;
486 
487     /*
488      * Detect whether we are trying to delete a pure root drive (e.g. "C:\\", but not "C:");
489      * if so, just return success. Otherwise the RemoveDirectory() call below would fail
490      * and return ERROR_ACCESS_DENIED.
491      */
492     if (GetFullPathName(Directory, ARRAYSIZE(szFullPath), szFullPath, NULL) == 3 &&
493         szFullPath[1] == _T(':') && szFullPath[2] == _T('\\'))
494     {
495         return TRUE;
496     }
497 
498     /* First attempt to delete the directory */
499     if (RemoveDirectory(Directory))
500         return TRUE;
501 
502     /*
503      * It failed; if it was due to an denied access, check whether it was
504      * due to the directory being read-only. If so, remove its attribute
505      * and retry deletion.
506      */
507     if (GetLastError() == ERROR_ACCESS_DENIED)
508     {
509         /* Force directory deletion even if it's read-only */
510         dwAttribs = GetFileAttributes(Directory);
511         if (dwAttribs & FILE_ATTRIBUTE_READONLY)
512         {
513             SetFileAttributes(Directory, dwAttribs & ~FILE_ATTRIBUTE_READONLY);
514             return RemoveDirectory(Directory);
515         }
516     }
517 
518     return FALSE;
519 }
520 
cmd_rmdir(LPTSTR param)521 INT cmd_rmdir(LPTSTR param)
522 {
523     INT nError = 0;
524     INT res;
525     LPTSTR *arg;
526     INT args;
527     INT dirCount;
528     INT i;
529     TCHAR ch;
530     BOOL bRecurseDir = FALSE;
531     BOOL bQuiet = FALSE;
532 
533     if (!_tcsncmp(param, _T("/?"), 2))
534     {
535         ConOutResPaging(TRUE,STRING_RMDIR_HELP);
536         return 0;
537     }
538 
539     arg = split(param, &args, FALSE, FALSE);
540     dirCount = 0;
541 
542     /* Check for options anywhere in command line */
543     for (i = 0; i < args; i++)
544     {
545         if (*arg[i] == _T('/'))
546         {
547             /* Found an option, but check to make sure it has something after it */
548             if (_tcslen(arg[i]) == 2)
549             {
550                 ch = _totupper(arg[i][1]);
551 
552                 if (ch == _T('S'))
553                     bRecurseDir = TRUE;
554                 else if (ch == _T('Q'))
555                     bQuiet = TRUE;
556             }
557         }
558         else
559         {
560             dirCount++;
561         }
562     }
563 
564     if (dirCount == 0)
565     {
566         /* No folder to remove */
567         error_req_param_missing();
568         freep(arg);
569         return 1;
570     }
571 
572     for (i = 0; i < args; i++)
573     {
574         if (*arg[i] == _T('/'))
575             continue;
576 
577         if (bRecurseDir)
578         {
579             /* Ask the user whether to delete everything in the folder */
580             if (!bQuiet)
581             {
582                 res = FilePromptYNA(STRING_DEL_HELP2);
583                 if (res == PROMPT_NO || res == PROMPT_BREAK)
584                 {
585                     nError = 1;
586                     continue;
587                 }
588                 if (res == PROMPT_ALL)
589                     bQuiet = TRUE;
590             }
591 
592             res = DeleteFolder(arg[i]);
593         }
594         else
595         {
596             /* Without /S, do not force directory deletion even if it's read-only */
597             res = RemoveDirectory(arg[i]);
598         }
599 
600         if (!res)
601         {
602             /* Couldn't delete the folder, print out the error */
603             nError = GetLastError();
604             ErrorMessage(nError, NULL);
605         }
606     }
607 
608     freep(arg);
609     return nError;
610 }
611 #endif
612 
613 
614 /*
615  * Either exits the command interpreter, or quits the current batch context.
616  */
617 
618 /* Enable this define for supporting EXIT /B even when extensions are disabled */
619 // #define SUPPORT_EXIT_B_NO_EXTENSIONS
620 
CommandExit(LPTSTR param)621 INT CommandExit(LPTSTR param)
622 {
623     if (!_tcsncmp(param, _T("/?"), 2))
624     {
625         ConOutResPaging(TRUE, STRING_EXIT_HELP);
626 
627         /* Just make sure we don't exit */
628         bExit = FALSE;
629         return 0;
630     }
631 
632     if (_tcsnicmp(param, _T("/B"), 2) == 0)
633     {
634         param += 2;
635 
636         /*
637          * If a current batch file is running, exit it,
638          * otherwise exit this command interpreter instance.
639          */
640         if (bc)
641         {
642             /* Windows' CMD compatibility: Use GOTO :EOF */
643             TCHAR EofLabel[] = _T(":EOF");
644 
645 #ifdef SUPPORT_EXIT_B_NO_EXTENSIONS
646             /*
647              * Temporarily enable extensions so as to support :EOF.
648              *
649              * Our GOTO implementation ensures that, when extensions are
650              * enabled and the label is ':EOF', no immediate change of batch
651              * context (done e.g. via ExitBatch() calls) is performed.
652              * This will therefore ensure that we do not spoil the extensions
653              * state when we restore it below.
654              */
655             BOOL bOldEnableExtensions = bEnableExtensions;
656             bEnableExtensions = TRUE;
657 #endif
658 
659             cmd_goto(EofLabel);
660 
661 #ifdef SUPPORT_EXIT_B_NO_EXTENSIONS
662             /* Restore the original state of the extensions */
663             bEnableExtensions = bOldEnableExtensions;
664 #endif
665         }
666         else
667         {
668             bExit = TRUE;
669         }
670     }
671     else
672     {
673         /* Exit this command interpreter instance */
674         bExit = TRUE;
675     }
676 
677     /* Search for an optional exit code */
678     while (_istspace(*param))
679         ++param;
680 
681     /* Set the errorlevel to the exit code */
682     if (_istdigit(*param))
683     {
684         nErrorLevel = _ttoi(param);
685         // if (fSingleCommand == 1) return nErrorLevel;
686     }
687 
688     return (bExit ? nErrorLevel : 0);
689 }
690 
691 #ifdef INCLUDE_CMD_REM
692 /*
693  * does nothing
694  */
CommandRem(LPTSTR param)695 INT CommandRem (LPTSTR param)
696 {
697     if (_tcsstr(param, _T("/?")) == param)
698     {
699         ConOutResPaging(TRUE,STRING_REM_HELP);
700     }
701 
702     return 0;
703 }
704 #endif /* INCLUDE_CMD_REM */
705 
706 
CommandShowCommands(LPTSTR param)707 INT CommandShowCommands(LPTSTR param)
708 {
709     PrintCommandList();
710     return 0;
711 }
712 
713 /* EOF */
714