001 /*
002 * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved.
003 *
004 * This software is distributable under the BSD license. See the terms of the
005 * BSD license in the documentation provided with this software.
006 */
007 package jline;
008
009 import java.util.*;
010
011 /**
012 * A {@link Completor} implementation that invokes a child completor
013 * using the appropriate <i>separator</i> argument. This
014 * can be used instead of the individual completors having to
015 * know about argument parsing semantics.
016 * <p>
017 * <strong>Example 1</strong>: Any argument of the command line can
018 * use file completion.
019 * <p>
020 * <pre>
021 * consoleReader.addCompletor (new ArgumentCompletor (
022 * new {@link FileNameCompletor} ()))
023 * </pre>
024 * <p>
025 * <strong>Example 2</strong>: The first argument of the command line
026 * can be completed with any of "foo", "bar", or "baz", and remaining
027 * arguments can be completed with a file name.
028 * <p>
029 * <pre>
030 * consoleReader.addCompletor (new ArgumentCompletor (
031 * new {@link SimpleCompletor} (new String [] { "foo", "bar", "baz"})));
032 * consoleReader.addCompletor (new ArgumentCompletor (
033 * new {@link FileNameCompletor} ()));
034 * </pre>
035 *
036 * <p>
037 * When the argument index is past the last embedded completors, the last
038 * completors is always used. To disable this behavior, have the last
039 * completor be a {@link NullCompletor}. For example:
040 * </p>
041 *
042 * <pre>
043 * consoleReader.addCompletor (new ArgumentCompletor (
044 * new {@link SimpleCompletor} (new String [] { "foo", "bar", "baz"}),
045 * new {@link SimpleCompletor} (new String [] { "xxx", "yyy", "xxx"}),
046 * new {@link NullCompletor}
047 * ));
048 * </pre>
049 * <p>
050 * TODO: handle argument quoting and escape characters
051 * </p>
052 *
053 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
054 */
055 public class ArgumentCompletor implements Completor {
056 final Completor[] completors;
057 final ArgumentDelimiter delim;
058 boolean strict = true;
059
060 /**
061 * Constuctor: create a new completor with the default
062 * argument separator of " ".
063 *
064 * @param completor the embedded completor
065 */
066 public ArgumentCompletor(final Completor completor) {
067 this(new Completor[] {
068 completor
069 });
070 }
071
072 /**
073 * Constuctor: create a new completor with the default
074 * argument separator of " ".
075 *
076 * @param completors the List of completors to use
077 */
078 public ArgumentCompletor(final List completors) {
079 this((Completor[]) completors.toArray(new Completor[completors.size()]));
080 }
081
082 /**
083 * Constuctor: create a new completor with the default
084 * argument separator of " ".
085 *
086 * @param completors the embedded argument completors
087 */
088 public ArgumentCompletor(final Completor[] completors) {
089 this(completors, new WhitespaceArgumentDelimiter());
090 }
091
092 /**
093 * Constuctor: create a new completor with the specified
094 * argument delimiter.
095 *
096 * @param completor the embedded completor
097 * @param delim the delimiter for parsing arguments
098 */
099 public ArgumentCompletor(final Completor completor,
100 final ArgumentDelimiter delim) {
101 this(new Completor[] {
102 completor
103 }, delim);
104 }
105
106 /**
107 * Constuctor: create a new completor with the specified
108 * argument delimiter.
109 *
110 * @param completors the embedded completors
111 * @param delim the delimiter for parsing arguments
112 */
113 public ArgumentCompletor(final Completor[] completors,
114 final ArgumentDelimiter delim) {
115 this.completors = completors;
116 this.delim = delim;
117 }
118
119 /**
120 * If true, a completion at argument index N will only succeed
121 * if all the completions from 0-(N-1) also succeed.
122 */
123 public void setStrict(final boolean strict) {
124 this.strict = strict;
125 }
126
127 /**
128 * Returns whether a completion at argument index N will succees
129 * if all the completions from arguments 0-(N-1) also succeed.
130 */
131 public boolean getStrict() {
132 return this.strict;
133 }
134
135 public int complete(final String buffer, final int cursor,
136 final List candidates) {
137 ArgumentList list = delim.delimit(buffer, cursor);
138 int argpos = list.getArgumentPosition();
139 int argIndex = list.getCursorArgumentIndex();
140
141 if (argIndex < 0) {
142 return -1;
143 }
144
145 final Completor comp;
146
147 // if we are beyond the end of the completors, just use the last one
148 if (argIndex >= completors.length) {
149 comp = completors[completors.length - 1];
150 } else {
151 comp = completors[argIndex];
152 }
153
154 // ensure that all the previous completors are successful before
155 // allowing this completor to pass (only if strict is true).
156 for (int i = 0; getStrict() && (i < argIndex); i++) {
157 Completor sub =
158 completors[(i >= completors.length) ? (completors.length - 1) : i];
159 String[] args = list.getArguments();
160 String arg = ((args == null) || (i >= args.length)) ? "" : args[i];
161
162 List subCandidates = new LinkedList();
163
164 if (sub.complete(arg, arg.length(), subCandidates) == -1) {
165 return -1;
166 }
167
168 if (subCandidates.size() == 0) {
169 return -1;
170 }
171 }
172
173 int ret = comp.complete(list.getCursorArgument(), argpos, candidates);
174
175 if (ret == -1) {
176 return -1;
177 }
178
179 int pos = ret + (list.getBufferPosition() - argpos);
180
181 /**
182 * Special case: when completing in the middle of a line, and the
183 * area under the cursor is a delimiter, then trim any delimiters
184 * from the candidates, since we do not need to have an extra
185 * delimiter.
186 *
187 * E.g., if we have a completion for "foo", and we
188 * enter "f bar" into the buffer, and move to after the "f"
189 * and hit TAB, we want "foo bar" instead of "foo bar".
190 */
191 if ((cursor != buffer.length()) && delim.isDelimiter(buffer, cursor)) {
192 for (int i = 0; i < candidates.size(); i++) {
193 String val = candidates.get(i).toString();
194
195 while ((val.length() > 0)
196 && delim.isDelimiter(val, val.length() - 1)) {
197 val = val.substring(0, val.length() - 1);
198 }
199
200 candidates.set(i, val);
201 }
202 }
203
204 ConsoleReader.debug("Completing " + buffer + "(pos=" + cursor + ") "
205 + "with: " + candidates + ": offset=" + pos);
206
207 return pos;
208 }
209
210 /**
211 * The {@link ArgumentCompletor.ArgumentDelimiter} allows custom
212 * breaking up of a {@link String} into individual arguments in
213 * order to dispatch the arguments to the nested {@link Completor}.
214 *
215 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
216 */
217 public static interface ArgumentDelimiter {
218 /**
219 * Break the specified buffer into individual tokens
220 * that can be completed on their own.
221 *
222 * @param buffer the buffer to split
223 * @param argumentPosition the current position of the
224 * cursor in the buffer
225 * @return the tokens
226 */
227 ArgumentList delimit(String buffer, int argumentPosition);
228
229 /**
230 * Returns true if the specified character is a whitespace
231 * parameter.
232 *
233 * @param buffer the complete command buffer
234 * @param pos the index of the character in the buffer
235 * @return true if the character should be a delimiter
236 */
237 boolean isDelimiter(String buffer, int pos);
238 }
239
240 /**
241 * Abstract implementation of a delimiter that uses the
242 * {@link #isDelimiter} method to determine if a particular
243 * character should be used as a delimiter.
244 *
245 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
246 */
247 public abstract static class AbstractArgumentDelimiter
248 implements ArgumentDelimiter {
249 private char[] quoteChars = new char[] { '\'', '"' };
250 private char[] escapeChars = new char[] { '\\' };
251
252 public void setQuoteChars(final char[] quoteChars) {
253 this.quoteChars = quoteChars;
254 }
255
256 public char[] getQuoteChars() {
257 return this.quoteChars;
258 }
259
260 public void setEscapeChars(final char[] escapeChars) {
261 this.escapeChars = escapeChars;
262 }
263
264 public char[] getEscapeChars() {
265 return this.escapeChars;
266 }
267
268 public ArgumentList delimit(final String buffer, final int cursor) {
269 List args = new LinkedList();
270 StringBuffer arg = new StringBuffer();
271 int argpos = -1;
272 int bindex = -1;
273
274 for (int i = 0; (buffer != null) && (i <= buffer.length()); i++) {
275 // once we reach the cursor, set the
276 // position of the selected index
277 if (i == cursor) {
278 bindex = args.size();
279 // the position in the current argument is just the
280 // length of the current argument
281 argpos = arg.length();
282 }
283
284 if ((i == buffer.length()) || isDelimiter(buffer, i)) {
285 if (arg.length() > 0) {
286 args.add(arg.toString());
287 arg.setLength(0); // reset the arg
288 }
289 } else {
290 arg.append(buffer.charAt(i));
291 }
292 }
293
294 return new ArgumentList((String[]) args.
295 toArray(new String[args.size()]), bindex, argpos, cursor);
296 }
297
298 /**
299 * Returns true if the specified character is a whitespace
300 * parameter. Check to ensure that the character is not
301 * escaped by any of
302 * {@link #getQuoteChars}, and is not escaped by ant of the
303 * {@link #getEscapeChars}, and returns true from
304 * {@link #isDelimiterChar}.
305 *
306 * @param buffer the complete command buffer
307 * @param pos the index of the character in the buffer
308 * @return true if the character should be a delimiter
309 */
310 public boolean isDelimiter(final String buffer, final int pos) {
311 if (isQuoted(buffer, pos)) {
312 return false;
313 }
314
315 if (isEscaped(buffer, pos)) {
316 return false;
317 }
318
319 return isDelimiterChar(buffer, pos);
320 }
321
322 public boolean isQuoted(final String buffer, final int pos) {
323 return false;
324 }
325
326 public boolean isEscaped(final String buffer, final int pos) {
327 if (pos <= 0) {
328 return false;
329 }
330
331 for (int i = 0; (escapeChars != null) && (i < escapeChars.length);
332 i++) {
333 if (buffer.charAt(pos) == escapeChars[i]) {
334 return !isEscaped(buffer, pos - 1); // escape escape
335 }
336 }
337
338 return false;
339 }
340
341 /**
342 * Returns true if the character at the specified position
343 * if a delimiter. This method will only be called if the
344 * character is not enclosed in any of the
345 * {@link #getQuoteChars}, and is not escaped by ant of the
346 * {@link #getEscapeChars}. To perform escaping manually,
347 * override {@link #isDelimiter} instead.
348 */
349 public abstract boolean isDelimiterChar(String buffer, int pos);
350 }
351
352 /**
353 * {@link ArgumentCompletor.ArgumentDelimiter}
354 * implementation that counts all
355 * whitespace (as reported by {@link Character#isWhitespace})
356 * as being a delimiter.
357 *
358 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
359 */
360 public static class WhitespaceArgumentDelimiter
361 extends AbstractArgumentDelimiter {
362 /**
363 * The character is a delimiter if it is whitespace, and the
364 * preceeding character is not an escape character.
365 */
366 public boolean isDelimiterChar(String buffer, int pos) {
367 return Character.isWhitespace(buffer.charAt(pos));
368 }
369 }
370
371 /**
372 * The result of a delimited buffer.
373 *
374 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
375 */
376 public static class ArgumentList {
377 private String[] arguments;
378 private int cursorArgumentIndex;
379 private int argumentPosition;
380 private int bufferPosition;
381
382 /**
383 * @param arguments the array of tokens
384 * @param cursorArgumentIndex the token index of the cursor
385 * @param argumentPosition the position of the cursor in the
386 * current token
387 * @param bufferPosition the position of the cursor in
388 * the whole buffer
389 */
390 public ArgumentList(String[] arguments, int cursorArgumentIndex,
391 int argumentPosition, int bufferPosition) {
392 this.arguments = arguments;
393 this.cursorArgumentIndex = cursorArgumentIndex;
394 this.argumentPosition = argumentPosition;
395 this.bufferPosition = bufferPosition;
396 }
397
398 public void setCursorArgumentIndex(int cursorArgumentIndex) {
399 this.cursorArgumentIndex = cursorArgumentIndex;
400 }
401
402 public int getCursorArgumentIndex() {
403 return this.cursorArgumentIndex;
404 }
405
406 public String getCursorArgument() {
407 if ((cursorArgumentIndex < 0)
408 || (cursorArgumentIndex >= arguments.length)) {
409 return null;
410 }
411
412 return arguments[cursorArgumentIndex];
413 }
414
415 public void setArgumentPosition(int argumentPosition) {
416 this.argumentPosition = argumentPosition;
417 }
418
419 public int getArgumentPosition() {
420 return this.argumentPosition;
421 }
422
423 public void setArguments(String[] arguments) {
424 this.arguments = arguments;
425 }
426
427 public String[] getArguments() {
428 return this.arguments;
429 }
430
431 public void setBufferPosition(int bufferPosition) {
432 this.bufferPosition = bufferPosition;
433 }
434
435 public int getBufferPosition() {
436 return this.bufferPosition;
437 }
438 }
439 }