001 /* 002 * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. 003 * 004 * This software is distributable under the BSD license. See the terms of the 005 * BSD license in the documentation provided with this software. 006 */ 007 package jline; 008 009 import java.util.*; 010 011 /** 012 * A {@link Completor} implementation that invokes a child completor 013 * using the appropriate <i>separator</i> argument. This 014 * can be used instead of the individual completors having to 015 * know about argument parsing semantics. 016 * <p> 017 * <strong>Example 1</strong>: Any argument of the command line can 018 * use file completion. 019 * <p> 020 * <pre> 021 * consoleReader.addCompletor (new ArgumentCompletor ( 022 * new {@link FileNameCompletor} ())) 023 * </pre> 024 * <p> 025 * <strong>Example 2</strong>: The first argument of the command line 026 * can be completed with any of "foo", "bar", or "baz", and remaining 027 * arguments can be completed with a file name. 028 * <p> 029 * <pre> 030 * consoleReader.addCompletor (new ArgumentCompletor ( 031 * new {@link SimpleCompletor} (new String [] { "foo", "bar", "baz"}))); 032 * consoleReader.addCompletor (new ArgumentCompletor ( 033 * new {@link FileNameCompletor} ())); 034 * </pre> 035 * 036 * <p> 037 * When the argument index is past the last embedded completors, the last 038 * completors is always used. To disable this behavior, have the last 039 * completor be a {@link NullCompletor}. For example: 040 * </p> 041 * 042 * <pre> 043 * consoleReader.addCompletor (new ArgumentCompletor ( 044 * new {@link SimpleCompletor} (new String [] { "foo", "bar", "baz"}), 045 * new {@link SimpleCompletor} (new String [] { "xxx", "yyy", "xxx"}), 046 * new {@link NullCompletor} 047 * )); 048 * </pre> 049 * <p> 050 * TODO: handle argument quoting and escape characters 051 * </p> 052 * 053 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a> 054 */ 055 public class ArgumentCompletor implements Completor { 056 final Completor[] completors; 057 final ArgumentDelimiter delim; 058 boolean strict = true; 059 060 /** 061 * Constuctor: create a new completor with the default 062 * argument separator of " ". 063 * 064 * @param completor the embedded completor 065 */ 066 public ArgumentCompletor(final Completor completor) { 067 this(new Completor[] { 068 completor 069 }); 070 } 071 072 /** 073 * Constuctor: create a new completor with the default 074 * argument separator of " ". 075 * 076 * @param completors the List of completors to use 077 */ 078 public ArgumentCompletor(final List completors) { 079 this((Completor[]) completors.toArray(new Completor[completors.size()])); 080 } 081 082 /** 083 * Constuctor: create a new completor with the default 084 * argument separator of " ". 085 * 086 * @param completors the embedded argument completors 087 */ 088 public ArgumentCompletor(final Completor[] completors) { 089 this(completors, new WhitespaceArgumentDelimiter()); 090 } 091 092 /** 093 * Constuctor: create a new completor with the specified 094 * argument delimiter. 095 * 096 * @param completor the embedded completor 097 * @param delim the delimiter for parsing arguments 098 */ 099 public ArgumentCompletor(final Completor completor, 100 final ArgumentDelimiter delim) { 101 this(new Completor[] { 102 completor 103 }, delim); 104 } 105 106 /** 107 * Constuctor: create a new completor with the specified 108 * argument delimiter. 109 * 110 * @param completors the embedded completors 111 * @param delim the delimiter for parsing arguments 112 */ 113 public ArgumentCompletor(final Completor[] completors, 114 final ArgumentDelimiter delim) { 115 this.completors = completors; 116 this.delim = delim; 117 } 118 119 /** 120 * If true, a completion at argument index N will only succeed 121 * if all the completions from 0-(N-1) also succeed. 122 */ 123 public void setStrict(final boolean strict) { 124 this.strict = strict; 125 } 126 127 /** 128 * Returns whether a completion at argument index N will succees 129 * if all the completions from arguments 0-(N-1) also succeed. 130 */ 131 public boolean getStrict() { 132 return this.strict; 133 } 134 135 public int complete(final String buffer, final int cursor, 136 final List candidates) { 137 ArgumentList list = delim.delimit(buffer, cursor); 138 int argpos = list.getArgumentPosition(); 139 int argIndex = list.getCursorArgumentIndex(); 140 141 if (argIndex < 0) { 142 return -1; 143 } 144 145 final Completor comp; 146 147 // if we are beyond the end of the completors, just use the last one 148 if (argIndex >= completors.length) { 149 comp = completors[completors.length - 1]; 150 } else { 151 comp = completors[argIndex]; 152 } 153 154 // ensure that all the previous completors are successful before 155 // allowing this completor to pass (only if strict is true). 156 for (int i = 0; getStrict() && (i < argIndex); i++) { 157 Completor sub = 158 completors[(i >= completors.length) ? (completors.length - 1) : i]; 159 String[] args = list.getArguments(); 160 String arg = ((args == null) || (i >= args.length)) ? "" : args[i]; 161 162 List subCandidates = new LinkedList(); 163 164 if (sub.complete(arg, arg.length(), subCandidates) == -1) { 165 return -1; 166 } 167 168 if (subCandidates.size() == 0) { 169 return -1; 170 } 171 } 172 173 int ret = comp.complete(list.getCursorArgument(), argpos, candidates); 174 175 if (ret == -1) { 176 return -1; 177 } 178 179 int pos = ret + (list.getBufferPosition() - argpos); 180 181 /** 182 * Special case: when completing in the middle of a line, and the 183 * area under the cursor is a delimiter, then trim any delimiters 184 * from the candidates, since we do not need to have an extra 185 * delimiter. 186 * 187 * E.g., if we have a completion for "foo", and we 188 * enter "f bar" into the buffer, and move to after the "f" 189 * and hit TAB, we want "foo bar" instead of "foo bar". 190 */ 191 if ((cursor != buffer.length()) && delim.isDelimiter(buffer, cursor)) { 192 for (int i = 0; i < candidates.size(); i++) { 193 String val = candidates.get(i).toString(); 194 195 while ((val.length() > 0) 196 && delim.isDelimiter(val, val.length() - 1)) { 197 val = val.substring(0, val.length() - 1); 198 } 199 200 candidates.set(i, val); 201 } 202 } 203 204 ConsoleReader.debug("Completing " + buffer + "(pos=" + cursor + ") " 205 + "with: " + candidates + ": offset=" + pos); 206 207 return pos; 208 } 209 210 /** 211 * The {@link ArgumentCompletor.ArgumentDelimiter} allows custom 212 * breaking up of a {@link String} into individual arguments in 213 * order to dispatch the arguments to the nested {@link Completor}. 214 * 215 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a> 216 */ 217 public static interface ArgumentDelimiter { 218 /** 219 * Break the specified buffer into individual tokens 220 * that can be completed on their own. 221 * 222 * @param buffer the buffer to split 223 * @param argumentPosition the current position of the 224 * cursor in the buffer 225 * @return the tokens 226 */ 227 ArgumentList delimit(String buffer, int argumentPosition); 228 229 /** 230 * Returns true if the specified character is a whitespace 231 * parameter. 232 * 233 * @param buffer the complete command buffer 234 * @param pos the index of the character in the buffer 235 * @return true if the character should be a delimiter 236 */ 237 boolean isDelimiter(String buffer, int pos); 238 } 239 240 /** 241 * Abstract implementation of a delimiter that uses the 242 * {@link #isDelimiter} method to determine if a particular 243 * character should be used as a delimiter. 244 * 245 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a> 246 */ 247 public abstract static class AbstractArgumentDelimiter 248 implements ArgumentDelimiter { 249 private char[] quoteChars = new char[] { '\'', '"' }; 250 private char[] escapeChars = new char[] { '\\' }; 251 252 public void setQuoteChars(final char[] quoteChars) { 253 this.quoteChars = quoteChars; 254 } 255 256 public char[] getQuoteChars() { 257 return this.quoteChars; 258 } 259 260 public void setEscapeChars(final char[] escapeChars) { 261 this.escapeChars = escapeChars; 262 } 263 264 public char[] getEscapeChars() { 265 return this.escapeChars; 266 } 267 268 public ArgumentList delimit(final String buffer, final int cursor) { 269 List args = new LinkedList(); 270 StringBuffer arg = new StringBuffer(); 271 int argpos = -1; 272 int bindex = -1; 273 274 for (int i = 0; (buffer != null) && (i <= buffer.length()); i++) { 275 // once we reach the cursor, set the 276 // position of the selected index 277 if (i == cursor) { 278 bindex = args.size(); 279 // the position in the current argument is just the 280 // length of the current argument 281 argpos = arg.length(); 282 } 283 284 if ((i == buffer.length()) || isDelimiter(buffer, i)) { 285 if (arg.length() > 0) { 286 args.add(arg.toString()); 287 arg.setLength(0); // reset the arg 288 } 289 } else { 290 arg.append(buffer.charAt(i)); 291 } 292 } 293 294 return new ArgumentList((String[]) args. 295 toArray(new String[args.size()]), bindex, argpos, cursor); 296 } 297 298 /** 299 * Returns true if the specified character is a whitespace 300 * parameter. Check to ensure that the character is not 301 * escaped by any of 302 * {@link #getQuoteChars}, and is not escaped by ant of the 303 * {@link #getEscapeChars}, and returns true from 304 * {@link #isDelimiterChar}. 305 * 306 * @param buffer the complete command buffer 307 * @param pos the index of the character in the buffer 308 * @return true if the character should be a delimiter 309 */ 310 public boolean isDelimiter(final String buffer, final int pos) { 311 if (isQuoted(buffer, pos)) { 312 return false; 313 } 314 315 if (isEscaped(buffer, pos)) { 316 return false; 317 } 318 319 return isDelimiterChar(buffer, pos); 320 } 321 322 public boolean isQuoted(final String buffer, final int pos) { 323 return false; 324 } 325 326 public boolean isEscaped(final String buffer, final int pos) { 327 if (pos <= 0) { 328 return false; 329 } 330 331 for (int i = 0; (escapeChars != null) && (i < escapeChars.length); 332 i++) { 333 if (buffer.charAt(pos) == escapeChars[i]) { 334 return !isEscaped(buffer, pos - 1); // escape escape 335 } 336 } 337 338 return false; 339 } 340 341 /** 342 * Returns true if the character at the specified position 343 * if a delimiter. This method will only be called if the 344 * character is not enclosed in any of the 345 * {@link #getQuoteChars}, and is not escaped by ant of the 346 * {@link #getEscapeChars}. To perform escaping manually, 347 * override {@link #isDelimiter} instead. 348 */ 349 public abstract boolean isDelimiterChar(String buffer, int pos); 350 } 351 352 /** 353 * {@link ArgumentCompletor.ArgumentDelimiter} 354 * implementation that counts all 355 * whitespace (as reported by {@link Character#isWhitespace}) 356 * as being a delimiter. 357 * 358 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a> 359 */ 360 public static class WhitespaceArgumentDelimiter 361 extends AbstractArgumentDelimiter { 362 /** 363 * The character is a delimiter if it is whitespace, and the 364 * preceeding character is not an escape character. 365 */ 366 public boolean isDelimiterChar(String buffer, int pos) { 367 return Character.isWhitespace(buffer.charAt(pos)); 368 } 369 } 370 371 /** 372 * The result of a delimited buffer. 373 * 374 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a> 375 */ 376 public static class ArgumentList { 377 private String[] arguments; 378 private int cursorArgumentIndex; 379 private int argumentPosition; 380 private int bufferPosition; 381 382 /** 383 * @param arguments the array of tokens 384 * @param cursorArgumentIndex the token index of the cursor 385 * @param argumentPosition the position of the cursor in the 386 * current token 387 * @param bufferPosition the position of the cursor in 388 * the whole buffer 389 */ 390 public ArgumentList(String[] arguments, int cursorArgumentIndex, 391 int argumentPosition, int bufferPosition) { 392 this.arguments = arguments; 393 this.cursorArgumentIndex = cursorArgumentIndex; 394 this.argumentPosition = argumentPosition; 395 this.bufferPosition = bufferPosition; 396 } 397 398 public void setCursorArgumentIndex(int cursorArgumentIndex) { 399 this.cursorArgumentIndex = cursorArgumentIndex; 400 } 401 402 public int getCursorArgumentIndex() { 403 return this.cursorArgumentIndex; 404 } 405 406 public String getCursorArgument() { 407 if ((cursorArgumentIndex < 0) 408 || (cursorArgumentIndex >= arguments.length)) { 409 return null; 410 } 411 412 return arguments[cursorArgumentIndex]; 413 } 414 415 public void setArgumentPosition(int argumentPosition) { 416 this.argumentPosition = argumentPosition; 417 } 418 419 public int getArgumentPosition() { 420 return this.argumentPosition; 421 } 422 423 public void setArguments(String[] arguments) { 424 this.arguments = arguments; 425 } 426 427 public String[] getArguments() { 428 return this.arguments; 429 } 430 431 public void setBufferPosition(int bufferPosition) { 432 this.bufferPosition = bufferPosition; 433 } 434 435 public int getBufferPosition() { 436 return this.bufferPosition; 437 } 438 } 439 }