001/* 002 * Copyright 2010-2020 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2010-2020 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-2020 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 |, &, 086 * ;, (, ), !, ", ', *, ?, $, and `. 087 */ 088@ThreadSafety(level = ThreadSafetyLevel.COMPLETELY_THREADSAFE) 089public final class ExampleCommandLineArgument implements Serializable 090{ 091 private static final long serialVersionUID = 2468880329239320437L; 092 093 // The argument that was passed in originally. 094 private final String rawForm; 095 096 // The Unix form of the argument. 097 private final String unixForm; 098 099 // The Windows form of the argument. 100 private final String windowsForm; 101 102 103 104 /** 105 * Private constructor. 106 * 107 * @param rawForm The original raw form of the command line argument. 108 * @param unixForm The Unix form of the argument. 109 * @param windowsForm The Windows form of the argument. 110 */ 111 private ExampleCommandLineArgument(final String rawForm, 112 final String unixForm, 113 final String windowsForm) 114 { 115 this.rawForm = rawForm; 116 this.unixForm = unixForm; 117 this.windowsForm = windowsForm; 118 } 119 120 121 122 /** 123 * Return the original, unquoted raw form of the argument. This is what 124 * was passed into the {@link #getCleanArgument} method. 125 * 126 * @return The original, unquoted form of the argument. 127 */ 128 public String getRawForm() 129 { 130 return rawForm; 131 } 132 133 134 135 /** 136 * Return the form of the argument that is safe to use in a Unix command 137 * line shell. 138 * 139 * @return The form of the argument that is safe to use in a Unix command 140 * line shell. 141 */ 142 public String getUnixForm() 143 { 144 return unixForm; 145 } 146 147 148 149 /** 150 * Return the form of the argument that is safe to use in a Windows command 151 * line shell. 152 * 153 * @return The form of the argument that is safe to use in a Windows command 154 * line shell. 155 */ 156 public String getWindowsForm() 157 { 158 return windowsForm; 159 } 160 161 162 163 /** 164 * Return the form of the argument that is safe to use in the command line 165 * shell of the current operating system platform. 166 * 167 * @return The form of the argument that is safe to use in a command line 168 * shell of the current operating system platform. 169 */ 170 public String getLocalForm() 171 { 172 if (StaticUtils.isWindows()) 173 { 174 return getWindowsForm(); 175 } 176 else 177 { 178 return getUnixForm(); 179 } 180 } 181 182 183 184 /** 185 * Return a clean form of the specified argument that can be used directly 186 * on the command line. 187 * 188 * @param argument The raw argument to convert into a clean form that can 189 * be used directly on the command line. 190 * 191 * @return The ExampleCommandLineArgument for the specified argument. 192 */ 193 public static ExampleCommandLineArgument getCleanArgument( 194 final String argument) 195 { 196 return new ExampleCommandLineArgument(argument, 197 getUnixForm(argument), 198 getWindowsForm(argument)); 199 } 200 201 202 203 /** 204 * Return a clean form of the specified argument that can be used directly 205 * on a Unix command line. 206 * 207 * @param argument The raw argument to convert into a clean form that can 208 * be used directly on the Unix command line. 209 * 210 * @return A form of the specified argument that is clean for us on a Unix 211 * command line. 212 */ 213 public static String getUnixForm(final String argument) 214 { 215 Validator.ensureNotNull(argument); 216 217 final QuotingRequirements requirements = getRequiredUnixQuoting(argument); 218 219 String quotedArgument = argument; 220 if (requirements.requiresSingleQuotesOnUnix()) 221 { 222 if (requirements.includesSingleQuote()) 223 { 224 // On the primary Unix shells (e.g. bash), single-quote cannot be 225 // included in a single-quoted string. So it has to be specified 226 // outside of the quoted part, and has to be included in "" itself. 227 quotedArgument = quotedArgument.replace("'", "'\"'\"'"); 228 } 229 quotedArgument = '\'' + quotedArgument + '\''; 230 } 231 else if (requirements.requiresDoubleQuotesOnUnix()) 232 { 233 quotedArgument = '"' + quotedArgument + '"'; 234 } 235 236 return quotedArgument; 237 } 238 239 240 241 /** 242 * Return a clean form of the specified argument that can be used directly 243 * on a Windows command line. 244 * 245 * @param argument The raw argument to convert into a clean form that can 246 * be used directly on the Windows command line. 247 * 248 * @return A form of the specified argument that is clean for us on a Windows 249 * command line. 250 */ 251 public static String getWindowsForm(final String argument) 252 { 253 Validator.ensureNotNull(argument); 254 255 final QuotingRequirements requirements = getRequiredUnixQuoting(argument); 256 257 String quotedArgument = argument; 258 259 // Windows only supports double-quotes. They are treated much more like 260 // single-quotes on Unix. Only " needs to be escaped, and it's done by 261 // repeating it, i.e. """"" gets passed into the program as just " 262 if (requirements.requiresSingleQuotesOnUnix() || 263 requirements.requiresDoubleQuotesOnUnix()) 264 { 265 if (requirements.includesDoubleQuote()) 266 { 267 quotedArgument = quotedArgument.replace("\"", "\"\""); 268 } 269 quotedArgument = '"' + quotedArgument + '"'; 270 } 271 272 return quotedArgument; 273 } 274 275 276 277 /** 278 * Return a list of raw parameters that were parsed from the specified String. 279 * This can be used to undo the quoting that was done by 280 * {@link #getCleanArgument}. It perfectly handles any String that was 281 * passed into this method, but it won't behave exactly as any single shell 282 * behaves because they aren't consistent. For instance, it will never 283 * treat \\ as an escape character. 284 * 285 * @param exampleCommandLine The command line to parse. 286 * 287 * @return A list of raw arguments that were parsed from the specified 288 * example usage command line. 289 */ 290 public static List<String> parseExampleCommandLine( 291 final String exampleCommandLine) 292 { 293 Validator.ensureNotNull(exampleCommandLine); 294 295 boolean inDoubleQuote = false; 296 boolean inSingleQuote = false; 297 298 final List<String> args = new ArrayList<>(20); 299 300 StringBuilder currentArg = new StringBuilder(); 301 boolean inArg = false; 302 for (int i = 0; i < exampleCommandLine.length(); i++) { 303 final Character c = exampleCommandLine.charAt(i); 304 305 Character nextChar = null; 306 if (i < (exampleCommandLine.length() - 1)) 307 { 308 nextChar = exampleCommandLine.charAt(i + 1); 309 } 310 311 if (inDoubleQuote) 312 { 313 if (c == '"') 314 { 315 if ((nextChar != null) && (nextChar == '"')) 316 { 317 // Handle the special case on Windows where a " is escaped inside 318 // of double-quotes using "", i.e. to get " passed into the program, 319 // """" must be specified. 320 currentArg.append('\"'); 321 i++; 322 } 323 else 324 { 325 inDoubleQuote = false; 326 } 327 } 328 else 329 { 330 currentArg.append(c); 331 } 332 } 333 else if (inSingleQuote) 334 { 335 if (c == '\'') 336 { 337 inSingleQuote = false; 338 } 339 else 340 { 341 currentArg.append(c); 342 } 343 } 344 else if (c == '"') 345 { 346 inDoubleQuote = true; 347 inArg = true; 348 } 349 else if (c == '\'') 350 { 351 inSingleQuote = true; 352 inArg = true; 353 } 354 else if ((c == ' ') || (c == '\t')) 355 { 356 if (inArg) 357 { 358 args.add(currentArg.toString()); 359 currentArg = new StringBuilder(); 360 inArg = false; 361 } 362 } 363 else 364 { 365 currentArg.append(c); 366 inArg = true; 367 } 368 } 369 370 if (inArg) 371 { 372 args.add(currentArg.toString()); 373 } 374 375 return args; 376 } 377 378 379 380 /** 381 * Examines the specified argument to determine how it will need to be 382 * quoted. 383 * 384 * @param argument The argument to examine. 385 * 386 * @return The QuotingRequirements for the specified argument. 387 */ 388 private static QuotingRequirements getRequiredUnixQuoting( 389 final String argument) 390 { 391 boolean requiresDoubleQuotes = false; 392 boolean requiresSingleQuotes = false; 393 boolean includesDoubleQuote = false; 394 boolean includesSingleQuote = false; 395 396 if (argument.isEmpty()) 397 { 398 requiresDoubleQuotes = true; 399 } 400 401 for (int i=0; i < argument.length(); i++) 402 { 403 final char c = argument.charAt(i); 404 switch (c) 405 { 406 case '"': 407 includesDoubleQuote = true; 408 requiresSingleQuotes = true; 409 break; 410 case '\\': 411 case '!': 412 case '`': 413 case '$': 414 case '@': 415 case '*': 416 requiresSingleQuotes = true; 417 break; 418 419 case '\'': 420 includesSingleQuote = true; 421 requiresDoubleQuotes = true; 422 break; 423 case ' ': 424 case '|': 425 case '&': 426 case ';': 427 case '(': 428 case ')': 429 case '<': 430 case '>': 431 requiresDoubleQuotes = true; 432 break; 433 434 case ',': 435 case '=': 436 case '-': 437 case '_': 438 case ':': 439 case '.': 440 case '/': 441 // These are safe, so just ignore them. 442 break; 443 444 default: 445 if (((c >= 'a') && (c <= 'z')) || 446 ((c >= 'A') && (c <= 'Z')) || 447 ((c >= '0') && (c <= '9'))) 448 { 449 // These are safe, so just ignore them. 450 } 451 else 452 { 453 requiresDoubleQuotes = true; 454 } 455 } 456 } 457 458 if (requiresSingleQuotes) 459 { 460 // Single-quoting trumps double-quotes. 461 requiresDoubleQuotes = false; 462 } 463 464 return new QuotingRequirements(requiresSingleQuotes, requiresDoubleQuotes, 465 includesSingleQuote, includesDoubleQuote); 466 } 467}