[Debtags-commits] [svn] r1631 - in tagcoll/trunk: tagcoll tools

Enrico Zini enrico at costa.debian.org
Sat Mar 4 01:28:48 UTC 2006


Author: enrico
Date: Sat Mar  4 01:28:46 2006
New Revision: 1631

Modified:
   tagcoll/trunk/tagcoll/Commandline.cc
   tagcoll/trunk/tagcoll/Commandline.h
   tagcoll/trunk/tools/tagcoll.cc
Log:
Put the new commandline parser into use


Modified: tagcoll/trunk/tagcoll/Commandline.cc
==============================================================================
--- tagcoll/trunk/tagcoll/Commandline.cc	(original)
+++ tagcoll/trunk/tagcoll/Commandline.cc	Sat Mar  4 01:28:46 2006
@@ -19,10 +19,54 @@
 	return true;
 }
 
-
 namespace Tagcoll {
 namespace commandline {
 
+int Option::intValue() const
+{
+	return strtoul(stringValue().c_str(), NULL, 10);
+}
+
+static string fmtshort(char c, const std::string& usage)
+{
+	if (usage.empty())
+		return string("-") + c;
+	else
+		return string("-") + c + " " + usage;
+}
+
+static string fmtlong(const std::string& c, const std::string& usage)
+{
+	if (usage.empty())
+		return string("--") + c;
+	else
+		return string("--") + c + "=" + usage;
+}
+
+const std::string& Option::fullUsage() const
+{
+	if (m_fullUsage.empty())
+	{
+		for (vector<char>::const_iterator i = shortNames.begin();
+				i != shortNames.end(); i++)
+		{
+			if (!m_fullUsage.empty())
+				m_fullUsage += ", ";
+			m_fullUsage += fmtshort(*i, usage);
+		}
+
+		for (vector<string>::const_iterator i = longNames.begin();
+				i != longNames.end(); i++)
+		{
+			if (!m_fullUsage.empty())
+				m_fullUsage += ", ";
+			m_fullUsage += fmtlong(*i, usage);
+		}
+	}
+	return m_fullUsage;
+}
+	
+
 bool StringOption::parse(const char* str)
 {
 	if (!str) throw BadOption("no string argument found");
@@ -30,9 +74,35 @@
 	return true;
 }
 
-void OptionParser::add(Option* o)
+std::string IntOption::stringValue() const
 {
-	const vector<char>& shorts = o->shortNames();
+	return stringf::fmt(m_value);
+}
+
+bool IntOption::parse(const char* str)
+{
+	if (!str) throw BadOption("no int argument found");
+	// Ensure that we're all numeric
+	for (const char* s = str; *s; ++s)
+		if (!isdigit(*s))
+			throw BadOption(string("value ") + str + " must be numeric");
+	m_value = strtoul(str, 0, 10);
+	return true;
+}
+
+bool ExistingFileOption::parse(const char* str)
+{
+	if (!str) throw BadOption("no file argument found");
+	if (access(str, F_OK) == -1)
+		throw BadOption(string("file ") + str + " must exist");
+	m_value = str;
+	return true;
+}
+
+
+void OptionParser::addWithoutAna(Option* o)
+{
+	const vector<char>& shorts = o->shortNames;
 	for (vector<char>::const_iterator i = shorts.begin(); i != shorts.end(); i++)
 	{
 		map<char, Option*>::iterator j = m_short.find(*i);
@@ -42,7 +112,7 @@
 		m_short[*i] = o;
 	}
 
-	const vector<string>& longs = o->longNames();
+	const vector<string>& longs = o->longNames;
 	for (vector<string>::const_iterator i = longs.begin(); i != longs.end(); i++)
 	{
 		map<string, Option*>::iterator j = m_long.find(*i);
@@ -51,19 +121,20 @@
 					string("long option ") + *i + " is already mapped to " + j->second->name());
 		m_long[*i] = o;
 	}
+}
 
-	map<string, Option*>::iterator i = m_options.find(o->name());
-	if (i != m_options.end())
-		throw ConsistencyCheckException("option " + o->name() + " has already been added");
-	m_options[o->name()] = o;
+void OptionParser::add(Option* o)
+{
+	addWithoutAna(o);
+	m_options.push_back(o);	
 }
 
-const Option* OptionParser::option(const std::string& str) const
+void OptionParser::add(OptionGroup* group)
 {
-	map<string, Option*>::const_iterator i = m_options.find(str);
-	if (i == m_options.end())
-		return 0;
-	return i->second;
+	const vector<Option*>& v = group->options;
+	for (vector<Option*>::const_iterator i = v.begin(); i != v.end(); i++)
+		addWithoutAna(*i);
+	m_groups.push_back(group);	
 }
 
 iter OptionParser::parseConsecutiveSwitches(arglist& list, iter begin)
@@ -155,27 +226,29 @@
 
 
 
-const OptionParser* CommandParser::command(const std::string& str) const
-{
-	map<string, OptionParser*>::const_iterator i = m_commands.find(str);
-	if (i == m_commands.end())
-		return 0;
-	return i->second;
-}
-
 void CommandParser::add(const std::string& alias, OptionParser* o)
 {
 	map<string, OptionParser*>::iterator a = m_aliases.find(alias);
 	if (a != m_aliases.end())
 		throw ConsistencyCheckException("command " + alias + " has already been set to " + a->second->name());
+	m_aliases[alias] = o;
+}
 
-	map<string, OptionParser*>::iterator i = m_commands.find(o->name());
-	if (i != m_commands.end() && i->second != o)
-		throw ConsistencyCheckException("trying to add two different parsers both named " + o->name());
-	else
-		m_commands[o->name()] = o;
+void CommandParser::add(OptionParser& o)
+{
+	add(o.primaryAlias, &o);
+	for (vector<string>::const_iterator i = o.aliases.begin();
+			i != o.aliases.end(); ++i)
+		add(*i, &o);
+}
 
-	m_aliases[alias] = o;
+OptionParser* CommandParser::command(const std::string& name) const
+{
+	map<string, OptionParser*>::const_iterator i = m_aliases.find(name);
+	if (i == m_aliases.end())
+		return 0;
+	else
+		return i->second;
 }
 
 iter CommandParser::parse(arglist& list, iter begin)
@@ -185,21 +258,28 @@
 	while (cmd != list.end() && isSwitch(*cmd))
 		++cmd;
 
+	map<string, OptionParser*>::iterator a;
 	if (cmd == list.end())
-		return begin;
+	{
+		// No command has been found.  Try to see if there is a 'generic'
+		// OptionParser with the "" alias.
+		if ((a = m_aliases.find(string())) != m_aliases.end())
+			;
+		else
+			return begin;
+	} else {
+		// Handle the command
+		a = m_aliases.find(*cmd);
+		if (a == m_aliases.end())
+			throw BadOption("unknown command " + string(*cmd));
 
-	// Handle the command
-	string command(*cmd);
-	map<string, OptionParser*>::iterator a = m_aliases.find(command);
-	if (a == m_aliases.end())
-		throw BadOption("unknown command " + command);
-
-	// Remove the command from list
-	if (cmd == begin)
-		++begin;
-	list.erase(cmd);
+		// Remove the command from list
+		if (cmd == begin)
+			++begin;
+		list.erase(cmd);
+	}
 
-	m_last_command = a->second->name();
+	m_last_command = a->second;
 
 	// Invoke the selected parser on the list
 	return a->second->parse(list, begin);
@@ -207,302 +287,238 @@
 
 
 
+class WordWrapper
+{
+	const std::string& s;
+	size_t cursor;
 
-#if 0
-#include <tagcoll/stringf.h>
-#include <assert.h>
-#include <ctype.h>
-#include <stdlib.h>
-#include <stdio.h>
+public:
+	WordWrapper(const std::string& s) : s(s), cursor(0) {}
 
-using namespace std;
-using namespace stringf;
+	void restart() { cursor = 0; }
 
-string CommandlineParser::WordWrapper::get(unsigned int width) throw ()
-{
-	if (cursor >= s.size())
-		return "";
-	
-	// Find the last work break before `width'
-	unsigned int brk = cursor;
-	for (unsigned int j = cursor; j < s.size() && j < cursor + width; j++)
+	bool hasData() const { return cursor < s.size(); }
+
+	string get(unsigned int width)
 	{
-		if (s[j] == '\n')
+		if (cursor >= s.size())
+			return "";
+
+		// Find the last work break before `width'
+		unsigned int brk = cursor;
+		for (unsigned int j = cursor; j < s.size() && j < cursor + width; j++)
 		{
-			brk = j;
-			break;
-		} else if (!isspace(s[j]) && (j + 1 == s.size() || isspace(s[j + 1])))
-			brk = j + 1;
-	}
-	if (brk == cursor)
-		brk = cursor + width;
-	
-	string res;
-	if (brk >= s.size())
-	{
-		res = string(s, cursor, string::npos);
-		cursor = s.size();
-	} else {
-		res = string(s, cursor, brk - cursor);
-		cursor = brk;
-		while (cursor < s.size() && isspace(s[cursor]))
-			cursor++;
+			if (s[j] == '\n')
+			{
+				brk = j;
+				break;
+			} else if (!isspace(s[j]) && (j + 1 == s.size() || isspace(s[j + 1])))
+				brk = j + 1;
+		}
+		if (brk == cursor)
+			brk = cursor + width;
+
+		string res;
+		if (brk >= s.size())
+		{
+			res = string(s, cursor, string::npos);
+			cursor = s.size();
+		} else {
+			res = string(s, cursor, brk - cursor);
+			cursor = brk;
+			while (cursor < s.size() && isspace(s[cursor]))
+				cursor++;
+		}
+		return res;
 	}
-	return res;
-}
+};
 
