如何从 ProcessStartInfo.Arguments 之类的单个参数字符串中获取单独的参数?

How to get separate arguments from single arguments string like ProcessStartInfo.Arguments?

假设我有一个 shell 命令行。一行文本,与您在 shell 中键入的内容完全相同。从参数中拆分命令是微不足道的。在世界上的每个 shell 中,它总是在命令后的 space。我知道,该命令理论上可以包含 spaces,但让我们假设它是一段时间的正常命令。

当我将参数作为字符串传递给 ProcessStartInfo.Arguments 字符串时 - 它会起作用。但这不是我想要的。

我希望将参数分开。 每一个。当参数类似于 @"C:\Program Files" - 它应该是一个参数。

手动获取它非常棘手。因为它对不同的 OSes 和 shells 做的不同。但是 - .NET 以某种方式在内部完成它,我需要开始使用它而不是重新发明轮子。

我真正想做的是首先像这样构建一个参数列表:

new[] { "dir", @"'C:\Program Files'" }

然后像这样:

new[] { "cmd", "/C", @"dir 'C:\Program Files'" }

所以它是双向的——我从一个字符串中提取参数,然后我从单独的参数构建一个参数字符串。全部根据 OS-特定规则。我的代码应该适用于 Linux 和 Windows.

问题是,它必须在多平台上工作,所以在 Linux 上可以是“bash -c”而不是“cmd /C”,你明白了。

要点是让 .NET 自己完成所有引用/取消引用,据我所知,它在 Windows 和 Linux 上都能正确完成。

我确实搜索了 Google 和 Stack Overflow - 好像什么都没有。

我试图在设置 Arguments 属性 后从 ProcessStartInfo 获取 ArgumentList。它不起作用。设置一个 属性 不会设置另一个。您实际上可以同时设置两者并在尝试启动该过程时获取异常。

我敢打赌这个问题绝非微不足道。有什么建议吗?

更新

我在挖掘 .NET 资源和 GitHub 讨论的基础上进行了更多研究。它看起来真的很糟糕(或者真的很好,这取决于一个观点)。好像没有这样的东西。所以我基本上需要一种方法来引用目标 OS 的一个参数,以及一种将命令行解析为未引用部分的方法。对于解析,我将采用标准,使用 FSM(有限状态机算法),引用很简单,只需添加 OS / shell 特定引号即可。

无论如何,如果它在 .NET 中的某个地方只是隐藏和咯咯地笑,请告诉我 ;)

看来它还不存在,所以我为它做了一个脑残的简单破解:

先决条件:

public class SpaceDelimitedStringParser {

    public char Quote { get; set; } = '"';

    public IEnumerable<string> Split(string input) {
        bool isQuoting = false;
        for (int i = 0, s = 0, n = input.Length; i < n; i++) {
            var c = input[i];
            var isWhiteSpace = c is ' ' or '\t';
            var isQuote = c == Quote;
            var isBreak = i == n - 1 || isWhiteSpace && !isQuoting;
            if (isBreak) {
                yield return input[s..i];
                s = i + 1;
                continue;
            }
            if (isWhiteSpace && !isQuoting) continue;
            if (isQuote) {
                if (!isQuoting) {
                    s = i + 1;
                    isQuoting = true;
                }
                else {
                    yield return input[s..i];
                    s = i + 1;
                    isQuoting = false;
                }
            }
        }
    }

    public string Join(IEnumerable<string> items)
        => String.Join(' ', items.Select(i => i.Contains(' ') || i.Contains('\t') ? (Quote + i + Quote) : i));

}

它只是拆分和连接 space 使用引号字符分隔的字符串:双引号。我测试了在 Windows 和 Linux.

上的相似处理

再来个好帮手class:

public class ShellStartInfo {

    public ShellStartInfo(string command, bool direct = false) {
        var (shell, exec) =
            OS.IsLinux ? ("bash", "-c") :
            OS.IsWindows ? ("cmd", "/C") :
            throw new PlatformNotSupportedException();
        var arguments = new[] { exec, command };
        StartInfo = new ProcessStartInfo(shell, new SpaceDelimitedStringParser().Join(arguments));
        if (!direct) {
            StartInfo.RedirectStandardInput = true;
            StartInfo.RedirectStandardOutput = true;
            StartInfo.RedirectStandardError = true;
            StartInfo.StandardOutputEncoding = Encoding.UTF8;
            StartInfo.StandardErrorEncoding = Encoding.UTF8;
        }
    }

    public static implicit operator ProcessStartInfo(ShellStartInfo startInfo) => startInfo.StartInfo;

    private readonly ProcessStartInfo StartInfo;

}

还有一个小帮手:

public static class OS {

    public static bool IsLinux => _IsLinux ?? (_IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux)).Value;

    public static bool IsWindows => _IsWindows ?? (_IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)).Value;

    private static bool? _IsLinux;
    private static bool? _IsWindows;

}

我们可以测试一下:

var command =
    OS.IsLinux ? @"cat 'File name with spaces.txt'" :
    OS.IsWindows ? @"type ""File name with spaces.txt""" :
    throw new PlatformNotSupportedException();
var process = Process.Start(new ShellStartInfo(command));
if (process is null) throw new InvalidOperationException();
await process.WaitForExitAsync();
var output = process.StandardOutput.ReadToEnd();
var error = process.StandardError.ReadToEnd();
if (output.Length > 0) Console.WriteLine($"OUTPUT:\n{output}\nEND.");
if (error.Length > 0) Console.WriteLine($"ERROR:\n{error}\nEND.");

适用于 Windows,适用于 Linux。完成了。这里不需要 split 函数。我认为在某处将参数作为数组传递可能很有用,但是很好。

通过 ArgumentList 属性 或 ProcessStartInfo class 传递参数有点问题。它只是没有按预期工作,尤其是当我将参数作为一个 shell 执行参数传递时。为什么? IDK,像示例中那样传递时有效。

顺便说一句,我的 ShellStartInfo class 还设置了内部 ProcessStartInfo 的一些其他属性,这有助于读取命令的输出。

在我的远程 shell 中它正常工作,本地终端的行为与远程终端相同。它不是完全成熟的 Putty 克隆,它只是一个小工具,可以将一小段“ssh”连接到甚至没有外部 IP、打开 SSH 端口甚至可能不接受 TCP 连接的客户端设备。事实上 - 客户端是一个运行 Linux.

的小型物联网设备