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