在c#的同一环境中执行多个命令

本文关键字:执行 命令 环境 | 更新日期: 2023-09-27 17:49:30

我正在开发一个小的c# GUI工具,它应该获取一些c++代码并在经过一些向导后编译它。如果我在运行著名的vcvarsall.bat之后从命令提示符运行它,这一切都很好。现在我希望用户不要先去命令提示符,而是让程序调用vcvars,然后是nmake和我需要的其他工具。要使vcvars设置的环境变量正常工作,显然应该保留。

我该怎么做呢?

我能找到的最好的解决方案是创建一个临时的cmd/bat脚本,它将调用其他工具,但我想知道是否有更好的方法。


更新:我同时试验了批处理文件和cmd。当使用批处理文件时,vcvars将终止整个批处理执行,因此我的第二个命令(即nmake)将不会执行。我目前的解决方法是这样的(缩短):

string command = "nmake";
string args = "";
string vcvars = "...vcvarsall.bat";
ProcessStartInfo info = new ProcessStartInfo();
info.WorkingDirectory = workingdir;
info.FileName = "cmd";
info.Arguments = "/c '"" + vcvars + " x86 && " + command + " " + args + "'"";
info.CreateNoWindow = true;
info.UseShellExecute = false;
info.RedirectStandardOutput = true;
Process p = Process.Start(info);

这可以工作,但是没有捕获cmd调用的输出。还在寻找更好的

在c#的同一环境中执行多个命令

我有几个不同的建议

  1. 你可能想研究使用MSBuild而不是NMake

    它更复杂,但它可以直接从。net进行控制,并且它是VS项目文件的格式,适用于从VS 2010开始的所有项目,以及c#/VB/等。

  2. 您可以使用一个小的辅助程序捕获环境并将其注入到您的进程中

    这可能有点过分,但它会工作。bat除了设置几个环境变量之外,并没有做任何神奇的事情,所以您所要做的就是记录运行它的结果,然后将其重播到您创建的进程的环境中。

辅助程序 (envcapture.exe)是微不足道的。它只是列出环境中的所有变量,并将它们打印到标准输出。这是整个程序代码;把它放在Main():

XElement documentElement = new XElement("Environment");
foreach (DictionaryEntry envVariable in Environment.GetEnvironmentVariables())
{
    documentElement.Add(new XElement(
        "Variable",
        new XAttribute("Name", envVariable.Key),
        envVariable.Value
        ));
}
Console.WriteLine(documentElement);

您可以通过调用set而不是这个程序来解析输出,但是如果任何环境变量包含换行符,则可能会中断。

在主程序中:

首先,必须捕获由vcvarsmall .bat初始化的环境。为此,我们将使用一个类似cmd.exe /s /c " "...'vcvarsall.bat" x86 && "...'envcapture.exe" "的命令行。Vcvarsall.bat修改环境,然后envcapture.exe打印出来。然后,主程序捕获该输出并将其解析为字典。(注意:vsVersion在这里可能是90或100或110)

private static Dictionary<string, string> CaptureBuildEnvironment(
    int vsVersion, 
    string architectureName
    )
{
    // assume the helper is in the same directory as this exe
    string myExeDir = Path.GetDirectoryName(
        Assembly.GetExecutingAssembly().Location
        );
    string envCaptureExe = Path.Combine(myExeDir, "envcapture.exe");
    string vsToolsVariableName = String.Format("VS{0}COMNTOOLS", vsVersion);
    string envSetupScript = Path.Combine(
        Environment.GetEnvironmentVariable(vsToolsVariableName),
        @"..'..'VC'vcvarsall.bat"
        );
    using (Process envCaptureProcess = new Process())
    {
        envCaptureProcess.StartInfo.FileName = "cmd.exe";
        // the /s and the extra quotes make sure that paths with
        // spaces in the names are handled properly
        envCaptureProcess.StartInfo.Arguments = String.Format(
            "/s /c '" '"{0}'" {1} && '"{2}'" '"",
            envSetupScript,
            architectureName,
            envCaptureExe
            );
        envCaptureProcess.StartInfo.RedirectStandardOutput = true;
        envCaptureProcess.StartInfo.RedirectStandardError = true;
        envCaptureProcess.StartInfo.UseShellExecute = false;
        envCaptureProcess.StartInfo.CreateNoWindow = true;
        envCaptureProcess.Start();
        // read and discard standard error, or else we won't get output from
        // envcapture.exe at all
        envCaptureProcess.ErrorDataReceived += (sender, e) => { };
        envCaptureProcess.BeginErrorReadLine();
        string outputString = envCaptureProcess.StandardOutput.ReadToEnd();
        // vsVersion < 110 prints out a line in vcvars*.bat. Ignore 
        // everything before the first '<'.
        int xmlStartIndex = outputString.IndexOf('<');
        if (xmlStartIndex == -1)
        {
            throw new Exception("No environment block was captured");
        }
        XElement documentElement = XElement.Parse(
            outputString.Substring(xmlStartIndex)
            );
        Dictionary<string, string> capturedVars 
            = new Dictionary<string, string>();
        foreach (XElement variable in documentElement.Elements("Variable"))
        {
            capturedVars.Add(
                (string)variable.Attribute("Name"),
                (string)variable
                );
        }
        return capturedVars;
    }
}

