xcopy: Add support for /P (Prompt).
[wine/wine64.git] / programs / xcopy / xcopy.c
blob0539716e4f5b0e353ab53fcdc60d4fa19e49744e
1 /*
2 * XCOPY - Wine-compatible xcopy program
4 * Copyright (C) 2007 J. Edmeades
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
22 * Notes:
23 * Apparently, valid return codes are:
24 * 0 - OK
25 * 1 - No files found to copy
26 * 2 - CTRL+C during copy
27 * 4 - Initialization error, or invalid source specification
28 * 5 - Disk write error
32 #include <stdio.h>
33 #include <windows.h>
34 #include <wine/debug.h>
36 /* Local #defines */
37 #define RC_OK 0
38 #define RC_NOFILES 1
39 #define RC_CTRLC 2
40 #define RC_INITERROR 4
41 #define RC_WRITEERROR 5
43 #define OPT_ASSUMEDIR 0x00000001
44 #define OPT_RECURSIVE 0x00000002
45 #define OPT_EMPTYDIR 0x00000004
46 #define OPT_QUIET 0x00000008
47 #define OPT_FULL 0x00000010
48 #define OPT_SIMULATE 0x00000020
49 #define OPT_PAUSE 0x00000040
50 #define OPT_NOCOPY 0x00000080
51 #define OPT_NOPROMPT 0x00000100
52 #define OPT_SHORTNAME 0x00000200
53 #define OPT_MUSTEXIST 0x00000400
54 #define OPT_REPLACEREAD 0x00000800
55 #define OPT_COPYHIDSYS 0x00001000
56 #define OPT_IGNOREERRORS 0x00002000
57 #define OPT_SRCPROMPT 0x00004000
59 #define MAXSTRING 8192
61 WINE_DEFAULT_DEBUG_CHANNEL(xcopy);
63 /* Prototypes */
64 static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem, WCHAR *spec);
65 static int XCOPY_ProcessDestParm(WCHAR *supplieddestination, WCHAR *stem,
66 WCHAR *spec, WCHAR *srcspec, DWORD flags);
67 static int XCOPY_DoCopy(WCHAR *srcstem, WCHAR *srcspec,
68 WCHAR *deststem, WCHAR *destspec,
69 DWORD flags);
70 static BOOL XCOPY_CreateDirectory(const WCHAR* path);
72 /* Global variables */
73 static ULONG filesCopied = 0; /* Number of files copied */
74 static const WCHAR wchr_slash[] = {'\\', 0};
75 static const WCHAR wchr_star[] = {'*', 0};
76 static const WCHAR wchr_dot[] = {'.', 0};
77 static const WCHAR wchr_dotdot[] = {'.', '.', 0};
79 /* Constants (Mostly for widechars) */
82 /* To minimize stack usage during recursion, some temporary variables
83 made global */
84 static WCHAR copyFrom[MAX_PATH];
85 static WCHAR copyTo[MAX_PATH];
88 /* =========================================================================
89 main - Main entrypoint for the xcopy command
91 Processes the args, and drives the actual copying
92 ========================================================================= */
93 int main (int argc, char *argv[])
95 int rc = 0;
96 WCHAR suppliedsource[MAX_PATH] = {0}; /* As supplied on the cmd line */
97 WCHAR supplieddestination[MAX_PATH] = {0};
98 WCHAR sourcestem[MAX_PATH] = {0}; /* Stem of source */
99 WCHAR sourcespec[MAX_PATH] = {0}; /* Filespec of source */
100 WCHAR destinationstem[MAX_PATH] = {0}; /* Stem of destination */
101 WCHAR destinationspec[MAX_PATH] = {0}; /* Filespec of destination */
102 WCHAR copyCmd[MAXSTRING]; /* COPYCMD env var */
103 DWORD flags = 0; /* Option flags */
104 LPWSTR *argvW = NULL;
105 const WCHAR PROMPTSTR1[] = {'/', 'Y', 0};
106 const WCHAR PROMPTSTR2[] = {'/', 'y', 0};
107 const WCHAR COPYCMD[] = {'C', 'O', 'P', 'Y', 'C', 'M', 'D', 0};
110 * Parse the command line
113 /* overwrite the command line */
114 argvW = CommandLineToArgvW( GetCommandLineW(), &argc );
116 /* Confirm at least one parameter */
117 if (argc < 2) {
118 printf("Invalid number of parameters - Use xcopy /? for help\n");
119 return RC_INITERROR;
122 /* Preinitialize flags based on COPYCMD */
123 if (GetEnvironmentVariable(COPYCMD, copyCmd, MAXSTRING)) {
124 if (wcsstr(copyCmd, PROMPTSTR1) != NULL ||
125 wcsstr(copyCmd, PROMPTSTR2) != NULL) {
126 flags |= OPT_NOPROMPT;
130 /* Skip first arg, which is the program name */
131 argvW++;
133 while (argc > 1)
135 argc--;
136 WINE_TRACE("Processing Arg: '%s'\n", wine_dbgstr_w(*argvW));
138 /* First non-switch parameter is source, second is destination */
139 if (*argvW[0] != '/') {
140 if (suppliedsource[0] == 0x00) {
141 lstrcpyW(suppliedsource, *argvW);
142 } else if (supplieddestination[0] == 0x00) {
143 lstrcpyW(supplieddestination, *argvW);
144 } else {
145 printf("Invalid number of parameters - Use xcopy /? for help\n");
146 return RC_INITERROR;
148 } else {
149 /* Process all the switch options
150 Note: Windows docs say /P prompts when dest is created
151 but tests show it is done for each src file
152 regardless of the destination */
153 switch (toupper(argvW[0][1])) {
154 case 'I': flags |= OPT_ASSUMEDIR; break;
155 case 'S': flags |= OPT_RECURSIVE; break;
156 case 'E': flags |= OPT_EMPTYDIR; break;
157 case 'Q': flags |= OPT_QUIET; break;
158 case 'F': flags |= OPT_FULL; break;
159 case 'L': flags |= OPT_SIMULATE; break;
160 case 'W': flags |= OPT_PAUSE; break;
161 case 'T': flags |= OPT_NOCOPY | OPT_RECURSIVE; break;
162 case 'Y': flags |= OPT_NOPROMPT; break;
163 case 'N': flags |= OPT_SHORTNAME; break;
164 case 'U': flags |= OPT_MUSTEXIST; break;
165 case 'R': flags |= OPT_REPLACEREAD; break;
166 case 'H': flags |= OPT_COPYHIDSYS; break;
167 case 'C': flags |= OPT_IGNOREERRORS; break;
168 case 'P': flags |= OPT_SRCPROMPT; break;
169 case '-': if (toupper(argvW[0][2])=='Y')
170 flags &= ~OPT_NOPROMPT; break;
171 default:
172 WINE_FIXME("Unhandled parameter '%s'\n", wine_dbgstr_w(*argvW));
175 argvW++;
178 /* Default the destination if not supplied */
179 if (supplieddestination[0] == 0x00)
180 lstrcpyW(supplieddestination, wchr_dot);
182 /* Trace out the supplied information */
183 WINE_TRACE("Supplied parameters:\n");
184 WINE_TRACE("Source : '%s'\n", wine_dbgstr_w(suppliedsource));
185 WINE_TRACE("Destination : '%s'\n", wine_dbgstr_w(supplieddestination));
187 /* Extract required information from source specification */
188 rc = XCOPY_ProcessSourceParm(suppliedsource, sourcestem, sourcespec);
190 /* Extract required information from destination specification */
191 rc = XCOPY_ProcessDestParm(supplieddestination, destinationstem,
192 destinationspec, sourcespec, flags);
194 /* Trace out the resulting information */
195 WINE_TRACE("Resolved parameters:\n");
196 WINE_TRACE("Source Stem : '%s'\n", wine_dbgstr_w(sourcestem));
197 WINE_TRACE("Source Spec : '%s'\n", wine_dbgstr_w(sourcespec));
198 WINE_TRACE("Dest Stem : '%s'\n", wine_dbgstr_w(destinationstem));
199 WINE_TRACE("Dest Spec : '%s'\n", wine_dbgstr_w(destinationspec));
201 /* Pause if necessary */
202 if (flags & OPT_PAUSE) {
203 DWORD count;
204 char pausestr[10];
206 printf("Press <enter> to begin copying\n");
207 ReadFile (GetStdHandle(STD_INPUT_HANDLE), pausestr, sizeof(pausestr),
208 &count, NULL);
211 /* Now do the hard work... */
212 rc = XCOPY_DoCopy(sourcestem, sourcespec,
213 destinationstem, destinationspec,
214 flags);
216 /* Finished - print trailer and exit */
217 if (flags & OPT_SIMULATE) {
218 printf("%d file(s) would be copied\n", filesCopied);
219 } else if (!(flags & OPT_NOCOPY)) {
220 printf("%d file(s) copied\n", filesCopied);
222 if (rc == RC_OK && filesCopied == 0) rc = RC_NOFILES;
223 return rc;
228 /* =========================================================================
229 XCOPY_ProcessSourceParm - Takes the supplied source parameter, and
230 converts it into a stem and a filespec
231 ========================================================================= */
232 static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem, WCHAR *spec)
234 WCHAR actualsource[MAX_PATH];
235 WCHAR *starPos;
236 WCHAR *questPos;
239 * Validate the source, expanding to full path ensuring it exists
241 if (GetFullPathName(suppliedsource, MAX_PATH, actualsource, NULL) == 0) {
242 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
243 return RC_INITERROR;
247 * Work out the stem of the source
250 /* If no wildcard were supplied then the source is either a single
251 file or a directory - in which case thats the stem of the search,
252 otherwise split off the wildcards and use the higher level as the
253 stem */
254 lstrcpyW(stem, actualsource);
255 starPos = wcschr(stem, '*');
256 questPos = wcschr(stem, '?');
257 if (starPos || questPos) {
258 WCHAR *lastDir;
260 if (starPos) *starPos = 0x00;
261 if (questPos) *questPos = 0x00;
263 lastDir = wcsrchr(stem, '\\');
264 if (lastDir) *(lastDir+1) = 0x00;
265 else {
266 WINE_FIXME("Unexpected syntax error in source parameter\n");
267 return RC_INITERROR;
269 lstrcpyW(spec, actualsource + (lastDir - stem)+1);
270 } else {
272 DWORD attribs = GetFileAttributes(actualsource);
274 if (attribs == INVALID_FILE_ATTRIBUTES) {
275 LPWSTR lpMsgBuf;
276 DWORD lastError = GetLastError();
277 int status;
278 status = FormatMessage (FORMAT_MESSAGE_ALLOCATE_BUFFER |
279 FORMAT_MESSAGE_FROM_SYSTEM,
280 NULL, lastError, 0, (LPWSTR) &lpMsgBuf, 0, NULL);
281 printf("%S\n", lpMsgBuf);
282 return RC_INITERROR;
284 /* Directory: */
285 } else if (attribs & FILE_ATTRIBUTE_DIRECTORY) {
286 lstrcatW(stem, wchr_slash);
287 lstrcpyW(spec, wchr_star);
289 /* File: */
290 } else {
291 WCHAR drive[MAX_PATH];
292 WCHAR dir[MAX_PATH];
293 WCHAR fname[MAX_PATH];
294 WCHAR ext[MAX_PATH];
295 _wsplitpath(actualsource, drive, dir, fname, ext);
296 lstrcpyW(stem, drive);
297 lstrcatW(stem, dir);
298 lstrcpyW(spec, fname);
299 lstrcatW(spec, ext);
302 return RC_OK;
305 /* =========================================================================
306 XCOPY_ProcessDestParm - Takes the supplied destination parameter, and
307 converts it into a stem
308 ========================================================================= */
309 static int XCOPY_ProcessDestParm(WCHAR *supplieddestination, WCHAR *stem, WCHAR *spec,
310 WCHAR *srcspec, DWORD flags)
312 WCHAR actualdestination[MAX_PATH];
313 DWORD attribs;
314 BOOL isDir = FALSE;
317 * Validate the source, expanding to full path ensuring it exists
319 if (GetFullPathName(supplieddestination, MAX_PATH, actualdestination, NULL) == 0) {
320 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
321 return RC_INITERROR;
324 /* Destination is either a directory or a file */
325 attribs = GetFileAttributes(actualdestination);
327 if (attribs == INVALID_FILE_ATTRIBUTES) {
329 /* If /I supplied and wildcard copy, assume directory */
330 if (flags & OPT_ASSUMEDIR &&
331 (wcschr(srcspec, '?') || wcschr(srcspec, '*'))) {
333 isDir = TRUE;
335 } else {
336 DWORD count;
337 char answer[10] = "";
339 while (answer[0] != 'F' && answer[0] != 'D') {
340 printf("Is %S a filename or directory\n"
341 "on the target?\n"
342 "(F - File, D - Directory)\n", supplieddestination);
344 ReadFile(GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer), &count, NULL);
345 WINE_TRACE("User answer %c\n", answer[0]);
347 answer[0] = toupper(answer[0]);
350 if (answer[0] == 'D') {
351 isDir = TRUE;
352 } else {
353 isDir = FALSE;
356 } else {
357 isDir = (attribs & FILE_ATTRIBUTE_DIRECTORY);
360 if (isDir) {
361 lstrcpyW(stem, actualdestination);
362 *spec = 0x00;
364 /* Ensure ends with a '\' */
365 if (stem[lstrlenW(stem)-1] != '\\') {
366 lstrcatW(stem, wchr_slash);
369 } else {
370 WCHAR drive[MAX_PATH];
371 WCHAR dir[MAX_PATH];
372 WCHAR fname[MAX_PATH];
373 WCHAR ext[MAX_PATH];
374 _wsplitpath(actualdestination, drive, dir, fname, ext);
375 lstrcpyW(stem, drive);
376 lstrcatW(stem, dir);
377 lstrcpyW(spec, fname);
378 lstrcatW(spec, ext);
380 return RC_OK;
383 /* =========================================================================
384 XCOPY_DoCopy - Recursive function to copy files based on input parms
385 of a stem and a spec
387 This works by using FindFirstFile supplying the source stem and spec.
388 If results are found, any non-directory ones are processed
389 Then, if /S or /E is supplied, another search is made just for
390 directories, and this function is called again for that directory
392 ========================================================================= */
393 static int XCOPY_DoCopy(WCHAR *srcstem, WCHAR *srcspec,
394 WCHAR *deststem, WCHAR *destspec,
395 DWORD flags)
397 WIN32_FIND_DATA *finddata;
398 HANDLE h;
399 BOOL findres = TRUE;
400 WCHAR *inputpath, *outputpath;
401 BOOL copiedFile = FALSE;
402 DWORD destAttribs, srcAttribs;
403 BOOL skipFile;
404 LPVOID lpMsgBuf;
405 DWORD error_code;
406 int status;
408 /* Allocate some working memory on heap to minimize footprint */
409 finddata = HeapAlloc(GetProcessHeap(), 0, sizeof(WIN32_FIND_DATA));
410 inputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
411 outputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
413 /* Build the search info into a single parm */
414 lstrcpyW(inputpath, srcstem);
415 lstrcatW(inputpath, srcspec);
417 /* Search 1 - Look for matching files */
418 h = FindFirstFile(inputpath, finddata);
419 while (h != INVALID_HANDLE_VALUE && findres) {
421 skipFile = FALSE;
423 /* Ignore . and .. */
424 if (lstrcmpW(finddata->cFileName, wchr_dot)==0 ||
425 lstrcmpW(finddata->cFileName, wchr_dotdot)==0 ||
426 finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
428 WINE_TRACE("Skipping directory, . or .. (%s)\n", wine_dbgstr_w(finddata->cFileName));
429 } else {
431 /* Get the filename information */
432 lstrcpyW(copyFrom, srcstem);
433 if (flags & OPT_SHORTNAME) {
434 lstrcatW(copyFrom, finddata->cAlternateFileName);
435 } else {
436 lstrcatW(copyFrom, finddata->cFileName);
439 lstrcpyW(copyTo, deststem);
440 if (*destspec == 0x00) {
441 if (flags & OPT_SHORTNAME) {
442 lstrcatW(copyTo, finddata->cAlternateFileName);
443 } else {
444 lstrcatW(copyTo, finddata->cFileName);
446 } else {
447 lstrcatW(copyTo, destspec);
450 /* Do the copy */
451 WINE_TRACE("ACTION: Copy '%s' -> '%s'\n", wine_dbgstr_w(copyFrom),
452 wine_dbgstr_w(copyTo));
453 if (!copiedFile && !(flags & OPT_SIMULATE)) XCOPY_CreateDirectory(deststem);
455 /* See if allowed to copy it */
456 srcAttribs = GetFileAttributesW(copyFrom);
457 if ((srcAttribs & FILE_ATTRIBUTE_HIDDEN) ||
458 (srcAttribs & FILE_ATTRIBUTE_SYSTEM)) {
460 if (!(flags & OPT_COPYHIDSYS)) {
461 skipFile = TRUE;
465 /* Prompt each file if necessary */
466 if (!skipFile && (flags & OPT_SRCPROMPT)) {
467 DWORD count;
468 char answer[10];
469 BOOL answered = FALSE;
471 while (!answered) {
472 printf("%S? (Yes|No)\n", copyFrom);
473 ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
474 &count, NULL);
476 answered = TRUE;
477 if (toupper(answer[0]) == 'N')
478 skipFile = TRUE;
479 else if (toupper(answer[0]) != 'Y')
480 answered = FALSE;
484 /* See if file exists */
485 destAttribs = GetFileAttributesW(copyTo);
486 if (!skipFile &&
487 destAttribs != INVALID_FILE_ATTRIBUTES && !(flags & OPT_NOPROMPT)) {
488 DWORD count;
489 char answer[10];
490 BOOL answered = FALSE;
492 while (!answered) {
493 printf("Overwrite %S? (Yes|No|All)\n", copyTo);
494 ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
495 &count, NULL);
497 answered = TRUE;
498 if (toupper(answer[0]) == 'A')
499 flags |= OPT_NOPROMPT;
500 else if (toupper(answer[0]) == 'N')
501 skipFile = TRUE;
502 else if (toupper(answer[0]) != 'Y')
503 answered = FALSE;
507 /* See if it has to exist! */
508 if (destAttribs == INVALID_FILE_ATTRIBUTES && (flags & OPT_MUSTEXIST)) {
509 skipFile = TRUE;
512 /* Output a status message */
513 if (!skipFile) {
514 if (flags & OPT_QUIET) {
515 /* Skip message */
516 } else if (flags & OPT_FULL) {
517 printf("%S -> %S\n", copyFrom, copyTo);
518 } else {
519 printf("%S\n", copyFrom);
522 /* If allowing overwriting of read only files, remove any
523 write protection */
524 if ((destAttribs & FILE_ATTRIBUTE_READONLY) &&
525 (flags & OPT_REPLACEREAD)) {
526 SetFileAttributes(copyTo, destAttribs & ~FILE_ATTRIBUTE_READONLY);
529 copiedFile = TRUE;
530 if (flags & OPT_SIMULATE || flags & OPT_NOCOPY) {
531 /* Skip copy */
532 } else if (CopyFile(copyFrom, copyTo, FALSE) == 0) {
533 printf("Copying of '%S' to '%S' failed with r/c %d\n",
534 copyFrom, copyTo, GetLastError());
536 error_code = GetLastError ();
537 status = FormatMessage (FORMAT_MESSAGE_ALLOCATE_BUFFER |
538 FORMAT_MESSAGE_FROM_SYSTEM,
539 NULL, error_code, 0,
540 (LPTSTR) &lpMsgBuf, 0, NULL);
541 if (!status) {
542 WINE_FIXME("FIXME: Cannot display message for error %d, status %d\n",
543 error_code, GetLastError());
544 } else {
545 printf("%S\n", lpMsgBuf);
546 LocalFree ((HLOCAL)lpMsgBuf);
548 if (flags & OPT_IGNOREERRORS) {
549 skipFile = TRUE;
550 } else {
551 return RC_WRITEERROR;
554 filesCopied++;
558 /* Find next file */
559 findres = FindNextFile(h, finddata);
561 FindClose(h);
563 /* Search 2 - do subdirs */
564 if (flags & OPT_RECURSIVE) {
565 lstrcpyW(inputpath, srcstem);
566 lstrcatW(inputpath, wchr_star);
567 findres = TRUE;
568 WINE_TRACE("Processing subdirs with spec: %s\n", wine_dbgstr_w(inputpath));
570 h = FindFirstFile(inputpath, finddata);
571 while (h != INVALID_HANDLE_VALUE && findres) {
573 /* Only looking for dirs */
574 if ((finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
575 (lstrcmpW(finddata->cFileName, wchr_dot) != 0) &&
576 (lstrcmpW(finddata->cFileName, wchr_dotdot) != 0)) {
578 WINE_TRACE("Handling subdir: %s\n", wine_dbgstr_w(finddata->cFileName));
580 /* Make up recursive information */
581 lstrcpyW(inputpath, srcstem);
582 lstrcatW(inputpath, finddata->cFileName);
583 lstrcatW(inputpath, wchr_slash);
585 lstrcpyW(outputpath, deststem);
586 if (*destspec == 0x00) {
587 lstrcatW(outputpath, finddata->cFileName);
589 /* If /E is supplied, create the directory now */
590 if ((flags & OPT_EMPTYDIR) &&
591 !(flags & OPT_SIMULATE))
592 XCOPY_CreateDirectory(outputpath);
594 lstrcatW(outputpath, wchr_slash);
597 XCOPY_DoCopy(inputpath, srcspec, outputpath, destspec, flags);
600 /* Find next one */
601 findres = FindNextFile(h, finddata);
605 /* free up memory */
606 HeapFree(GetProcessHeap(), 0, finddata);
607 HeapFree(GetProcessHeap(), 0, inputpath);
608 HeapFree(GetProcessHeap(), 0, outputpath);
610 return 0;
613 /* =========================================================================
614 * Routine copied from cmd.exe md command -
615 * This works recursivly. so creating dir1\dir2\dir3 will create dir1 and
616 * dir2 if they do not already exist.
617 * ========================================================================= */
618 static BOOL XCOPY_CreateDirectory(const WCHAR* path)
620 int len;
621 WCHAR *new_path;
622 BOOL ret = TRUE;
624 new_path = HeapAlloc(GetProcessHeap(),0, sizeof(WCHAR) * (lstrlenW(path)+1));
625 lstrcpyW(new_path,path);
627 while ((len = lstrlenW(new_path)) && new_path[len - 1] == '\\')
628 new_path[len - 1] = 0;
630 while (!CreateDirectory(new_path,NULL))
632 WCHAR *slash;
633 DWORD last_error = GetLastError();
634 if (last_error == ERROR_ALREADY_EXISTS)
635 break;
637 if (last_error != ERROR_PATH_NOT_FOUND)
639 ret = FALSE;
640 break;
643 if (!(slash = wcsrchr(new_path,'\\')) && ! (slash = wcsrchr(new_path,'/')))
645 ret = FALSE;
646 break;
649 len = slash - new_path;
650 new_path[len] = 0;
651 if (!XCOPY_CreateDirectory(new_path))
653 ret = FALSE;
654 break;
656 new_path[len] = '\\';
658 HeapFree(GetProcessHeap(),0,new_path);
659 return ret;