-/*
-string CommandlineParser::WordWrapper::get(unsigned int width) throw ()
+class HelpWriter
 {
-	if (i >= s.size())
-		return "";
-	
-	int k = width;
-	while (k > 0 && k + i < s.size() && !isspace(s[i + k]))
-		k--;
-	if (k == 0)
-		k = width;
+	// Width of the console
+	std::ostream& out;
+	int m_width;
 
-	string res(s, i, k);
-	i += k;
-	return res;
-}
-*/
+public:
+	HelpWriter(std::ostream& out);
 
-int CommandlineParser::option::intVal() const throw ()
-{
-	return atoi(_value.c_str());
-}
+	// Write 'size' spaces to out
+	void pad(size_t size);
+
+	// Output an item from a list.  The first bulletsize columns will be used to
+	// output bullet, the rest will have text, wordwrapped and properly aligned
+	void outlist(const std::string& bullet, size_t bulletsize, const std::string& text);
 
-const CommandlineParser::option& CommandlineParser::get(const string& name) const throw ()
+	void outstring(const std::string& str);
+};
+
+HelpWriter::HelpWriter(std::ostream& out) : out(out)
 {
-	map<string, int>::const_iterator i = byname.find(name);
-	if (i == byname.end())
-	{
-		fprintf(stderr, "Program error: requested info about nonexistant commandline option \"%.*s\".\nAvailable: \n", PFSTR(name));
-		for (i = byname.begin(); i != byname.end(); i++)
-			fprintf(stderr, "  \"%.*s\"\n", PFSTR(i->first));
-	}
-	assert (i != byname.end());
-	const option& o = options[i->second];
-	return o;
+	char* columns = getenv("COLUMNS");
+	m_width = columns ? atoi(columns) : 80;
 }
 
-void CommandlineParser::add(const string& name, char shortopt, const string& longopt, const
-		string& help, const string& valname, bool val_optional) throw ()
+void HelpWriter::pad(size_t size)
 {
-	options.push_back(option(name, shortopt, longopt, help, valname, val_optional));
-	if (shortopt)
-		shortopts.insert(pair<char, int>(shortopt, options.size() - 1));
-	if (longopt.size())
-		longopts.insert(pair<string, int>(longopt, options.size() - 1));
-	byname.insert(pair<string, int>(name, options.size() - 1));
+	for (size_t i = 0; i < size; i++) out << " ";
 }
 
-void CommandlineParser::printHelp() throw ()
+void HelpWriter::outlist(const std::string& bullet, size_t bulletsize, const std::string& text)
 {
-	char* columns = getenv("COLUMNS");
-	int width = columns ? atoi(columns) : 80;
-	unsigned int summax = 0;
-	vector<string> summaries;
-
-	// Print the first usage line
-	fprintf(stderr, "Usage: %.*s %.*s\n",
-			PFSTR(argv0), PFSTR(cmdline_summary));
-	
-	// Prepare switch summaries and compute their maximum width
-	for (unsigned int i = 0; i < options.size(); i++)
-	{
-		string s;
-		if (options[i]._shortopt)
-			s += string("-") + options[i]._shortopt;
-		if (options[i]._longopt.size())
-		{
-			if (s.size())
-				s += ", ";
-			s += "--" + options[i]._longopt;
-		}
-		if (options[i]._valname.size())
-		{
-			string vname = options[i]._val_optional ?
-							"[" + options[i]._valname + "]"
-							: options[i]._valname;
-			if (options[i]._longopt.size())
-				s += "=" + vname;
-			else
-				s += " " + vname;
-		}
-		if (s.size() > summax)
-			summax = s.size();
-		summaries.push_back(s);
-	}
-
-	// Print the help
-	for (unsigned int i = 0; i < options.size(); i++)
-	{
-		WordWrapper ww(options[i]._help);
+	WordWrapper wrapper(text);
+	size_t rightcol = m_width - bulletsize;
 
-		string h = ww.get(width - summax - 4);
-		fprintf(stderr, "  %-*.*s  %.*s\n",
-					summax, summax, summaries[i].c_str(), PFSTR(h));
+	out << bullet;
+	pad(bulletsize - bullet.size());
+	out << wrapper.get(rightcol);
+	out << endl;
 
-		while (ww.hasData())
-		{
-			string h = ww.get(width - summax - 4);
-			fprintf(stderr, "  %-*.*s  %.*s\n",
-					summax, summax, "", PFSTR(h));
-		}
+	while (wrapper.hasData())
+	{
+		pad(bulletsize);
+		out << wrapper.get(rightcol);
+		out << endl;
 	}
+}
 
-	fputc('\n', stderr);
+void HelpWriter::outstring(const std::string& str)
+{
+	WordWrapper wrapper(str);
 
-	fprintf(stderr, "%.*s\n", PFSTR(description));
-	/*
-	WordWrapper ww(description);
-	while (ww.hasData())
+	while (wrapper.hasData())
 	{
-		string h = ww.get(width);
-		fprintf(stderr, "%.*s\n", PFSTR(h));
+		out << wrapper.get(m_width);
+		out << endl;
 	}
-	*/
 }
 
-bool CommandlineParser::parseLongOption(const string& name, const string& value)
-	throw ()
+void Help::outputVersion(std::ostream& out)
 {
-	// Have argument
-	map<string, int>::iterator o = longopts.find(name);
-	if (o == longopts.end())
-	{
-		fprintf(stderr, "Unknown option name: %.*s\n", PFSTR(name));
-		return false;
-	}
-	options[o->second]._defined = true;
+	out << m_app << " version " << m_ver << endl;
+}
+
+void Help::outputHelp(std::ostream& out, const CommandParser& cp)
+{
+	std::map<std::string, OptionParser*> m_info;
 	
-	if (options[o->second]._valname.size())
-	{
-		if (value.size())
-			options[o->second]._value = value;
-		else if (!options[o->second]._val_optional)
-		{
-			fprintf(stderr, "Option %.*s requires the %.*s argument\n",
-					PFSTR(name), PFSTR(options[o->second]._valname));
-			return false;
+	// Dig informations from cp
+	for(map<string, OptionParser*>::const_iterator i = cp.m_aliases.begin();
+			i != cp.m_aliases.end(); i++)
+	{
+		if (i->first == string())
+			continue;
+		map<string, OptionParser*>::iterator j = m_info.find(i->second->name());
+		if (j == m_info.end())
+			m_info[i->second->name()] = i->second;
+	}
+
+	HelpWriter writer(out);
+
+	// Compute the maximum length of alias names
+	size_t maxAliasSize = 0;
+	for (map<string, OptionParser*>::const_iterator i = m_info.begin();
+			i != m_info.end(); i++)
+	{
+		const string& str = i->second->primaryAlias;
+		if (maxAliasSize < str.size())
+			maxAliasSize = str.size();
+	}
+
+	out << "Usage: " << m_app << " [options] " << cp.usage << endl;
+	out << endl;
+	writer.outstring("Description: " + cp.description);
+	out << endl;
+	out << "Commands are:" << endl;
+	out << endl;
+
+	// Print the list
+	for (map<string, OptionParser*>::const_iterator i = m_info.begin();
+			i != m_info.end(); i++)
+	{
+		string aliases;
+		const vector<string>& v = i->second->aliases;
+		if (!v.empty())
+		{
+			aliases += "  May also be invoked as ";
+			for (vector<string>::const_iterator j = v.begin();
+					j != v.end(); j++)
+				if (j == v.begin())
+					aliases += *j;
+				else
+					aliases += " or " + *j;
+			aliases += ".";
 		}
+
+		writer.outlist(" " + i->second->primaryAlias, maxAliasSize + 3, i->second->description + "." + aliases);
 	}
-	return true;
+
+	out << endl;
 }
 
-bool CommandlineParser::parseShortOption(char opt, const string& value)
-	throw ()
+void Help::outputHelp(std::ostream& out, const OptionParser& o)
 {
-	// Have argument
-	map<char, int>::iterator o = shortopts.find(opt);
-	if (o == shortopts.end())
+	HelpWriter writer(out);
+
+	out << "Usage: " << m_app << " [options] " << o.primaryAlias << " [options] " << o.usage << endl;
+	out << endl;
+	if (!o.aliases.empty())
 	{
-		fprintf(stderr, "Unknown option: `%c'\n", opt);
-		return false;
+		out << "Command aliases: ";
+		for (vector<string>::const_iterator i = o.aliases.begin();
+				i != o.aliases.end(); i++)
+			if (i == o.aliases.begin())
+				out << *i;
+			else
+				out << ", " << *i;
+		out << "." << endl;
+		out << endl;
 	}
-	options[o->second]._defined = true;
+	writer.outstring("Description: " + o.description);
 
-	if (options[o->second]._valname.size())
-	{
-		if (value.size())
-			options[o->second]._value = value;
-		else if (!options[o->second]._val_optional)
+	// Compute size of option display
+	size_t maxLeftCol = 0;
+	for (vector<OptionGroup*>::const_iterator i = o.m_groups.begin();
+			i != o.m_groups.end(); i++)
+		for (vector<Option*>::const_iterator j = (*i)->options.begin();
+				j != (*i)->options.end(); j++)
 		{
-			fprintf(stderr, "Option `%c' requires the %.*s argument\n",
-					opt, PFSTR(options[o->second]._valname));
-			return false;
+			size_t w = (*j)->fullUsage().size();
+			if (w > maxLeftCol)
+				maxLeftCol = w;
 		}
+	for (vector<Option*>::const_iterator j = o.m_options.begin();
+			j != o.m_options.end(); j++)
+	{
+		size_t w = (*j)->fullUsage().size();
+		if (w > maxLeftCol)
+			maxLeftCol = w;
 	}
-	return true;
-}
 
-bool CommandlineParser::parse(int& argc, const char**& argv) throw ()
-{
-	int nargc = 1;
-	bool success = true;
-	bool parses_opts = true;
-
-	for (int i = 1; i < argc; i++)
+	if (maxLeftCol)
 	{
-		if (parses_opts && argv[i][0] == '-' && argv[i][1] != 0)
+		// Output the options
+		out << endl;
+		out << "Options are:" << endl;
+		for (vector<OptionGroup*>::const_iterator i = o.m_groups.begin();
+				i != o.m_groups.end(); i++)
 		{
-			if (argv[i][1] == '-')
-			{
-				if (argv[i][2] == 0)
-					// -- option terminator
-					parses_opts = false;
-				else
-				{
-					// Long option
-					string optName = string(argv[i], 2, string::npos);
-					unsigned int eqsign = optName.find('=');
-					if (eqsign == string::npos)
-					{
-						// No argument
-						if (!parseLongOption(optName))
-							success = false;
-					} else {
-						// With argument
-						if (!parseLongOption(optName.substr(0, eqsign),
-									optName.substr(eqsign + 1)))
-							success = false;
-					}
-				}
-			}
-			else
-			{
-				// Short option(s)
-				if (argv[i][2] == 0)
-				{
-					// Unpacked short option, might have an argument
-					char opt = argv[i][1];
-					map<char, int>::iterator o = shortopts.find(opt);
-					if (o == shortopts.end())
-					{
-						fprintf(stderr, "Unknown option: `%c'\n", opt);
-						return false;
-					}
-					if (options[o->second]._valname.size())
-					{
-						// Accepts arguments
-						if (argv[i + 1] && argv[i + 1][0] != '-')
-						{
-							// Is followed by an argument
-							i++;
-							if (!parseShortOption(opt, argv[i]))
-								success = false;
-						} else
-							// Is not followed by an argument
-							if (!parseShortOption(opt))
-								success = false;
-					} else
-						// Does not accept arguments
-						if (!parseShortOption(opt))
-							success = false;
-				} else {
-					// Packed short options
-					for (const char* s = argv[i] + 1; *s; s++)
-						if (!parseShortOption(*s))
-							success = false;
-				}
-			}
+			if (!(*i)->description.empty())
+				writer.outstring((*i)->description + ":");
+			for (vector<Option*>::const_iterator j = (*i)->options.begin();
+					j != (*i)->options.end(); j++)
+				writer.outlist(" " + (*j)->fullUsage(), maxLeftCol + 3, (*j)->description);
 		}
-		else
+		if (!o.m_options.empty())
 		{
-			// Not a switch: keep it in the new argv
-			if (i != nargc)
-				argv[nargc] = argv[i];
-			nargc++;
-		}	
-	}
-	argc = nargc;
-	argv[nargc] = 0;
-	return success;
+			out << endl;
+			writer.outstring("Other options:");
+			for (vector<Option*>::const_iterator j = o.m_options.begin();
+					j != o.m_options.end(); j++)
+				writer.outlist(" " + (*j)->fullUsage(), maxLeftCol + 3, (*j)->description);
+		}
+	}
+	out << endl;
 }
-#endif
 
 }
 }
