view src/main/java/org/monetdb/util/CmdLineOpts.java @ 833:e890195256ac

Update copyright for the new year, move to MonetDB Foundation, add SPDX.
author Sjoerd Mullender <sjoerd@acm.org>
date Fri, 29 Dec 2023 14:37:42 +0100 (15 months ago)
parents aeb268156580
children d9a45743536d
line wrap: on
line source
/*
 * SPDX-License-Identifier: MPL-2.0
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0.  If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Copyright 2024 MonetDB Foundation;
 * Copyright August 2008 - 2023 MonetDB B.V.;
 * Copyright 1997 - July 2008 CWI.
 */

package org.monetdb.util;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Properties;

public final class CmdLineOpts {
	/** the arguments we handle */
	private HashMap<String, OptionContainer> opts = new HashMap<String, OptionContainer>();
	/** the options themself */
	private ArrayList<OptionContainer> options = new ArrayList<OptionContainer>();

	/** no arguments */
	public static final int CAR_ZERO	= 0;
	/** always one argument */
	public static final int CAR_ONE	= 1;
	/** zero or one argument */
	public static final int CAR_ZERO_ONE	= 2;
	/** zero or many arguments */
	public static final int CAR_ZERO_MANY	= 3;
	/** one or many arguments */
	public static final int CAR_ONE_MANY	= 4;

	public CmdLineOpts() {
	}

	public void addOption(
			final String shorta,
			final String longa,
			final int type,
			final String defaulta,
			final String descriptiona)
		throws OptionsException {
		final OptionContainer oc =
			new OptionContainer(
				shorta,
				longa,
				type,
				defaulta,
				descriptiona
			);
		if (shorta != null)
			opts.put(shorta, oc);
		if (longa != null)
			opts.put(longa, oc);
	}

	public void removeOption(final String name) {
		final OptionContainer oc = opts.get(name);
		if (oc != null) {
			opts.remove(oc.shorta);
			opts.remove(oc.longa);
		}
	}

	public OptionContainer getOption(final String key) throws OptionsException {
		final OptionContainer ret = opts.get(key);
		if (ret == null)
			throw new OptionsException("No such option: " + key);

		return ret;
	}

	public void processFile(final java.io.File file) throws OptionsException {
		// the file is there, parse it and set its settings
		final Properties prop = new Properties();
		try {
			final java.io.FileInputStream in = new java.io.FileInputStream(file);
			try {
				prop.load(in);
			} finally {
				in.close();
			}

			String key;
			OptionContainer option = null;
			for (java.util.Enumeration<?> e = prop.propertyNames(); e.hasMoreElements(); ) {
				key = (String) e.nextElement();
				option = opts.get(key);
				if (option != null) {
					option.resetArguments();
					option.addArgument(prop.getProperty(key));
				} else
					// ignore unknown options (it used to throw an OptionsException)
					System.out.println("Info: Ignoring unknown/unsupported option (in " + file.getAbsolutePath() + "): " + key);
			}
		} catch (java.io.IOException e) {
			throw new OptionsException("File IO Exception: " + e);
		}
	}

	public void processArgs(final String args[]) throws OptionsException {
		// parse and set the command line arguments
		OptionContainer option = null;
		int quant = -1;
		int qcount = 0;
		boolean moreData = false;
		for (int i = 0; i < args.length; i++) {
			if (option == null) {
				if (args[i].charAt(0) != '-')
					throw new OptionsException("Unexpected value: " + args[i]);

				// see what kind of argument we have
				if (args[i].length() == 1)
					throw new OptionsException("Illegal argument: '-'");

				if (args[i].charAt(1) == '-') {
					// we have a long argument
					// since we don't accept inline values we can take
					// everything left in the string as argument, unless
					// there is a = in there...
					final String tmp = args[i].substring(2);
					final int pos = tmp.indexOf('=');
					if (pos == -1) {
						option = opts.get(tmp);
						moreData = false;
					} else {
						option = opts.get(tmp.substring(0, pos));
						// modify the option a bit so the code below
						// handles the moreData correctly
						args[i] = "-?" + tmp.substring(pos + 1);
						moreData = true;
					}
				} else if (args[i].charAt(1) == 'X') {
					// extra argument, same as long argument
					final String tmp = args[i].substring(1);
					final int pos = tmp.indexOf('=');
					if (pos == -1) {
						option = opts.get(tmp);
						moreData = false;
					} else {
						option = opts.get(tmp.substring(0, pos));
						// modify the option a bit so the code below
						// handles the moreData correctly
						args[i] = "-?" + tmp.substring(pos + 1);
						moreData = true;
					}
				} else {
					// single char argument
					option = opts.get("" + args[i].charAt(1));
					// is there more data left in the argument?
					moreData = args[i].length() > 2 ? true : false;
				}

				if (option != null) {
					// make sure we overwrite previously set arguments
					option.resetArguments();
					final int card = option.getCardinality();
					if (card == CAR_ONE) {
						if (moreData) {
							option.addArgument(args[i].substring(2));
							option = null;
						} else {
							quant = 1;
						}
					} else if (card == CAR_ZERO_ONE) {
						option.setPresent();
						qcount = 1;
						quant = 2;
						if (moreData) {
							option.addArgument(args[i].substring(2));
							option = null;
						}
					} else if (card == CAR_ZERO_MANY) {
						option.setPresent();
						qcount = 1;
						quant = -1;
						if (moreData) {
							option.addArgument(args[i].substring(2));
							qcount++;
						}
					} else if (card == CAR_ZERO) {
						option.setPresent();
						option = null;
					}
				} else {
					throw new OptionsException("Unknown argument: " + args[i]);
				}
			} else {
				// store the `value'
				option.addArgument(args[i]);
				if (++qcount == quant) {
					quant = 0;
					qcount = 0;
					option = null;
				}
			}
		}
	}

