我正在尝试将C#程序的输出重定向到文件。当使用“cmd.exe”时,我可以简单地使用myprogram.exe arg1 arg2 > out.txt来运行它,但是我想使用Visual Studio的“启动选项”来完成同样的事情。

我创建了一个C#空项目并添加了以下代码:

using System;
class Test
{
    public static void Main(string[] args)
    {
        foreach (var arg in args) Console.WriteLine(arg);
    }
}

然后在“项目设置”中编辑了命令行参数:

使用Ctrl + F5运行项目无法正常工作。我将命令行参数打印在控制台中,而不是输出文件中:
arg1
arg2
>
output.txt

如果将命令行参数更改为:arg1 arg2 "> output.txt",则会得到以下输出:
arg1
arg2
^> output.txt

我注意到在Output文件夹中创建了一个空的output.txt文件。

这件事有可能完成吗?还是我不得不继续使用cmd.exe来启动程序?

最佳答案

从严格的意义上讲,您被迫使用命令提示符启动带有重定向输出的程序。否则,您将需要自己解析命令行,GUI shell可能不会这样做。

如果您只是想在Start Debugging时重定向输出,那么取消选中Enable the Visual Studio hosting process复选框,就可以了。

如果没有,实际上您在此处看到的"output.txt"并不是由应用程序生成的,而是由Visual Studio IDE生成的"YourApplication.vshost.exe",它是在您开始调试之前生成的Start Without Debugging。内容将始终为空,并且无法写入;因为它被Hosting Process锁定了。

但是,如果您希望应用程序以任何方式运行,其行为都相同,则情况会更加复杂。

当您开始使用该应用程序进行调试时,它以以下内容开始:



因为输出已经被IDE重定向。

当您使用"devenv"时,它的开头是:



这是让您的应用程序获取您指定的所有参数的正确方法。

您可能需要看一下我以前对How can I detect if "Press any key to continue . . ." will be displayed?的回答。

在这里,我在下面的代码中使用类似atavistic throwback的方法:

  • 应用程序代码
    using System.Diagnostics;
    using System.Linq;
    using System;
    
    class Test {
        public static void Main(string[] args) {
            foreach(var arg in args)
                Console.WriteLine(arg);
        }
    
        static Test() {
            var current=Process.GetCurrentProcess();
            var parent=current.GetParentProcess();
            var grand=parent.GetParentProcess();
    
            if(null==grand
                ||grand.MainModule.FileName!=current.MainModule.FileName)
                using(var child=Process.Start(
                    new ProcessStartInfo {
                        FileName=Environment.GetEnvironmentVariable("comspec"),
                        Arguments="/c\x20"+Environment.CommandLine,
                        RedirectStandardOutput=true,
                        UseShellExecute=false
                    })) {
                    Console.Write(child.StandardOutput.ReadToEnd());
                    child.WaitForExit();
                    Environment.Exit(child.ExitCode);
                }
    #if false // change to true if child process debugging is needed
            else {
                if(!Debugger.IsAttached)
                    Debugger.Launch();
    
                Main(Environment.GetCommandLineArgs().Skip(1).ToArray());
                current.Kill(); // or Environment.Exit(0);
            }
    #endif
        }
    }
    

  • 我们还需要以下代码,以便它可以工作:
  • 扩展方法代码
    using System.Management; // add reference is required
    
    using System.Runtime.InteropServices;
    using System.Diagnostics;
    
    using System.Collections.Generic;
    using System.Linq;
    using System;
    
    public static partial class NativeMethods {
        [DllImport("kernel32.dll")]
        public static extern bool TerminateThread(
            IntPtr hThread, uint dwExitCode);
    
        [DllImport("kernel32.dll")]
        public static extern IntPtr OpenThread(
            uint dwDesiredAccess, bool bInheritHandle, uint dwThreadId);
    }
    
    public static partial class ProcessThreadExtensions /* public methods */ {
        public static void Abort(this ProcessThread t) {
            NativeMethods.TerminateThread(
                NativeMethods.OpenThread(1, false, (uint)t.Id), 1);
        }
    
        public static IEnumerable<Process> GetChildProcesses(this Process p) {
            return p.GetProcesses(1);
        }
    
        public static Process GetParentProcess(this Process p) {
            return p.GetProcesses(-1).SingleOrDefault();
        }
    }
    
    partial class ProcessThreadExtensions /* non-public methods */ {
        static IEnumerable<Process> GetProcesses(
            this Process p, int direction) {
            return
                from format in new[] {
                    "select {0} from Win32_Process where {1}" }
                let selectName=direction<0?"ParentProcessId":"ProcessId"
                let filterName=direction<0?"ProcessId":"ParentProcessId"
                let filter=String.Format("{0} = {1}", p.Id, filterName)
                let query=String.Format(format, selectName, filter)
                let searcher=new ManagementObjectSearcher("root\\CIMV2", query)
                from ManagementObject x in searcher.Get()
                let process=
                    ProcessThreadExtensions.GetProcessById(x[selectName])
                where null!=process
                select process;
        }
    
        // not a good practice to use generics like this;
        // but for the convenience ..
        static Process GetProcessById<T>(T processId) {
            try {
                var id=(int)Convert.ChangeType(processId, typeof(int));
                return Process.GetProcessById(id);
            }
            catch(ArgumentException) {
                return default(Process);
            }
        }
    }
    

  • 由于在调试时父级将是Visual Studio IDE(当前名称为Main)。实际上,父进程和祖 parent 进程是多种多样的,我们需要一条规则来执行一些检查。

    棘手的部分是,孙子是真正碰到null的孙子。每次运行时,代码都会检查祖 parent 进程。如果祖 parent 是%comspec%,那么它将生成,但是生成的进程将是Main,它也是新进程的父级,它将以与current相同的可执行文件开始。因此,如果祖 parent 与自己相同,那么它将不会继续生成,而只会遇到Main

    该代码中使用了Static Constructor,该代码在Debugger.Launch之前启动。 SO上有一个已回答的问题:How does a static constructor work?

    当我们开始调试时,我们正在调试祖 parent 进程(产生)。为了使用孙子进程进行调试,我使用条件编译制作了Main,该条件编译将调用Main,以使ojit_code保持清晰。

    回答有关调试器的问题也将有所帮助:Attach debugger in C# to another process

    07-24 09:44
    查看更多