@@ -679,6 +695,7 @@
 			yell.addAlias("yell");
 			add(&random);
 			add(&yell);
+			aliases.push_back("mess");
 		}
 
 		BoolOption random;
@@ -706,9 +723,8 @@
 	TestCParser() :
 		CommandParser("test")
 	{
-		add("scramble", &scramble);
-		add("mess", &scramble);
-		add("fix", &fix);
+		add(scramble);
+		add(fix);
 	}
 
 	Scramble scramble;
@@ -729,7 +745,7 @@
 		iter i = parser.parseList(opts);
 		gen_ensure(i == opts.end());
 		gen_ensure_equals(opts.size(), 0u);
-		gen_ensure_equals(parser.lastCommand(), "scramble");
+		gen_ensure_equals(parser.lastCommand(), &parser.scramble);
 		gen_ensure_equals(parser.scramble.yell.stringValue(), "foo");
 		gen_ensure_equals(parser.scramble.random.boolValue(), true);
 		gen_ensure_equals(parser.fix.yell.stringValue(), string());
@@ -745,7 +761,7 @@
 		iter i = parser.parseList(opts);
 		gen_ensure(i == opts.end());
 		gen_ensure_equals(opts.size(), 0u);
-		gen_ensure_equals(parser.lastCommand(), "fix");
+		gen_ensure_equals(parser.lastCommand(), &parser.fix);
 		gen_ensure_equals(parser.scramble.yell.stringValue(), string());
 		gen_ensure_equals(parser.scramble.random.boolValue(), false);
 		gen_ensure_equals(parser.fix.yell.stringValue(), "foo");

Modified: tagcoll/trunk/tagcoll/Commandline.h
==============================================================================
--- tagcoll/trunk/tagcoll/Commandline.h	(original)
+++ tagcoll/trunk/tagcoll/Commandline.h	Sat Mar  4 01:28:46 2006
@@ -6,6 +6,7 @@
 #include <vector>
 #include <list>
 #include <map>
