[c#] C #에서 명령 줄 매개 변수가 포함 된 문자열을 string []으로 분할

다른 실행 파일에 전달할 명령 줄 매개 변수가 포함 된 단일 문자열이 있으며 명령 줄에 명령이 지정된 경우 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 #에서 실제로 생성 된 것과 일치하도록 예상 결과를 변경해야했습니다 (분할 문자열에서 추가 “제거됨).



답변

받는 사람 또한 좋은 순수 관리 솔루션 에 의해 Earwicker , 그것은 윈도우도를 제공, 완벽을 위해서 언급 할만큼 가치가 될 수 있습니다CommandLineToArgvW 문자열의 배열로 문자열을 나누는 기능 :

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

유니 코드 명령 줄 문자열을 구문 분석하고 표준 C 런타임 argv 및 argc 값과 유사한 방식으로 이러한 인수의 개수와 함께 명령 줄 인수에 대한 포인터 배열을 반환합니다.

C #에서이 API를 호출하고 관리 코드에서 결과 문자열 배열의 압축을 푸는 예는 ” 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);
    }
}


답변

각 문자를 검사하는 기능을 기반으로 문자열을 분할하는 기능이 없다는 것이 나를 짜증나게합니다. 있다면 다음과 같이 작성할 수 있습니다.

    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);
    }

상황에 따라 빈 문자열이 생성 될 수 있지만 해당 정보가 다른 경우에 유용 할 수 있으므로이 함수에서 빈 항목을 제거하지 않습니다.

두 번째로 (더 평범하게) 문자열의 시작과 끝에서 일치하는 따옴표 쌍을 잘라내는 작은 도우미입니다. 표준 Trim 방법보다 까다 롭습니다. 각 끝에서 하나의 문자 만 잘라 내고 한쪽 끝에서만 잘라 내지 않습니다.

    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 함수 덕분에). 이것이 일반적인 명령 줄 해석의 일부라고 생각합니다.


답변

Windows 명령 줄 구문 분석기는 앞에 닫히지 않은 따옴표가없는 한 공백으로 분할되어 말한대로 작동합니다. 파서를 직접 작성하는 것이 좋습니다. 아마도 다음과 같습니다.

    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');
    }


답변

저는 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);
    }


답변

Earwicker훌륭하고 순수한 관리 솔루션 은 다음과 같은 인수를 처리하지 못했습니다.

Test("\"He whispered to her \\\"I love you\\\".\"", "He whispered to her \"I love you\".");

3 개의 요소를 반환했습니다.

"He whispered to her \"I
love
you\"."

그래서 다음은 “인용 된 \”escape \ “인용구”를 지원하는 수정입니다.

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

    return commandLine.Split(c => {
        if (c == '\\' && !isEscaping) { isEscaping = true; return false; }

        if (c == '\"' && !isEscaping)
            inQuotes = !inQuotes;

        isEscaping = false;

        return !inQuotes && Char.IsWhiteSpace(c)/*c == ' '*/;
        })
        .Select(arg => arg.Trim().TrimMatchingQuotes('\"').Replace("\\\"", "\""))
        .Where(arg => !string.IsNullOrEmpty(arg));
}

2 개의 추가 사례로 테스트되었습니다.

Test("\"C:\\Program Files\"", "C:\\Program Files");
Test("\"He whispered to her \\\"I love you\\\".\"", "He whispered to her \"I love you\".");

또한 CommandLineToArgvW 를 사용 하는 Atif Aziz수락 된 답변 도 실패했습니다. 4 개의 요소를 반환했습니다.

He whispered to her \
I
love
you".

이것이 미래에 그러한 솔루션을 찾는 데 도움이되기를 바랍니다.


답변


답변

나는 반복기를 좋아하고 요즘 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();
}