From: Joe Souza jsouza@yahoo.com
--- programs/cmd/batch.c | 4 +- programs/cmd/wcmd.h | 1 + programs/cmd/wcmdmain.c | 412 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 414 insertions(+), 3 deletions(-)
diff --git a/programs/cmd/batch.c b/programs/cmd/batch.c index 8bc3b5cb84b..6a6fd89983a 100644 --- a/programs/cmd/batch.c +++ b/programs/cmd/batch.c @@ -227,9 +227,7 @@ static WCHAR *WCMD_fgets_helper(WCHAR *buf, DWORD noChars, HANDLE h, UINT code_p /* We can't use the native f* functions because of the filename syntax differences between DOS and Unix. Also need to lose the LF (or CRLF) from the line. */
- if (VerifyConsoleIoHandle(h) && ReadConsoleW(h, buf, noChars, &charsRead, NULL) && charsRead) { - if (!charsRead) return NULL; - + if (VerifyConsoleIoHandle(h) && WCMD_read_console(h, buf, noChars, &charsRead) && charsRead) { /* Find first EOL */ for (i = 0; i < charsRead; i++) { if (buf[i] == '\n' || buf[i] == '\r') diff --git a/programs/cmd/wcmd.h b/programs/cmd/wcmd.h index d4adc7fd7c8..4c85d7ad8cb 100644 --- a/programs/cmd/wcmd.h +++ b/programs/cmd/wcmd.h @@ -212,6 +212,7 @@ WCHAR *WCMD_LoadMessage(UINT id); WCHAR *WCMD_strsubstW(WCHAR *start, const WCHAR* next, const WCHAR* insert, int len); RETURN_CODE WCMD_wait_for_input(HANDLE hIn); BOOL WCMD_ReadFile(const HANDLE hIn, WCHAR *intoBuf, const DWORD maxChars, LPDWORD charsRead); +BOOL WCMD_read_console(const HANDLE hInput, WCHAR *inputBuffer, const DWORD inputBufferLength, LPDWORD numRead);
enum read_parse_line {RPL_SUCCESS, RPL_EOF, RPL_SYNTAXERROR}; enum read_parse_line WCMD_ReadAndParseLine(const WCHAR *initialcmd, CMD_NODE **output); diff --git a/programs/cmd/wcmdmain.c b/programs/cmd/wcmdmain.c index 49419d02caa..1b442a89cf2 100644 --- a/programs/cmd/wcmdmain.c +++ b/programs/cmd/wcmdmain.c @@ -3,6 +3,7 @@ * * Copyright (C) 1999 - 2001 D A Pickles * Copyright (C) 2007 J Edmeades + * Copyright (C) 2025 Joe Souza (tab-completion support) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -32,6 +33,18 @@
WINE_DEFAULT_DEBUG_CHANNEL(cmd);
+typedef struct _SEARCH_CONTEXT +{ + WIN32_FIND_DATAW *fd; + BOOL have_quotes; + BOOL is_dir_search; + int search_pos; + int insert_pos; + int entry_count; + int current_entry; + WCHAR searchstr[MAX_PATH]; +} SEARCH_CONTEXT, *PSEARCH_CONTEXT; + extern const WCHAR inbuilt[][10]; extern struct env_stack *pushd_directories;
@@ -61,6 +74,405 @@ static HANDLE control_c_event;
#define MAX_WRITECONSOLE_SIZE 65535
+ +static BOOL is_directory_operation(WCHAR *inputBuffer) +{ + WCHAR *param = NULL, *first_param; + BOOL ret = FALSE; + + first_param = WCMD_parameter(inputBuffer, 0, ¶m, TRUE, FALSE); + + if (!wcsicmp(first_param, L"cd") || + !wcsicmp(first_param, L"rd") || + !wcsicmp(first_param, L"md") || + !wcsicmp(first_param, L"chdir") || + !wcsicmp(first_param, L"rmdir") || + !wcsicmp(first_param, L"mkdir")) { + + ret = TRUE; + } + + return ret; +} + +static void clear_console_characters(const HANDLE hOutput, SHORT cCount, const SHORT width) +{ + CONSOLE_SCREEN_BUFFER_INFO csbi; + DWORD written; + SHORT chars; + + GetConsoleScreenBufferInfo(hOutput, &csbi); + + /* Need to handle clearing multiple lines, in case user resized console window. */ + while (cCount) { + chars = min(width - csbi.dwCursorPosition.X, cCount); + FillConsoleOutputCharacterW(hOutput, L' ', chars, csbi.dwCursorPosition, &written); + csbi.dwCursorPosition.Y++; /* Bump to next row. */ + csbi.dwCursorPosition.X = 0; /* First column in the row. */ + cCount -= chars; + } +} + +static void set_cursor_visible(const HANDLE hOutput, const BOOL visible) +{ + CONSOLE_CURSOR_INFO cursorInfo; + + if (GetConsoleCursorInfo(hOutput, &cursorInfo)) { + cursorInfo.bVisible = visible; + SetConsoleCursorInfo(hOutput, &cursorInfo); + } +} + +static void build_search_string(WCHAR *inputBuffer, int len, PSEARCH_CONTEXT sc) +{ + int cc = 0, nn = 0; + WCHAR *param = NULL, *last_param, *stripped_copy = NULL; + WCHAR last_stripped_copy[MAX_PATH] = L"\0"; + + 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). + * Otherwise, parse the buffer to find the last parameter in the buffer, + * where tab was pressed. + */ + if (inputBuffer[len-1] == L' ') { + cc = len; + } else { + /* Handle spaces in directory names. Need to quote paths if they contain spaces. + * Also, there can be multiple quoted paths on a command line, so find the current + * one. + */ + do { + last_param = param; + if (stripped_copy) { + lstrcpynW(last_stripped_copy, stripped_copy, ARRAY_SIZE(last_stripped_copy)); + } + stripped_copy = WCMD_parameter(inputBuffer, nn++, ¶m, FALSE, FALSE); + } while (param); + + if (last_param) { + cc = last_param - inputBuffer; + } + + if (inputBuffer[cc] == L'"') { + sc->have_quotes = TRUE; + cc++; + } + + if (last_stripped_copy[0]) { + lstrcpynW(sc->searchstr, last_stripped_copy, ARRAY_SIZE(sc->searchstr) - 1); + } + } + + sc->search_pos = cc; + lstrcatW(sc->searchstr, L"*"); +} + +static void find_insert_pos(const WCHAR *inputBuffer, int len, PSEARCH_CONTEXT sc) +{ + int cc = len; + + /* Handle paths here. Find last '\'. + * If '\' isn't found then insert pos is the same as search pos. + */ + while (cc > sc->search_pos && inputBuffer[cc] != L'\') { + cc--; + } + + if (inputBuffer[cc] == L'"' || inputBuffer[cc] == L'\') { + cc++; + } + + sc->insert_pos = cc; +} + +/* Based on code in WCMD_list_directory. + * Could have used a linked-list, but array is more efficient for + * build once / read mostly. + */ +static void build_directory_entry_list(PSEARCH_CONTEXT sc) +{ + HANDLE hff; + + sc->entry_count = 0; + sc->current_entry = 0; + + sc->fd = xalloc(sizeof(WIN32_FIND_DATAW)); + + WINE_TRACE("Looking for matches to '%s'\n", wine_dbgstr_w(sc->searchstr)); + hff = FindFirstFileW(sc->searchstr, &sc->fd[sc->entry_count]); + if (hff != INVALID_HANDLE_VALUE) { + do { + /* Always skip "." and ".." entries. */ + if (wcscmp(sc->fd[sc->entry_count].cFileName, L".") && wcscmp(sc->fd[sc->entry_count].cFileName, L"..")) { + if (!sc->is_dir_search || sc->fd[sc->entry_count].dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + sc->entry_count++; + sc->fd = xrealloc(sc->fd, (sc->entry_count + 1) * sizeof(WIN32_FIND_DATAW)); + } + } + } while (FindNextFileW(hff, &sc->fd[sc->entry_count])); + + FindClose (hff); + } +} + +static void free_directory_entry_list(PSEARCH_CONTEXT sc) +{ + if (sc->fd) { + free(sc->fd); + sc->fd = NULL; + } + sc->entry_count = 0; + sc->current_entry = 0; +} + +static void get_next_matching_directory_entry(PSEARCH_CONTEXT sc, BOOL reverse) +{ + if (reverse) { + sc->current_entry--; + if (sc->current_entry < 0) { + sc->current_entry = sc->entry_count - 1; + } + } else { + sc->current_entry++; + if (sc->current_entry >= sc->entry_count) { + sc->current_entry = 0; + } + } +} + +static void update_input_buffer(WCHAR *inputBuffer, const DWORD inputBufferLength, PSEARCH_CONTEXT sc) +{ + BOOL needQuotes = FALSE; + BOOL removeQuotes = FALSE; + int len, quote_pos = -1; + + /* We have found the insert position for the results. Terminate the string here. */ + inputBuffer[sc->insert_pos] = L'\0'; + + /* Special case: If user entered a backslash after quotes, ignore the final quotes. + * (i.e. "C:\Program Files"<tab> or "C:\Program Files"\Com<tab> ) + */ + if (sc->insert_pos > 2 && inputBuffer[sc->insert_pos-2] == L'"' && inputBuffer[sc->insert_pos-1] == L'\') { + TRACE("Trim quote before trailing backslash\n"); + inputBuffer[sc->insert_pos-2] = L'\'; + inputBuffer[sc->insert_pos-1] = L'\0'; + sc->insert_pos--; + } + + /* If there are no spaces in the path then we can remove quotes when appending + * the search result, unless the search result itself requires them. + */ + if (sc->have_quotes && !wcschr(&inputBuffer[sc->search_pos], L' ')) { + TRACE("removeQuotes = TRUE\n"); + removeQuotes = TRUE; + } + + /* Online documentation states that paths or filenames should be quoted if they are long + * file names or contain spaces. In practice, modern Windows seems to quote paths/files + * only if they contain spaces. + */ + needQuotes = wcschr(sc->fd[sc->current_entry].cFileName, L' ') ? TRUE : FALSE; + len = lstrlenW(inputBuffer); + /* Remove starting quotes, if able. */ + if (removeQuotes && !needQuotes) { + /* Quotes are at search_pos-1 if they were already present at the start of this search. + * Otherwise quotes are at search_pos if we added them. + */ + TRACE("search_pos: %d\n", sc->search_pos); + if (inputBuffer[sc->search_pos] == L'"') { + quote_pos = sc->search_pos; + } else if (sc->search_pos > 0 && inputBuffer[sc->search_pos-1] == L'"') { + quote_pos = sc->search_pos-1; + } + if (quote_pos >= 0) { + memmove(&inputBuffer[quote_pos], &inputBuffer[quote_pos+1], (len - quote_pos) * sizeof(WCHAR)); + sc->have_quotes = FALSE; + sc->insert_pos--; + } + } else + /* Add starting quotes if needed. */ + if (needQuotes && !sc->have_quotes) { + if (len < inputBufferLength - 1) { + if (sc->search_pos <= len) { + memmove(&inputBuffer[sc->search_pos+1], &inputBuffer[sc->search_pos], (len - sc->search_pos + 1) * sizeof(WCHAR)); + inputBuffer[sc->search_pos] = L'"'; + sc->have_quotes = TRUE; + sc->insert_pos++; + } + } + } + lstrcatW(inputBuffer, sc->fd[sc->current_entry].cFileName); + /* Add closing quotes if needed. */ + if (needQuotes || (sc->have_quotes && !removeQuotes)) { + len = lstrlenW(inputBuffer); + if (len < inputBufferLength - 1) { + inputBuffer[len] = L'"'; + inputBuffer[len+1] = L'\0'; + } + } +} + +/* Intended as a mostly drop-in replacement for ReadConsole, but with tab-completion support. + */ +BOOL WCMD_read_console(const HANDLE hInput, WCHAR *inputBuffer, const DWORD inputBufferLength, LPDWORD numRead) +{ + HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); + SEARCH_CONTEXT sc = {0}; + WCHAR *lastResult = NULL; + CONSOLE_SCREEN_BUFFER_INFO startConsoleInfo, lastConsoleInfo, currentConsoleInfo; + DWORD numWritten; + UINT oldCurPos, curPos; + BOOL done = FALSE; + BOOL ret = FALSE; + static int maxLen = 0; /* Track maximum length in case user fetches a long string from a previous iteration in history. */ + + if (hInput == INVALID_HANDLE_VALUE || hOutput == INVALID_HANDLE_VALUE || !inputBuffer || !inputBufferLength) { + return FALSE; + } + + *inputBuffer = L'\0'; + curPos = 0; + + /* Get starting cursor position and size */ + GetConsoleScreenBufferInfo(hOutput, &startConsoleInfo); + lastConsoleInfo = startConsoleInfo; + + while (!done) { + CONSOLE_READCONSOLE_CONTROL inputControl; + int len, len2; + + len = lstrlenW(inputBuffer); + len2 = len; + + /* Update current input display in console */ + set_cursor_visible(hOutput, FALSE); + + /* Calculate cursor position at beginning of prompt, accounting for lines scrolled + * due to length of input. + */ + GetConsoleScreenBufferInfo(hOutput, ¤tConsoleInfo); + currentConsoleInfo.dwCursorPosition.X = startConsoleInfo.dwCursorPosition.X; + len2 -= (currentConsoleInfo.dwSize.X - currentConsoleInfo.dwCursorPosition.X); + if (len2 > 0) { + currentConsoleInfo.dwCursorPosition.Y -= ((len2 / currentConsoleInfo.dwSize.X) + 1); + } + SetConsoleCursorPosition(hOutput, currentConsoleInfo.dwCursorPosition); + + WriteConsoleW(hOutput, inputBuffer, len, &numWritten, NULL); + if (maxLen > len) { + clear_console_characters(hOutput, maxLen - len, lastConsoleInfo.dwSize.X); /* width at time of last console update */ + } + set_cursor_visible(hOutput, TRUE); + + /* Remember current dimensions in case user resizes console window. */ + GetConsoleScreenBufferInfo(hOutput, &lastConsoleInfo); + + inputControl.nLength = sizeof(inputControl); + inputControl.nInitialChars = len; + inputControl.dwCtrlWakeupMask = (1 << '\t'); + /* FIXME: In the Windows SDK this is called dwControlKeyState */ + inputControl.dwConsoleKeyState = 0; + + /* Allow room for NULL terminator. inputBufferLength is at least 1 due to check above. */ + ret = ReadConsoleW(hInput, inputBuffer, inputBufferLength - 1, numRead, &inputControl); + + if (ret) { + inputBuffer[*numRead] = L'\0'; + TRACE("ReadConsole: [%lu][%s]\n", *numRead, wine_dbgstr_w(inputBuffer)); + len = *numRead; + if (len > maxLen) { + maxLen = len; + } + oldCurPos = curPos; + curPos = 0; + while (curPos < len && inputBuffer[curPos] != L'\t') { + curPos++; + } + /* curPos is often numRead - 1, but not always, as in the case where history is retrieved + * and then user backspaces to somewhere mid-string and then hits Tab. + */ + TRACE("numRead: %lu, curPos: %u\n", *numRead, curPos); + + switch (inputBuffer[curPos]) { + case L'\t': + TRACE("TAB: [%s]\n", wine_dbgstr_w(inputBuffer)); + inputBuffer[curPos] = L'\0'; + + /* See if we need to conduct a new search. */ + if (curPos != oldCurPos || (!lastResult || wcscmp(inputBuffer, lastResult))) { + /* New search */ + + sc.have_quotes = FALSE; + sc.search_pos = 0; + sc.insert_pos = 0; + + build_search_string(inputBuffer, curPos, &sc); + TRACE("New search: [%s]\n", wine_dbgstr_w(sc.searchstr)); + + sc.is_dir_search = is_directory_operation(inputBuffer); + + free_directory_entry_list(&sc); + build_directory_entry_list(&sc); + } + + if (sc.entry_count) { + get_next_matching_directory_entry(&sc, (inputControl.dwConsoleKeyState & SHIFT_PRESSED) ? TRUE : FALSE); + + /* If this is our first time through here for this search, we need to find the insert position + * for the results. Note that this is very likely not the same location as the search position. + */ + if (!sc.insert_pos) { + /* If user entered a backslash after quotes, remove the quotes. */ + if (curPos > 2 && inputBuffer[curPos-2] == L'"' && inputBuffer[curPos-1] == L'\') { + TRACE("Remove quote before trailing backslash\n"); + inputBuffer[curPos-2] = L'\'; + inputBuffer[curPos-1] = L'\0'; + curPos--; + } + + find_insert_pos(inputBuffer, curPos, &sc); + } + + /* Copy search results to input buffer. */ + update_input_buffer(inputBuffer, inputBufferLength, &sc); + + /* Save last result in case user edits existing portion of the string before hitting tab again. */ + if (lastResult) { + free(lastResult); + } + lastResult = wcsdup(inputBuffer); + + /* Update cursor position to end of buffer. */ + curPos = lstrlenW(inputBuffer); + if (curPos > maxLen) { + maxLen = curPos; + } + } + break; + + default: + TRACE("RETURN: [%s]\n", wine_dbgstr_w(inputBuffer)); + done = TRUE; + break; + } + + } else { + /* ReadConsole failed */ + done = TRUE; + } + } + + /* Cleanup any existing search results and related data before exiting. */ + free_directory_entry_list(&sc); + if (lastResult) { + free(lastResult); + } + + return ret; +} + /* * Returns a buffer for reading from/writing to file * Never freed