	/**
	 * Returns a help message for all options loaded.
	 *
	 * @return the help message
	 */
	public String produceHelpMessage() {
		// first calculate how much space is necessary for the options
		int maxlen = 0;
		for (OptionContainer oc : options) {
			final String shorta = oc.getShort();
			final String longa = oc.getLong();
			int len = 0;
			if (shorta != null)
				len += shorta.length() + 1 + 1;
			if (longa != null)
				len += longa.length() + 1 + (longa.charAt(0) == 'X' ? 0 : 1) + 1;
			// yes, we don't care about branch mispredictions here ;)
			if (maxlen < len)
				maxlen = len;
		}

		// get the individual strings
		final StringBuilder ret = new StringBuilder();
		for (OptionContainer oc : options) {
			ret.append(produceHelpMessage(oc, maxlen));
		}

		return ret.toString();
	}

	/**
	 * Returns the help message for the given OptionContainer.  The
	 * command line flags will be padded to optwidth spaces to allow for
	 * aligning.  The description of the flags will be wrapped at 80
	 * characters.
	 *
	 * @param oc the OptionContainer
	 * @param indentwidth padding width for the command line flags
	 * @return the help message for the option
	 */
	public String produceHelpMessage(final OptionContainer oc, int indentwidth) {
		final String desc = oc.getDescription();
		if (desc == null)
			return "";

		final String shorta = oc.getShort();
		final String longa = oc.getLong();
		int optwidth = 0;
		if (shorta != null)
			optwidth += shorta.length() + 1 + 1;
		if (longa != null)
			optwidth += longa.length() + 1 + (longa.charAt(0) == 'X' ? 0 : 1) + 1;
		final int descwidth = 80 - indentwidth;

		// default to with of command line flags if no width given
		if (indentwidth <= 0)
			indentwidth = optwidth;

		final StringBuilder ret = new StringBuilder();

		// add the command line flags
		if (shorta != null)
			ret.append('-').append(shorta).append(' ');
		if (longa != null) {
			ret.append('-');
			if (longa.charAt(0) != 'X')
				ret.append('-');
			ret.append(longa).append(' ');
		}

		for (int i = optwidth; i < indentwidth; i++)
			ret.append(' ');
		// add the description, wrap around words
		int pos = 0, lastpos = 0;
		while (pos < desc.length()) {
			pos += descwidth;
			if (lastpos != 0) {
				for (int i = 0; i < indentwidth; i++)
					ret.append(' ');
			}
			if (pos >= desc.length()) {
				ret.append(desc.substring(lastpos)).append('\n');
				break;
			}
			int space;
			for (space = pos; desc.charAt(space) != ' '; space--) {
				if (space == 0) {
					space = pos;
					break;
				}
			}
			pos = space;
			ret.append(desc.substring(lastpos, pos)).append('\n');
			while (desc.charAt(pos) == ' ')
				pos++;
			lastpos = pos;
		}

		return ret.toString();
	}

	public final class OptionContainer {
		private final int cardinality;
		private final String shorta;
		private final String longa;
		private final ArrayList<String> values = new ArrayList<String>();
		private final String name;
		private final String defaulta;
		private final String descriptiona;
		private boolean present;

		public OptionContainer(
				final String shorta,
				final String longa,
				final int cardinality,
				final String defaulta,
				final String descriptiona)
			throws IllegalArgumentException
		{
			this.cardinality = cardinality;
			this.shorta = shorta;
			this.longa = longa;
			this.defaulta = defaulta;
			this.descriptiona = descriptiona;
			this.present = false;

			if (cardinality != CAR_ZERO &&
			    cardinality != CAR_ONE &&
			    cardinality != CAR_ZERO_ONE &&
			    cardinality != CAR_ZERO_MANY &&
			    cardinality != CAR_ONE_MANY)
				throw new IllegalArgumentException("unknown cardinality");
			if (shorta != null && shorta.length() != 1)
				throw new IllegalArgumentException("short option should consist of exactly one character");
			if (shorta == null && longa == null)
				throw new IllegalArgumentException("either a short or long argument should be given");
			if ((cardinality == CAR_ZERO ||
			     cardinality == CAR_ZERO_ONE ||
			     cardinality == CAR_ZERO_MANY) &&
			    defaulta != null) {
				throw new IllegalArgumentException("cannot specify a default for a (possible) zero argument option");
			}

			name = (longa != null) ? longa : shorta;
			options.add(this);
		}

		public final void resetArguments() {
			values.clear();
		}

		public void addArgument(final String val) throws OptionsException {
			if (cardinality == CAR_ZERO) {
				throw new OptionsException("option " + name + " does not allow arguments");
			}
			if ((cardinality == CAR_ONE || cardinality == CAR_ZERO_ONE) && values.size() >= 1) {
				throw new OptionsException("option " + name + " does at max allow only one argument");
			}
			// we can add it
			values.add(val);
			setPresent();
		}

		public final void setPresent() {
			present = true;
		}

		public final boolean isPresent() {
			return present;
		}

		public final int getCardinality() {
			return cardinality;
		}

		public final int getArgumentCount() {
			return values.size();
		}

		public final String getArgument() {
			final String ret = getArgument(1);
			if (ret == null)
				return defaulta;
			return ret;
		}

		public final String getArgument(final int index) {
			final String[] args = getArguments();
			if (index < 1 || index > args.length)
				return null;
			return args[index - 1];
		}

		public final String[] getArguments() {
			return values.toArray(new String[values.size()]);
		}

		public final String getShort() {
			return shorta;
		}

		public final String getLong() {
			return longa;
		}

		public final String getDescription() {
			return descriptiona;
		}
	}
}