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