Разделить строку, содержащую параметры командной строки, в строку в C #

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

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

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

string parameterString = @"/src:""C:\tmp\Some Folder\Sub Folder"" /users:""[email protected]"" tasks:""SomeTask,Some Other Task"" -someParam foo"; 

Пример результата:

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

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

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

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

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

Разбирает строку командной строки Unicode и возвращает массив указателей на аргументы командной строки вместе с подсчетом таких аргументов таким образом, который похож на стандартные значения времени 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); } } 

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

  public static IEnumerable 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 Split(this string str, Func 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); } 

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

Во-вторых, (и более mundanely) небольшой помощник, который обрезает совпадающую пару кавычек от начала и конца строки. Это более суетливый, чем стандартный метод 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("ab ", "a", "b"); Test("ab \"cd\"", "a", "b", "cd"); 

Вот тест для ваших требований:

  Test(@"/src:""C:\tmp\Some Folder\Sub Folder"" /users:""[email protected]"" tasks:""SomeTask,Some Other Task"" -someParam", @"/src:""C:\tmp\Some Folder\Sub Folder""", @"/users:""[email protected]""", @"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'); } 

Я взял ответ от Джеффри Л. Уитледжа и немного укрепил его. У меня еще недостаточно кредитов, чтобы прокомментировать его ответ.

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

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

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

Environment.GetCommandLineArgs ()

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

Протестировано с двумя дополнительными случаями:

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

Также отметил, что принятый ответ Атифа Азиза, который использует CommandLineToArgvW, также потерпел неудачу. Он возвратил 4 элемента:

 He whispered to her \ I love you". 

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

Google говорит: C # /. NET Командная строка Аргументы Parser

Мне нравятся iteratorы, и в настоящее время Linq делает IEnumerable столь же легко используемым, как массивы строки, поэтому я беру на себя ответ Джеффри L Whitledge (как метод расширения для строки):

  public static IEnumerable 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(); } 

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

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

Надеюсь, это поможет!

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

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

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

Я знаю, что это старо, но кто-то может найти полезное решение с чисто управляемым управлением . Слишком много «проблемных» комментариев для функции WINAPI, и она недоступна на других платформах. Вот мой код, который имеет четко определенное поведение (которое вы можете изменить, если хотите). Он должен делать то же, что и .NET / Windows при предоставлении параметра string[] args , я сравнил его с рядом «интересных» значений.

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

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

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

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

 cmd /c "argdump.exe "abc"" 

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

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

 ///  /// Reads command line arguments from a single string. ///  /// The string that contains the entire command line. /// An array of the parsed arguments. public string[] ReadArgs(string argsString) { // Collects the split argument strings List args = new List(); // 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(); } 

В настоящее время это код, который у меня есть:

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

Он не работает с экранированными кавычками, но он работает для тех случаев, с которыми я столкнулся до сих пор.

Это ответ на код Антона, который не работает с экранированными кавычками. Я изменил 3 места.

  1. Конструктор для StringBuilder в SplitCommandLineArguments , заменяющий любой \ “ на \ r
  2. В for-loop в SplitCommandLineArguments теперь я заменяю символ \ r на \ “ .
  3. Изменен метод SplitCommandLineArgument от частного к публичному статическому .

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

вы можете посмотреть код, который я опубликовал вчера:

http://social.msdn.microsoft.com/Forums/fr-FR/netfx64bit/thread/2dfe45f5-7940-48cd-bd57-add8f3d94102

Он разделил имя файла + аргументы на строку []. Короткие пути, переменная среды, отсутствие расширения файла обрабатываются.

