1 /* 2 * Copyright (c) 2002-2012, the original author or authors. 3 * 4 * This software is distributable under the BSD license. See the terms of the 5 * BSD license in the documentation provided with this software. 6 * 7 * http://www.opensource.org/licenses/bsd-license.php 8 */ 9 package jline.console.completer; 10 11 import jline.internal.Log; 12 13 import java.util.ArrayList; 14 import java.util.Arrays; 15 import java.util.Collection; 16 import java.util.LinkedList; 17 import java.util.List; 18 19 import static jline.internal.Preconditions.checkNotNull; 20 21 /** 22 * A {@link Completer} implementation that invokes a child completer using the appropriate <i>separator</i> argument. 23 * This can be used instead of the individual completers having to know about argument parsing semantics. 24 * 25 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a> 26 * @author <a href="mailto:jason@planet57.com">Jason Dillon</a> 27 * @since 2.3 28 */ 29 public class ArgumentCompleter 30 implements Completer 31 { 32 private final ArgumentDelimiter delimiter; 33 34 private final List<Completer> completers = new ArrayList<Completer>(); 35 36 private boolean strict = true; 37 38 /** 39 * Create a new completer with the specified argument delimiter. 40 * 41 * @param delimiter The delimiter for parsing arguments 42 * @param completers The embedded completers 43 */ 44 public ArgumentCompleter(final ArgumentDelimiter delimiter, final Collection<Completer> completers) { 45 this.delimiter = checkNotNull(delimiter); 46 checkNotNull(completers); 47 this.completers.addAll(completers); 48 } 49 50 /** 51 * Create a new completer with the specified argument delimiter. 52 * 53 * @param delimiter The delimiter for parsing arguments 54 * @param completers The embedded completers 55 */ 56 public ArgumentCompleter(final ArgumentDelimiter delimiter, final Completer... completers) { 57 this(delimiter, Arrays.asList(completers)); 58 } 59 60 /** 61 * Create a new completer with the default {@link WhitespaceArgumentDelimiter}. 62 * 63 * @param completers The embedded completers 64 */ 65 public ArgumentCompleter(final Completer... completers) { 66 this(new WhitespaceArgumentDelimiter(), completers); 67 } 68 69 /** 70 * Create a new completer with the default {@link WhitespaceArgumentDelimiter}. 71 * 72 * @param completers The embedded completers 73 */ 74 public ArgumentCompleter(final List<Completer> completers) { 75 this(new WhitespaceArgumentDelimiter(), completers); 76 } 77 78 /** 79 * If true, a completion at argument index N will only succeed 80 * if all the completions from 0-(N-1) also succeed. 81 */ 82 public void setStrict(final boolean strict) { 83 this.strict = strict; 84 } 85 86 /** 87 * Returns whether a completion at argument index N will success 88 * if all the completions from arguments 0-(N-1) also succeed. 89 * 90 * @return True if strict. 91 * @since 2.3 92 */ 93 public boolean isStrict() { 94 return this.strict; 95 } 96 97 /** 98 * @since 2.3 99 */ 100 public ArgumentDelimiter getDelimiter() { 101 return delimiter; 102 } 103 104 /** 105 * @since 2.3 106 */ 107 public List<Completer> getCompleters() { 108 return completers; 109 } 110 111 public int complete(final String buffer, final int cursor, final List<CharSequence> candidates) { 112 // buffer can be null 113 checkNotNull(candidates); 114 115 ArgumentDelimiter delim = getDelimiter(); 116 ArgumentList list = delim.delimit(buffer, cursor); 117 int argpos = list.getArgumentPosition(); 118 int argIndex = list.getCursorArgumentIndex(); 119 120 if (argIndex < 0) { 121 return -1; 122 } 123 124 List<Completer> completers = getCompleters(); 125 Completer completer; 126 127 // if we are beyond the end of the completers, just use the last one 128 if (argIndex >= completers.size()) { 129 completer = completers.get(completers.size() - 1); 130 } 131 else { 132 completer = completers.get(argIndex); 133 } 134 135 // ensure that all the previous completers are successful before allowing this completer to pass (only if strict). 136 for (int i = 0; isStrict() && (i < argIndex); i++) { 137 Completer sub = completers.get(i >= completers.size() ? (completers.size() - 1) : i); 138 String[] args = list.getArguments(); 139 String arg = (args == null || i >= args.length) ? "" : args[i]; 140 141 List<CharSequence> subCandidates = new LinkedList<CharSequence>(); 142 143 if (sub.complete(arg, arg.length(), subCandidates) == -1) { 144 return -1; 145 } 146 147 if (subCandidates.size() == 0) { 148 return -1; 149 } 150 } 151 152 int ret = completer.complete(list.getCursorArgument(), argpos, candidates); 153 154 if (ret == -1) { 155 return -1; 156 } 157 158 int pos = ret + list.getBufferPosition() - argpos; 159 160 // Special case: when completing in the middle of a line, and the area under the cursor is a delimiter, 161 // then trim any delimiters from the candidates, since we do not need to have an extra delimiter. 162 // 163 // E.g., if we have a completion for "foo", and we enter "f bar" into the buffer, and move to after the "f" 164 // and hit TAB, we want "foo bar" instead of "foo bar". 165 166 if ((cursor != buffer.length()) && delim.isDelimiter(buffer, cursor)) { 167 for (int i = 0; i < candidates.size(); i++) { 168 CharSequence val = candidates.get(i); 169 170 while (val.length() > 0 && delim.isDelimiter(val, val.length() - 1)) { 171 val = val.subSequence(0, val.length() - 1); 172 } 173 174 candidates.set(i, val); 175 } 176 } 177 178 Log.trace("Completing ", buffer, " (pos=", cursor, ") with: ", candidates, ": offset=", pos); 179 180 return pos; 181 } 182 183 /** 184 * The {@link ArgumentCompleter.ArgumentDelimiter} allows custom breaking up of a {@link String} into individual 185 * arguments in order to dispatch the arguments to the nested {@link Completer}. 186 * 187 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a> 188 */ 189 public static interface ArgumentDelimiter 190 { 191 /** 192 * Break the specified buffer into individual tokens that can be completed on their own. 193 * 194 * @param buffer The buffer to split 195 * @param pos The current position of the cursor in the buffer 196 * @return The tokens 197 */ 198 ArgumentList delimit(CharSequence buffer, int pos); 199 200 /** 201 * Returns true if the specified character is a whitespace parameter. 202 * 203 * @param buffer The complete command buffer 204 * @param pos The index of the character in the buffer 205 * @return True if the character should be a delimiter 206 */ 207 boolean isDelimiter(CharSequence buffer, int pos); 208 } 209 210 /** 211 * Abstract implementation of a delimiter that uses the {@link #isDelimiter} method to determine if a particular 212 * character should be used as a delimiter. 213 * 214 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a> 215 */ 216 public abstract static class AbstractArgumentDelimiter 217 implements ArgumentDelimiter 218 { 219 private char[] quoteChars = {'\'', '"'}; 220 221 private char[] escapeChars = {'\\'}; 222 223 public void setQuoteChars(final char[] chars) { 224 this.quoteChars = chars; 225 } 226 227 public char[] getQuoteChars() { 228 return this.quoteChars; 229 } 230 231 public void setEscapeChars(final char[] chars) { 232 this.escapeChars = chars; 233 } 234 235 public char[] getEscapeChars() { 236 return this.escapeChars; 237 } 238 239 public ArgumentList delimit(final CharSequence buffer, final int cursor) { 240 List<String> args = new LinkedList<String>(); 241 StringBuilder arg = new StringBuilder(); 242 int argpos = -1; 243 int bindex = -1; 244 int quoteStart = -1; 245 246 for (int i = 0; (buffer != null) && (i < buffer.length()); i++) { 247 // once we reach the cursor, set the 248 // position of the selected index 249 if (i == cursor) { 250 bindex = args.size(); 251 // the position in the current argument is just the 252 // length of the current argument 253 argpos = arg.length(); 254 } 255 256 if (quoteStart < 0 && isQuoteChar(buffer, i)) { 257 // Start a quote block 258 quoteStart = i; 259 } else if (quoteStart >= 0) { 260 // In a quote block 261 if (buffer.charAt(quoteStart) == buffer.charAt(i) && !isEscaped(buffer, i)) { 262 // End the block; arg could be empty, but that's fine 263 args.add(arg.toString()); 264 arg.setLength(0); 265 quoteStart = -1; 266 } else if (!isEscapeChar(buffer, i)) { 267 // Take the next character 268 arg.append(buffer.charAt(i)); 269 } 270 } else { 271 // Not in a quote block 272 if (isDelimiter(buffer, i)) { 273 if (arg.length() > 0) { 274 args.add(arg.toString()); 275 arg.setLength(0); // reset the arg 276 } 277 } else if (!isEscapeChar(buffer, i)) { 278 arg.append(buffer.charAt(i)); 279 } 280 } 281 } 282 283 if (cursor == buffer.length()) { 284 bindex = args.size(); 285 // the position in the current argument is just the 286 // length of the current argument 287 argpos = arg.length(); 288 } 289 if (arg.length() > 0) { 290 args.add(arg.toString()); 291 } 292 293 return new ArgumentList(args.toArray(new String[args.size()]), bindex, argpos, cursor); 294 } 295 296 /** 297 * Returns true if the specified character is a whitespace parameter. Check to ensure that the character is not 298 * escaped by any of {@link #getQuoteChars}, and is not escaped by ant of the {@link #getEscapeChars}, and 299 * returns true from {@link #isDelimiterChar}. 300 * 301 * @param buffer The complete command buffer 302 * @param pos The index of the character in the buffer 303 * @return True if the character should be a delimiter 304 */ 305 public boolean isDelimiter(final CharSequence buffer, final int pos) { 306 return !isQuoted(buffer, pos) && !isEscaped(buffer, pos) && isDelimiterChar(buffer, pos); 307 } 308 309 public boolean isQuoted(final CharSequence buffer, final int pos) { 310 return false; 311 } 312 313 public boolean isQuoteChar(final CharSequence buffer, final int pos) { 314 if (pos < 0) { 315 return false; 316 } 317 318 for (int i = 0; (quoteChars != null) && (i < quoteChars.length); i++) { 319 if (buffer.charAt(pos) == quoteChars[i]) { 320 return !isEscaped(buffer, pos); 321 } 322 } 323 324 return false; 325 } 326 327 /** 328 * Check if this character is a valid escape char (i.e. one that has not been escaped) 329 * 330 * @param buffer 331 * @param pos 332 * @return 333 */ 334 public boolean isEscapeChar(final CharSequence buffer, final int pos) { 335 if (pos < 0) { 336 return false; 337 } 338 339 for (int i = 0; (escapeChars != null) && (i < escapeChars.length); i++) { 340 if (buffer.charAt(pos) == escapeChars[i]) { 341 return !isEscaped(buffer, pos); // escape escape 342 } 343 } 344 345 return false; 346 } 347 348 /** 349 * Check if a character is escaped (i.e. if the previous character is an escape) 350 * 351 * @param buffer 352 * the buffer to check in 353 * @param pos 354 * the position of the character to check 355 * @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 356 * escape character. 357 */ 358 public boolean isEscaped(final CharSequence buffer, final int pos) { 359 if (pos <= 0) { 360 return false; 361 } 362 363 return isEscapeChar(buffer, pos - 1); 364 } 365 366 /** 367 * Returns true if the character at the specified position if a delimiter. This method will only be called if 368 * the character is not enclosed in any of the {@link #getQuoteChars}, and is not escaped by ant of the 369 * {@link #getEscapeChars}. To perform escaping manually, override {@link #isDelimiter} instead. 370 */ 371 public abstract boolean isDelimiterChar(CharSequence buffer, int pos); 372 } 373 374 /** 375 * {@link ArgumentCompleter.ArgumentDelimiter} implementation that counts all whitespace (as reported by 376 * {@link Character#isWhitespace}) as being a delimiter. 377 * 378 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a> 379 */ 380 public static class WhitespaceArgumentDelimiter 381 extends AbstractArgumentDelimiter 382 { 383 /** 384 * The character is a delimiter if it is whitespace, and the 385 * preceding character is not an escape character. 386 */ 387 @Override 388 public boolean isDelimiterChar(final CharSequence buffer, final int pos) { 389 return Character.isWhitespace(buffer.charAt(pos)); 390 } 391 } 392 393 /** 394 * The result of a delimited buffer. 395 * 396 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a> 397 */ 398 public static class ArgumentList 399 { 400 private String[] arguments; 401 402 private int cursorArgumentIndex; 403 404 private int argumentPosition; 405 406 private int bufferPosition; 407 408 /** 409 * @param arguments The array of tokens 410 * @param cursorArgumentIndex The token index of the cursor 411 * @param argumentPosition The position of the cursor in the current token 412 * @param bufferPosition The position of the cursor in the whole buffer 413 */ 414 public ArgumentList(final String[] arguments, final int cursorArgumentIndex, final int argumentPosition, final int bufferPosition) { 415 this.arguments = checkNotNull(arguments); 416 this.cursorArgumentIndex = cursorArgumentIndex; 417 this.argumentPosition = argumentPosition; 418 this.bufferPosition = bufferPosition; 419 } 420 421 public void setCursorArgumentIndex(final int i) { 422 this.cursorArgumentIndex = i; 423 } 424 425 public int getCursorArgumentIndex() { 426 return this.cursorArgumentIndex; 427 } 428 429 public String getCursorArgument() { 430 if ((cursorArgumentIndex < 0) || (cursorArgumentIndex >= arguments.length)) { 431 return null; 432 } 433 434 return arguments[cursorArgumentIndex]; 435 } 436 437 public void setArgumentPosition(final int pos) { 438 this.argumentPosition = pos; 439 } 440 441 public int getArgumentPosition() { 442 return this.argumentPosition; 443 } 444 445 public void setArguments(final String[] arguments) { 446 this.arguments = arguments; 447 } 448 449 public String[] getArguments() { 450 return this.arguments; 451 } 452 453 public void setBufferPosition(final int pos) { 454 this.bufferPosition = pos; 455 } 456 457 public int getBufferPosition() { 458 return this.bufferPosition; 459 } 460 } 461 }