应用场景
将用户从控制台输入的完整命令行,解析为具体的选项参数。
主要包含两个要点:
- 如何可靠准确的将命令行完整的字符串解析为选项字符串数组。
- 将解析出来的选项放的提取具体的选项内容。
方法详解
Apache 组件库 commons-cli
提供了对命令行参数解析的 API,下面方法是 DefaultParser
类中 parse
方法。
public CommandLine parse(final Options options, final String[] arguments) throws ParseException {
return parse(options, arguments, null);
}
方法接收2个参数,一个是定义好的各个具体参数的集合,一个是选项字符串参数数组。
先说第一个参数,这个很容易,自己细心定义即可,下面是一个示例类:
public final class Arguments {
// 存储所有的选项参数
public final static Options ALL_OPTIONS = new Options ();
public final static Option USER = Arguments.add (Option.builder ("u").longOpt ("user").desc ("credentials").required (false).hasArg (true).desc ("user:password").build ());
public final static Option CA_CERT = Arguments.add (Option.builder ("cacert").longOpt ("cacert").desc ("CA certificate").required (false).hasArg (true).desc ("CA_CERT").build ());
public final static Option CERT = Arguments.add (Option.builder ("E").longOpt ("cert").desc ("client certificate").required (false).hasArg (true).desc ("CERT[:password]").build ());
// 此处省略更多选项参数定义
// ...
private static Option add (final Option option) {
Arguments.ALL_OPTIONS.addOption (option);
return option;
}
}
再说第二个参数,咋一看将命令行输入的一行字符串,基于空格分隔为一个字符串数组不是很简单吗,实际上并没有想象的那么容易!
实际应用中,你会发现参数的值可能被双引号包裹,也可能被单引号包裹,值中也可以包含转译的双引号、空格、单引号,等因素。
你可以尝试用自己的方法分隔转换下面的这两个字符串用例试试:
// 字符串:--option1=value1 --option2="value with spaces" arg1 'arg2 with spaces' --option3='another value' "escaped \" quote"
String line1 = "--option1=value1 --option2=\"value with spaces\" arg1 'arg2 with spaces' --option3='another value' \"escaped \\\" quote\"";
// 下面是一个标准的curl命令
String line2 = "curl 'https://miao.baidu.com/abdr?_o=https%3A%2F%2Fbaijiahao.baidu.com' \\\n" +
" -H 'Accept: */*' \\\n" +
" -H 'Accept-Language: zh-CN,zh;q=0.9' \\\n" +
" -H 'Connection: keep-alive' \\\n" +
" -H 'Content-Type: text/plain;charset=UTF-8' \\\n" +
" -H 'Cookie: ab_jid=b4efe3bf4f00d7f80189fee50d5afe1b6600; ab_jid_BFESS=b4efe3bf4f00d7f80189fee50d5afe1b6600; " +
"BDUSS_BFESS=zR2dWMtUzJtbkR0WldHYlhxNlFicUktZkJZfmtUZW40MXVUR0FjQ09TT2FtbVJrSVFBQUFBJCQAAAAAAAAAAAEAAADtof106eTKxc6ow8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJoNPWSaDT1kR; BAIDUID=2FC2C4AC7C364639730D0E398254BD61:FG=1; PSTM=1716286127; BIDUPSID=B7E72815C5ECAA9C1F891984074B8428; H_PS_PSSID=60237_60270_60298; H_WISE_SIDS=60237_60270_60298; H_WISE_SIDS_BFESS=60237_60270_60298; MAWEBCUID=web_wOYCleriZcOSUwnYRPiaWULsULqtqhbdycMgzZJtHtnjebqXry; MCITY=-315%3A; BDORZ=FFFB88E999055A3F8A630C64834BD6D0; BAIDUID_BFESS=2FC2C4AC7C364639730D0E398254BD61:FG=1; PSINO=5; delPer=0; BA_HECTOR=81ag2g84aka0010g242l8k8l5ll51u1j5iep01u; ZFY=XD2K1H1:BOEvnsmQcDQCZ9ZAGAJgqcbJNs1102qmVVaE:C; BDRCVFR[V3EOV_cRy1C]=mk3SLVN4HKm; ab_bid=fe1b6600b4efe3bf4f00d7f80189fee50d5a; ab_sr=1.0.1_ZDg0NjJmNzNjZDViZTM1OWU2MWMyMTcyNjcwZTEwNjg1MTNlYTY3OTAzMWNiMjhjMTNmM2M4YzAzYjNkYzIzOGQ1OTRiNGE1NWJkYzk0YjBkNmRkODRhZWQ1NzE0NzhhYjhjZTUyZTIzYzU5YTRjZDQ1MzljZjJlMDZhMjk4ZTJmYWI4M2ZiZjczNDVjMzJkZmZkM2FiMmIwNWI1YWJiODI5M2NkMjhmMTMyYjdjYWQ5Yjk4OGJkMDJjMGYwODA2MzkxN2VlMDg2MWMxNGQwNWIxMTVjZjkzNDhhYjFkYjA=' \\\n" +
" -H 'Origin: https://baijiahao.baidu.com' \\\n" +
" -H 'Referer: https://baijiahao.baidu.com/s?id=1800522539495462150&wfr=spider&for=pc' \\\n" +
" -H 'Sec-Fetch-Dest: empty' \\\n" +
" -H 'Sec-Fetch-Mode: cors' \\\n" +
" -H 'Sec-Fetch-Site: same-site' \\\n" +
" -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36' \\\n" +
" -H 'sec-ch-ua: \"Google Chrome\";v=\"125\", \"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\"' \\\n" +
" -H 'sec-ch-ua-mobile: ?0' \\\n" +
" -H 'sec-ch-ua-platform: \"Windows\"' \\\n" +
" --data-raw '{\"data\":\"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\",\"key_id\":\"1b4ff72b8fcf4e14\",\"enc\":2}'";
正确的转换结果,line1 应该是6长度的数组,line2 应该是47长度的数组。
封装的解析类
如下解析类,是一个完整的独立的类,可以直接拷贝到项目中使用,能完成可靠的转换。
import lombok.Data;
import lombok.Getter;
import lombok.ToString;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 解析命令行输入参数
* <p>
* String line = "--option1=value1 --option2=\"value with spaces\" arg1 'arg2 with spaces' --option3='another value' \"escaped \\\" quote\"";
* String[] args = CommandLineParser.getInstance().parseToArgs(input);
* 正确的解析结果args.length应该是6个值
* </p>
*
* @author 单红宇
* @since 2024/11/23 8:30
*/
@Data
public class CommandLineParser {
/**
* 被视为引号的字符
*/
private char[] quoteChars = {'\'', '"'};
/**
* 被视为转义符的字符
*/
private char[] escapeChars = {'\\'};
/**
* 是否在未闭合的引号处结束解析
*/
private boolean eofOnUnclosedQuote;
/**
* 是否在转义的换行符处结束解析
*/
private boolean eofOnEscapedNewLine;
/**
* 开始括号字符数组,用于匹配嵌套结构
*/
private char[] openingBrackets = null;
/**
* 结束括号字符数组,用于匹配嵌套结构
*/
private char[] closingBrackets = null;
/**
* 单行注释分隔符数组,用于识别单行注释的开始
*/
private String[] lineCommentDelims = null;
/**
* 块注释分隔符对象,用于识别块注释的开始和结束
*/
private BlockCommentDelims blockCommentDelims = null;
/**
* 变量名的正则表达式,匹配以字母或下划线开头,后续可以是字母、数字、下划线、破折号,
* 并且可以包含点、方括号等嵌套结构
*/
private String regexVariable = "[a-zA-Z_]+[a-zA-Z0-9_-]*((\\.|\\['|\\[\"|\\[)[a-zA-Z0-9_-]*(|']|\"]|]))?";
/**
* 命令的正则表达式,匹配可选的前导冒号,后跟字母开头,后续可以是字母、数字、下划线、破折号
*/
private String regexCommand = "[:]?[a-zA-Z]+[a-zA-Z0-9_-]*";
/**
* 命令匹配的正则表达式组索引,用于提取命令部分
*/
private int commandGroup = 4;
/**
* build
*
* @return DefaultParser
*/
public static CommandLineParser getInstance() {
return new CommandLineParser();
}
/**
* CommandLineParser
*/
public CommandLineParser() {
// default
}
/**
* 解析命令行字符串,分隔为字符串数组
*
* @param line 要解析的命令行字符串。
* @return 解析后分隔为字符串数组
* @throws EOFError 如果解析过程中遇到语法错误,将抛出此异常。
*/
public String[] parseToArgs(String line) throws EOFError {
return parse(line).words().toArray(new String[0]);
}
/**
* 解析命令行字符串
*
* @param line 要解析的命令行字符串。
* @return 解析后的命令行对象 {@link ParsedLine},包含解析结果和相关信息。
* @throws EOFError 如果解析过程中遇到语法错误,将抛出此异常。
*/
public ParsedLine parse(String line) throws EOFError {
return parse(line, 0, ParseContext.UNSPECIFIED);
}
/**
* 解析命令行字符串
*
* @param line 要解析的命令行字符串。
* @param cursor 当前光标的位置,用于指示解析的起点。
* @return 解析后的命令行对象 {@link ParsedLine},包含解析结果和相关信息。
* @throws EOFError 如果解析过程中遇到语法错误,将抛出此异常。
*/
public ParsedLine parse(String line, int cursor) throws EOFError {
return parse(line, cursor, ParseContext.UNSPECIFIED);
}
/**
* 设置 lineCommentDelims
* 此方法与 {@link CommandLineParser#setLineCommentDelims(String[])} 方法的功能相同
*
* @param lineCommentDelims lineCommentDelims
* @return DefaultParser
*/
public CommandLineParser lineCommentDelims(final String[] lineCommentDelims) {
this.setLineCommentDelims(lineCommentDelims);
return this;
}
/**
* 设置 blockCommentDelims
* 此方法与 {@link this.setBlockCommentDelims(BlockCommentDelims)} 方法的功能相同
*
* @param blockCommentDelims blockCommentDelims
* @return DefaultParser
*/
public CommandLineParser blockCommentDelims(final BlockCommentDelims blockCommentDelims) {
this.setBlockCommentDelims(blockCommentDelims);
return this;
}
/**
* 设置 quoteChars
*
* @param chars chars
* @return DefaultParser
*/
public CommandLineParser quoteChars(final char[] chars) {
this.setQuoteChars(chars);
return this;
}
/**
* 设置 escapeChars
*
* @param chars chars
* @return DefaultParser
*/
public CommandLineParser escapeChars(final char[] chars) {
this.setEscapeChars(chars);
return this;
}
/**
* 设置 eofOnUnclosedQuote
*
* @param eofOnUnclosedQuote eofOnUnclosedQuote
* @return DefaultParser
*/
public CommandLineParser eofOnUnclosedQuote(boolean eofOnUnclosedQuote) {
this.setEofOnUnclosedQuote(eofOnUnclosedQuote);
return this;
}
/**
* 设置 eofOnUnclosedBracket
*
* @param brackets brackets
* @return DefaultParser
*/
public CommandLineParser eofOnUnclosedBracket(CommandLineParser.Bracket... brackets) {
setEofOnUnclosedBracket(brackets);
return this;
}
/**
* 设置 eofOnEscapedNewLine
*
* @param eofOnEscapedNewLine eofOnEscapedNewLine
* @return DefaultParser
*/
public CommandLineParser eofOnEscapedNewLine(boolean eofOnEscapedNewLine) {
this.setEofOnEscapedNewLine(eofOnEscapedNewLine);
return this;
}
/**
* regexVariable
*
* @param regexVariable regexVariable
* @return DefaultParser
*/
public CommandLineParser regexVariable(String regexVariable) {
this.setRegexVariable(regexVariable);
return this;
}
/**
* 设置 regexCommand
*
* @param regexCommand regexCommand
* @return DefaultParser
*/
public CommandLineParser regexCommand(String regexCommand) {
this.setRegexCommand(regexCommand);
return this;
}
/**
* 设置 commandGroup
*
* @param commandGroup commandGroup
* @return DefaultParser
*/
public CommandLineParser commandGroup(int commandGroup) {
this.setCommandGroup(commandGroup);
return this;
}
/**
* setEofOnUnclosedBracket
*
* @param brackets brackets
*/
public void setEofOnUnclosedBracket(CommandLineParser.Bracket... brackets) {
if (brackets == null) {
openingBrackets = null;
closingBrackets = null;
} else {
Set<CommandLineParser.Bracket> bs = new HashSet<>(Arrays.asList(brackets));
openingBrackets = new char[bs.size()];
closingBrackets = new char[bs.size()];
int i = 0;
for (CommandLineParser.Bracket b : bs) {
switch (b) {
case ROUND:
openingBrackets[i] = '(';
closingBrackets[i] = ')';
break;
case CURLY:
openingBrackets[i] = '{';
closingBrackets[i] = '}';
break;
case SQUARE:
openingBrackets[i] = '[';
closingBrackets[i] = ']';
break;
case ANGLE:
openingBrackets[i] = '<';
closingBrackets[i] = '>';
break;
}
i++;
}
}
}
/**
* validCommandName
*
* @param name name
* @return data
*/
public boolean validCommandName(String name) {
return name != null && name.matches(regexCommand);
}
/**
* validVariableName
*
* @param name name
* @return data
*/
public boolean validVariableName(String name) {
return name != null && regexVariable != null && name.matches(regexVariable);
}
/**
* getCommand
*
* @param line line
* @return String
*/
public String getCommand(final String line) {
String out = "";
boolean checkCommandOnly = regexVariable == null;
if (!checkCommandOnly) {
Pattern patternCommand = Pattern.compile("^\\s*" + regexVariable + "=(" + regexCommand + ")(\\s+|$)");
Matcher matcher = patternCommand.matcher(line);
if (matcher.find()) {
out = matcher.group(commandGroup);
} else {
checkCommandOnly = true;
}
}
if (checkCommandOnly) {
out = line.trim().split("\\s+")[0];
if (!out.matches(regexCommand)) {
out = "";
}
}
return out;
}
/**
* getVariable
*
* @param line line
* @return String
*/
public String getVariable(final String line) {
String out = null;
if (regexVariable != null) {
Pattern patternCommand = Pattern.compile("^\\s*(" + regexVariable + ")\\s*=[^=~].*");
Matcher matcher = patternCommand.matcher(line);
if (matcher.find()) {
out = matcher.group(1);
}
}
return out;
}
/**
* parse
*
* @param line line
* @param cursor cursor
* @param context context
* @return ParsedLine
*/
@SuppressWarnings("all")
public ParsedLine parse(final String line, final int cursor, ParseContext context) {
List<String> words = new LinkedList<>();
StringBuilder current = new StringBuilder();
int wordCursor = -1;
int wordIndex = -1;
int quoteStart = -1;
int rawWordCursor = -1;
int rawWordLength = -1;
int rawWordStart = 0;
CommandLineParser.BracketChecker bracketChecker = new CommandLineParser.BracketChecker(cursor);
boolean quotedWord = false;
boolean lineCommented = false;
boolean blockCommented = false;
boolean blockCommentInRightOrder = true;
final String blockCommentEnd = blockCommentDelims == null ? null : blockCommentDelims.getEnd();
final String blockCommentStart = blockCommentDelims == null ? null : blockCommentDelims.getStart();
for (int i = 0; (line != null) && (i < line.length()); i++) {
// once we reach the cursor, set the
// position of the selected index
if (i == cursor) {
wordIndex = words.size();
// the position in the current argument is just the
// length of the current argument
wordCursor = current.length();
rawWordCursor = i - rawWordStart;
}
if (quoteStart < 0 && isQuoteChar(line, i) && !lineCommented && !blockCommented) {
// Start a quote block
quoteStart = i;
if (current.length() == 0) {
quotedWord = true;
if (context == ParseContext.SPLIT_LINE) {
current.append(line.charAt(i));
}
} else {
current.append(line.charAt(i));
}
} else if (quoteStart >= 0 && line.charAt(quoteStart) == line.charAt(i) && notEscaped(line, i)) {
// End quote block
if (!quotedWord || context == ParseContext.SPLIT_LINE) {
current.append(line.charAt(i));
} else if (rawWordCursor >= 0 && rawWordLength < 0) {
rawWordLength = i - rawWordStart + 1;
}
quoteStart = -1;
quotedWord = false;
} else if (quoteStart < 0 && isDelimiter(line, i)) {
if (lineCommented) {
if (isCommentDelim(line, i, System.lineSeparator())) {
lineCommented = false;
}
} else if (blockCommented) {
if (isCommentDelim(line, i, blockCommentEnd)) {
blockCommented = false;
}
} else {
// Delimiter
rawWordLength = handleDelimiterAndGetRawWordLength(
current, words, rawWordStart, rawWordCursor, rawWordLength, i);
rawWordStart = i + 1;
}
} else {
if (quoteStart < 0 && !blockCommented && (lineCommented || isLineCommentStarted(line, i))) {
lineCommented = true;
} else if (quoteStart < 0
&& !lineCommented
&& (blockCommented || isCommentDelim(line, i, blockCommentStart))) {
if (blockCommented) {
if (blockCommentEnd != null && isCommentDelim(line, i, blockCommentEnd)) {
blockCommented = false;
i += blockCommentEnd.length() - 1;
}
} else {
blockCommented = true;
rawWordLength = handleDelimiterAndGetRawWordLength(
current, words, rawWordStart, rawWordCursor, rawWordLength, i);
i += blockCommentStart == null ? 0 : blockCommentStart.length() - 1;
rawWordStart = i + 1;
}
} else if (quoteStart < 0 && !lineCommented && isCommentDelim(line, i, blockCommentEnd)) {
current.append(line.charAt(i));
blockCommentInRightOrder = false;
} else if (!isEscapeChar(line, i)) {
current.append(line.charAt(i));
if (quoteStart < 0) {
bracketChecker.check(line, i);
}
} else if (context == ParseContext.SPLIT_LINE) {
current.append(line.charAt(i));
}
}
}
if (current.length() > 0 || cursor == Objects.requireNonNull(line).length()) {
words.add(current.toString());
if (rawWordCursor >= 0 && rawWordLength < 0) {
rawWordLength = line.length() - rawWordStart;
}
}
if (cursor == Objects.requireNonNull(line).length()) {
wordIndex = words.size() - 1;
wordCursor = words.get(words.size() - 1).length();
rawWordCursor = cursor - rawWordStart;
rawWordLength = rawWordCursor;
}
if (context != ParseContext.COMPLETE && context != ParseContext.SPLIT_LINE) {
if (eofOnEscapedNewLine && isEscapeChar(line, line.length() - 1)) {
throw new EOFError(-1, -1, "Escaped new line", "newline");
}
if (eofOnUnclosedQuote && quoteStart >= 0) {
throw new EOFError(
-1, -1, "Missing closing quote", line.charAt(quoteStart) == '\'' ? "quote" : "dquote");
}
if (blockCommented) {
throw new EOFError(-1, -1, "Missing closing block comment delimiter", "add: " + blockCommentEnd);
}
if (!blockCommentInRightOrder) {
throw new EOFError(-1, -1, "Missing opening block comment delimiter", "missing: " + blockCommentStart);
}
if (bracketChecker.isClosingBracketMissing() || bracketChecker.isOpeningBracketMissing()) {
String message = null;
String missing = null;
if (bracketChecker.isClosingBracketMissing()) {
message = "Missing closing brackets";
missing = "add: " + bracketChecker.getMissingClosingBrackets();
} else {
message = "Missing opening bracket";
missing = "missing: " + bracketChecker.getMissingOpeningBracket();
}
throw new EOFError(
-1,
-1,
message,
missing,
bracketChecker.getOpenBrackets(),
bracketChecker.getNextClosingBracket());
}
}
String openingQuote = quotedWord ? line.substring(quoteStart, quoteStart + 1) : null;
return new ArgumentList(this, line, words, wordIndex, wordCursor, cursor, openingQuote, rawWordCursor, rawWordLength);
}
/**
* Returns true if the specified character is a whitespace parameter. Check to ensure that the character is not
* escaped by any of {@link #getQuoteChars}, and is not escaped by any of the {@link #getEscapeChars}, and
* returns true from {@link #isDelimiterChar}.
*
* @param buffer The complete command buffer
* @param pos The index of the character in the buffer
* @return True if the character should be a delimiter
*/
public boolean isDelimiter(final CharSequence buffer, final int pos) {
return !isQuoted(buffer, pos) && notEscaped(buffer, pos) && isDelimiterChar(buffer, pos);
}
/**
* handleDelimiterAndGetRawWordLength
*
* @param current current
* @param words words
* @param rawWordStart rawWordStart
* @param rawWordCursor rawWordCursor
* @param rawWordLength rawWordLength
* @param pos pos
* @return data
*/
private int handleDelimiterAndGetRawWordLength(
StringBuilder current,
List<String> words,
int rawWordStart,
int rawWordCursor,
int rawWordLength,
int pos) {
if (current.length() > 0) {
words.add(current.toString());
current.setLength(0); // reset the arg
if (rawWordCursor >= 0 && rawWordLength < 0) {
return pos - rawWordStart;
}
}
return rawWordLength;
}
/**
* isQuoted
*
* @param buffer buffer
* @param pos pos
* @return data
*/
public boolean isQuoted(final CharSequence buffer, final int pos) {
return false;
}
/**
* isQuoteChar
*
* @param buffer buffer
* @param pos pos
* @return data
*/
public boolean isQuoteChar(final CharSequence buffer, final int pos) {
if (pos < 0) {
return false;
}
if (quoteChars != null) {
for (char e : quoteChars) {
if (e == buffer.charAt(pos)) {
return notEscaped(buffer, pos);
}
}
}
return false;
}
/**
* isCommentDelim
*
* @param buffer buffer
* @param pos pos
* @param pattern pattern
* @return data
*/
private boolean isCommentDelim(final CharSequence buffer, final int pos, final String pattern) {
if (pos < 0) {
return false;
}
if (pattern != null) {
final int length = pattern.length();
if (length <= buffer.length() - pos) {
for (int i = 0; i < length; i++) {
if (pattern.charAt(i) != buffer.charAt(pos + i)) {
return false;
}
}
return true;
}
}
return false;
}
/**
* isLineCommentStarted
*
* @param buffer buffer
* @param pos pos
* @return data
*/
public boolean isLineCommentStarted(final CharSequence buffer, final int pos) {
if (lineCommentDelims != null) {
for (String comment : lineCommentDelims) {
if (isCommentDelim(buffer, pos, comment)) {
return true;
}
}
}
return false;
}
/**
* isEscapeChar
*
* @param ch ch
* @return data
*/
public boolean isEscapeChar(char ch) {
if (escapeChars != null) {
for (char e : escapeChars) {
if (e == ch) {
return true;
}
}
}
return false;
}
/**
* Check if this character is a valid escape char (i.e. one that has not been escaped)
*
* @param buffer the buffer to check in
* @param pos the position of the character to check
* @return true if the character at the specified position in the given buffer is
* an escape character and the character immediately preceding it is not an escape character.
*/
public boolean isEscapeChar(final CharSequence buffer, final int pos) {
if (pos < 0) {
return false;
}
char ch = buffer.charAt(pos);
return isEscapeChar(ch) && notEscaped(buffer, pos);
}
/**
* Check if a character is escaped (i.e. if the previous character is an escape)
*
* @param buffer the buffer to check in
* @param pos the position of the character to check
* @return true if the character at the specified position in the given buffer is
* an escape character and the character immediately preceding it is an escape character.
*/
public boolean isEscaped(final CharSequence buffer, final int pos) {
if (pos <= 0) {
return false;
}
return isEscapeChar(buffer, pos - 1);
}
/**
* Check if a character is not escaped
*
* @param buffer buffer
* @param pos pos
* @return data
*/
public boolean notEscaped(final CharSequence buffer, final int pos) {
return !isEscaped(buffer, pos);
}
/**
* Returns true if the character at the specified position if a delimiter. This method will only be called if
* the character is not enclosed in any of the {@link #getQuoteChars}, and is not escaped by any of the
* {@link #getEscapeChars}. To perform escaping manually, override {@link #isDelimiter} instead.
*
* @param buffer the buffer to check in
* @param pos the position of the character to check
* @return true if the character at the specified position in the given buffer is a delimiter.
*/
public boolean isDelimiterChar(CharSequence buffer, int pos) {
return Character.isWhitespace(buffer.charAt(pos));
}
/**
* BracketChecker
*
* @author 单红宇
* @since 2024-11-23 09:03:00
*/
@Getter
private class BracketChecker {
/**
* missingOpeningBracket
*/
private int missingOpeningBracket = -1;
/**
* nested
*/
private final List<Integer> nested = new ArrayList<>();
/**
* openBrackets
*/
private int openBrackets = 0;
/**
* cursor
*/
private final int cursor;
/**
* nextClosingBracket
*/
private String nextClosingBracket;
/**
* BracketChecker
*
* @param cursor cursor
*/
public BracketChecker(int cursor) {
this.cursor = cursor;
}
/**
* check
*
* @param buffer buffer
* @param pos pos
*/
public void check(final CharSequence buffer, final int pos) {
if (openingBrackets == null || pos < 0) {
return;
}
int bid = bracketId(openingBrackets, buffer, pos);
if (bid >= 0) {
nested.add(bid);
} else {
bid = bracketId(closingBrackets, buffer, pos);
if (bid >= 0) {
if (!nested.isEmpty() && bid == nested.get(nested.size() - 1)) {
nested.remove(nested.size() - 1);
} else {
missingOpeningBracket = bid;
}
}
}
if (cursor > pos) {
openBrackets = nested.size();
if (!nested.isEmpty()) {
nextClosingBracket = String.valueOf(closingBrackets[nested.get(nested.size() - 1)]);
}
}
}
/**
* isOpeningBracketMissing
*
* @return data
*/
public boolean isOpeningBracketMissing() {
return missingOpeningBracket != -1;
}
/**
* getMissingOpeningBracket
*
* @return String
*/
public String getMissingOpeningBracket() {
if (!isOpeningBracketMissing()) {
return null;
}
return Character.toString(openingBrackets[missingOpeningBracket]);
}
/**
* isClosingBracketMissing
*
* @return data
*/
public boolean isClosingBracketMissing() {
return !nested.isEmpty();
}
/**
* getMissingClosingBrackets
*
* @return String
*/
public String getMissingClosingBrackets() {
if (!isClosingBracketMissing()) {
return null;
}
StringBuilder out = new StringBuilder();
for (int i = nested.size() - 1; i > -1; i--) {
out.append(closingBrackets[nested.get(i)]);
}
return out.toString();
}
/**
* getNextClosingBracket
*
* @return String
*/
public String getNextClosingBracket() {
return nested.size() == 2 ? nextClosingBracket : null;
}
/**
* bracketId
*
* @param brackets brackets
* @param buffer buffer
* @param pos pos
* @return data
*/
private int bracketId(final char[] brackets, final CharSequence buffer, final int pos) {
for (int i = 0; i < brackets.length; i++) {
if (buffer.charAt(pos) == brackets[i]) {
return i;
}
}
return -1;
}
}
/**
* Bracket
*
* @author 单红宇
* @since 2024-11-23 09:03:00
*/
public enum Bracket {
/**
* ROUND
*/
ROUND, // ()
/**
* CURLY
*/
CURLY, // {}
/**
* SQUARE
*/
SQUARE, // []
/**
* ANGLE
*/
ANGLE // <>
}
/**
* ParseContext
*
* @author 单红宇
* @since 2024-11-23 09:03:00
*/
public enum ParseContext {
/**
* UNSPECIFIED
*/
UNSPECIFIED,
/**
* Try a real "final" parse.
* May throw EOFError in which case we have incomplete input.
*/
ACCEPT_LINE,
/**
* Parsed words will have all characters present in input line
* including quotes and escape chars.
* We should tolerate and ignore errors.
*/
SPLIT_LINE,
/**
* Parse to find completions (typically after a Tab).
* We should tolerate and ignore errors.
*/
COMPLETE,
/**
* Called when we need to update the secondary prompts.
* Specifically, when we need the 'missing' field from EOFError,
* which is used by a "%M" in a prompt pattern.
*/
SECONDARY_PROMPT
}
/**
* BlockCommentDelims
*
* @author 单红宇
* @since 2024-11-23 09:03:00
*/
@Getter
public static class BlockCommentDelims {
/**
* start
*/
private final String start;
/**
* end
*/
private final String end;
/**
* BlockCommentDelims
*
* @param start start
* @param end end
*/
public BlockCommentDelims(String start, String end) {
if (start == null || end == null || start.isEmpty() || end.isEmpty() || start.equals(end)) {
throw new IllegalArgumentException("Bad block comment delimiter!");
}
this.start = start;
this.end = end;
}
}
/**
* 参数集合
*
* @author 单红宇
* @since 2024/11/23 9:37
*/
public static class ArgumentList implements ParsedLine, CompletingParsedLine {
/**
* line
*/
private final String line;
/**
* words
*/
private final List<String> words;
/**
* wordIndex
*/
private final int wordIndex;
/**
* wordCursor
*/
private final int wordCursor;
/**
* cursor
*/
private final int cursor;
/**
* openingQuote
*/
private final String openingQuote;
/**
* rawWordCursor
*/
private final int rawWordCursor;
/**
* rawWordLength
*/
private final int rawWordLength;
/**
* parser
*/
private final CommandLineParser parser;
/**
* ArgumentList
*
* @param parser parser
* @param line the command line being edited
* @param words the list of words
* @param wordIndex the index of the current word in the list of words
* @param wordCursor the cursor position within the current word
* @param cursor the cursor position within the line
* @param openingQuote the opening quote (usually '\"' or '\'') or null
* @param rawWordCursor the cursor position inside the raw word (i.e. including quotes and escape characters)
* @param rawWordLength the raw word length, including quotes and escape characters
*/
@SuppressWarnings("java:S107") // 忽略经过:参数过多
public ArgumentList(final CommandLineParser parser,
final String line,
final List<String> words,
final int wordIndex,
final int wordCursor,
final int cursor,
final String openingQuote,
final int rawWordCursor,
final int rawWordLength) {
this.parser = parser;
this.line = line;
this.words = Collections.unmodifiableList(Objects.requireNonNull(words));
this.wordIndex = wordIndex;
this.wordCursor = wordCursor;
this.cursor = cursor;
this.openingQuote = openingQuote;
this.rawWordCursor = rawWordCursor;
this.rawWordLength = rawWordLength;
}
/**
* isRawEscapeChar
*
* @param key key
* @return data
*/
private boolean isRawEscapeChar(char key) {
if (parser.getEscapeChars() != null) {
for (char e : parser.getEscapeChars()) {
if (e == key) {
return true;
}
}
}
return false;
}
/**
* isRawQuoteChar
*
* @param key key
* @return data
*/
private boolean isRawQuoteChar(char key) {
if (parser.getQuoteChars() != null) {
for (char e : parser.getQuoteChars()) {
if (e == key) {
return true;
}
}
}
return false;
}
/**
* wordIndex
*
* @return int
*/
public int wordIndex() {
return this.wordIndex;
}
/**
* word() should always be contained in words()
*
* @return String
*/
public String word() {
if ((wordIndex < 0) || (wordIndex >= words.size())) {
return "";
}
return words.get(wordIndex);
}
/**
* wordCursor
*
* @return int
*/
public int wordCursor() {
return this.wordCursor;
}
/**
* words
*
* @return List
*/
public List<String> words() {
return this.words;
}
/**
* cursor
*
* @return int
*/
public int cursor() {
return this.cursor;
}
/**
* line
*
* @return String
*/
public String line() {
return line;
}
/**
* escape
*
* @param candidate candidate
* @param complete complete
* @return CharSequence
*/
@SuppressWarnings("all")
public CharSequence escape(CharSequence candidate, boolean complete) {
StringBuilder sb = new StringBuilder(candidate);
Predicate<Integer> needToBeEscaped;
String quote = openingQuote;
boolean middleQuotes = false;
if (openingQuote == null) {
for (int i = 0; i < sb.length(); i++) {
if (parser.isQuoteChar(sb, i)) {
middleQuotes = true;
break;
}
}
}
if (parser.getEscapeChars() != null) {
if (parser.getEscapeChars().length > 0) {
// Completion is protected by an opening quote:
// Delimiters (spaces) don't need to be escaped, nor do other quotes, but everything else does.
// Also, close the quote at the end
if (openingQuote != null) {
needToBeEscaped = i -> isRawEscapeChar(sb.charAt(i))
|| String.valueOf(sb.charAt(i)).equals(openingQuote);
}
// Completion is protected by middle quotes:
// Delimiters (spaces) don't need to be escaped, nor do quotes, but everything else does.
else if (middleQuotes) {
needToBeEscaped = i -> isRawEscapeChar(sb.charAt(i));
}
// No quote protection, need to escape everything: delimiter chars (spaces), quote chars
// and escapes themselves
else {
needToBeEscaped = i ->
parser.isDelimiterChar(sb, i) || isRawEscapeChar(sb.charAt(i)) || isRawQuoteChar(sb.charAt(i));
}
for (int i = 0; i < sb.length(); i++) {
if (needToBeEscaped.test(i)) {
sb.insert(i++, parser.getEscapeChars()[0]);
}
}
}
} else if (openingQuote == null && !middleQuotes) {
for (int i = 0; i < sb.length(); i++) {
if (parser.isDelimiterChar(sb, i)) {
quote = "'";
break;
}
}
}
if (quote != null) {
sb.insert(0, quote);
if (complete) {
sb.append(quote);
}
}
return sb;
}
@Override
public int rawWordCursor() {
return rawWordCursor;
}
@Override
public int rawWordLength() {
return rawWordLength;
}
}
/**
* <code>ParsedLine</code> objects are returned by the
* during completion or when accepting the line.
* <p>
* The instances should implement the {@link CompletingParsedLine}
* interface so that escape chars and quotes can be correctly handled.
*
* @author 单红宇
* @see CompletingParsedLine
* @since 2024/11/23 9:35
*/
public interface ParsedLine {
/**
* The current word being completed.
* If the cursor is after the last word, an empty string is returned.
*
* @return the word being completed or an empty string
*/
String word();
/**
* The cursor position within the current word.
*
* @return the cursor position within the current word
*/
int wordCursor();
/**
* The index of the current word in the list of words.
*
* @return the index of the current word in the list of words
*/
int wordIndex();
/**
* The list of words.
*
* @return the list of words
*/
List<String> words();
/**
* The unparsed line.
*
* @return the unparsed line
*/
String line();
/**
* The cursor position within the line.
*
* @return the cursor position within the line
*/
int cursor();
}
/**
* An extension of {@link org.jline.reader.ParsedLine} that, being aware of the quoting and escaping rules
* of the {@link org.jline.reader.Parser} that produced it, knows if and how a completion candidate
* should be escaped/quoted.
*
* @author 单红宇
* @since 2024/11/23 9:36
*/
public interface CompletingParsedLine {
/**
* escape
*
* @param candidate candidate
* @param complete complete
* @return CharSequence
*/
CharSequence escape(CharSequence candidate, boolean complete);
/**
* rawWordCursor
*
* @return data
*/
int rawWordCursor();
/**
* rawWordLength
*
* @return data
*/
int rawWordLength();
}
/**
* EOFError
*
* @author 单红宇
* @since 2024-11-23 10:10:04
*/
@ToString
public static class EOFError extends RuntimeException {
/**
* line
*/
private final int line;
private final int column;
/**
* missing
*/
private final String missing;
/**
* openBrackets
*/
private final int openBrackets;
/**
* nextClosingBracket
*/
private final String nextClosingBracket;
/**
* EOFError
*
* @param line line
* @param column column
* @param message message
* @param missing missing
*/
public EOFError(int line, int column, String message, String missing) {
this(line, column, message, missing, -1, null);
}
/**
* EOFError
*
* @param line line
* @param column column
* @param message message
* @param missing missing
* @param openBrackets openBrackets
* @param nextClosingBracket nextClosingBracket
*/
public EOFError(int line, int column, String message, String missing, int openBrackets, String nextClosingBracket) {
super(message);
this.line = line;
this.column = column;
this.missing = missing;
this.openBrackets = openBrackets;
this.nextClosingBracket = nextClosingBracket;
}
}
}
类参考自第三方jline组件库,因为项目只需要这一个功能,所以不希望依赖整个jline,故自己整理了一个完整的独立的类。
(END)