fix errorlevel for copy. left to do is /D = copy encrypte file and decypte to destina...
[reactos.git] / reactos / subsys / system / cmd / copy.c
1 /*
2 * COPY.C -- copy internal command.
3 *
4 *
5 * History:
6 *
7 * 01-Aug-98 (Rob Lake z63rrl@morgan.ucs.mun.ca)
8 * started
9 *
10 * 13-Aug-1998 (John P. Price)
11 * fixed memory leak problem in copy function.
12 * fixed copy function so it would work with wildcards in the source
13 *
14 * 13-Dec-1998 (Eric Kohl <ekohl@abo.rhein-zeitung.de>)
15 * Added COPY command to CMD.
16 *
17 * 26-Jan-1998 (Eric Kohl <ekohl@abo.rhein-zeitung.de>)
18 * Replaced CRT io functions by Win32 io functions.
19 *
20 * 27-Oct-1998 (Eric Kohl <ekohl@abo.rhein-zeitung.de>)
21 * Disabled prompting when used in batch mode.
22 *
23 * 03-Apr-2005 (Magnus Olsen) <magnus@greatlord.com>)
24 * Remove all hardcode string to En.rc
25 *
26 * 13-Jul-2005 (Brandon Turner) <turnerb7@msu.edu>)
27 * Rewrite to clean up copy and support wildcard.
28 */
29
30 #include <precomp.h>
31 #include "resource.h"
32
33 #ifdef INCLUDE_CMD_COPY
34
35 enum
36 {
37 COPY_ASCII = 0x001, /* /A */
38 COPY_DECRYPT = 0x004, /* /D : Not Impleneted */
39 COPY_VERIFY = 0x008, /* /V : Dummy, Never will be Impleneted */
40 COPY_SHORTNAME = 0x010, /* /N : Not Impleneted */
41 COPY_NO_PROMPT = 0x020, /* /Y */
42 COPY_PROMPT = 0x040, /* /-Y */
43 COPY_RESTART = 0x080, /* /Z : Not Impleneted */
44 COPY_BINARY = 0x100, /* /B */
45 };
46
47 #define BUFF_SIZE 16384 /* 16k = max buffer size */
48
49
50 int copy (LPTSTR source, LPTSTR dest, int append, DWORD lpdwFlags)
51 {
52 TCHAR szMsg[RC_STRING_MAX_SIZE];
53 FILETIME srctime;
54 HANDLE hFileSrc;
55 HANDLE hFileDest;
56 LPBYTE buffer;
57 DWORD dwAttrib;
58 DWORD dwRead;
59 DWORD dwWritten;
60 DWORD i;
61 BOOL bEof = FALSE;
62
63 #ifdef _DEBUG
64 DebugPrintf (_T("checking mode\n"));
65 #endif
66
67 dwAttrib = GetFileAttributes (source);
68
69 hFileSrc = CreateFile (source, GENERIC_READ, FILE_SHARE_READ,
70 NULL, OPEN_EXISTING, 0, NULL);
71 if (hFileSrc == INVALID_HANDLE_VALUE)
72 {
73 LoadString(CMD_ModuleHandle, STRING_COPY_ERROR1, szMsg, RC_STRING_MAX_SIZE);
74 ConOutPrintf(szMsg, source);
75 nErrorLevel = 1;
76 return 0;
77 }
78
79 #ifdef _DEBUG
80 DebugPrintf (_T("getting time\n"));
81 #endif
82
83 GetFileTime (hFileSrc, &srctime, NULL, NULL);
84
85 #ifdef _DEBUG
86 DebugPrintf (_T("copy: flags has %s\n"),
87 *lpdwFlags & ASCII ? "ASCII" : "BINARY");
88 #endif
89
90 if (!IsExistingFile (dest))
91 {
92 #ifdef _DEBUG
93 DebugPrintf (_T("opening/creating\n"));
94 #endif
95 hFileDest =
96 CreateFile (dest, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);
97 }
98 else if (!append)
99 {
100 if (!_tcscmp (dest, source))
101 {
102 LoadString(CMD_ModuleHandle, STRING_COPY_ERROR2, szMsg, RC_STRING_MAX_SIZE);
103 ConOutPrintf(szMsg, source);
104
105 CloseHandle (hFileSrc);
106 nErrorLevel = 1;
107 return 0;
108 }
109
110 #ifdef _DEBUG
111 DebugPrintf (_T("SetFileAttributes (%s, FILE_ATTRIBUTE_NORMAL);\n"), dest);
112 #endif
113 SetFileAttributes (dest, FILE_ATTRIBUTE_NORMAL);
114
115 #ifdef _DEBUG
116 DebugPrintf (_T("DeleteFile (%s);\n"), dest);
117 #endif
118 DeleteFile (dest);
119
120 hFileDest = CreateFile (dest, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);
121 }
122 else
123 {
124 LONG lFilePosHigh = 0;
125
126 if (!_tcscmp (dest, source))
127 {
128 CloseHandle (hFileSrc);
129 return 0;
130 }
131
132 #ifdef _DEBUG
133 DebugPrintf (_T("opening/appending\n"));
134 #endif
135 SetFileAttributes (dest, FILE_ATTRIBUTE_NORMAL);
136
137 hFileDest =
138 CreateFile (dest, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
139
140 /* Move to end of file to start writing */
141 SetFilePointer (hFileDest, 0, &lFilePosHigh,FILE_END);
142 }
143
144
145 if (hFileDest == INVALID_HANDLE_VALUE)
146 {
147 CloseHandle (hFileSrc);
148 error_path_not_found ();
149 return 0;
150 }
151 buffer = (LPBYTE)malloc (BUFF_SIZE);
152 if (buffer == NULL)
153 {
154 CloseHandle (hFileDest);
155 CloseHandle (hFileSrc);
156 error_out_of_memory ();
157 return 0;
158 }
159
160 do
161 {
162 ReadFile (hFileSrc, buffer, BUFF_SIZE, &dwRead, NULL);
163 if (lpdwFlags & COPY_ASCII)
164 {
165 for (i = 0; i < dwRead; i++)
166 {
167 if (((LPTSTR)buffer)[i] == 0x1A)
168 {
169 bEof = TRUE;
170 break;
171 }
172 }
173 dwRead = i;
174 }
175
176 if (dwRead == 0)
177 break;
178
179 WriteFile (hFileDest, buffer, dwRead, &dwWritten, NULL);
180 if (dwWritten != dwRead)
181 {
182 ConOutResPuts(STRING_COPY_ERROR3);
183
184 free (buffer);
185 CloseHandle (hFileDest);
186 CloseHandle (hFileSrc);
187 nErrorLevel = 1;
188 return 0;
189 }
190 }
191 while (dwRead && !bEof);
192
193 #ifdef _DEBUG
194 DebugPrintf (_T("setting time\n"));
195 #endif
196 SetFileTime (hFileDest, &srctime, NULL, NULL);
197
198 if (lpdwFlags & COPY_ASCII)
199 {
200 ((LPTSTR)buffer)[0] = 0x1A;
201 ((LPTSTR)buffer)[1] = _T('\0');
202 #ifdef _DEBUG
203 DebugPrintf (_T("appending ^Z\n"));
204 #endif
205 WriteFile (hFileDest, buffer, sizeof(TCHAR), &dwWritten, NULL);
206 }
207
208 free (buffer);
209 CloseHandle (hFileDest);
210 CloseHandle (hFileSrc);
211
212 #ifdef _DEBUG
213 DebugPrintf (_T("setting mode\n"));
214 #endif
215 SetFileAttributes (dest, dwAttrib);
216
217 return 1;
218 }
219
220
221 static INT Overwrite (LPTSTR fn)
222 {
223 /*ask the user if they want to override*/
224 TCHAR szMsg[RC_STRING_MAX_SIZE];
225 INT res;
226 LoadString(CMD_ModuleHandle, STRING_COPY_HELP1, szMsg, RC_STRING_MAX_SIZE);
227 ConOutPrintf(szMsg,fn);
228 res = FilePromptYNA ("");
229 return res;
230 }
231
232
233 INT cmd_copy (LPTSTR cmd, LPTSTR param)
234 {
235 TCHAR szMsg[RC_STRING_MAX_SIZE];
236 LPTSTR *arg;
237 INT argc, i, nFiles, nOverwrite = 0, nSrc = -1, nDes = -1;
238 /* this is the path up to the folder of the src and dest ie C:\windows\ */
239 TCHAR szDestPath[MAX_PATH];
240 TCHAR szSrcPath[MAX_PATH];
241 DWORD dwFlags = 0;
242 /* If this is the type of copy where we are adding files */
243 BOOL bAppend = FALSE;
244 WIN32_FIND_DATA findBuffer;
245 HANDLE hFile;
246 /* Used when something like "copy c*.exe d*.exe" during the process of
247 figuring out the new name */
248 TCHAR tmpName[MAX_PATH] = _T("");
249 /* Pointer to keep track of how far through the append input(file1+file2+file3) we are */
250 TCHAR * appendPointer = _T("\0");
251 /* The full path to src and dest. This has drive letter, folders, and filename */
252 TCHAR tmpDestPath[MAX_PATH];
253 TCHAR tmpSrcPath[MAX_PATH];
254 /* A bool on weather or not the destination name will be taking from the input */
255 BOOL bSrcName = FALSE;
256 /* Seems like a waste but it is a pointer used to copy from input to PreserveName */
257 TCHAR * UseThisName;
258 /* Stores the name( i.e. blah.txt or blah*.txt) which later we might need */
259 TCHAR PreserveName[MAX_PATH];
260 /* for CMDCOPY env */
261 TCHAR *evar;
262 int size;
263
264
265 /*Show help/usage info*/
266 if (!_tcsncmp (param, _T("/?"), 2))
267 {
268 ConOutResPaging(TRUE, STRING_COPY_HELP2);
269 return 0;
270 }
271
272 nErrorLevel = 0;
273
274 /* Get the envor value if it exists */
275 evar = malloc(512);
276 size = GetEnvironmentVariable (_T("COPYCMD"), evar, 512);
277 if (size > 512)
278 {
279 evar = realloc(evar,size * sizeof(TCHAR) );
280 if (evar!=NULL)
281 {
282 size = GetEnvironmentVariable (_T("COPYCMD"), evar, size);
283 }
284 }
285 /* check see if we did get any env variable */
286 if (size !=0)
287 {
288 int t=0;
289 /* scan and set the flags */
290 for (t=0;t<size;t++)
291 {
292 if (_tcsncicmp(_T("/A"),&evar[t],2)==0)
293 {
294 evar[t]=_T(' ');
295 evar[t+1]=_T(' ');
296 }
297
298 else if (_tcsncicmp(_T("/B"),&evar[t],2)==0)
299 {
300 dwFlags |= COPY_BINARY;
301 evar[t]=_T(' ');
302 evar[t+1]=_T(' ');
303 }
304 else if (_tcsncicmp(_T("/D"),&evar[t],2)==0)
305 {
306 dwFlags |= COPY_DECRYPT;
307 evar[t]=_T(' ');
308 evar[t+1]=_T(' ');
309 }
310
311 else if (_tcsncicmp(_T("/V"),&evar[t],2)==0)
312 {
313 dwFlags |= COPY_VERIFY;
314 evar[t]=_T(' ');
315 evar[t+1]=_T(' ');
316 }
317
318 else if (_tcsncicmp(_T("/V"),&evar[t],2)==0)
319 {
320 dwFlags |= COPY_SHORTNAME;
321 evar[t]=_T(' ');
322 evar[t+1]=_T(' ');
323 }
324
325 else if (_tcsncicmp(_T("/Y"),&evar[t],2)==0)
326 {
327 dwFlags |= COPY_NO_PROMPT;
328 evar[t]=_T(' ');
329 evar[t+1]=_T(' ');
330 }
331
332 else if (_tcsncicmp(_T("/-Y"),&evar[t],3)==0)
333 {
334 dwFlags |= COPY_PROMPT;
335 evar[t]=_T(' ');
336 evar[t+1]=_T(' ');
337 evar[t+2]=_T(' ');
338 }
339
340 else if (_tcsncicmp(_T("/Z"),&evar[t],2)==0)
341 {
342 dwFlags |= COPY_PROMPT;
343 evar[t]=_T(' ');
344 evar[t+1]=_T(' ');
345 }
346 }
347 }
348 free(evar);
349
350
351 /*Split the user input into array*/
352 arg = split (param, &argc, FALSE);
353 nFiles = argc;
354
355
356 /*Read switches and count files*/
357 for (i = 0; i < argc; i++)
358 {
359 if (*arg[i] == _T('/'))
360 {
361 if (_tcslen(arg[i]) >= 2)
362 {
363 switch (_totupper(arg[i][1]))
364 {
365
366 case _T('A'):
367 dwFlags |= COPY_ASCII;
368 break;
369
370 case _T('B'):
371 dwFlags |= COPY_BINARY;
372 break;
373
374 case _T('D'):
375 dwFlags |= COPY_DECRYPT;
376 break;
377
378 case _T('V'):
379 dwFlags |= COPY_VERIFY;
380 break;
381
382 case _T('N'):
383 dwFlags |= COPY_SHORTNAME;
384 break;
385
386 case _T('Y'):
387 dwFlags |= COPY_NO_PROMPT;
388 dwFlags &= ~COPY_PROMPT;
389 break;
390
391 case _T('-'):
392 if(_tcslen(arg[i]) >= 3)
393 if(_totupper(arg[i][2]) == _T('Y'))
394 {
395 dwFlags &= ~COPY_NO_PROMPT;
396 dwFlags |= COPY_PROMPT;
397 }
398
399 break;
400
401 case _T('Z'):
402 dwFlags |= COPY_RESTART;
403 break;
404
405 default:
406 /* invaild switch */
407 error_invalid_switch(_totupper(arg[i][1]));
408 return 1;
409 break;
410 }
411 }
412 /*If it was a switch, subtract from total arguments*/
413 nFiles--;
414 }
415 else
416 {
417 /*if it isnt a switch then it is the source or destination*/
418 if(nSrc == -1)
419 nSrc = i;
420 else if(nDes == -1)
421 nDes = i;
422
423 }
424 }
425
426 if(nFiles < 1)
427 {
428 /* There is not enough files, there has to be at least 1 */
429 error_req_param_missing();
430 return 1;
431 }
432
433 if(nFiles > 2)
434 {
435 /* there is too many file names in command */
436 error_too_many_parameters("");
437 return 1;
438 }
439
440 if((nDes != -1) &&
441 ((_tcschr (arg[nSrc], _T('+')) != NULL) ||
442 (_tcschr (arg[nSrc], _T('*')) != NULL && _tcschr (arg[nDes], _T('*')) == NULL) ||
443 (IsExistingDirectory (arg[nSrc]) && (_tcschr (arg[nDes], _T('*')) == NULL && !IsExistingDirectory (arg[nDes])))
444 ))
445 {
446 /* There is a + in the source filename, this means
447 that there is more then one file being put into
448 one file. */
449 bAppend = TRUE;
450 if(_tcschr (arg[nSrc], _T('+')) != NULL)
451 appendPointer = arg[nSrc];
452 }
453
454 /* Reusing the number of files variable */
455 nFiles = 0;
456
457 do
458 {
459
460 /* Set up the string that is the path to the destination */
461 if(nDes != -1)
462 {
463 /* Check to make sure if they entered c:, if they do then GFPN
464 return current directory even though msdn says it will return c:\ */
465 if(_tcslen(arg[nDes]) == 2)
466 {
467 if(arg[nDes][1] == _T(':'))
468 {
469 _tcscpy (szDestPath, arg[nDes]);
470 _tcscat (szDestPath, _T("\\"));
471 }
472 }
473 else
474 /* If the user entered two file names then form the full string path*/
475 GetFullPathName (arg[nDes], MAX_PATH, szDestPath, NULL);
476
477 }
478 else
479 {
480 /* If no destination was entered then just use
481 the current directory as the destination */
482 GetCurrentDirectory (MAX_PATH, szDestPath);
483 }
484
485
486 /* Get the full string of the path to the source file*/
487 if(_tcschr (arg[nSrc], _T('+')) != NULL)
488 {
489
490 _tcscpy(tmpName,_T("\0"));
491 /* Loop through the source file name and copy all
492 the chars one at a time until it gets too + */
493 while(TRUE)
494 {
495
496 if(!_tcsncmp (appendPointer,_T("+"),1) || !_tcsncmp (appendPointer,_T("\0"),1))
497 {
498 /* Now that the pointer is on the + we
499 need to go to the start of the next filename */
500 if(!_tcsncmp (appendPointer,_T("+"),1))
501 appendPointer++;
502 break;
503 }
504 _tcsncat(tmpName,appendPointer,1);
505 appendPointer++;
506 }
507 /* Finish the string off with a null char */
508 _tcsncat(tmpName,_T("\0"),1);
509 /* Check to make sure if they entered c:, if they do then GFPN
510 return current directory even though msdn says it will return c:\ */
511 if(_tcslen(tmpName) == 2)
512 {
513 if(tmpName[1] == _T(':'))
514 {
515 _tcscpy (szSrcPath, tmpName);
516 _tcscat (szSrcPath, _T("\\"));
517 }
518 }
519 else
520 /* Get the full path to first file in the string of file names */
521 GetFullPathName (tmpName, MAX_PATH, szSrcPath, NULL);
522 }
523 else
524 {
525 /* Check to make sure if they entered c:, if they do then GFPN
526 return current directory even though msdn says it will return c:\ */
527 if(_tcslen(arg[nSrc]) == 2)
528 {
529 if(arg[nSrc][1] == _T(':'))
530 {
531 _tcscpy (szSrcPath, arg[nSrc]);
532 _tcscat (szSrcPath, _T("\\"));
533 }
534 }
535 else
536 /* Get the full path of the source file */
537 GetFullPathName (arg[nSrc], MAX_PATH, szSrcPath, NULL);
538
539 }
540
541 /* From this point on, we can assume that the shortest path is 3 letters long
542 and that would be [DriveLetter]:\ */
543
544 /* If there is no * in the path name and it is a folder
545 then we will need to add a wildcard to the pathname
546 so FindFirstFile comes up with all the files in that
547 folder */
548 if(_tcschr (szSrcPath, _T('*')) == NULL &&
549 IsExistingDirectory (szSrcPath))
550 {
551 /* If it doesnt have a \ at the end already then on needs to be added */
552 if(szSrcPath[_tcslen(szSrcPath) - 1] != _T('\\'))
553 _tcscat (szSrcPath, _T("\\"));
554 /* Add a wildcard after the \ */
555 _tcscat (szSrcPath, _T("*"));
556 }
557 /* Make sure there is an ending slash to the path if the dest is a folder */
558 if(_tcschr (szDestPath, _T('*')) == NULL &&
559 IsExistingDirectory(szDestPath))
560 {
561 if(szDestPath[_tcslen(szDestPath) - 1] != _T('\\'))
562 _tcscat (szDestPath, _T("\\"));
563 }
564
565
566 /* Get a list of all the files */
567 hFile = FindFirstFile (szSrcPath, &findBuffer);
568
569
570 /* We need to figure out what the name of the file in the is going to be */
571 if((szDestPath[_tcslen(szDestPath) - 1] == _T('*') && szDestPath[_tcslen(szDestPath) - 2] == _T('\\')) ||
572 szDestPath[_tcslen(szDestPath) - 1] == _T('\\'))
573 {
574 /* In this case we will be using the same name as the source file
575 for the destination file because destination is a folder */
576 bSrcName = TRUE;
577 }
578 else
579 {
580 /* Save the name the user entered */
581 UseThisName = _tcsrchr(szDestPath,_T('\\'));
582 UseThisName++;
583 _tcscpy(PreserveName,UseThisName);
584 }
585
586 /* Strip the paths back to the folder they are in */
587 for(i = (_tcslen(szSrcPath) - 1); i > -1; i--)
588 if(szSrcPath[i] != _T('\\'))
589 szSrcPath[i] = _T('\0');
590 else
591 break;
592
593 for(i = (_tcslen(szDestPath) - 1); i > -1; i--)
594 if(szDestPath[i] != _T('\\'))
595 szDestPath[i] = _T('\0');
596 else
597 break;
598
599
600 do
601 {
602 /* Set the override to yes each new file */
603 nOverwrite = 1;
604
605 /* If it couldnt open the file handle, print out the error */
606 if(hFile == INVALID_HANDLE_VALUE)
607 {
608 ConOutFormatMessage (GetLastError(), szSrcPath);
609 freep (arg);
610 return 1;
611 }
612
613 /* Ignore the . and .. files */
614 if(!_tcscmp (findBuffer.cFileName, _T(".")) ||
615 !_tcscmp (findBuffer.cFileName, _T(".."))||
616 findBuffer.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
617 continue;
618
619 /* Copy the base folder over to a tmp string */
620 _tcscpy(tmpDestPath,szDestPath);
621
622 /* Can't put a file into a folder that isnt there */
623 if(!IsExistingDirectory(szDestPath))
624 {
625 ConOutFormatMessage (GetLastError (), szSrcPath);
626 freep (arg);
627 return 1;
628 }
629 /* Copy over the destination path name */
630 if(bSrcName)
631 _tcscat (tmpDestPath, findBuffer.cFileName);
632 else
633 {
634 /* If there is no wildcard you can use the name the user entered */
635 if(_tcschr (PreserveName, _T('*')) == NULL)
636 {
637 _tcscat (tmpDestPath, PreserveName);
638 }
639 else
640 {
641 /* The following lines of copy were written by someone else
642 (most likely Eric Khoul) and it was taken from ren.c */
643 LPTSTR p,q,r;
644 TCHAR DoneFile[MAX_PATH];
645 /* build destination file name */
646 p = findBuffer.cFileName;
647 q = PreserveName;
648 r = DoneFile;
649 while(*q != 0)
650 {
651 if (*q == '*')
652 {
653 q++;
654 while (*p != 0 && *p != *q)
655 {
656 *r = *p;
657 p++;
658 r++;
659 }
660 }
661 else if (*q == '?')
662 {
663 q++;
664 if (*p != 0)
665 {
666 *r = *p;
667 p++;
668 r++;
669 }
670 }
671 else
672 {
673 *r = *q;
674 if (*p != 0)
675 p++;
676 q++;
677 r++;
678 }
679 }
680 *r = 0;
681 /* Add the filename to the tmp string path */
682 _tcscat (tmpDestPath, DoneFile);
683
684 }
685 }
686
687
688 /* Build the string path to the source file */
689 _tcscpy(tmpSrcPath,szSrcPath);
690 _tcscat (tmpSrcPath, findBuffer.cFileName);
691
692 /* Check to see if the file is the same file */
693 if(!_tcscmp (tmpSrcPath, tmpDestPath))
694 continue;
695
696 /* Handle any overriding / prompting that needs to be done */
697 if((!(dwFlags & COPY_NO_PROMPT) && IsExistingFile (tmpDestPath)) || dwFlags & COPY_PROMPT)
698 nOverwrite = Overwrite(tmpDestPath);
699 if(nOverwrite == PROMPT_NO || nOverwrite == PROMPT_BREAK)
700 continue;
701 if(nOverwrite == PROMPT_ALL || (nOverwrite == PROMPT_YES && bAppend))
702 dwFlags |= COPY_NO_PROMPT;
703
704 /* Tell weather the copy was successful or not */
705 if(copy(tmpSrcPath,tmpDestPath, bAppend, dwFlags))
706 {
707 nFiles++;
708 /* only print source name when more then one file */
709 if(_tcschr (arg[nSrc], _T('+')) != NULL || _tcschr (arg[nSrc], _T('*')) != NULL)
710 ConOutPrintf("%s\n",findBuffer.cFileName);
711 //LoadString(CMD_ModuleHandle, STRING_MOVE_ERROR1, szMsg, RC_STRING_MAX_SIZE);
712 }
713 else
714 {
715 /* print out the error message */
716 LoadString(CMD_ModuleHandle, STRING_COPY_ERROR3, szMsg, RC_STRING_MAX_SIZE);
717 ConOutPrintf(szMsg);
718 nErrorLevel = 1;
719 }
720
721 /* Loop through all wildcard files */
722 }while(FindNextFile (hFile, &findBuffer));
723 /* Loop through all files in src string with a + */
724 }while(_tcsncmp (appendPointer,_T("\0"),1));
725
726 /* print out the number of files copied */
727 LoadString(CMD_ModuleHandle, STRING_COPY_FILE, szMsg, RC_STRING_MAX_SIZE);
728 ConOutPrintf(szMsg, nFiles);
729
730 CloseHandle(hFile);
731 freep (arg);
732 return 0;
733 }
734
735
736 #endif /* INCLUDE_CMD_COPY */