如何在 C 或 C++ 中包装参数并将它们传递给 system 或 exec*

Posted

技术标签:

【中文标题】如何在 C 或 C++ 中包装参数并将它们传递给 system 或 exec*【英文标题】:How to wrap arguments in C or C++ and pass them to system or exec* 【发布时间】:2020-05-11 09:51:41 【问题描述】:

我想编写一个包装器,用 argv 做一些简单的事情并调用一些脚本。我有以下要求:

包装器必须是.exe 文件 包装器必须能够正确处理空格和引号 包装器将在用户端生成 生成过程必须很小(比如使用https://bellard.org/tcc)

我最初的做法:

编写一个 c 程序首先清理参数,然后将它们用引号括起来,然后调用 system。 不幸的是,我无法从 systemexec* 函数中获得结构良好的行为。 我希望以下所有示例都输出类似arg1=1; arg2=2; arg3=3; arg4= 的内容(引号换行有一些差异),但它在某些示例上出错,并在 execl 上暂停:

输入文件:

@:: 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;

输出运行:

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=

(示例 6 暂停,直到我按下 Enter

问题:

    有没有办法以允许system 中的空格的方式正确包装路径/参数? 我可以在 system 的参数中转义引号吗? 是否有运行exec* 的非阻塞方式? exec* 方法能否确保包装程序的标准输入标准输出和标准错误正常运行(没有奇怪的溢出或奇怪的阻塞?)

【问题讨论】:

不要在 Windows 中使用 exec*。它与 Unix 中的 exec* 完全不同,它实际上替换了进程映像。 Windows CRT 实现只是创建一个新进程并终止调用进程,这对于命令行使用来说是一场噩梦,因为它会终止 shell 正在等待的进程。请改用_[w]spawn*。最好使用_wspawn,因为文件路径是Unicode,除非它恰好是UTF-8,否则活动进程代码页通常是不够的,即使在Windows 10中也很少见。 感谢@Eryk Sun,我会跟随 _wspawn 的领导并提供一些反馈。 _wpawn* 将带有空格的参数(不带引号)连接到lpCommandLineCreateProcessW 参数中。引用参数由调用者决定,因为它不能被概括。但大多数程序都遵循 Microsoft C argv rules。这与 Unix exec* 的简单性形成对比,其中传递的参数数组成为子进程的 argv,并且除了 shell 命令之外,不需要命令行解析。 【参考方案1】:

这样的事情应该可以工作:

 string cmd = "test.bat";

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

 system(cmd.c_str());

当然,其中包含空格的 args 需要通过添加引号来进一步处理,并且带有引号的参数可能需要转义,并且在 args 包含无法直接处理的内容的情况下还有许多其他复杂情况与)

作为替代方案,您可以查看Use CreateProcess to Run a Batch File

【讨论】:

我将更好地了解 CreateProcess。它可能会起作用,但它看起来确实和其他所有东西一样复杂和棘手。它的参数似乎也需要转义和处理,所以要包装一个脚本(可能有空格)和命令行参数(可能有空格和引号等等)还有很长的路要走。 [_w]system(command) 通过CreateProcess[A|W] 执行"%ComSpec%" /c command。如果运行非控制台命令可能是不可取的,因为ComSpec 解释器是 cmd.exe,它将分配一个控制台(空窗口)并等待进程退出。但是当CreateProcessW 失败时,CMD 也会自动退回到ShellExecuteExW,即对于PE 可执行文件和批处理脚本以外的文件类型。如果您直接调用CreateProcessW,则必须手动编写代码。如果您总是在运行批处理脚本,这也不是问题。 对于命令行,CreateProcessW 只关心通过双引号引用,如果它必须解析带有空格的可执行路径lpCommandLine。它不关心命令行的其余部分;这取决于应用程序本身。此外,如果在lpApplicationName 中传递可执行路径,它根本不关心非空lpCommandLine【参考方案2】:

实施

我最终为两种不同的场景编写了两个包装器:一个用于保持终端窗口打开,一个用于静默运行。

它通过将exe 文件重命名为name-of-script.exe 来工作,然后包装器将尝试运行&lt;exedir&gt;\bin\name-of-script.bat 并传递所有命令行参数。

我使用程序Resource Hacker将漂亮的图标或版本信息放入exe中,使其看起来像一个合适的程序。

代码

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

【讨论】:

以上是关于如何在 C 或 C++ 中包装参数并将它们传递给 system 或 exec*的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Mono Project 将 C/C++ 指针传递和保存到 C#,并将它们返回

在 Cython 中包装具有非标准类型作为参数的 C 函数

如何将 C++ 回调传递给 C 库函数?

SWIG 如何在 Python 中包装 map<string,string>?

如何选择来自脚本输出的参数并将其传递给 Bamboo 中的下一个脚本或作业?

将指针分配给指针(并将指针传递给类)时使用删除的 C++ 混淆