001 /** 002 * jline - Java console input library 003 * Copyright (c) 2002,2003 Marc Prud'hommeaux marc@apocalypse.org 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:marc@apocalypse.org">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 (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 (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 (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 (Completor completor, ArgumentDelimiter delim) 122 { 123 this (new Completor [] { completor }, delim); 124 } 125 126 127 /** 128 * Constuctor: create a new completor with the specified 129 * argument delimiter. 130 * 131 * @param completors the embedded completors 132 * @param delim the delimiter for parsing arguments 133 */ 134 public ArgumentCompletor (Completor [] completors, ArgumentDelimiter delim) 135 { 136 this.completors = completors; 137 this.delim = delim; 138 } 139 140 141 /** 142 * If true, a completion at argument index N will only succeed 143 * if all the completions from 0-(N-1) also succeed. 144 */ 145 public void setStrict (boolean strict) 146 { 147 this.strict = strict; 148 } 149 150 151 /** 152 * Returns whether a completion at argument index N will succees 153 * if all the completions from arguments 0-(N-1) also succeed. 154 */ 155 public boolean getStrict () 156 { 157 return this.strict; 158 } 159 160 161 public int complete (String buffer, int cursor, List candidates) 162 { 163 ArgumentList list = delim.delimit (buffer, cursor); 164 int argpos = list.getArgumentPosition (); 165 int argIndex = list.getCursorArgumentIndex (); 166 if (argIndex < 0) 167 return -1; 168 169 final Completor comp; 170 171 // if we are beyond the end of the completors, just use the last one 172 if (argIndex >= completors.length) 173 comp = completors [completors.length - 1]; 174 else 175 comp = completors [argIndex]; 176 177 // ensure that all the previous completors are successful before 178 // allowing this completor to pass (only if strict is true). 179 for (int i = 0; getStrict () && i < argIndex; i++) 180 { 181 Completor sub = completors [i >= completors.length 182 ? completors.length - 1 : i]; 183 String [] args = list.getArguments (); 184 String arg = args == null || i >= args.length ? "" : args [i]; 185 186 List subCandidates = new LinkedList (); 187 if (sub.complete (arg, arg.length (), subCandidates) == -1) 188 return -1; 189 190 if (subCandidates.size () == 0) 191 return -1; 192 } 193 194 int ret = comp.complete (list.getCursorArgument (), argpos, candidates); 195 if (ret == -1) 196 return -1; 197 198 int pos = ret + (list.getBufferPosition () - argpos + 1); 199 200 /** 201 * Special case: when completing in the middle of a line, and the 202 * area under the cursor is a delimiter, then trim any delimiters 203 * from the candidates, since we do not need to have an extra 204 * delimiter. 205 * 206 * E.g., if we have a completion for "foo", and we 207 * enter "f bar" into the buffer, and move to after the "f" 208 * and hit TAB, we want "foo bar" instead of "foo bar". 209 */ 210 if (cursor != buffer.length () && delim.isDelimiter (buffer, cursor)) 211 { 212 for (int i = 0; i < candidates.size (); i++) 213 { 214 String val = candidates.get (i).toString (); 215 while (val.length () > 0 && 216 delim.isDelimiter (val, val.length () - 1)) 217 val = val.substring (0, val.length () - 1); 218 219 candidates.set (i, val); 220 } 221 } 222 223 ConsoleReader.debug ("Completing " + buffer + "(pos=" + cursor + ") " 224 + "with: " + candidates + ": offset=" + pos); 225 226 return pos; 227 } 228 229 230 /** 231 * The {@link ArgumentCompletor.ArgumentDelimiter} allows custom 232 * breaking up of a {@link String} into individual arguments in 233 * order to dispatch the arguments to the nested {@link Completor}. 234 * 235 * @author <a href="mailto:marc@apocalypse.org">Marc Prud'hommeaux</a> 236 */ 237 public static interface ArgumentDelimiter 238 { 239 /** 240 * Break the specified buffer into individual tokens 241 * that can be completed on their own. 242 * 243 * @param buffer the buffer to split 244 * @param argumentPosition the current position of the 245 * cursor in the buffer 246 * @return the tokens 247 */ 248 public ArgumentList delimit (String buffer, int argumentPosition); 249 250 251 /** 252 * Returns true if the specified character is a whitespace 253 * parameter. 254 * 255 * @param buffer the complete command buffer 256 * @param pos the index of the character in the buffer 257 * @return true if the character should be a delimiter 258 */ 259 public boolean isDelimiter (String buffer, int pos); 260 } 261 262 263 /** 264 * Abstract implementation of a delimiter that uses the 265 * {@link #isDelimiter} method to determine if a particular 266 * character should be used as a delimiter. 267 * 268 * @author <a href="mailto:marc@apocalypse.org">Marc Prud'hommeaux</a> 269 */ 270 public static abstract class AbstractArgumentDelimiter 271 implements ArgumentDelimiter 272 { 273 private char [] quoteChars = new char [] { '\'', '"' }; 274 private char [] escapeChars = new char [] { '\\' }; 275 276 277 public void setQuoteChars (char [] quoteChars) 278 { 279 this.quoteChars = quoteChars; 280 } 281 282 283 public char [] getQuoteChars () 284 { 285 return this.quoteChars; 286 } 287 288 289 public void setEscapeChars (char [] escapeChars) 290 { 291 this.escapeChars = escapeChars; 292 } 293 294 295 public char [] getEscapeChars () 296 { 297 return this.escapeChars; 298 } 299 300 301 302 public ArgumentList delimit (String buffer, int cursor) 303 { 304 List args = new LinkedList (); 305 StringBuffer arg = new StringBuffer (); 306 int argpos = -1; 307 int bindex = -1; 308 309 for (int i = 0; buffer != null && i <= buffer.length (); i++) 310 { 311 // once we reach the cursor, set the 312 // position of the selected index 313 if (i == cursor) 314 { 315 bindex = args.size (); 316 // the position in the current argument is just the 317 // length of the current argument 318 argpos = arg.length (); 319 } 320 321 if (i == buffer.length () || isDelimiter (buffer, i)) 322 { 323 if (arg.length () > 0) 324 { 325 args.add (arg.toString ()); 326 arg.setLength (0); // reset the arg 327 } 328 } 329 else 330 { 331 arg.append (buffer.charAt (i)); 332 } 333 } 334 335 return new ArgumentList ( 336 (String [])args.toArray (new String [args.size ()]), 337 bindex, argpos, cursor); 338 } 339 340 341 /** 342 * Returns true if the specified character is a whitespace 343 * parameter. Check to ensure that the character is not 344 * escaped by any of 345 * {@link #getQuoteChars}, and is not escaped by ant of the 346 * {@link #getEscapeChars}, and returns true from 347 * {@link #isDelimiterChar}. 348 * 349 * @param buffer the complete command buffer 350 * @param pos the index of the character in the buffer 351 * @return true if the character should be a delimiter 352 */ 353 public boolean isDelimiter (String buffer, int pos) 354 { 355 if (isQuoted (buffer, pos)) 356 return false; 357 if (isEscaped (buffer, pos)) 358 return false; 359 360 return isDelimiterChar (buffer, pos); 361 } 362 363 364 public boolean isQuoted (String buffer, int pos) 365 { 366 return false; 367 } 368 369 370 public boolean isEscaped (String buffer, int pos) 371 { 372 if (pos <= 0) 373 return false; 374 375 for (int i = 0; escapeChars != null && i < escapeChars.length; i++) 376 { 377 if (buffer.charAt (pos) == escapeChars [i]) 378 return !isEscaped (buffer, pos - 1); // escape escape 379 } 380 381 return false; 382 } 383 384 385 /** 386 * Returns true if the character at the specified position 387 * if a delimiter. This method will only be called if the 388 * character is not enclosed in any of the 389 * {@link #getQuoteChars}, and is not escaped by ant of the 390 * {@link #getEscapeChars}. To perform escaping manually, 391 * override {@link #isDelimiter} instead. 392 */ 393 public abstract boolean isDelimiterChar (String buffer, int pos); 394 } 395 396 397 /** 398 * {@link ArgumentCompletor.ArgumentDelimiter} 399 * implementation that counts all 400 * whitespace (as reported by {@link Character#isWhitespace}) 401 * as being a delimiter. 402 * 403 * @author <a href="mailto:marc@apocalypse.org">Marc Prud'hommeaux</a> 404 */ 405 public static class WhitespaceArgumentDelimiter 406 extends AbstractArgumentDelimiter 407 { 408 /** 409 * The character is a delimiter if it is whitespace, and the 410 * preceeding character is not an escape character. 411 */ 412 public boolean isDelimiterChar (String buffer, int pos) 413 { 414 return Character.isWhitespace (buffer.charAt (pos)); 415 } 416 } 417 418 419 /** 420 * The result of a delimited buffer. 421 * 422 * @author <a href="mailto:marc@apocalypse.org">Marc Prud'hommeaux</a> 423 */ 424 public static class ArgumentList 425 { 426 private String [] arguments; 427 private int cursorArgumentIndex; 428 private int argumentPosition; 429 private int bufferPosition; 430 431 /** 432 * @param arguments the array of tokens 433 * @param cursorArgumentIndex the token index of the cursor 434 * @param argumentPosition the position of the cursor in the 435 * current token 436 * @param bufferPosition the position of the cursor in 437 * the whole buffer 438 */ 439 public ArgumentList (String [] arguments, int cursorArgumentIndex, 440 int argumentPosition, int bufferPosition) 441 { 442 this.arguments = arguments; 443 this.cursorArgumentIndex = cursorArgumentIndex; 444 this.argumentPosition = argumentPosition; 445 this.bufferPosition = bufferPosition; 446 } 447 448 449 public void setCursorArgumentIndex (int cursorArgumentIndex) 450 { 451 this.cursorArgumentIndex = cursorArgumentIndex; 452 } 453 454 455 public int getCursorArgumentIndex () 456 { 457 return this.cursorArgumentIndex; 458 } 459 460 461 public String getCursorArgument () 462 { 463 if (cursorArgumentIndex < 0 464 || cursorArgumentIndex >= arguments.length) 465 return null; 466 467 return arguments [cursorArgumentIndex]; 468 } 469 470 471 public void setArgumentPosition (int argumentPosition) 472 { 473 this.argumentPosition = argumentPosition; 474 } 475 476 477 public int getArgumentPosition () 478 { 479 return this.argumentPosition; 480 } 481 482 483 public void setArguments (String [] arguments) 484 { 485 this.arguments = arguments; 486 } 487 488 489 public String [] getArguments () 490 { 491 return this.arguments; 492 } 493 494 495 public void setBufferPosition (int bufferPosition) 496 { 497 this.bufferPosition = bufferPosition; 498 } 499 500 501 public int getBufferPosition () 502 { 503 return this.bufferPosition; 504 } 505 } 506 } 507