+#include <ostream>
 
 namespace Tagcoll {
 namespace commandline {
@@ -59,23 +60,27 @@
 class Option
 {
 	std::string m_name;
-	std::vector<char> m_shortNames;
-	std::vector<std::string> m_longNames;
+	mutable std::string m_fullUsage;
 
 public:
 	Option(const std::string& name) : m_name(name) {}
+	Option(const std::string& name, char shortName, const std::string& longName) : m_name(name)
+	{
+		if (shortName != 0)
+			shortNames.push_back(shortName);
+		if (!longName.empty())
+			longNames.push_back(longName);
+	}
 	virtual ~Option() {}
 
 	const std::string& name() const { return m_name; }
 
-	const std::vector<char>& shortNames() const { return m_shortNames; }
-	const std::vector<std::string>& longNames() const { return m_longNames; }
-
-	void addAlias(char c) { m_shortNames.push_back(c); }
-	void addAlias(const std::string& str) { m_longNames.push_back(str); }
+	void addAlias(char c) { shortNames.push_back(c); }
+	void addAlias(const std::string& str) { longNames.push_back(str); }
 
 	virtual bool boolValue() const = 0;
 	virtual std::string stringValue() const = 0;
+	virtual int intValue() const;
 
 	/**
 	 * Signal that the option has been found, with the given argument (or 0 if
@@ -85,6 +90,15 @@
 	 *   true if it used the argument, else false
 	 */
 	virtual bool parse(const char* str = 0) = 0;
+
+	/// Return a full usage message including all the aliases for this option
+	const std::string& fullUsage() const;
+
+	std::vector<char> shortNames;
+	std::vector<std::string> longNames;
+
+	std::string usage;
+	std::string description;
 };
 
 /// Boolean option
@@ -94,6 +108,8 @@
 public:
 	BoolOption(const std::string& name)
 		: Option(name), m_value(false) {}
+	BoolOption(const std::string& name, char shortName, const std::string& longName)
+		: Option(name, shortName, longName), m_value(false) {}
 
 	bool boolValue() const { return m_value; }
 	std::string stringValue() const { return m_value ? "true" : "false"; }
@@ -107,7 +123,61 @@
 	std::string m_value;
 public:
 	StringOption(const std::string& name)
-		: Option(name) {}
+		: Option(name)
+	{
+		usage = "<val>";
+	}
+	StringOption(const std::string& name, char shortName, const std::string& longName)
+		: Option(name, shortName, longName)
+	{
+		usage = "<val>";
+	}
+
+	bool boolValue() const { return !m_value.empty(); }
+	std::string stringValue() const { return m_value; }
+
+	bool parse(const char* str);
+};
+
+// Option needing a compulsory int value
+class IntOption : public Option
+{
+	bool m_has_value;
+	int m_value;
+
+public:
+	IntOption(const std::string& name)
+		: Option(name), m_has_value(false), m_value(0)
+	{
+		usage = "<num>";
+	}
+	IntOption(const std::string& name, char shortName, const std::string& longName)
+		: Option(name, shortName, longName), m_has_value(false), m_value(0)
+	{
+		usage = "<num>";
+	}
+
+	bool boolValue() const { return m_has_value; }
+	int intValue() const { return m_value; }
+	std::string stringValue() const;
+
+	bool parse(const char* str);
+};
+
+class ExistingFileOption : public Option
+{
+	std::string m_value;
+public:
+	ExistingFileOption(const std::string& name)
+		: Option(name)
+	{
+		usage = "<file>";
+	}
+	ExistingFileOption(const std::string& name, char shortName, const std::string& longName)
+		: Option(name, shortName, longName)
+	{
+		usage = "<file>";
+	}
 
 	bool boolValue() const { return !m_value.empty(); }
 	std::string stringValue() const { return m_value; }
@@ -115,44 +185,70 @@
 	bool parse(const char* str);
 };
 
+class OptionGroup
+{
+
+public:
+	void add(Option* o) { options.push_back(o); }
+
+	std::vector<Option*> options;
+
+	std::string description;
+};
+
 /// Parser of many short or long switches all starting with '-'
 class OptionParser : public Parser
 {
 	std::map<char, Option*> m_short;
 	std::map<std::string, Option*> m_long;
-	std::map<std::string, Option*> m_options;
 
 	/// Parse a consecutive sequence of switches
 	iter parseConsecutiveSwitches(arglist& list, iter begin);
 
+	void addWithoutAna(Option* o);
+
+protected:
+	std::vector<OptionGroup*> m_groups;
+	std::vector<Option*> m_options;
+
 public:
 	OptionParser(const std::string& name)
-		: Parser(name) {}
+		: Parser(name), primaryAlias(name) {}
 
 	void add(Option* o);
-	const Option* option(const std::string& str) const;
+	void add(OptionGroup* group);
 
 	/**
 	 * Parse all the switches in list, leaving only the non-switch arguments or
 	 * the arguments following "--"
 	 */
 	virtual iter parse(arglist& list, iter begin);
+
+	std::string primaryAlias;
+	std::vector<std::string> aliases;
+	std::string usage;
+	std::string description;
+
+	friend class Help;
 };
 
 class CommandParser : public Parser
 {
-	std::string m_last_command;
+	OptionParser* m_last_command;
+
+	void add(const std::string& alias, OptionParser* o);
+
+protected:
 	std::map<std::string, OptionParser*> m_aliases;
-	std::map<std::string, OptionParser*> m_commands;
 
 public:
 	CommandParser(const std::string& name)
-		: Parser(name) {}
+		: Parser(name), m_last_command(0) {}
 
-	const std::string& lastCommand() const { return m_last_command; }
-	const OptionParser* command(const std::string& str) const;
+	OptionParser* lastCommand() const { return m_last_command; }
+	OptionParser* command(const std::string& name) const;
 
-	void add(const std::string& alias, OptionParser* o);
+	void add(OptionParser& o);
 
 	/**
 	 * Look for a command as the first non-switch parameter found, then invoke
@@ -163,85 +259,26 @@
 	 * If no commands have been found, returns begin.
 	 */
 	virtual iter parse(arglist& list, iter begin);
-};
 
-#if 0
-class Option
-{
-protected:
-	std::string _name;
-	std::string _value;
-	char _shortopt;
-	std::string _longopt;
-	std::string _help;
-	std::string _valname;
-	bool _val_optional;
-	bool _defined;
-
-public:
-	Option(const std::string& name, char shortopt, const std::string& longopt, const
-			std::string& help, const std::string& valname, bool val_optional)
-		throw() : _name(name), _shortopt(shortopt), _longopt(longopt),
-					_help(help), _valname(valname),
-						_val_optional(val_optional), _defined(false) {}
-
-	bool defined() const throw () { return _defined; }
-	bool hasValue() const throw () { return _value.size() > 0; }
-	std::string stringVal() const throw () { return _value; }
-	int intVal() const throw ();
+	std::string usage;
+	std::string description;
 
-	friend class Parser;
+	friend class Help;
 };
 
-class Parser
+class Help
 {
-protected:
-	class WordWrapper
-	{
-	protected:
-		const std::string& s;
-		unsigned int cursor;
-		
-	public:
-		WordWrapper(const std::string& s) throw () : s(s), cursor(0) {}
-
-		void restart() throw () { cursor = 0; }
-
-		bool hasData() const throw () { return cursor < s.size(); }
-		
-		std::string get(unsigned int width) throw ();
-	};
-
-	std::vector<option> options;
-	std::map<char, int> shortopts;
-	std::map<std::string, int> longopts;
-	std::map<std::string, int> byname;
-	std::string argv0;
-	std::string cmdline_summary;
-	std::string description;
-	
-	bool parseLongOption(const std::string& name, const std::string& value = "") throw ();
-	bool parseShortOption(char opt, const std::string& value = "") throw ();
-	
-public:
-	CommandlineParser(const std::string& argv0, const std::string& cmdline_summary,
-			const std::string& description) throw ()
-		: argv0(argv0), cmdline_summary(cmdline_summary), description(description)
-	{
-		add("help", 0, "help", "print this help message");
-	}
+	std::string m_app;
+	std::string m_ver;
 
-	void add(const std::string& name, char shortopt, const std::string& longopt, const
-			std::string& help, const std::string& valname = "",
-			bool val_optional = false) throw ();
+public:
+	Help(const std::string& app, const std::string& ver)
+		: m_app(app), m_ver(ver) {}
 	
-	void printHelp() throw ();
-
-	bool parse(int& argc, const char**& argv) throw ();
-
-	const option& get(const std::string& name) const throw ();
+	void outputVersion(std::ostream& out);
+	void outputHelp(std::ostream& out, const CommandParser& cp);
+	void outputHelp(std::ostream& out, const OptionParser& cp);
 };
-#endif
 
 }
 }

Modified: tagcoll/trunk/tools/tagcoll.cc
==============================================================================
--- tagcoll/trunk/tools/tagcoll.cc	(original)
+++ tagcoll/trunk/tools/tagcoll.cc	Sat Mar  4 01:28:46 2006
@@ -27,8 +27,6 @@
 #define VERSION "unknown"
 #endif
 
-#include "CommandlineParser.h"
-
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <unistd.h>
@@ -42,6 +40,7 @@
 
 #include <tagcoll/stringf.h>
 #include <tagcoll/Exception.h>
+#include <tagcoll/Commandline.h>
 
 #include <tagcoll/CardinalityStore.h>
 #include <tagcoll/SmartHierarchy.h>
@@ -61,10 +60,299 @@
 #include <tagcoll/Expression.h>
 
 #include <algorithm>
+#include <iostream>
 
 using namespace std;
 using namespace Tagcoll;
 
