96

別の実行可能ファイルに渡すコマンド ライン パラメーターを含む単一の文字列があり、コマンドがコマンド ラインで指定された場合に C# と同じ方法で、個々のパラメーターを含む string[] を抽出する必要があります。string[] は、リフレクションを介して別のアセンブリ エントリ ポイントを実行するときに使用されます。

このための標準機能はありますか?または、パラメーターを正しく分割するための推奨される方法 (正規表現?) はありますか? スペースを含む可能性のある '"' で区切られた文字列を正しく処理する必要があるため、' ' で分割することはできません。

文字列の例:

string parameterString = @"/src:""C:\tmp\Some Folder\Sub Folder"" /users:""abcdefg@hijkl.com"" tasks:""SomeTask,Some Other Task"" -someParam foo";

結果の例:

string[] parameterArray = new string[] { 
  @"/src:C:\tmp\Some Folder\Sub Folder",
  @"/users:abcdefg@hijkl.com",
  @"tasks:SomeTask,Some Other Task",
  @"-someParam",
  @"foo"
};

コマンドライン解析ライブラリは必要ありません。生成する必要がある String[] を取得する方法だけです。

更新: C# によって実際に生成されるものと一致するように、期待される結果を変更する必要がありました (分割文字列の余分な " を削除しました)

4

26 に答える 26

109

各文字を調べる関数に基づいて文字列を分割する関数がないことは、私を悩ませます。ある場合は、次のように記述できます。

    public static IEnumerable<string> SplitCommandLine(string commandLine)
    {
        bool inQuotes = false;

        return commandLine.Split(c =>
                                 {
                                     if (c == '\"')
                                         inQuotes = !inQuotes;

                                     return !inQuotes && c == ' ';
                                 })
                          .Select(arg => arg.Trim().TrimMatchingQuotes('\"'))
                          .Where(arg => !string.IsNullOrEmpty(arg));
    }

それを書いたが、必要な拡張メソッドを書いてみませんか。わかりました、あなたは私に話しかけました...

まず、指定された文字が文字列を分割する必要があるかどうかを決定する必要がある関数を取るSplitの私自身のバージョン:

    public static IEnumerable<string> Split(this string str, 
                                            Func<char, bool> controller)
    {
        int nextPiece = 0;

        for (int c = 0; c < str.Length; c++)
        {
            if (controller(str[c]))
            {
                yield return str.Substring(nextPiece, c - nextPiece);
                nextPiece = c + 1;
            }
        }

        yield return str.Substring(nextPiece);
    }

状況によっては空の文字列が生成される場合もありますが、その情報は別の場合に役立つ可能性があるため、この関数では空のエントリを削除しません。

2 つ目は (そしてもっとありふれたことですが)、文字列の先頭と末尾から一致する引用符のペアを削除する小さなヘルパーです。これは、標準の Trim メソッドよりも手間がかかります。両端から 1 文字だけをトリムし、一方の端だけをトリムするわけではありません。

    public static string TrimMatchingQuotes(this string input, char quote)
    {
        if ((input.Length >= 2) && 
            (input[0] == quote) && (input[input.Length - 1] == quote))
            return input.Substring(1, input.Length - 2);

        return input;
    }

また、いくつかのテストも必要になると思います。じゃあ、いいよ。しかし、これは絶対に最後でなければなりません!まず、分割の結果を予想される配列の内容と比較するヘルパー関数:

    public static void Test(string cmdLine, params string[] args)
    {
        string[] split = SplitCommandLine(cmdLine).ToArray();

        Debug.Assert(split.Length == args.Length);

        for (int n = 0; n < split.Length; n++)
            Debug.Assert(split[n] == args[n]);
    }

次に、次のようなテストを記述できます。

        Test("");
        Test("a", "a");
        Test(" abc ", "abc");
        Test("a b ", "a", "b");
        Test("a b \"c d\"", "a", "b", "c d");

要件のテストは次のとおりです。

        Test(@"/src:""C:\tmp\Some Folder\Sub Folder"" /users:""abcdefg@hijkl.com"" tasks:""SomeTask,Some Other Task"" -someParam",
             @"/src:""C:\tmp\Some Folder\Sub Folder""", @"/users:""abcdefg@hijkl.com""", @"tasks:""SomeTask,Some Other Task""", @"-someParam");

