001/*
002 * Copyright 2010-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2010-2018 Ping Identity Corporation
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.util;
022
023
024
025import java.util.List;
026import java.util.ArrayList;
027import java.io.Serializable;
028
029
030
031/**
032 * This class provides access to a form of a command-line argument that is
033 * safe to use in a shell.  It includes both forms for both Unix (bash shell
034 * specifically) and Windows, since there are differences between the two
035 * platforms.  Quoting of arguments is performed with the following goals:
036 *
037 * <UL>
038 *   <LI>The same form should be used for both Unix and Windows whenever
039 *       possible.</LI>
040 *   <LI>If the same form cannot be used for both platforms, then make it
041 *       as easy as possible to convert the form to the other platform.</LI>
042 *   <LI>If neither platform requires quoting of an argument, then it is not
043 *       quoted.</LI>
044 * </UL>
045 *
046 * To that end, here is the approach that we've taken:
047 *
048 * <UL>
049 *   <LI>Characters in the output are never escaped with the \ character
050 *       because Windows does not understand \ used to escape.</LI>
051 *   <LI>On Unix, double-quotes are used to quote whenever possible since
052 *       Windows does not treat single quotes specially.</LI>
053 *   <LI>If a String needs to be quoted on either platform, then it is quoted
054 *       on both.  If it needs to be quoted with single-quotes on Unix, then
055 *       it will be quoted with double quotes on Windows.
056 *   <LI>On Unix, single-quote presents a problem if it's included in a
057 *       string that needs to be singled-quoted, for instance one that includes
058 *       the $ or ! characters.  In this case, we have to wrap it in
059 *       double-quotes outside of the single-quotes.  For instance, Server's!
060 *       would end up as 'Server'"'"'s!'.</LI>
061 *   <LI>On Windows, double-quotes present a problem.  They have to be
062 *       escaped using two double-quotes inside of a double-quoted string.
063 *       For instance "Quoted" ends up as """Quoted""".</LI>
064 * </UL>
065 *
066 * All of the forms can be unambiguously parsed using the
067 * {@link #parseExampleCommandLine} method regardless of the platform.  This
068 * method can be used when needing to parse a command line that was generated
069 * by this class outside of a shell environment, e.g. if the full command line
070 * was read from a file.  Special characters that are escaped include |, &amp;,
071 * ;, (, ), !, ", ', *, ?, $, and `.
072 */
073@ThreadSafety(level = ThreadSafetyLevel.COMPLETELY_THREADSAFE)
074public final class ExampleCommandLineArgument implements Serializable
075{
076  private static final long serialVersionUID = 2468880329239320437L;
077
078  // The argument that was passed in originally.
079  private final String rawForm;
080
081  // The Unix form of the argument.
082  private final String unixForm;
083
084  // The Windows form of the argument.
085  private final String windowsForm;
086
087
088
089  /**
090   * Private constructor.
091   *
092   * @param  rawForm      The original raw form of the command line argument.
093   * @param  unixForm     The Unix form of the argument.
094   * @param  windowsForm  The Windows form of the argument.
095   */
096  private ExampleCommandLineArgument(final String rawForm,
097                                     final String unixForm,
098                                     final String windowsForm)
099  {
100    this.rawForm = rawForm;
101    this.unixForm     = unixForm;
102    this.windowsForm  = windowsForm;
103  }
104
105
106
107  /**
108   * Return the original, unquoted raw form of the argument.  This is what
109   * was passed into the {@link #getCleanArgument} method.
110   *
111   * @return  The original, unquoted form of the argument.
112   */
113  public String getRawForm()
114  {
115    return rawForm;
116  }
117
118
119
120  /**
121   * Return the form of the argument that is safe to use in a Unix command
122   * line shell.
123   *
124   * @return  The form of the argument that is safe to use in a Unix command
125   *          line shell.
126   */
127  public String getUnixForm()
128  {
129    return unixForm;
130  }
131
132
133
134  /**
135   * Return the form of the argument that is safe to use in a Windows command
136   * line shell.
137   *
138   * @return  The form of the argument that is safe to use in a Windows command
139   *          line shell.
140   */
141  public String getWindowsForm()
142  {
143    return windowsForm;
144  }
145
146
147
148  /**
149   * Return the form of the argument that is safe to use in the command line
150   * shell of the current operating system platform.
151   *
152   * @return  The form of the argument that is safe to use in a command line
153   *          shell of the current operating system platform.
154   */
155  public String getLocalForm()
156  {
157    if (StaticUtils.isWindows())
158    {
159      return getWindowsForm();
160    }
161    else
162    {
163      return getUnixForm();
164    }
165  }
166
167
168
169  /**
170   * Return a clean form of the specified argument that can be used directly
171   * on the command line.
172   *
173   * @param  argument  The raw argument to convert into a clean form that can
174   *                   be used directly on the command line.
175   *
176   * @return  The ExampleCommandLineArgument for the specified argument.
177   */
178  public static ExampleCommandLineArgument getCleanArgument(
179                                             final String argument)
180  {
181    return new ExampleCommandLineArgument(argument,
182                                          getUnixForm(argument),
183                                          getWindowsForm(argument));
184  }
185
186
187
188  /**
189   * Return a clean form of the specified argument that can be used directly
190   * on a Unix command line.
191   *
192   * @param  argument  The raw argument to convert into a clean form that can
193   *                   be used directly on the Unix command line.
194   *
195   * @return  A form of the specified argument that is clean for us on a Unix
196   *          command line.
197   */
198  public static String getUnixForm(final String argument)
199  {
200    Validator.ensureNotNull(argument);
201
202    final QuotingRequirements requirements = getRequiredUnixQuoting(argument);
203
204    String quotedArgument = argument;
205    if (requirements.requiresSingleQuotesOnUnix())
206    {
207      if (requirements.includesSingleQuote())
208      {
209        // On the primary Unix shells (e.g. bash), single-quote cannot be
210        // included in a single-quoted string.  So it has to be specified
211        // outside of the quoted part, and has to be included in "" itself.
212        quotedArgument = quotedArgument.replace("'", "'\"'\"'");
213      }
214      quotedArgument = '\'' + quotedArgument + '\'';
215    }
216    else if (requirements.requiresDoubleQuotesOnUnix())
217    {
218      quotedArgument = '"' + quotedArgument + '"';
219    }
220
221    return quotedArgument;
222  }
223
224
225
226  /**
227   * Return a clean form of the specified argument that can be used directly
228   * on a Windows command line.
229   *
230   * @param  argument  The raw argument to convert into a clean form that can
231   *                   be used directly on the Windows command line.
232   *
233   * @return  A form of the specified argument that is clean for us on a Windows
234   *          command line.
235   */
236  public static String getWindowsForm(final String argument)
237  {
238    Validator.ensureNotNull(argument);
239
240    final QuotingRequirements requirements = getRequiredUnixQuoting(argument);
241
242    String quotedArgument = argument;
243
244    // Windows only supports double-quotes.  They are treated much more like
245    // single-quotes on Unix.  Only " needs to be escaped, and it's done by
246    // repeating it, i.e. """"" gets passed into the program as just "
247    if (requirements.requiresSingleQuotesOnUnix() ||
248        requirements.requiresDoubleQuotesOnUnix())
249    {
250      if (requirements.includesDoubleQuote())
251      {
252        quotedArgument = quotedArgument.replace("\"", "\"\"");
253      }
254      quotedArgument = '"' + quotedArgument + '"';
255    }
256
257    return quotedArgument;
258  }
259
260
261
262  /**
263   * Return a list of raw parameters that were parsed from the specified String.
264   * This can be used to undo the quoting that was done by
265   * {@link #getCleanArgument}.  It perfectly handles any String that was
266   * passed into this method, but it won't behave exactly as any single shell
267   * behaves because they aren't consistent.  For instance, it will never
268   * treat \\ as an escape character.
269   *
270   * @param  exampleCommandLine  The command line to parse.
271   *
272   * @return  A list of raw arguments that were parsed from the specified
273   *          example usage command line.
274   */
275  public static List<String> parseExampleCommandLine(
276                                 final String exampleCommandLine)
277  {
278    Validator.ensureNotNull(exampleCommandLine);
279
280    boolean inDoubleQuote = false;
281    boolean inSingleQuote = false;
282
283    final List<String> args = new ArrayList<>(20);
284
285    StringBuilder currentArg = new StringBuilder();
286    boolean inArg = false;
287    for (int i = 0; i < exampleCommandLine.length(); i++) {
288      final Character c = exampleCommandLine.charAt(i);
289
290      Character nextChar = null;
291      if (i < (exampleCommandLine.length() - 1))
292      {
293        nextChar = exampleCommandLine.charAt(i + 1);
294      }
295
296      if (inDoubleQuote)
297      {
298        if (c == '"')
299        {
300          if ((nextChar != null) && (nextChar == '"'))
301          {
302            // Handle the special case on Windows where a " is escaped inside
303            // of double-quotes using "", i.e. to get " passed into the program,
304            // """" must be specified.
305            currentArg.append('\"');
306            i++;
307          }
308          else
309          {
310            inDoubleQuote = false;
311          }
312        }
313        else
314        {
315          currentArg.append(c);
316        }
317      }
318      else if (inSingleQuote)
319      {
320        if (c == '\'')
321        {
322          inSingleQuote = false;
323        }
324        else
325        {
326          currentArg.append(c);
327        }
328      }
329      else if (c == '"')
330      {
331        inDoubleQuote = true;
332        inArg = true;
333      }
334      else if (c == '\'')
335      {
336        inSingleQuote = true;
337        inArg = true;
338      }
339      else if ((c == ' ') || (c == '\t'))
340      {
341        if (inArg)
342        {
343          args.add(currentArg.toString());
344          currentArg = new StringBuilder();
345          inArg = false;
346        }
347      }
348      else
349      {
350        currentArg.append(c);
351        inArg = true;
352      }
353    }
354
355    if (inArg)
356    {
357      args.add(currentArg.toString());
358    }
359
360    return args;
361  }
362
363
364
365  /**
366   * Examines the specified argument to determine how it will need to be
367   * quoted.
368   *
369   * @param  argument  The argument to examine.
370   *
371   * @return  The QuotingRequirements for the specified argument.
372   */
373  private static QuotingRequirements getRequiredUnixQuoting(
374                                         final String argument)
375  {
376    boolean requiresDoubleQuotes = false;
377    boolean requiresSingleQuotes = false;
378    boolean includesDoubleQuote = false;
379    boolean includesSingleQuote = false;
380
381    if (argument.isEmpty())
382    {
383      requiresDoubleQuotes = true;
384    }
385
386    for (int i=0; i < argument.length(); i++)
387    {
388      final char c = argument.charAt(i);
389      switch (c)
390      {
391        case '"':
392          includesDoubleQuote = true;
393          requiresSingleQuotes = true;
394          break;
395        case '\\':
396        case '!':
397        case '`':
398        case '$':
399        case '@':
400        case '*':
401          requiresSingleQuotes = true;
402          break;
403
404        case '\'':
405          includesSingleQuote = true;
406          requiresDoubleQuotes = true;
407          break;
408        case ' ':
409        case '|':
410        case '&':
411        case ';':
412        case '(':
413        case ')':
414        case '<':
415        case '>':
416          requiresDoubleQuotes = true;
417          break;
418
419        case ',':
420        case '=':
421        case '-':
422        case '_':
423        case ':':
424        case '.':
425        case '/':
426          // These are safe, so just ignore them.
427          break;
428
429        default:
430          if (((c >= 'a') && (c <= 'z')) ||
431              ((c >= 'A') && (c <= 'Z')) ||
432              ((c >= '0') && (c <= '9')))
433          {
434            // These are safe, so just ignore them.
435          }
436          else
437          {
438            requiresDoubleQuotes = true;
439          }
440      }
441    }
442
443    if (requiresSingleQuotes)
444    {
445      // Single-quoting trumps double-quotes.
446      requiresDoubleQuotes = false;
447    }
448
449    return new QuotingRequirements(requiresSingleQuotes, requiresDoubleQuotes,
450                                   includesSingleQuote, includesDoubleQuote);
451  }
452}