Divida a string contendo parâmetros de linha de comando em string [] em C #


88

Eu tenho uma única string que contém os parâmetros da linha de comando a serem passados ​​para outro executável e preciso extrair a string [] que contém os parâmetros individuais da mesma forma que o C # faria se os comandos tivessem sido especificados na linha de comando. A string [] será usada ao executar outro ponto de entrada de assemblies via reflexão.

Existe uma função padrão para isso? Ou existe um método preferido (regex?) Para dividir os parâmetros corretamente? Ele deve lidar com strings delimitadas por '"' que podem conter espaços corretamente, então não posso simplesmente dividir em ''.

String de exemplo:

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

Resultado de exemplo:

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

Não preciso de uma biblioteca de análise de linha de comando, apenas uma forma de obter o String [] que deve ser gerado.

Atualização : tive que alterar o resultado esperado para corresponder ao que é realmente gerado pelo C # (removidos os extras "nas strings divididas)



5
Cada vez que alguém responde, você parece ter uma objeção baseada em material que não está em sua postagem. Eu sugiro que você atualize sua postagem com este material. Você pode obter respostas melhores.
tvanfosson

1
Boa pergunta, procurando o mesmo. Esperava encontrar alguém que dissesse "ei .net expõe isso aqui ..." :) Se eu encontrar isso em algum momento, vou postar aqui, mesmo que já tenha uns 6 anos. Ainda é uma pergunta válida!
MikeJansen

Criei uma versão puramente gerenciada em uma resposta abaixo, pois também precisava dessa função.
ygoe

Respostas:


74

Além da boa e pura solução gerenciada por Earwicker , pode valer a pena mencionar, para fins de completude, que o Windows também fornece a CommandLineToArgvWfunção para quebrar uma string em uma série de strings:

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

Analisa uma string de linha de comando Unicode e retorna uma matriz de ponteiros para os argumentos da linha de comando, junto com uma contagem de tais argumentos, de maneira semelhante aos valores argv e argc de tempo de execução C padrão.

Um exemplo de como chamar esta API a partir do C # e descompactar a matriz de string resultante em código gerenciado pode ser encontrado em “ Convertendo String de Linha de Comando em Args [] usando CommandLineToArgvW () API .” Abaixo está uma versão um pouco mais simples do mesmo código:

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

1
Esta função requer que você escape da barra invertida final de um caminho entre aspas. "C: \ Arquivos de programas \" deve ser "C: \ Arquivos de programas \\" para que funcione e analise a string corretamente.
Magnus Lindhe

8
Também é importante notar que CommandLineArgvW espera que o primeiro argumento seja o nome do programa, e a magia de análise aplicada não é exatamente a mesma se não for transmitida. Você pode fingir com algo como:CommandLineToArgs("foo.exe " + commandLine).Skip(1).ToArray();
Scott Wegner

4
Para fins de integridade, o MSVCRT não usa CommandLineToArgvW () para converter a linha de comando em argc / argv. Ele usa seu próprio código, o que é diferente. Por exemplo, tente chamar CreateProcess com esta string: a "b c" def. Em main (), você obteria 3 argumentos (conforme documentado no MSDN), mas a combinação CommandLineToArgvW () / GetCommandLineW () dará a você 2.
LRN

7
OMG, isso é uma bagunça. sopa típica de MS. nada é canonizado e nunca o KISS é respeitado no mundo MS.
v.oddou

1
Publiquei uma versão multiplataforma da implementação do MSVCRT traduzida pela Microsoft e uma aproximação de alta precisão usando Regex. Eu sei que isso é antigo, mas hey - nenhum pergaminho do corpo.
TylerY86

101

Me irrita que não haja função para dividir uma string com base em uma função que examina cada caractere. Se houvesse, você poderia escrever assim:

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

Apesar de ter escrito isso, por que não escrever os métodos de extensão necessários. Ok, você me convenceu ...

Em primeiro lugar, minha própria versão de Split que assume uma função que tem que decidir se o caractere especificado deve dividir a string:

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

