[PATCH v20 0/3] MR8200: cmd: COPY should output file names as they are copied.
In native Windows, the COPY command will display the names of the files as they are copied. Wine should do the same. This change enables that. -- v20: cmd: Allow '+' as delimiter for tab-completion, e.g. 'copy file+file2'. https://gitlab.winehq.org/wine/wine/-/merge_requests/8200
From: Joe Souza <jsouza(a)yahoo.com> --- programs/cmd/builtins.c | 29 +++++++++++++++++++++++++++-- programs/cmd/cmd.rc | 1 + programs/cmd/wcmd.h | 1 + 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/programs/cmd/builtins.c b/programs/cmd/builtins.c index b78ae9d293b..c2379598976 100644 --- a/programs/cmd/builtins.c +++ b/programs/cmd/builtins.c @@ -656,6 +656,7 @@ RETURN_CODE WCMD_copy(WCHAR * args) WCHAR copycmd[4]; DWORD len; BOOL dstisdevice = FALSE; + unsigned numcopied = 0; typedef struct _COPY_FILES { @@ -954,6 +955,8 @@ RETURN_CODE WCMD_copy(WCHAR * args) WCHAR *filenamepart; DWORD attributes; BOOL srcisdevice = FALSE; + BOOL havewildcards = FALSE; + BOOL displaynames = anyconcats; /* Display names if we are concatenating. */ /* If it was not explicit, we now know whether we are concatenating or not and hence whether to copy as binary or ascii */ @@ -965,6 +968,13 @@ RETURN_CODE WCMD_copy(WCHAR * args) return errorlevel = ERROR_INVALID_FUNCTION; WINE_TRACE("Full src name is '%s'\n", wine_dbgstr_w(srcpath)); + havewildcards = wcspbrk(srcpath, L"*?") ? TRUE : FALSE; + /* If we are not already displaying file names due to concatenation, then display them + if using wildards. */ + if (!displaynames) { + displaynames = havewildcards; + } + /* If parameter is a directory, ensure it ends in \* */ attributes = GetFileAttributesW(srcpath); if (ends_with_backslash( srcpath )) { @@ -972,17 +982,19 @@ RETURN_CODE WCMD_copy(WCHAR * args) /* We need to know where the filename part starts, so append * and recalculate the full resulting path */ lstrcatW(thiscopy->name, L"*"); + displaynames = TRUE; if (!WCMD_get_fullpath(thiscopy->name, ARRAY_SIZE(srcpath), srcpath, &filenamepart)) return errorlevel = ERROR_INVALID_FUNCTION; WINE_TRACE("Directory, so full name is now '%s'\n", wine_dbgstr_w(srcpath)); - } else if ((wcspbrk(srcpath, L"*?") == NULL) && + } else if (!havewildcards && (attributes != INVALID_FILE_ATTRIBUTES) && (attributes & FILE_ATTRIBUTE_DIRECTORY)) { /* We need to know where the filename part starts, so append \* and recalculate the full resulting path */ lstrcatW(thiscopy->name, L"\\*"); + displaynames = TRUE; if (!WCMD_get_fullpath(thiscopy->name, ARRAY_SIZE(srcpath), srcpath, &filenamepart)) return errorlevel = ERROR_INVALID_FUNCTION; WINE_TRACE("Directory, so full name is now '%s'\n", wine_dbgstr_w(srcpath)); @@ -1064,6 +1076,10 @@ RETURN_CODE WCMD_copy(WCHAR * args) /* Do the copy as appropriate */ if (overwrite) { + if (displaynames) { + WCMD_output_asis(srcpath); + WCMD_output_asis(L"\r\n"); + } if (anyconcats && WCMD_IsSameFile(srcpath, outname)) { /* behavior is as Unix 'touch' (change last-written time only) */ HANDLE file = CreateFileW(srcpath, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, @@ -1097,7 +1113,12 @@ RETURN_CODE WCMD_copy(WCHAR * args) return_code = ERROR_INVALID_FUNCTION; } else { WINE_TRACE("Copied successfully\n"); - if (anyconcats) writtenoneconcat = TRUE; + if (anyconcats) { + writtenoneconcat = TRUE; + numcopied = 1; + } else { + numcopied++; + } /* Append EOF if ascii destination and we are not going to add more onto the end Note: Testing shows windows has an optimization whereas if you have a binary @@ -1135,6 +1156,10 @@ RETURN_CODE WCMD_copy(WCHAR * args) } } + if (numcopied) { + WCMD_output(WCMD_LoadMessage(WCMD_NUMCOPIED), numcopied); + } + /* Exit out of the routine, freeing any remaining allocated memory */ exitreturn: diff --git a/programs/cmd/cmd.rc b/programs/cmd/cmd.rc index 90091090e11..29d105fa7a2 100644 --- a/programs/cmd/cmd.rc +++ b/programs/cmd/cmd.rc @@ -406,4 +406,5 @@ Enter HELP <command> for further information on any of the above commands.\n" WCMD_BADTOKEN, "Syntax error: unexpected %1\n" WCMD_ENDOFLINE, "End of line" WCMD_ENDOFFILE, "End of file" + WCMD_NUMCOPIED, "%t%1!u! file(s) copied\n" } diff --git a/programs/cmd/wcmd.h b/programs/cmd/wcmd.h index 4c85d7ad8cb..2731743c27a 100644 --- a/programs/cmd/wcmd.h +++ b/programs/cmd/wcmd.h @@ -454,3 +454,4 @@ extern WCHAR version_string[]; #define WCMD_BADTOKEN 1047 #define WCMD_ENDOFLINE 1048 #define WCMD_ENDOFFILE 1049 +#define WCMD_NUMCOPIED 1050 -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/8200
From: Joe Souza <jsouza(a)yahoo.com> --- programs/cmd/tests/test_builtins.cmd | 79 ++++++++++++++++++------ programs/cmd/tests/test_builtins.cmd.exp | 42 +++++++++++++ 2 files changed, 102 insertions(+), 19 deletions(-) diff --git a/programs/cmd/tests/test_builtins.cmd b/programs/cmd/tests/test_builtins.cmd index 58d78194380..8ae733681eb 100644 --- a/programs/cmd/tests/test_builtins.cmd +++ b/programs/cmd/tests/test_builtins.cmd @@ -3416,6 +3416,30 @@ shift if not "%1"=="" goto :CheckNotExist goto :eof +:CheckOutputExist +find /i "%1" test1.txt >nul 2>&1 +if errorlevel 0 ( + echo Passed: Found expected %1 in COPY output +) else ( + echo Failed: Did not find expected %1 in COPY output +) +shift +if not "%1"=="" goto :CheckOutputExist +del /q test1.txt >nul 2>&1 +goto :eof + +:CheckOutputNotExist +find /i "%1" test1.txt >nul 2>&1 +if errorlevel 1 ( + echo Passed: Did not find %1 in COPY output +) else ( + echo Failed: Unexpectedly found %1 in COPY output +) +shift +if not "%1"=="" goto :CheckOutputNotExist +del /q test1.txt >nul 2>&1 +goto :eof + rem Note: No way to check file size on NT4 so skip the test :CheckFileSize if not exist "%1" ( @@ -3444,28 +3468,33 @@ rem ----------------------- rem Simple single file copy rem ----------------------- rem Simple single file copy, normally used syntax -copy file1 dummy.file >nul 2>&1 +copy file1 dummy.file >test1.txt 2>&1 if errorlevel 1 echo Incorrect errorlevel +call :CheckOutputNotExist file1 call :CheckExist dummy.file rem Simple single file copy, destination supplied as two forms of directory -copy file1 dir1 >nul 2>&1 +copy file1 dir1 >test1.txt 2>&1 if errorlevel 1 echo Incorrect errorlevel +call :CheckOutputNotExist file1 call :CheckExist dir1\file1 -copy file1 dir1\ >nul 2>&1 +copy file1 dir1\ >test1.txt 2>&1 if errorlevel 1 echo Incorrect errorlevel +call :CheckOutputNotExist file1 call :CheckExist dir1\file1 rem Simple single file copy, destination supplied as fully qualified destination -copy file1 dir1\file99 >nul 2>&1 +copy file1 dir1\file99 >test1.txt 2>&1 if errorlevel 1 echo Incorrect errorlevel +call :CheckOutputNotExist file1 call :CheckExist dir1\file99 rem Simple single file copy, destination not supplied cd dir1 -copy ..\file1 >nul 2>&1 +copy ..\file1 >test1.txt 2>&1 if errorlevel 1 echo Incorrect errorlevel +call :CheckOutputNotExist file1 call :CheckExist file1 cd .. @@ -3477,19 +3506,22 @@ call :CheckNotExist dir2 dir2\file1 rem ----------------------- rem Wildcarded copy rem ----------------------- -rem Simple single file copy, destination supplied as two forms of directory -copy file? dir1 >nul 2>&1 +rem Simple wildcarded file copy, destination supplied as two forms of directory +copy file? dir1 >test1.txt 2>&1 if errorlevel 1 echo Incorrect errorlevel +call :CheckOutputExist file1 file2 file3 call :CheckExist dir1\file1 dir1\file2 dir1\file3 -copy file* dir1\ >nul 2>&1 +copy file* dir1\ >test1.txt 2>&1 if errorlevel 1 echo Incorrect errorlevel +call :CheckOutputExist file1 file2 file3 call :CheckExist dir1\file1 dir1\file2 dir1\file3 -rem Simple single file copy, destination not supplied +rem Simple wildcarded file copy, destination not supplied cd dir1 -copy ..\file*.* >nul 2>&1 +copy ..\file*.* >test1.txt 2>&1 if errorlevel 1 echo Incorrect errorlevel +call :CheckOutputExist file1 file2 file3 call :CheckExist file1 file2 file3 cd .. @@ -3502,51 +3534,60 @@ rem ------------------------------------------------ rem Confirm overwrite works (cannot test prompting!) rem ------------------------------------------------ copy file1 testfile >nul 2>&1 -copy /y file2 testfile >nul 2>&1 +copy /y file2 testfile >test1.txt 2>&1 +call :CheckOutputNotExist file2 call :CheckExist testfile rem ------------------------------------------------ rem Test concatenation rem ------------------------------------------------ rem simple case, no wildcards -copy file1+file2 testfile >nul 2>&1 +copy file1+file2 testfile >test1.txt 2>&1 if errorlevel 1 echo Incorrect errorlevel +call :CheckOutputExist file1 file2 call :CheckExist testfile rem simple case, wildcards, no concatenation -copy file* testfile >nul 2>&1 +copy file* testfile >test1.txt 2>&1 if errorlevel 1 echo Incorrect errorlevel +call :CheckOutputExist file1 file2 file3 call :CheckExist testfile rem simple case, wildcards, and concatenation echo ddddd > fred -copy file*+fred testfile >nul 2>&1 +copy file*+fred testfile >test1.txt 2>&1 if errorlevel 1 echo Incorrect errorlevel +call :CheckOutputExist file1 file2 file3 fred call :CheckExist testfile rem simple case, wildcards, and concatenation -copy fred+file* testfile >nul 2>&1 +copy fred+file* testfile >test1.txt 2>&1 if errorlevel 1 echo Incorrect errorlevel +call :CheckOutputExist fred file1 file2 file3 call :CheckExist testfile rem Calculate destination name -copy fred+file* dir1 >nul 2>&1 +copy fred+file* dir1 >test1.txt 2>&1 if errorlevel 1 echo Incorrect errorlevel +call :CheckOutputExist fred file1 file2 file3 call :CheckExist dir1\fred rem Calculate destination name -copy fred+file* dir1\ >nul 2>&1 +copy fred+file* dir1\ >test1.txt 2>&1 if errorlevel 1 echo Incorrect errorlevel +call :CheckOutputExist fred file1 file2 file3 call :CheckExist dir1\fred rem Calculate destination name (none supplied) cd dir1 -copy ..\fred+..\file* >nul 2>&1 +copy ..\fred+..\file* >test1.txt 2>&1 if errorlevel 1 echo Incorrect errorlevel +call :CheckOutputExist fred file1 file2 file3 call :CheckExist fred -copy ..\fr*+..\file1 >nul 2>&1 +copy ..\fr*+..\file1 >test1.txt 2>&1 if errorlevel 1 echo Incorrect errorlevel +call :CheckOutputExist fred file1 call :CheckExist fred cd .. diff --git a/programs/cmd/tests/test_builtins.cmd.exp b/programs/cmd/tests/test_builtins.cmd.exp index ff6f944cffb..95feb67602b 100644 --- a/programs/cmd/tests/test_builtins.cmd.exp +++ b/programs/cmd/tests/test_builtins.cmd.exp @@ -1936,19 +1936,33 @@ file correctly deleted --- a batch file can alter itself bar ---------- Testing copy +Passed: Did not find file1 in COPY output Passed: Found expected dummy.file +Passed: Did not find file1 in COPY output Passed: Found expected dir1\file1 +Passed: Did not find file1 in COPY output Passed: Found expected dir1\file1 +Passed: Did not find file1 in COPY output Passed: Found expected dir1\file99 +Passed: Did not find file1 in COPY output Passed: Found expected file1 Passed: Did not find dir2 Passed: Did not find dir2\file1 +Passed: Found expected file1 in COPY output +Passed: Found expected file2 in COPY output +Passed: Found expected file3 in COPY output Passed: Found expected dir1\file1 Passed: Found expected dir1\file2 Passed: Found expected dir1\file3 +Passed: Found expected file1 in COPY output +Passed: Found expected file2 in COPY output +Passed: Found expected file3 in COPY output Passed: Found expected dir1\file1 Passed: Found expected dir1\file2 Passed: Found expected dir1\file3 +Passed: Found expected file1 in COPY output +Passed: Found expected file2 in COPY output +Passed: Found expected file3 in COPY output Passed: Found expected file1 Passed: Found expected file2 Passed: Found expected file3 @@ -1956,14 +1970,42 @@ Passed: Did not find dir2 Passed: Did not find dir2\file1 Passed: Did not find dir2\file2 Passed: Did not find dir2\file3 +Passed: Did not find file2 in COPY output Passed: Found expected testfile +Passed: Found expected file1 in COPY output +Passed: Found expected file2 in COPY output Passed: Found expected testfile +Passed: Found expected file1 in COPY output +Passed: Found expected file2 in COPY output +Passed: Found expected file3 in COPY output Passed: Found expected testfile +Passed: Found expected file1 in COPY output +Passed: Found expected file2 in COPY output +Passed: Found expected file3 in COPY output +Passed: Found expected fred in COPY output Passed: Found expected testfile +Passed: Found expected fred in COPY output +Passed: Found expected file1 in COPY output +Passed: Found expected file2 in COPY output +Passed: Found expected file3 in COPY output Passed: Found expected testfile +Passed: Found expected fred in COPY output +Passed: Found expected file1 in COPY output +Passed: Found expected file2 in COPY output +Passed: Found expected file3 in COPY output Passed: Found expected dir1\fred +Passed: Found expected fred in COPY output +Passed: Found expected file1 in COPY output +Passed: Found expected file2 in COPY output +Passed: Found expected file3 in COPY output Passed: Found expected dir1\fred +Passed: Found expected fred in COPY output +Passed: Found expected file1 in COPY output +Passed: Found expected file2 in COPY output +Passed: Found expected file3 in COPY output Passed: Found expected fred +Passed: Found expected fred in COPY output +Passed: Found expected file1 in COPY output Passed: Found expected fred Passed: file size check on file1 [5]@or_broken(a)Skipping file size check on NT4 Passed: file size check on file2 [8]@or_broken(a)Skipping file size check on NT4 -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/8200
From: Joe Souza <jsouza(a)yahoo.com> --- programs/cmd/wcmdmain.c | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/programs/cmd/wcmdmain.c b/programs/cmd/wcmdmain.c index e9c5be4ceb3..674ea8b1025 100644 --- a/programs/cmd/wcmdmain.c +++ b/programs/cmd/wcmdmain.c @@ -33,6 +33,10 @@ WINE_DEFAULT_DEBUG_CHANNEL(cmd); +#define BASE_DELIMS L",=;~!^&()+{}[]" +#define PATH_SEPARATION_DELIMS L" " BASE_DELIMS +#define INTRA_PATH_DELIMS L"\\" BASE_DELIMS + typedef struct _SEARCH_CONTEXT { WIN32_FIND_DATAW *fd; @@ -133,12 +137,12 @@ static void build_search_string(WCHAR *inputBuffer, int len, SEARCH_CONTEXT *sc) sc->searchstr[0] = L'\0'; - /* If inputBuffer ends in a space then the user hit tab beyond the last - * parameter, so use that as the search pos (i.e. a wildcard search). + /* If inputBuffer ends in one of our delimiters then use that as the search + * pos (i.e. a wildcard search). * Otherwise, parse the buffer to find the last parameter in the buffer, * where tab was pressed. */ - if (len && inputBuffer[len-1] == L' ') { + if (len && wcschr(PATH_SEPARATION_DELIMS, inputBuffer[len-1])) { cc = len; } else { /* Handle spaces in directory names. Need to quote paths if they contain spaces. @@ -150,7 +154,7 @@ static void build_search_string(WCHAR *inputBuffer, int len, SEARCH_CONTEXT *sc) if (stripped_copy) { wcsncpy_s(last_stripped_copy, ARRAY_SIZE(last_stripped_copy), stripped_copy, _TRUNCATE); } - stripped_copy = WCMD_parameter(inputBuffer, nn++, ¶m, FALSE, FALSE); + stripped_copy = WCMD_parameter_with_delims(inputBuffer, nn++, ¶m, FALSE, FALSE, PATH_SEPARATION_DELIMS); } while (param); if (last_param) { @@ -184,16 +188,16 @@ static void build_search_string(WCHAR *inputBuffer, int len, SEARCH_CONTEXT *sc) static void find_insert_pos(const WCHAR *inputBuffer, int len, SEARCH_CONTEXT *sc) { - int cc = len; + int cc = len - 1; - /* Handle paths here. Find last '\\'. - * If '\\' isn't found then insert pos is the same as search pos. + /* Handle paths here. Find last '\\' or other delimiter. + * If not found then insert pos is the same as search pos. */ - while (cc > sc->search_pos && inputBuffer[cc] != L'\\') { + while (cc > sc->search_pos && !wcschr(INTRA_PATH_DELIMS, inputBuffer[cc])) { cc--; } - if (inputBuffer[cc] == L'\"' || inputBuffer[cc] == L'\\') { + if (inputBuffer[cc] == L'\"' || wcschr(INTRA_PATH_DELIMS, inputBuffer[cc])) { cc++; } -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/8200
On Mon Jun 16 00:37:54 2025 +0000, eric pouech wrote:
the copy part looks good to me (first 2 commits) handling of separators in completion still lacks a couple of items `dir "a+<TAB>` differs from native `dir a,<TAB>` differs from native `dir a"<TAB>` differs from native note there are characters (other than '+') that native (tested on Win10 only) uses for file separation: '\\', '&', '(', ')', '/', '!', '\~', '{', '}', '\[', '\]' and surprisingly '^' (as ^ is an escape character) what is not supported as separator: '\<', '\>' how strange!!! you want a filename after a redirection! '|' (that's strange I would have expected & and | to behave similarly) and `dir a><TAB>` the '\>' is swallowed by completion... not sure we want that (esp. since you'd expect to type a filename after a redirection operator) Latest changes pushed. This should be very close to what you are looking for. Notable exceptions are examples containing quote characters. I don't see a good way to resolve these issues if we are using WCMD_parameter to parse the line, since this function will remove all quotes it finds. Otherwise, tab-completion should be working with the remainder of these delimiter characters now.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/8200#note_106656
participants (2)
-
Joe Souza -
Joe Souza (@JoeS209)