/* * Copyright (C) 2006 Steve Ratcliffe * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * * Author: Steve Ratcliffe * Create date: 01-Jan-2007 */ package uk.me.parabola.mkgmap; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Formatter; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import uk.me.parabola.imgfmt.ExitException; import uk.me.parabola.log.Logger; import uk.me.parabola.util.EnhancedProperties; /** * Command line arguments for Main. Arguments consist of options and filenames. * You read arguments from left to right and when a filename is encountered * the file is processed with the options that were in force at the time. * * Since it is likely that the number of options will become quite large, you * can place options in a file. Place the options each on a separate line * without the initial '--'. * * @author Steve Ratcliffe */ public class CommandArgsReader { private static final Logger log = Logger.getLogger(CommandArgsReader.class); private final ArgumentProcessor proc; private boolean mapnameWasSet; private final ArgList arglist = new ArgList(); private final EnhancedProperties args = new EnhancedProperties(); private Set<String> validOptions; { // Set some default values. It is as if these were on the command // line before any user supplied options. add(new CommandOption("mapname", "63240001")); add(new CommandOption("description", "OSM street map")); add(new CommandOption("overview-mapname", "osmmap")); add(new CommandOption("overview-mapnumber", "63240000")); add(new CommandOption("poi-address", "")); add(new CommandOption("merge-lines", "")); } public CommandArgsReader(ArgumentProcessor proc) { this.proc = proc; } /** * Read and interpret the command line arguments. Most have a double hyphen * preceding them and these work just the same if they are in a config * file. * <p/> * There are a few options that consist of a single hyphen followed by a * single letter that are short cuts for a long option. * <p/> * The -c option is special. It is followed by the name of a file in which * there are further command line options. Any option on the command line * that comes after the -c option will override the value that is set in * this file. * * @param args The command line arguments. */ public void readArgs(String[] args) { proc.startOptions(); int i = 0; while (i < args.length) { String arg = args[i++]; if (arg.startsWith("--")) { // This is a long style 'property' format option. addOption(arg.substring(2)); } else if (arg.equals("-c")) { // Config file readConfigFile(args[i++]); } else if (arg.equals("-n")) { // Map name (should be an 8 digit number). addOption("mapname", args[i++]); } else if (arg.equals("-v")) { // make commands more verbose addOption("verbose"); } else if (arg.startsWith("-")) { // this is an unrecognised option. System.err.println("unrecognised option " + arg); } else { log.debug("adding filename:", arg); add(new Filename(arg)); } } // If there is more than one filename argument we inform of this fact // via a fake option. proc.processOption("number-of-files", String.valueOf(arglist.getFilenameCount())); // Now process the arguments in order. for (ArgType a : arglist) { a.processArg(); } proc.endOptions(new CommandArgs(this.args)); } /** * Add an option based on the option and value separately. * @param option The option name. * @param value Its value. */ private void addOption(String option, String value) { CommandOption opt = new CommandOption(option, value); addOption(opt); } /** * Add an option from a raw string. * @param optval The option=value string. */ private void addOption(String optval) { CommandOption opt = new CommandOption(new Option(optval)); boolean legacyOptionDetected = false; // translate legacy options drive-on-left and drive-on-right String option = opt.getOption(); if ("drive-on-left".equals(option)){ opt = new CommandOption(new Option("drive-on=left")); legacyOptionDetected = true; } if ("drive-on-right".equals(option)){ opt = new CommandOption(new Option("drive-on=right")); legacyOptionDetected = true; } if (legacyOptionDetected){ System.err.println("Option " + option + " is deprecated. Will use " + opt.getOption() + "=" + opt.getValue() + " as replacement for " + optval); } addOption(opt); } /** * Actually add the option. Some of these are special in that they are * filename arguments or instructions to read options from another file. * * @param opt The decoded option. */ private void addOption(CommandOption opt) { String option = opt.getOption(); String value = opt.getValue(); if (validOptions != null && !validOptions.contains(option) && !opt.isExperimental()) { throw new ExitException(String.format("Invalid option: '%s'", option)); } log.debug("adding option", option, value); // Note if an explicit mapname is set if (option.equals("mapname")) mapnameWasSet = true; switch (option) { case "input-file": if (value != null){ log.debug("adding filename", value); add(new Filename(value)); } break; case "read-config": readConfigFile(value); break; case "latin1": add(new CommandOption("code-page", "1252")); break; case "unicode": add(new CommandOption("code-page", "65001")); break; default: add(opt); break; } } private void add(CommandOption option) { arglist.add(option); } private void add(Filename filename) { arglist.add(filename); } /** * Read a config file that contains more options. When the number of * options becomes large it is more convenient to place them in a file. * * @param filename The filename to obtain options from. */ private void readConfigFile(String filename) { Options opts = new Options(new OptionProcessor() { public void processOption(Option opt) { log.debug("incoming opt", opt.getOption(), opt.getValue()); addOption(new CommandOption(opt)); } }); try { opts.readOptionFile(filename); } catch (IOException e) { throw new ExitException("Failed to read option file", e); } } public void setValidOptions(Set<String> validOptions) { this.validOptions = validOptions; } /** * Interface that represents an argument type. It provides a method for * the argument to be processed in order. Options can be interspersed with * filenames. The options take effect where they appear. */ interface ArgType { public abstract void processArg(); } /** * A filename. */ class Filename implements ArgType { private final String name; private boolean useFilenameAsMapname = true; private Filename(String name) { this.name = name; if (mapnameWasSet) useFilenameAsMapname = false; } public void processArg() { // If there was no explicit mapname specified and the input filename // looks like it contains an 8digit number then we use that. String mapname; if (useFilenameAsMapname) { mapname = extractMapName(name); if (mapname != null) args.setProperty("mapname", mapname); } // Now process the file proc.processFilename(new CommandArgs(args), name); // Increase the name number. If the next arg sets it then that // will override this new name. mapname = args.getProperty("mapname"); try { Formatter fmt = new Formatter(); try { int n = Integer.parseInt(mapname); fmt.format("%08d", ++n); } catch (NumberFormatException e) { fmt.format("%8.8s", mapname); } args.setProperty("mapname", fmt.toString()); fmt.close(); } catch (NumberFormatException e) { // If the name is not a number then we just leave it alone... } } private String extractMapName(String path) { File file = new File(path); String fname = file.getName(); Pattern pat = Pattern.compile("([0-9]{8})"); Matcher matcher = pat.matcher(fname); boolean found = matcher.find(); if (found) return matcher.group(1); return null; } } /** * An option argument. A key value pair. */ class CommandOption implements ArgType { private final Option option; private CommandOption(Option option) { this.option = option; } private CommandOption(String key, String val) { this.option = new Option(key, val); } public void processArg() { if (option.isReset()) { args.remove(option.getOption()); proc.removeOption(option.getOption()); } else { args.setProperty(option.getOption(), option.getValue()); proc.processOption(option.getOption(), option.getValue()); } } public String getOption() { return option.getOption(); } public String getValue() { return option.getValue(); } public boolean isExperimental() { return option.isExperimental(); } } /** * The arguments are held in this list. */ class ArgList implements Iterable<ArgType> { private final List<ArgType> alist; private int filenameCount; ArgList() { alist = new ArrayList<ArgType>(); } protected void add(CommandOption option) { alist.add(option); } public void add(Filename name) { filenameCount++; alist.add(name); } public Iterator<ArgType> iterator() { return alist.iterator(); } public int getFilenameCount() { return filenameCount; } } }