Pode render algumas strings vazias dependendo da situação, mas talvez essa informação seja útil em outros casos, então eu não removo as entradas vazias nesta função.

Em segundo lugar (e de forma mais prática), um pequeno auxiliar que irá cortar um par de aspas correspondentes do início e do fim de uma string. É mais complicado do que o método Trim padrão - ele cortará apenas um caractere de cada extremidade e não apenas de uma extremidade:

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

E suponho que você queira alguns testes também. Bem, tudo bem então. Mas isso deve ser absolutamente a última coisa! Primeiro, uma função auxiliar que compara o resultado da divisão com o conteúdo esperado da matriz:

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

Então posso escrever testes como este:

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

Este é o teste para seus requisitos:

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

Observe que a implementação tem o recurso extra de remover as aspas em torno de um argumento, se isso fizer sentido (graças à função TrimMatchingQuotes). Eu acredito que isso faz parte da interpretação normal da linha de comando.


Tive que desmarcar isso como a resposta porque não tinha os resultados esperados corretos. A saída real não deve ter o "s na matriz final
Anton

16
Venho para o Stack Overflow para fugir de requisitos que mudam o tempo todo! :) Você pode usar Replace ("\" "," ") em vez de TrimMatchingQuotes () para se livrar de todas as aspas. Mas o Windows suporta \" para permitir que um caractere de aspas seja passado. Minha função Split não pode fazer isso.
Daniel Earwicker

1
Legal Earwicker :) Anton: Esta é a solução que eu estava tentando descrever para você em meu post anterior, mas Earwicker fez um trabalho muito melhor ao escrevê-la;) E também a ampliou muito;)
Israr Khan

um espaço em branco não é o único caractere de separação para argumentos de linha de comando, não é?
Louis Rhys,

@Louis Rhys - Não tenho certeza. Se isso for uma preocupação, é muito fácil de resolver: use em char.IsWhiteSpacevez de== ' '
Daniel Earwicker

25

O analisador de linha de comando do Windows se comporta exatamente como você diz, dividido no espaço, a menos que haja uma citação não fechada antes dele. Eu recomendaria escrever o analisador você mesmo. Algo assim talvez:

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

2
Acabei ficando com a mesma coisa, exceto que usei .Split (new char [] {'\ n'}, StringSplitOptions.RemoveEmptyEntries) na linha final caso houvesse '' s extras entre os parâmetros. Parece estar funcionando.
Anton

3
Presumo que o Windows deve ter uma maneira de escapar as aspas nos parâmetros ... este algoritmo não leva isso em consideração.
rmeador

A remoção de linhas em branco, a remoção de aspas externas e o tratamento de citações com escape são deixados como um excersize para o leitor.
Jeffrey L Whitledge

Char.IsWhiteSpace () poderia ajudar aqui
Sam Mackrill

Essa solução é boa se os argumentos forem separados por um único espaço, mas falhar se os argumentos forem separados por vários espaços. Link para a solução correta: stackoverflow.com/a/59131568/3926504
Dilip Nannaware

13

Peguei a resposta de Jeffrey L. Whitledge e a aprimorei um pouco.

Agora ele suporta aspas simples e duplas. Você pode usar aspas nos próprios parâmetros usando outras aspas digitadas.

Também remove as aspas dos argumentos, uma vez que não contribuem para as informações do argumento.

    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

A solução bem gerenciada por Earwicker falhou em lidar com argumentos como este:

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

Ele retornou 3 elementos:

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

Portanto, aqui está uma correção para suportar a "citação de escape \" "citada":

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

Testado com 2 casos adicionais:

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

Também observou que a resposta aceita por Atif Aziz que usa CommandLineToArgvW também falhou. Ele retornou 4 elementos:

He whispered to her \ 
I 
love 
you". 

Espero que isso ajude alguém que esteja procurando por essa solução no futuro


