From: Francis De Brabandere <francisdb@gmail.com> Report user-visible "Input Error" messages to stdout matching Windows behavior for missing script files and nonexistent files. Display usage information with Wine version when invoked with no arguments. Script runtime errors (OnScriptError) remain on stderr. Implement //nologo and //logo flags to control the banner displayed before script execution. Treat unrecognized options as filenames to support Unix-style paths on Wine. Add FIXME stubs for other recognized but unimplemented options (//X, //S, //E:, //H:, //Job:, //T:). --- programs/cscript/Makefile.in | 3 +- programs/wscript/Makefile.in | 3 +- programs/wscript/main.c | 245 +++++++++++++++++++++----- programs/wscript/resource.h | 23 +++ programs/wscript/tests/run.c | 329 ++++++++++++++++++++++++++++++++++- programs/wscript/wscript.rc | 54 ++++++ 6 files changed, 609 insertions(+), 48 deletions(-) create mode 100644 programs/wscript/resource.h create mode 100644 programs/wscript/wscript.rc diff --git a/programs/cscript/Makefile.in b/programs/cscript/Makefile.in index 1aeda6cad0f..f08ef278822 100644 --- a/programs/cscript/Makefile.in +++ b/programs/cscript/Makefile.in @@ -9,4 +9,5 @@ SOURCES = \ arguments.c \ host.c \ ihost.idl \ - main.c + main.c \ + wscript.rc diff --git a/programs/wscript/Makefile.in b/programs/wscript/Makefile.in index 21511a3ca81..fc220f49717 100644 --- a/programs/wscript/Makefile.in +++ b/programs/wscript/Makefile.in @@ -7,4 +7,5 @@ SOURCES = \ arguments.c \ host.c \ ihost.idl \ - main.c + main.c \ + wscript.rc diff --git a/programs/wscript/main.c b/programs/wscript/main.c index 2f1e517d419..01b09030832 100644 --- a/programs/wscript/main.c +++ b/programs/wscript/main.c @@ -29,6 +29,7 @@ #include <initguid.h> #include "wscript.h" +#include "resource.h" #include <wine/debug.h> @@ -52,6 +53,8 @@ WCHAR scriptFullName[MAX_PATH]; ITypeInfo *host_ti; ITypeInfo *arguments_ti; +static BOOL nologo; + static HRESULT query_interface(REFIID,void**); static HRESULT WINAPI ActiveScriptSite_QueryInterface(IActiveScriptSite *iface, @@ -120,20 +123,15 @@ static HRESULT WINAPI ActiveScriptSite_OnStateChange(IActiveScriptSite *iface, return S_OK; } -static void print_error(const WCHAR *string) +static void write_to_handle(HANDLE handle, const WCHAR *string) { DWORD count, ret, len, lena; char *buf; - if(wshInteractive) { - MessageBoxW(NULL, string, L"Windows Script Host", MB_OK); - return; - } - len = lstrlenW(string); - ret = WriteConsoleW(GetStdHandle(STD_ERROR_HANDLE), string, len, &count, NULL); + ret = WriteConsoleW(handle, string, len, &count, NULL); if(ret) { - WriteConsoleW(GetStdHandle(STD_ERROR_HANDLE), L"\r\n", 2, &count, NULL); + WriteConsoleW(handle, L"\r\n", 2, &count, NULL); return; } @@ -143,9 +141,101 @@ static void print_error(const WCHAR *string) return; WideCharToMultiByte(GetOEMCP(), 0, string, len, buf, lena, NULL, NULL); - WriteFile(GetStdHandle(STD_ERROR_HANDLE), buf, lena, &count, FALSE); + WriteFile(handle, buf, lena, &count, FALSE); free(buf); - WriteFile(GetStdHandle(STD_ERROR_HANDLE), "\r\n", 2, &count, FALSE); + WriteFile(handle, "\r\n", 2, &count, FALSE); +} + +static void print_error(const WCHAR *string) +{ + if(wshInteractive) { + MessageBoxW(NULL, string, L"Windows Script Host", MB_OK); + return; + } + + write_to_handle(GetStdHandle(STD_ERROR_HANDLE), string); +} + +static void print_string(const WCHAR *string) +{ + write_to_handle(GetStdHandle(STD_OUTPUT_HANDLE), string); +} + +static void output_writeconsole(const WCHAR *str, DWORD wlen) +{ + DWORD count; + + if(!WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), str, wlen, &count, NULL)) { + DWORD len; + char *buf; + + /* On Windows WriteConsoleW() fails if the output is redirected. So fall + * back to WriteFile() with OEM code page. */ + len = WideCharToMultiByte(GetOEMCP(), 0, str, wlen, NULL, 0, NULL, NULL); + buf = malloc(len); + + WideCharToMultiByte(GetOEMCP(), 0, str, wlen, buf, len, NULL, NULL); + WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), buf, len, &count, FALSE); + free(buf); + } +} + +static void output_formatstring(const WCHAR *fmt, va_list va_args) +{ + WCHAR *str; + DWORD len; + + len = FormatMessageW(FORMAT_MESSAGE_FROM_STRING | FORMAT_MESSAGE_ALLOCATE_BUFFER, + fmt, 0, 0, (WCHAR *)&str, 0, &va_args); + if(len == 0 && GetLastError() != ERROR_NO_WORK_DONE) { + WINE_FIXME("Could not format string: le=%lu, fmt=%s\n", GetLastError(), wine_dbgstr_w(fmt)); + return; + } + if(wshInteractive) { + MessageBoxW(NULL, str, L"Windows Script Host", MB_OK); + }else { + output_writeconsole(str, len); + } + LocalFree(str); +} + +static void WINAPIV print_resource(unsigned int id, ...) +{ + WCHAR *fmt = NULL; + int len; + va_list va_args; + + if(!(len = LoadStringW(GetModuleHandleW(NULL), id, (WCHAR *)&fmt, 0))) { + WINE_FIXME("LoadString failed with %ld\n", GetLastError()); + return; + } + + len++; + fmt = malloc(len * sizeof(WCHAR)); + if(!fmt) + return; + + LoadStringW(GetModuleHandleW(NULL), id, fmt, len); + + va_start(va_args, id); + output_formatstring(fmt, va_args); + va_end(va_args); + + free(fmt); +} + +static void print_banner(void) +{ + const char * (CDECL *wine_get_version)(void); + WCHAR header[64]; + + wine_get_version = (void *)GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "wine_get_version"); + if(wine_get_version) + swprintf(header, ARRAY_SIZE(header), L"Wine %S Script Host", wine_get_version()); + else + swprintf(header, ARRAY_SIZE(header), L"Wine Script Host"); + print_string(header); + print_string(L""); } static HRESULT WINAPI ActiveScriptSite_OnScriptError(IActiveScriptSite *iface, @@ -389,6 +479,11 @@ static BSTR get_script_str(const WCHAR *filename) return NULL; size = GetFileSize(file, NULL); + if(!size) { + CloseHandle(file); + return SysAllocStringLen(NULL, 0); + } + map = CreateFileMappingW(file, NULL, PAGE_READONLY, 0, 0, NULL); CloseHandle(file); if(map == INVALID_HANDLE_VALUE) @@ -407,28 +502,25 @@ static BSTR get_script_str(const WCHAR *filename) return ret; } -static void run_script(const WCHAR *filename, IActiveScript *script, IActiveScriptParse *parser) +static BOOL run_script(BSTR text, IActiveScript *script, IActiveScriptParse *parser) { - BSTR text; HRESULT hres; - text = get_script_str(filename); - if(!text) { - WINE_FIXME("Could not get script text\n"); - return; - } - hres = IActiveScriptParse_ParseScriptText(parser, text, NULL, NULL, NULL, 1, 0, SCRIPTTEXT_HOSTMANAGESSOURCE|SCRIPTITEM_ISVISIBLE, NULL, NULL); - SysFreeString(text); if(FAILED(hres)) { - WINE_FIXME("ParseScriptText failed: %08lx\n", hres); - return; + if(hres != SCRIPT_E_REPORTED) + WINE_WARN("ParseScriptText failed: %08lx\n", hres); + return FALSE; } hres = IActiveScript_SetScriptState(script, SCRIPTSTATE_STARTED); - if(FAILED(hres)) - WINE_FIXME("SetScriptState failed: %08lx\n", hres); + if(FAILED(hres)) { + if(hres != SCRIPT_E_REPORTED) + WINE_WARN("SetScriptState failed: %08lx\n", hres); + } + + return TRUE; } static BOOL set_host_properties(const WCHAR *prop) @@ -446,16 +538,27 @@ static BOOL set_host_properties(const WCHAR *prop) else if(wcsicmp(prop, L"b") == 0) wshInteractive = VARIANT_FALSE; else if(wcsicmp(prop, L"nologo") == 0) - WINE_FIXME("ignored %s switch\n", debugstr_w(L"nologo")); + nologo = TRUE; + else if(wcsicmp(prop, L"logo") == 0) + nologo = FALSE; else if(wcsicmp(prop, L"d") == 0) - WINE_FIXME("ignoring /d\n"); + WINE_FIXME("ignoring //d\n"); + else if(wcsicmp(prop, L"x") == 0) + WINE_FIXME("ignoring //x\n"); else if(wcsicmp(prop, L"u") == 0) - WINE_FIXME("ignoring /u\n"); + WINE_FIXME("ignoring //u\n"); + else if(wcsicmp(prop, L"s") == 0) + WINE_FIXME("ignoring //s\n"); + else if(wcsnicmp(prop, L"e:", 2) == 0) + WINE_FIXME("ignoring //e:\n"); + else if(wcsnicmp(prop, L"h:", 2) == 0) + WINE_FIXME("ignoring //h:\n"); + else if(wcsnicmp(prop, L"job:", 4) == 0) + WINE_FIXME("ignoring //job:\n"); + else if(wcsnicmp(prop, L"t:", 2) == 0) + WINE_FIXME("ignoring //t:\n"); else - { - WINE_FIXME("unsupported switch %s\n", debugstr_w(prop)); return FALSE; - } return TRUE; } @@ -464,9 +567,11 @@ int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPWSTR cmdline, int cm WCHAR *ext, *filepart, *filename = NULL; IActiveScriptParse *parser; IActiveScript *script; + BSTR script_text; WCHAR **argv; CLSID clsid; int argc, i; + int ret = 0; DWORD res; WINE_TRACE("(%p %p %s %x)\n", hInst, hPrevInst, wine_dbgstr_w(cmdline), cmdshow); @@ -475,26 +580,84 @@ int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPWSTR cmdline, int cm if(!argv) return 1; + /* Pass 1: consume // host options from all positions. */ for(i=1; i<argc; i++) { - if(*argv[i] == '/' || *argv[i] == '-') { - if(!set_host_properties(argv[i])) - return 1; - }else { - filename = argv[i]; - argums = argv+i+1; - numOfArgs = argc-i-1; - break; + if(argv[i][0] == '/' && argv[i][1] == '/' && set_host_properties(argv[i])) + argv[i] = NULL; + } + + /* Pass 2: find filename, consuming single-/ and - options before it. */ + for(i=1; i<argc; i++) { + if(!argv[i]) + continue; + if(argv[i][0] == '-' && argv[i][1] == '-' && !argv[i][2]) + continue; /* -- separator */ + if((*argv[i] == '/' || *argv[i] == '-') && set_host_properties(argv[i])) + continue; + filename = argv[i]; + i++; + break; + } + + /* Pass 3: compact script args, skipping consumed // options. */ + if(filename) { + int j = 0, first_arg = i; + for(; i<argc; i++) { + if(!argv[i]) + continue; + argv[first_arg + j] = argv[i]; + j++; + } + argums = argv + first_arg; + numOfArgs = j; + } + + if(!filename && argc == 1) { + if(wshInteractive) { + WINE_FIXME("Settings dialog not implemented\n"); + return 0; } + if(!nologo) + print_banner(); + print_resource(IDS_USAGE); + return 0; } + if(!nologo) + print_banner(); + if(!filename) { - WINE_FIXME("No file name specified\n"); + print_resource(IDS_NO_SCRIPT_FILE); return 1; } + res = GetFullPathNameW(filename, ARRAY_SIZE(scriptFullName), scriptFullName, &filepart); if(!res || res > ARRAY_SIZE(scriptFullName)) return 1; + script_text = get_script_str(scriptFullName); + if(!script_text) { + DWORD err = GetLastError(); + if(err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) { + print_resource(IDS_FILE_NOT_FOUND, scriptFullName); + }else { + WCHAR *syserr = NULL; + FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (WCHAR *)&syserr, 0, NULL); + if(syserr) { + /* Trim trailing whitespace from system error message. */ + int len = lstrlenW(syserr); + while(len > 0 && (syserr[len - 1] == '\r' || syserr[len - 1] == '\n' || syserr[len - 1] == ' ')) + syserr[--len] = 0; + print_resource(IDS_SCRIPT_LOAD_ERROR, scriptFullName, syserr); + LocalFree(syserr); + }else { + print_resource(IDS_SCRIPT_LOAD_ERROR, scriptFullName, L""); + } + } + return 1; + } + ext = wcsrchr(filepart, '.'); if(!ext || !get_engine_clsid(ext, &clsid)) { WINE_FIXME("Could not find engine for %s\n", wine_dbgstr_w(ext)); @@ -510,17 +673,19 @@ int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPWSTR cmdline, int cm } if(init_engine(script, parser)) { - run_script(filename, script, parser); + if(!run_script(script_text, script, parser)) + ret = 1; IActiveScript_Close(script); ITypeInfo_Release(host_ti); }else { WINE_FIXME("Script initialization failed\n"); } + SysFreeString(script_text); IActiveScript_Release(script); IActiveScriptParse_Release(parser); CoUninitialize(); - return 0; + return ret; } diff --git a/programs/wscript/resource.h b/programs/wscript/resource.h new file mode 100644 index 00000000000..552290d582f --- /dev/null +++ b/programs/wscript/resource.h @@ -0,0 +1,23 @@ +/* + * Copyright 2025 Francis De Brabandere + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#define IDS_USAGE 1 +#define IDS_NO_SCRIPT_FILE 2 +#define IDS_FILE_NOT_FOUND 3 +#define IDS_SCRIPT_LOAD_ERROR 4 + diff --git a/programs/wscript/tests/run.c b/programs/wscript/tests/run.c index 9aae91ddc7a..abb01774117 100644 --- a/programs/wscript/tests/run.c +++ b/programs/wscript/tests/run.c @@ -324,20 +324,49 @@ static IClassFactory testobj_cf = { &ClassFactoryVtbl }; static void run_script_file(const char *file_name, DWORD expected_exit_code) { char command[MAX_PATH]; + SECURITY_ATTRIBUTES sa = {sizeof(sa), NULL, TRUE}; STARTUPINFOA si = {sizeof(si)}; PROCESS_INFORMATION pi; - DWORD exit_code; + HANDLE stdout_read, stdout_write; + HANDLE stderr_read, stderr_write; + char stdout_buf[4096], stderr_buf[4096]; + DWORD exit_code, size; BOOL bres; script_name = file_name; sprintf(command, "wscript.exe %s arg1 2 ar3", file_name); + bres = CreatePipe(&stdout_read, &stdout_write, &sa, 0); + ok(bres, "CreatePipe failed: %lu\n", GetLastError()); + if(!bres) + return; + + bres = CreatePipe(&stderr_read, &stderr_write, &sa, 0); + ok(bres, "CreatePipe failed: %lu\n", GetLastError()); + if(!bres) { + CloseHandle(stdout_read); + CloseHandle(stdout_write); + return; + } + + SetHandleInformation(stdout_read, HANDLE_FLAG_INHERIT, 0); + SetHandleInformation(stderr_read, HANDLE_FLAG_INHERIT, 0); + + si.dwFlags = STARTF_USESTDHANDLES; + si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + si.hStdOutput = stdout_write; + si.hStdError = stderr_write; + SET_EXPECT(reportSuccess); bres = CreateProcessA(NULL, command, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi); + CloseHandle(stdout_write); + CloseHandle(stderr_write); if(!bres) { win_skip("script.exe is not available\n"); CLEAR_CALLED(reportSuccess); + CloseHandle(stdout_read); + CloseHandle(stderr_read); return; } @@ -348,8 +377,18 @@ static void run_script_file(const char *file_name, DWORD expected_exit_code) ok(bres, "GetExitCodeProcess failed: %lu\n", GetLastError()); ok(exit_code == expected_exit_code, "exit_code = %lu, expected %lu\n", exit_code, expected_exit_code); + memset(stderr_buf, 0, sizeof(stderr_buf)); + ReadFile(stderr_read, stderr_buf, sizeof(stderr_buf) - 1, &size, NULL); + stderr_buf[size] = 0; + ok(stderr_buf[0] == 0, "expected no stderr output, got: %s\n", stderr_buf); + + /* Drain stdout to prevent child from blocking on a full pipe */ + ReadFile(stdout_read, stdout_buf, sizeof(stdout_buf) - 1, &size, NULL); + CloseHandle(pi.hThread); CloseHandle(pi.hProcess); + CloseHandle(stdout_read); + CloseHandle(stderr_read); CHECK_CALLED(reportSuccess); } @@ -463,8 +502,9 @@ static void run_cscript_error_test(void) SECURITY_ATTRIBUTES sa = {sizeof(sa), NULL, TRUE}; STARTUPINFOA si = {sizeof(si)}; PROCESS_INFORMATION pi; + HANDLE stdout_read, stdout_write; HANDLE stderr_read, stderr_write; - char stderr_buf[4096]; + char stdout_buf[4096], stderr_buf[4096]; DWORD exit_code, size; HANDLE file; BOOL bres; @@ -481,23 +521,34 @@ static void run_cscript_error_test(void) if(!bres) goto cleanup; - bres = CreatePipe(&stderr_read, &stderr_write, &sa, 0); + bres = CreatePipe(&stdout_read, &stdout_write, &sa, 0); ok(bres, "CreatePipe failed: %lu\n", GetLastError()); if(!bres) goto cleanup; + bres = CreatePipe(&stderr_read, &stderr_write, &sa, 0); + ok(bres, "CreatePipe failed: %lu\n", GetLastError()); + if(!bres) { + CloseHandle(stdout_read); + CloseHandle(stdout_write); + goto cleanup; + } + + SetHandleInformation(stdout_read, HANDLE_FLAG_INHERIT, 0); SetHandleInformation(stderr_read, HANDLE_FLAG_INHERIT, 0); si.dwFlags = STARTF_USESTDHANDLES; si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); - si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); + si.hStdOutput = stdout_write; si.hStdError = stderr_write; sprintf(command, "cscript.exe //nologo %s", file_name); bres = CreateProcessA(NULL, command, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi); + CloseHandle(stdout_write); CloseHandle(stderr_write); if(!bres) { win_skip("cscript.exe is not available\n"); + CloseHandle(stdout_read); CloseHandle(stderr_read); goto cleanup; } @@ -508,6 +559,11 @@ static void run_cscript_error_test(void) ok(bres, "GetExitCodeProcess failed: %lu\n", GetLastError()); ok(exit_code == 0, "exit_code = %lu\n", exit_code); + memset(stdout_buf, 0, sizeof(stdout_buf)); + ReadFile(stdout_read, stdout_buf, sizeof(stdout_buf) - 1, &size, NULL); + stdout_buf[size] = 0; + ok(stdout_buf[0] == 0, "expected no stdout with //nologo, got: %s\n", stdout_buf); + memset(stderr_buf, 0, sizeof(stderr_buf)); ReadFile(stderr_read, stderr_buf, sizeof(stderr_buf) - 1, &size, NULL); stderr_buf[size] = 0; @@ -515,17 +571,270 @@ static void run_cscript_error_test(void) ok(size > 0, "expected error output on stderr, got nothing\n"); ok(strstr(stderr_buf, "test_err.vbs(1,") != NULL, "expected file and line reference in error, got: %s\n", stderr_buf); - ok(strstr(stderr_buf, "Type mismatch") != NULL, - "expected 'Type mismatch' in output, got: %s\n", stderr_buf); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); + CloseHandle(stdout_read); + CloseHandle(stderr_read); + +cleanup: + DeleteFileA(file_name); +} + +static DWORD run_cscript(const char *args, char *stdout_buf, size_t stdout_buf_size, + char *stderr_buf, size_t stderr_buf_size) +{ + char command[MAX_PATH]; + SECURITY_ATTRIBUTES sa = {sizeof(sa), NULL, TRUE}; + STARTUPINFOA si = {sizeof(si)}; + PROCESS_INFORMATION pi; + HANDLE stdout_read, stdout_write; + HANDLE stderr_read, stderr_write; + DWORD exit_code, size; + BOOL bres; + + bres = CreatePipe(&stderr_read, &stderr_write, &sa, 0); + ok(bres, "CreatePipe failed: %lu\n", GetLastError()); + if(!bres) + return ~0u; + + bres = CreatePipe(&stdout_read, &stdout_write, &sa, 0); + ok(bres, "CreatePipe failed: %lu\n", GetLastError()); + if(!bres) { + CloseHandle(stderr_read); + CloseHandle(stderr_write); + return ~0u; + } + + SetHandleInformation(stderr_read, HANDLE_FLAG_INHERIT, 0); + SetHandleInformation(stdout_read, HANDLE_FLAG_INHERIT, 0); + + si.dwFlags = STARTF_USESTDHANDLES; + si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + si.hStdOutput = stdout_write; + si.hStdError = stderr_write; + + sprintf(command, "cscript.exe %s", args); + bres = CreateProcessA(NULL, command, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi); + CloseHandle(stderr_write); + CloseHandle(stdout_write); + if(!bres) { + win_skip("cscript.exe is not available\n"); + CloseHandle(stderr_read); + CloseHandle(stdout_read); + return ~0u; + } + + WaitForSingleObject(pi.hProcess, INFINITE); + + bres = GetExitCodeProcess(pi.hProcess, &exit_code); + ok(bres, "GetExitCodeProcess failed: %lu\n", GetLastError()); + + memset(stdout_buf, 0, stdout_buf_size); + ReadFile(stdout_read, stdout_buf, stdout_buf_size - 1, &size, NULL); + stdout_buf[size] = 0; + + memset(stderr_buf, 0, stderr_buf_size); + ReadFile(stderr_read, stderr_buf, stderr_buf_size - 1, &size, NULL); + stderr_buf[size] = 0; + + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + CloseHandle(stdout_read); CloseHandle(stderr_read); + return exit_code; +} + +static void run_cscript_unknown_option_test(void) +{ + char stdout_buf[4096], stderr_buf[4096]; + DWORD exit_code; + + exit_code = run_cscript("//nologo /unknownoption", stdout_buf, sizeof(stdout_buf), + stderr_buf, sizeof(stderr_buf)); + if(exit_code == ~0u) + return; + ok(exit_code == 1, "exit_code = %lu, expected 1\n", exit_code); + /* On Windows, cscript reports a localized error on stdout. On Wine, + * unrecognized single-/ options are treated as filenames to allow Unix-style + * paths (/tmp/foo.vbs), so the error will be about the file instead. */ + ok(stdout_buf[0] != 0, "expected error message on stdout\n"); + ok(stderr_buf[0] == 0, "expected no stderr output, got: %s\n", stderr_buf); +} + +static void run_cscript_no_file_test(void) +{ + char stdout_buf[4096], stderr_buf[4096]; + DWORD exit_code; + + exit_code = run_cscript("//nologo", stdout_buf, sizeof(stdout_buf), + stderr_buf, sizeof(stderr_buf)); + if(exit_code == ~0u) + return; + ok(exit_code == 1, "exit_code = %lu, expected 1\n", exit_code); + ok(stdout_buf[0] != 0, "expected error message on stdout\n"); +} + +static void run_cscript_usage_test(void) +{ + char stdout_buf[4096], stderr_buf[4096]; + DWORD exit_code; + + exit_code = run_cscript("", stdout_buf, sizeof(stdout_buf), + stderr_buf, sizeof(stderr_buf)); + if(exit_code == ~0u) + return; + ok(exit_code == 0, "exit_code = %lu, expected 0\n", exit_code); + ok(stdout_buf[0] != 0, "expected usage output on stdout\n"); +} + +static BOOL create_temp_vbs(const char *file_name, const char *data) +{ + HANDLE file; + DWORD size; + BOOL bres; + + file = CreateFileA(file_name, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, NULL); + ok(file != INVALID_HANDLE_VALUE, "CreateFile failed: %lu\n", GetLastError()); + if(file == INVALID_HANDLE_VALUE) + return FALSE; + + bres = WriteFile(file, data, strlen(data), &size, NULL); + CloseHandle(file); + ok(bres, "Could not write to file: %lu\n", GetLastError()); + return bres; +} + +static void run_cscript_logo_test(void) +{ + static const char empty_script[] = "\n"; + char file_name[] = "test_logo.vbs"; + char stdout_buf[4096], stderr_buf[4096]; + DWORD exit_code; + size_t len; + + if(!create_temp_vbs(file_name, empty_script)) + goto cleanup; + + /* Without //nologo: banner should be shown on stdout */ + exit_code = run_cscript(file_name, stdout_buf, sizeof(stdout_buf), + stderr_buf, sizeof(stderr_buf)); + if(exit_code == ~0u) + goto cleanup; + ok(exit_code == 0, "exit_code = %lu, expected 0\n", exit_code); + ok(stdout_buf[0] != 0, "expected banner in stdout\n"); + + /* Banner should be followed by a blank line */ + ok(strstr(stdout_buf, "\r\n\r\n") != NULL, + "expected blank line after banner, got: %s\n", stdout_buf); + + /* Output should end with a blank line (\r\n\r\n at the end) */ + len = strlen(stdout_buf); + ok(len >= 4 && !memcmp(stdout_buf + len - 4, "\r\n\r\n", 4), + "expected trailing blank line, got: %s\n", stdout_buf); + +cleanup: + DeleteFileA(file_name); +} + +static void run_cscript_nologo_test(void) +{ + static const char empty_script[] = "\n"; + char file_name[] = "test_nologo.vbs"; + char stdout_buf[4096], stderr_buf[4096]; + DWORD exit_code; + + if(!create_temp_vbs(file_name, empty_script)) + goto cleanup; + + /* With //nologo: no banner, no output for empty script */ + exit_code = run_cscript("//nologo test_nologo.vbs", stdout_buf, sizeof(stdout_buf), + stderr_buf, sizeof(stderr_buf)); + if(exit_code == ~0u) + goto cleanup; + ok(exit_code == 0, "exit_code = %lu, expected 0\n", exit_code); + ok(stdout_buf[0] == 0, "expected no stdout output with //nologo, got: %s\n", stdout_buf); + +cleanup: + DeleteFileA(file_name); +} + +static void run_cscript_host_option_after_filename_test(void) +{ + /* Script that echoes its argument count */ + static const char script_data[] = "WScript.Echo WScript.Arguments.Count\n"; + char file_name[] = "test_args.vbs"; + char stdout_buf[4096], stderr_buf[4096]; + DWORD exit_code; + + if(!create_temp_vbs(file_name, script_data)) + goto cleanup; + + /* //nologo after filename should be consumed as host option, not passed as arg */ + exit_code = run_cscript("test_args.vbs //nologo", stdout_buf, sizeof(stdout_buf), + stderr_buf, sizeof(stderr_buf)); + if(exit_code == ~0u) + goto cleanup; + ok(exit_code == 0, "exit_code = %lu, expected 0\n", exit_code); + /* No banner (//nologo consumed) and arg count = 0 */ + ok(stdout_buf[0] == '0', + "expected '0' as first output (no banner, //nologo consumed as host option), got: %s\n", stdout_buf); + + /* //nologo between script args should be consumed */ + exit_code = run_cscript("//nologo test_args.vbs arg1 //b arg2", stdout_buf, sizeof(stdout_buf), + stderr_buf, sizeof(stderr_buf)); + if(exit_code == ~0u) + goto cleanup; + ok(exit_code == 0, "exit_code = %lu, expected 0\n", exit_code); + /* //b suppresses output AND is consumed: arg count should be 2 (arg1 + arg2) */ + ok(stdout_buf[0] == 0 || strstr(stdout_buf, "2") != NULL, + "//b between args should be consumed, got: %s\n", stdout_buf); cleanup: DeleteFileA(file_name); } +static void run_cscript_nonexistent_file_test(void) +{ + char stdout_buf[4096], stderr_buf[4096]; + DWORD exit_code; + + /* Nonexistent file with //nologo: error on stdout */ + exit_code = run_cscript("//nologo nonexistent_file.vbs", stdout_buf, sizeof(stdout_buf), + stderr_buf, sizeof(stderr_buf)); + if(exit_code == ~0u) + return; + ok(exit_code == 1, "exit_code = %lu, expected 1\n", exit_code); + ok(strstr(stdout_buf, "nonexistent_file") != NULL, + "expected filename in error on stdout, got: %s\n", stdout_buf); + + /* Nonexistent file without //nologo: banner + error on stdout */ + exit_code = run_cscript("nonexistent_file.vbs", stdout_buf, sizeof(stdout_buf), + stderr_buf, sizeof(stderr_buf)); + if(exit_code == ~0u) + return; + ok(exit_code == 1, "exit_code = %lu, expected 1\n", exit_code); + ok(strstr(stdout_buf, "nonexistent_file") != NULL, + "expected filename in error on stdout, got: %s\n", stdout_buf); +} + +static void run_cscript_error_on_stdout_test(void) +{ + char stdout_buf[4096], stderr_buf[4096]; + DWORD exit_code; + + /* Unknown option without //nologo: banner + error on stdout */ + exit_code = run_cscript("/unknownoption", stdout_buf, sizeof(stdout_buf), + stderr_buf, sizeof(stderr_buf)); + if(exit_code == ~0u) + return; + ok(exit_code == 1, "exit_code = %lu, expected 1\n", exit_code); + ok(stdout_buf[0] != 0, "expected banner and error on stdout\n"); + /* Error message should be on stdout, not stderr */ + ok(stderr_buf[0] == 0, "expected no stderr output, got: %s\n", stderr_buf); +} + START_TEST(run) { char **argv; @@ -551,6 +860,14 @@ START_TEST(run) "winetest.ok(false, 'not quit?');\n", 3); run_cscript_error_test(); + run_cscript_unknown_option_test(); + run_cscript_no_file_test(); + run_cscript_usage_test(); + run_cscript_logo_test(); + run_cscript_nologo_test(); + run_cscript_host_option_after_filename_test(); + run_cscript_nonexistent_file_test(); + run_cscript_error_on_stdout_test(); } init_registry(FALSE); diff --git a/programs/wscript/wscript.rc b/programs/wscript/wscript.rc new file mode 100644 index 00000000000..fe877e2b03a --- /dev/null +++ b/programs/wscript/wscript.rc @@ -0,0 +1,54 @@ +/* + * Copyright 2025 Francis De Brabandere + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include <windef.h> +#include "resource.h" + +#pragma makedep po + +LANGUAGE LANG_ENGLISH, SUBLANG_DEFAULT + +STRINGTABLE +{ + IDS_USAGE, +"Usage: CScript scriptname.extension [option...] [arguments...]\n\ +\n\ +Options:\n\ + //B Batch mode: Suppresses script errors and prompts from displaying\n\ + //D Enable Active Debugging\n\ + //E:engine Use engine for executing script\n\ + //H:CScript Changes the default script host to CScript.exe\n\ + //H:WScript Changes the default script host to WScript.exe (default)\n\ + //I Interactive mode (default, opposite of //B)\n\ + //Job:xxxx Execute a WSF job\n\ + //Logo Display logo (default)\n\ + //Nologo Prevent logo display: No banner will be shown at execution time\n\ + //S Save current command line options for this user\n\ + //T:nn Time out in seconds: Maximum time a script is permitted to run\n\ + //X Execute script in debugger\n\ + //U Use Unicode for redirected I/O from the console\n" + + IDS_NO_SCRIPT_FILE, "Input Error: There is no script file specified.\n" + + IDS_FILE_NOT_FOUND, "Input Error: Can not find script file ""%1"".\n" + + IDS_SCRIPT_LOAD_ERROR, "CScript Error: Loading script ""%1"" failed (%2).\n" +} + + + -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10518