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