3
Desculpe pela necromancia, mas esta solução ainda não contém coisas como o bla.exe aAAA"b\"ASDS\"c"dSADSDque resulta em aAAAb"ASDS"cdSADSDonde esta solução será exibida aAAA"b"ASDS"c"dSADSD. Eu posso considerar mudar o TrimMatchingQuotespara um Regex("(?<!\\\\)\\\"")e usá-lo assim .
Scis de

4

2
Útil - mas isso só fornecerá os argumentos de linha de comando enviados para o processo atual. O requisito era obter uma string [] de uma string "da mesma forma que o C # faria se os comandos tivessem sido especificados na linha de comando". Acho que poderíamos usar um descompilador para ver como o MS implementou isso ...
rohancragg

Como Jon Galloway também descobriu ( weblogs.asp.net/jgalloway/archive/2006/09/13/… ) um descompilador não ajuda muito, o que nos leva de volta à resposta de Atif ( stackoverflow.com/questions/298830/… )
rohancragg

4

Eu gosto de iteradores e hoje em dia o LINQ torna-se IEnumerable<String>tão facilmente utilizável como arrays de string, então minha opinião seguindo o espírito da resposta de Jeffrey L Whitledge é (como um método de extensão para 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();
}

3

Em sua pergunta, você pediu um regex, e eu sou um grande fã e usuário deles, então quando precisei dividir o mesmo argumento que você, escrevi meu próprio regex após pesquisar no Google e não encontrar uma solução simples. Gosto de soluções curtas, então fiz uma e aqui está:

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

Ele lida com espaços em branco e aspas entre aspas e converte "" em "entre aspas. Sinta-se à vontade para usar o código!


3

Oh, que diabo. É tudo ... Eugh. Mas isso é oficial legítimo. Da Microsoft em C # para .NET Core, talvez apenas Windows, talvez multiplataforma, mas licenciado pelo MIT.

Selecione petiscos, declarações de métodos e comentários notáveis;

internal static unsafe string[] InternalCreateCommandLine(bool includeArg0)
private static unsafe int SegmentCommandLine(char * pCmdLine, string[] argArray, bool includeArg0)
private static unsafe int ScanArgument0(ref char* psrc, char[] arg)
private static unsafe int ScanArgument(ref char* psrc, ref bool inquote, char[] arg)

-

// First, parse the program name (argv[0]). Argv[0] is parsed under special rules. Anything up to 
// the first whitespace outside a quoted subtring is accepted. Backslashes are treated as normal 
// characters.

-

// Rules: 2N backslashes + " ==> N backslashes and begin/end quote
//      2N+1 backslashes + " ==> N backslashes + literal "
//         N backslashes     ==> N backslashes

Este é o código transferido para o .NET Core a partir do .NET Framework a partir do que presumo ser a biblioteca C MSVC ou CommandLineToArgvW.

Aqui está minha tentativa indiferente de lidar com algumas das travessuras com expressões regulares e ignorar o argumento zero bit. É um pouco mágico.

private static readonly Regex RxWinArgs
  = new Regex("([^\\s\"]+\"|((?<=\\s|^)(?!\"\"(?!\"))\")+)(\"\"|.*?)*\"[^\\s\"]*|[^\\s]+",
    RegexOptions.Compiled
    | RegexOptions.Singleline
    | RegexOptions.ExplicitCapture
    | RegexOptions.CultureInvariant);

internal static IEnumerable<string> ParseArgumentsWindows(string args) {
  var match = RxWinArgs.Match(args);

  while (match.Success) {
    yield return match.Value;
    match = match.NextMatch();
  }
}

Testei bastante na saída gerada maluca. Sua saída corresponde a uma boa porcentagem do que os macacos digitaram e executaram CommandLineToArgvW.




1
Reavivamento por tempo limitado. pastebin.com/ajhrBS4t
TylerY86

2

Este artigo do The Code Project é o que eu usei no passado. É um bom código, mas pode funcionar.

Este artigo do MSDN é a única coisa que pude encontrar que explica como o C # analisa argumentos de linha de comando.


