1
2
3
4
5
6
7 package jline;
8
9 import java.util.*;
10
11 /***
12 * A {@link Completor} implementation that invokes a child completor
13 * using the appropriate <i>separator</i> argument. This
14 * can be used instead of the individual completors having to
15 * know about argument parsing semantics.
16 * <p>
17 * <strong>Example 1</strong>: Any argument of the command line can
18 * use file completion.
19 * <p>
20 * <pre>
21 * consoleReader.addCompletor (new ArgumentCompletor (
22 * new {@link FileNameCompletor} ()))
23 * </pre>
24 * <p>
25 * <strong>Example 2</strong>: The first argument of the command line
26 * can be completed with any of "foo", "bar", or "baz", and remaining
27 * arguments can be completed with a file name.
28 * <p>
29 * <pre>
30 * consoleReader.addCompletor (new ArgumentCompletor (
31 * new {@link SimpleCompletor} (new String [] { "foo", "bar", "baz"})));
32 * consoleReader.addCompletor (new ArgumentCompletor (
33 * new {@link FileNameCompletor} ()));
34 * </pre>
35 *
36 * <p>
37 * When the argument index is past the last embedded completors, the last
38 * completors is always used. To disable this behavior, have the last
39 * completor be a {@link NullCompletor}. For example:
40 * </p>
41 *
42 * <pre>
43 * consoleReader.addCompletor (new ArgumentCompletor (
44 * new {@link SimpleCompletor} (new String [] { "foo", "bar", "baz"}),
45 * new {@link SimpleCompletor} (new String [] { "xxx", "yyy", "xxx"}),
46 * new {@link NullCompletor}
47 * ));
48 * </pre>
49 * <p>
50 * TODO: handle argument quoting and escape characters
51 * </p>
52 *
53 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
54 */
55 public class ArgumentCompletor implements Completor {
56 final Completor[] completors;
57 final ArgumentDelimiter delim;
58 boolean strict = true;
59
60 /***
61 * Constuctor: create a new completor with the default
62 * argument separator of " ".
63 *
64 * @param completor the embedded completor
65 */
66 public ArgumentCompletor(final Completor completor) {
67 this(new Completor[] {
68 completor
69 });
70 }
71
72 /***
73 * Constuctor: create a new completor with the default
74 * argument separator of " ".
75 *
76 * @param completors the List of completors to use
77 */
78 public ArgumentCompletor(final List completors) {
79 this((Completor[]) completors.toArray(new Completor[completors.size()]));
80 }
81
82 /***
83 * Constuctor: create a new completor with the default
84 * argument separator of " ".
85 *
86 * @param completors the embedded argument completors
87 */
88 public ArgumentCompletor(final Completor[] completors) {
89 this(completors, new WhitespaceArgumentDelimiter());
90 }
91
92 /***
93 * Constuctor: create a new completor with the specified
94 * argument delimiter.
95 *
96 * @param completor the embedded completor
97 * @param delim the delimiter for parsing arguments
98 */
99 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
148 if (argIndex >= completors.length) {
149 comp = completors[completors.length - 1];
150 } else {
151 comp = completors[argIndex];
152 }
153
154
155
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
276
277 if (i == cursor) {
278 bindex = args.size();
279
280
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);
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);
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 }