View Javadoc

1   /**
2    *	jline - Java console input library
3    *	Copyright (c) 2002, 2003, 2004, 2005, Marc Prud'hommeaux mwp1@cornell.edu
4    *	All rights reserved.
5    *
6    *	Redistribution and use in source and binary forms, with or
7    *	without modification, are permitted provided that the following
8    *	conditions are met:
9    *
10   *	Redistributions of source code must retain the above copyright
11   *	notice, this list of conditions and the following disclaimer.
12   *
13   *	Redistributions in binary form must reproduce the above copyright
14   *	notice, this list of conditions and the following disclaimer
15   *	in the documentation and/or other materials provided with
16   *	the distribution.
17   *
18   *	Neither the name of JLine nor the names of its contributors
19   *	may be used to endorse or promote products derived from this
20   *	software without specific prior written permission.
21   *
22   *	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23   *	"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
24   *	BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
25   *	AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
26   *	EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
27   *	FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
28   *	OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
29   *	PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
30   *	DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
31   *	AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
32   *	LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
33   *	IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
34   *	OF THE POSSIBILITY OF SUCH DAMAGE.
35   */
36  package jline;
37  
38  import java.util.*;
39  
40  
41  /**
42   *  A {@link Completor} implementation that invokes a child completor
43   *  using the appropriate <i>separator</i> argument. This
44   *  can be used instead of the individual completors having to
45   *  know about argument parsing semantics.
46   *  <p>
47   *  <strong>Example 1</strong>: Any argument of the command line can
48   *  use file completion.
49   *  <p>
50   *  <pre>
51   *	consoleReader.addCompletor (new ArgumentCompletor (
52   *		new {@link FileNameCompletor} ()))
53   *  </pre>
54   *  <p>
55   *  <strong>Example 2</strong>: The first argument of the command line
56   *  can be completed with any of "foo", "bar", or "baz", and remaining
57   *  arguments can be completed with a file name.
58   *  <p>
59   *  <pre>
60   *	consoleReader.addCompletor (new ArgumentCompletor (
61   *		new {@link SimpleCompletor} (new String [] { "foo", "bar", "baz"})));
62   *	consoleReader.addCompletor (new ArgumentCompletor (
63   *		new {@link FileNameCompletor} ()));
64   *  </pre>
65   *
66   *  <p>
67   *	When the argument index is past the last embedded completors, the last
68   *	completors is always used. To disable this behavior, have the last
69   *	completor be a {@link NullCompletor}. For example:
70   *	</p>
71   *
72   *	<pre>
73   *	consoleReader.addCompletor (new ArgumentCompletor (
74   *		new {@link SimpleCompletor} (new String [] { "foo", "bar", "baz"}),
75   *		new {@link SimpleCompletor} (new String [] { "xxx", "yyy", "xxx"}),
76   *		new {@link NullCompletor}
77   *		));
78   *	</pre>
79   *  <p>
80   *  TODO: handle argument quoting and escape characters
81   *  </p>
82   *
83   *  @author  <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
84   */
85  public class ArgumentCompletor
86  	implements Completor
87  {
88  	final Completor []			completors;
89  	final ArgumentDelimiter		delim;
90  	boolean						strict = true;
91  
92  
93  	/**
94  	 *  Constuctor: create a new completor with the default
95  	 *  argument separator of " ".
96  	 *
97  	 *  @param  completor  the embedded completor
98  	 */
99  	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 || completors.length == 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