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