Разбить строку, содержащую параметры командной строки, на строку [] в C # - PullRequest
80 голосов
/ 18 ноября 2008

У меня есть одна строка, содержащая параметры командной строки для передачи другому исполняемому файлу, и мне нужно извлечь строку [], содержащую отдельные параметры, так же, как C #, если бы команды были указаны в команде -линия. Строка [] будет использоваться при выполнении другой точки входа сборок с помощью отражения.

Есть ли стандартная функция для этого? Или есть предпочтительный метод (регулярное выражение?) Для правильного разделения параметров? Он должен обрабатывать строки с разделителями "" ", которые могут содержать пробелы правильно, поэтому я не могу просто разбить на" ".

Пример строки:

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

Мне не нужна библиотека для разбора командной строки, просто способ получить строку [], которая должна быть сгенерирована.

Обновление : мне пришлось изменить ожидаемый результат, чтобы он соответствовал тому, что фактически генерируется C # (убрал лишние "в разделенных строках)

Ответы [ 21 ]

94 голосов
/ 18 ноября 2008

Меня раздражает, что нет функции для разделения строки на основе функции, которая проверяет каждый символ. Если бы было, вы могли бы написать это так:

    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). Я считаю, что это часть обычной интерпретации командной строки.

66 голосов
/ 15 апреля 2009

В дополнение к хорошему и чистому управляемому решению от Earwicker , для полноты картины стоит упомянуть, что Windows также предоставляет CommandLineToArgvW функция для разбиения строки на массив строк:

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

Анализирует строку командной строки Unicode и возвращает массив указателей на аргументы командной строки, наряду с количество таких аргументов, в пути это похоже на стандартный C значения времени выполнения argv и argc.

Пример вызова этого API из C # и распаковки результирующего массива строк в управляемом коде можно найти по адресу: « Преобразование строки командной строки в Args [] с использованием API CommandLineToArgvW () ». Ниже приведен немного более простая версия того же кода:

[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);
    }
}
23 голосов
/ 18 ноября 2008

Анализатор командной строки 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');
    }
12 голосов
/ 25 января 2010

Я взял ответ от Джеффри Л. Уитледжа и немного его улучшил.

Теперь он поддерживает одинарные и двойные кавычки. Вы можете использовать кавычки в самих параметрах, используя другие типизированные кавычки.

Он также удаляет кавычки из аргументов, поскольку они не вносят вклад в информацию об аргументах.

    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);
    }
7 голосов
/ 18 июля 2014

хорошее и чистое управляемое решение от 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\".");

Также отмечено, что принятый ответ от Atif Aziz , использующий CommandLineToArgvW , также не удался. Вернуло 4 элемента:

He whispered to her \ 
I 
love 
you". 

Надеюсь, это поможет кому-то искать такое решение в будущем.

4 голосов
/ 18 ноября 2008
4 голосов
/ 15 октября 2011

Мне нравятся итераторы, и в настоящее время LINQ делает IEnumerable<String> столь же легко используемым, как массивы строк, поэтому мой подход, следуя духу Джеффри Л. Уитледжа, отвечает (как расширение метод 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();
}
2 голосов
/ 18 ноября 2008

Это Статья Code Project - это то, что я использовал в прошлом. Это хороший код, но он может сработать.

Эта статья MSDN - единственное, что я могу найти, объясняющее, как C # анализирует аргументы командной строки.

1 голос
/ 30 мая 2014

A чисто управляемое решение может быть полезным. Слишком много «проблемных» комментариев для функции WINAPI, и она недоступна на других платформах. Вот мой код с четко определенным поведением (которое вы можете изменить, если хотите).

Он должен делать то же самое, что и .NET / Windows при предоставлении этого параметра string[] args, и я сравнил его с рядом "интересных" значений.

Это классическая реализация конечного автомата, которая берет каждый отдельный символ из входной строки и интерпретирует его для текущего состояния, создавая выходные данные и новое состояние. Состояние определяется в переменных escape, inQuote, hadQuote и prevCh, а выходные данные собираются в currentArg и args.

Некоторые из особенностей, которые я обнаружил экспериментами в реальной командной строке (Windows 7): \\ производит \, \" производит ", "" в указанном диапазоне производит ".

Символ ^ тоже кажется волшебным: он всегда исчезает, если его не удвоить. В противном случае это не влияет на настоящую командную строку. Моя реализация не поддерживает это, так как я не нашел шаблон в этом поведении. Может быть, кто-то знает об этом больше.

Что-то, что не вписывается в этот шаблон, является следующей командой:

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

Команда cmd, кажется, перехватывает внешние кавычки и принимает остальное дословно. В этом должен быть особый волшебный соус.

Я не делал никаких тестов для моего метода, но считаю его достаточно быстрым. Он не использует Regex и не выполняет конкатенацию строк, а вместо этого использует 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();
}
1 голос
/ 30 сентября 2013

В вашем вопросе вы попросили регулярное выражение, и я большой поклонник и пользователь из них, поэтому, когда мне нужно было разделить этот же аргумент с вами, я написал свое собственное регулярное выражение после поиска в Google и не нашел простого решения. , Мне нравятся короткие решения, поэтому я сделал одно и вот оно:

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

Он обрабатывает пробелы и кавычки внутри кавычек и преобразует заключенные "" в ". Не стесняйтесь использовать код!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...