在Java中将包含命令行参数的字符串拆分为String[]

34

与C#的此线程类似,我需要拆分包含程序命令行参数的字符串,以便用户可以轻松运行多个命令。例如,我可能有以下字符串:

-p /path -d "here's my description" --verbose other args

鉴于上述情况,Java通常会将以下内容传递给main函数:
Array[0] = -p
Array[1] = /path
Array[2] = -d
Array[3] = here's my description
Array[4] = --verbose
Array[5] = other
Array[6] = args

我不需要担心任何shell扩展,但它必须足够智能,能处理字符串中可能存在的单引号、双引号和转义字符。有人知道如何在这些条件下解析字符串以模拟shell吗?
注意:我不需要进行命令行解析,我已经在使用joptsimple进行解析。相反,我想让我的程序易于脚本化。例如,我希望用户能够在单个文件中放置一组命令,每个命令都可以在命令行上有效。例如,他们可以在文件中输入以下内容:
--addUser admin --password Admin --roles administrator,editor,reviewer,auditor
--addUser editor --password Editor --roles editor
--addUser reviewer --password Reviewer --roles reviewer
--addUser auditor --password Auditor --roles auditor

然后用户将按以下方式运行我的管理员工具:

adminTool --script /path/to/above/file

main()函数将查找--script选项并迭代文件中的不同行,将每一行分割成一个数组,然后将其发送到joptsimple实例,该实例将传递到我的应用程序驱动程序。

joptsimple带有一个解析器,其中包含一个parse方法,但它仅支持String数组。 同样,GetOpt构造函数也需要一个String[] - 因此需要一个解析器。


3
你能否直接使用在main()中提供给你的args数组,而不是试图自己解析它? - Jeff Mercado
我已经更新了我的问题,描述了为什么我需要解析该字符串以及与命令行解析的区别。 - Kaleb Pederson
我认为这与命令行解析没有任何区别,可以参考我的答案附录,了解我过去如何处理类似的问题。 - user177800
刚刚添加了一个简短的答案,你可能会发现有用——既然你已经给你的问题添加了一些解释 :-) - Andreas Dolk
6个回答

32

这里有一个相当简单的替代方案,用于将文件中的文本行拆分为参数向量,以便您可以将其输入到选项解析器中:

以下是解决方案:

public static void main(String[] args) {
    String myArgs[] = Commandline.translateCommandline("-a hello -b world -c \"Hello world\"");
    for (String arg:myArgs)
        System.out.println(arg);
}

神奇的类Commandlineant的一部分。因此,您需要将ant放在类路径上,或者将Commandline类作为静态使用的方法。


1
作为文档,translateCommandline 处理单引号和双引号字符串及其中的转义字符,但不像 POSIX shell 那样识别反斜杠,因为这会在基于 DOS 的系统上引起问题。 - Kaleb Pederson
有一个ant的源代码分发版本。此时,我将采取“translateCommandline”的实现并对其进行修改以适应我的需求。 - Andreas Dolk
1
小心,对于此方法,\t\r\n不是空格。 - basin
4
仍然是唯一的方法吗?核心库中有什么吗? - Mr_and_Mrs_D
4
实现(第337行):translateCommandline - Mr_and_Mrs_D

10

如果您只需要支持类UNIX操作系统,有一种更好的解决方案。与来自ant的Commandline不同,来自DrJava的ArgumentTokenizer更像是sh:它支持转义!

