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