+namespace Tagcoll {
+namespace commandline {
+
+struct CommandlineParser : public CommandParser
+{
+	struct HelpGroup : public OptionGroup
+	{
+		BoolOption* help;
+		BoolOption* version;
+
+		HelpGroup()
+		{
+			add(help = new BoolOption("help", 'h', "help"));
+			add(version = new BoolOption("version", 'V', "version"));
+			help->shortNames.push_back('?');
+			help->description = "show commandline help";
+			version->description = "show program version";
+		}
+		~HelpGroup()
+		{
+			delete help;
+		}
+	} helpGroup;
+
+	struct InputGroup : public OptionGroup
+	{
+		ExistingFileOption* derived;
+		ExistingFileOption* extimpl;
+		ExistingFileOption* rename;
+		ExistingFileOption* patch;
+		StringOption* rmunfaceted;
+		StringOption* rmtags;
+
+		InputGroup()
+		{
+			add(derived		= new ExistingFileOption("derived-tags-from", 'e', "derived"));
+			add(extimpl		= new ExistingFileOption("extimpl", 'i', "implications-from"));
+			add(rename		= new ExistingFileOption("rename", 'r', "rename-from"));
+			add(patch		= new ExistingFileOption("patch", 'p', "patch-with"));
+			add(rmunfaceted	= new StringOption("rmunfaceted", 0, "remove-unfaceted"));
+			add(rmtags		= new StringOption("rmtags", 0, "remove-tags"));
+
+			derived->description = "expand derived tags using the given list";
+			extimpl->description = "use an external list of implications";
+			rename->description = "rename tags using the given mapping list";
+			patch->description = "apply patches from the given tag patch file";
+			patch->longNames.push_back("patch");
+			rmunfaceted->description = "while parsing, remove all tags with no facet part";
+			rmtags->usage = "<expression>";
+			rmtags->description = "while parsing, remove all tags matching the given tag expression";
+		}
+		~InputGroup()
+		{
+			delete derived; delete extimpl; delete rename; delete patch;
+			delete rmunfaceted; delete rmtags;
+		}
+	} inputGroup;
+
+	struct OutputGroup : public OptionGroup
+	{
+		BoolOption* group;
+		BoolOption* redundant;
+		
+		OutputGroup()
+		{
+			add(group		= new BoolOption("group", 'g', "group"));
+			add(redundant	= new BoolOption("redundant", 0, "redundant"));
+			group->description = "group items with the same tagset in the output collection";
+			group->longNames.push_back("group-items");
+			redundant->description = "when implications are provided, expand them explicitly in the output";
+		}
+		~OutputGroup()
+		{
+			delete group;
+			delete redundant;
+		}
+	} outputGroup;
+
+	struct HierarchyGroup : public OptionGroup
+	{
+		IntOption* flatten;
+		IntOption* filter;
+
+		HierarchyGroup()
+		{
+			add(flatten = new IntOption("flatten", 0, "flatten-threshold"));
+			add(filter = new IntOption("filter", 'f', "filter"));
+
+			flatten->description = "set the number of total items below which a branch is flattened when using the \"hierarchy\" command (defaults to 0, meaning \"don't flatten\")";
+			filter->description = "filter out the tags with cardinality less than the given value (defaults to not filter; currently only works when building hierarchies)";
+		}
+
+		~HierarchyGroup()
+		{
+			delete flatten;
+			delete filter;
+		}
+	} hierarchyGroup;
+
+	struct Generic : public OptionParser
+	{
+		Generic(CommandlineParser* cp) : OptionParser("")
+		{
+			add(&cp->helpGroup);
+		}
+	} generic;
+	struct Help : public OptionParser
+	{
+		Help(CommandlineParser* cp) : OptionParser("help")
+		{
+			usage = "[command]";
+			description = "print help informations";
+		}
+	} help;
+	struct Copy : public OptionParser
+	{
+		Copy(CommandlineParser* cp) : OptionParser("copy")
+		{
+			usage = "[files...]";
+			description = "output the collection";
+			add(&cp->inputGroup);
+			add(&cp->outputGroup);
+			add(&cp->helpGroup);
+			aliases.push_back("cat");
+		}
+	} copy;
+	struct Reverse : public OptionParser
+	{
+		StringOption* untaggedTag;
+
+		Reverse(CommandlineParser* cp) : OptionParser("reverse")
+		{
+			add(untaggedTag = new StringOption("untagged-tag", 0, "untagged-tag"));
+			untaggedTag->usage = "<tag>";
+			untaggedTag->description = "set item name to use for associating untagged items when using the \"reverse\" command.  If not specified, untagged items are not included in the output";
+				
+			usage = "[files...]";
+			description = "\"reverse\" the collection, outputting one with items associated to tags";
+			add(&cp->helpGroup);
+		}
+		~Reverse()
+		{
+			delete untaggedTag;
+		}
+	} reverse;
+	struct Diff : public OptionParser
+	{
+		Diff(CommandlineParser* cp) : OptionParser("diff")
+		{
+			usage = "<file1> <file2>";
+			description = "output a tag patch file with the differences between two files";
+			add(&cp->helpGroup);
+		}
+	} diff;
+	struct Related : public OptionParser
+	{
+		IntOption* distance;
+		
+		Related(CommandlineParser* cp) : OptionParser("related")
+		{
+			add(distance = new IntOption("distance", 'd', "distance"));
+			distance->description = "set the maximum distance to use for the \"related\" command (defaults to 0)";
+			
+			usage = "<item> [files...]";
+			description = "print a list of items related to the given one";
+			add(&cp->helpGroup);
+		}
+		~Related()
+		{
+			delete distance;
+		}
+	} related;
+	struct Implications : public OptionParser
+	{
+		Implications(CommandlineParser* cp) : OptionParser("implications")
+		{
+			usage = "[files...]";
+			description = "compute a list of tag implications";
+			add(&cp->helpGroup);
+		}
+	} implications;
+	struct Hierarchy : public OptionParser
+	{
+		Hierarchy(CommandlineParser* cp) : OptionParser("hierarchy")
+		{
+			usage = "[files...]";
+			description = "build a smart hierarchy with the collection data";
+			add(&cp->hierarchyGroup);
+			add(&cp->helpGroup);
+		}
+	} hierarchy;
+	struct CleanHierarchy : public OptionParser
+	{
+		CleanHierarchy(CommandlineParser* cp) : OptionParser("cleanhierarchy")
+		{
+			usage = "[files...]";
+			description = "build a cleaned smart hierarchy with the collection data";
+			add(&cp->hierarchyGroup);
+			add(&cp->helpGroup);
+		}
+	} cleanhierarchy;
+	struct FindSpecials : public OptionParser
+	{
+		FindSpecials(CommandlineParser* cp) : OptionParser("findspecials")
+		{
+			usage = "[files...]";
+			description =
+				"generate a smart hierarchy and print, for each toplevel tag, "
+				"what are the items that make it toplevel instead of going below "
+				"another tag";
+			add(&cp->hierarchyGroup);
+			add(&cp->helpGroup);
+		}
+	} findspecials;
+	struct Grep : public OptionParser
+	{
+		Grep(CommandlineParser* cp) : OptionParser("grep")
+		{
+			usage = "<expression> [files...]";
+			description = "output the collection of tags that match the given tag expression";
+			add(&cp->helpGroup);
+		}
+	} grep;
+	struct Items : public OptionParser
+	{
+		Items(CommandlineParser* cp) : OptionParser("items")
+		{
+			usage = "[files...]";
+			description = "output only the items of the input collection";
+			add(&cp->helpGroup);
+		}
+	} items;
+
+	arglist args;
+
+	CommandlineParser(int argc, const char* argv[]) : CommandParser("main"),
+		generic(this),
+		help(this), copy(this), reverse(this), diff(this), related(this), implications(this),
+		hierarchy(this), cleanhierarchy(this), findspecials(this), grep(this), items(this)
+	{
+		add(generic);
+		add(help);
+		add(copy);
+		add(reverse);
+		add(diff);
+		add(related);
+		add(implications);
+		add(hierarchy);
+		add(cleanhierarchy);
+		add(findspecials);
+		add(grep);
+		add(items);
+
+		usage = "<command> [options and arguments]";
+		description = "Perform various operations on a tagged collection";
+
+		for (int i = 1; i < argc; i++)
+			args.push_back(argv[i]);
+	}
+
+	arglist parse()
+	{
+		parseList(args);
+		if (!lastCommand())
+			throw commandline::BadOption("could not understand the command to execute");
+		return args;
+	}
+
+	bool hasNext() const { return !args.empty(); }
+	string next()
+	{
+		if (args.empty())
+			return string();
+		string res(*args.begin());
+		args.erase(args.begin());
+		return res;
+	}
+
+#if 0
+		//opts.add("verbose", 'v', "verbose", "enable verbose output");
+		//opts.add("debug", 0, "debug", "enable debugging output (including verbose output)");
+#endif
+};
+
+}
+}
+
+
 bool isdir(const std::string& file)
 {
 	struct stat st;
@@ -278,92 +566,6 @@
 	ItemsOnly(Consumer<ITEM, TAG>& cons) : Filter<ITEM, TAG>(cons) {}
 };
 
