Подтвердить что ты не робот

Токенизация строки, но игнорирование разделителей в кавычках

Я хочу иметь следующую строку

!cmd 45 90 "An argument" Another AndAnother "Another one in quotes"

чтобы стать массивом следующих

{ "!cmd", "45", "90", "An argument", "Another", "AndAnother", "Another one in quotes" }

Я пробовал

new StringTokenizer(cmd, "\"")

но это вернет "Другой" и "AndAnother как" Another AndAnother ", который не является желаемым эффектом.

Спасибо.

EDIT: Я снова изменил пример, на этот раз я считаю, что он лучше всего объясняет ситуацию, хотя это ничем не отличается от второго примера.

4b9b3361

Ответ 1

Намного проще использовать java.util.regex.Matcher и сделать find(), а не любой тип split в этих сценариях.

То есть вместо определения шаблона разделителя между токенами вы определяете шаблон для самих токенов.

Вот пример:

    String text = "1 2 \"333 4\" 55 6    \"77\" 8 999";
    // 1 2 "333 4" 55 6    "77" 8 999

    String regex = "\"([^\"]*)\"|(\\S+)";

    Matcher m = Pattern.compile(regex).matcher(text);
    while (m.find()) {
        if (m.group(1) != null) {
            System.out.println("Quoted [" + m.group(1) + "]");
        } else {
            System.out.println("Plain [" + m.group(2) + "]");
        }
    }

Вышеприведенные отпечатки (как видно на ideone.com):

Plain [1]
Plain [2]
Quoted [333 4]
Plain [55]
Plain [6]
Quoted [77]
Plain [8]
Plain [999]

Образец по существу:

"([^"]*)"|(\S+)
 \_____/  \___/
    1       2

Есть 2 варианта:

  • Первая альтернатива соответствует двойной двойной кавычке, последовательности всего, кроме двойной кавычки (зафиксированной в группе 1), затем закрывающей двойной кавычки
  • Вторая альтернатива соответствует любой последовательности символов без пробелов, снятых в группе 2
  • Порядок альтернативной материи в этом шаблоне

Обратите внимание, что это не обрабатывает скрытые двойные кавычки в указанных сегментах. Если вам нужно это сделать, шаблон становится более сложным, но решение Matcher все еще работает.

Ссылки

См. также


Приложение

Обратите внимание, что StringTokenizer является устаревшим классом. Рекомендуется использовать java.util.Scanner или String.split, или, конечно, java.util.regex.Matcher для большей гибкости.

Связанные вопросы

Ответ 2

Сделайте это старомодным способом. Сделайте функцию, которая смотрит на каждый символ в цикле for. Если символ - это пробел, возьмите все до этого (исключая пробел) и добавьте его как запись в массив. Обратите внимание на позицию и сделайте то же самое, добавив следующую часть в массив после пробела. Когда встречается двойная кавычка, помечайте логическое имя "inQuote" как true и игнорируйте пробелы, когда inQuote истинно. Когда вы нажимаете кавычки, когда inQuote истинно, отметьте его как false и вернитесь, чтобы разбить вещи, когда пространство встречается. Затем вы можете расширить его по мере необходимости, чтобы поддерживать escape-символы и т.д.

Можно ли это сделать с помощью регулярного выражения? Наверное, я не знаю. Но вся функция будет писать меньше, чем этот ответ.

Ответ 3

Старомодно:

public static String[] split(String str) {
    str += " "; // To detect last token when not quoted...
    ArrayList<String> strings = new ArrayList<String>();
    boolean inQuote = false;
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < str.length(); i++) {
        char c = str.charAt(i);
        if (c == '"' || c == ' ' && !inQuote) {
            if (c == '"')
                inQuote = !inQuote;
            if (!inQuote && sb.length() > 0) {
                strings.add(sb.toString());
                sb.delete(0, sb.length());
            }
        } else
            sb.append(c);
    }
    return strings.toArray(new String[strings.size()]);
}

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

Ответ 4

Apache Commons на помощь!

import org.apache.commons.text.StringTokenizer
import org.apache.commons.text.matcher.StringMatcher
import org.apache.commons.text.matcher.StringMatcherFactory
@Grab(group='org.apache.commons', module='commons-text', version='1.3')