Tentei refletor na biblioteca C #, mas vai para uma chamada C ++ nativa para a qual não tenho o código e não consigo ver nenhuma maneira de chamar sem p-invocá-la. Também não quero uma biblioteca de análise de linha de comando, só quero a string [].
Anton

Refletir sobre o .NET também não me trouxe a lugar nenhum. Olhar para o código- fonte do Mono sugere que essa divisão de argumento não é feita pelo CLR, mas sim já vem do sistema operacional. Pense nos parâmetros argc, argv da função C principal. Portanto, não há nada para reutilizar além da API do sistema operacional.
ygoe

1

Uma solução puramente gerenciada pode ser útil. Há muitos comentários de "problema" para a função WINAPI e ela não está disponível em outras plataformas. Aqui está meu código que tem um comportamento bem definido (que você pode alterar se quiser).

Ele deve fazer o mesmo que o .NET / Windows faz ao fornecer esse string[] argsparâmetro, e eu comparei com uma série de valores "interessantes".

Esta é uma implementação clássica de máquina de estado que pega cada caractere da string de entrada e o interpreta para o estado atual, produzindo saída e um novo estado. O estado é definido nas variáveis escape, inQuote, hadQuotee prevCh, a saída e é recolhido em currentArge args.

Algumas das especialidades que descobri por meio de experimentos em um prompt de comando real (Windows 7): \\produz \, \"produz ", ""dentro de uma faixa citada, produz ".

O ^personagem também parece mágico: sempre desaparece quando não é duplicado. Caso contrário, não tem efeito em uma linha de comando real. Minha implementação não suporta isso, pois não encontrei um padrão neste comportamento. Talvez alguém saiba mais sobre isso.

Algo que não se encaixa nesse padrão é o seguinte comando:

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

O cmdcomando parece pegar as aspas externas e pegar o resto literalmente. Deve haver algum molho mágico especial nisso.

Não fiz benchmarks em meu método, mas considere-o razoavelmente rápido. Ele não usa Regexe não faz nenhuma concatenação de string, mas em vez disso usa um StringBuilderpara coletar os caracteres para um argumento e colocá-los em uma lista.

/// <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

Usar:

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<parmChars.Length; i++, j++) {
        parmChars[j] = parmChars[i];

        if(!escaped) {
            if(parmChars[i] == '^') {
                escaped = true;
                j--;
            } else if(parmChars[i] == '"' && !inSingleQuote) {
                inDoubleQuote = !inDoubleQuote;
                parmChars[j] = '\n';
                justSplitted = true;
                justQuoted = true;
            } else if(parmChars[i] == '\'' && !inDoubleQuote) {
                inSingleQuote = !inSingleQuote;
                parmChars[j] = '\n';
                justSplitted = true;
                justQuoted = true;
            } else if(!inSingleQuote && !inDoubleQuote && parmChars[i] == ' ') {
                parmChars[j] = '\n';
                justSplitted = true;
            }

            if(justSplitted && lastSplitted && (!lastQuoted || !justQuoted))
                j--;

            lastSplitted = justSplitted;
            justSplitted = false;

            lastQuoted = justQuoted;
            justQuoted = false;
        } else {
            escaped = false;
        }
    }

    if(lastQuoted)
        j--;

    return (new string(parmChars, 0, j)).Split(new[] { '\n' });
}

Baseado na resposta do Vapor in the Alley , este também suporta ^ escapes.

Exemplos:

  • isto é um teste
    • isto
    • é
    • uma
    • teste
  • isto é um teste
    • isto
    • é um
    • teste
  • este ^ "é um teste ^"
    • isto
    • uma"
    • teste
  • este "" "é um teste ^^"
    • isto
    • é um ^ teste

Ele também suporta vários espaços (quebra os argumentos apenas uma vez por bloco de espaços).


O último dos três interfere de alguma forma no Markdown e não é processado conforme o pretendido.
Peter Mortensen

Corrigido com um espaço de largura zero.
Fabio Iotti

0

Atualmente, este é o código que tenho:

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