-class CommandlineParserWithCommand : public CommandlineParser
-{
-protected:
-	map<string, int> command_map;
-
-public:
-	CommandlineParserWithCommand(const std::string& argv0,
-									const std::string& cmdline_summary,
-									const std::string& description) throw ()
-		: CommandlineParser(argv0, cmdline_summary, description)
-	{
-		add("version", 0, "version", "print the program version, then exit");
-	}
-
-	void addCommand(const std::string name, int id) throw ()
-	{
-		command_map[name] = id;
-	}
-
-	int parse(int& argc, const char**& argv) throw ()
-	{
-		if (!CommandlineParser::parse(argc, argv))
-		{
-			printHelp();
-			return false;
-		}
-		if (get("help").defined())
-		{
-			printHelp();
-			exit(0);
-		}
-		if (get("version").defined())
-		{
-			printf("%s ver." VERSION "\n", APPNAME);
-			exit(0);
-		}
-		if (argc == 1)
-		{
-			printHelp();
-			exit(1);
-		}
-
-		string command_string = argv[1];
-		map<string, int>::const_iterator cmap_i = command_map.find(command_string);
-		if (cmap_i == command_map.end())
-		{
-			fprintf(stderr, "Invalid command: \"%.*s\"\n", PFSTR(command_string));
-			printHelp();
-			exit(1);
-		}
-
-		for (int i = 1; i < argc; i++)
-			argv[i] = argv[i + 1];
-		--argc;
-		
-		return cmap_i->second;
-	}
-};
-
-class CommandlineArgs
-{
-protected:
-	int argc;
-	const char** argv;
-	int _next;
-
-public:
-	CommandlineArgs(int argc, const char* argv[]) throw () : argc(argc), argv(argv), _next(1) {}
-
-	// Return true if there is another argument left in the list
-	bool hasNext() const throw () { return argc >= _next + 1; }
-
-	// Return the next argument in the list
-	string next() throw ()
-	{
-		if (hasNext())
-		{
-			return argv[_next++];
-		} else {
-			return "-";
-		}
-	}
-};
-
-enum valid_command { COPY, DIFF, RELATED, IMPLICATIONS, HIERARCHY, CLEANHIERARCHY, REVERSE, FINDSPECIALS, GREP, ITEMS };
-
 class Reader
 {
 	// Prepare the input filter chain
@@ -381,35 +583,35 @@
 	FilterTagsByExpression<string, string> filterByExpression;
 
 public:
-	Reader(CommandlineParserWithCommand& opts, valid_command cmd)
+	Reader(commandline::CommandlineParser& opts)
 		: addImplied(implications), removeImplied(implications),
 		  addDerived(derivedTags), removeDerived(derivedTags),
 		  unfacetedRemover("::"), filterByExpression("")
 	{
-		if (opts.get("rename").defined())
+		if (opts.inputGroup.rename->boolValue())
 		{
-			readCollection(opts.get("rename").stringVal(), substitutions.substitutions());
+			readCollection(opts.inputGroup.rename->stringValue(), substitutions.substitutions());
 			filters.appendFilter(substitutions);
 		}
-		if (opts.get("patch").defined())
+		if (opts.inputGroup.patch->boolValue())
 		{
-			patches = readPatches(opts.get("patch").stringVal());
+			patches = readPatches(opts.inputGroup.patch->stringValue());
 			filters.appendFilter(patches);
 		}
 
-		if (opts.get("extimpl").defined())
+		if (opts.inputGroup.extimpl->boolValue())
 		{
-			readCollection(opts.get("extimpl").stringVal(), implications);
+			readCollection(opts.inputGroup.extimpl->stringValue(), implications);
 			// Pack the structure for faster expansions
 			implications.pack();
 		}
-		if (opts.get("derived").defined())
-			readDerivedTags(opts.get("derived").stringVal(), derivedTags);
+		if (opts.inputGroup.derived->boolValue())
+			readDerivedTags(opts.inputGroup.derived->stringValue(), derivedTags);
 
 		// Intermix implications and derived tags as seems best
-		bool compressOutput = (cmd == COPY && !opts.get("expanded").defined());
-		bool hasImpl = opts.get("extimpl").defined();
-		bool hasDerv = opts.get("derived").defined();
+		bool compressOutput = (opts.lastCommand()->name() == "copy" && !opts.outputGroup.redundant->boolValue());
+		bool hasImpl = opts.inputGroup.extimpl->boolValue();
+		bool hasDerv = opts.inputGroup.derived->boolValue();
 
 		if (compressOutput)
 		{
@@ -438,12 +640,12 @@
 			}
 		}
 
-		if (opts.get("remove-unfaceted").defined())
+		if (opts.inputGroup.rmunfaceted->boolValue())
 			filters.appendFilter(unfacetedRemover);
 
-		if (opts.get("remove-tags").defined())
+		if (opts.inputGroup.rmtags->boolValue())
 		{
-			filterByExpression.setExpression(not Expression(opts.get("remove-tags").stringVal())); 
+			filterByExpression.setExpression(not Expression(opts.inputGroup.rmtags->stringValue())); 
 			filters.appendFilter(filterByExpression);
 		}
 	}
@@ -452,345 +654,360 @@
 		filters.setConsumer(cons);
 		readCollection(file, filters);
 	}
-	void output(CommandlineArgs args, Consumer<string, string>& cons)
+	void output(commandline::CommandlineParser& opts, Consumer<string, string>& cons)
 	{
-		if (args.hasNext())
-			while (args.hasNext())
-				output(args.next(), cons);
+		if (opts.hasNext())
+			while (opts.hasNext())
+				output(opts.next(), cons);
 		else
 			output("-", cons);
 	}
 };
 
