Tokenizar una Cadena pero ignorando los delimitadores dentro de las comillas

Deseo tener la siguiente cadena

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

para convertirse en una matriz de los siguientes

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

Lo intenté

 new StringTokenizer(cmd, "\"") 

pero esto devolvería “Otro” y “YA otro como” Otro y Otro “que no es el efecto deseado.

Gracias.

EDITAR: He cambiado el ejemplo una vez más, esta vez creo que explica mejor la situación, aunque no es diferente del segundo ejemplo.

Es mucho más fácil usar un java.util.regex.Matcher y hacer un find() lugar de cualquier tipo de split en este tipo de escenario.

Es decir, en lugar de definir el patrón para el delimitador entre los tokens, usted define el patrón para los tokens .

Aquí hay un ejemplo:

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

Las impresiones de arriba ( como se ve en ideone.com ):

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

El patrón es esencialmente:

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

Hay 2 suplentes:

  • El primer alterno coincide con la comilla doble de apertura, una secuencia de todo menos comillas dobles (capturadas en el grupo 1), luego la comilla doble de cierre
  • La segunda alternativa coincide con cualquier secuencia de caracteres que no sean espacios en blanco, capturados en el grupo 2
  • El orden de las alternativas importa en este patrón

Tenga en cuenta que esto no maneja las comillas dobles escapadas dentro de los segmentos entre comillas. Si necesita hacer esto, entonces el patrón se vuelve más complicado, pero la solución de Matcher todavía funciona.

Referencias

  • regular-expressions.info/Brackets para agrupar y capturar , alternancia con barra vertical , clase de personaje , repetición con estrella y más

Ver también

  • regular-expressions.info/Examples – Programmer – Strings – para patrón con comillas escapadas

Apéndice

Tenga en cuenta que StringTokenizer es una clase heredada . Se recomienda usar java.util.Scanner o String.split , o por supuesto java.util.regex.Matcher para mayor flexibilidad.

Preguntas relacionadas

  • ¿Diferencia entre una API obsoleta y heredada?
  • Scanner vs. StringTokenizer vs. String.Split
  • Validar la entrada usando java.util.Scanner – tiene muchos ejemplos

Hazlo a la vieja usanza. Haga una función que mire cada carácter en un ciclo for. Si el personaje es un espacio, tome todo hasta eso (excluyendo el espacio) y agréguelo como una entrada al conjunto. Observe la posición, y haga lo mismo de nuevo, agregando esa siguiente parte a la matriz después de un espacio. Cuando se encuentra una comilla doble, marque un booleano llamado ‘inQuote’ como verdadero, e ignore los espacios cuando inQuote es verdadero. Cuando pulses comillas cuando inQuote sea verdadero, márcalo como falso y vuelve a dividir las cosas cuando se encuentre un espacio. A continuación, puede extender esto según sea necesario para admitir caracteres de escape, etc.

¿Podría hacerse esto con una expresión regular? No lo sé, supongo. Pero toda la función requeriría menos para escribir que esta respuesta.

