View Javadoc

1   /*
2    * Copyright (c) 2002-2012, the original author or authors.
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    * http://www.opensource.org/licenses/bsd-license.php
8    */
9   package jline.console.completer;
10  
11  import jline.internal.Log;
12  
13  import java.util.ArrayList;
14  import java.util.Arrays;
15  import java.util.Collection;
16  import java.util.LinkedList;
17  import java.util.List;
18  
19  import static jline.internal.Preconditions.checkNotNull;
20  
21  /**
22   * A {@link Completer} implementation that invokes a child completer using the appropriate <i>separator</i> argument.
23   * This can be used instead of the individual completers having to know about argument parsing semantics.
24   *
25   * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
26   * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
27   * @since 2.3
28   */
29  public class ArgumentCompleter
30      implements Completer
31  {
32      private final ArgumentDelimiter delimiter;
33  
34      private final List<Completer> completers = new ArrayList<Completer>();
35  
36      private boolean strict = true;
37  
38      /**
39       * Create a new completer with the specified argument delimiter.
40       *
41       * @param delimiter     The delimiter for parsing arguments
42       * @param completers    The embedded completers
43       */
44      public ArgumentCompleter(final ArgumentDelimiter delimiter, final Collection<Completer> completers) {
45          this.delimiter = checkNotNull(delimiter);
46          checkNotNull(completers);
47          this.completers.addAll(completers);
48      }
49  
50      /**
51       * Create a new completer with the specified argument delimiter.
52       *
53       * @param delimiter     The delimiter for parsing arguments
54       * @param completers    The embedded completers
55       */
56      public ArgumentCompleter(final ArgumentDelimiter delimiter, final Completer... completers) {
57          this(delimiter, Arrays.asList(completers));
58      }
59  
60      /**
61       * Create a new completer with the default {@link WhitespaceArgumentDelimiter}.
62       *
63       * @param completers    The embedded completers
64       */
65      public ArgumentCompleter(final Completer... completers) {
66          this(new WhitespaceArgumentDelimiter(), completers);
67      }
68  
69      /**
70       * Create a new completer with the default {@link WhitespaceArgumentDelimiter}.
71       *
72       * @param completers    The embedded completers
73       */
74      public ArgumentCompleter(final List<Completer> completers) {
75          this(new WhitespaceArgumentDelimiter(), completers);
76      }
77  
78      /**
79       * If true, a completion at argument index N will only succeed
80       * if all the completions from 0-(N-1) also succeed.
81       */
82      public void setStrict(final boolean strict) {
83          this.strict = strict;
84      }
85  
86      /**
87       * Returns whether a completion at argument index N will success
88       * if all the completions from arguments 0-(N-1) also succeed.
89       *
90       * @return  True if strict.
91       * @since 2.3
92       */
93      public boolean isStrict() {
94          return this.strict;
95      }
96  
97      /**
98       * @since 2.3
99       */
100     public ArgumentDelimiter getDelimiter() {
101         return delimiter;
102     }
103 
104     /**
105      * @since 2.3
106      */
107     public List<Completer> getCompleters() {
108         return completers;
109     }
110 
111     public int complete(final String buffer, final int cursor, final List<CharSequence> candidates) {
112         // buffer can be null
113         checkNotNull(candidates);
114 
115         ArgumentDelimiter delim = getDelimiter();
116         ArgumentList list = delim.delimit(buffer, cursor);
117         int argpos = list.getArgumentPosition();
118         int argIndex = list.getCursorArgumentIndex();
119 
120         if (argIndex < 0) {
121             return -1;
122         }
123 
124         List<Completer> completers = getCompleters();
125         Completer completer;
126 
127         // if we are beyond the end of the completers, just use the last one
128         if (argIndex >= completers.size()) {
129             completer = completers.get(completers.size() - 1);
130         }
131         else {
132             completer = completers.get(argIndex);
133         }
134 
135         // ensure that all the previous completers are successful before allowing this completer to pass (only if strict).
136         for (int i = 0; isStrict() && (i < argIndex); i++) {
137             Completer sub = completers.get(i >= completers.size() ? (completers.size() - 1) : i);
138             String[] args = list.getArguments();
139             String arg = (args == null || i >= args.length) ? "" : args[i];
140 
141             List<CharSequence> subCandidates = new LinkedList<CharSequence>();
142 
143             if (sub.complete(arg, arg.length(), subCandidates) == -1) {
144                 return -1;
145             }
146 
147             if (subCandidates.size() == 0) {
148                 return -1;
149             }
150         }
151 
152         int ret = completer.complete(list.getCursorArgument(), argpos, candidates);
153 
154         if (ret == -1) {
155             return -1;
156         }
157 
158         int pos = ret + list.getBufferPosition() - argpos;
159 
160         // Special case: when completing in the middle of a line, and the area under the cursor is a delimiter,
161         // then trim any delimiters from the candidates, since we do not need to have an extra delimiter.
162         //
163         // E.g., if we have a completion for "foo", and we enter "f bar" into the buffer, and move to after the "f"
164         // and hit TAB, we want "foo bar" instead of "foo  bar".
165 
166         if ((cursor != buffer.length()) && delim.isDelimiter(buffer, cursor)) {
167             for (int i = 0; i < candidates.size(); i++) {
168                 CharSequence val = candidates.get(i);
169 
170                 while (val.length() > 0 && delim.isDelimiter(val, val.length() - 1)) {
171                     val = val.subSequence(0, val.length() - 1);
172                 }
173 
174                 candidates.set(i, val);
175             }
176         }
177 
178         Log.trace("Completing ", buffer, " (pos=", cursor, ") with: ", candidates, ": offset=", pos);
179 
180         return pos;
181     }
182 
183     /**
184      * The {@link ArgumentCompleter.ArgumentDelimiter} allows custom breaking up of a {@link String} into individual
185      * arguments in order to dispatch the arguments to the nested {@link Completer}.
186      *
187      * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
188      */
189     public static interface ArgumentDelimiter
190     {
191         /**
192          * Break the specified buffer into individual tokens that can be completed on their own.
193          *
194          * @param buffer    The buffer to split
195          * @param pos       The current position of the cursor in the buffer
196          * @return          The tokens
197          */
198         ArgumentList delimit(CharSequence buffer, int pos);
199 
200         /**
201          * Returns true if the specified character is a whitespace parameter.
202          *
203          * @param buffer    The complete command buffer
204          * @param pos       The index of the character in the buffer
205          * @return          True if the character should be a delimiter
206          */
207         boolean isDelimiter(CharSequence buffer, int pos);
208     }
209 
210     /**
211      * Abstract implementation of a delimiter that uses the {@link #isDelimiter} method to determine if a particular
212      * character should be used as a delimiter.
213      *
214      * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
215      */
216     public abstract static class AbstractArgumentDelimiter
217         implements ArgumentDelimiter
218     {
219         private char[] quoteChars = {'\'', '"'};
220 
221         private char[] escapeChars = {'\\'};
222 
223         public void setQuoteChars(final char[] chars) {
224             this.quoteChars = chars;
225         }
226 
227         public char[] getQuoteChars() {
228             return this.quoteChars;
229         }
230 
231         public void setEscapeChars(final char[] chars) {
232             this.escapeChars = chars;
233         }
234 
235         public char[] getEscapeChars() {
236             return this.escapeChars;
237         }
238 
239         public ArgumentList delimit(final CharSequence buffer, final int cursor) {
240             List<String> args = new LinkedList<String>();
241             StringBuilder arg = new StringBuilder();
242             int argpos = -1;
243             int bindex = -1;
244             int quoteStart = -1;
245 
246             for (int i = 0; (buffer != null) && (i < buffer.length()); i++) {
247                 // once we reach the cursor, set the
248                 // position of the selected index
249                 if (i == cursor) {
250                     bindex = args.size();
251                     // the position in the current argument is just the
252                     // length of the current argument
253                     argpos = arg.length();
254                 }
255 
256                 if (quoteStart < 0 && isQuoteChar(buffer, i)) {
257                     // Start a quote block
258                     quoteStart = i;
259                 } else if (quoteStart >= 0) {
260                     // In a quote block
261                     if (buffer.charAt(quoteStart) == buffer.charAt(i) && !isEscaped(buffer, i)) {
262                         // End the block; arg could be empty, but that's fine
263                         args.add(arg.toString());
264                         arg.setLength(0);
265                         quoteStart = -1;
266                     } else if (!isEscapeChar(buffer, i)) {
267                         // Take the next character
268                         arg.append(buffer.charAt(i));
269                     }
270                 } else {
271                     // Not in a quote block
272                     if (isDelimiter(buffer, i)) {
273                         if (arg.length() > 0) {
274                             args.add(arg.toString());
275                             arg.setLength(0); // reset the arg
276                         }
277                     } else if (!isEscapeChar(buffer, i)) {
278                         arg.append(buffer.charAt(i));
279                     }
280                 }
281             }
282 
283             if (cursor == buffer.length()) {
284                 bindex = args.size();
285                 // the position in the current argument is just the
286                 // length of the current argument
287                 argpos = arg.length();
288             }
289             if (arg.length() > 0) {
290                 args.add(arg.toString());
291             }
292 
293             return new ArgumentList(args.toArray(new String[args.size()]), bindex, argpos, cursor);
294         }
295 
296         /**
297          * Returns true if the specified character is a whitespace parameter. Check to ensure that the character is not
298          * escaped by any of {@link #getQuoteChars}, and is not escaped by ant of the {@link #getEscapeChars}, and
299          * returns true from {@link #isDelimiterChar}.
300          *
301          * @param buffer    The complete command buffer
302          * @param pos       The index of the character in the buffer
303          * @return          True if the character should be a delimiter
304          */
305         public boolean isDelimiter(final CharSequence buffer, final int pos) {
306             return !isQuoted(buffer, pos) && !isEscaped(buffer, pos) && isDelimiterChar(buffer, pos);
307         }
308 
309         public boolean isQuoted(final CharSequence buffer, final int pos) {
310             return false;
311         }
312 
313         public boolean isQuoteChar(final CharSequence buffer, final int pos) {
314             if (pos < 0) {
315                 return false;
316             }
317 
318             for (int i = 0; (quoteChars != null) && (i < quoteChars.length); i++) {
319                 if (buffer.charAt(pos) == quoteChars[i]) {
320                     return !isEscaped(buffer, pos);
321                 }
322             }
323 
324             return false;
325         }
326 
327         /**
328          * Check if this character is a valid escape char (i.e. one that has not been escaped)
329          *
330          * @param buffer
331          * @param pos
332          * @return
333          */
334         public boolean isEscapeChar(final CharSequence buffer, final int pos) {
335             if (pos < 0) {
336                 return false;
337             }
338 
339             for (int i = 0; (escapeChars != null) && (i < escapeChars.length); i++) {
340                 if (buffer.charAt(pos) == escapeChars[i]) {
341                     return !isEscaped(buffer, pos); // escape escape
342                 }
343             }
344 
345             return false;
346         }
347 
348         /**
349          * Check if a character is escaped (i.e. if the previous character is an escape)
350          *
351          * @param buffer
352          *          the buffer to check in
353          * @param pos
354          *          the position of the character to check
355          * @return true if the character at the specified position in the given buffer is an escape character and the character immediately preceding it is not an
356          *         escape character.
357          */
358         public boolean isEscaped(final CharSequence buffer, final int pos) {
359             if (pos <= 0) {
360                 return false;
361             }
362 
363             return isEscapeChar(buffer, pos - 1);
364         }
365 
366         /**
367          * Returns true if the character at the specified position if a delimiter. This method will only be called if
368          * the character is not enclosed in any of the {@link #getQuoteChars}, and is not escaped by ant of the
369          * {@link #getEscapeChars}. To perform escaping manually, override {@link #isDelimiter} instead.
370          */
371         public abstract boolean isDelimiterChar(CharSequence buffer, int pos);
372     }
373 
374     /**
375      * {@link ArgumentCompleter.ArgumentDelimiter} implementation that counts all whitespace (as reported by
376      * {@link Character#isWhitespace}) as being a delimiter.
377      *
378      * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
379      */
380     public static class WhitespaceArgumentDelimiter
381         extends AbstractArgumentDelimiter
382     {
383         /**
384          * The character is a delimiter if it is whitespace, and the
385          * preceding character is not an escape character.
386          */
387         @Override
388         public boolean isDelimiterChar(final CharSequence buffer, final int pos) {
389             return Character.isWhitespace(buffer.charAt(pos));
390         }
391     }
392 
393     /**
394      * The result of a delimited buffer.
395      *
396      * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
397      */
398     public static class ArgumentList
399     {
400         private String[] arguments;
401 
402         private int cursorArgumentIndex;
403 
404         private int argumentPosition;
405 
406         private int bufferPosition;
407 
408         /**
409          * @param arguments             The array of tokens
410          * @param cursorArgumentIndex   The token index of the cursor
411          * @param argumentPosition      The position of the cursor in the current token
412          * @param bufferPosition        The position of the cursor in the whole buffer
413          */
414         public ArgumentList(final String[] arguments, final int cursorArgumentIndex, final int argumentPosition, final int bufferPosition) {
415             this.arguments = checkNotNull(arguments);
416             this.cursorArgumentIndex = cursorArgumentIndex;
417             this.argumentPosition = argumentPosition;
418             this.bufferPosition = bufferPosition;
419         }
420 
421         public void setCursorArgumentIndex(final int i) {
422             this.cursorArgumentIndex = i;
423         }
424 
425         public int getCursorArgumentIndex() {
426             return this.cursorArgumentIndex;
427         }
428 
429         public String getCursorArgument() {
430             if ((cursorArgumentIndex < 0) || (cursorArgumentIndex >= arguments.length)) {
431                 return null;
432             }
433 
434             return arguments[cursorArgumentIndex];
435         }
436 
437         public void setArgumentPosition(final int pos) {
438             this.argumentPosition = pos;
439         }
440 
441         public int getArgumentPosition() {
442             return this.argumentPosition;
443         }
444 
445         public void setArguments(final String[] arguments) {
446             this.arguments = arguments;
447         }
448 
449         public String[] getArguments() {
450             return this.arguments;
451         }
452 
453         public void setBufferPosition(final int pos) {
454             this.bufferPosition = pos;
455         }
456 
457         public int getBufferPosition() {
458             return this.bufferPosition;
459         }
460     }
461 }