C#调用执行命令行窗口cmd,及需要交互执行的处理

1,884 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第13天,点击查看活动详情

C#执行外部程序用到的是Process进程类,打开一个进程,可以指定进程的启动信息StartInfo(启动的程序名、输入输出是否重定向、是否显示UI界面、一些必要参数等)。

相关代码在网上可以找到很多,本篇在参考这些代码的基础上,进行一些修改,尤其是后面探讨交互执行时的情况。

交互执行输入信息,完成程序的执行,是一个相对很必要的情况。

需要添加命名空间System.Diagnostics

CMCode项目中添加文件夹ExecApplications,存放执行外部程序的相关文件代码。

定义一个Process执行外部程序的输出类

public class ExecResult
{
    public string Output { get; set; }
    /// <summary>
    /// 程序正常执行后的错误输出,需要根据实际内容判断是否成功。如果Output为空但Error不为空,则基本可以说明发生了问题或错误,但是可以正常执行结束
    /// </summary>
    public string Error { get; set; }
    /// <summary>
    /// 执行发生的异常,表示程序没有正常执行并结束
    /// </summary>
    public Exception ExceptError { get; set; }
}

调用cmd执行命令行

调用cmd直接执行

如下为执行cmd的帮助类,提供了异步Async方法、同步方法和一次执行多个命令的方法。主要代码部分都有注释。

获取Windows系统环境特殊文件夹路径的方法: Environment.GetFolderPath(Environment.SpecialFolder.SystemX86),获取C:\Windows\System32\

