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