1 /**
2 	Defines the behavior of the DUB command line client.
3 
4 	Copyright: © 2012-2013 Matthias Dondorff, Copyright © 2012-2016 Sönke Ludwig
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Matthias Dondorff, Sönke Ludwig
7 */
8 module dub.commandline;
9 
10 import dub.compilers.compiler;
11 import dub.dependency;
12 import dub.dub;
13 import dub.generators.generator;
14 import dub.internal.vibecompat.core.file;
15 import dub.internal.vibecompat.core.log;
16 import dub.internal.vibecompat.data.json;
17 import dub.internal.vibecompat.inet.url;
18 import dub.package_;
19 import dub.packagemanager;
20 import dub.packagesupplier;
21 import dub.platform : determineCompiler;
22 import dub.project;
23 import dub.internal.utils : getDUBVersion, getClosestMatch;
24 
25 import std.algorithm;
26 import std.array;
27 import std.conv;
28 import std.encoding;
29 import std.exception;
30 import std.file;
31 import std.getopt;
32 import std.process;
33 import std.stdio;
34 import std.string;
35 import std.typecons : Tuple, tuple;
36 import std.variant;
37 
38 
39 /** Retrieves a list of all available commands.
40 
41 	Commands are grouped by category.
42 */
43 CommandGroup[] getCommands()
44 {
45 	return [
46 		CommandGroup("Package creation",
47 			new InitCommand
48 		),
49 		CommandGroup("Build, test and run",
50 			new RunCommand,
51 			new BuildCommand,
52 			new TestCommand,
53 			new GenerateCommand,
54 			new DescribeCommand,
55 			new CleanCommand,
56 			new DustmiteCommand
57 		),
58 		CommandGroup("Package management",
59 			new FetchCommand,
60 			new InstallCommand,
61 			new RemoveCommand,
62 			new UninstallCommand,
63 			new UpgradeCommand,
64 			new AddPathCommand,
65 			new RemovePathCommand,
66 			new AddLocalCommand,
67 			new RemoveLocalCommand,
68 			new ListCommand,
69 			new SearchCommand,
70 			new ListInstalledCommand,
71 			new AddOverrideCommand,
72 			new RemoveOverrideCommand,
73 			new ListOverridesCommand,
74 			new CleanCachesCommand,
75 			new ConvertCommand,
76 		)
77 	];
78 }
79 
80 
81 /** Processes the given command line and executes the appropriate actions.
82 
83 	Params:
84 		args = This command line argument array as received in `main`. The first
85 			entry is considered to be the name of the binary invoked.
86 
87 	Returns:
88 		Returns the exit code that is supposed to be returned to the system.
89 */
90 int runDubCommandLine(string[] args)
91 {
92 	logDiagnostic("DUB version %s", getDUBVersion());
93 
94 	version(Windows){
95 		// rdmd uses $TEMP to compute a temporary path. since cygwin substitutes backslashes
96 		// with slashes, this causes OPTLINK to fail (it thinks path segments are options)
97 		// we substitute the other way around here to fix this.
98 		environment["TEMP"] = environment["TEMP"].replace("/", "\\");
99 	}
100 
101 	// special single-file package shebang syntax
102 	if (args.length >= 2 && args[1].endsWith(".d")) {
103 		args = args[0] ~ ["run", "-q", "--temp-build", "--single", args[1], "--"] ~ args[2 ..$];
104 	}
105 
106 	// split application arguments from DUB arguments
107 	string[] app_args;
108 	auto app_args_idx = args.countUntil("--");
109 	if (app_args_idx >= 0) {
110 		app_args = args[app_args_idx+1 .. $];
111 		args = args[0 .. app_args_idx];
112 	}
113 	args = args[1 .. $]; // strip the application name
114 
115 	// handle direct dub options
116 	if (args.length) switch (args[0])
117 	{
118 	case "--version":
119 		showVersion();
120 		return 0;
121 
122 	default:
123 		break;
124 	}
125 
126 	// parse general options
127 	CommonOptions options;
128 	LogLevel loglevel = LogLevel.info;
129 	options.root_path = getcwd();
130 
131 	auto common_args = new CommandArgs(args);
132 	try {
133 		options.prepare(common_args);
134 
135 		if (options.vverbose) loglevel = LogLevel.debug_;
136 		else if (options.verbose) loglevel = LogLevel.diagnostic;
137 		else if (options.vquiet) loglevel = LogLevel.none;
138 		else if (options.quiet) loglevel = LogLevel.warn;
139 		setLogLevel(loglevel);
140 	} catch (Throwable e) {
141 		logError("Error processing arguments: %s", e.msg);
142 		logDiagnostic("Full exception: %s", e.toString().sanitize);
143 		logInfo("Run 'dub help' for usage information.");
144 		return 1;
145 	}
146 
147 	// create the list of all supported commands
148 	CommandGroup[] commands = getCommands();
149 
150 	// extract the command
151 	string cmdname;
152 	args = common_args.extractRemainingArgs();
153 	if (args.length >= 1 && !args[0].startsWith("-")) {
154 		cmdname = args[0];
155 		args = args[1 .. $];
156 	} else {
157 		if (options.help) {
158 			showHelp(commands, common_args);
159 			return 0;
160 		}
161 		cmdname = "run";
162 	}
163 	auto command_args = new CommandArgs(args);
164 
165 	if (cmdname == "help") {
166 		showHelp(commands, common_args);
167 		return 0;
168 	}
169 
170 	// find the selected command
171 	Command cmd;
172 	foreach (grp; commands)
173 		foreach (c; grp.commands)
174 			if (c.name == cmdname) {
175 				cmd = c;
176 				break;
177 			}
178 
179 	if (!cmd) {
180 		logError("Unknown command: %s", cmdname);
181 		writeln();
182 		showHelp(commands, common_args);
183 		return 1;
184 	}
185 
186 	// process command line options for the selected command
187 	try {
188 		cmd.prepare(command_args);
189 		enforceUsage(cmd.acceptsAppArgs || app_args.length == 0, cmd.name ~ " doesn't accept application arguments.");
190 	} catch (Throwable e) {
191 		logError("Error processing arguments: %s", e.msg);
192 		logDiagnostic("Full exception: %s", e.toString().sanitize);
193 		logInfo("Run 'dub help' for usage information.");
194 		return 1;
195 	}
196 
197 	if (options.help) {
198 		showCommandHelp(cmd, command_args, common_args);
199 		return 0;
200 	}
201 
202 	auto remaining_args = command_args.extractRemainingArgs();
203 	if (remaining_args.any!(a => a.startsWith("-"))) {
204 		logError("Unknown command line flags: %s", remaining_args.filter!(a => a.startsWith("-")).array.join(" "));
205 		logError(`Type "dub %s -h" to get a list of all supported flags.`, cmdname);
206 		return 1;
207 	}
208 
209 	Dub dub;
210 
211 	// initialize the root package
212 	if (!cmd.skipDubInitialization) {
213 		if (options.bare) {
214 			dub = new Dub(Path(getcwd()));
215 			dub.defaultPlacementLocation = options.placementLocation;
216 		} else {
217 			// initialize DUB
218 			auto package_suppliers = options.registry_urls.map!(url => cast(PackageSupplier)new RegistryPackageSupplier(URL(url))).array;
219 			dub = new Dub(options.root_path, package_suppliers, options.skipRegistry);
220 			dub.dryRun = options.annotate;
221 			dub.defaultPlacementLocation = options.placementLocation;
222 
223 			// make the CWD package available so that for example sub packages can reference their
224 			// parent package.
225 			try dub.packageManager.getOrLoadPackage(Path(options.root_path));
226 			catch (Exception e) { logDiagnostic("No package found in current working directory."); }
227 		}
228 	}
229 
230 	// execute the command
231 	try return cmd.execute(dub, remaining_args, app_args);
232 	catch (UsageException e) {
233 		logError("%s", e.msg);
234 		logDebug("Full exception: %s", e.toString().sanitize);
235 		logInfo(`Run "dub %s -h" for more information about the "%s" command.`, cmdname, cmdname);
236 		return 1;
237 	}
238 	catch (Throwable e) {
239 		logError("%s", e.msg);
240 		logDebug("Full exception: %s", e.toString().sanitize);
241 		return 2;
242 	}
243 }
244 
245 
246 /** Contains and parses options common to all commands.
247 */
248 struct CommonOptions {
249 	bool verbose, vverbose, quiet, vquiet;
250 	bool help, annotate, bare;
251 	string[] registry_urls;
252 	string root_path;
253 	SkipPackageSuppliers skipRegistry = SkipPackageSuppliers.none;
254 	PlacementLocation placementLocation = PlacementLocation.user;
255 
256 	/// Parses all common options and stores the result in the struct instance.
257 	void prepare(CommandArgs args)
258 	{
259 		args.getopt("h|help", &help, ["Display general or command specific help"]);
260 		args.getopt("root", &root_path, ["Path to operate in instead of the current working dir"]);
261 		args.getopt("registry", &registry_urls, ["Search the given DUB registry URL first when resolving dependencies. Can be specified multiple times."]);
262 		args.getopt("skip-registry", &skipRegistry, [
263 			"Skips searching certain package registries for dependencies:",
264 			"  none: Search all configured registries (default)",
265 			"  standard: Don't search on "~defaultRegistryURL,
266 			"  all: Search none of the configured registries",
267 			]);
268 		args.getopt("annotate", &annotate, ["Do not perform any action, just print what would be done"]);
269 		args.getopt("bare", &bare, ["Read only packages contained in the current directory"]);
270 		args.getopt("v|verbose", &verbose, ["Print diagnostic output"]);
271 		args.getopt("vverbose", &vverbose, ["Print debug output"]);
272 		args.getopt("q|quiet", &quiet, ["Only print warnings and errors"]);
273 		args.getopt("vquiet", &vquiet, ["Print no messages"]);
274 		args.getopt("cache", &placementLocation, ["Puts any fetched packages in the specified location [local|system|user]."]);
275 	}
276 }
277 
278 /** Encapsulates a set of application arguments.
279 
280 	This class serves two purposes. The first is to provide an API for parsing
281 	command line arguments (`getopt`). At the same time it records all calls
282 	to `getopt` and provides a list of all possible options using the
283 	`recognizedArgs` property.
284 */
285 class CommandArgs {
286 	struct Arg {
287 		Variant defaultValue;
288 		Variant value;
289 		string names;
290 		string[] helpText;
291 	}
292 	private {
293 		string[] m_args;
294 		Arg[] m_recognizedArgs;
295 	}
296 
297 	/** Initializes the list of source arguments.
298 
299 		Note that all array entries are considered application arguments (i.e.
300 		no application name entry is present as the first entry)
301 	*/
302 	this(string[] args)
303 	{
304 		m_args = "dummy" ~ args;
305 	}
306 
307 	/** Returns the list of all options recognized.
308 
309 		This list is created by recording all calls to `getopt`.
310 	*/
311 	@property const(Arg)[] recognizedArgs() { return m_recognizedArgs; }
312 
313 	void getopt(T)(string names, T* var, string[] help_text = null)
314 	{
315 		foreach (ref arg; m_recognizedArgs)
316 			if (names == arg.names) {
317 				assert(help_text is null);
318 				*var = arg.value.get!T;
319 				return;
320 			}
321 		assert(help_text.length > 0);
322 		Arg arg;
323 		arg.defaultValue = *var;
324 		arg.names = names;
325 		arg.helpText = help_text;
326 		m_args.getopt(config.passThrough, names, var);
327 		arg.value = *var;
328 		m_recognizedArgs ~= arg;
329 	}
330 
331 	/** Resets the list of available source arguments.
332 	*/
333 	void dropAllArgs()
334 	{
335 		m_args = null;
336 	}
337 
338 	/** Returns the list of unprocessed arguments and calls `dropAllArgs`.
339 	*/
340 	string[] extractRemainingArgs()
341 	{
342 		auto ret = m_args[1 .. $];
343 		m_args = null;
344 		return ret;
345 	}
346 }
347 
348 
349 /** Base class for all commands.
350 
351 	This cass contains a high-level description of the command, including brief
352 	and full descriptions and a human readable command line pattern. On top of
353 	that it defines the two main entry functions for command execution.
354 */
355 class Command {
356 	string name;
357 	string argumentsPattern;
358 	string description;
359 	string[] helpText;
360 	bool acceptsAppArgs;
361 	bool hidden = false; // used for deprecated commands
362 	bool skipDubInitialization = false;
363 
364 	/** Parses all known command line options without executing any actions.
365 
366 		This function will be called prior to execute, or may be called as
367 		the only method when collecting the list of recognized command line
368 		options.
369 
370 		Only `args.getopt` should be called within this method.
371 	*/
372 	abstract void prepare(scope CommandArgs args);
373 
374 	/** Executes the actual action.
375 
376 		Note that `prepare` will be called before any call to `execute`.
377 	*/
378 	abstract int execute(Dub dub, string[] free_args, string[] app_args);
379 
380 	private bool loadCwdPackage(Dub dub, bool warn_missing_package)
381 	{
382 		bool found = existsFile(dub.rootPath ~ "source/app.d");
383 		if (!found)
384 			foreach (f; packageInfoFiles)
385 				if (existsFile(dub.rootPath ~ f.filename)) {
386 					found = true;
387 					break;
388 				}
389 
390 		if (!found) {
391 			if (warn_missing_package) {
392 				logInfo("");
393 				logInfo("Neither a package description file, nor source/app.d was found in");
394 				logInfo(dub.rootPath.toNativeString());
395 				logInfo("Please run DUB from the root directory of an existing package, or run");
396 				logInfo("\"dub init --help\" to get information on creating a new package.");
397 				logInfo("");
398 			}
399 			return false;
400 		}
401 
402 		dub.loadPackage();
403 
404 		return true;
405 	}
406 }
407 
408 
409 /** Encapsulates a group of commands that fit into a common category.
410 */
411 struct CommandGroup {
412 	/// Caption of the command category
413 	string caption;
414 
415 	/// List of commands contained inthis group
416 	Command[] commands;
417 
418 	this(string caption, Command[] commands...)
419 	{
420 		this.caption = caption;
421 		this.commands = commands.dup;
422 	}
423 }
424 
425 
426 /******************************************************************************/
427 /* INIT                                                                       */
428 /******************************************************************************/
429 
430 class InitCommand : Command {
431 	private{
432 		string m_templateType = "minimal";
433 		PackageFormat m_format = PackageFormat.json;
434 		bool m_nonInteractive;
435 	}
436 	this()
437 	{
438 		this.name = "init";
439 		this.argumentsPattern = "[<directory> [<dependency>...]]";
440 		this.description = "Initializes an empty package skeleton";
441 		this.helpText = [
442 			"Initializes an empty package of the specified type in the given directory. By default, the current working dirctory is used."
443 		];
444 	}
445 
446 	override void prepare(scope CommandArgs args)
447 	{
448 		args.getopt("t|type", &m_templateType, [
449 			"Set the type of project to generate. Available types:",
450 			"",
451 			"minimal - simple \"hello world\" project (default)",
452 			"vibe.d  - minimal HTTP server based on vibe.d",
453 			"deimos  - skeleton for C header bindings",
454 		]);
455 		args.getopt("f|format", &m_format, [
456 			"Sets the format to use for the package description file. Possible values:",
457 			"  " ~ [__traits(allMembers, PackageFormat)].map!(f => f == m_format.init.to!string ? f ~ " (default)" : f).join(", ")
458 		]);
459 		args.getopt("n|non-interactive", &m_nonInteractive, ["Don't enter interactive mode."]);
460 	}
461 
462 	override int execute(Dub dub, string[] free_args, string[] app_args)
463 	{
464 		string dir;
465 		enforceUsage(app_args.empty, "Unexpected application arguments.");
466 		if (free_args.length)
467 		{
468 			dir = free_args[0];
469 			free_args = free_args[1 .. $];
470 		}
471 
472 		string input(string caption, string default_value)
473 		{
474 			writef("%s [%s]: ", caption, default_value);
475 			auto inp = readln();
476 			return inp.length > 1 ? inp[0 .. $-1] : default_value;
477 		}
478 
479 		void depCallback(ref PackageRecipe p, ref PackageFormat fmt) {
480 			if (m_nonInteractive) return;
481 
482 			while (true) {
483 				string rawfmt = input("Package recipe format (sdl/json)", fmt.to!string);
484 				if (!rawfmt.length) break;
485 				try {
486 					fmt = rawfmt.to!PackageFormat;
487 					break;
488 				} catch (Exception) {
489 					logError("Invalid format, \""~rawfmt~"\", enter either \"sdl\" or \"json\".");
490 				}
491 			}
492 			auto author = p.authors.join(", ");
493 			p.name = input("Name", p.name);
494 			p.description = input("Description", p.description);
495 			p.authors = input("Author name", author).split(",").map!(a => a.strip).array;
496 			p.license = input("License", p.license);
497 			p.copyright = input("Copyright string", p.copyright);
498 
499 			while (true) {
500 				auto depname = input("Add dependency (leave empty to skip)", null);
501 				if (!depname.length) break;
502 				try {
503 					auto ver = dub.getLatestVersion(depname);
504 					auto dep = ver.isBranch ? Dependency(ver) : Dependency("~>" ~ ver.toString());
505 					p.buildSettings.dependencies[depname] =	dep;
506 					logInfo("Added dependency %s %s", depname, dep.versionSpec);
507 				} catch (Exception e) {
508 					logError("Could not find package '%s'.", depname);
509 					logDebug("Full error: %s", e.toString().sanitize);
510 				}
511 			}
512 		}
513 
514 		//TODO: Remove this block in next version
515 		// Checks if argument uses current method of specifying project type.
516 		if (free_args.length)
517 		{
518 			if (["vibe.d", "deimos", "minimal"].canFind(free_args[0]))
519 			{
520 				m_templateType = free_args[0];
521 				free_args = free_args[1 .. $];
522 				logInfo("Deprecated use of init type. Use --type=[vibe.d | deimos | minimal] in future.");
523 			}
524 		}
525 		dub.createEmptyPackage(Path(dir), free_args, m_templateType, m_format, &depCallback);
526 
527 		logInfo("Package successfully created in %s", dir.length ? dir : ".");
528 		return 0;
529 	}
530 }
531 
532 
533 /******************************************************************************/
534 /* GENERATE / BUILD / RUN / TEST / DESCRIBE                                   */
535 /******************************************************************************/
536 
537 abstract class PackageBuildCommand : Command {
538 	protected {
539 		string m_buildType;
540 		BuildMode m_buildMode;
541 		string m_buildConfig;
542 		string m_compilerName;
543 		string m_arch;
544 		string[] m_debugVersions;
545 		Compiler m_compiler;
546 		BuildPlatform m_buildPlatform;
547 		BuildSettings m_buildSettings;
548 		string m_defaultConfig;
549 		bool m_nodeps;
550 		bool m_forceRemove = false;
551 		bool m_single;
552 	}
553 
554 	override void prepare(scope CommandArgs args)
555 	{
556 		args.getopt("b|build", &m_buildType, [
557 			"Specifies the type of build to perform. Note that setting the DFLAGS environment variable will override the build type with custom flags.",
558 			"Possible names:",
559 			"  debug (default), plain, release, release-debug, release-nobounds, unittest, profile, profile-gc, docs, ddox, cov, unittest-cov and custom types"
560 		]);
561 		args.getopt("c|config", &m_buildConfig, [
562 			"Builds the specified configuration. Configurations can be defined in dub.json"
563 		]);
564 		args.getopt("compiler", &m_compilerName, [
565 			"Specifies the compiler binary to use (can be a path).",
566 			"Arbitrary pre- and suffixes to the identifiers below are recognized (e.g. ldc2 or dmd-2.063) and matched to the proper compiler type:",
567 			"  "~["dmd", "gdc", "ldc", "gdmd", "ldmd"].join(", ")
568 		]);
569 		args.getopt("a|arch", &m_arch, [
570 			"Force a different architecture (e.g. x86 or x86_64)"
571 		]);
572 		args.getopt("d|debug", &m_debugVersions, [
573 			"Define the specified debug version identifier when building - can be used multiple times"
574 		]);
575 		args.getopt("nodeps", &m_nodeps, [
576 			"Do not check/update dependencies before building"
577 		]);
578 		args.getopt("force-remove", &m_forceRemove, [
579 			"Force deletion of fetched packages with untracked files when upgrading"
580 		]);
581 		args.getopt("build-mode", &m_buildMode, [
582 			"Specifies the way the compiler and linker are invoked. Valid values:",
583 			"  separate (default), allAtOnce, singleFile"
584 		]);
585 		args.getopt("single", &m_single, [
586 			"Treats the package name as a filename. The file must contain a package recipe comment."
587 		]);
588 	}
589 
590 	protected void setupPackage(Dub dub, string package_name, string default_build_type = "debug")
591 	{
592 		if (!m_compilerName.length) m_compilerName = dub.defaultCompiler;
593 		m_compiler = getCompiler(m_compilerName);
594 		m_buildPlatform = m_compiler.determinePlatform(m_buildSettings, m_compilerName, m_arch);
595 		m_buildSettings.addDebugVersions(m_debugVersions);
596 
597 		m_defaultConfig = null;
598 		enforce (loadSpecificPackage(dub, package_name), "Failed to load package.");
599 
600 		if (m_buildConfig.length != 0 && !dub.configurations.canFind(m_buildConfig))
601 		{
602 			string msg = "Unknown build configuration: "~m_buildConfig;
603 			enum distance = 3;
604 			auto match = dub.configurations.getClosestMatch(m_buildConfig, distance);
605 			if (match !is null) msg ~= ". Did you mean '" ~ match ~ "'?";
606 			enforce(0, msg);
607 		}
608 
609 		if (m_buildType.length == 0) {
610 			if (environment.get("DFLAGS") !is null) m_buildType = "$DFLAGS";
611 			else m_buildType = default_build_type;
612 		}
613 
614 		if (!m_nodeps) {
615 			// TODO: only upgrade(select) if necessary, only upgrade(upgrade) every now and then
616 
617 			// retrieve missing packages
618 			dub.project.reinit();
619 			if (!dub.project.hasAllDependencies) {
620 				logDiagnostic("Checking for missing dependencies.");
621 				if (m_single) dub.upgrade(UpgradeOptions.select | UpgradeOptions.noSaveSelections);
622 				else dub.upgrade(UpgradeOptions.select);
623 			}
624 
625 			if (!m_single) {
626 				logDiagnostic("Checking for upgrades.");
627 				dub.upgrade(UpgradeOptions.upgrade|UpgradeOptions.printUpgradesOnly|UpgradeOptions.useCachedResult);
628 			}
629 		}
630 
631 		dub.project.validate();
632 	}
633 
634 	private bool loadSpecificPackage(Dub dub, string package_name)
635 	{
636 		if (m_single) {
637 			enforce(package_name.length, "Missing file name of single-file package.");
638 			dub.loadSingleFilePackage(package_name);
639 			return true;
640 		}
641 
642 		// load package in root_path to enable searching for sub packages
643 		if (loadCwdPackage(dub, package_name.length == 0)) {
644 			if (package_name.startsWith(":"))
645 				package_name = dub.projectName ~ package_name;
646 			if (!package_name.length) return true;
647 		}
648 
649 		enforce(package_name.length, "No valid root package found - aborting.");
650 
651 		auto pack = dub.packageManager.getFirstPackage(package_name);
652 		enforce(pack, "Failed to find a package named '"~package_name~"'.");
653 		logInfo("Building package %s in %s", pack.name, pack.path.toNativeString());
654 		dub.rootPath = pack.path;
655 		dub.loadPackage(pack);
656 		return true;
657 	}
658 }
659 
660 class GenerateCommand : PackageBuildCommand {
661 	protected {
662 		string m_generator;
663 		bool m_rdmd = false;
664 		bool m_tempBuild = false;
665 		bool m_run = false;
666 		bool m_force = false;
667 		bool m_combined = false;
668 		bool m_parallel = false;
669 		bool m_printPlatform, m_printBuilds, m_printConfigs;
670 	}
671 
672 	this()
673 	{
674 		this.name = "generate";
675 		this.argumentsPattern = "<generator> [<package>]";
676 		this.description = "Generates project files using the specified generator";
677 		this.helpText = [
678 			"Generates project files using one of the supported generators:",
679 			"",
680 			"visuald - VisualD project files",
681 			"sublimetext - SublimeText project file",
682 			"cmake - CMake build scripts",
683 			"build - Builds the package directly",
684 			"",
685 			"An optional package name can be given to generate a different package than the root/CWD package."
686 		];
687 	}
688 
689 	override void prepare(scope CommandArgs args)
690 	{
691 		super.prepare(args);
692 
693 		args.getopt("combined", &m_combined, [
694 			"Tries to build the whole project in a single compiler run."
695 		]);
696 
697 		args.getopt("print-builds", &m_printBuilds, [
698 			"Prints the list of available build types"
699 		]);
700 		args.getopt("print-configs", &m_printConfigs, [
701 			"Prints the list of available configurations"
702 		]);
703 		args.getopt("print-platform", &m_printPlatform, [
704 			"Prints the identifiers for the current build platform as used for the build fields in dub.json"
705 		]);
706 		args.getopt("parallel", &m_parallel, [
707 			"Runs multiple compiler instances in parallel, if possible."
708 		]);
709 	}
710 
711 	override int execute(Dub dub, string[] free_args, string[] app_args)
712 	{
713 		string package_name;
714 		if (!m_generator.length) {
715 			enforceUsage(free_args.length >= 1 && free_args.length <= 2, "Expected one or two arguments.");
716 			m_generator = free_args[0];
717 			if (free_args.length >= 2) package_name = free_args[1];
718 		} else {
719 			enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
720 			if (free_args.length >= 1) package_name = free_args[0];
721 		}
722 
723 		setupPackage(dub, package_name);
724 
725 		if (m_printBuilds) { // FIXME: use actual package data
726 			logInfo("Available build types:");
727 			foreach (tp; ["debug", "release", "unittest", "profile"])
728 				logInfo("  %s", tp);
729 			logInfo("");
730 		}
731 
732 		m_defaultConfig = dub.project.getDefaultConfiguration(m_buildPlatform);
733 		if (m_printConfigs) {
734 			logInfo("Available configurations:");
735 			foreach (tp; dub.configurations)
736 				logInfo("  %s%s", tp, tp == m_defaultConfig ? " [default]" : null);
737 			logInfo("");
738 		}
739 
740 		GeneratorSettings gensettings;
741 		gensettings.platform = m_buildPlatform;
742 		gensettings.config = m_buildConfig.length ? m_buildConfig : m_defaultConfig;
743 		gensettings.buildType = m_buildType;
744 		gensettings.buildMode = m_buildMode;
745 		gensettings.compiler = m_compiler;
746 		gensettings.buildSettings = m_buildSettings;
747 		gensettings.combined = m_combined;
748 		gensettings.run = m_run;
749 		gensettings.runArgs = app_args;
750 		gensettings.force = m_force;
751 		gensettings.rdmd = m_rdmd;
752 		gensettings.tempBuild = m_tempBuild;
753 		gensettings.parallelBuild = m_parallel;
754 
755 		logDiagnostic("Generating using %s", m_generator);
756 		dub.generateProject(m_generator, gensettings);
757 		if (m_buildType == "ddox") dub.runDdox(gensettings.run, app_args);
758 		return 0;
759 	}
760 }
761 
762 class BuildCommand : GenerateCommand {
763 	this()
764 	{
765 		this.name = "build";
766 		this.argumentsPattern = "[<package>]";
767 		this.description = "Builds a package (uses the main package in the current working directory by default)";
768 		this.helpText = [
769 			"Builds a package (uses the main package in the current working directory by default)"
770 		];
771 	}
772 
773 	override void prepare(scope CommandArgs args)
774 	{
775 		args.getopt("rdmd", &m_rdmd, [
776 			"Use rdmd instead of directly invoking the compiler"
777 		]);
778 
779 		args.getopt("f|force", &m_force, [
780 			"Forces a recompilation even if the target is up to date"
781 		]);
782 		super.prepare(args);
783 		m_generator = "build";
784 	}
785 
786 	override int execute(Dub dub, string[] free_args, string[] app_args)
787 	{
788 		return super.execute(dub, free_args, app_args);
789 	}
790 }
791 
792 class RunCommand : BuildCommand {
793 	this()
794 	{
795 		this.name = "run";
796 		this.argumentsPattern = "[<package>]";
797 		this.description = "Builds and runs a package (default command)";
798 		this.helpText = [
799 			"Builds and runs a package (uses the main package in the current working directory by default)"
800 		];
801 		this.acceptsAppArgs = true;
802 	}
803 
804 	override void prepare(scope CommandArgs args)
805 	{
806 		args.getopt("temp-build", &m_tempBuild, [
807 			"Builds the project in the temp folder if possible."
808 		]);
809 
810 		super.prepare(args);
811 		m_run = true;
812 	}
813 
814 	override int execute(Dub dub, string[] free_args, string[] app_args)
815 	{
816 		return super.execute(dub, free_args, app_args);
817 	}
818 }
819 
820 class TestCommand : PackageBuildCommand {
821 	private {
822 		string m_mainFile;
823 		bool m_combined = false;
824 		bool m_force = false;
825 	}
826 
827 	this()
828 	{
829 		this.name = "test";
830 		this.argumentsPattern = "[<package>]";
831 		this.description = "Executes the tests of the selected package";
832 		this.helpText = [
833 			`Builds the package and executes all contained unit tests.`,
834 			``,
835 			`If no explicit configuration is given, an existing "unittest" ` ~
836 			`configuration will be preferred for testing. If none exists, the ` ~
837 			`first library type configuration will be used, and if that doesn't ` ~
838 			`exist either, the first executable configuration is chosen.`,
839 			``,
840 			`When a custom main file (--main-file) is specified, only library ` ~
841 			`configurations can be used. Otherwise, depending on the type of ` ~
842 			`the selected configuration, either an existing main file will be ` ~
843 			`used (and needs to be properly adjusted to just run the unit ` ~
844 			`tests for 'version(unittest)'), or DUB will generate one for ` ~
845 			`library type configurations.`,
846 			``,
847 			`Finally, if the package contains a dependency to the "tested" ` ~
848 			`package, the automatically generated main file will use it to ` ~
849 			`run the unit tests.`
850 		];
851 		this.acceptsAppArgs = true;
852 	}
853 
854 	override void prepare(scope CommandArgs args)
855 	{
856 		args.getopt("main-file", &m_mainFile, [
857 			"Specifies a custom file containing the main() function to use for running the tests."
858 		]);
859 		args.getopt("combined", &m_combined, [
860 			"Tries to build the whole project in a single compiler run."
861 		]);
862 		args.getopt("f|force", &m_force, [
863 			"Forces a recompilation even if the target is up to date"
864 		]);
865 		bool coverage = false;
866 		args.getopt("coverage", &coverage, [
867 			"Enables code coverage statistics to be generated."
868 		]);
869 		if (coverage) m_buildType = "unittest-cov";
870 
871 		super.prepare(args);
872 	}
873 
874 	override int execute(Dub dub, string[] free_args, string[] app_args)
875 	{
876 		string package_name;
877 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
878 		if (free_args.length >= 1) package_name = free_args[0];
879 
880 		setupPackage(dub, package_name, "unittest");
881 
882 		GeneratorSettings settings;
883 		settings.platform = m_buildPlatform;
884 		settings.compiler = getCompiler(m_buildPlatform.compilerBinary);
885 		settings.buildType = m_buildType;
886 		settings.buildMode = m_buildMode;
887 		settings.buildSettings = m_buildSettings;
888 		settings.combined = m_combined;
889 		settings.force = m_force;
890 		settings.tempBuild = m_single;
891 		settings.run = true;
892 		settings.runArgs = app_args;
893 
894 		dub.testProject(settings, m_buildConfig, Path(m_mainFile));
895 		return 0;
896 	}
897 }
898 
899 class DescribeCommand : PackageBuildCommand {
900 	private {
901 		bool m_importPaths = false;
902 		bool m_stringImportPaths = false;
903 		bool m_dataList = false;
904 		bool m_dataNullDelim = false;
905 		string[] m_data;
906 	}
907 
908 	this()
909 	{
910 		this.name = "describe";
911 		this.argumentsPattern = "[<package>]";
912 		this.description = "Prints a JSON description of the project and its dependencies";
913 		this.helpText = [
914 			"Prints a JSON build description for the root package an all of " ~
915 			"their dependencies in a format similar to a JSON package " ~
916 			"description file. This is useful mostly for IDEs.",
917 			"",
918 			"All usual options that are also used for build/run/generate apply.",
919 			"",
920 			"When --data=VALUE is supplied, specific build settings for a project " ~
921 			"will be printed instead (by default, formatted for the current compiler).",
922 			"",
923 			"The --data=VALUE option can be specified multiple times to retrieve " ~
924 			"several pieces of information at once. A comma-separated list is " ~
925 			"also acceptable (ex: --data=dflags,libs). The data will be output in " ~
926 			"the same order requested on the command line.",
927 			"",
928 			"The accepted values for --data=VALUE are:",
929 			"",
930 			"main-source-file, dflags, lflags, libs, linker-files, " ~
931 			"source-files, versions, debug-versions, import-paths, " ~
932 			"string-import-paths, import-files, options",
933 			"",
934 			"The following are also accepted by --data if --data-list is used:",
935 			"",
936 			"target-type, target-path, target-name, working-directory, " ~
937 			"copy-files, string-import-files, pre-generate-commands, " ~
938 			"post-generate-commands, pre-build-commands, post-build-commands, " ~
939 			"requirements",
940 		];
941 	}
942 
943 	override void prepare(scope CommandArgs args)
944 	{
945 		super.prepare(args);
946 
947 		args.getopt("import-paths", &m_importPaths, [
948 			"Shortcut for --data=import-paths --data-list"
949 		]);
950 
951 		args.getopt("string-import-paths", &m_stringImportPaths, [
952 			"Shortcut for --data=string-import-paths --data-list"
953 		]);
954 
955 		args.getopt("data", &m_data, [
956 			"Just list the values of a particular build setting, either for this "~
957 			"package alone or recursively including all dependencies. Accepts a "~
958 			"comma-separated list. See above for more details and accepted "~
959 			"possibilities for VALUE."
960 		]);
961 
962 		args.getopt("data-list", &m_dataList, [
963 			"Output --data information in list format (line-by-line), instead "~
964 			"of formatting for a compiler command line.",
965 		]);
966 
967 		args.getopt("data-0", &m_dataNullDelim, [
968 			"Output --data information using null-delimiters, rather than "~
969 			"spaces or newlines. Result is usable with, ex., xargs -0.",
970 		]);
971 	}
972 
973 	override int execute(Dub dub, string[] free_args, string[] app_args)
974 	{
975 		enforceUsage(
976 			!(m_importPaths && m_stringImportPaths),
977 			"--import-paths and --string-import-paths may not be used together."
978 		);
979 
980 		enforceUsage(
981 			!(m_data && (m_importPaths || m_stringImportPaths)),
982 			"--data may not be used together with --import-paths or --string-import-paths."
983 		);
984 
985 		// disable all log output to stdout and use "writeln" to output the JSON description
986 		auto ll = getLogLevel();
987 		setLogLevel(LogLevel.warn);
988 		scope (exit) setLogLevel(ll);
989 
990 		string package_name;
991 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
992 		if (free_args.length >= 1) package_name = free_args[0];
993 		setupPackage(dub, package_name);
994 
995 		m_defaultConfig = dub.project.getDefaultConfiguration(m_buildPlatform);
996 
997 		auto config = m_buildConfig.length ? m_buildConfig : m_defaultConfig;
998 
999 		GeneratorSettings settings;
1000 		settings.platform = m_buildPlatform;
1001 		settings.config = config;
1002 		settings.buildType = m_buildType;
1003 		settings.compiler = m_compiler;
1004 
1005 		if (m_importPaths) { m_data = ["import-paths"]; m_dataList = true; }
1006 		else if (m_stringImportPaths) { m_data = ["string-import-paths"]; m_dataList = true; }
1007 
1008 		if (m_data.length) {
1009 			ListBuildSettingsFormat lt;
1010 			with (ListBuildSettingsFormat)
1011 				lt = m_dataList ? (m_dataNullDelim ? listNul : list) : (m_dataNullDelim ? commandLineNul : commandLine);
1012 			dub.listProjectData(settings, m_data, lt);
1013 		} else {
1014 			auto desc = dub.project.describe(settings);
1015 			writeln(desc.serializeToPrettyJson());
1016 		}
1017 
1018 		return 0;
1019 	}
1020 }
1021 
1022 class CleanCommand : Command {
1023 	private {
1024 		bool m_allPackages;
1025 	}
1026 
1027 	this()
1028 	{
1029 		this.name = "clean";
1030 		this.argumentsPattern = "[<package>]";
1031 		this.description = "Removes intermediate build files and cached build results";
1032 		this.helpText = [
1033 			"This command removes any cached build files of the given package(s). The final target file, as well as any copyFiles are currently not removed.",
1034 			"Without arguments, the package in the current working directory will be cleaned."
1035 		];
1036 	}
1037 
1038 	override void prepare(scope CommandArgs args)
1039 	{
1040 		args.getopt("all-packages", &m_allPackages, [
1041 			"Cleans up *all* known packages (dub list)"
1042 		]);
1043 	}
1044 
1045 	override int execute(Dub dub, string[] free_args, string[] app_args)
1046 	{
1047 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1048 		enforceUsage(app_args.length == 0, "Application arguments are not supported for the clean command.");
1049 		enforceUsage(!m_allPackages || !free_args.length, "The --all-packages flag may not be used together with an explicit package name.");
1050 
1051 		enforce(free_args.length == 0, "Cleaning a specific package isn't possible right now.");
1052 
1053 		if (m_allPackages) {
1054 			foreach (p; dub.packageManager.getPackageIterator())
1055 				dub.cleanPackage(p.path);
1056 		} else {
1057 			dub.cleanPackage(dub.rootPath);
1058 		}
1059 
1060 		return 0;
1061 	}
1062 }
1063 
1064 
1065 /******************************************************************************/
1066 /* FETCH / REMOVE / UPGRADE                                                   */
1067 /******************************************************************************/
1068 
1069 class UpgradeCommand : Command {
1070 	private {
1071 		bool m_prerelease = false;
1072 		bool m_forceRemove = false;
1073 		bool m_missingOnly = false;
1074 		bool m_verify = false;
1075 	}
1076 
1077 	this()
1078 	{
1079 		this.name = "upgrade";
1080 		this.argumentsPattern = "[<package>]";
1081 		this.description = "Forces an upgrade of all dependencies";
1082 		this.helpText = [
1083 			"Upgrades all dependencies of the package by querying the package registry(ies) for new versions.",
1084 			"",
1085 			"This will also update the versions stored in the selections file ("~SelectedVersions.defaultFile~") accordingly.",
1086 			"",
1087 			"If a package specified, (only) that package will be upgraded. Otherwise all direct and indirect dependencies of the current package will get upgraded."
1088 		];
1089 	}
1090 
1091 	override void prepare(scope CommandArgs args)
1092 	{
1093 		args.getopt("prerelease", &m_prerelease, [
1094 			"Uses the latest pre-release version, even if release versions are available"
1095 		]);
1096 		args.getopt("force-remove", &m_forceRemove, [
1097 			"Force deletion of fetched packages with untracked files"
1098 		]);
1099 		args.getopt("verify", &m_verify, [
1100 			"Updates the project and performs a build. If successful, rewrites the selected versions file <to be implemeted>."
1101 		]);
1102 		args.getopt("missing-only", &m_missingOnly, [
1103 			"Performs an upgrade only for dependencies that don't yet have a version selected. This is also done automatically before each build."
1104 		]);
1105 	}
1106 
1107 	override int execute(Dub dub, string[] free_args, string[] app_args)
1108 	{
1109 		enforceUsage(free_args.length <= 1, "Unexpected arguments.");
1110 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1111 		enforceUsage(!m_verify, "--verify is not yet implemented.");
1112 		dub.loadPackage();
1113 		logInfo("Upgrading project in %s", dub.projectPath.toNativeString());
1114 		auto options = UpgradeOptions.upgrade|UpgradeOptions.select;
1115 		if (m_missingOnly) options &= ~UpgradeOptions.upgrade;
1116 		if (m_prerelease) options |= UpgradeOptions.preRelease;
1117 		if (m_forceRemove) options |= UpgradeOptions.forceRemove;
1118 		enforceUsage(app_args.length == 0, "Upgrading a specific package is not yet implemented.");
1119 		dub.upgrade(options);
1120 		return 0;
1121 	}
1122 }
1123 
1124 class FetchRemoveCommand : Command {
1125 	protected {
1126 		string m_version;
1127 		bool m_forceRemove = false;
1128 	}
1129 
1130 	override void prepare(scope CommandArgs args)
1131 	{
1132 		args.getopt("version", &m_version, [
1133 			"Use the specified version/branch instead of the latest available match",
1134 			"The remove command also accepts \"*\" here as a wildcard to remove all versions of the package from the specified location"
1135 		]);
1136 
1137 		args.getopt("force-remove", &m_forceRemove, [
1138 			"Force deletion of fetched packages with untracked files"
1139 		]);
1140 	}
1141 
1142 	abstract override int execute(Dub dub, string[] free_args, string[] app_args);
1143 }
1144 
1145 class FetchCommand : FetchRemoveCommand {
1146 	this()
1147 	{
1148 		this.name = "fetch";
1149 		this.argumentsPattern = "<name>";
1150 		this.description = "Manually retrieves and caches a package";
1151 		this.helpText = [
1152 			"Note: Use the \"dependencies\" field in the package description file (e.g. dub.json) if you just want to use a certain package as a dependency, you don't have to explicitly fetch packages.",
1153 			"",
1154 			"Explicit retrieval/removal of packages is only needed when you want to put packages to a place where several applications can share these. If you just have an dependency to a package, just add it to your dub.json, dub will do the rest for you.",
1155 			"",
1156 			"Without specified options, placement/removal will default to a user wide shared location.",
1157 			"",
1158 			"Complete applications can be retrieved and run easily by e.g.",
1159 			"$ dub fetch vibelog --local",
1160 			"$ cd vibelog",
1161 			"$ dub",
1162 			"",
1163 			"This will grab all needed dependencies and compile and run the application.",
1164 			"",
1165 			"Note: DUB does not do a system installation of packages. Packages are instead only registered within DUB's internal ecosystem. Generation of native system packages/installers may be added later as a separate feature."
1166 		];
1167 	}
1168 
1169 	override void prepare(scope CommandArgs args)
1170 	{
1171 		super.prepare(args);
1172 	}
1173 
1174 	override int execute(Dub dub, string[] free_args, string[] app_args)
1175 	{
1176 		enforceUsage(free_args.length == 1, "Expecting exactly one argument.");
1177 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1178 
1179 		auto location = dub.defaultPlacementLocation;
1180 
1181 		auto name = free_args[0];
1182 
1183 		FetchOptions fetchOpts;
1184 		fetchOpts |= FetchOptions.forceBranchUpgrade;
1185 		fetchOpts |= m_forceRemove ? FetchOptions.forceRemove : FetchOptions.none;
1186 		if (m_version.length) dub.fetch(name, Dependency(m_version), location, fetchOpts);
1187 		else {
1188 			try {
1189 				dub.fetch(name, Dependency(">=0.0.0"), location, fetchOpts);
1190 				logInfo(
1191 					"Please note that you need to use `dub run <pkgname>` " ~
1192 					"or add it to dependencies of your package to actually use/run it. " ~
1193 					"dub does not do actual installation of packages outside of its own ecosystem.");
1194 			}
1195 			catch(Exception e){
1196 				logInfo("Getting a release version failed: %s", e.msg);
1197 				logInfo("Retry with ~master...");
1198 				dub.fetch(name, Dependency("~master"), location, fetchOpts);
1199 			}
1200 		}
1201 		return 0;
1202 	}
1203 }
1204 
1205 class InstallCommand : FetchCommand {
1206 	this() { this.name = "install"; hidden = true; }
1207 	override void prepare(scope CommandArgs args) { super.prepare(args); }
1208 	override int execute(Dub dub, string[] free_args, string[] app_args)
1209 	{
1210 		warnRenamed("install", "fetch");
1211 		return super.execute(dub, free_args, app_args);
1212 	}
1213 }
1214 
1215 class RemoveCommand : FetchRemoveCommand {
1216 	private {
1217 		bool m_nonInteractive;
1218 	}
1219 
1220 	this()
1221 	{
1222 		this.name = "remove";
1223 		this.argumentsPattern = "<name>";
1224 		this.description = "Removes a cached package";
1225 		this.helpText = [
1226 			"Removes a package that is cached on the local system."
1227 		];
1228 	}
1229 
1230 	override void prepare(scope CommandArgs args)
1231 	{
1232 		super.prepare(args);
1233 		args.getopt("n|non-interactive", &m_nonInteractive, ["Don't enter interactive mode."]);
1234 	}
1235 
1236 	override int execute(Dub dub, string[] free_args, string[] app_args)
1237 	{
1238 		enforceUsage(free_args.length == 1, "Expecting exactly one argument.");
1239 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1240 
1241 		auto package_id = free_args[0];
1242 		auto location = dub.defaultPlacementLocation;
1243 
1244 		size_t resolveVersion(in Package[] packages) {
1245 			// just remove only package version
1246 			if (packages.length == 1)
1247 				return 0;
1248 
1249 			writeln("Select version of '", package_id, "' to remove from location '", location, "':");
1250 			foreach (i, pack; packages)
1251 				writefln("%s) %s", i + 1, pack.version_);
1252 			writeln(packages.length + 1, ") ", "all versions");
1253 			while (true) {
1254 				writef("> ");
1255 				auto inp = readln();
1256 				if (!inp.length) // Ctrl+D
1257 					return size_t.max;
1258 				inp = inp.stripRight;
1259 				if (!inp.length) // newline or space
1260 					continue;
1261 				try {
1262 					immutable selection = inp.to!size_t - 1;
1263 					if (selection <= packages.length)
1264 						return selection;
1265 				} catch (ConvException e) {
1266 				}
1267 				logError("Please enter a number between 1 and %s.", packages.length + 1);
1268 			}
1269 		}
1270 
1271 		if (m_nonInteractive || !m_version.empty)
1272 			dub.remove(package_id, m_version, location, m_forceRemove);
1273 		else
1274 			dub.remove(package_id, location, m_forceRemove, &resolveVersion);
1275 		return 0;
1276 	}
1277 }
1278 
1279 class UninstallCommand : RemoveCommand {
1280 	this() { this.name = "uninstall"; hidden = true; }
1281 	override void prepare(scope CommandArgs args) { super.prepare(args); }
1282 	override int execute(Dub dub, string[] free_args, string[] app_args)
1283 	{
1284 		warnRenamed("uninstall", "remove");
1285 		return super.execute(dub, free_args, app_args);
1286 	}
1287 }
1288 
1289 
1290 /******************************************************************************/
1291 /* ADD/REMOVE PATH/LOCAL                                                      */
1292 /******************************************************************************/
1293 
1294 abstract class RegistrationCommand : Command {
1295 	private {
1296 		bool m_system;
1297 	}
1298 
1299 	override void prepare(scope CommandArgs args)
1300 	{
1301 		args.getopt("system", &m_system, [
1302 			"Register system-wide instead of user-wide"
1303 		]);
1304 	}
1305 
1306 	abstract override int execute(Dub dub, string[] free_args, string[] app_args);
1307 }
1308 
1309 class AddPathCommand : RegistrationCommand {
1310 	this()
1311 	{
1312 		this.name = "add-path";
1313 		this.argumentsPattern = "<path>";
1314 		this.description = "Adds a default package search path";
1315 		this.helpText = [
1316 			"Adds a default package search path. All direct sub folders of this path will be searched for package descriptions and will be made available as packages. Using this command has the equivalent effect as calling 'dub add-local' on each of the sub folders manually.",
1317 			"",
1318 			"Any packages registered using add-path will be preferred over packages downloaded from the package registry when searching for dependencies during a build operation.",
1319 			"",
1320 			"The version of the packages will be determined by one of the following:",
1321 			"  - For GIT working copies, the last tag (git describe) is used to determine the version",
1322 			"  - If the package contains a \"version\" field in the package description, this is used",
1323 			"  - If neither of those apply, \"~master\" is assumed"
1324 		];
1325 	}
1326 
1327 	override int execute(Dub dub, string[] free_args, string[] app_args)
1328 	{
1329 		enforceUsage(free_args.length == 1, "Missing search path.");
1330 		dub.addSearchPath(free_args[0], m_system);
1331 		return 0;
1332 	}
1333 }
1334 
1335 class RemovePathCommand : RegistrationCommand {
1336 	this()
1337 	{
1338 		this.name = "remove-path";
1339 		this.argumentsPattern = "<path>";
1340 		this.description = "Removes a package search path";
1341 		this.helpText = ["Removes a package search path previously added with add-path."];
1342 	}
1343 
1344 	override int execute(Dub dub, string[] free_args, string[] app_args)
1345 	{
1346 		enforceUsage(free_args.length == 1, "Expected one argument.");
1347 		dub.removeSearchPath(free_args[0], m_system);
1348 		return 0;
1349 	}
1350 }
1351 
1352 class AddLocalCommand : RegistrationCommand {
1353 	this()
1354 	{
1355 		this.name = "add-local";
1356 		this.argumentsPattern = "<path> [<version>]";
1357 		this.description = "Adds a local package directory (e.g. a git repository)";
1358 		this.helpText = [
1359 			"Adds a local package directory to be used during dependency resolution. This command is useful for registering local packages, such as GIT working copies, that are either not available in the package registry, or are supposed to be overwritten.",
1360 			"",
1361 			"The version of the package is either determined automatically (see the \"add-path\" command, or can be explicitly overwritten by passing a version on the command line.",
1362 			"",
1363 			"See 'dub add-path -h' for a way to register multiple local packages at once."
1364 		];
1365 	}
1366 
1367 	override int execute(Dub dub, string[] free_args, string[] app_args)
1368 	{
1369 		enforceUsage(free_args.length == 1 || free_args.length == 2, "Expecting one or two arguments.");
1370 		string ver = free_args.length == 2 ? free_args[1] : null;
1371 		dub.addLocalPackage(free_args[0], ver, m_system);
1372 		return 0;
1373 	}
1374 }
1375 
1376 class RemoveLocalCommand : RegistrationCommand {
1377 	this()
1378 	{
1379 		this.name = "remove-local";
1380 		this.argumentsPattern = "<path>";
1381 		this.description = "Removes a local package directory";
1382 		this.helpText = ["Removes a local package directory"];
1383 	}
1384 
1385 	override int execute(Dub dub, string[] free_args, string[] app_args)
1386 	{
1387 		enforceUsage(free_args.length >= 1, "Missing package path argument.");
1388 		enforceUsage(free_args.length <= 1, "Expected the package path to be the only argument.");
1389 		dub.removeLocalPackage(free_args[0], m_system);
1390 		return 0;
1391 	}
1392 }
1393 
1394 class ListCommand : Command {
1395 	this()
1396 	{
1397 		this.name = "list";
1398 		this.argumentsPattern = "";
1399 		this.description = "Prints a list of all local packages dub is aware of";
1400 		this.helpText = [
1401 			"Prints a list of all local packages. This includes all cached packages (user or system wide), all packages in the package search paths (\"dub add-path\") and all manually registered packages (\"dub add-local\")."
1402 		];
1403 	}
1404 	override void prepare(scope CommandArgs args) {}
1405 	override int execute(Dub dub, string[] free_args, string[] app_args)
1406 	{
1407 		logInfo("Packages present in the system and known to dub:");
1408 		foreach (p; dub.packageManager.getPackageIterator())
1409 			logInfo("  %s %s: %s", p.name, p.version_, p.path.toNativeString());
1410 		logInfo("");
1411 		return 0;
1412 	}
1413 }
1414 
1415 class ListInstalledCommand : ListCommand {
1416 	this() { this.name = "list-installed"; hidden = true; }
1417 	override void prepare(scope CommandArgs args) { super.prepare(args); }
1418 	override int execute(Dub dub, string[] free_args, string[] app_args)
1419 	{
1420 		warnRenamed("list-installed", "list");
1421 		return super.execute(dub, free_args, app_args);
1422 	}
1423 }
1424 
1425 class SearchCommand : Command {
1426 	this()
1427 	{
1428 		this.name = "search";
1429 		this.argumentsPattern = "<query>";
1430 		this.description = "Search for available packages.";
1431 		this.helpText = [
1432 			"Search all specified DUB registries for packages matching query."
1433 		];
1434 	}
1435 	override void prepare(scope CommandArgs args) {}
1436 	override int execute(Dub dub, string[] free_args, string[] app_args)
1437 	{
1438 		enforce(free_args.length == 1, "Expected one argument.");
1439 		auto res = dub.searchPackages(free_args[0]);
1440 		if (res.empty)
1441 		{
1442 			logError("No matches found.");
1443 			return 1;
1444 		}
1445 		auto justify = res
1446 			.map!((descNmatches) => descNmatches[1])
1447 			.joiner
1448 			.map!(m => m.name.length + m.version_.length)
1449 			.reduce!max + " ()".length;
1450 		justify += (~justify & 3) + 1; // round to next multiple of 4
1451 		foreach (desc, matches; res)
1452 		{
1453 			logInfo("==== %s ====", desc);
1454 			foreach (m; matches)
1455 				logInfo("%s%s", leftJustify(m.name ~ " (" ~ m.version_ ~ ")", justify), m.description);
1456 		}
1457 		return 0;
1458 	}
1459 }
1460 
1461 
1462 /******************************************************************************/
1463 /* OVERRIDES                                                                  */
1464 /******************************************************************************/
1465 
1466 class AddOverrideCommand : Command {
1467 	private {
1468 		bool m_system = false;
1469 	}
1470 
1471 	this()
1472 	{
1473 		this.name = "add-override";
1474 		this.argumentsPattern = "<package> <version-spec> <target-path/target-version>";
1475 		this.description = "Adds a new package override.";
1476 		this.helpText = [
1477 		];
1478 	}
1479 
1480 	override void prepare(scope CommandArgs args)
1481 	{
1482 		args.getopt("system", &m_system, [
1483 			"Register system-wide instead of user-wide"
1484 		]);
1485 	}
1486 
1487 	override int execute(Dub dub, string[] free_args, string[] app_args)
1488 	{
1489 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1490 		enforceUsage(free_args.length == 3, "Expected three arguments, not "~free_args.length.to!string);
1491 		auto scope_ = m_system ? LocalPackageType.system : LocalPackageType.user;
1492 		auto pack = free_args[0];
1493 		auto ver = Dependency(free_args[1]);
1494 		if (existsFile(Path(free_args[2]))) {
1495 			auto target = Path(free_args[2]);
1496 			dub.packageManager.addOverride(scope_, pack, ver, target);
1497 			logInfo("Added override %s %s => %s", pack, ver, target);
1498 		} else {
1499 			auto target = Version(free_args[2]);
1500 			dub.packageManager.addOverride(scope_, pack, ver, target);
1501 			logInfo("Added override %s %s => %s", pack, ver, target);
1502 		}
1503 		return 0;
1504 	}
1505 }
1506 
1507 class RemoveOverrideCommand : Command {
1508 	private {
1509 		bool m_system = false;
1510 	}
1511 
1512 	this()
1513 	{
1514 		this.name = "remove-override";
1515 		this.argumentsPattern = "<package> <version-spec>";
1516 		this.description = "Removes an existing package override.";
1517 		this.helpText = [
1518 		];
1519 	}
1520 
1521 	override void prepare(scope CommandArgs args)
1522 	{
1523 		args.getopt("system", &m_system, [
1524 			"Register system-wide instead of user-wide"
1525 		]);
1526 	}
1527 
1528 	override int execute(Dub dub, string[] free_args, string[] app_args)
1529 	{
1530 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1531 		enforceUsage(free_args.length == 2, "Expected two arguments, not "~free_args.length.to!string);
1532 		auto scope_ = m_system ? LocalPackageType.system : LocalPackageType.user;
1533 		dub.packageManager.removeOverride(scope_, free_args[0], Dependency(free_args[1]));
1534 		return 0;
1535 	}
1536 }
1537 
1538 class ListOverridesCommand : Command {
1539 	this()
1540 	{
1541 		this.name = "list-overrides";
1542 		this.argumentsPattern = "";
1543 		this.description = "Prints a list of all local package overrides";
1544 		this.helpText = [
1545 			"Prints a list of all overridden packages added via \"dub add-override\"."
1546 		];
1547 	}
1548 	override void prepare(scope CommandArgs args) {}
1549 	override int execute(Dub dub, string[] free_args, string[] app_args)
1550 	{
1551 		void printList(in PackageOverride[] overrides, string caption)
1552 		{
1553 			if (overrides.length == 0) return;
1554 			logInfo("# %s", caption);
1555 			foreach (ovr; overrides) {
1556 				if (!ovr.targetPath.empty) logInfo("%s %s => %s", ovr.package_, ovr.version_, ovr.targetPath);
1557 				else logInfo("%s %s => %s", ovr.package_, ovr.version_, ovr.targetVersion);
1558 			}
1559 		}
1560 		printList(dub.packageManager.getOverrides(LocalPackageType.user), "User wide overrides");
1561 		printList(dub.packageManager.getOverrides(LocalPackageType.system), "System wide overrides");
1562 		return 0;
1563 	}
1564 }
1565 
1566 /******************************************************************************/
1567 /* Cache cleanup                                                              */
1568 /******************************************************************************/
1569 
1570 class CleanCachesCommand : Command {
1571 	this()
1572 	{
1573 		this.name = "clean-caches";
1574 		this.argumentsPattern = "";
1575 		this.description = "Removes cached metadata";
1576 		this.helpText = [
1577 			"This command removes any cached metadata like the list of available packages and their latest version."
1578 		];
1579 	}
1580 
1581 	override void prepare(scope CommandArgs args) {}
1582 
1583 	override int execute(Dub dub, string[] free_args, string[] app_args)
1584 	{
1585 		return 0;
1586 	}
1587 }
1588 
1589 /******************************************************************************/
1590 /* DUSTMITE                                                                   */
1591 /******************************************************************************/
1592 
1593 class DustmiteCommand : PackageBuildCommand {
1594 	private {
1595 		int m_compilerStatusCode = int.min;
1596 		int m_linkerStatusCode = int.min;
1597 		int m_programStatusCode = int.min;
1598 		string m_compilerRegex;
1599 		string m_linkerRegex;
1600 		string m_programRegex;
1601 		string m_testPackage;
1602 		bool m_combined;
1603 	}
1604 
1605 	this()
1606 	{
1607 		this.name = "dustmite";
1608 		this.argumentsPattern = "<destination-path>";
1609 		this.acceptsAppArgs = true;
1610 		this.description = "Create reduced test cases for build errors";
1611 		this.helpText = [
1612 			"This command uses the Dustmite utility to isolate the cause of build errors in a DUB project.",
1613 			"",
1614 			"It will create a copy of all involved packages and run dustmite on this copy, leaving a reduced test case.",
1615 			"",
1616 			"Determining the desired error condition is done by checking the compiler/linker status code, as well as their output (stdout and stderr combined). If --program-status or --program-regex is given and the generated binary is an executable, it will be executed and its output will also be incorporated into the final decision."
1617 		];
1618 	}
1619 
1620 	override void prepare(scope CommandArgs args)
1621 	{
1622 		args.getopt("compiler-status", &m_compilerStatusCode, ["The expected status code of the compiler run"]);
1623 		args.getopt("compiler-regex", &m_compilerRegex, ["A regular expression used to match against the compiler output"]);
1624 		args.getopt("linker-status", &m_linkerStatusCode, ["The expected status code of the liner run"]);
1625 		args.getopt("linker-regex", &m_linkerRegex, ["A regular expression used to match against the linker output"]);
1626 		args.getopt("program-status", &m_programStatusCode, ["The expected status code of the built executable"]);
1627 		args.getopt("program-regex", &m_programRegex, ["A regular expression used to match against the program output"]);
1628 		args.getopt("test-package", &m_testPackage, ["Perform a test run - usually only used internally"]);
1629 		args.getopt("combined", &m_combined, ["Builds multiple packages with one compiler run"]);
1630 		super.prepare(args);
1631 
1632 		// speed up loading when in test mode
1633 		if (m_testPackage.length) {
1634 			skipDubInitialization = true;
1635 			m_nodeps = true;
1636 		}
1637 	}
1638 
1639 	override int execute(Dub dub, string[] free_args, string[] app_args)
1640 	{
1641 		import std.format : formattedWrite;
1642 
1643 		if (m_testPackage.length) {
1644 			dub = new Dub(Path(getcwd()));
1645 
1646 			setupPackage(dub, m_testPackage);
1647 			m_defaultConfig = dub.project.getDefaultConfiguration(m_buildPlatform);
1648 
1649 			GeneratorSettings gensettings;
1650 			gensettings.platform = m_buildPlatform;
1651 			gensettings.config = m_buildConfig.length ? m_buildConfig : m_defaultConfig;
1652 			gensettings.buildType = m_buildType;
1653 			gensettings.compiler = m_compiler;
1654 			gensettings.buildSettings = m_buildSettings;
1655 			gensettings.combined = m_combined;
1656 			gensettings.run = m_programStatusCode != int.min || m_programRegex.length;
1657 			gensettings.runArgs = app_args;
1658 			gensettings.force = true;
1659 			gensettings.compileCallback = check(m_compilerStatusCode, m_compilerRegex);
1660 			gensettings.linkCallback = check(m_linkerStatusCode, m_linkerRegex);
1661 			gensettings.runCallback = check(m_programStatusCode, m_programRegex);
1662 			try dub.generateProject("build", gensettings);
1663 			catch (DustmiteMismatchException) {
1664 				logInfo("Dustmite test doesn't match.");
1665 				return 3;
1666 			}
1667 			catch (DustmiteMatchException) {
1668 				logInfo("Dustmite test matches.");
1669 				return 0;
1670 			}
1671 		} else {
1672 			enforceUsage(free_args.length == 1, "Expected destination path.");
1673 			auto path = Path(free_args[0]);
1674 			path.normalize();
1675 			enforceUsage(path.length > 0, "Destination path must not be empty.");
1676 			if (!path.absolute) path = Path(getcwd()) ~ path;
1677 			enforceUsage(!path.startsWith(dub.rootPath), "Destination path must not be a sub directory of the tested package!");
1678 
1679 			setupPackage(dub, null);
1680 			auto prj = dub.project;
1681 			if (m_buildConfig.empty)
1682 				m_buildConfig = prj.getDefaultConfiguration(m_buildPlatform);
1683 
1684 			void copyFolderRec(Path folder, Path dstfolder)
1685 			{
1686 				mkdirRecurse(dstfolder.toNativeString());
1687 				foreach (de; iterateDirectory(folder.toNativeString())) {
1688 					if (de.name.startsWith(".")) continue;
1689 					if (de.isDirectory) {
1690 						copyFolderRec(folder ~ de.name, dstfolder ~ de.name);
1691 					} else {
1692 						if (de.name.endsWith(".o") || de.name.endsWith(".obj")) continue;
1693 						if (de.name.endsWith(".exe")) continue;
1694 						try copyFile(folder ~ de.name, dstfolder ~ de.name);
1695 						catch (Exception e) {
1696 							logWarn("Failed to copy file %s: %s", (folder ~ de.name).toNativeString(), e.msg);
1697 						}
1698 					}
1699 				}
1700 			}
1701 
1702 			static void fixPathDependency(string pack, ref Dependency dep) {
1703 				if (dep.path.length > 0) {
1704 					auto mainpack = getBasePackageName(pack);
1705 					dep.path = Path("../") ~ mainpack;
1706 				}
1707 			}
1708 
1709 			void fixPathDependencies(ref PackageRecipe recipe, Path base_path)
1710 			{
1711 				foreach (name, ref dep; recipe.buildSettings.dependencies)
1712 					fixPathDependency(name, dep);
1713 
1714 				foreach (ref cfg; recipe.configurations)
1715 					foreach (name, ref dep; cfg.buildSettings.dependencies)
1716 						fixPathDependency(name, dep);
1717 
1718 				foreach (ref subp; recipe.subPackages)
1719 					if (subp.path.length) {
1720 						auto sub_path = base_path ~ Path(subp.path);
1721 						auto pack = prj.packageManager.getOrLoadPackage(sub_path);
1722 						fixPathDependencies(pack.recipe, sub_path);
1723 						pack.storeInfo(sub_path);
1724 					} else fixPathDependencies(subp.recipe, base_path);
1725 			}
1726 
1727 			bool[string] visited;
1728 			foreach (pack_; prj.getTopologicalPackageList()) {
1729 				auto pack = pack_.basePackage;
1730 				if (pack.name in visited) continue;
1731 				visited[pack.name] = true;
1732 				auto dst_path = path ~ pack.name;
1733 				logInfo("Copy package '%s' to destination folder...", pack.name);
1734 				copyFolderRec(pack.path, dst_path);
1735 
1736 				// adjust all path based dependencies
1737 				fixPathDependencies(pack.recipe, dst_path);
1738 
1739 				// overwrite package description file with additional version information
1740 				pack.storeInfo(dst_path);
1741 			}
1742 
1743 			logInfo("Executing dustmite...");
1744 			auto testcmd = appender!string();
1745 			testcmd.formattedWrite("%s dustmite --vquiet --test-package=%s --build=%s --config=%s",
1746 				thisExePath, prj.name, m_buildType, m_buildConfig);
1747 
1748 			if (m_compilerName.length) testcmd.formattedWrite(" \"--compiler=%s\"", m_compilerName);
1749 			if (m_arch.length) testcmd.formattedWrite(" --arch=%s", m_arch);
1750 			if (m_compilerStatusCode != int.min) testcmd.formattedWrite(" --compiler-status=%s", m_compilerStatusCode);
1751 			if (m_compilerRegex.length) testcmd.formattedWrite(" \"--compiler-regex=%s\"", m_compilerRegex);
1752 			if (m_linkerStatusCode != int.min) testcmd.formattedWrite(" --linker-status=%s", m_linkerStatusCode);
1753 			if (m_linkerRegex.length) testcmd.formattedWrite(" \"--linker-regex=%s\"", m_linkerRegex);
1754 			if (m_programStatusCode != int.min) testcmd.formattedWrite(" --program-status=%s", m_programStatusCode);
1755 			if (m_programRegex.length) testcmd.formattedWrite(" \"--program-regex=%s\"", m_programRegex);
1756 			if (m_combined) testcmd ~= " --combined";
1757 			// TODO: pass *all* original parameters
1758 			logDiagnostic("Running dustmite: %s", testcmd);
1759 			auto dmpid = spawnProcess(["dustmite", path.toNativeString(), testcmd.data]);
1760 			return dmpid.wait();
1761 		}
1762 		return 0;
1763 	}
1764 
1765 	void delegate(int, string) check(int code_match, string regex_match)
1766 	{
1767 		return (code, output) {
1768 			import std.encoding;
1769 			import std.regex;
1770 
1771 			logInfo("%s", output);
1772 
1773 			if (code_match != int.min && code != code_match) {
1774 				logInfo("Exit code %s doesn't match expected value %s", code, code_match);
1775 				throw new DustmiteMismatchException;
1776 			}
1777 
1778 			if (regex_match.length > 0 && !match(output.sanitize, regex_match)) {
1779 				logInfo("Output doesn't match regex:");
1780 				logInfo("%s", output);
1781 				throw new DustmiteMismatchException;
1782 			}
1783 
1784 			if (code != 0 && code_match != int.min || regex_match.length > 0) {
1785 				logInfo("Tool failed, but matched either exit code or output - counting as match.");
1786 				throw new DustmiteMatchException;
1787 			}
1788 		};
1789 	}
1790 
1791 	static class DustmiteMismatchException : Exception {
1792 		this(string message = "", string file = __FILE__, int line = __LINE__, Throwable next = null)
1793 		{
1794 			super(message, file, line, next);
1795 		}
1796 	}
1797 
1798 	static class DustmiteMatchException : Exception {
1799 		this(string message = "", string file = __FILE__, int line = __LINE__, Throwable next = null)
1800 		{
1801 			super(message, file, line, next);
1802 		}
1803 	}
1804 }
1805 
1806 
1807 /******************************************************************************/
1808 /* CONVERT command                                                               */
1809 /******************************************************************************/
1810 
1811 class ConvertCommand : Command {
1812 	private {
1813 		string m_format;
1814 		bool m_stdout;
1815 	}
1816 
1817 	this()
1818 	{
1819 		this.name = "convert";
1820 		this.argumentsPattern = "";
1821 		this.description = "Converts the file format of the package recipe.";
1822 		this.helpText = [
1823 			"This command will convert between JSON and SDLang formatted package recipe files.",
1824 			"",
1825 			"Warning: Beware that any formatting and comments within the package recipe will get lost in the conversion process."
1826 		];
1827 	}
1828 
1829 	override void prepare(scope CommandArgs args)
1830 	{
1831 		args.getopt("f|format", &m_format, ["Specifies the target package recipe format. Possible values:", "  json, sdl"]);
1832 		args.getopt("s|stdout", &m_stdout, ["Outputs the converted package recipe to stdout instead of writing to disk."]);
1833 	}
1834 
1835 	override int execute(Dub dub, string[] free_args, string[] app_args)
1836 	{
1837 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1838 		enforceUsage(free_args.length == 0, "Unexpected arguments: "~free_args.join(" "));
1839 		enforceUsage(m_format.length > 0, "Missing target format file extension (--format=...).");
1840 		if (!loadCwdPackage(dub, true)) return 1;
1841 		dub.convertRecipe(m_format, m_stdout);
1842 		return 0;
1843 	}
1844 }
1845 
1846 
1847 /******************************************************************************/
1848 /* HELP                                                                       */
1849 /******************************************************************************/
1850 
1851 private {
1852 	enum shortArgColumn = 2;
1853 	enum longArgColumn = 6;
1854 	enum descColumn = 24;
1855 	enum lineWidth = 80 - 1;
1856 }
1857 
1858 private void showHelp(in CommandGroup[] commands, CommandArgs common_args)
1859 {
1860 	writeln(
1861 `USAGE: dub [--version] [<command>] [<options...>] [-- [<application arguments...>]]
1862 
1863 Manages the DUB project in the current directory. If the command is omitted,
1864 DUB will default to "run". When running an application, "--" can be used to
1865 separate DUB options from options passed to the application.
1866 
1867 Run "dub <command> --help" to get help for a specific command.
1868 
1869 You can use the "http_proxy" environment variable to configure a proxy server
1870 to be used for fetching packages.
1871 
1872 
1873 Available commands
1874 ==================`);
1875 
1876 	foreach (grp; commands) {
1877 		writeln();
1878 		writeWS(shortArgColumn);
1879 		writeln(grp.caption);
1880 		writeWS(shortArgColumn);
1881 		writerep!'-'(grp.caption.length);
1882 		writeln();
1883 		foreach (cmd; grp.commands) {
1884 			if (cmd.hidden) continue;
1885 			writeWS(shortArgColumn);
1886 			writef("%s %s", cmd.name, cmd.argumentsPattern);
1887 			auto chars_output = cmd.name.length + cmd.argumentsPattern.length + shortArgColumn + 1;
1888 			if (chars_output < descColumn) {
1889 				writeWS(descColumn - chars_output);
1890 			} else {
1891 				writeln();
1892 				writeWS(descColumn);
1893 			}
1894 			writeWrapped(cmd.description, descColumn, descColumn);
1895 		}
1896 	}
1897 	writeln();
1898 	writeln();
1899 	writeln(`Common options`);
1900 	writeln(`==============`);
1901 	writeln();
1902 	writeOptions(common_args);
1903 	writeln();
1904 	showVersion();
1905 }
1906 
1907 private void showVersion()
1908 {
1909 	writefln("DUB version %s, built on %s", getDUBVersion(), __DATE__);
1910 }
1911 
1912 private void showCommandHelp(Command cmd, CommandArgs args, CommandArgs common_args)
1913 {
1914 	writefln(`USAGE: dub %s %s [<options...>]%s`, cmd.name, cmd.argumentsPattern, cmd.acceptsAppArgs ? " [-- <application arguments...>]": null);
1915 	writeln();
1916 	foreach (ln; cmd.helpText)
1917 		ln.writeWrapped();
1918 
1919 	if (args.recognizedArgs.length) {
1920 		writeln();
1921 		writeln();
1922 		writeln("Command specific options");
1923 		writeln("========================");
1924 		writeln();
1925 		writeOptions(args);
1926 	}
1927 
1928 	writeln();
1929 	writeln();
1930 	writeln("Common options");
1931 	writeln("==============");
1932 	writeln();
1933 	writeOptions(common_args);
1934 	writeln();
1935 	writefln("DUB version %s, built on %s", getDUBVersion(), __DATE__);
1936 }
1937 
1938 private void writeOptions(CommandArgs args)
1939 {
1940 	foreach (arg; args.recognizedArgs) {
1941 		auto names = arg.names.split("|");
1942 		assert(names.length == 1 || names.length == 2);
1943 		string sarg = names[0].length == 1 ? names[0] : null;
1944 		string larg = names[0].length > 1 ? names[0] : names.length > 1 ? names[1] : null;
1945 		if (sarg !is null) {
1946 			writeWS(shortArgColumn);
1947 			writef("-%s", sarg);
1948 			writeWS(longArgColumn - shortArgColumn - 2);
1949 		} else writeWS(longArgColumn);
1950 		size_t col = longArgColumn;
1951 		if (larg !is null) {
1952 			if (arg.defaultValue.peek!bool) {
1953 				writef("--%s", larg);
1954 				col += larg.length + 2;
1955 			} else {
1956 				writef("--%s=VALUE", larg);
1957 				col += larg.length + 8;
1958 			}
1959 		}
1960 		if (col < descColumn) {
1961 			writeWS(descColumn - col);
1962 		} else {
1963 			writeln();
1964 			writeWS(descColumn);
1965 		}
1966 		foreach (i, ln; arg.helpText) {
1967 			if (i > 0) writeWS(descColumn);
1968 			ln.writeWrapped(descColumn, descColumn);
1969 		}
1970 	}
1971 }
1972 
1973 private void writeWrapped(string string, size_t indent = 0, size_t first_line_pos = 0)
1974 {
1975 	// handle pre-indented strings and bullet lists
1976 	size_t first_line_indent = 0;
1977 	while (string.startsWith(" ")) {
1978 		string = string[1 .. $];
1979 		indent++;
1980 		first_line_indent++;
1981 	}
1982 	if (string.startsWith("- ")) indent += 2;
1983 
1984 	auto wrapped = string.wrap(lineWidth, getRepString!' '(first_line_pos+first_line_indent), getRepString!' '(indent));
1985 	wrapped = wrapped[first_line_pos .. $];
1986 	foreach (ln; wrapped.splitLines())
1987 		writeln(ln);
1988 }
1989 
1990 private void writeWS(size_t num) { writerep!' '(num); }
1991 private void writerep(char ch)(size_t num) { write(getRepString!ch(num)); }
1992 
1993 private string getRepString(char ch)(size_t len)
1994 {
1995 	static string buf;
1996 	if (len > buf.length) buf ~= [ch].replicate(len-buf.length);
1997 	return buf[0 .. len];
1998 }
1999 
2000 /***
2001 */
2002 
2003 
2004 private void enforceUsage(bool cond, string text)
2005 {
2006 	if (!cond) throw new UsageException(text);
2007 }
2008 
2009 private class UsageException : Exception {
2010 	this(string message, string file = __FILE__, int line = __LINE__, Throwable next = null)
2011 	{
2012 		super(message, file, line, next);
2013 	}
2014 }
2015 
2016 private void warnRenamed(string prev, string curr)
2017 {
2018 	logWarn("The '%s' Command was renamed to '%s'. Please update your scripts.", prev, curr);
2019 }