/// <summary>
/// 执行cmd命令
/// </summary>
public static class ExecCMD
{
    #region 异步方法
    /// <summary>
    /// 执行cmd命令 返回cmd窗口显示的信息
    /// 多命令请使用批处理命令连接符:
    /// <![CDATA[
    /// &:同时执行两个命令
    /// |:将上一个命令的输出,作为下一个命令的输入
    /// &&:当&&前的命令成功时,才执行&&后的命令
    /// ||:当||前的命令失败时,才执行||后的命令]]>
    /// </summary>
    ///<param name="command">执行的命令</param>
    ///<param name="workDirectory">工作目录</param>
    /// <returns>cmd命令执行窗口的输出</returns>
    public static async Task<ExecResult> RunAsync(string command,string workDirectory=null)
    {
        command = command.Trim().TrimEnd('&') + "&exit";  //说明:不管命令是否成功均执行exit命令,否则当调用ReadToEnd()方法时,会处于假死状态

        string cmdFileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SystemX86), "cmd.exe");// @"C:\Windows\System32\cmd.exe";
        using (Process p = new Process())
        {
            var result = new ExecResult();
            try
            {
                p.StartInfo.FileName = cmdFileName;
                p.StartInfo.UseShellExecute = false;        //是否使用操作系统shell启动
                p.StartInfo.RedirectStandardInput = true;   //接受来自调用程序的输入信息
                p.StartInfo.RedirectStandardOutput = true;  //由调用程序获取输出信息
                p.StartInfo.RedirectStandardError = true;   //重定向标准错误输出
                p.StartInfo.CreateNoWindow = true;          //不显示程序窗口

                if (!string.IsNullOrWhiteSpace(workDirectory))
                {
                    p.StartInfo.WorkingDirectory = workDirectory;
                }

                p.Start();//启动程序

                //向cmd窗口写入命令
                p.StandardInput.WriteLine(command);
                p.StandardInput.AutoFlush = true;

                // 若要使用StandardError,必须设置ProcessStartInfo.UseShellExecute为false,并且必须设置 ProcessStartInfo.RedirectStandardError 为 true。 否则,从 StandardError 流中读取将引发异常。
                //获取cmd的输出信息
                result.Output = await p.StandardOutput.ReadToEndAsync();
                result.Error = await p.StandardError.ReadToEndAsync();

                p.WaitForExit();//等待程序执行完退出进程。应在最后调用
                p.Close();
            }
            catch (Exception ex)
            {
                result.ExceptError = ex;
            }
            return result;
        }
    }

    /// <summary>
    /// 执行多个cmd命令 返回cmd窗口显示的信息
    /// 此处执行的多条命令并不是交互执行的信息,是多条独立的命令。也可以使用&连接多条命令为一句执行
    /// </summary>
    ///<param name="command">执行的命令</param>
    /// <returns>cmd命令执行窗口的输出</returns>
    /// <returns>工作目录</returns>
    public static async Task<ExecResult> RunAsync(string[] commands,string workDirectory=null)
    {
        if (commands == null)
        {
            throw new ArgumentNullException();
        }
        if (commands.Length == 0)
        {
            return default(ExecResult);
        }
        return await Task.Run(() =>
        {
            commands[commands.Length - 1] = commands[commands.Length - 1].Trim().TrimEnd('&') + "&exit";  //说明:不管命令是否成功均执行exit命令

            string cmdFileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SystemX86), "cmd.exe");// @"C:\Windows\System32\cmd.exe";
            using (Process p = new Process())
            {
                var result = new ExecResult();
                try
                {
                    p.StartInfo.FileName = cmdFileName;
                    p.StartInfo.UseShellExecute = false;        //是否使用操作系统shell启动
                    p.StartInfo.RedirectStandardInput = true;   //接受来自调用程序的输入信息
                    p.StartInfo.RedirectStandardOutput = true;  //由调用程序获取输出信息
                    p.StartInfo.RedirectStandardError = true;   //重定向标准错误输出
                    p.StartInfo.CreateNoWindow = true;          //不显示程序窗口

                    if (!string.IsNullOrWhiteSpace(workDirectory))
                    {
                        p.StartInfo.WorkingDirectory = workDirectory;
                    }

                    // 接受输出的方式逐次执行每条
                    //var output = string.Empty;
                    var inputI = 1;
                    p.OutputDataReceived += (sender, e) =>
                    {
                        // cmd中的输出会包含换行;其他应用可以考虑接收数据是添加换行 Environment.NewLine
                        result.Output +=$"{ e.Data}{Environment.NewLine}" ;// 获取输出 
                        if (inputI >= commands.Length)
                        {
                            return;
                        }
                        if (e.Data.Contains(commands[inputI - 1]))
                        {
                            p.StandardInput.WriteLine(commands[inputI]);
                        }
                        inputI++;
                    };
                    
                    p.ErrorDataReceived+= (sender, e) =>
                    {
                        result.Error += $"{ e.Data}{Environment.NewLine}";// 获取输出 
                        if (inputI>= commands.Length)
                        {
                            return;
                        }
                        if (e.Data.Contains(commands[inputI - 1]))
                        {
                            p.StandardInput.WriteLine(commands[inputI]);
                        }
                        inputI++;
                    };

                    p.Start();//启动程序

                    // 开始异步读取输出流
                    p.BeginOutputReadLine();
                    p.BeginErrorReadLine();
                    //向cmd窗口写入命令
                    p.StandardInput.WriteLine(commands[0]);
                    p.StandardInput.AutoFlush = true;

                    p.WaitForExit();//等待程序执行完退出进程。应在最后调用
                    p.Close();
                }
                catch (Exception ex)
                {
                    result.ExceptError = ex;
                }
                return result;
            }
        });
    }
    #endregion
    /// <summary>
    /// 执行cmd命令 返回cmd窗口显示的信息
    /// 多命令请使用批处理命令连接符:
    /// <![CDATA[
    /// &:同时执行两个命令
    /// |:将上一个命令的输出,作为下一个命令的输入
    /// &&:当&&前的命令成功时,才执行&&后的命令
    /// ||:当||前的命令失败时,才执行||后的命令]]>
    /// </summary>
    ///<param name="command">执行的命令</param>
    ///<param name="workDirectory">工作目录</param>
    public static ExecResult Run(string command, string workDirectory = null)
    {
        command = command.Trim().TrimEnd('&') + "&exit";  //说明:不管命令是否成功均执行exit命令,否则当调用ReadToEnd()方法时,会处于假死状态

        string cmdFileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SystemX86), "cmd.exe");// @"C:\Windows\System32\cmd.exe";
        using (Process p = new Process())
        {
            var result = new ExecResult();
            try
            {
                p.StartInfo.FileName = cmdFileName;
                p.StartInfo.UseShellExecute = false;        //是否使用操作系统shell启动,设置为false可以重定向输入输出错误流;同时会影响WorkingDirectory的值
                p.StartInfo.RedirectStandardInput = true;   //接受来自调用程序的输入信息
                p.StartInfo.RedirectStandardOutput = true;  //由调用程序获取输出信息
                p.StartInfo.RedirectStandardError = true;   //重定向标准错误输出
                p.StartInfo.CreateNoWindow = true;          //不显示程序窗口

                if (!string.IsNullOrWhiteSpace(workDirectory))
                {
                    p.StartInfo.WorkingDirectory = workDirectory;
                }

                p.Start();//启动程序

                //向cmd窗口写入命令
                p.StandardInput.WriteLine(command);
                p.StandardInput.AutoFlush = true;

                //获取cmd的输出信息
                result.Output = p.StandardOutput.ReadToEnd();
                result.Error = p.StandardError.ReadToEnd();
                p.WaitForExit();//等待程序执行完退出进程。应在最后调用
                p.Close();
            }
            catch (Exception ex)
            {
                result.ExceptError = ex;
            }
            return result;
        }
    }
}

