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