From db238cc23f3bb5c9768d30ae24b99e67a5795203 Mon Sep 17 00:00:00 2001
From: Denis Krjuchkov <denis@crazydev.net>
Date: Sun, 24 Nov 2013 17:19:51 +0600
Subject: [PATCH] CommandLine: new command line parser

This implementation behaves mostly identical to old parser.
Few observable differences:
- There are no option groups (single group is used for all options)
- Option --stdout is hidden (it has been obsolete for a long time)
- MPD executable name (mpd) is hardcoded for simplicity
---
 Makefile.am               |   2 +
 src/CommandLine.cxx       | 226 ++++++++++++++++++++++++--------------
 src/CommandLine.hxx       |  10 +-
 src/util/OptionDef.hxx    |  63 +++++++++++
 src/util/OptionParser.cxx |  59 ++++++++++
 src/util/OptionParser.hxx |  88 +++++++++++++++
 6 files changed, 358 insertions(+), 90 deletions(-)
 create mode 100644 src/util/OptionDef.hxx
 create mode 100644 src/util/OptionParser.cxx
 create mode 100644 src/util/OptionParser.hxx

diff --git a/Makefile.am b/Makefile.am
index 7240cb3f1..f7ce9e377 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -265,6 +265,8 @@ libutil_a_SOURCES = \
 	src/util/SliceBuffer.hxx \
 	src/util/HugeAllocator.cxx src/util/HugeAllocator.hxx \
 	src/util/PeakBuffer.cxx src/util/PeakBuffer.hxx \
+	src/util/OptionParser.cxx OptionParser.hxx \
+	src/util/OptionDef.hxx \
 	src/util/list.h \
 	src/util/list_sort.c src/util/list_sort.h \
 	src/util/ByteReverse.cxx src/util/ByteReverse.hxx \
diff --git a/src/CommandLine.cxx b/src/CommandLine.cxx
index 05f0a358c..6c0f7e6a1 100644
--- a/src/CommandLine.cxx
+++ b/src/CommandLine.cxx
@@ -36,6 +36,8 @@
 #include "fs/FileSystem.hxx"
 #include "util/Error.hxx"
 #include "util/Domain.hxx"
+#include "util/OptionDef.hxx"
+#include "util/OptionParser.hxx"
 #include "system/FatalError.hxx"
 
 #ifdef ENABLE_ENCODER
@@ -61,6 +63,25 @@
 #define USER_CONFIG_FILE_LOCATION_XDG	"mpd/mpd.conf"
 #endif
 
+static const OptionDef opt_kill(
+	"kill", "kill the currently running mpd session");
+static const OptionDef opt_no_config(
+	"no-config", "don't read from config");
+static const OptionDef opt_no_daemon(
+	"no-daemon", "don't detach from console");
+static const OptionDef opt_stdout(
+	"stdout", nullptr); // hidden, compatibility with old versions
+static const OptionDef opt_stderr(
+	"stderr", "print messages to stderr");
+static const OptionDef opt_verbose(
+	"verbose", 'v', "verbose logging");
+static const OptionDef opt_version(
+	"version", 'V', "print version number");
+static const OptionDef opt_help(
+	"help", 'h', "show help options");
+static const OptionDef opt_help_alt(
+	nullptr, '?', nullptr); // hidden, standard alias for --help
+
 static constexpr Domain cmdline_domain("cmdline");
 
 gcc_noreturn
@@ -132,8 +153,39 @@ static void version(void)
 	exit(EXIT_SUCCESS);
 }
 
-static const char *summary =
-	"Music Player Daemon - a daemon for playing music.";
+static void PrintOption(const OptionDef &opt)
+{
+	if (opt.HasShortOption())
+		printf("  -%c, --%-12s%s\n",
+		       opt.GetShortOption(),
+		       opt.GetLongOption(),
+		       opt.GetDescription());
+	else
+		printf("  --%-16s%s\n",
+		       opt.GetLongOption(),
+		       opt.GetDescription());
+}
+
+gcc_noreturn
+static void help(void)
+{
+	puts("Usage:\n"
+	     "  mpd [OPTION...] [path/to/mpd.conf]\n"
+	     "\n"
+	     "Music Player Daemon - a daemon for playing music.\n"
+	     "\n"
+	     "Options:");
+
+	PrintOption(opt_help);
+	PrintOption(opt_kill);
+	PrintOption(opt_no_config);
+	PrintOption(opt_no_daemon);
+	PrintOption(opt_stderr);
+	PrintOption(opt_verbose);
+	PrintOption(opt_version);
+
+	exit(EXIT_SUCCESS);
+}
 
 gcc_pure
 static AllocatedPath
