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 |, &, 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}