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