@@ -149,105 +201,111 @@ bool
 parse_cmdline(int argc, char **argv, struct options *options,
 	      Error &error)
 {
-	GOptionContext *context;
-	bool ret;
-	static gboolean option_version,
-		option_no_daemon,
-		option_no_config;
-	const GOptionEntry entries[] = {
-		{ "kill", 0, 0, G_OPTION_ARG_NONE, &options->kill,
-		  "kill the currently running mpd session", nullptr },
-		{ "no-config", 0, 0, G_OPTION_ARG_NONE, &option_no_config,
-		  "don't read from config", nullptr },
-		{ "no-daemon", 0, 0, G_OPTION_ARG_NONE, &option_no_daemon,
-		  "don't detach from console", nullptr },
-		{ "stdout", 0, 0, G_OPTION_ARG_NONE, &options->log_stderr,
-		  nullptr, nullptr },
-		{ "stderr", 0, 0, G_OPTION_ARG_NONE, &options->log_stderr,
-		  "print messages to stderr", nullptr },
-		{ "verbose", 'v', 0, G_OPTION_ARG_NONE, &options->verbose,
-		  "verbose logging", nullptr },
-		{ "version", 'V', 0, G_OPTION_ARG_NONE, &option_version,
-		  "print version number", nullptr },
-		{ nullptr, 0, 0, G_OPTION_ARG_NONE, nullptr, nullptr, nullptr }
-	};
-
+	bool use_config_file = true;
 	options->kill = false;
 	options->daemon = true;
 	options->log_stderr = false;
 	options->verbose = false;
 
-	context = g_option_context_new("[path/to/mpd.conf]");
-	g_option_context_add_main_entries(context, entries, nullptr);
+	// First pass: handle command line options
+	OptionParser parser(argc, argv);
+	while (parser.HasEntries()) {
+		if (!parser.ParseNext())
+			continue;
+		if (parser.CheckOption(opt_kill)) {
+			options->kill = true;
+			continue;
+		}
+		if (parser.CheckOption(opt_no_config)) {
+			use_config_file = false;
+			continue;
+		}
+		if (parser.CheckOption(opt_no_daemon)) {
+			options->daemon = false;
+			continue;
+		}
+		if (parser.CheckOption(opt_stderr, opt_stdout)) {
+			options->log_stderr = true;
+			continue;
+		}
+		if (parser.CheckOption(opt_verbose)) {
+			options->verbose = true;
+			continue;
+		}
+		if (parser.CheckOption(opt_version))
+			version();
+		if (parser.CheckOption(opt_help, opt_help_alt))
+			help();
 
-	g_option_context_set_summary(context, summary);
-
-	GError *gerror = nullptr;
-	ret = g_option_context_parse(context, &argc, &argv, &gerror);
-	g_option_context_free(context);
-
-	if (!ret)
-		FatalError("option parsing failed", gerror);
-
-	if (option_version)
-		version();
+		error.Format(cmdline_domain, "invalid option: %s",
+			     parser.GetOption());
+		return false;
+	}
 
 	/* initialize the logging library, so the configuration file
 	   parser can use it already */
 	log_early_init(options->verbose);
 
-	options->daemon = !option_no_daemon;
-
-	if (option_no_config) {
+	if (!use_config_file) {
 		LogDebug(cmdline_domain,
 			 "Ignoring config, using daemon defaults");
 		return true;
-	} else if (argc <= 1) {
-		/* default configuration file path */
+	}
 
-#ifdef WIN32
-		AllocatedPath path = PathBuildChecked(AllocatedPath::FromUTF8(g_get_user_config_dir()),
-					     CONFIG_FILE_LOCATION);
-		if (!path.IsNull() && FileExists(path))
-			return ReadConfigFile(path, error);
-
-		const char *const*system_config_dirs =
-			g_get_system_config_dirs();
-
-		for (unsigned i = 0; system_config_dirs[i] != nullptr; ++i) {
-			path = PathBuildChecked(AllocatedPath::FromUTF8(system_config_dirs[i]),
-						CONFIG_FILE_LOCATION);
-			if (!path.IsNull() && FileExists(path))
-				return ReadConfigFile(path, error);
+	// Second pass: find non-option parameters (i.e. config file)
+	const char *config_file = nullptr;
+	for (int i = 1; i < argc; ++i) {
+		if (OptionParser::IsOption(argv[i]))
+			continue;
+		if (config_file == nullptr) {
+			config_file = argv[i];
+			continue;
 		}
-#else
-		AllocatedPath path = PathBuildChecked(AllocatedPath::FromUTF8(g_get_user_config_dir()),
-						      USER_CONFIG_FILE_LOCATION_XDG);
-		if (!path.IsNull() && FileExists(path))
-			return ReadConfigFile(path, error);
-
-		path = PathBuildChecked(AllocatedPath::FromUTF8(g_get_home_dir()),
-					     USER_CONFIG_FILE_LOCATION1);
-		if (!path.IsNull() && FileExists(path))
-			return ReadConfigFile(path, error);
-
-		path = PathBuildChecked(AllocatedPath::FromUTF8(g_get_home_dir()),
-					USER_CONFIG_FILE_LOCATION2);
-		if (!path.IsNull() && FileExists(path))
-			return ReadConfigFile(path, error);
-
-		path = AllocatedPath::FromUTF8(SYSTEM_CONFIG_FILE_LOCATION);
-		if (!path.IsNull() && FileExists(path))
-			return ReadConfigFile(path, error);
-#endif
-
-		error.Set(cmdline_domain, "No configuration file found");
-		return false;
-	} else if (argc == 2) {
-		/* specified configuration file */
-		return ReadConfigFile(Path::FromFS(argv[1]), error);
-	} else {
 		error.Set(cmdline_domain, "too many arguments");
 		return false;
 	}
+
+	if (config_file != nullptr) {
+		/* use specified configuration file */
+		return ReadConfigFile(Path::FromFS(config_file), error);
+	}
+
+	/* use default configuration file path */
+#ifdef WIN32
+	AllocatedPath path = PathBuildChecked(AllocatedPath::FromUTF8(g_get_user_config_dir()),
+					      CONFIG_FILE_LOCATION);
+	if (!path.IsNull() && FileExists(path))
+		return ReadConfigFile(path, error);
+
+	const char *const*system_config_dirs =
+		g_get_system_config_dirs();
+
+	for (unsigned i = 0; system_config_dirs[i] != nullptr; ++i) {
+		path = PathBuildChecked(AllocatedPath::FromUTF8(system_config_dirs[i]),
+					CONFIG_FILE_LOCATION);
+		if (!path.IsNull() && FileExists(path))
+			return ReadConfigFile(path, error);
+	}
+#else
+	AllocatedPath path = PathBuildChecked(AllocatedPath::FromUTF8(g_get_user_config_dir()),
+					      USER_CONFIG_FILE_LOCATION_XDG);
+	if (!path.IsNull() && FileExists(path))
+		return ReadConfigFile(path, error);
+
+	path = PathBuildChecked(AllocatedPath::FromUTF8(g_get_home_dir()),
+				USER_CONFIG_FILE_LOCATION1);
+	if (!path.IsNull() && FileExists(path))
+		return ReadConfigFile(path, error);
+
+	path = PathBuildChecked(AllocatedPath::FromUTF8(g_get_home_dir()),
+				USER_CONFIG_FILE_LOCATION2);
+	if (!path.IsNull() && FileExists(path))
+		return ReadConfigFile(path, error);
+
+	path = AllocatedPath::FromUTF8(SYSTEM_CONFIG_FILE_LOCATION);
+	if (!path.IsNull() && FileExists(path))
+		return ReadConfigFile(path, error);
+#endif
+	error.Set(cmdline_domain, "No configuration file found");
+	return false;
 }
diff --git a/src/CommandLine.hxx b/src/CommandLine.hxx
index 214150eae..3b24a0d77 100644
--- a/src/CommandLine.hxx
+++ b/src/CommandLine.hxx
@@ -20,15 +20,13 @@
 #ifndef MPD_COMMAND_LINE_HXX
 #define MPD_COMMAND_LINE_HXX
 
-#include <glib.h>
-
 class Error;
 
 struct options {
-	gboolean kill;
-	gboolean daemon;
-	gboolean log_stderr;
-	gboolean verbose;
+	bool kill;
+	bool daemon;
+	bool log_stderr;
+	bool verbose;
 };
 
 bool
diff --git a/src/util/OptionDef.hxx b/src/util/OptionDef.hxx
new file mode 100644
index 000000000..ca9e6083f
--- /dev/null
+++ b/src/util/OptionDef.hxx
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2003-2013 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_UTIL_OPTIONDEF_HXX
+#define MPD_UTIL_OPTIONDEF_HXX
+
+/**
+ * Command line option definition.
+ */
+class OptionDef
+{
+	const char *long_option;
+	char short_option;
+	const char *desc;
+public:
+	constexpr OptionDef(const char *_long_option, const char *_desc)
+		: long_option(_long_option),
+		  short_option(0),
+		  desc(_desc) { }
+
+	constexpr OptionDef(const char *_long_option,
+			    char _short_option, const char *_desc)
+		: long_option(_long_option),
+		  short_option(_short_option),
+		  desc(_desc) { }
+
+	bool HasLongOption() const { return long_option != nullptr; }
+	bool HasShortOption() const { return short_option != 0; }
+	bool HasDescription() const { return desc != nullptr; }
+
+	const char *GetLongOption() const {
+		assert(HasLongOption());
+		return long_option;
+	}
+
+	char GetShortOption() const {
+		assert(HasShortOption());
+		return short_option;
+	}
+
+	const char *GetDescription() const {
+		assert(HasDescription());
+		return desc;
+	}
+};
+
+#endif
diff --git a/src/util/OptionParser.cxx b/src/util/OptionParser.cxx
new file mode 100644
index 000000000..b8addb9a2
--- /dev/null
+++ b/src/util/OptionParser.cxx
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2003-2013 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "OptionParser.hxx"
+#include "OptionDef.hxx"
+
+#include <string.h>
+
+bool OptionParser::CheckOption(const OptionDef &opt)
+{
+	assert(option != nullptr);
+
+	if (is_long)
+		return opt.HasLongOption() &&
+		       strcmp(option, opt.GetLongOption()) == 0;
+
+	return opt.HasShortOption() &&
+	       option[0] == opt.GetShortOption() &&
+	       option[1] == '\0';
+}
+
+bool OptionParser::ParseNext()
+{
+	assert(HasEntries());
+	char *arg = *argv;
+	++argv;
+	--argc;
+	if (arg[0] == '-') {
+		if (arg[1] == '-') {
+			option = arg + 2;
+			is_long = true;
+		}
+		else {
+			option = arg + 1;
+			is_long = false;
+		}
+		option_raw = arg;
+		return true;
+	}
+	option = nullptr;
+	option_raw = nullptr;
+	return false;
+}
diff --git a/src/util/OptionParser.hxx b/src/util/OptionParser.hxx
new file mode 100644
index 000000000..74091bc29
--- /dev/null
+++ b/src/util/OptionParser.hxx
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2003-2013 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_UTIL_OPTIONPARSER_HXX
+#define MPD_UTIL_OPTIONPARSER_HXX
+
+#include <assert.h>
+
+class OptionDef;
+
+/**
+ * Command line option parser.
+ */
+class OptionParser
+{
+	int argc;
+	char **argv;
+	char *option;
+	char *option_raw;
+	bool is_long;
+public:
+	/**
+	 * Constructs #OptionParser.
+	 */
+	OptionParser(int _argc, char **_argv)
+		: argc(_argc - 1), argv(_argv + 1),
+		  option(nullptr), option_raw(nullptr), is_long(false) { }
+
+	/**
+	 * Checks if there are command line entries to process.
+	 */
+	bool HasEntries() const { return argc > 0; }
+
+	/**
+	 * Gets the last parsed option.
+	 */
+	char *GetOption() {
+		assert(option_raw != nullptr);
+		return option_raw;
+	}
+
+	/**
+	 * Checks if current option is a specified option.
+	 */
+	bool CheckOption(const OptionDef& opt);
+
+	/**
+	 * Checks if current option is a specified option
+	 * or specified alternative option.
+	 */
+	bool CheckOption(const OptionDef& opt, const OptionDef &alt_opt) {
+		return CheckOption(opt) || CheckOption(alt_opt);
+	}
+
+	/**
+	 * Parses current command line entry.
+	 * Returns true on success, false otherwise.
+	 * Regardless of result, advances current position to the next
+	 * command line entry. 
+	 */
+	bool ParseNext();
+
+	/**
+	 * Checks if specified string is a command line option.
+	 */
+	static bool IsOption(const char *s) {
+		assert(s != nullptr);
+		return s[0] == '-';
+	}
+};
+
+#endif