调用cmd执行交互命令

很多情况下,命令的执行都需要输入交互信息。在C#调用cmd执行时,处理交互信息却并不是直接输入那么简单(可自行测试...)

cmd中执行获取输出一般意味着命令(或新程序)已经执行结束,而交互命令则需要未执行完等待输入。

因此,大多数情况下交互信息都会新开一个窗口进行输入,这就不受当前Process的控制了,通常是等待使用者输入。这时需要额外操作新开窗口输入信息。

当然,也会有在当前主线程等待输入的情况,这时直接输入就可以了。

总之,交互命令或信息的输入,要根据实际情况来出来,很难抽象为一个统一的方法。

下面以PostgreSQL的createuser命令(位于其bin目录)为例,借助Win32 API窗口句柄相关的EnumWindows方法遍历查找新开的cmd窗口,通过SendMessage输入密码和回车,最后关闭窗口(之前已经介绍过窗口句柄的操作,相关方法作为WndHelper帮助类直接使用,如有需要请查看之前文章,不再重复)。完成用户的新建过程。

由于是新打开了窗口,尝试连续输入、或异步读取输出都无法与新窗口交互,只能借助窗口句柄操作,也可以改为UI自动化输入信息。

PostgreSQL命令行创建用户和数据库,也可以查看之前的介绍。

// 用户密码
var userName = "myuser1";
var userPwd = "mypwd1";

// bin路径
var psqlBinPath = Path.Combine("C:\PostgreSQL", "pgsql", "bin");
// 执行createuser.exe
var result_cuser = await RunCMDPSqlCreateUserAsync(Path.Combine(psqlBinPath, "createuser.exe")+$" -P {userName}", userPwd);

如下为命令行执行时新窗口交互输入密码口令的实现。有打开窗口需要时间,因此查找窗口前等待一段时间,同时,输入口令和确认口令时也有个等待时间。

执行完成后需要关闭新开的窗口,否则会一直处于等待中。

需要注意的是,新开的窗口的标题应该包含执行的命令,但是其显示的内容命令中多了一个空格,因此,查找上也是额外处理的。

传入的命令C:\PostgreSQL\pgsql\bin\createuser.exe -P myuser1,在新窗口标题中变为了C:\PostgreSQL\pgsql\bin\createuser.exe -P myuser1

