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