De una manera pasada de moda:

 public static String[] split(String str) { str += " "; // To detect last token when not quoted... ArrayList strings = new ArrayList(); 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()]); } 

Supongo que las comillas anidadas son ilegales, y también que los tokens vacíos se pueden omitir.

¡Apache Commons al rescate!

 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 

Salida:

[es, esto, completamente “imposible”, o, “un poco” imposible, para, analizar?]

Algunas notas:

  1. esto está escrito en Groovy … de hecho es una secuencia de comandos Groovy. La línea @Grab da una pista sobre el tipo de línea de dependencia que necesitas (por ejemplo, en build.gradle ) … o simplemente incluye el .jar en tu classpath por supuesto
  2. StringTokenizer aquí NO es java.util.StringTokenizer … ya que la línea de import muestra que es org.apache.commons.text.StringTokenizer
  3. la línea def str = ... es una forma de producir una String en Groovy que contiene tanto comillas simples como comillas dobles sin tener que entrar para escapar
  4. StringMatcherFactory en apache commons-text 1.3 se puede encontrar aquí : como puede ver, el INSTANCE puede proporcionarle un montón de diferentes StringMatcher . Incluso podría lanzar el suyo propio, pero tendría que examinar el código fuente de StringMatcherFactory para ver cómo se hace.
  5. ¡SÍ! No solo puede incluir el “otro tipo de cita” y se interpreta correctamente como que no es un límite simbólico … pero incluso puede escapar de la cita real que se utiliza para desactivar la duplicación , duplicando la cita dentro de la tokenización. -protegido un poco de la cadena! Intenta implementar eso con unas pocas líneas de código … ¡o mejor dicho, no!

PD: ¿por qué es mejor usar Apache Commons que con cualquier otra solución? Aparte del hecho de que no tiene sentido reinventar la rueda, puedo pensar en al menos dos razones:

  1. Se puede contar con que los ingenieros de Apache anticiparon todos los errores y desarrollaron un código robusto, probado exhaustivamente y confiable.
  2. Significa que no ocupas tu hermoso código con los métodos de utilidad stoopid: solo tienes un código bonito y limpio que hace exactamente lo que dice en la lata, dejándote para seguir con las, um, cosas interesantes. .

PPS Nada te obliga a mirar el código Apache como misteriosas “cajas negras”. La fuente está abierta y escrita en Java generalmente “accesible”. Por lo tanto, eres libre de examinar cómo se hacen las cosas para tu corazón. A menudo es bastante instructivo hacerlo.

luego

Suficientemente intrigada por la pregunta de ArtB eché un vistazo a la fuente:

en StringMatcherFactory.java vemos:

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

… más bien aburrido …

entonces eso lleva a mirar a StringTokenizer.java:

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

OK … y luego, en el mismo archivo java:

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

que contiene el comentario:

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

… No puedo molestarme en seguir las pistas más allá. Usted tiene una opción: su solución “hackish”, donde preprocesa sistemáticamente sus cadenas antes de enviarlas para la creación de tokens, convirtiendo | \\\ “| s en | \” \ “| s … (es decir, donde reemplaza cada | | | con | ” “ |) …
O … examina org.apache.commons.text.StringTokenizer.java para descubrir cómo modificar el código. Es un archivo pequeño. No creo que sea tan difícil. Luego comstack, esencialmente haciendo un tenedor del código Apache.

No creo que se pueda configurar. Pero si encontraste una solución de corrección de código que tenía sentido, podrías enviarla a Apache y luego podría ser aceptada para la siguiente iteración del código, y tu nombre figuraría al menos en la parte de “solicitudes de características” de Apache: podría ser una forma de kleos a través de la cual logras la inmortalidad de progtwigción …

El ejemplo que tienes aquí simplemente debe dividirse por el carácter de comillas dobles.

Esta es una vieja pregunta, sin embargo, esta fue mi solución como una máquina de estados finitos.

Eficientes, predecibles y sin trucos de fantasía.

100% de cobertura en las pruebas.

Arrastra y suelta en tu código.

 /** * 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 splitCommand(String inputString) { List matchList = new LinkedList<>(); LinkedList charList = inputString.chars() .mapToObj(i -> (char) i) .collect(Collectors.toCollection(LinkedList::new)); // Finite-State Automaton for parsing. CommandSplitterState state = CommandSplitterState.BeginningChunk; LinkedList 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 } 

Otra forma de la vieja escuela es:

 public static void main(String[] args) { String text = "One two \"three four\" five \"six seven eight\" nine \"ten\""; String[] splits = text.split(" "); List 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); } 

Salida: – [Uno, dos, “tres cuatro”, cinco, “seis siete ocho”, nueve]

prueba esto:

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

No sé el contexto de lo que estás tratando de hacer, pero parece que tratas de analizar los argumentos de la línea de comando. En general, esto es bastante complicado con todos los problemas de escape; si este es tu objective, personalmente miraría algo como JCommander.

Prueba esto:

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

Es un poco complicado porque necesitas escapar de las comillas dobles. Esta expresión regular debe tokenizar la cadena usando un espacio en blanco (s) o una comilla doble.

Debe usar el método de split de String porque acepta expresiones regulares, mientras que el argumento constructor para el delimitador en StringTokenizer no lo hace. Al final de lo que proporcioné arriba, puede agregar lo siguiente:

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