Não funciona com aspas escapadas, mas funciona para os casos que encontrei até agora.


0

Esta é uma resposta ao código de Anton, que não funciona com aspas de escape. Eu modifiquei 3 lugares.

  1. O construtor de StringBuilder em SplitCommandLineArguments , substituindo qualquer \ " por \ r
  2. No loop for em SplitCommandLineArguments , agora substituo o caractere \ r de volta para \ " .
  3. Alterado o método SplitCommandLineArgument de privado para público estático .

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

Estou lidando com o mesmo problema, você pensaria que hoje em dia existiria uma solução simples para strings de argumento de linha de comando de teste de unidade. Tudo que eu quero ter certeza é o comportamento que resultará de uma determinada string de argumento da linha de comando. Estou desistindo por enquanto e irei criar testes de unidade para string [], mas posso adicionar alguns testes de integração para cobrir isso.
Charlie Barker

0

Eu não acho que existem aspas simples ou aspas ^ para aplicativos C #. A seguinte função está funcionando bem para mim:

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

0

Você pode dar uma olhada no código que postei ontem:

[C #] Strings de caminho e argumentos

Ele divide um nome de arquivo + argumentos em string []. Caminhos curtos, variáveis ​​de ambiente e extensões de arquivo ausentes são tratados.

(Inicialmente era para UninstallString no Registro.)


0

Experimente este código:

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

Está escrito em português.


a documentação é em português
Enamul Hassan

@EnamulHassan Eu diria que o código também está em português, por exemplo posicao_ponteiro += ((fim - posicao_ponteiro)+1);.
MEMark

0

Aqui está uma linha que faz o trabalho (veja a linha que faz todo o trabalho dentro do método BurstCmdLineArgs (...)).

Não é o que eu chamaria de linha de código mais legível, mas você pode separá-la para fins de legibilidade. É simples propositalmente e não funciona bem para todos os casos de argumento (como argumentos de nome de arquivo que contêm o delimitador de caractere de string dividido).

Esta solução funcionou bem nas minhas soluções que a utilizam. Como eu disse, ele executa o trabalho sem um ninho de rato de código para lidar com todos os formatos de argumento n-fatoriais possíveis.

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<string, string> BurstCmdLineArgs(string[] args)
        {
            var argDict = new Dictionary<string, string>();

            // 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".
            // E.g. -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;
        }
    }
}

0

Não consegui encontrar nada de que gostei aqui. Eu odeio bagunçar a pilha com magia de rendimento para uma linha de comando pequena (se fosse um fluxo de um terabyte, seria outra história).

Aqui está minha opinião, ele suporta escapes de aspas com aspas duplas como estes:

param = "a tela de 15" "não é ruim" param2 = 'a tela de 15 "não é ruim' param3 =" "param4 = / param5

resultado:

param = "uma tela de 15" não é ruim "

param2 = 'uma tela de 15 "não é ruim'

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

0

