CommandLine.java revision 4002:4a4fd9ecca20
1/* 2 * Copyright (c) 1999, 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26package com.sun.tools.javac.main; 27 28import java.io.IOException; 29import java.io.Reader; 30import java.nio.file.Files; 31import java.nio.file.Paths; 32import java.util.ArrayList; 33import java.util.Arrays; 34import java.util.List; 35 36/** 37 * Various utility methods for processing Java tool command line arguments. 38 * 39 * <p><b>This is NOT part of any supported API. 40 * If you write code that depends on this, you do so at your own risk. 41 * This code and its internal interfaces are subject to change or 42 * deletion without notice.</b> 43 */ 44public class CommandLine { 45 /** 46 * Process Win32-style command files for the specified command line 47 * arguments and return the resulting arguments. A command file argument 48 * is of the form '@file' where 'file' is the name of the file whose 49 * contents are to be parsed for additional arguments. The contents of 50 * the command file are parsed using StreamTokenizer and the original 51 * '@file' argument replaced with the resulting tokens. Recursive command 52 * files are not supported. The '@' character itself can be quoted with 53 * the sequence '@@'. 54 * @param args the arguments that may contain @files 55 * @return the arguments, with @files expanded 56 * @throws IOException if there is a problem reading any of the @files 57 */ 58 public static String[] parse(String[] args) throws IOException { 59 List<String> newArgs = new ArrayList<>(); 60 appendParsedCommandArgs(newArgs, Arrays.asList(args)); 61 return newArgs.toArray(new String[newArgs.size()]); 62 } 63 64 private static void appendParsedCommandArgs(List<String> newArgs, List<String> args) throws IOException { 65 for (String arg : args) { 66 if (arg.length() > 1 && arg.charAt(0) == '@') { 67 arg = arg.substring(1); 68 if (arg.charAt(0) == '@') { 69 newArgs.add(arg); 70 } else { 71 loadCmdFile(arg, newArgs); 72 } 73 } else { 74 newArgs.add(arg); 75 } 76 } 77 } 78 79 /** 80 * Process the given environment variable and appends any Win32-style 81 * command files for the specified command line arguments and return 82 * the resulting arguments. A command file argument 83 * is of the form '@file' where 'file' is the name of the file whose 84 * contents are to be parsed for additional arguments. The contents of 85 * the command file are parsed using StreamTokenizer and the original 86 * '@file' argument replaced with the resulting tokens. Recursive command 87 * files are not supported. The '@' character itself can be quoted with 88 * the sequence '@@'. 89 * @param envVariable the env variable to process 90 * @param args the arguments that may contain @files 91 * @return the arguments, with environment variable's content and expansion of @files 92 * @throws IOException if there is a problem reading any of the @files 93 * @throws com.sun.tools.javac.main.CommandLine.UnmatchedQuote 94 */ 95 public static List<String> parse(String envVariable, List<String> args) 96 throws IOException, UnmatchedQuote { 97 98 List<String> inArgs = new ArrayList<>(); 99 appendParsedEnvVariables(inArgs, envVariable); 100 inArgs.addAll(args); 101 List<String> newArgs = new ArrayList<>(); 102 appendParsedCommandArgs(newArgs, inArgs); 103 return newArgs; 104 } 105 106 /** 107 * Process the given environment variable and appends any Win32-style 108 * command files for the specified command line arguments and return 109 * the resulting arguments. A command file argument 110 * is of the form '@file' where 'file' is the name of the file whose 111 * contents are to be parsed for additional arguments. The contents of 112 * the command file are parsed using StreamTokenizer and the original 113 * '@file' argument replaced with the resulting tokens. Recursive command 114 * files are not supported. The '@' character itself can be quoted with 115 * the sequence '@@'. 116 * @param envVariable the env variable to process 117 * @param args the arguments that may contain @files 118 * @return the arguments, with environment variable's content and expansion of @files 119 * @throws IOException if there is a problem reading any of the @files 120 * @throws com.sun.tools.javac.main.CommandLine.UnmatchedQuote 121 */ 122 public static String[] parse(String envVariable, String[] args) throws IOException, UnmatchedQuote { 123 List<String> out = parse(envVariable, Arrays.asList(args)); 124 return out.toArray(new String[out.size()]); 125 } 126 127 private static void loadCmdFile(String name, List<String> args) throws IOException { 128 try (Reader r = Files.newBufferedReader(Paths.get(name))) { 129 Tokenizer t = new Tokenizer(r); 130 String s; 131 while ((s = t.nextToken()) != null) { 132 args.add(s); 133 } 134 } 135 } 136 137 public static class Tokenizer { 138 private final Reader in; 139 private int ch; 140 141 public Tokenizer(Reader in) throws IOException { 142 this.in = in; 143 ch = in.read(); 144 } 145 146 public String nextToken() throws IOException { 147 skipWhite(); 148 if (ch == -1) { 149 return null; 150 } 151 152 StringBuilder sb = new StringBuilder(); 153 char quoteChar = 0; 154 155 while (ch != -1) { 156 switch (ch) { 157 case ' ': 158 case '\t': 159 case '\f': 160 if (quoteChar == 0) { 161 return sb.toString(); 162 } 163 sb.append((char) ch); 164 break; 165 166 case '\n': 167 case '\r': 168 return sb.toString(); 169 170 case '\'': 171 case '"': 172 if (quoteChar == 0) { 173 quoteChar = (char) ch; 174 } else if (quoteChar == ch) { 175 quoteChar = 0; 176 } else { 177 sb.append((char) ch); 178 } 179 break; 180 181 case '\\': 182 if (quoteChar != 0) { 183 ch = in.read(); 184 switch (ch) { 185 case '\n': 186 case '\r': 187 while (ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t' || ch == '\f') { 188 ch = in.read(); 189 } 190 continue; 191 192 case 'n': 193 ch = '\n'; 194 break; 195 case 'r': 196 ch = '\r'; 197 break; 198 case 't': 199 ch = '\t'; 200 break; 201 case 'f': 202 ch = '\f'; 203 break; 204 } 205 } 206 sb.append((char) ch); 207 break; 208 209 default: 210 sb.append((char) ch); 211 } 212 213 ch = in.read(); 214 } 215 216 return sb.toString(); 217 } 218 219 void skipWhite() throws IOException { 220 while (ch != -1) { 221 switch (ch) { 222 case ' ': 223 case '\t': 224 case '\n': 225 case '\r': 226 case '\f': 227 break; 228 229 case '#': 230 ch = in.read(); 231 while (ch != '\n' && ch != '\r' && ch != -1) { 232 ch = in.read(); 233 } 234 break; 235 236 default: 237 return; 238 } 239 240 ch = in.read(); 241 } 242 } 243 } 244 245 @SuppressWarnings("fallthrough") 246 private static void appendParsedEnvVariables(List<String> newArgs, String envVariable) 247 throws UnmatchedQuote { 248 249 if (envVariable == null) { 250 return; 251 } 252 String in = System.getenv(envVariable); 253 if (in == null || in.trim().isEmpty()) { 254 return; 255 } 256 257 final char NUL = (char)0; 258 final int len = in.length(); 259 260 int pos = 0; 261 StringBuilder sb = new StringBuilder(); 262 char quote = NUL; 263 char ch; 264 265 loop: 266 while (pos < len) { 267 ch = in.charAt(pos); 268 switch (ch) { 269 case '\"': case '\'': 270 if (quote == NUL) { 271 quote = ch; 272 } else if (quote == ch) { 273 quote = NUL; 274 } else { 275 sb.append(ch); 276 } 277 pos++; 278 break; 279 case '\f': case '\n': case '\r': case '\t': case ' ': 280 if (quote == NUL) { 281 newArgs.add(sb.toString()); 282 sb.setLength(0); 283 while (ch == '\f' || ch == '\n' || ch == '\r' || ch == '\t' || ch == ' ') { 284 pos++; 285 if (pos >= len) { 286 break loop; 287 } 288 ch = in.charAt(pos); 289 } 290 break; 291 } 292 // fall through 293 default: 294 sb.append(ch); 295 pos++; 296 } 297 } 298 if (sb.length() != 0) { 299 newArgs.add(sb.toString()); 300 } 301 if (quote != NUL) { 302 throw new UnmatchedQuote(envVariable); 303 } 304 } 305 306 public static class UnmatchedQuote extends Exception { 307 private static final long serialVersionUID = 0; 308 309 public final String variableName; 310 311 UnmatchedQuote(String variable) { 312 this.variableName = variable; 313 } 314 } 315} 316