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