View Javadoc

1   /*
2    * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved.
3    *
4    * This software is distributable under the BSD license. See the terms of the
5    * BSD license in the documentation provided with this software.
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         // 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 }