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