说真的,即使是一些疯狂的东西,比如sh -c 'echo "\"un'\''kno\"wn\$\$\$'\'' with \$\"\$\$. \"zzz\""'也会被正确地分解成[bash, -c, echo "\"un'kno\"wn\$\$\$' with \$\"\$\$. \"zzz\""](顺便提一下,运行此命令会输出"un'kno"wn$$$' with $"$$. "zzz")。


8
你应该使用一个功能齐全的现代面向对象命令行参数解析器,我建议使用我最喜欢的Java Simple Argument Parser。并且如何使用JSAP,这是以Groovy为例,但是对于直接的Java也是一样的。还有args4j,在某些方面比JSAP更加现代化,因为它使用注释。不要使用apache.commons.cli,它已经过时了,在API上非常程序化和不符合Java规范。但是我仍然依赖JSAP,因为它非常容易构建自己的自定义参数处理程序。
有许多默认的解析器可以用于URL、数字、InetAddress、颜色、日期、文件、类等,而且很容易添加自己的解析器。
例如,这里是一个将参数映射到枚举的处理程序:
import com.martiansoftware.jsap.ParseException;
import com.martiansoftware.jsap.PropertyStringParser;

/*
This is a StringParser implementation that maps a String to an Enum instance using Enum.valueOf()
 */
public class EnumStringParser extends PropertyStringParser
{
    public Object parse(final String s) throws ParseException
    {
        try
        {
            final Class klass = Class.forName(super.getProperty("klass"));
            return Enum.valueOf(klass, s.toUpperCase());
        }
        catch (ClassNotFoundException e)
        {
            throw new ParseException(super.getProperty("klass") + " could not be found on the classpath");
        }
    }
}

我不喜欢通过XML进行配置编程,但JSAP有一种非常好的方法来声明选项和设置,使你的代码不会被上百行的设置所淹没,这些设置会让真正的功能代码变得难以理解。请参考我的链接如何使用JSAP,这是其他库中代码最少的例子。
这是一个解决你在更新中澄清的问题的方向性解决方案,你的“脚本”文件中的行仍然是命令行。逐行从文件中读取并调用JSAP.parse(String);
我经常使用这种技术为Web应用程序提供“命令行”功能。其中一个特定的用途是在具有导演/Flash前端的大型多人在线游戏中,我们启用从聊天中执行“命令”,并在后端使用JSAP解析它们并执行基于解析内容的代码。非常类似于您想要做的事情,只是您从文件而不是套接字中读取“命令”。我建议放弃joptsimple,直接使用JSAP,您将真正被其强大的可扩展性所吸引。

JSAP是我见过的第一个可以接受字符串的解析器,但不幸的是,它返回一个JSAPResult而不是一个String[],所以我将不得不切换我的命令行解析库:(。 - Kaleb Pederson
一个 String[] 是相当无用的,JSAP 的整个目的就是为了帮你完成所有的解析、规则执行和检查。我认为如果你真正回过头来看看自己的位置,重新思考一下你的方法并进行一些重构将会非常有益。请参考我的更新,基于你最后的编辑。 - user177800
我不想构建一个 shell 字符串解析器。line.split(" ") 并不够智能。它会在创建 Array[3] 的参数上失败,正如我在帖子中指出的那样,因为参数中可能包含空格和转义序列。我需要一个完整的解析器来处理所有可能性——但我需要一个字符串到 String[] 解析器,而不是一个命令行解析器。 - Kaleb Pederson
1
JSAP可能需要阅读文档几次才能理解它提供的选项,但它是一个非常好的解决方案,适用于命令行解析需求,并且运行良好-绝对推荐... - Gwyn Evans
JSAP的CommandLineTokenizer非常接近我所需要的(可能足够了)。它像Windows 2000一样解析字符串,而不是像Unix shell那样。 - Kaleb Pederson
显示剩余3条评论

6
/**
 * [code borrowed from ant.jar]
 * Crack a command line.
 * @param toProcess the command line to process.
 * @return the command line broken into strings.
 * An empty or null toProcess parameter results in a zero sized array.
 */
public static String[] translateCommandline(String toProcess) {
    if (toProcess == null || toProcess.length() == 0) {
        //no command? no string
        return new String[0];
    }
    // parse with a simple finite state machine

    final int normal = 0;
    final int inQuote = 1;
    final int inDoubleQuote = 2;
    int state = normal;
    final StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true);
    final ArrayList<String> result = new ArrayList<String>();
    final StringBuilder current = new StringBuilder();
    boolean lastTokenHasBeenQuoted = false;

    while (tok.hasMoreTokens()) {
        String nextTok = tok.nextToken();
        switch (state) {
        case inQuote:
            if ("\'".equals(nextTok)) {
                lastTokenHasBeenQuoted = true;
                state = normal;
            } else {
                current.append(nextTok);
            }
            break;
        case inDoubleQuote:
            if ("\"".equals(nextTok)) {
                lastTokenHasBeenQuoted = true;
                state = normal;
            } else {
                current.append(nextTok);
            }
            break;
        default:
            if ("\'".equals(nextTok)) {
                state = inQuote;
            } else if ("\"".equals(nextTok)) {
                state = inDoubleQuote;
            } else if (" ".equals(nextTok)) {
                if (lastTokenHasBeenQuoted || current.length() != 0) {
                    result.add(current.toString());
                    current.setLength(0);
                }
            } else {
                current.append(nextTok);
            }
            lastTokenHasBeenQuoted = false;
            break;
        }
    }
    if (lastTokenHasBeenQuoted || current.length() != 0) {
        result.add(current.toString());
    }
    if (state == inQuote || state == inDoubleQuote) {
        throw new RuntimeException("unbalanced quotes in " + toProcess);
    }
    return result.toArray(new String[result.size()]);
}

3

-2

1
除非我漏掉了什么,否则 getopt 端口不接受字符串,只接受 String[] - Kaleb Pederson
你能详细说明如何使用它吗?仅仅提供一个链接不太好。 - Angelo Fuchs
链接已失效... - Graham Leggett

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接