blob: a7334ccea4a75b3fd8e55a1d2eb8594116b5acba [file] [log] [blame]
/*
* Copyright (c) 2002-2020, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* https://opensource.org/licenses/BSD-3-Clause
*/
package jdk.internal.org.jline.reader.impl;
import java.util.*;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jdk.internal.org.jline.reader.CompletingParsedLine;
import jdk.internal.org.jline.reader.EOFError;
import jdk.internal.org.jline.reader.ParsedLine;
import jdk.internal.org.jline.reader.Parser;
public class DefaultParser implements Parser {
public enum Bracket {
ROUND, // ()
CURLY, // {}
SQUARE, // []
ANGLE; // <>
}
private char[] quoteChars = {'\'', '"'};
private char[] escapeChars = {'\\'};
private boolean eofOnUnclosedQuote;
private boolean eofOnEscapedNewLine;
private char[] openingBrackets = null;
private char[] closingBrackets = null;
private String regexVariable = "[a-zA-Z_]{1,}[a-zA-Z0-9_-]*((.|\\['|\\[\\\"|\\[)[a-zA-Z0-9_-]*(|'\\]|\\\"\\]|\\])){0,1}";
private String regexCommand = "[:]{0,1}[a-zA-Z]{1,}[a-zA-Z0-9_-]*";
private int commandGroup = 4;
//
// Chainable setters
//
public DefaultParser quoteChars(final char[] chars) {
this.quoteChars = chars;
return this;
}
public DefaultParser escapeChars(final char[] chars) {
this.escapeChars = chars;
return this;
}
public DefaultParser eofOnUnclosedQuote(boolean eofOnUnclosedQuote) {
this.eofOnUnclosedQuote = eofOnUnclosedQuote;
return this;
}
public DefaultParser eofOnUnclosedBracket(Bracket... brackets) {
setEofOnUnclosedBracket(brackets);
return this;
}
public DefaultParser eofOnEscapedNewLine(boolean eofOnEscapedNewLine) {
this.eofOnEscapedNewLine = eofOnEscapedNewLine;
return this;
}
public DefaultParser regexVariable(String regexVariable) {
this.regexVariable = regexVariable;
return this;
}
public DefaultParser regexCommand(String regexCommand) {
this.regexCommand = regexCommand;
return this;
}
public DefaultParser commandGroup(int commandGroup) {
this.commandGroup = commandGroup;
return this;
}
//
// Java bean getters and setters
//
public void setQuoteChars(final char[] chars) {
this.quoteChars = chars;
}
public char[] getQuoteChars() {
return this.quoteChars;
}
public void setEscapeChars(final char[] chars) {
this.escapeChars = chars;
}
public char[] getEscapeChars() {
return this.escapeChars;
}
public void setEofOnUnclosedQuote(boolean eofOnUnclosedQuote) {
this.eofOnUnclosedQuote = eofOnUnclosedQuote;
}
public boolean isEofOnUnclosedQuote() {
return eofOnUnclosedQuote;
}
public void setEofOnEscapedNewLine(boolean eofOnEscapedNewLine) {
this.eofOnEscapedNewLine = eofOnEscapedNewLine;
}
public boolean isEofOnEscapedNewLine() {
return eofOnEscapedNewLine;
}
public void setEofOnUnclosedBracket(Bracket... brackets) {
if (brackets == null) {
openingBrackets = null;
closingBrackets = null;
} else {
Set<Bracket> bs = new HashSet<>(Arrays.asList(brackets));
openingBrackets = new char[bs.size()];
closingBrackets = new char[bs.size()];
int i = 0;
for (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++;
}
}
}
public void setRegexVariable(String regexVariable) {
this.regexVariable = regexVariable;
}
public void setRegexCommand(String regexCommand) {
this.regexCommand = regexCommand;
}
public void setCommandGroup(int commandGroup) {
this.commandGroup = commandGroup;
}
@Override
public boolean validCommandName(String name) {
return name != null && name.matches(regexCommand);
}
@Override
public boolean validVariableName(String name) {
return name != null && name.matches(regexVariable);
}
@Override
public String getCommand(final String line) {
String out = "";
Pattern patternCommand = Pattern.compile("^\\s*" + regexVariable + "=(" + regexCommand + ")(\\s+.*|$)");
Matcher matcher = patternCommand.matcher(line);
if (matcher.find()) {
out = matcher.group(commandGroup);
} else {
out = line.trim().split("\\s+")[0];
int idx = out.indexOf("=");
if (idx > -1) {
out = out.substring(idx + 1);
}
if (!out.matches(regexCommand)) {
out = "";
}
}
return out;
}
@Override
public String getVariable(final String line) {
String out = null;
Pattern patternCommand = Pattern.compile("^\\s*(" + regexVariable + ")\\s*=[^=~].*");
Matcher matcher = patternCommand.matcher(line);
if (matcher.find()) {
out = matcher.group(1);
}
return out;
}
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;
BracketChecker bracketChecker = new BracketChecker(cursor);
boolean quotedWord = false;
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)) {
// 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) && !isEscaped(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)) {
// Delimiter
if (current.length() > 0) {
words.add(current.toString());
current.setLength(0); // reset the arg
if (rawWordCursor >= 0 && rawWordLength < 0) {
rawWordLength = i - rawWordStart;
}
}
rawWordStart = i + 1;
} 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 == line.length()) {
words.add(current.toString());
if (rawWordCursor >= 0 && rawWordLength < 0) {
rawWordLength = line.length() - rawWordStart;
}
}
if (cursor == line.length()) {
wordIndex = words.size() - 1;
wordCursor = words.get(words.size() - 1).length();
rawWordCursor = cursor - rawWordStart;
rawWordLength = rawWordCursor;
}
if (context != ParseContext.COMPLETE) {
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 (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(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) && !isEscaped(buffer, pos) && isDelimiterChar(buffer, pos);
}
public boolean isQuoted(final CharSequence buffer, final int pos) {
return false;
}
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 !isEscaped(buffer, pos);
}
}
}
return false;
}
@Override
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) && !isEscaped(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);
}
/**
* 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));
}
private boolean isRawEscapeChar(char key) {
if (escapeChars != null) {
for (char e : escapeChars) {
if (e == key) {
return true;
}
}
}
return false;
}
private boolean isRawQuoteChar(char key) {
if (quoteChars != null) {
for (char e : quoteChars) {
if (e == key) {
return true;
}
}
}
return false;
}
private class BracketChecker {
private int missingOpeningBracket = -1;
private List<Integer> nested = new ArrayList<>();
private int openBrackets = 0;
private int cursor;
private String nextClosingBracket;
public BracketChecker(int cursor) {
this.cursor = cursor;
}
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.size() > 0) {
nextClosingBracket = String.valueOf(closingBrackets[nested.get(nested.size() - 1)]);
}
}
}
public boolean isOpeningBracketMissing() {
return missingOpeningBracket != -1;
}
public String getMissingOpeningBracket() {
if (!isOpeningBracketMissing()) {
return null;
}
return Character.toString(openingBrackets[missingOpeningBracket]);
}
public boolean isClosingBracketMissing() {
return !nested.isEmpty();
}
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();
}
public int getOpenBrackets() {
return openBrackets;
}
public String getNextClosingBracket() {
return nested.size() == 2 ? nextClosingBracket : null;
}
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;
}
}
/**
* The result of a delimited buffer.
*
* @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
*/
public class ArgumentList implements ParsedLine, CompletingParsedLine
{
private final String line;
private final List<String> words;
private final int wordIndex;
private final int wordCursor;
private final int cursor;
private final String openingQuote;
private final int rawWordCursor;
private final int rawWordLength;
@Deprecated
public ArgumentList(final String line, final List<String> words,
final int wordIndex, final int wordCursor,
final int cursor) {
this(line, words, wordIndex, wordCursor, cursor,
null, wordCursor, words.get(wordIndex).length());
}
/**
*
* @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
*/
public ArgumentList(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.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;
}
public int wordIndex() {
return this.wordIndex;
}
public String word() {
// TODO: word() should always be contained in words()
if ((wordIndex < 0) || (wordIndex >= words.size())) {
return "";
}
return words.get(wordIndex);
}
public int wordCursor() {
return this.wordCursor;
}
public List<String> words() {
return this.words;
}
public int cursor() {
return this.cursor;
}
public String line() {
return line;
}
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 (isQuoteChar(sb, i)) {
middleQuotes = true;
break;
}
}
}
if (escapeChars != null) {
// 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 -> 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++, escapeChars[0]);
}
}
} else if (openingQuote == null && !middleQuotes) {
for (int i = 0; i < sb.length(); i++) {
if (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;
}
}
}