-int main(int argc, const char* argv[])
+class Writer : public Consumer<string, string>
 {
-	try {
-		CommandlineParserWithCommand opts(APPNAME, "[options] [command] [file1|-] [file2|-]",
-				"Perform various operations on a tagged collection\n\n"
-				"Commands are:\n"
-				"  copy or cat  output the collection\n"
-				"  reverse      \"reverse\" the collection, outputting one with items\n"
-				"               associated to tags\n"
-				"  diff         output a tag patch file with the differences between two files\n"
-				"  related <item> print a list of items related to the given one\n"
-				"  implications compute a list of tag implications\n"
-				"  hierarchy    build a smart hierarchy with the collection data\n"
-				"  cleanhierarchy build a cleaned smart hierarchy with the collection data\n"
-				"  findspecials generate a smart hierarchy and print, for each toplevel tag,\n"
-				"               what are the items that make it toplevel instead of going below\n"
-				"               another tag.\n"
-				"  grep <expr>  output the collection of tags that match the given tag expression\n"
-				"  items        output only the items of the input collection\n");
-
-		/*
-		opts.add("hierarchy", 's', "smart-hierarchy", "build a smart hierarchy");
-		opts.add("implications", 'm', "show-implications", "output a list of tag implications");
-		opts.add("copy", 'c', "copy", "output the collection");
-		opts.add("diff", 'd', "diff", "output a tag patch file with the differences between two files (requires two file arguments)");
-		*/
-
-		opts.add("derived", 'e', "derived-tags-from", "use an external list of derived tags", "file");
-		opts.add("extimpl", 'i', "implications-from", "use an external list of implications", "file");
-		opts.add("rename", 'r', "rename-from", "rename tags using the given mapping list", "file");
-		opts.add("patch", 'p', "patch-with", "apply patches from the given tag patch file", "file");
-
-		opts.add("expanded", 'x', "expanded-output", "produce full (and redundant) output data instead of compact");
-		opts.add("groupitems", 'g', "group-items", "group items with the same tagset in the output collection");
-		opts.add("distance", 'd', "distance", "set the maximum distance to use for the \"related\" command (defaults to 0)", "num");
-		opts.add("flatten", 0, "flatten-threshold", "set the number of total items below which a branch is flattened when using the \"hierarchy\" command (defaults to 0, meaning \"don't flatten\")", "num");
-		opts.add("filter", 'f', "filter", "filter out the tags with cardinality less than the given value (defaults to not filter; currently only works when building hierarchies)", "num");
-		opts.add("untagged-tag", 0, "untagged-tag", "set item name to use for associating untagged items when using the \"reverse\" command.  If not specified, untagged items are not included in the output.", "tag");
-		opts.add("remove-unfaceted", 0, "remove-unfaceted", "while parsing, remove all tags with no facet part.");
-		opts.add("remove-tags", 0, "remove-tags", "while parsing, remove all tags matching the given tag expression.", "expression");
-		
-		//opts.add("verbose", 'v', "verbose", "enable verbose output");
-		//opts.add("debug", 0, "debug", "enable debugging output (including verbose output)");
+	TrivialConverter<string, string> conv;
+	TextFormat<string, string> output;
+	ItemGrouper<string, string>* grouper;
 
-		opts.addCommand("copy", (int)COPY);
-		opts.addCommand("cat", (int)COPY);
-		opts.addCommand("reverse", (int)REVERSE);
-		opts.addCommand("diff", (int)DIFF);
-		opts.addCommand("related", (int)RELATED);
-		opts.addCommand("implications", (int)IMPLICATIONS);
-		opts.addCommand("hierarchy", (int)HIERARCHY);
-		opts.addCommand("cleanhierarchy", (int)CLEANHIERARCHY);
-		opts.addCommand("findspecials", (int)FINDSPECIALS);
-		opts.addCommand("grep", (int)GREP);
-		opts.addCommand("items", (int)ITEMS);
+protected:
+	/// Process an untagged item
+	virtual void consumeItemUntagged(const string& item)
+	{
+		if (grouper)
+			grouper->consume(item);
+		else
+			output.consume(item);
+	}
+
+	/// Process a tagged item, with its tags
+	virtual void consumeItem(const string& item, const OpSet<string>& tags)
+	{
+		if (grouper)
+			grouper->consume(item, tags);
+		else
+			output.consume(item, tags);
+	}
+
+	/// Process a set of items, all with no tags
+	virtual void consumeItemsUntagged(const OpSet<string>& items)
+	{
+		if (grouper)
+			grouper->consume(items);
+		else
+			// Explicitly split groups
+			for (OpSet<string>::const_iterator i = items.begin();
+					i != items.end(); i++)
+				output.consume(*i);
+	}
+
+	/// Process a set of items identically tagged, with their tags
+	virtual void consumeItems(const OpSet<string>& items, const OpSet<string>& tags)
+	{
+		if (grouper)
+			grouper->consume(items, tags);
+		else
+			// Explicitly split groups
+			for (OpSet<string>::const_iterator i = items.begin();
+					i != items.end(); i++)
+				output.consume(*i, tags);
+	}
+
+public:
+	Writer(commandline::CommandlineParser& opts)
+		: output(conv, conv, stdout), grouper(0)
+	{
+		if (opts.outputGroup.group->boolValue())
+			grouper = new ItemGrouper<string, string>;
+	}
+	~Writer()
+	{
+		// Flush output if needed
+		if (grouper)
+		{
+			grouper->output(output);
+			delete grouper;
+		}
+	}
+};
 
-		// Process the commandline
-		valid_command cmd = (valid_command)opts.parse(argc, argv);
+int main(int argc, const char* argv[])
+{
+	commandline::CommandlineParser opts(argc, argv);
 
-		CommandlineArgs args(argc, argv);
+	try {
+		opts.parse();
 
-		Reader reader(opts, cmd);
+		Reader reader(opts);
 		
 		// Perform the correct operation
-		switch (cmd)
+		if (opts.helpGroup.help->boolValue())
 		{
-			case IMPLICATIONS:
-			{
-				CardinalityStore<string, string> coll;
-				reader.output(args, coll);
+			// Provide help as requested
+			commandline::Help help(APPNAME, VERSION);
+			commandline::OptionParser* o = opts.lastCommand();
+
+			if (o && !o->name().empty())
+				// Help on a specific command
+				help.outputHelp(cout, *o);
+			else
+				// General help
+				help.outputHelp(cout, opts);
+		}
+		else if (opts.helpGroup.version->boolValue())
+		{
+			// Print the program version
+			commandline::Help help(APPNAME, VERSION);
+			help.outputVersion(cout);
+		}
+		else if (opts.lastCommand() == &opts.generic)
+		{
+			commandline::Help help(APPNAME, VERSION);
+			help.outputHelp(cout, opts);
+		}
+		else if (opts.lastCommand() == &opts.help)
+		{
+			commandline::Help help(APPNAME, VERSION);
+			commandline::OptionParser* o = 0;
+			if (opts.hasNext())
+				o = opts.command(opts.next());
 
-				Implications<string> newImpls;
+			if (o)
+				// Help on a specific command
+				help.outputHelp(cout, *o);
+			else
+				// General help
+				help.outputHelp(cout, opts);
+		}
+		else if (opts.lastCommand() == &opts.implications)
+		{
+			CardinalityStore<string, string> coll;
+			reader.output(opts, coll);
 
-				// Find tag implications
-				OpSet<string> allTags = coll.getAllTags();
-				for (OpSet<string>::const_iterator t = allTags.begin();
-						t != allTags.end(); t++)
-				{
-					OpSet<string> implied = coll.getImpliedBy(*t);
-					if (!implied.empty())
-						newImpls.consume(*t, implied);
-				}
+			Implications<string> newImpls;
 
-				newImpls.pack();
-				
-				TrivialConverter<string, string> conv;
-				TextFormat<string, string> output(conv, conv, stdout);
-				if (opts.get("expanded").defined())
-					newImpls.outputFull(output);
-				else
-					newImpls.output(output);
-				break;
-			}
-			case HIERARCHY:
-			{
-				int flattenThreshold = 0;
-				if (opts.get("flatten").defined())
-					flattenThreshold = opts.get("flatten").intVal();
-
-				CardinalityStore<string, string> coll;
-				reader.output(args, coll);
-
-				if (opts.get("filter").defined())
-					coll.removeTagsWithCardinalityLessThan(opts.get("filter").intVal());
-
-				// Default operation: build the smart hierarchy
-				HierarchyNode<string, string>* root =
-					new SmartHierarchyNode<string, string>("_top", coll, flattenThreshold);
-				printNode(root, "/");
-				break;
-			}
-			case CLEANHIERARCHY:
+			// Find tag implications
+			OpSet<string> allTags = coll.getAllTags();
+			for (OpSet<string>::const_iterator t = allTags.begin();
+					t != allTags.end(); t++)
 			{
-				int flattenThreshold = 0;
-				if (opts.get("flatten").defined())
-					flattenThreshold = opts.get("flatten").intVal();
-
-				CardinalityStore<string, string> coll;
-				reader.output(args, coll);
-
-				if (opts.get("filter").defined())
-					coll.removeTagsWithCardinalityLessThan(opts.get("filter").intVal());
-
-				// Default operation: build the smart hierarchy
-				HierarchyNode<string, string>* root = new CleanSmartHierarchyNode<string, string>("_top", coll, flattenThreshold);
-				printNode(root, "/");
-				break;
+				OpSet<string> implied = coll.getImpliedBy(*t);
+				if (!implied.empty())
+					newImpls.consume(*t, implied);
 			}
-			case DIFF:
-			{
-				InputMerger<string, string> merger1;
-				reader.output(args.next(), merger1);
 
-				InputMerger<string, string> merger2;
-				reader.output(args.next(), merger2);
+			newImpls.pack();
 
-				PatchList<string, string> newpatches;
-				newpatches.addPatch(merger1, merger2);
+			TrivialConverter<string, string> conv;
+			TextFormat<string, string> output(conv, conv, stdout);
+			if (opts.outputGroup.redundant->boolValue())
+				newImpls.outputFull(output);
+			else
+				newImpls.output(output);
+		}
+		else if (opts.lastCommand() == &opts.hierarchy)
+		{
+			int flattenThreshold = 0;
+			if (opts.hierarchyGroup.flatten->boolValue())
+				flattenThreshold = opts.hierarchyGroup.flatten->intValue();
 
-				TrivialConverter<string, string> conv;
-				TextFormat<string, string>::outputPatch(conv, conv, newpatches, stdout);
-				break;
-			}
-			case RELATED:
-			{
-				string item = args.next();
-				InputMerger<string, string> merger;
-				reader.output(args, merger);
-
-				int maxdist = 0;
-				if (opts.get("distance").defined())
-					maxdist = opts.get("distance").intVal();
-
-				// Split the items on commas
-				string splititem;
-				set<string> splititems;
-				for (string::const_iterator c = item.begin();
-						c != item.end(); c++)
-					if (*c == ',')
-					{
-						if (!merger.hasItem(splititem))
-						{
-							fprintf(stderr, "Item \"%.*s\" does not exist in the collection\n", PFSTR(splititem));
-							return 1;
-						}
-						splititems.insert(splititem);
-						splititem = string();
-					} else
-						splititem += *c;
-				if (!merger.hasItem(splititem))
-				{
-					fprintf(stderr, "Item \"%.*s\" does not exist in the collection\n", PFSTR(splititem));
-					return 1;
-				}
-				splititems.insert(splititem);
-				
-				// Get the tagset as the intersection of the tagsets of all input items
-				set<string>::const_iterator i = splititems.begin();
-				OpSet<string> ts = merger.getTags(*i);
-				for (++i; i != splititems.end(); i++)
-					ts = ts ^ merger.getTags(*i);
+			CardinalityStore<string, string> coll;
+			reader.output(opts, coll);
 
-				if (ts.empty())
-				{
-					if (splititems.size() > 1)
-						fprintf(stderr, "The items %.*s are unrelated: cannot find a barycenter to start computing relationships from.\n", PFSTR(item));
-					else
-						fprintf(stderr, "The items %.*s has no tags attached.\n", PFSTR(item));
-					return 1;
-				}
-				
-				// Build a full TagCollection
-				CardinalityStore<string, string> coll;
-				merger.output(coll);
+			if (opts.hierarchyGroup.filter->boolValue())
+				coll.removeTagsWithCardinalityLessThan(opts.hierarchyGroup.filter->intValue());
 
-				printItems(coll.getItemsExactMatch(ts));
+			// Default operation: build the smart hierarchy
+			HierarchyNode<string, string>* root =
+				new SmartHierarchyNode<string, string>("_top", coll, flattenThreshold);
+			printNode(root, "/");
+		}
+		else if (opts.lastCommand() == &opts.cleanhierarchy)
+		{
+			int flattenThreshold = 0;
+			if (opts.hierarchyGroup.flatten->boolValue())
+				flattenThreshold = opts.hierarchyGroup.flatten->intValue();
 
-				if (maxdist)
-				{
-					// Get the related tagsets
-					list< OpSet<string> > rel = coll.getRelatedTagsets(ts, maxdist);
+			CardinalityStore<string, string> coll;
+			reader.output(opts, coll);
 
-					// Print the output list
-					for (list< OpSet<string> >::const_iterator i = rel.begin();
-							i != rel.end(); i++)
-						printItems(coll.getItemsExactMatch(*i));
-				}
-				break;
-			}
-			case REVERSE:
-			{
-				ItemGrouper<string, string> reverser;
-				reader.output(args, reverser);
+			if (opts.hierarchyGroup.filter->boolValue())
+				coll.removeTagsWithCardinalityLessThan(opts.hierarchyGroup.filter->intValue());
+
+			// Default operation: build the smart hierarchy
+			HierarchyNode<string, string>* root = new CleanSmartHierarchyNode<string, string>("_top", coll, flattenThreshold);
+			printNode(root, "/");
+		}
+		else if (opts.lastCommand() == &opts.diff)
+		{
+			InputMerger<string, string> merger1;
+			reader.output(opts.next(), merger1);
 
-				/*
-				if (opts.get("untagged-tag").defined())
-					reverser.setUntaggedItemName(opts.get("untagged-tag").stringVal());
-				 */
-
-				TrivialConverter<string, string> conv;
-				TextFormat<string, string> writer(conv, conv, stdout);
-				if (opts.get("groupitems").defined())
+			InputMerger<string, string> merger2;
+			reader.output(opts.next(), merger2);
+
+			PatchList<string, string> newpatches;
+			newpatches.addPatch(merger1, merger2);
+
+			TrivialConverter<string, string> conv;
+			TextFormat<string, string>::outputPatch(conv, conv, newpatches, stdout);
+		}
+		else if (opts.lastCommand() == &opts.related)
+		{
+			string item = opts.next();
+			InputMerger<string, string> merger;
+			reader.output(opts, merger);
+
+			int maxdist = 0;
+			if (opts.related.distance->boolValue())
+				maxdist = opts.related.distance->intValue();
+
+			// Split the items on commas
+			string splititem;
+			set<string> splititems;
+			for (string::const_iterator c = item.begin();
+					c != item.end(); c++)
+				if (*c == ',')
 				{
-					reverser.outputReversed(writer);
+					if (!merger.hasItem(splititem))
+					{
+						fprintf(stderr, "Item \"%.*s\" does not exist in the collection\n", PFSTR(splititem));
+						return 1;
+					}
+					splititems.insert(splititem);
+					splititem = string();
 				} else
-					// FIXME: we need a filter that does the opposite of ItemGrouper
-					reverser.outputReversed(writer);
-				break;
-			}
-			case COPY:
+					splititem += *c;
+			if (!merger.hasItem(splititem))
 			{
-				TrivialConverter<string, string> conv;
-				TextFormat<string, string> writer(conv, conv, stdout);
-				if (opts.get("groupitems").defined())
-				{
-					ItemGrouper<string, string> grouper;
-					reader.output(args, grouper);
-					grouper.output(writer);
-				} else
-					reader.output(args, writer);
-				break;
+				fprintf(stderr, "Item \"%.*s\" does not exist in the collection\n", PFSTR(splititem));
+				return 1;
 			}
-			case FINDSPECIALS:
+			splititems.insert(splititem);
+
+			// Get the tagset as the intersection of the tagsets of all input items
+			set<string>::const_iterator i = splititems.begin();
+			OpSet<string> ts = merger.getTags(*i);
+			for (++i; i != splititems.end(); i++)
+				ts = ts ^ merger.getTags(*i);
+
+			if (ts.empty())
 			{
-				int flattenThreshold = 0;
-				if (opts.get("flatten").defined())
-					flattenThreshold = opts.get("flatten").intVal();
-
-				CardinalityStore<string, string> coll;
-				reader.output(args, coll);
-
-				if (opts.get("filter").defined())
-					coll.removeTagsWithCardinalityLessThan(opts.get("filter").intVal());
-
-				// Default operation: build the smart hierarchy
-				SmartHierarchyNode<string, string> root("_top", coll, flattenThreshold);
-
-				OpSet<string> seen;
-				for (HierarchyNode<string, string>::iterator i = root.begin();
-						i != root.end(); i++)
-				{
-					OpSet<string> items = getItems(*i);
+				if (splititems.size() > 1)
+					fprintf(stderr, "The items %.*s are unrelated: cannot find a barycenter to start computing relationships from.\n", PFSTR(item));
+				else
+					fprintf(stderr, "The items %.*s has no tags attached.\n", PFSTR(item));
+				return 1;
+			}
 
-					// Find the items in this branch that are not present in
-					// any of the previous ones
-					OpSet<string> newItems;
-					if (!seen.empty())
-					{
-						for (OpSet<string>::const_iterator j = items.begin();
-								j != items.end(); j++)
-						{
-							OpSet<string> tags = coll.getTags(*j) ^ seen;
-							if (tags.empty())
-								newItems += *j;
-						}
-
-						printf("%.*s: %d items, %d special items:\n",
-								PFSTR((*i)->tag()), items.size(), newItems.size());
-
-						int indent = (*i)->tag().size() + 2;
-						for (OpSet<string>::const_iterator j = newItems.begin(); j != newItems.end(); j++)
-							printf("%*s%.*s\n", indent, "", PFSTR(*j));
-					}
+			// Build a full TagCollection
+			CardinalityStore<string, string> coll;
+			merger.output(coll);
 
-					seen += (*i)->tag();
-				}
+			printItems(coll.getItemsExactMatch(ts));
 
-				break;
-			}
-			case GREP:
+			if (maxdist)
 			{
-				FilterItemsByExpression<string, string> filter(args.next());
+				// Get the related tagsets
+				list< OpSet<string> > rel = coll.getRelatedTagsets(ts, maxdist);
 
-				TrivialConverter<string, string> conv;
-				TextFormat<string, string> writer(conv, conv, stdout);
-				if (opts.get("groupitems").defined())
-				{
-					ItemGrouper<string, string> grouper;
-					filter.setConsumer(grouper);
-					reader.output(args, filter);
-					grouper.output(writer);
-				} else {
-					filter.setConsumer(writer);
-					reader.output(args, filter);
-				}
-				break;
+				// Print the output list
+				for (list< OpSet<string> >::const_iterator i = rel.begin();
+						i != rel.end(); i++)
+					printItems(coll.getItemsExactMatch(*i));
 			}
-			case ITEMS:
+		}
+		else if (opts.lastCommand() == &opts.reverse)
+		{
+			ItemGrouper<string, string> reverser;
+			reader.output(opts, reverser);
+
+			/*
+			if (opts.reverse.untaggedTag->boolValue())
+				reverser.setUntaggedItemName(opts.reverse.untaggedTag->stringValue());
+			*/
+
+			Writer writer(opts);
+			reverser.outputReversed(writer);
+		}
+		else if (opts.lastCommand() == &opts.copy)
+		{
+			Writer writer(opts);
+			reader.output(opts, writer);
+		}
+		else if (opts.lastCommand() == &opts.findspecials)
+		{
+			int flattenThreshold = 0;
+			if (opts.hierarchyGroup.flatten->boolValue())
+				flattenThreshold = opts.hierarchyGroup.flatten->intValue();
+
+			CardinalityStore<string, string> coll;
+			reader.output(opts, coll);
+
+			if (opts.hierarchyGroup.filter->boolValue())
+				coll.removeTagsWithCardinalityLessThan(opts.hierarchyGroup.filter->intValue());
+
+			// Default operation: build the smart hierarchy
+			SmartHierarchyNode<string, string> root("_top", coll, flattenThreshold);
+
+			OpSet<string> seen;
+			for (HierarchyNode<string, string>::iterator i = root.begin();
+					i != root.end(); i++)
 			{
-				ItemsOnly<string, string> filter;
-				TrivialConverter<string, string> conv;
-				TextFormat<string, string> writer(conv, conv, stdout);
-				if (opts.get("groupitems").defined())
+				OpSet<string> items = getItems(*i);
+
+				// Find the items in this branch that are not present in
+				// any of the previous ones
+				OpSet<string> newItems;
+				if (!seen.empty())
 				{
-					ItemGrouper<string, string> grouper;
-					filter.setConsumer(grouper);
-					reader.output(args, filter);
-					grouper.output(writer);
-				} else {
-					filter.setConsumer(writer);
-					reader.output(args, filter);
+					for (OpSet<string>::const_iterator j = items.begin();
+							j != items.end(); j++)
+					{
+						OpSet<string> tags = coll.getTags(*j) ^ seen;
+						if (tags.empty())
+							newItems += *j;
+					}
+
+					printf("%.*s: %d items, %d special items:\n",
+							PFSTR((*i)->tag()), items.size(), newItems.size());
+
+					int indent = (*i)->tag().size() + 2;
+					for (OpSet<string>::const_iterator j = newItems.begin(); j != newItems.end(); j++)
+						printf("%*s%.*s\n", indent, "", PFSTR(*j));
 				}
-				break;
+
+				seen += (*i)->tag();
 			}
 		}
+		else if (opts.lastCommand() == &opts.grep)
+		{
+			Writer writer(opts);
+			FilterItemsByExpression<string, string> filter(writer, opts.next());
+			reader.output(opts, filter);
+		}
+		else if (opts.lastCommand() == &opts.items)
+		{
+			Writer writer(opts);
+			ItemsOnly<string, string> filter(writer);
+			reader.output(opts, filter);
+		}
+		else
+			throw commandline::BadOption(string("unhandled command ") +
+						(opts.lastCommand() ? opts.lastCommand()->name() : "(null)"));
 
 		return 0;
-	}
-	catch (Exception& e)
+	} catch (commandline::BadOption& e) {
+		cerr << e.desc() << endl;
+		commandline::Help help(APPNAME, VERSION);
+		if (opts.lastCommand())
+		{
+			help.outputHelp(cerr, *opts.lastCommand());
+		} else {
+			help.outputHelp(cerr, opts);
+		}
+		exit(1);
+	} catch (Exception& e)
 	{
 		fprintf(stderr, "%s: %.*s\n", e.type(), PFSTR(e.desc()));
 		return 1;



More information about the Debtags-commits mailing list