Chromium Code Reviews| Index: tools/binary_size/java/src/org/chromium/tools/binary_size/ParallelAddress2Line.java |
| diff --git a/tools/binary_size/java/src/org/chromium/tools/binary_size/ParallelAddress2Line.java b/tools/binary_size/java/src/org/chromium/tools/binary_size/ParallelAddress2Line.java |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..f6131e5efd6f6c478b37806b62eb59f1ddcc1336 |
| --- /dev/null |
| +++ b/tools/binary_size/java/src/org/chromium/tools/binary_size/ParallelAddress2Line.java |
| @@ -0,0 +1,588 @@ |
| +// Copyright 2014 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| +package org.chromium.tools.binary_size; |
| + |
| +import java.io.BufferedReader; |
| +import java.io.File; |
| +import java.io.FileNotFoundException; |
| +import java.io.FileOutputStream; |
| +import java.io.FileReader; |
| +import java.io.IOException; |
| +import java.io.InputStream; |
| +import java.io.OutputStream; |
| +import java.util.Timer; |
| +import java.util.TimerTask; |
| +import java.util.concurrent.CountDownLatch; |
| +import java.util.concurrent.TimeUnit; |
| +import java.util.concurrent.atomic.AtomicBoolean; |
| +import java.util.concurrent.atomic.AtomicInteger; |
| +import java.util.regex.Matcher; |
| +import java.util.regex.Pattern; |
| + |
| +// This tool is intentionally written to be standalone so that it can be |
| +// compiled without reliance upon any other libraries. All that is required |
| +// is a vanilla installation of the Java Runtime Environment, 1.5 or later. |
|
bulach
2014/01/07 19:38:30
nit: move this comment inside the following block?
Andrew Hayden (chromium.org)
2014/01/08 00:56:45
Done.
|
| +/** |
| + * A tool for parallelizing "addr2line" against a given binary. |
| + * The tool runs "nm" to dump the symbols from a library, then spawns a pool |
| + * of addr2name workers that resolve addresses to name in parallel. |
| + */ |
| +public class ParallelAddress2Line { |
| + private final AtomicBoolean stillEnqueuing = new AtomicBoolean(true); |
| + private final AtomicInteger enqueuedCount = |
| + new AtomicInteger(Integer.MAX_VALUE); |
| + private final AtomicInteger doneCount = new AtomicInteger(0); |
| + private final AtomicInteger successCount = new AtomicInteger(0); |
| + private final AtomicInteger addressSkipCount = new AtomicInteger(0); |
| + private final String libraryPath; |
| + private final String nmPath; |
| + private final String nmInPath; |
| + private final String addr2linePath; |
| + private final boolean verbose; |
| + private final boolean noProgress; |
| + private Addr2LineWorkerPool pool; |
| + private final boolean noDedupe; |
| + private final boolean disambiguate; |
| + private final NmDumper nmDumper; |
| + |
| + // Regex for parsing "nm" output. A sample line looks like this: |
| + // 0167b39c 00000018 t ACCESS_DESCRIPTION_free /path/file.c:95 |
| + // |
| + // The fields are: address, size, type, name, source location |
| + // Regular expression explained ( see also: https://xkcd.com/208 ): |
| + // ([0-9a-f]{8}+) The address |
| + // [\\s]+ Whitespace separator |
| + // ([0-9a-f]{8}+) The size. From here on out it's all optional. |
| + // [\\s]+ Whitespace separator |
| + // (\\S?) The symbol type, which is any non-whitespace char |
| + // [\\s*] Whitespace separator |
| + // ([^\\t]*) Symbol name, any non-tab character (spaces ok!) |
| + // [\\t]? Tab separator |
| + // (.*) The location (filename[:linennum|?][ (discriminator n)] |
| + private static final String NM_REGEX = |
|
bulach
2014/01/07 19:38:30
nit: could have this as private "static final Patt
Andrew Hayden (chromium.org)
2014/01/08 00:56:45
Done.
|
| + "([0-9a-f]{8}+)[\\s]+([0-9a-f]{8}+)[\\s]*(\\S?)[\\s*]([^\\t]*)[\\t]?(.*)"; |
| + |
| + private ParallelAddress2Line( |
| + final String libraryPath, |
| + final String nmPath, |
| + final String nmInPath, |
| + final String addr2linePath, |
| + final String outPath, |
| + final String skipPath, |
| + final String failPath, |
| + final boolean verbose, |
| + final boolean noProgress, |
| + final boolean noDedupe, |
| + final boolean disambiguate) { |
| + this.libraryPath = libraryPath; |
| + this.nmPath = nmPath; |
| + this.nmInPath = nmInPath; |
| + this.addr2linePath = addr2linePath; |
| + this.verbose = verbose; |
| + this.noProgress = noProgress; |
| + this.noDedupe = noDedupe; |
| + this.disambiguate = disambiguate; |
| + this.nmDumper = new NmDumper(outPath, failPath, skipPath); |
| + |
| + final File libraryFile = new File(libraryPath); |
| + if (!(libraryFile.exists() && libraryFile.canRead())) { |
| + throw new IllegalStateException("Can't read library file: " + libraryPath); |
| + } |
| + } |
| + |
| + private static final File findFile(File directory, String target) { |
| + for (File file : directory.listFiles()) { |
| + if (file.isDirectory() && file.canRead()) { |
| + File result = findFile(file, target); |
| + if (result != null) return result; |
| + } else { |
| + if (target.equals(file.getName())) return file; |
| + } |
| + } |
| + return null; |
| + } |
| + |
| + private void run(final int addr2linePoolSize) throws InterruptedException { |
| + try { |
| + runInternal(addr2linePoolSize); |
| + } finally { |
| + nmDumper.close(); |
| + } |
| + } |
| + |
| + private void runInternal(final int addr2linePoolSize) throws InterruptedException { |
| + // Step 1: Dump symbols with nm |
| + final String nmOutputPath; |
| + if (nmInPath == null) { |
| + // Generate nm output with nm binary |
| + logVerbose("Running nm to dump symbols from " + libraryPath); |
| + try { |
| + nmOutputPath = dumpSymbols(); |
| + } catch (Exception e) { |
| + throw new RuntimeException("nm failed", e); |
| + } |
| + } else { |
| + // Use user-supplied nm output |
| + logVerbose("Using user-supplied nm file: " + nmInPath); |
| + nmOutputPath = nmInPath; |
| + } |
| + |
| + // Step 2: Prepare addr2line worker pool to process nm output |
| + try { |
| + logVerbose("Creating " + addr2linePoolSize + " workers for " + addr2linePath); |
| + pool = new Addr2LineWorkerPool(addr2linePoolSize, |
| + addr2linePath, libraryPath, disambiguate, !noDedupe); |
| + } catch (IOException e) { |
| + throw new RuntimeException("Couldn't initialize name2address pool!", e); |
| + } |
| + |
| + // Step 3: Spool symbol-processing tasks to workers |
| + final long startTime = System.currentTimeMillis(); |
| + Timer timer = null; |
| + if (!noProgress) { |
| + timer = startTaskMonitor(startTime); |
| + } |
| + final int queued = spoolTasks(nmOutputPath); |
| + |
| + // All tasks |
| + enqueuedCount.set(queued); |
| + stillEnqueuing.set(false); |
| + pool.allRecordsSubmitted(); |
| + float percentAddressesSkipped = 100f * (addressSkipCount.floatValue() |
| + / (queued + addressSkipCount.get())); |
| + float percentAddressesQueued = 100f - percentAddressesSkipped; |
| + int totalAddresses = addressSkipCount.get() + queued; |
| + logVerbose("All addresses have been enqueued (total " + queued + ")."); |
| + pool.await(5, TimeUnit.MINUTES); |
|
bulach
2014/01/07 19:38:30
nit: send this as a command line param
Andrew Hayden (chromium.org)
2014/01/08 00:56:45
This is just a safety valve to make sure we eventu
|
| + if (!noProgress) timer.cancel(); |
| + log(totalAddresses + " addresses discovered; " |
| + + queued + " queued for processing (" |
|
bulach
2014/01/07 19:38:30
nit: the first "+" operator goes in the previous l
Andrew Hayden (chromium.org)
2014/01/08 00:56:45
Done here and elsewhere.
|
| + + String.format("%.2f", percentAddressesQueued) + "%), " |
| + + addressSkipCount.get() + " skipped (" |
| + + String.format("%.2f", percentAddressesSkipped) + "%)"); |
| + dumpStats(startTime); |
| + log("Done."); |
| + } |
| + |
| + /** |
| + * Monitors the pool periodically printing status updates to stdout. |
| + * @param addressProcessingStartTime the time address processing began |
| + * @return the daemon timer that is generating the status updates |
| + */ |
| + private final Timer startTaskMonitor( |
| + final long addressProcessingStartTime) { |
| + Runnable monitorTask = new OutputSpooler(); |
| + Thread monitor = new Thread(monitorTask, "progress monitor"); |
| + monitor.setDaemon(true); |
| + monitor.start(); |
| + |
| + TimerTask task = new TimerTask() { |
| + @Override |
| + public void run() { |
| + dumpStats(addressProcessingStartTime); |
| + } |
| + }; |
| + Timer timer = new Timer(true); |
| + timer.schedule(task, 1000L, 1000L); |
| + return timer; |
| + } |
| + |
| + /** |
| + * Spools address-lookup tasks to the addr2line workers. |
| + * This method will block until most of (or possibly all of) the tasks |
| + * have been spooled. |
| + * If a skip path is set, any line in the input file that doesn't have |
| + * an address will be copied into the skip file. |
| + * |
| + * @param inputPath the path to the dump produced by nm |
| + * @return the number of tasks spooled |
| + */ |
| + private final int spoolTasks(final String inputPath) { |
| + FileReader inputReader = null; |
| + try { |
| + inputReader = new FileReader(inputPath); |
| + } catch (IOException e) { |
| + throw new RuntimeException("Can't open input file: " + inputPath, e); |
| + } |
| + final BufferedReader bufferedReader = new BufferedReader(inputReader); |
| + final Pattern pattern = Pattern.compile(NM_REGEX); |
| + |
| + String currentLine = null; |
| + int numSpooled = 0; |
| + try { |
| + while ((currentLine = bufferedReader.readLine()) != null) { |
| + try { |
| + final Matcher matcher = pattern.matcher(currentLine); |
| + if (!matcher.matches()) { |
| + // HACK: Special case for ICU data. |
| + // This thing is HUGE (5+ megabytes) and is currently |
| + // missed because there is no size information. |
| + // torne@ has volunteered to patch the generation code |
| + // so that the generated ASM includes a size attribute |
| + // so that this hard-coding can go away in the future. |
| + if (currentLine.endsWith("icudt46_dat")) { |
| + // Line looks like this: |
|
bulach
2014/01/07 19:38:30
nit: I guess it'd be more readable to move 225-250
Andrew Hayden (chromium.org)
2014/01/08 00:56:45
Done.
|
| + // 01c9ee00 r icudt46_dat |
| + String[] parts = currentLine.split("\\s"); |
| + if (parts.length == 3) { |
| + // Convert /src/out/Release/lib/[libraryfile] -> /src/out/Release |
| + final File libraryOutputDirectory = new File(libraryPath) |
| + .getParentFile().getParentFile().getCanonicalFile(); |
| + final File icuDir = new File( |
| + libraryOutputDirectory.getAbsolutePath() |
| + + "/obj/third_party/icu"); |
| + final File icuFile = findFile(icuDir, "icudata.icudt46l_dat.o"); |
| + if (icuFile.exists()) { |
| + final Record record = new Record(); |
| + record.address = parts[0]; |
| + record.symbolType = parts[1].charAt(0); |
| + record.symbolName = parts[2]; |
| + record.size = Integer.toHexString((int) icuFile.length()); |
| + record.location = icuFile.getCanonicalPath() + ":0"; |
| + record.resolvedSuccessfully = true; |
| + while (record.size.length() < 8) { |
| + record.size = "0" + record.size; |
| + } |
| + numSpooled++; |
| + pool.submit(record); |
| + continue; |
| + } |
| + } |
| + } |
| + nmDumper.skipped(currentLine); |
| + addressSkipCount.incrementAndGet(); |
| + continue; |
| + } |
| + final Record record = new Record(); |
| + record.address = matcher.group(1); |
| + record.size = matcher.group(2); |
| + if (matcher.groupCount() >= 3) { |
| + record.symbolType = matcher.group(3).charAt(0); |
| + } |
| + if (matcher.groupCount() >= 4) { |
| + // May or may not be present |
| + record.symbolName = matcher.group(4); |
| + } |
| + numSpooled++; |
| + pool.submit(record); |
| + } catch (Exception e) { |
| + throw new RuntimeException("Error processing line: '" + currentLine + "'", e); |
| + } |
| + } |
| + } catch (Exception e) { |
| + throw new RuntimeException("Input processing failed", e); |
| + } finally { |
| + try { |
| + bufferedReader.close(); |
| + } catch (Exception ignored) { |
| + // Nothing to be done |
| + } |
| + try { |
| + inputReader.close(); |
| + } catch (Exception ignored) { |
| + // Nothing to be done |
| + } |
| + } |
| + return numSpooled; |
| + } |
| + |
| + /** |
| + * @return the path to the file that nm wrote |
| + * @throws Exception |
| + * @throws FileNotFoundException |
| + * @throws InterruptedException |
| + */ |
| + private String dumpSymbols() throws Exception, FileNotFoundException, InterruptedException { |
| + final Process process = createNmProcess(); |
| + final File tempFile = File.createTempFile("ParallelAddress2Line", "nm"); |
| + tempFile.deleteOnExit(); |
| + final CountDownLatch completionLatch = sink( |
| + process.getInputStream(), new FileOutputStream(tempFile), true); |
| + sink(process.getErrorStream(), System.err, false); |
| + logVerbose("Dumping symbols to: " + tempFile.getAbsolutePath()); |
| + final int nmRc = process.waitFor(); |
| + if (nmRc != 0) { |
| + throw new RuntimeException("nm process returned " + nmRc); |
| + } |
| + completionLatch.await(); // wait for output to be done |
|
bulach
2014/01/07 19:38:30
not quite sure what this sink is bringing? why not
Andrew Hayden (chromium.org)
2014/01/08 00:56:45
If the process is trying to write to stderr and it
|
| + return tempFile.getAbsolutePath(); |
| + } |
| + |
| + private void dumpStats(final long startTime) { |
| + long successful = successCount.get(); |
| + long doneNow = doneCount.get(); |
| + long unsuccessful = doneNow - successful; |
| + float successPercent = doneNow == 0 ? 100f : 100f * ((float)successful / (float)doneNow); |
| + long elapsedMillis = System.currentTimeMillis() - startTime; |
| + float elapsedSeconds = elapsedMillis / 1000f; |
| + long throughput = doneNow / (elapsedMillis / 1000); |
| + final int mapLookupSuccess = pool.getMapLookupSuccessCount(); |
| + final int mapLookupFailure = pool.getMapLookupFailureCount(); |
| + final int mapLookupTotal = mapLookupSuccess + mapLookupFailure; |
| + float mapLookupSuccessPercent = 0f; |
| + if (mapLookupTotal != 0 && mapLookupSuccess != 0) { |
| + mapLookupSuccessPercent = 100f * |
| + ((float) mapLookupSuccess / (float) mapLookupTotal); |
| + } |
| + |
| + log(doneNow + " addresses processed (" |
| + + successCount.get() + " ok, " + unsuccessful + " failed)" |
|
bulach
2014/01/07 19:38:30
nit: move first + to the previous line
Andrew Hayden (chromium.org)
2014/01/08 00:56:45
Done.
|
| + + ", avg " + throughput + " addresses/sec, " |
| + + String.format("%.2f", successPercent) + "% success" |
| + + ", " + mapLookupTotal + " ambiguous path" |
| + + (!disambiguate ? "" : |
| + ", (" + String.format("%.2f", mapLookupSuccessPercent) + "% disambiguated)") |
| + + (noDedupe ? "" : ", " + pool.getDedupeCount() + " deduped") |
| + + ", elapsed time " + String.format("%.3f", elapsedSeconds) + " seconds"); |
| + } |
| + |
| + private Process createNmProcess() throws Exception { |
| + ProcessBuilder builder = new ProcessBuilder( |
| + nmPath, |
| + "-C", // demangle (for the humans) |
| + "-S", // print size |
| + libraryPath); |
| + logVerbose("Creating process: " + builder.command()); |
| + return builder.start(); |
| + } |
| + |
| + /** |
| + * Make a pipe to drain the specified input stream into the specified |
| + * output stream asynchronously. |
| + * @param in read from here |
| + * @param out and write to here |
| + * @param closeWhenDone whether or not to close the target output stream |
| + * when the input stream terminates |
| + * @return a latch that can be used to await the final write to the |
| + * output stream, which occurs when either of the streams closes |
| + */ |
| + private static final CountDownLatch sink(final InputStream in, |
| + final OutputStream out, final boolean closeWhenDone) { |
| + final CountDownLatch latch = new CountDownLatch(1); |
| + final Runnable task = new Runnable() { |
| + @Override |
| + public void run() { |
| + byte[] buffer = new byte[4096]; |
| + try { |
| + int numRead = 0; |
| + do { |
| + numRead = in.read(buffer); |
| + if (numRead > 0) { |
| + out.write(buffer, 0, numRead); |
| + out.flush(); |
| + } |
| + } while (numRead >= 0); |
| + } catch (Exception e) { |
| + e.printStackTrace(); |
| + } finally { |
| + try { out.flush(); } catch (Exception ignored) { |
| + // Nothing to be done |
| + } |
| + if (closeWhenDone) { |
| + try { out.close(); } catch (Exception ignored) { |
| + // Nothing to be done |
| + } |
| + } |
| + latch.countDown(); |
| + } |
| + } |
| + }; |
| + final Thread worker = new Thread(task, "pipe " + in + "->" + out); |
| + worker.setDaemon(true); |
| + worker.start(); |
| + return latch; |
| + } |
| + |
| + private final class OutputSpooler implements Runnable { |
| + @Override |
| + public void run() { |
| + do { |
| + readRecord(); |
| + } while (stillEnqueuing.get() || (doneCount.get() < enqueuedCount.get())); |
| + } |
| + |
| + /** |
| + * Read a record and process it. |
| + */ |
| + private void readRecord() { |
| + Record record = pool.poll(); |
|
bulach
2014/01/07 19:38:30
does this poll take a timeout? can it replace the
Andrew Hayden (chromium.org)
2014/01/08 00:56:45
Sadly ConcurrentLinkedQueue does not have a poll()
|
| + if (record != null) { |
| + doneCount.incrementAndGet(); |
| + if (record.resolvedSuccessfully) { |
| + successCount.incrementAndGet(); |
| + nmDumper.succeeded(record); |
| + } else { |
| + nmDumper.failed(record); |
| + } |
| + } else { |
| + try { |
| + // wait to keep going |
| + Thread.sleep(100); |
| + } catch (InterruptedException e) { |
| + e.printStackTrace(); |
| + } |
| + } |
| + } |
| + } |
| + |
| + /** |
| + * Log a message to the console. |
| + * @param message the message to log |
| + */ |
| + private final void log(String message) { |
| + System.out.println(message); |
| + } |
| + |
| + /** |
| + * Log a message to the console iff verbose logging is enabled. |
| + * @param message the message to log |
| + */ |
| + private final void logVerbose(String message) { |
| + if (verbose) log(message); |
| + } |
| + |
| + /** |
| + * Runs the tool. Run with --help for limited help. |
| + * @param args |
| + * @throws Exception if anything explodes |
| + */ |
| + public static void main(String[] args) throws Exception { |
| + MicroOptions options = new MicroOptions(); |
| + options.option("library").describedAs( |
| + "path to the library to process, " + |
| + "e.g. out/Release/lib/libchromeview.so").isRequired(); |
| + options.option("threads").describedAs( |
| + "number of parallel worker threads to create. " + |
| + "Start low and watch your memory, defaults to 1"); |
| + options.option("nm").describedAs( |
| + "instead of the 'nm' in $PATH, use this " + |
| + "(e.g., arch-specific binary)"); |
| + options.option("nm-infile").describedAs( |
| + "instead of running nm on the specified library, ingest the specified nm file."); |
| + options.option("addr2line").describedAs( |
| + "instead of the 'addr2line' in $PATH, use this (e.g., arch-specific binary)"); |
| + options.option("skipfile").describedAs( |
| + "output skipped symbols to the specified file"); |
| + options.option("failfile").describedAs( |
| + "output symbols from failed lookups to the specified file"); |
| + options.option("outfile").describedAs( |
| + "output results into the specified file").isRequired(); |
| + options.option("verbose").describedAs("be verbose").isUnary(); |
| + options.option("no-progress").describedAs( |
| + "don't output periodic progress reports").isUnary(); |
| + options.option("no-dedupe").describedAs( |
| + "don't de-dupe symbols that live at the same address; " + |
| + "deduping more accurately describes the use of space within the binary; " + |
| + "if spatial analysis is your goal, leave deduplication on.").isUnary(); |
| + options.option("disambiguate").describedAs( |
| + "create a listing of all source files which can be used to disambiguate " + |
| + "some percentage of ambiguous source references; only useful on some " + |
| + "architectures and adds significant startup cost").isUnary(); |
| + try { |
| + options.parse(args); |
| + } catch (MicroOptions.OptionException e) { |
| + System.err.println(e.getMessage()); |
| + System.err.println(options.usageString()); |
| + System.exit(-1); |
| + } |
| + ParallelAddress2Line tool = new ParallelAddress2Line( |
| + options.getArg("library"), |
| + options.getArg("nm", "nm"), |
| + options.getArg("nm-infile"), |
| + options.getArg("addr2line", "addr2line"), |
| + options.getArg("outfile"), |
| + options.getArg("skipfile"), |
| + options.getArg("failfile"), |
| + options.has("verbose"), |
| + options.has("no-progress"), |
| + options.has("no-dedupe"), |
| + options.has("disambiguate")); |
| + tool.run(Integer.parseInt(options.getArg("threads", "1"))); |
| + } |
| + |
| + // Provides simple command-line support without adding a new third-party library dependency. |
| + // Source: https://github.com/andrewhayden/uopt4j |
|
bulach
2014/01/07 19:38:30
I understand it's yours and license compatible, bu
Andrew Hayden (chromium.org)
2014/01/08 00:56:45
Yeah. I'll bug open source approvers. One of the l
|
| + private static class MicroOptions { |
| + public static class OptionException extends RuntimeException { |
| + public OptionException(String message) { super(message); } } |
| + public static class UnsupportedOptionException extends OptionException { |
| + public UnsupportedOptionException(String o) { |
| + super("Unsupported option '" + o + "'"); } } |
| + public static class MissingArgException extends OptionException { |
| + public MissingArgException(String o) { |
| + super("Missing argument for option '" + o + "'"); } } |
| + public static class RequiredOptionException extends OptionException { |
| + public RequiredOptionException(String o) { |
| + super("Missing required option '" + o + "'"); } } |
| + public class Option{ |
| + private String n,d; |
| + private boolean u,r; |
| + private Option(String n) { this.n = n; } |
| + public Option describedAs(String d) { this.d = d; return this; } |
| + public Option isRequired() { this.r = true; return this; } |
| + public Option isUnary() { this.u = true; return this; } |
| + } |
| + private final java.util.Map<String,Option> opts = |
| + new java.util.TreeMap<String,Option>(); |
| + private final java.util.Map<String,String> args = |
| + new java.util.HashMap<String, String>(); |
| + public MicroOptions() { super(); } |
| + public String usageString() { |
| + int max = 0; |
| + for (String s : opts.keySet()) max = Math.max(s.length(), max); |
| + StringBuilder b = new StringBuilder(); |
| + java.util.Iterator<Option> i = opts.values().iterator(); |
| + while (i.hasNext()) { |
| + Option o = i.next(); |
| + b.append(o.u ? " -" : "--"); |
| + b.append(String.format("%1$-" + max + "s", o.n)); |
| + b.append(o.u ? " " : " [ARG] "); |
| + b.append(o.d == null ? "" : o.d + " "); |
| + b.append(o.r ? "(required)" : "(optional)"); |
| + if (i.hasNext()) b.append('\n'); |
| + } |
| + return b.toString(); |
| + } |
| + public void parse(String... strings) { |
| + for (int i = 0; i < strings.length; i++) { |
| + String k = strings[i]; String value = null; |
| + if (k.matches("-[[^\\s]&&[^-]]")) { k = k.substring(1); } |
| + else if (k.matches("--[\\S]{2,}")) { k = k.substring(2); } |
| + else throw new UnsupportedOptionException(k); |
| + if (!opts.containsKey(k)) throw new UnsupportedOptionException(k); |
| + Option o = opts.get(k); |
| + if (!o.u) { |
| + if (i + 1 >= strings.length) throw new MissingArgException(k); |
| + value = strings[++i]; |
| + } |
| + args.put(k, value); |
| + } |
| + for (Option o : opts.values()) |
| + if (o.r && !args.containsKey(o.n)) |
| + throw new RequiredOptionException(o.n); |
| + } |
| + public Option option(String name) { |
| + checkName(name); |
| + Option o = new Option(name); opts.put(name, o); return o; } |
| + private void checkName(String name) { |
| + if (name == null || name.length() == 0 || name.charAt(0) == '-') |
| + throw new UnsupportedOptionException("illegal name: " + name); |
| + } |
| + public boolean has(String option) { |
| + checkName(option); |
| + if (!opts.containsKey(option)) |
| + throw new UnsupportedOptionException(option); |
| + return args.containsKey(option); |
| + } |
| + public String getArg(String option) { return getArg(option, null); } |
| + public String getArg(String option, String defaultValue) { |
| + checkName(option); |
| + if (!opts.containsKey(option)) |
| + throw new UnsupportedOptionException(option); |
| + if (opts.get(option).u) |
| + throw new OptionException("Option takes no arguments: " + option); |
| + return args.containsKey(option) ? args.get(option) : defaultValue; |
| + } |
| + } |
| +} |