(Первоначально это было для UninstallString в реестре).

 public static string[] SplitArguments(string args) { char[] parmChars = args.ToCharArray(); bool inSingleQuote = false; bool inDoubleQuote = false; bool escaped = false; bool lastSplitted = false; bool justSplitted = false; bool lastQuoted = false; bool justQuoted = false; int i, j; for(i=0, j=0; i 

основанный на Vapor в ответе Аллеи , этот также поддерживает ^ escape-последовательности

Примеры:

  • Это тест
    • это
    • является
    • контрольная работа
  • Это тест
    • это
    • это
    • контрольная работа
  • это ^ "есть ^" тест
    • это
    • "является
    • А»
    • контрольная работа
  • это "" "является тестом ^^"
    • это
    • является a ^ тестом

также поддерживает несколько пробелов (разрывает args всего один раз на блок пробелов)

Попробуйте этот код:

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

Это написано на португальском языке.

Вот один лайнер, который выполняет работу (см. Одну строку, которая выполняет всю работу внутри метода BurstCmdLineArgs (…)). Не то, что я бы назвал наиболее читаемой строкой кода, но вы можете раскрыть ее ради удобочитаемости. Это простое назначение и не работает хорошо для всех аргументов (например, аргументы имени файла, которые содержат разделитель символов разделенных строк). Это решение хорошо зарекомендовало себя в моих решениях, которые его используют. Как я уже сказал, он выполняет свою работу без крысиного гнезда кода, чтобы обрабатывать все возможные аргументы n-factorial.

 using System; using System.Collections.Generic; using System.Linq; namespace CmdArgProcessor { class Program { static void Main(string[] args) { // test switches and switches with values // -test1 1 -test2 2 -test3 -test4 -test5 5 string dummyString = string.Empty; var argDict = BurstCmdLineArgs(args); Console.WriteLine("Value for switch = -test1: {0}", argDict["test1"]); Console.WriteLine("Value for switch = -test2: {0}", argDict["test2"]); Console.WriteLine("Switch -test3 is present? {0}", argDict.TryGetValue("test3", out dummyString)); Console.WriteLine("Switch -test4 is present? {0}", argDict.TryGetValue("test4", out dummyString)); Console.WriteLine("Value for switch = -test5: {0}", argDict["test5"]); // Console output: // // Value for switch = -test1: 1 // Value for switch = -test2: 2 // Switch -test3 is present? True // Switch -test4 is present? True // Value for switch = -test5: 5 } public static Dictionary BurstCmdLineArgs(string[] args) { var argDict = new Dictionary(); // Flatten the args in to a single string separated by a space. // Then split the args on the dash delimiter of a cmd line "switch". // Eg -mySwitch myValue // or -JustMySwitch (no value) // where: all values must follow a switch. // Then loop through each string returned by the split operation. // If the string can be split again by a space character, // then the second string is a value to be paired with a switch, // otherwise, only the switch is added as a key with an empty string as the value. // Use dictionary indexer to retrieve values for cmd line switches. // Use Dictionary::ContainsKey(...) where only a switch is recorded as the key. string.Join(" ", args).Split('-').ToList().ForEach(s => argDict.Add(s.Split()[0], (s.Split().Count() > 1 ? s.Split()[1] : ""))); return argDict; } } } 

Я не думаю, что для приложений C # есть одинарные кавычки или ^ кавычки. Следующая функция работает отлично для меня:

 public static IEnumerable SplitArguments(string commandLine) { Char quoteChar = '"'; Char escapeChar = '\\'; Boolean insideQuote = false; Boolean insideEscape = false; StringBuilder currentArg = new StringBuilder(); // needed to keep "" as argument but drop whitespaces between arguments Int32 currentArgCharCount = 0; for (Int32 i = 0; i < commandLine.Length; i++) { Char c = commandLine[i]; if (c == quoteChar) { currentArgCharCount++; if (insideEscape) { currentArg.Append(c); // found \" -> add " to arg insideEscape = false; } else if (insideQuote) { insideQuote = false; // quote ended } else { insideQuote = true; // quote started } } else if (c == escapeChar) { currentArgCharCount++; if (insideEscape) // found \\ -> add \\ (only \" will be ") currentArg.Append(escapeChar + escapeChar); insideEscape = !insideEscape; } else if (Char.IsWhiteSpace(c)) { if (insideQuote) { currentArgCharCount++; currentArg.Append(c); // append whitespace inside quote } else { if (currentArgCharCount > 0) yield return currentArg.ToString(); currentArgCharCount = 0; currentArg.Clear(); } } else { currentArgCharCount++; if (insideEscape) { // found non-escaping backslash -> add \ (only \" will be ") currentArg.Append(escapeChar); currentArgCharCount = 0; insideEscape = false; } currentArg.Append(c); } } if (currentArgCharCount > 0) yield return currentArg.ToString(); } 

Не уверен, понял ли я вас, но является ли проблема, что символ, используемый в качестве разделителя, также находится внутри текста? (За исключением того, что он сбежал с двойным “?)

Если это так, я бы создал цикл for и заменил все экземпляры, где <"> присутствует с <|> (или другим” безопасным “символом, но убедитесь, что он заменяет только <">, а не <"">

После итерации строки я бы сделал, как и ранее, разделил строку, но теперь на символ <|>

EDIT: для читаемости я добавил: «написано как <»>, так как стало немного непонятно, что я имел в виду, когда писал только «» и «, или |

Да, строковый объект имеет встроенную функцию Split (), которая принимает один параметр, определяющий символ, который нужно искать в качестве разделителя, и возвращает массив строк (string []) с отдельными значениями в нем

Давайте будем гением компьютера.