Implementei a máquina de estado para ter os mesmos resultados do analisador como se args fosse passado para o aplicativo .NET e processado no static void Main(string[] args)método.

    public static IList<string> ParseCommandLineArgsString(string commandLineArgsString)
    {
        List<string> args = new List<string>();

        commandLineArgsString = commandLineArgsString.Trim();
        if (commandLineArgsString.Length == 0)
            return args;

        int index = 0;
        while (index != commandLineArgsString.Length)
        {
            args.Add(ReadOneArgFromCommandLineArgsString(commandLineArgsString, ref index));
        }

        return args;
    }

    private static string ReadOneArgFromCommandLineArgsString(string line, ref int index)
    {
        if (index >= line.Length)
            return string.Empty;

        var sb = new StringBuilder(512);
        int state = 0;
        while (true)
        {
            char c = line[index];
            index++;
            switch (state)
            {
                case 0: //string outside quotation marks
                    if (c == '\\') //possible escaping character for quotation mark otherwise normal character
                    {
                        state = 1;
                    }
                    else if (c == '"') //opening quotation mark for string between quotation marks
                    {
                        state = 2;
                    }
                    else if (c == ' ') //closing arg
                    {
                        return sb.ToString();
                    }
                    else
                    {
                        sb.Append(c);
                    }

                    break;
                case 1: //possible escaping \ for quotation mark or normal character
                    if (c == '"') //If escaping quotation mark only quotation mark is added into result
                    {
                        state = 0;
                        sb.Append(c);
                    }
                    else // \ works as not-special character
                    {
                        state = 0;
                        sb.Append('\\');
                        index--;
                    }

                    break;
                case 2: //string between quotation marks
                    if (c == '"') //quotation mark in string between quotation marks can be escape mark for following quotation mark or can be ending quotation mark for string between quotation marks
                    {
                        state = 3;
                    }
                    else if (c == '\\') //escaping \ for possible following quotation mark otherwise normal character
                    {
                        state = 4;
                    }
                    else //text in quotation marks
                    {
                        sb.Append(c);
                    }

                    break;
                case 3: //quotation mark in string between quotation marks
                    if (c == '"') //Quotation mark after quotation mark - that means that this one is escaped and can added into result and we will stay in string between quotation marks state
                    {
                        state = 2;
                        sb.Append(c);
                    }
                    else //we had two consecutive quotation marks - this means empty string but the following chars (until space) will be part of same arg result as well
                    {
                        state = 0;
                        index--;
                    }

                    break;
                case 4: //possible escaping \ for quotation mark or normal character in string between quotation marks
                    if (c == '"') //If escaping quotation mark only quotation mark added into result
                    {
                        state = 2;
                        sb.Append(c);
                    }
                    else
                    {
                        state = 2;
                        sb.Append('\\');
                        index--;
                    }

                    break;
            }

            if (index == line.Length)
                return sb.ToString();
        }
    }

0

Aqui está a solução que trata os espaços (espaços simples ou múltiplos) como separadores de parâmetros de linha de comando e retorna os argumentos reais da linha de comando:

static string[] ParseMultiSpacedArguments(string commandLine)
{
    var isLastCharSpace = false;
    char[] parmChars = commandLine.ToCharArray();
    bool inQuote = false;
    for (int index = 0; index < parmChars.Length; index++)
    {
        if (parmChars[index] == '"')
            inQuote = !inQuote;
        if (!inQuote && parmChars[index] == ' ' && !isLastCharSpace)
            parmChars[index] = '\n';

        isLastCharSpace = parmChars[index] == '\n' || parmChars[index] == ' ';
    }

    return (new string(parmChars)).Split('\n');
}

-2

Não tenho certeza se entendi você, mas será que o problema do caractere usado como divisor também se encontra dentro do texto? (Exceto que é escapado com duplo "?)

Nesse caso, eu criaria um forloop e substituiria todas as instâncias onde <"> está presente por <|> (ou outro caractere" seguro ", mas certifique-se de que ele apenas substitua <">, e não <"">

Depois de iterar a string, eu faria como postado anteriormente, dividir a string, mas agora no caractere <|>.


Os "" duplos são porque é um literal de string @ "..", Os duplos "dentro da string @" .. "são equivalentes a um \ escaped" em uma string normal
Anton

"a única restrição (eu acredito) é que as strings são delimitadas por espaço, a menos que o espaço ocorra dentro de um" ... "bloco" -> Pode estar atirando em um pássaro com uma bazuca, mas coloque um booleano que vá "verdadeiro" quando dentro de uma citação, e se um espaço for detectado dentro de while "true", continue, senão <> = <|>
Israr Khan

-6

Sim, o objeto string tem uma função interna chamada Split()que usa um único parâmetro especificando o caractere a ser procurado como um delimitador e retorna uma matriz de strings (string []) com os valores individuais nele.


1
Isso dividiria a parte src: "C: \ tmp \ Some Folder \ Sub Folder" incorretamente.
Anton

E as aspas dentro da string que desativam temporariamente a divisão em espaços?
Daniel Earwicker
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.