/// <summary>
/// 执行cmd命令行创建用户,交互输入密码
/// </summary>
///<param name="createCommand">执行的命令</param>
/// <param name="userPwd">交互输入的口令密码</param>
/// <returns>cmd命令执行的输出</returns>
public static async System.Threading.Tasks.Task<ExecResult> RunCMDPSqlCreateUserAsync(string createCommand, string userPwd)
{
    createCommand = createCommand.Trim().TrimEnd('&');

    string cmdFileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe");// @"C:\Windows\System32\cmd.exe";
    using (Process p = new Process())
    {
        var result = new ExecResult();
        try
        {

            p.StartInfo.FileName = cmdFileName;
            p.StartInfo.UseShellExecute = false;        //是否使用操作系统shell启动
            p.StartInfo.RedirectStandardInput = true;   //接受来自调用程序的输入信息
            p.StartInfo.RedirectStandardOutput = true;  //由调用程序获取输出信息
            p.StartInfo.RedirectStandardError = true;   //重定向标准错误输出
            p.StartInfo.CreateNoWindow = false;          //需要显示程序窗口(操作新打开的确认口令窗口)

            p.Start();//启动程序

            p.StandardInput.AutoFlush = false;
            //向cmd窗口写入命令
            p.StandardInput.WriteLine(createCommand);
            p.StandardInput.Flush();
            // 等待一会,等待新窗口打开
            Thread.Sleep(500);

            // 命令行窗口包标题-P前包含两个空格,和传入的不符,因此无法查找到 @"管理员: C:\WINDOWS\system32\cmd.exe - C:\PostgreSQL\pgsql\bin\createuser.exe  -P myuser".Contains(command);
            var windows = WndHelper.FindAllWindows(x => x.Title.Contains(@"C:\PostgreSQL\pgsql\bin\createuser.exe")); // x.Title.Contains(command)
            var window = windows[0];

            WndHelper.SendText(window.Hwnd, userPwd);
            WndHelper.SendEnter(window.Hwnd);
            // 等待
            Thread.Sleep(100);

            WndHelper.SendText(window.Hwnd, userPwd);
            WndHelper.SendEnter(window.Hwnd);
            // 等待 需要等待多一点再关闭,否则可能创建不成功
            Thread.Sleep(500);

            // 处理完后关闭窗口,否则后面一直阻塞
            WndHelper.CloseWindow(window.Hwnd);
            // 或者发送 exit 和回车

            // 不能直接使用标准输出,会一直等待;需要关闭新打开的窗口(或执行完exti退出)
            result.Output = await p.StandardOutput.ReadToEndAsync();
            result.Error = await p.StandardError.ReadToEndAsync();
            p.WaitForExit();//等待程序执行完退出进程。应在最后调用
            p.Close();
        }
        catch (Exception ex)
        {
            result.ExceptError = ex;
        }
        return result;
    }
}

执行结果如下:

为了实现这个交互查找了很多资料,后来在大佬的提醒下,显示窗口查看一下问题。从而确认是新开了窗口。

未显示窗口时查看异步输出信息,会一直等待没有确认口令的提示:

显示窗口时,可以看到新的窗口有提示确认口令的信息,这个信息和原Process接受到的是分开的,如果通过Process与此新窗口交互。

cmd命令作为参数执行时需要指定/K

在执行cmd命令时,也可以将命令作为cmd的启动参数执行

但是,如果执行下面的命令,将会看到没有任何效果(只打开cmd窗口)。

Process.Start(@"C:\WINDOWS\system32\cmd.exe", "ping 127.0.0.1");

命令作为cmd的启动参数执行时,需要在最开始指定/K参数。

Process.Start(@"C:\WINDOWS\system32\cmd.exe", "/K ping 127.0.0.1");

将会正确执行。

或者,使用ProcessStartInfo对象。

Process process = new Process();
ProcessStartInfo startInfo = new ProcessStartInfo();
//startInfo.WindowStyle = ProcessWindowStyle.Hidden;
startInfo.FileName = "cmd.exe";
startInfo.Arguments = "/K ping 127.0.0.1";
process.StartInfo = startInfo;
process.Start();

cmd /k 的含义是执行后面的命令,并且执行完毕后保留窗口。

参考