稍后,当您想要在构建环境中运行命令时,您只需将新进程中的环境变量替换为先前捕获的环境变量。您应该只需要在每次运行程序时对每个参数组合调用一次CaptureBuildEnvironment。不要试图在运行之间保存它,否则它会过时的。

static void Main()
{
    string command = "nmake";
    string args = "";
    Dictionary<string, string> buildEnvironment = 
        CaptureBuildEnvironment(100, "x86");
    ProcessStartInfo info = new ProcessStartInfo();
    // the search path from the adjusted environment doesn't seem
    // to get used in Process.Start, but cmd will use it.
    info.FileName = "cmd.exe";
    info.Arguments = String.Format(
        "/s /c '" '"{0}'" {1} '"",
        command,
        args
        );
    info.CreateNoWindow = true;
    info.UseShellExecute = false;
    info.RedirectStandardOutput = true;
    info.RedirectStandardError = true;
    foreach (var i in buildEnvironment)
    {
        info.EnvironmentVariables[(string)i.Key] = (string)i.Value;
    }
    using (Process p = Process.Start(info))
    {
        // do something with your process. If you're capturing standard output,
        // you'll also need to capture standard error. Be careful to avoid the
        // deadlock bug mentioned in the docs for
        // ProcessStartInfo.RedirectStandardOutput. 
    }
}

如果您使用这个,请注意,如果vcvarsmall .bat丢失或失败,它可能会可怕地死亡,并且使用非en-US语言环境的系统可能会出现问题。

可能没有比收集所需的所有数据,生成bat文件并使用Process类运行它更好的方法了。正如你所写的,你正在重定向输出,这意味着你必须设置UseShellExecute = false;,所以我认为没有办法设置你的变量,然后从bat文件调用set。

编辑:为nmake调用添加一个特定的用例

我需要得到各种各样的"构建路径的东西"在过去,这是我所使用的-你可能需要调整这里或那里的东西来适应,但基本上,vcvars做的唯一的事情是设置一堆路径;这些辅助方法获取路径名,你只需要将它们传递到start info:

public static string GetFrameworkPath()
{
    var frameworkVersion = string.Format("v{0}.{1}.{2}", Environment.Version.Major, Environment.Version.Minor, Environment.Version.Build);
    var is64BitProcess = Environment.Is64BitProcess;
    var windowsPath = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
    return Path.Combine(windowsPath, "Microsoft.NET", is64BitProcess ? "Framework64" : "Framework", frameworkVersion);  
}
public static string GetPathToVisualStudio(string version)
{   
    var is64BitProcess = Environment.Is64BitProcess;
    var registryKeyName = string.Format(@"Software'{0}Microsoft'VisualStudio'SxS'VC7", is64BitProcess ? @"Wow6432Node'" : string.Empty);
    var vsKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(registryKeyName);
    var versionExists = vsKey.GetValueNames().Any(valueName => valueName.Equals(version));
    if(versionExists)
    {
        return vsKey.GetValue(version).ToString();
    }
    else
    {
        return null;
    }
}

你可以这样利用这些东西:

var paths = new[]
    { 
        GetFrameworkPath(), 
        GetPathToVisualStudio("10.0"),
        Path.Combine(GetPathToVisualStudio("10.0"), "bin"),
    };  
var previousPaths = Environment.GetEnvironmentVariable("PATH").ToString();
var newPaths = string.Join(";", previousPaths.Split(';').Concat(paths));
Environment.SetEnvironmentVariable("PATH", newPaths);
var startInfo = new ProcessStartInfo()
{
    FileName = "nmake",
    Arguments = "whatever you'd pass in here",
};
var process = Process.Start(startInfo);