正则表达的麻烦,逃脱报价

时间:2011-05-17 11:56:04

标签: java regex unix command-line

基本上,我正在传递一个字符串,我需要以与命令行选项由* nix shell标记的方式相同的方式对其进行标记

说我有以下字符串

"Hello\" World" "Hello Universe" Hi

我怎么能把它变成3元素列表

  • Hello“World
  • Hello Universe
  • 您好

以下是我的第一次尝试,但它有很多问题

  • 它留下引号字符
  • 它没有捕获转义报价

代码:

public void test() {
    String str = "\"Hello\\\" World\" \"Hello Universe\" Hi";
    List<String> list = split(str);
}

public static List<String> split(String str) {
    Pattern pattern = Pattern.compile(
        "\"[^\"]*\"" + /* double quoted token*/
        "|'[^']*'" + /*single quoted token*/
        "|[A-Za-z']+" /*everything else*/
    );

    List<String> opts = new ArrayList<String>();
    Scanner scanner = new Scanner(str).useDelimiter(pattern);

    String token;
    while ((token = scanner.findInLine(pattern)) != null) {
        opts.add(token);
    }
    return opts;
}

因此以下代码的错误输出是

  • “你好\”
  • 世界
  • “”
  • 你好
  • 宇宙
  • 您好

编辑我对非正则表达式解决方案完全开放。这只是我想到的第一个解决方案

5 个答案:

答案 0 :(得分:2)

我很确定你不能通过标记正则表达式来做到这一点。如果需要处理嵌套和转义分隔符,则需要编写解析器。参见例如http://kore-nordmann.de/blog/do_NOT_parse_using_regexp.html

虽然我不知道,但是会有开源解析器可以做你想做的事情。您还应该查看StreamTokenizer类。

答案 1 :(得分:2)

如果您决定放弃正则表达式,并进行解析,则有几种选择。如果您愿意仅使用双引号或单引号(但不是两者)作为引用,那么您可以使用StreamTokenizer轻松解决此问题:

public static List<String> tokenize(String s) throws IOException {
    List<String> opts = new ArrayList<String>();
    StreamTokenizer st = new StreamTokenizer(new StringReader(s));
    st.quoteChar('\"');
    while (st.nextToken() != StreamTokenizer.TT_EOF) {
        opts.add(st.sval);
    }

    return opts;
}

如果你必须同时支持两个引号,这里有一个天真的实现应该有效(告诫像''blah \“blah”blah'这样的字符串会产生类似'blah“blahblah'的东西。如果不行,你需要做一些改变):

   public static List<String> splitSSV(String in) throws IOException {
        ArrayList<String> out = new ArrayList<String>();

        StringReader r = new StringReader(in);
        StringBuilder b = new StringBuilder();
        int inQuote = -1;
        boolean escape = false;
        int c;
        // read each character
        while ((c = r.read()) != -1) {
            if (escape) {  // if the previous char is escape, add the current char
                b.append((char)c);
                escape = false;
                continue;
            }
            switch (c) {
            case '\\':   // deal with escape char
                escape = true;
                break;
            case '\"':
            case '\'':  // deal with quote chars
                if (c == '\"' || c == '\'') {
                    if (inQuote == -1) {  // not in a quote
                        inQuote = c;  // now we are
                    } else {
                        inQuote = -1;  // we were in a quote and now we aren't
                    }
                }
                break;
            case ' ':
                if (inQuote == -1) {  // if we aren't in a quote, then add token to list
                    out.add(b.toString());
                    b.setLength(0);
                } else {
                    b.append((char)c); // else append space to current token
                }
                break;
            default:
                b.append((char)c);  // append all other chars to current token
            }
        }
        if (b.length() > 0) {
            out.add(b.toString()); // add final token to list
        }
        return out;
    }

答案 2 :(得分:2)

总结一下,你想要在空白上拆分,除非用双引号括起来,前面没有反斜杠。

第1步:将输入标记为:/([ \t]+)|(\\")|(")|([^ \t"]+)/

这为您提供了一系列SPACE,ESCAPED_QUOTE,QUOTE和TEXT标记。

步骤2:构建匹配并对令牌作出反应的有限状态机:

状态:START

  • 空间 - &gt;返回空字符串
  • ESCAPED_QUOTE - &gt;错误(?)
  • QUOTE - &gt;州:= WITHIN_QUOTES
  • TEXT - &gt;返回文字

状态:WITHIN_QUOTES

  • 空间 - &gt;为累加器添加值
  • ESCAPED_QUOTE - &gt;添加报价到累加器
  • QUOTE - &gt;返回和清除累加器;州:= START
  • TEXT - &gt;将文本添加到累加器

第3步:获利!!

答案 3 :(得分:1)

我想如果你使用这样的模式:

Pattern pattern = Pattern.compile("\".*?(?<!\\\\)\"|'.*?(?<!\\\\)'|[A-Za-z']+");

然后它会给你想要的输出。当我使用您的输入数据运行时,我得到了这个列表:

["Hello\" World", "Hello Universe", Hi]


我在您自己的问题中使用了[A-Za-z']+但不应该只是:[A-Za-z]+

修改

将您的opts.add(token);行更改为:

opts.add(token.replaceAll("^\"|\"$|^'|'$", ""));

答案 4 :(得分:0)

你需要做的第一件事是停止以split()的方式思考这项工作。 split()用于分解this/that/the other之类的简单字符串,其中/始终是分隔符。但你试图分裂空白,除非空格在引号内,除了如果引号用反斜杠转义(如果反斜杠转义引号,它们可能会逃避其他事物,像其他反斜杠一样)。

除了所有异常例外之外,创建正则表达式以匹配所有可能的分隔符是不可能的,甚至不能使用像外观,条件,不情愿和占有量词等花哨的噱头。你想要做的是匹配令牌,而不是分隔符。

在下面的代码中,用双引号或单引号括起来的标记可能包含空格以及引号字符(如果前面带有反斜杠)。除了封闭引号之外的所有内容都在第1组(对于双引号)或第2组(单引号)中捕获。即使在非引用的标记中,任何字符都可以使用反斜杠进行转义;在单独的步骤中删除“转义”反斜杠。

public static void test()
{
  String str = "\"Hello\\\" World\" 'Hello Universe' Hi";
  List<String> commands = parseCommands(str);
  for (String s : commands)
  {
    System.out.println(s);
  }
}

public static List<String> parseCommands(String s)
{
  String rgx = "\"((?:[^\"\\\\]++|\\\\.)*+)\""  // double-quoted
             + "|'((?:[^'\\\\]++|\\\\.)*+)'"    // single-quoted
             + "|\\S+";                         // not quoted
  Pattern p = Pattern.compile(rgx);
  Matcher m = p.matcher(s);
  List<String> commands = new ArrayList<String>();
  while (m.find())
  {
    String cmd = m.start(1) != -1 ? m.group(1) // strip double-quotes
               : m.start(2) != -1 ? m.group(2) // strip single-quotes
               : m.group();
    cmd = cmd.replaceAll("\\\\(.)", "$1");  // remove escape characters
    commands.add(cmd);
  }
  return commands;
}

输出:

Hello" World
Hello Universe
Hi

这与基于正则表达式的解决方案一样简单 - 它并不真正处理格式错误的输入,如不平衡的报价。如果您不熟悉正则表达式,那么使用纯手工编码的解决方案或者更好的是专用的命令行解释器(CLI)库可能会更好。