def str = /is this   'completely "impossible"' or """slightly"" impossible" to parse?/

StringTokenizer st = new StringTokenizer( str )
StringMatcher sm = StringMatcherFactory.INSTANCE.quoteMatcher()
st.setQuoteMatcher( sm )

println st.tokenList

Выход:

[это совершенно "невозможно" или, "слегка" невозможно, разобрать?]

Несколько заметок:

  1. это написано в Groovy... это на самом деле Groovy сценарий. @Grab дает ключ к виду @Grab линии зависимости (например, в build.gradle)... или, конечно, просто включает .jar в ваш путь к классам.
  2. StringTokenizer здесь НЕ является java.util.StringTokenizer... как показывает строка import это org.apache.commons.text.StringTokenizer
  3. строка def str =... - это способ создания String в Groovy, которая содержит как одинарные, так и двойные кавычки без необходимости экранирования
  4. StringMatcherFactory в apache commons-text 1.3 можно найти здесь: как вы можете видеть, INSTANCE может предоставить вам кучу разных StringMatcher. Вы даже можете свернуть свой собственный: но вам нужно изучить исходный код StringMatcherFactory чтобы увидеть, как это делается.
  5. ДА! Вы можете не только включить "другой тип цитаты", и он правильно интерпретируется как не являющийся границей токена... но вы даже можете избежать реальной цитаты, которая используется для отключения токенизации, удвоив кавычку в токенизации -защищенная строка! Попробуйте реализовать это с помощью нескольких строк кода... или, скорее, не надо!

PS Почему лучше использовать Apache Commons, чем любое другое решение? Помимо того, что нет смысла заново изобретать колесо, я могу придумать как минимум две причины:

  1. Можно рассчитывать, что инженеры Apache предвосхитят все ошибки и разработают надежный, всесторонне протестированный, надежный код
  2. Это означает, что вы не загромождаете свой красивый код с помощью утилитарных методов - у вас просто есть хороший, чистый кусок кода, который в точности соответствует тому, что написано на банке, оставляя вам возможность заняться интересными вещами...,

PPS Ничто не обязывает вас смотреть на код Apache как на таинственные "черные ящики". Исходный код открыт и написан обычно на "доступной" Java. Следовательно, вы можете исследовать, как все делается с вашим сердцем. Это часто весьма поучительно.

потом

Достаточно заинтригованный вопросом ArtB, я взглянул на источник:

в StringMatcherFactory.java мы видим:

private static final AbstractStringMatcher.CharSetMatcher QUOTE_MATCHER = new AbstractStringMatcher.CharSetMatcher(
            "'\"".toCharArray());

... довольно скучно...

так что это заставляет взглянуть на StringTokenizer.java:

public StringTokenizer setQuoteMatcher(final StringMatcher quote) {
        if (quote != null) {
            this.quoteMatcher = quote;
        }
        return this;
}

ОК... а затем, в том же файле Java:

private int readWithQuotes(final char[] srcChars ...

который содержит комментарий:

// If we've found a quote character, see if it followed by a second quote. If so, then we need to actually put the quote character into the token rather than end the token.

... Я не могу потрудиться следовать подсказкам дальше. У вас есть выбор: либо ваше "хакерское" решение, где вы систематически предварительно обрабатываете свои строки перед тем, как отправлять их на токенизацию, превращая | \\\ "| s в | \"\"| s... (то есть где вы заменяете каждый | \ " | с | " " |)...
Или... вы изучаете org.apache.commons.text.StringTokenizer.java, чтобы выяснить, как настроить код. Это маленький файл. Я не думаю, что это будет так сложно. Затем вы компилируете, по сути, создавая форк кода Apache.

Я не думаю, что это можно настроить. Но если вы нашли решение для подстройки кода, которое имело смысл, вы могли бы отправить его в Apache, а затем оно могло бы быть принято для следующей итерации кода, и ваше имя фигурировало бы, по крайней мере, в части Apache "запрос возможностей": может быть формой kleos, с помощью которой вы достигаете бессмертия программирования...

Ответ 5

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

Один из возможных случаев:

"/opt/jboss-eap/bin/jboss-cli.sh --connect - -c ontroller = localhost: 9990 -c command = \" deploy/app/jboss-eap-7.1/standalone/updates/sample.war --force\""

Это должно было быть разделено на

/opt/jboss-eap/bin/jboss-cli.sh
--connect
--controller=localhost:9990
-c
command="deploy /app/jboss-eap-7.1/standalone/updates/sample.war --force"

Просто добавьте к ответу @polygenelubricants, имея любой непробельный символ до и после того, как может сработать сопоставитель цитат.

"\\S*\"([^\"]*)\"\\S*|(\\S+)"

Пример:

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Tokenizer {

    public static void main(String[] args){

        String a = "/opt/jboss-eap/bin/jboss-cli.sh --connect --controller=localhost:9990 -c command=\"deploy " +
                "/app/jboss-eap-7.1/standalone/updates/sample.war --force\"";
        String b = "Hello \"Stack Overflow\"";
        String c = "cmd=\"abcd efgh ijkl mnop\" \"apple\" banana mango";
        String d = "abcd ef=\"ghij klmn\"op qrst";
        String e = "1 2 \"333 4\" 55 6    \"77\" 8 999";

        List<String> matchList = new ArrayList<String>();
        Pattern regex = Pattern.compile("\\S*\"([^\"]*)\"\\S*|(\\S+)");
        Matcher regexMatcher = regex.matcher(a);
        while (regexMatcher.find()) {
            matchList.add(regexMatcher.group());
        }
        System.out.println("matchList="+matchList);
    }
}

Выход:

matchList = [/opt/jboss-eap/bin/jboss -c li.sh, --connect, - -c ontroller = localhost: 9990, -c, команда = "deploy/app/jboss-eap-7.1/standalone/updates/sample.war --force "]

Ответ 6

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

Ответ 7

Это старый вопрос, но это было мое решение как конечная машина.

Эффективные, предсказуемые и не фантастические трюки.

100% охват тестов.

Перетащите в свой код.

/**
 * Splits a command on whitespaces. Preserves whitespace in quotes. Trims excess whitespace between chunks. Supports quote
 * escape within quotes. Failed escape will preserve escape char.
 *
 * @return List of split commands
 */
static List<String> splitCommand(String inputString) {
    List<String> matchList = new LinkedList<>();
    LinkedList<Character> charList = inputString.chars()
            .mapToObj(i -> (char) i)
            .collect(Collectors.toCollection(LinkedList::new));

    // Finite-State Automaton for parsing.

    CommandSplitterState state = CommandSplitterState.BeginningChunk;
    LinkedList<Character> chunkBuffer = new LinkedList<>();

    for (Character currentChar : charList) {
        switch (state) {
            case BeginningChunk:
                switch (currentChar) {
                    case '"':
                        state = CommandSplitterState.ParsingQuote;
                        break;
                    case ' ':
                        break;
                    default:
                        state = CommandSplitterState.ParsingWord;
                        chunkBuffer.add(currentChar);
                }
                break;
            case ParsingWord:
                switch (currentChar) {
                    case ' ':
                        state = CommandSplitterState.BeginningChunk;
                        String newWord = chunkBuffer.stream().map(Object::toString).collect(Collectors.joining());
                        matchList.add(newWord);
                        chunkBuffer = new LinkedList<>();
                        break;
                    default:
                        chunkBuffer.add(currentChar);
                }
                break;
            case ParsingQuote:
                switch (currentChar) {
                    case '"':
                        state = CommandSplitterState.BeginningChunk;
                        String newWord = chunkBuffer.stream().map(Object::toString).collect(Collectors.joining());
                        matchList.add(newWord);
                        chunkBuffer = new LinkedList<>();
                        break;
                    case '\\':
                        state = CommandSplitterState.EscapeChar;
                        break;
                    default:
                        chunkBuffer.add(currentChar);
                }
                break;
            case EscapeChar:
                switch (currentChar) {
                    case '"': // Intentional fall through
                    case '\\':
                        state = CommandSplitterState.ParsingQuote;
                        chunkBuffer.add(currentChar);
                        break;
                    default:
                        state = CommandSplitterState.ParsingQuote;
                        chunkBuffer.add('\\');
                        chunkBuffer.add(currentChar);
                }
        }
    }

    if (state != CommandSplitterState.BeginningChunk) {
        String newWord = chunkBuffer.stream().map(Object::toString).collect(Collectors.joining());
        matchList.add(newWord);
    }
    return matchList;
}

private enum CommandSplitterState {
    BeginningChunk, ParsingWord, ParsingQuote, EscapeChar
}

Ответ 8

Другой способ старой школы:

public static void main(String[] args) {

    String text = "One two \"three four\" five \"six seven eight\" nine \"ten\"";
    String[] splits = text.split(" ");
    List<String> list = new ArrayList<>();
    String token = null;
    for(String s : splits) {

        if(s.startsWith("\"") ) {
            token = "" + s; 
        } else if (s.endsWith("\"")) {
            token = token + " "+ s;
            list.add(token);
            token = null;
        } else {
            if (token != null) {
                token = token + " " + s;
            } else {
                list.add(s);
            }
        }
    }
    System.out.println(list);
}

Вывод: - [Раз, два, "три четыре", пять, "шесть семь восемь", девять]

Ответ 9

Попробуйте следующее:

String str = "One two \"three four\" five \"six seven eight\" nine \"ten\"";
String strArr[] = str.split("\"|\s");

Это довольно сложно, потому что вам нужно избегать двойных кавычек. Это регулярное выражение должно токенизировать строку, используя либо пробел (\ s), либо двойную кавычку.

Вы должны использовать метод String split, потому что он принимает регулярные выражения, тогда как аргумент конструктора для разделителя в StringTokenizer не работает. В конце того, что я указал выше, вы можете просто добавить следующее:

String s;
for(String k : strArr) {
     s += k;
}
StringTokenizer strTok = new StringTokenizer(s);

Ответ 10

попробуйте следующее:

String str = "One two \"three four\" five \"six seven eight\" nine \"ten\"";
String[] strings = str.split("[ ]?\"[ ]?");

Ответ 11

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