1

I want to write a wrappers that does some simple stuff with argv and calls some script. I have the following requirements:

  • the wrappers must be .exe files
  • the wrappers must be able to handle spaces and quotes correctly
  • the wrappers will be generated user-side
  • the generation process must be small (like using https://bellard.org/tcc )

My initial approach:

Write a c program to first sanitize the arguments, then wrap them in quotes, and then call system. Unfortunately, I cannot get well-structured behavior from the system or exec* functions. I expect all of the following examples to output something like arg1=1; arg2=2; arg3=3; arg4= (with some variance in quote wraps), but it errors on some of the examples, and pauses on execl:

Input files:

@:: test.bat
@echo off

echo arg1=%1; arg2=%2; arg3=%3; arg4=%4
//minimal-example.c
#include <Windows.h>
#include <stdio.h>

int main( int argc, char ** argv ) {
  puts("\nExample 1:");
  system("\"test.bat\" \"1\" \"2\" \"3\" ");

  puts("\nExample 2:");
  system("test.bat \"1\" \"2\" \"3\" ");

  puts("\nExample 3:");
  system("test.bat 1 2 \"3\" ");

  puts("\nExample 4:");
  system("\"test.bat\" 1 \"2\" 3 ");

  puts("\nExample 5:");
  system("\"test.bat\" 1 2 3 ");

  puts("\nExample 6:");
  execl(argv[0], "test.bat", "1", "2", "3", NULL);

  return 0;
}

Output run:

Example 1:
'test.bat" "1" "2" "3' is not recognized as an internal or external command,
operable program or batch file.

Example 2:
arg1="1"; arg2="2"; arg3="3"; arg4=

Example 3:
arg1=1; arg2=2; arg3="3"; arg4=

Example 4:
'test.bat" 1 "2' is not recognized as an internal or external command,
operable program or batch file.

Example 5:
arg1=1; arg2=2; arg3=3; arg4=

Example 6:
arg1=1; arg2=2; arg3=3; arg4=

(Example 6 pauses until I press Enter)

Question:

  1. Is there a way to correctly wrap the path/arguments in such a way as to allow spaces in system?
  2. Can I escape quotes in arguments to system?
  3. Is there a non-blocking way to run exec*?
  4. Would a exec* approach ensure that the wrapped program's stdin stdout and stderr behaves correctly (no strange overflows or weird blocking things?)
Simon Streicher
  • 2,638
  • 1
  • 26
  • 30
  • Do not use `exec*` in Windows. It's nothing at all like `exec*` in Unix, which actually replaces the process image. The Windows CRT implementation just creates a new process and terminates the calling process, which is a nightmare for command line usage since it terminates the process that the shell is waiting on. Use `_[w]spawn*` instead. Preferably use `_wspawn` since file paths are Unicode, for which the active process codepage is generally insufficient unless it happens to be UTF-8, which is pretty rare even in Windows 10. – Eryk Sun May 11 '20 at 10:59
  • Thanks @Eryk Sun, I will follow the _wspawn lead and give some feedback. – Simon Streicher May 11 '20 at 11:07
  • `_wpawn*` joins the arguments with spaces, without quoting, into the `lpCommandLine` parameter of `CreateProcessW`. It's up to the caller to quote arguments, since it can't be generalized. But most programs follow Microsoft C [`argv` rules](https://learn.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments?view=vs-2019). This is in contrast to the simplicity of Unix `exec*`, in which the passed argument array becomes the `argv` of the child process, and, other than shell commands, there's no need for command line parsing. – Eryk Sun May 11 '20 at 11:50

2 Answers2

0

Something like this should work:

 string cmd = "test.bat";

 for(int i = 1; i < argc; i++) {
    cmd += " ";
    cmd += argv[i]
 }

 system(cmd.c_str());

Of course, args that have spaces in them will need to be processed further, by adding quotes, and arguments with quotes may need escaping, and lots of other complications in cases where args contains stuff that isn't straight forward to deal with)

As an alternative, you can take a look at Use CreateProcess to Run a Batch File

Stf Kolev
  • 640
  • 1
  • 8
  • 21
  • I'll take a better look at CreateProcess. It might work, but it does seem as complicated and gotcha-ey as everything else. It's arguments also seems to need escaping and dealing with, so it's still a long way to get to wrapping a script (with possible spaces) and it's commandline arguments (with possible spaces and quotes and what-nots). – Simon Streicher May 11 '20 at 10:14
  • `[_w]system(command)` executes `"%ComSpec%" /c command` via `CreateProcess[A|W]`. It's probably not desirable if running a non-console command because the `ComSpec` interpreter is cmd.exe, which will allocate a console (empty window) and wait for the process to exit. But CMD also automatically falls back on `ShellExecuteExW` when `CreateProcessW` fails, i.e. for file types other than PE executables and batch scripts. That's something you'd have to code manually if you called `CreateProcessW` directly. Neither is an issue if you're always running batch scripts anyway. – Eryk Sun May 11 '20 at 10:50
  • As to the command line, `CreateProcessW` only cares about quoting via double quotes if it has to parse an executable path with spaces out of `lpCommandLine`. It doesn't care about the rest of the command line; that's up to the application itself. Moreover, it doesn't care at all about a non-empty `lpCommandLine` if the executable path is passed in `lpApplicationName`. – Eryk Sun May 11 '20 at 10:52
0

Implementation

I ended up writing two wrappers for two different scenarios: One for keeping a terminal window open, and one for a silent run.

It works by renaming the exe file to name-of-script.exe, then the wrapper will try to run <exedir>\bin\name-of-script.bat and pass all the commandline parameters.

I use the program Resource Hacker to put nice icons or version information in the exe, to make it look like a proper program.

Code

tcc.exe -D_UNICODE -DNOSHELL launcher.c -luser32 -lkernel32 -mwindows -o launcher-noshell.exe
tcc.exe -D_UNICODE launcher.c -luser32 -lkernel32 -o launcher.exe
#define _WIN32_WINNT 0x0500
#include <windows.h>
#include <stdbool.h>
#include <tchar.h>

#ifdef NOSHELL
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
#else
int main( int argc, char ** argv ) 
#endif
{
    //*******************************************
    //Get commanline string as a whole
    //*******************************************
    TCHAR* cmdArgs = GetCommandLineW();
    TCHAR* cmdPath;
    cmdPath = (TCHAR*) malloc((_tcslen(cmdArgs)+1)*2);
    _tcscpy(cmdPath, cmdArgs);


    //*******************************************
    //Split filepath, filename, and commandline
    //*******************************************
    bool inQuote = false;
    bool isArgs = false;
    int j = 0;

    for(int i=0; i<_tcslen(cmdArgs)+1; i++){
      //must be easier way to index unicode string
      TCHAR c = *(TCHAR *)(&cmdArgs[i*2]);

      if(c == L'"'){inQuote = !inQuote;}
      if(c == L' ' && !inQuote){ isArgs = true;}

      if(isArgs){
        cmdPath[i*2]   = '\0';
        cmdPath[i*2+1] = '\0';
      }

      //do for both unicode bits
      cmdArgs[j*2  ] = cmdArgs[i*2  ];
      cmdArgs[j*2+1] = cmdArgs[i*2+1];

      //sync j with i after filepath
      if(isArgs){ j++; }
    }


    //*******************************************
    //Remove quotes around filepath
    //*******************************************
    if(*(TCHAR *)(&cmdPath[0]) == L'"'){
      cmdPath = &cmdPath[2];
    }
    int cmdPathEnd = _tcslen(cmdPath);
    if(*(TCHAR *)(&cmdPath[(cmdPathEnd-1)*2]) == L'"'){
      cmdPath[(cmdPathEnd-1)*2]='\0';
      cmdPath[(cmdPathEnd-1)*2+1]='\0';
    }


    //*******************************************
    //Find basedir of cmdPath
    //*******************************************
    TCHAR* cmdBaseDir;
    cmdBaseDir = (TCHAR*) malloc((_tcslen(cmdPath)+1)*2);
    _tcscpy(cmdBaseDir, cmdPath);


    int nrOfSlashed = 0;
    int slashLoc = 0;
    for(int i=0; i<_tcslen(cmdBaseDir); i++){
      //must be easier way to index unicode string
      TCHAR c = *(TCHAR *)(&cmdBaseDir[i*2]);
      if(c == L'\\' || c == L'//'){
        nrOfSlashed+=1;
        slashLoc=i;
      }
    }

    if(nrOfSlashed==0){
      _tcscpy(cmdBaseDir, L".");
    }else{
      cmdBaseDir[2*slashLoc] = '\0';
      cmdBaseDir[2*slashLoc+1] = '\0';  
    }


    //*******************************************
    //Find filename without .exe
    //*******************************************
    TCHAR* cmdName;
    cmdName = (TCHAR*) malloc((_tcslen(cmdPath)+1)*2);
    _tcscpy(cmdName, cmdPath);

    cmdName = &cmdPath[slashLoc==0?0:slashLoc*2+2];
    int fnameend = _tcslen(cmdName);
    if(0 < fnameend-4){
        cmdName[(fnameend-4)*2]   = '\0';
        cmdName[(fnameend-4)*2+1] = '\0';
    }

    //_tprintf(L"%s\n", cmdName);

    //********************************************
    //Bat name to be checked
    //********************************************
    int totlen;

    TCHAR* batFile1  = cmdBaseDir;
    TCHAR* batFile2  = L"\\bin\\";
    TCHAR* batFile3  = cmdName;
    TCHAR* batFile4  = L".bat";

    totlen = (_tcslen(batFile1)+ _tcslen(batFile2)+ _tcslen(batFile3)+ _tcslen(batFile4));

    TCHAR* batFile;
    batFile = (TCHAR*) malloc((totlen+1)*2);
    _tcscpy(batFile, batFile1);
    _tcscat(batFile, batFile2);
    _tcscat(batFile, batFile3);
    _tcscat(batFile, batFile4);

    if(0 != _waccess(batFile, 0)){
        system("powershell -command \"[reflection.assembly]::LoadWithPartialName('System.Windows.Forms')|out-null;[windows.forms.messagebox]::Show('Could not find the launcher .bat in bin directory.', 'Execution error')\" ");
    };

    //_tprintf(L"%s\n", batFile);

    //*******************************************
    //Get into this form: cmd.exe /c ""c:\path\...bat" arg1 arg2 ... "
    //*******************************************
    TCHAR* cmdLine1  = L"cmd.exe /c \"";
    TCHAR* cmdLine2  = L"\"";
    TCHAR* cmdLine3  = batFile;
    TCHAR* cmdLine4  = L"\" "; 
    TCHAR* cmdLine5  = cmdArgs;
    TCHAR* cmdLine6 = L"\"";

    totlen = (_tcslen(cmdLine1)+_tcslen(cmdLine2)+_tcslen(cmdLine3)+_tcslen(cmdLine4)+_tcslen(cmdLine5)+_tcslen(cmdLine6));

    TCHAR* cmdLine;
    cmdLine = (TCHAR*) malloc((totlen+1)*2);

    _tcscpy(cmdLine, cmdLine1);
    _tcscat(cmdLine, cmdLine2);
    _tcscat(cmdLine, cmdLine3);
    _tcscat(cmdLine, cmdLine4);
    _tcscat(cmdLine, cmdLine5);
    _tcscat(cmdLine, cmdLine6);

    //_tprintf(L"%s\n", cmdLine);

    //************************************
    //Prepare and run CreateProcessW
    //************************************
    PROCESS_INFORMATION pi;
    STARTUPINFO si;

    memset(&si, 0, sizeof(si));
    si.cb = sizeof(si);

    #ifdef NOSHELL
    CreateProcessW(NULL, cmdLine, NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
    #else
    CreateProcessW(NULL, cmdLine, NULL, NULL, TRUE, NULL,             NULL, NULL, &si, &pi);
    #endif

    //************************************
    //Return ErrorLevel
    //************************************
    DWORD result = WaitForSingleObject(pi.hProcess,15000);

    if(result == WAIT_TIMEOUT){return -2;} //Timeout error

    DWORD exitCode=0;
    if(!GetExitCodeProcess(pi.hProcess, &exitCode) ){return -1;} //Cannot get exitcode

    return exitCode; //Correct exitcode
}
Simon Streicher
  • 2,638
  • 1
  • 26
  • 30