1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.base;
7 import android.text.TextUtils;
8 import android.util.Log;
11 import java.io.FileInputStream;
12 import java.io.FileNotFoundException;
13 import java.io.IOException;
14 import java.io.InputStreamReader;
15 import java.io.Reader;
16 import java.util.ArrayList;
17 import java.util.Arrays;
18 import java.util.HashMap;
19 import java.util.concurrent.atomic.AtomicReference;
22 * Java mirror of base/command_line.h.
23 * Android applications don't have command line arguments. Instead, they're "simulated" by reading a
24 * file at a specific location early during startup. Applications each define their own files, e.g.,
25 * ContentShellActivity.COMMAND_LINE_FILE or ChromeShellApplication.COMMAND_LINE_FILE.
27 public abstract class CommandLine {
28 // Public abstract interface, implemented in derived classes.
29 // All these methods reflect their native-side counterparts.
31 * Returns true if this command line contains the given switch.
32 * (Switch names ARE case-sensitive).
34 public abstract boolean hasSwitch(String switchString);
37 * Return the value associated with the given switch, or null.
38 * @param switchString The switch key to lookup. It should NOT start with '--' !
39 * @return switch value, or null if the switch is not set or set to empty.
41 public abstract String getSwitchValue(String switchString);
44 * Return the value associated with the given switch, or {@code defaultValue} if the switch
46 * @param switchString The switch key to lookup. It should NOT start with '--' !
47 * @param defaultValue The default value to return if the switch isn't set.
48 * @return Switch value, or {@code defaultValue} if the switch is not set or set to empty.
50 public String getSwitchValue(String switchString, String defaultValue) {
51 String value = getSwitchValue(switchString);
52 return TextUtils.isEmpty(value) ? defaultValue : value;
56 * Append a switch to the command line. There is no guarantee
57 * this action happens before the switch is needed.
58 * @param switchString the switch to add. It should NOT start with '--' !
60 public abstract void appendSwitch(String switchString);
63 * Append a switch and value to the command line. There is no
64 * guarantee this action happens before the switch is needed.
65 * @param switchString the switch to add. It should NOT start with '--' !
66 * @param value the value for this switch.
67 * For example, --foo=bar becomes 'foo', 'bar'.
69 public abstract void appendSwitchWithValue(String switchString, String value);
72 * Append switch/value items in "command line" format (excluding argv[0] program name).
73 * E.g. { '--gofast', '--username=fred' }
74 * @param array an array of switch or switch/value items in command line format.
75 * Unlike the other append routines, these switches SHOULD start with '--' .
76 * Unlike init(), this does not include the program name in array[0].
78 public abstract void appendSwitchesAndArguments(String[] array);
81 * Determine if the command line is bound to the native (JNI) implementation.
82 * @return true if the underlying implementation is delegating to the native command line.
84 public boolean isNativeImplementation() {
88 private static final AtomicReference<CommandLine> sCommandLine =
89 new AtomicReference<CommandLine>();
92 * @returns true if the command line has already been initialized.
94 public static boolean isInitialized() {
95 return sCommandLine.get() != null;
98 // Equivalent to CommandLine::ForCurrentProcess in C++.
99 public static CommandLine getInstance() {
100 CommandLine commandLine = sCommandLine.get();
101 assert commandLine != null;
106 * Initialize the singleton instance, must be called exactly once (either directly or
107 * via one of the convenience wrappers below) before using the static singleton instance.
108 * @param args command line flags in 'argv' format: args[0] is the program name.
110 public static void init(String[] args) {
111 setInstance(new JavaCommandLine(args));
115 * Initialize the command line from the command-line file.
117 * @param file The fully qualified command line file.
119 public static void initFromFile(String file) {
120 // Arbitrary clamp of 8k on the amount of file we read in.
121 char[] buffer = readUtf8FileFully(file, 8 * 1024);
122 init(buffer == null ? null : tokenizeQuotedAruments(buffer));
126 * Resets both the java proxy and the native command lines. This allows the entire
127 * command line initialization to be re-run including the call to onJniLoaded.
129 public static void reset() {
134 * Public for testing (TODO: why are the tests in a different package?)
135 * Parse command line flags from a flat buffer, supporting double-quote enclosed strings
136 * containing whitespace. argv elements are derived by splitting the buffer on whitepace;
137 * double quote characters may enclose tokens containing whitespace; a double-quote literal
138 * may be escaped with back-slash. (Otherwise backslash is taken as a literal).
139 * @param buffer A command line in command line file format as described above.
140 * @return the tokenized arguments, suitable for passing to init().
142 public static String[] tokenizeQuotedAruments(char[] buffer) {
143 ArrayList<String> args = new ArrayList<String>();
144 StringBuilder arg = null;
145 final char noQuote = '\0';
146 final char singleQuote = '\'';
147 final char doubleQuote = '"';
148 char currentQuote = noQuote;
149 for (char c : buffer) {
150 // Detect start or end of quote block.
151 if ((currentQuote == noQuote && (c == singleQuote || c == doubleQuote)) ||
153 if (arg != null && arg.length() > 0 && arg.charAt(arg.length() - 1) == '\\') {
154 // Last char was a backslash; pop it, and treat c as a literal.
155 arg.setCharAt(arg.length() - 1, c);
157 currentQuote = currentQuote == noQuote ? c : noQuote;
159 } else if (currentQuote == noQuote && Character.isWhitespace(c)) {
161 args.add(arg.toString());
165 if (arg == null) arg = new StringBuilder();
170 if (currentQuote != noQuote) {
171 Log.w(TAG, "Unterminated quoted string: " + arg);
173 args.add(arg.toString());
175 return args.toArray(new String[args.size()]);
178 private static final String TAG = "CommandLine";
179 private static final String SWITCH_PREFIX = "--";
180 private static final String SWITCH_TERMINATOR = SWITCH_PREFIX;
181 private static final String SWITCH_VALUE_SEPARATOR = "=";
183 public static void enableNativeProxy() {
184 // Make a best-effort to ensure we make a clean (atomic) switch over from the old to
185 // the new command line implementation. If another thread is modifying the command line
186 // when this happens, all bets are off. (As per the native CommandLine).
187 sCommandLine.set(new NativeCommandLine());
190 public static String[] getJavaSwitchesOrNull() {
191 CommandLine commandLine = sCommandLine.get();
192 if (commandLine != null) {
193 assert !commandLine.isNativeImplementation();
194 return ((JavaCommandLine) commandLine).getCommandLineArguments();
199 private static void setInstance(CommandLine commandLine) {
200 CommandLine oldCommandLine = sCommandLine.getAndSet(commandLine);
201 if (oldCommandLine != null && oldCommandLine.isNativeImplementation()) {
207 * @param fileName the file to read in.
208 * @param sizeLimit cap on the file size.
209 * @return Array of chars read from the file, or null if the file cannot be read
210 * or if its length exceeds |sizeLimit|.
212 private static char[] readUtf8FileFully(String fileName, int sizeLimit) {
213 Reader reader = null;
214 File f = new File(fileName);
215 long fileLength = f.length();
217 if (fileLength == 0) {
221 if (fileLength > sizeLimit) {
222 Log.w(TAG, "File " + fileName + " length " + fileLength + " exceeds limit "
228 char[] buffer = new char[(int) fileLength];
229 reader = new InputStreamReader(new FileInputStream(f), "UTF-8");
230 int charsRead = reader.read(buffer);
231 // Debug check that we've exhausted the input stream (will fail e.g. if the
232 // file grew after we inspected its length).
233 assert !reader.ready();
234 return charsRead < buffer.length ? Arrays.copyOfRange(buffer, 0, charsRead) : buffer;
235 } catch (FileNotFoundException e) {
237 } catch (IOException e) {
241 if (reader != null) reader.close();
242 } catch (IOException e) {
243 Log.e(TAG, "Unable to close file reader.", e);
248 private CommandLine() {}
250 private static class JavaCommandLine extends CommandLine {
251 private HashMap<String, String> mSwitches = new HashMap<String, String>();
252 private ArrayList<String> mArgs = new ArrayList<String>();
254 // The arguments begin at index 1, since index 0 contains the executable name.
255 private int mArgsBegin = 1;
257 JavaCommandLine(String[] args) {
258 if (args == null || args.length == 0 || args[0] == null) {
262 appendSwitchesInternal(args, 1);
264 // Invariant: we always have the argv[0] program name element.
265 assert mArgs.size() > 0;
269 * Returns the switches and arguments passed into the program, with switches and their
270 * values coming before all of the arguments.
272 private String[] getCommandLineArguments() {
273 return mArgs.toArray(new String[mArgs.size()]);
277 public boolean hasSwitch(String switchString) {
278 return mSwitches.containsKey(switchString);
282 public String getSwitchValue(String switchString) {
283 // This is slightly round about, but needed for consistency with the NativeCommandLine
284 // version which does not distinguish empty values from key not present.
285 String value = mSwitches.get(switchString);
286 return value == null || value.isEmpty() ? null : value;
290 public void appendSwitch(String switchString) {
291 appendSwitchWithValue(switchString, null);
295 * Appends a switch to the current list.
296 * @param switchString the switch to add. It should NOT start with '--' !
297 * @param value the value for this switch.
300 public void appendSwitchWithValue(String switchString, String value) {
301 mSwitches.put(switchString, value == null ? "" : value);
303 // Append the switch and update the switches/arguments divider mArgsBegin.
304 String combinedSwitchString = SWITCH_PREFIX + switchString;
305 if (value != null && !value.isEmpty())
306 combinedSwitchString += SWITCH_VALUE_SEPARATOR + value;
308 mArgs.add(mArgsBegin++, combinedSwitchString);
312 public void appendSwitchesAndArguments(String[] array) {
313 appendSwitchesInternal(array, 0);
316 // Add the specified arguments, but skipping the first |skipCount| elements.
317 private void appendSwitchesInternal(String[] array, int skipCount) {
318 boolean parseSwitches = true;
319 for (String arg : array) {
325 if (arg.equals(SWITCH_TERMINATOR)) {
326 parseSwitches = false;
329 if (parseSwitches && arg.startsWith(SWITCH_PREFIX)) {
330 String[] parts = arg.split(SWITCH_VALUE_SEPARATOR, 2);
331 String value = parts.length > 1 ? parts[1] : null;
332 appendSwitchWithValue(parts[0].substring(SWITCH_PREFIX.length()), value);
340 private static class NativeCommandLine extends CommandLine {
342 public boolean hasSwitch(String switchString) {
343 return nativeHasSwitch(switchString);
347 public String getSwitchValue(String switchString) {
348 return nativeGetSwitchValue(switchString);
352 public void appendSwitch(String switchString) {
353 nativeAppendSwitch(switchString);
357 public void appendSwitchWithValue(String switchString, String value) {
358 nativeAppendSwitchWithValue(switchString, value);
362 public void appendSwitchesAndArguments(String[] array) {
363 nativeAppendSwitchesAndArguments(array);
367 public boolean isNativeImplementation() {
372 private static native void nativeReset();
373 private static native boolean nativeHasSwitch(String switchString);
374 private static native String nativeGetSwitchValue(String switchString);
375 private static native void nativeAppendSwitch(String switchString);
376 private static native void nativeAppendSwitchWithValue(String switchString, String value);
377 private static native void nativeAppendSwitchesAndArguments(String[] array);