実装には、意味がある場合に引数を囲む引用符を削除するという追加機能があることに注意してください (TrimMatchingQuotes 関数のおかげです)。それは通常のコマンドライン解釈の一部だと思います。

于 2008-11-18T15:06:58.367 に答える
79

Earwickerによる優れた純粋なマネージド ソリューションに加えて、完全を期すために、Windows は文字列を文字列の配列に分割する機能も提供していることに言及する価値があるかもしれません。CommandLineToArgvW

LPWSTR *CommandLineToArgvW(
    LPCWSTR lpCmdLine, int *pNumArgs);

Unicode コマンド ライン文字列を解析し、標準の C ランタイム argv および argc 値と同様の方法で、コマンド ライン引数へのポインターの配列とそのような引数の数を返します。

この API を C# から呼び出し、結果の文字列配列をマネージ コードでアンパックする例は、「<a href="http://intellitect.com/converting-command-line-string-to-args-using- commandlinetoargvw-api/" rel="noreferrer">CommandLineToArgvW() API を使用してコマンド ライン文字列を Args[] に変換しています。" 以下は、同じコードの少し単純なバージョンです。

[DllImport("shell32.dll", SetLastError = true)]
static extern IntPtr CommandLineToArgvW(
    [MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, out int pNumArgs);

public static string[] CommandLineToArgs(string commandLine)
{
    int argc;
    var argv = CommandLineToArgvW(commandLine, out argc);        
    if (argv == IntPtr.Zero)
        throw new System.ComponentModel.Win32Exception();
    try
    {
        var args = new string[argc];
        for (var i = 0; i < args.Length; i++)
        {
            var p = Marshal.ReadIntPtr(argv, i * IntPtr.Size);
            args[i] = Marshal.PtrToStringUni(p);
        }

        return args;
    }
    finally
    {
        Marshal.FreeHGlobal(argv);
    }
}
于 2009-04-14T22:55:23.117 に答える
25

The Windows command-line parser behaves just as you say, split on space unless there's a unclosed quote before it. I would recommend writing the parser yourself. Something like this maybe:

    static string[] ParseArguments(string commandLine)
    {
        char[] parmChars = commandLine.ToCharArray();
        bool inQuote = false;
        for (int index = 0; index < parmChars.Length; index++)
        {
            if (parmChars[index] == '"')
                inQuote = !inQuote;
            if (!inQuote && parmChars[index] == ' ')
                parmChars[index] = '\n';
        }
        return (new string(parmChars)).Split('\n');
    }
于 2008-11-18T15:00:58.270 に答える
14

Jeffrey L Whitledgeから回答を受け取り、少し強化しました。

一重引用符と二重引用符の両方をサポートするようになりました。他の型付き引用符を使用して、パラメーター自体で引用符を使用できます。

また、これらは引数情報に寄与しないため、引数から引用符を取り除きます。

    public static string[] SplitArguments(string commandLine)
    {
        var parmChars = commandLine.ToCharArray();
        var inSingleQuote = false;
        var inDoubleQuote = false;
        for (var index = 0; index < parmChars.Length; index++)
        {
            if (parmChars[index] == '"' && !inSingleQuote)
            {
                inDoubleQuote = !inDoubleQuote;
                parmChars[index] = '\n';
            }
            if (parmChars[index] == '\'' && !inDoubleQuote)
            {
                inSingleQuote = !inSingleQuote;
                parmChars[index] = '\n';
            }
            if (!inSingleQuote && !inDoubleQuote && parmChars[index] == ' ')
                parmChars[index] = '\n';
        }
        return (new string(parmChars)).Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
    }
于 2010-01-25T11:53:30.843 に答える
5

私はイテレータが好きです。最近、LINQIEnumerable<String>文字列の配列と同じくらい簡単に使用できるようになっているので、Jeffrey L Whitledgeの答えの精神に従った私の見解は(への拡張メソッドとしてstring):

public static IEnumerable<string> ParseArguments(this string commandLine)
{
    if (string.IsNullOrWhiteSpace(commandLine))
        yield break;

    var sb = new StringBuilder();
    bool inQuote = false;
    foreach (char c in commandLine) {
        if (c == '"' && !inQuote) {
            inQuote = true;
            continue;
        }

        if (c != '"' && !(char.IsWhiteSpace(c) && !inQuote)) {
            sb.Append(c);
            continue;
        }

        if (sb.Length > 0) {
            var result = sb.ToString();
            sb.Clear();
            inQuote = false;
            yield return result;
        }
    }

    if (sb.Length > 0)
        yield return sb.ToString();
}
于 2011-10-14T22:24:37.937 に答える
4

Environment.GetCommandLineArgs()

于 2008-11-18T15:08:56.227 に答える
3

あなたの質問であなたは正規表現を求めました. 私は短い解決策が好きなので、それを作成しました。

            var re = @"\G(""((""""|[^""])+)""|(\S+)) *";
            var ms = Regex.Matches(CmdLine, re);
            var list = ms.Cast<Match>()
                         .Select(m => Regex.Replace(
                             m.Groups[2].Success
                                 ? m.Groups[2].Value
                                 : m.Groups[4].Value, @"""""", @"""")).ToArray();

引用符内の空白と引用符を処理し、囲まれた "" を " に変換します。コードを自由に使用してください。

于 2013-09-30T10:33:39.133 に答える
3

必要な機能を正確に含む NuGet パッケージがあります。

Microsoft.CodeAnalysis.Commonには、メソッドSplitCommandLineIntoArgumentsを持つCommandLineParserクラスが含まれています。

次のように使用します。

using Microsoft.CodeAnalysis;
// [...]
var cli = @"/src:""C:\tmp\Some Folder\Sub Folder"" /users:""abcdefg@hijkl.com"" tasks:""SomeTask,Some Other Task"" -someParam foo";
var cliArgs = CommandLineParser.SplitCommandLineIntoArguments(cli, true);

Console.WriteLine(string.Join('\n', cliArgs));
// prints out:
// /src:"C:\tmp\Some Folder\Sub Folder"
// /users:"abcdefg@hijkl.com"
// tasks:"SomeTask,Some Other Task"
// -someParam
// foo
于 2020-10-26T21:21:29.883 に答える
2

このThe Code Project の記事は、私が過去に使用したものです。ちょっとしたコードですが、うまくいくかもしれません。

C# がコマンド ライン引数を解析する方法を説明しているのは、このMSDN の記事だけです。

于 2008-11-18T14:23:04.620 に答える
1

純粋に管理されたソリューションが役立つ場合があります。WINAPI 関数には「問題」のコメントが多すぎて、他のプラットフォームでは使用できません。これは、明確に定義された動作を持つ私のコードです (必要に応じて変更できます)。

そのパラメーターを提供するときに .NET/Windows が行うことと同じことを行う必要がありstring[] args、多くの「興味深い」値と比較しました。

これは、入力文字列から各文字を取得し、それを現在の状態として解釈し、出力と新しい状態を生成する、従来のステート マシンの実装です。状態は変数escapeinQuotehadQuoteおよびprevChで定義され、出力は および に収集されcurrentArgますargs

実際のコマンド\\プロンプト(Windows 7) での実験\で私が発見したいくつか\"の特徴:""""

キャラクターも魔法の^ようで、2倍にしないと必ず消えてしまいます。それ以外の場合は、実際のコマンド ラインには影響しません。この動作のパターンが見つからないため、私の実装はこれをサポートしていません。多分誰かがそれについてもっと知っています。

このパターンに当てはまらないのは、次のコマンドです。

cmd /c "argdump.exe "a b c""

コマンドはcmd、外側の引用符をキャッチし、残りを逐語的に取るようです。これには特別な魔法のソースが入っているに違いありません。

メソッドのベンチマークは行っていませんが、かなり高速であると考えてください。文字列の連結は使用Regexせず、行いませんが、代わりに a を使用しStringBuilderて引数の文字を収集し、それらをリストに入れます。

/// <summary>
/// Reads command line arguments from a single string.
/// </summary>
/// <param name="argsString">The string that contains the entire command line.</param>
/// <returns>An array of the parsed arguments.</returns>
public string[] ReadArgs(string argsString)
{
    // Collects the split argument strings
    List<string> args = new List<string>();
    // Builds the current argument
    var currentArg = new StringBuilder();
    // Indicates whether the last character was a backslash escape character
    bool escape = false;
    // Indicates whether we're in a quoted range
    bool inQuote = false;
    // Indicates whether there were quotes in the current arguments
    bool hadQuote = false;
    // Remembers the previous character
    char prevCh = '\0';
    // Iterate all characters from the input string
    for (int i = 0; i < argsString.Length; i++)
    {
        char ch = argsString[i];
        if (ch == '\\' && !escape)
        {
            // Beginning of a backslash-escape sequence
            escape = true;
        }
        else if (ch == '\\' && escape)
        {
            // Double backslash, keep one
            currentArg.Append(ch);
            escape = false;
        }
        else if (ch == '"' && !escape)
        {
            // Toggle quoted range
            inQuote = !inQuote;
            hadQuote = true;
            if (inQuote && prevCh == '"')
            {
                // Doubled quote within a quoted range is like escaping
                currentArg.Append(ch);
            }
        }
        else if (ch == '"' && escape)
        {
            // Backslash-escaped quote, keep it
            currentArg.Append(ch);
            escape = false;
        }
        else if (char.IsWhiteSpace(ch) && !inQuote)
        {
            if (escape)
            {
                // Add pending escape char
                currentArg.Append('\\');
                escape = false;
            }
            // Accept empty arguments only if they are quoted
            if (currentArg.Length > 0 || hadQuote)
            {
                args.Add(currentArg.ToString());
            }
            // Reset for next argument
            currentArg.Clear();
            hadQuote = false;
        }
        else
        {
            if (escape)
            {
                // Add pending escape char
                currentArg.Append('\\');
                escape = false;
            }
            // Copy character from input, no special meaning
            currentArg.Append(ch);
        }
        prevCh = ch;
    }
    // Save last argument
    if (currentArg.Length > 0 || hadQuote)
    {
        args.Add(currentArg.ToString());
    }
    return args.ToArray();
}
于 2014-05-30T18:59:01.787 に答える
0

現在、これは私が持っているコードです:

    private String[] SplitCommandLineArgument(String argumentString)
    {
        StringBuilder translatedArguments = new StringBuilder(argumentString);
        bool escaped = false;
        for (int i = 0; i < translatedArguments.Length; i++)
        {
            if (translatedArguments[i] == '"')
            {
                escaped = !escaped;
            }
            if (translatedArguments[i] == ' ' && !escaped)
            {
                translatedArguments[i] = '\n';
            }
        }

        string[] toReturn = translatedArguments.ToString().Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
        for(int i = 0; i < toReturn.Length; i++)
        {
            toReturn[i] = RemoveMatchingQuotes(toReturn[i]);
        }
        return toReturn;
    }

    public static string RemoveMatchingQuotes(string stringToTrim)
    {
        int firstQuoteIndex = stringToTrim.IndexOf('"');
        int lastQuoteIndex = stringToTrim.LastIndexOf('"');
        while (firstQuoteIndex != lastQuoteIndex)
        {
            stringToTrim = stringToTrim.Remove(firstQuoteIndex, 1);
            stringToTrim = stringToTrim.Remove(lastQuoteIndex - 1, 1); //-1 because we've shifted the indicies left by one
            firstQuoteIndex = stringToTrim.IndexOf('"');
            lastQuoteIndex = stringToTrim.LastIndexOf('"');
        }
        return stringToTrim;
    }

エスケープされた引用符では機能しませんが、これまでに遭遇したケースでは機能します.

于 2008-11-18T19:10:46.753 に答える
0

昨日投稿したコードをご覧ください。

[C#] パスと引数の文字列

ファイル名 + 引数を文字列 [] に分割します。短いパス、環境変数、欠落しているファイル拡張子が処理されます。

(当初はレジストリの UninstallString 用でした。)

于 2012-04-26T10:53:25.770 に答える
0

これは、エスケープされた引用符では機能しない Anton のコードへの返信です。3箇所修正しました。

  1. SplitCommandLineArgumentsのStringBuilderコンストラクタで、\"\rに置き換えます
  2. SplitCommandLineArgumentsfor ループで、 \r文字を\"に置き換えます。
  3. SplitCommandLineArgumentメソッドをprivateからpublic staticに変更しました。

public static string[] SplitCommandLineArgument( String argumentString )
{
    StringBuilder translatedArguments = new StringBuilder( argumentString ).Replace( "\\\"", "\r" );
    bool InsideQuote = false;
    for ( int i = 0; i < translatedArguments.Length; i++ )
    {
        if ( translatedArguments[i] == '"' )
        {
            InsideQuote = !InsideQuote;
        }
        if ( translatedArguments[i] == ' ' && !InsideQuote )
        {
            translatedArguments[i] = '\n';
        }
    }

    string[] toReturn = translatedArguments.ToString().Split( new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries );
    for ( int i = 0; i < toReturn.Length; i++ )
    {
        toReturn[i] = RemoveMatchingQuotes( toReturn[i] );
        toReturn[i] = toReturn[i].Replace( "\r", "\"" );
    }
    return toReturn;
}

public static string RemoveMatchingQuotes( string stringToTrim )
{
    int firstQuoteIndex = stringToTrim.IndexOf( '"' );
    int lastQuoteIndex = stringToTrim.LastIndexOf( '"' );
    while ( firstQuoteIndex != lastQuoteIndex )
    {
        stringToTrim = stringToTrim.Remove( firstQuoteIndex, 1 );
        stringToTrim = stringToTrim.Remove( lastQuoteIndex - 1, 1 ); //-1 because we've shifted the indicies left by one
        firstQuoteIndex = stringToTrim.IndexOf( '"' );
        lastQuoteIndex = stringToTrim.LastIndexOf( '"' );
    }
    return stringToTrim;
}
于 2009-01-21T22:28:40.253 に答える
0

このコードを試してください:

    string[] str_para_linha_comando(string str, out int argumentos)
    {
        string[] linhaComando = new string[32];
        bool entre_aspas = false;
        int posicao_ponteiro = 0;
        int argc = 0;
        int inicio = 0;
        int fim = 0;
        string sub;

        for(int i = 0; i < str.Length;)
        {
            if (entre_aspas)
            {
                // Está entre aspas
                sub = str.Substring(inicio+1, fim - (inicio+1));
                linhaComando[argc - 1] = sub;

                posicao_ponteiro += ((fim - posicao_ponteiro)+1);
                entre_aspas = false;
                i = posicao_ponteiro;
            }
            else
            {
            tratar_aspas:
                if (str.ElementAt(i) == '\"')
                {
                    inicio = i;
                    fim = str.IndexOf('\"', inicio + 1);
                    entre_aspas = true;
                    argc++;
                }
                else
                {
                    // Se não for aspas, então ler até achar o primeiro espaço em branco
                    if (str.ElementAt(i) == ' ')
                    {
                        if (str.ElementAt(i + 1) == '\"')
                        {
                            i++;
                            goto tratar_aspas;
                        }

                        // Pular os espaços em branco adiconais
                        while(str.ElementAt(i) == ' ') i++;

                        argc++;
                        inicio = i;
                        fim = str.IndexOf(' ', inicio);
                        if (fim == -1) fim = str.Length;
                        sub = str.Substring(inicio, fim - inicio);
                        linhaComando[argc - 1] = sub;
                        posicao_ponteiro += (fim - posicao_ponteiro);

                        i = posicao_ponteiro;
                        if (posicao_ponteiro == str.Length) break;
                    }
                    else
                    {
                        argc++;
                        inicio = i;
                        fim = str.IndexOf(' ', inicio);
                        if (fim == -1) fim = str.Length;

                        sub = str.Substring(inicio, fim - inicio);
                        linhaComando[argc - 1] = sub;
                        posicao_ponteiro += fim - posicao_ponteiro;
                        i = posicao_ponteiro;
                        if (posicao_ponteiro == str.Length) break;
                    }
                }
            }
        }

        argumentos = argc;

        return linhaComando;
    }

ポルトガル語で書かれています。

于 2015-07-24T23:46:07.370 に答える
0

ProcessStartInfoファイル名と引数文字列を分離する必要がある使用のために、ファイル名をその引数から分離するメソッドを作成しました。

たとえば、結果"C:\Users\Me\Something.exe" -a として{ "C:\Users\Me\Something.exe", "-a" }

以下のコード:

    public static string[] SplitCommandFromArgs(string commandLine)
    {
        commandLine = commandLine.Trim();
        if (commandLine[0] == '"')
        {
            bool isEscaped = false;
            for (int c = 1; c < commandLine.Length; c++)
            {
                if (commandLine[c] == '"' && !isEscaped)
                {
                    return new string[] { commandLine.Substring(1, c - 1), commandLine.Substring(c + 1).Trim() };
                }
                isEscaped = commandLine[c] == '\\';
            }
        }
        else
        {
            for (int c = 1; c < commandLine.Length; c++) {
                if (commandLine[c] == ' ')
                {
                    return new string[] { commandLine.Substring(0, c), commandLine.Substring(c).Trim() };
                }
            }
        }
        return new string[] { commandLine, "" };
    }
于 2021-03-07T02:55:03.903 に答える
0

ここで気に入ったものは見つかりませんでした。私は、小さなコマンド ラインに対して yield マジックを使用してスタックを台無しにするのは嫌いです (それが 1 テラバイトのストリームである場合は、別の話になります)。

これが私の見解です。次のような二重引用符を使用した引用エスケープをサポートしています。

param="a 15"" 画面は悪くない" param2='a 15" 画面は悪くない' param3="" param4= /param5

結果:

param="15 インチの画面も悪くない"

param2='15 インチの画面も悪くない'

param3=""

param4=

/param5

public static string[] SplitArguments(string commandLine)
{
    List<string> args         = new List<string>();
    List<char>   currentArg   = new List<char>();
    char?        quoteSection = null; // Keeps track of a quoted section (and the type of quote that was used to open it)
    char[]       quoteChars   = new[] {'\'', '\"'};
    char         previous     = ' '; // Used for escaping double quotes

    for (var index = 0; index < commandLine.Length; index++)
    {
        char c = commandLine[index];
        if (quoteChars.Contains(c))
        {
            if (previous == c) // Escape sequence detected
            {
                previous = ' '; // Prevent re-escaping
                if (!quoteSection.HasValue)
                {
                    quoteSection = c; // oops, we ended the quoted section prematurely
                    continue;         // don't add the 2nd quote (un-escape)
                }

                if (quoteSection.Value == c)
                    quoteSection = null; // appears to be an empty string (not an escape sequence)
            }
            else if (quoteSection.HasValue)
            {
                if (quoteSection == c)
                    quoteSection = null; // End quoted section
            }
            else
                quoteSection = c; // Start quoted section
        }
        else if (char.IsWhiteSpace(c))
        {
            if (!quoteSection.HasValue)
            {
                args.Add(new string(currentArg.ToArray()));
                currentArg.Clear();
                previous = c;
                continue;
            }
        }

        currentArg.Add(c);
        previous = c;
    }

    if (currentArg.Count > 0)
        args.Add(new string(currentArg.ToArray()));

    return args.ToArray();
}
于 2019-04-29T12:22:14.917 に答える
-2

理解できたかどうかわかりませんが、スプリッターとして使用されている文字の問題は、テキスト内にもありますか? (それ以外はダブル"?"でエスケープ)

もしそうなら、私はforループを作成し、<"> が存在するすべてのインスタンスを <|> (または別の「安全な」文字ですが、<""> ではなく <"> のみを置き換えるようにしてください) に置き換えます。

文字列を反復した後、以前に投稿したように文字列を分割しますが、文字 <|> で分割します。

于 2008-11-18T14:20:17.947 に答える
-5

はい、文字列オブジェクトには、Split()検索する文字を区切り文字として指定する単一のパラメーターを取り、個々の値を含む文字列の配列 (string[]) を返すという組み込み関数があります。

于 2008-11-18T14:14:05.853 に答える