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.data.json;
16 import dub.internal.vibecompat.inet.path;
17 import dub.internal.logging;
18 import dub.package_;
19 import dub.packagemanager;
20 import dub.packagesuppliers;
21 import dub.project;
22 import dub.internal.utils : getDUBVersion, getClosestMatch, getTempFile;
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.path : expandTilde, absolutePath, buildNormalizedPath;
32 import std.process;
33 import std.stdio;
34 import std.string;
35 import std.sumtype;
36 import std.typecons : Tuple, tuple;
37 import std.variant;
38 import std.path: setExtension;
39 
40 /** Retrieves a list of all available commands.
41 
42 	Commands are grouped by category.
43 */
44 CommandGroup[] getCommands() @safe pure nothrow
45 {
46 	return [
47 		CommandGroup("Package creation",
48 			new InitCommand
49 		),
50 		CommandGroup("Build, test and run",
51 			new RunCommand,
52 			new BuildCommand,
53 			new TestCommand,
54 			new LintCommand,
55 			new GenerateCommand,
56 			new DescribeCommand,
57 			new CleanCommand,
58 			new DustmiteCommand
59 		),
60 		CommandGroup("Package management",
61 			new FetchCommand,
62 			new AddCommand,
63 			new RemoveCommand,
64 			new UpgradeCommand,
65 			new AddPathCommand,
66 			new RemovePathCommand,
67 			new AddLocalCommand,
68 			new RemoveLocalCommand,
69 			new ListCommand,
70 			new SearchCommand,
71 			new AddOverrideCommand,
72 			new RemoveOverrideCommand,
73 			new ListOverridesCommand,
74 			new CleanCachesCommand,
75 			new ConvertCommand,
76 		)
77 	];
78 }
79 
80 /** Extract the command name from the argument list
81 
82 	Params:
83 		args = a list of string arguments that will be processed
84 
85 	Returns:
86 		A structure with two members. `value` is the command name
87 		`remaining` is a list of unprocessed arguments
88 */
89 auto extractCommandNameArgument(string[] args)
90 {
91 	struct Result {
92 		string value;
93 		string[] remaining;
94 	}
95 
96 	if (args.length >= 1 && !args[0].startsWith("-") && !args[0].canFind(":")) {
97 		return Result(args[0], args[1 .. $]);
98 	}
99 
100 	return Result(null, args);
101 }
102 
103 /// test extractCommandNameArgument usage
104 unittest {
105 	/// It returns an empty string on when there are no args
106 	assert(extractCommandNameArgument([]).value == "");
107 	assert(extractCommandNameArgument([]).remaining == []);
108 
109 	/// It returns the first argument when it does not start with `-`
110 	assert(extractCommandNameArgument(["test"]).value == "test");
111 
112 	/// There is nothing to extract when the arguments only contain the `test` cmd
113 	assert(extractCommandNameArgument(["test"]).remaining == []);
114 
115 	/// It extracts two arguments when they are not a command
116 	assert(extractCommandNameArgument(["-a", "-b"]).remaining == ["-a", "-b"]);
117 
118 	/// It returns the an empty string when it starts with `-`
119 	assert(extractCommandNameArgument(["-test"]).value == "");
120 
121 	// Sub package names are ignored as command names
122 	assert(extractCommandNameArgument(["foo:bar"]).value == "");
123 	assert(extractCommandNameArgument([":foo"]).value == "");
124 }
125 
126 /** Handles the Command Line options and commands.
127 */
128 struct CommandLineHandler
129 {
130 	/// The list of commands that can be handled
131 	CommandGroup[] commandGroups;
132 
133 	/// General options parser
134 	CommonOptions options;
135 
136 	/** Create the list of all supported commands
137 
138 	Returns:
139 		Returns the list of the supported command names
140 	*/
141 	string[] commandNames()
142 	{
143 		return commandGroups.map!(g => g.commands.map!(c => c.name).array).join;
144 	}
145 
146 	/** Parses the general options and sets up the log level
147 		and the root_path
148 	*/
149 	void prepareOptions(CommandArgs args) {
150 		LogLevel loglevel = LogLevel.info;
151 
152 		options.prepare(args);
153 
154 		if (options.vverbose) loglevel = LogLevel.debug_;
155 		else if (options.verbose) loglevel = LogLevel.diagnostic;
156 		else if (options.vquiet) loglevel = LogLevel.none;
157 		else if (options.quiet) loglevel = LogLevel.warn;
158 		else if (options.verror) loglevel = LogLevel.error;
159 		setLogLevel(loglevel);
160 
161 		if (options.root_path.empty)
162 		{
163 			options.root_path = getcwd();
164 		}
165 		else
166 		{
167 			options.root_path = options.root_path.expandTilde.absolutePath.buildNormalizedPath;
168 		}
169 
170 		final switch (options.color_mode) with (options.color)
171 		{
172 			case automatic:
173 				// Use default determined in internal.logging.initLogging().
174 				break;
175 			case on:
176 				foreach (ref grp; commandGroups)
177 					foreach (ref cmd; grp.commands)
178 						if (auto pc = cast(PackageBuildCommand)cmd)
179 							pc.baseSettings.buildSettings.options |= BuildOption.color;
180 				setLoggingColorsEnabled(true);  // enable colors, no matter what
181 				break;
182 			case off:
183 				foreach (ref grp; commandGroups)
184 					foreach (ref cmd; grp.commands)
185 						if (auto pc = cast(PackageBuildCommand)cmd)
186 							pc.baseSettings.buildSettings.options &= ~BuildOption.color;
187 				setLoggingColorsEnabled(false); // disable colors, no matter what
188 				break;
189 		}
190 	}
191 
192 	/** Get an instance of the requested command.
193 
194 	If there is no command in the argument list, the `run` command is returned
195 	by default.
196 
197 	If the `--help` argument previously handled by `prepareOptions`,
198 	`this.options.help` is already `true`, with this returning the requested
199 	command. If no command was requested (just dub --help) this returns the
200 	help command.
201 
202 	Params:
203 		name = the command name
204 
205 	Returns:
206 		Returns the command instance if it exists, null otherwise
207 	*/
208 	Command getCommand(string name) {
209 		if (name == "help" || (name == "" && options.help))
210 		{
211 			return new HelpCommand();
212 		}
213 
214 		if (name == "")
215 		{
216 			name = "run";
217 		}
218 
219 		foreach (grp; commandGroups)
220 			foreach (c; grp.commands)
221 				if (c.name == name) {
222 					return c;
223 				}
224 
225 		return null;
226 	}
227 
228 	/** Get an instance of the requested command after the args are sent.
229 
230 	It uses getCommand to get the command instance and then calls prepare.
231 
232 	Params:
233 		name = the command name
234 		args = the command arguments
235 
236 	Returns:
237 		Returns the command instance if it exists, null otherwise
238 	*/
239 	Command prepareCommand(string name, CommandArgs args) {
240 		auto cmd = getCommand(name);
241 
242 		if (cmd !is null && !(cast(HelpCommand)cmd))
243 		{
244 			// process command line options for the selected command
245 			cmd.prepare(args);
246 			enforceUsage(cmd.acceptsAppArgs || !args.hasAppArgs, name ~ " doesn't accept application arguments.");
247 		}
248 
249 		return cmd;
250 	}
251 }
252 
253 /// Can get the command names
254 unittest {
255 	CommandLineHandler handler;
256 	handler.commandGroups = getCommands();
257 
258 	assert(handler.commandNames == ["init", "run", "build", "test", "lint", "generate",
259 		"describe", "clean", "dustmite", "fetch", "add", "remove",
260 		"upgrade", "add-path", "remove-path", "add-local", "remove-local", "list", "search",
261 		"add-override", "remove-override", "list-overrides", "clean-caches", "convert"]);
262 }
263 
264 /// It sets the cwd as root_path by default
265 unittest {
266 	CommandLineHandler handler;
267 
268 	auto args = new CommandArgs([]);
269 	handler.prepareOptions(args);
270 	assert(handler.options.root_path == getcwd());
271 }
272 
273 /// It can set a custom root_path
274 unittest {
275 	CommandLineHandler handler;
276 
277 	auto args = new CommandArgs(["--root=/tmp/test"]);
278 	handler.prepareOptions(args);
279 	assert(handler.options.root_path == "/tmp/test".absolutePath.buildNormalizedPath);
280 
281 	args = new CommandArgs(["--root=./test"]);
282 	handler.prepareOptions(args);
283 	assert(handler.options.root_path == "./test".absolutePath.buildNormalizedPath);
284 }
285 
286 /// It sets the info log level by default
287 unittest {
288 	scope(exit) setLogLevel(LogLevel.info);
289 	CommandLineHandler handler;
290 
291 	auto args = new CommandArgs([]);
292 	handler.prepareOptions(args);
293 	assert(getLogLevel() == LogLevel.info);
294 }
295 
296 /// It can set a custom error level
297 unittest {
298 	scope(exit) setLogLevel(LogLevel.info);
299 	CommandLineHandler handler;
300 
301 	auto args = new CommandArgs(["--vverbose"]);
302 	handler.prepareOptions(args);
303 	assert(getLogLevel() == LogLevel.debug_);
304 
305 	handler = CommandLineHandler();
306 	args = new CommandArgs(["--verbose"]);
307 	handler.prepareOptions(args);
308 	assert(getLogLevel() == LogLevel.diagnostic);
309 
310 	handler = CommandLineHandler();
311 	args = new CommandArgs(["--vquiet"]);
312 	handler.prepareOptions(args);
313 	assert(getLogLevel() == LogLevel.none);
314 
315 	handler = CommandLineHandler();
316 	args = new CommandArgs(["--quiet"]);
317 	handler.prepareOptions(args);
318 	assert(getLogLevel() == LogLevel.warn);
319 
320 	handler = CommandLineHandler();
321 	args = new CommandArgs(["--verror"]);
322 	handler.prepareOptions(args);
323 	assert(getLogLevel() == LogLevel.error);
324 }
325 
326 /// It returns the `run` command by default
327 unittest {
328 	CommandLineHandler handler;
329 	handler.commandGroups = getCommands();
330 	assert(handler.getCommand("").name == "run");
331 }
332 
333 /// It returns the `help` command when there is none set and the --help arg
334 /// was set
335 unittest {
336 	CommandLineHandler handler;
337 	auto args = new CommandArgs(["--help"]);
338 	handler.prepareOptions(args);
339 	handler.commandGroups = getCommands();
340 	assert(cast(HelpCommand)handler.getCommand("") !is null);
341 }
342 
343 /// It returns the `help` command when the `help` command is sent
344 unittest {
345 	CommandLineHandler handler;
346 	handler.commandGroups = getCommands();
347 	assert(cast(HelpCommand) handler.getCommand("help") !is null);
348 }
349 
350 /// It returns the `init` command when the `init` command is sent
351 unittest {
352 	CommandLineHandler handler;
353 	handler.commandGroups = getCommands();
354 	assert(handler.getCommand("init").name == "init");
355 }
356 
357 /// It returns null when a missing command is sent
358 unittest {
359 	CommandLineHandler handler;
360 	handler.commandGroups = getCommands();
361 	assert(handler.getCommand("missing") is null);
362 }
363 
364 /** Processes the given command line and executes the appropriate actions.
365 
366 	Params:
367 		args = This command line argument array as received in `main`. The first
368 			entry is considered to be the name of the binary invoked.
369 
370 	Returns:
371 		Returns the exit code that is supposed to be returned to the system.
372 */
373 int runDubCommandLine(string[] args)
374 {
375 	static string[] toSinglePackageArgs (string args0, string file, string[] trailing)
376 	{
377 		return [args0, "run", "-q", "--temp-build", "--single", file, "--"] ~ trailing;
378 	}
379 
380 	// Initialize the logging module, ensure that whether stdout/stderr are a TTY
381 	// or not is detected in order to disable colors if the output isn't a console
382 	initLogging();
383 
384 	logDiagnostic("DUB version %s", getDUBVersion());
385 
386 	version(Windows){
387 		// rdmd uses $TEMP to compute a temporary path. since cygwin substitutes backslashes
388 		// with slashes, this causes OPTLINK to fail (it thinks path segments are options)
389 		// we substitute the other way around here to fix this.
390 		environment["TEMP"] = environment["TEMP"].replace("/", "\\");
391 	}
392 
393 	auto handler = CommandLineHandler(getCommands());
394 	auto commandNames = handler.commandNames();
395 
396 	// Special syntaxes need to be handled before regular argument parsing
397 	if (args.length >= 2)
398 	{
399 		// Read input source code from stdin
400 		if (args[1] == "-")
401 		{
402 			auto path = getTempFile("app", ".d");
403 			stdin.byChunk(4096).joiner.toFile(path.toNativeString());
404 			args = toSinglePackageArgs(args[0], path.toNativeString(), args[2 .. $]);
405 		}
406 
407 		// Dub has a shebang syntax to be able to use it as script, e.g.
408 		// #/usr/bin/env dub
409 		// With this approach, we need to support the file having
410 		// both the `.d` extension, or having none at all.
411 		// We also need to make sure arguments passed to the script
412 		// are passed to the program, not `dub`, e.g.:
413 		// ./my_dub_script foo bar
414 		// Gives us `args = [ "dub", "./my_dub_script" "foo", "bar" ]`,
415 		// which we need to interpret as:
416 		// `args = [ "dub", "./my_dub_script", "--", "foo", "bar" ]`
417 		else if (args[1].endsWith(".d"))
418 			args = toSinglePackageArgs(args[0], args[1], args[2 .. $]);
419 
420 		// Here we have a problem: What if the script name is a command name ?
421 		// We have to assume it isn't, and to reduce the risk of false positive
422 		// we only consider the case where the file name is the first argument,
423 		// as the shell invocation cannot be controlled.
424 		else if (!commandNames.canFind(args[1]) && !args[1].startsWith("-")) {
425 			if (exists(args[1])) {
426 				auto path = getTempFile("app", ".d");
427 				copy(args[1], path.toNativeString());
428 				args = toSinglePackageArgs(args[0], path.toNativeString(), args[2 .. $]);
429 			} else if (exists(args[1].setExtension(".d"))) {
430 				args = toSinglePackageArgs(args[0], args[1].setExtension(".d"), args[2 .. $]);
431 			}
432 		}
433 	}
434 
435 	auto common_args = new CommandArgs(args[1..$]);
436 
437 	try handler.prepareOptions(common_args);
438 	catch (Exception e) {
439 		logError("Error processing arguments: %s", e.msg);
440 		logDiagnostic("Full exception: %s", e.toString().sanitize);
441 		logInfo("Run 'dub help' for usage information.");
442 		return 1;
443 	}
444 
445 	if (handler.options.version_)
446 	{
447 		showVersion();
448 		return 0;
449 	}
450 
451 	// extract the command
452 	args = common_args.extractAllRemainingArgs();
453 
454 	auto command_name_argument = extractCommandNameArgument(args);
455 
456 	auto command_args = new CommandArgs(command_name_argument.remaining);
457 	Command cmd;
458 
459 	try {
460 		cmd = handler.prepareCommand(command_name_argument.value, command_args);
461 	} catch (Exception e) {
462 		logError("Error processing arguments: %s", e.msg);
463 		logDiagnostic("Full exception: %s", e.toString().sanitize);
464 		logInfo("Run 'dub help' for usage information.");
465 		return 1;
466 	}
467 
468 	if (cmd is null) {
469 		logError("Unknown command: %s", command_name_argument.value);
470 		writeln();
471 		showHelp(handler.commandGroups, common_args);
472 		return 1;
473 	}
474 
475 	if (cast(HelpCommand)cmd !is null) {
476 		showHelp(handler.commandGroups, common_args);
477 		return 0;
478 	}
479 
480 	if (handler.options.help) {
481 		showCommandHelp(cmd, command_args, common_args);
482 		return 0;
483 	}
484 
485 	auto remaining_args = command_args.extractRemainingArgs();
486 	if (remaining_args.any!(a => a.startsWith("-"))) {
487 		logError("Unknown command line flags: %s", remaining_args.filter!(a => a.startsWith("-")).array.join(" ").color(Mode.bold));
488 		logInfo(`Type "%s" to get a list of all supported flags.`, text("dub ", cmd.name, " -h").color(Mode.bold));
489 		return 1;
490 	}
491 
492 	// initialize the root package
493 	Dub dub = cmd.prepareDub(handler.options);
494 
495 	// execute the command
496 	try return cmd.execute(dub, remaining_args, command_args.appArgs);
497 	catch (UsageException e) {
498 		// usage exceptions get thrown before any logging, so we are
499 		// making the errors more narrow to better fit on small screens.
500 		tagWidth.push(5);
501 		logError("%s", e.msg);
502 		logDebug("Full exception: %s", e.toString().sanitize);
503 		logInfo(`Run "%s" for more information about the "%s" command.`,
504 			text("dub ", cmd.name, " -h").color(Mode.bold), cmd.name.color(Mode.bold));
505 		return 1;
506 	}
507 	catch (Exception e) {
508 		// most exceptions get thrown before logging, so same thing here as
509 		// above. However this might be subject to change if it results in
510 		// weird behavior anywhere.
511 		tagWidth.push(5);
512 		logError("%s", e.msg);
513 		logDebug("Full exception: %s", e.toString().sanitize);
514 		return 2;
515 	}
516 }
517 
518 
519 /** Contains and parses options common to all commands.
520 */
521 struct CommonOptions {
522 	bool verbose, vverbose, quiet, vquiet, verror, version_;
523 	bool help, annotate, bare;
524 	string[] registry_urls;
525 	string root_path;
526 	enum color { automatic, on, off } // Lower case "color" in support of invalid option error formatting.
527 	color color_mode = color.automatic;
528 	SkipPackageSuppliers skipRegistry = SkipPackageSuppliers.none;
529 	PlacementLocation placementLocation = PlacementLocation.user;
530 
531 	/// Parses all common options and stores the result in the struct instance.
532 	void prepare(CommandArgs args)
533 	{
534 		args.getopt("h|help", &help, ["Display general or command specific help"]);
535 		args.getopt("root", &root_path, ["Path to operate in instead of the current working dir"]);
536 		args.getopt("registry", &registry_urls, [
537 			"Search the given registry URL first when resolving dependencies. Can be specified multiple times. Available registry types:",
538 			"  DUB: URL to DUB registry (default)",
539 			"  Maven: URL to Maven repository + group id containing dub packages as artifacts. E.g. mvn+http://localhost:8040/maven/libs-release/dubpackages",
540 			]);
541 		args.getopt("skip-registry", &skipRegistry, [
542 			"Sets a mode for skipping the search on certain package registry types:",
543 			"  none: Search all configured or default registries (default)",
544 			"  standard: Don't search the main registry (e.g. "~defaultRegistryURLs[0]~")",
545 			"  configured: Skip all default and user configured registries",
546 			"  all: Only search registries specified with --registry",
547 			]);
548 		args.getopt("annotate", &annotate, ["Do not perform any action, just print what would be done"]);
549 		args.getopt("bare", &bare, ["Read only packages contained in the current directory"]);
550 		args.getopt("v|verbose", &verbose, ["Print diagnostic output"]);
551 		args.getopt("vverbose", &vverbose, ["Print debug output"]);
552 		args.getopt("q|quiet", &quiet, ["Only print warnings and errors"]);
553 		args.getopt("verror", &verror, ["Only print errors"]);
554 		args.getopt("vquiet", &vquiet, ["Print no messages"]);
555 		args.getopt("color", &color_mode, [
556 			"Configure colored output. Accepted values:",
557 			"  automatic: Colored output on console/terminal,",
558 			"             unless NO_COLOR is set and non-empty (default)",
559 			"         on: Force colors enabled",
560 			"        off: Force colors disabled"
561 			]);
562 		args.getopt("cache", &placementLocation, ["Puts any fetched packages in the specified location [local|system|user]."]);
563 
564 		version_ = args.hasAppVersion;
565 	}
566 }
567 
568 /** Encapsulates a set of application arguments.
569 
570 	This class serves two purposes. The first is to provide an API for parsing
571 	command line arguments (`getopt`). At the same time it records all calls
572 	to `getopt` and provides a list of all possible options using the
573 	`recognizedArgs` property.
574 */
575 class CommandArgs {
576 	struct Arg {
577 		Variant defaultValue;
578 		Variant value;
579 		string names;
580 		string[] helpText;
581 		bool hidden;
582 	}
583 	private {
584 		string[] m_args;
585 		Arg[] m_recognizedArgs;
586 		string[] m_appArgs;
587 	}
588 
589 	/** Initializes the list of source arguments.
590 
591 		Note that all array entries are considered application arguments (i.e.
592 		no application name entry is present as the first entry)
593 	*/
594 	this(string[] args) @safe pure nothrow
595 	{
596 		auto app_args_idx = args.countUntil("--");
597 
598 		m_appArgs = app_args_idx >= 0 ? args[app_args_idx+1 .. $] : [];
599 		m_args = "dummy" ~ (app_args_idx >= 0 ? args[0..app_args_idx] : args);
600 	}
601 
602 	/** Checks if the app arguments are present.
603 
604 	Returns:
605 		true if an -- argument is given with arguments after it, otherwise false
606 	*/
607 	@property bool hasAppArgs() { return m_appArgs.length > 0; }
608 
609 
610 	/** Checks if the `--version` argument is present on the first position in
611 	the list.
612 
613 	Returns:
614 		true if the application version argument was found on the first position
615 	*/
616 	@property bool hasAppVersion() { return m_args.length > 1 && m_args[1] == "--version"; }
617 
618 	/** Returns the list of app args.
619 
620 		The app args are provided after the `--` argument.
621 	*/
622 	@property string[] appArgs() { return m_appArgs; }
623 
624 	/** Returns the list of all options recognized.
625 
626 		This list is created by recording all calls to `getopt`.
627 	*/
628 	@property const(Arg)[] recognizedArgs() { return m_recognizedArgs; }
629 
630 	void getopt(T)(string names, T* var, string[] help_text = null, bool hidden=false)
631 	{
632 		foreach (ref arg; m_recognizedArgs)
633 			if (names == arg.names) {
634 				assert(help_text is null, format!("Duplicated argument '%s' must not change helptext, consider to remove the duplication")(names));
635 				*var = arg.value.get!T;
636 				return;
637 			}
638 		assert(help_text.length > 0);
639 		Arg arg;
640 		arg.defaultValue = *var;
641 		arg.names = names;
642 		arg.helpText = help_text;
643 		arg.hidden = hidden;
644 		m_args.getopt(config.passThrough, names, var);
645 		arg.value = *var;
646 		m_recognizedArgs ~= arg;
647 	}
648 
649 	/** Resets the list of available source arguments.
650 	*/
651 	void dropAllArgs()
652 	{
653 		m_args = null;
654 	}
655 
656 	/** Returns the list of unprocessed arguments, ignoring the app arguments,
657 	and resets the list of available source arguments.
658 	*/
659 	string[] extractRemainingArgs()
660 	{
661 		assert(m_args !is null, "extractRemainingArgs must be called only once.");
662 
663 		auto ret = m_args[1 .. $];
664 		m_args = null;
665 		return ret;
666 	}
667 
668 	/** Returns the list of unprocessed arguments, including the app arguments
669 		and resets the list of available source arguments.
670 	*/
671 	string[] extractAllRemainingArgs()
672 	{
673 		auto ret = extractRemainingArgs();
674 
675 		if (this.hasAppArgs)
676 		{
677 			ret ~= "--" ~ m_appArgs;
678 		}
679 
680 		return ret;
681 	}
682 }
683 
684 /// Using CommandArgs
685 unittest {
686 	/// It should not find the app version for an empty arg list
687 	assert(new CommandArgs([]).hasAppVersion == false);
688 
689 	/// It should find the app version when `--version` is the first arg
690 	assert(new CommandArgs(["--version"]).hasAppVersion == true);
691 
692 	/// It should not find the app version when `--version` is the second arg
693 	assert(new CommandArgs(["a", "--version"]).hasAppVersion == false);
694 
695 	/// It returns an empty app arg list when `--` arg is missing
696 	assert(new CommandArgs(["1", "2"]).appArgs == []);
697 
698 	/// It returns an empty app arg list when `--` arg is missing
699 	assert(new CommandArgs(["1", "2"]).appArgs == []);
700 
701 	/// It returns app args set after "--"
702 	assert(new CommandArgs(["1", "2", "--", "a"]).appArgs == ["a"]);
703 	assert(new CommandArgs(["1", "2", "--"]).appArgs == []);
704 	assert(new CommandArgs(["--"]).appArgs == []);
705 	assert(new CommandArgs(["--", "a"]).appArgs == ["a"]);
706 
707 	/// It returns the list of all args when no args are processed
708 	assert(new CommandArgs(["1", "2", "--", "a"]).extractAllRemainingArgs == ["1", "2", "--", "a"]);
709 }
710 
711 /// It removes the extracted args
712 unittest {
713 	auto args = new CommandArgs(["-a", "-b", "--", "-c"]);
714 	bool value;
715 	args.getopt("b", &value, [""]);
716 
717 	assert(args.extractAllRemainingArgs == ["-a", "--", "-c"]);
718 }
719 
720 /// It should not be able to remove app args
721 unittest {
722 	auto args = new CommandArgs(["-a", "-b", "--", "-c"]);
723 	bool value;
724 	args.getopt("-c", &value, [""]);
725 
726 	assert(!value);
727 	assert(args.extractAllRemainingArgs == ["-a", "-b", "--", "-c"]);
728 }
729 
730 /** Base class for all commands.
731 
732 	This cass contains a high-level description of the command, including brief
733 	and full descriptions and a human readable command line pattern. On top of
734 	that it defines the two main entry functions for command execution.
735 */
736 class Command {
737 	string name;
738 	string argumentsPattern;
739 	string description;
740 	string[] helpText;
741 	bool acceptsAppArgs;
742 	bool hidden = false; // used for deprecated commands
743 
744 	/** Parses all known command line options without executing any actions.
745 
746 		This function will be called prior to execute, or may be called as
747 		the only method when collecting the list of recognized command line
748 		options.
749 
750 		Only `args.getopt` should be called within this method.
751 	*/
752 	abstract void prepare(scope CommandArgs args);
753 
754 	/**
755 	 * Initialize the dub instance used by `execute`
756 	 */
757 	public Dub prepareDub(CommonOptions options) {
758 		Dub dub;
759 
760 		if (options.bare) {
761 			dub = new Dub(NativePath(options.root_path), NativePath(getcwd()));
762 			dub.defaultPlacementLocation = options.placementLocation;
763 
764 			return dub;
765 		}
766 
767 		// initialize DUB
768 		auto package_suppliers = options.registry_urls
769 			.map!((url) {
770 				// Allow to specify fallback mirrors as space separated urls. Undocumented as we
771 				// should simply retry over all registries instead of using a special
772 				// FallbackPackageSupplier.
773 				auto urls = url.splitter(' ');
774 				PackageSupplier ps = getRegistryPackageSupplier(urls.front);
775 				urls.popFront;
776 				if (!urls.empty)
777 					ps = new FallbackPackageSupplier(ps ~ urls.map!getRegistryPackageSupplier.array);
778 				return ps;
779 			})
780 			.array;
781 
782 		dub = new Dub(options.root_path, package_suppliers, options.skipRegistry);
783 		dub.dryRun = options.annotate;
784 		dub.defaultPlacementLocation = options.placementLocation;
785 
786 		// make the CWD package available so that for example sub packages can reference their
787 		// parent package.
788 		try dub.packageManager.getOrLoadPackage(NativePath(options.root_path), NativePath.init, false, StrictMode.Warn);
789 		catch (Exception e) { logDiagnostic("No valid package found in current working directory: %s", e.msg); }
790 
791 		return dub;
792 	}
793 
794 	/** Executes the actual action.
795 
796 		Note that `prepare` will be called before any call to `execute`.
797 	*/
798 	abstract int execute(Dub dub, string[] free_args, string[] app_args);
799 
800 	private bool loadCwdPackage(Dub dub, bool warn_missing_package)
801 	{
802 		auto filePath = Package.findPackageFile(dub.rootPath);
803 
804 		if (filePath.empty) {
805 			if (warn_missing_package) {
806 				logInfoNoTag("");
807 				logInfoNoTag("No package manifest (dub.json or dub.sdl) was found in");
808 				logInfoNoTag(dub.rootPath.toNativeString());
809 				logInfoNoTag("Please run DUB from the root directory of an existing package, or run");
810 				logInfoNoTag("\"%s\" to get information on creating a new package.", "dub init --help".color(Mode.bold));
811 				logInfoNoTag("");
812 			}
813 			return false;
814 		}
815 
816 		dub.loadPackage();
817 
818 		return true;
819 	}
820 }
821 
822 
823 /** Encapsulates a group of commands that fit into a common category.
824 */
825 struct CommandGroup {
826 	/// Caption of the command category
827 	string caption;
828 
829 	/// List of commands contained inthis group
830 	Command[] commands;
831 
832 	this(string caption, Command[] commands...) @safe pure nothrow
833 	{
834 		this.caption = caption;
835 		this.commands = commands.dup;
836 	}
837 }
838 
839 /******************************************************************************/
840 /* HELP                                                                       */
841 /******************************************************************************/
842 
843 class HelpCommand : Command {
844 
845 	this() @safe pure nothrow
846 	{
847 		this.name = "help";
848 		this.description = "Shows the help message";
849 		this.helpText = [
850 			"Shows the help message and the supported command options."
851 		];
852 	}
853 
854 	/// HelpCommand.prepare is not supposed to be called, use
855 	/// cast(HelpCommand)this to check if help was requested before execution.
856 	override void prepare(scope CommandArgs args)
857 	{
858 		assert(false, "HelpCommand.prepare is not supposed to be called, use cast(HelpCommand)this to check if help was requested before execution.");
859 	}
860 
861 	/// HelpCommand.execute is not supposed to be called, use
862 	/// cast(HelpCommand)this to check if help was requested before execution.
863 	override int execute(Dub dub, string[] free_args, string[] app_args) {
864 		assert(false, "HelpCommand.execute is not supposed to be called, use cast(HelpCommand)this to check if help was requested before execution.");
865 	}
866 }
867 
868 /******************************************************************************/
869 /* INIT                                                                       */
870 /******************************************************************************/
871 
872 class InitCommand : Command {
873 	private{
874 		string m_templateType = "minimal";
875 		PackageFormat m_format = PackageFormat.json;
876 		bool m_nonInteractive;
877 	}
878 	this() @safe pure nothrow
879 	{
880 		this.name = "init";
881 		this.argumentsPattern = "[<directory> [<dependency>...]]";
882 		this.description = "Initializes an empty package skeleton";
883 		this.helpText = [
884 			"Initializes an empty package of the specified type in the given directory. By default, the current working directory is used."
885 		];
886 		this.acceptsAppArgs = true;
887 	}
888 
889 	override void prepare(scope CommandArgs args)
890 	{
891 		args.getopt("t|type", &m_templateType, [
892 			"Set the type of project to generate. Available types:",
893 			"",
894 			"minimal - simple \"hello world\" project (default)",
895 			"vibe.d  - minimal HTTP server based on vibe.d",
896 			"deimos  - skeleton for C header bindings",
897 			"custom  - custom project provided by dub package",
898 		]);
899 		args.getopt("f|format", &m_format, [
900 			"Sets the format to use for the package description file. Possible values:",
901 			"  " ~ [__traits(allMembers, PackageFormat)].map!(f => f == m_format.init.to!string ? f ~ " (default)" : f).join(", ")
902 		]);
903 		args.getopt("n|non-interactive", &m_nonInteractive, ["Don't enter interactive mode."]);
904 	}
905 
906 	override int execute(Dub dub, string[] free_args, string[] app_args)
907 	{
908 		string dir;
909 		if (free_args.length)
910 		{
911 			dir = free_args[0];
912 			free_args = free_args[1 .. $];
913 		}
914 
915 		static string input(string caption, string default_value)
916 		{
917 			writef("%s [%s]: ", caption, default_value);
918 			stdout.flush();
919 			auto inp = readln();
920 			return inp.length > 1 ? inp[0 .. $-1] : default_value;
921 		}
922 
923 		void depCallback(ref PackageRecipe p, ref PackageFormat fmt) {
924 			import std.datetime: Clock;
925 
926 			if (m_nonInteractive) return;
927 
928 			while (true) {
929 				string rawfmt = input("Package recipe format (sdl/json)", fmt.to!string);
930 				if (!rawfmt.length) break;
931 				try {
932 					fmt = rawfmt.to!PackageFormat;
933 					break;
934 				} catch (Exception) {
935 					logError(`Invalid format '%s', enter either 'sdl' or 'json'.`, rawfmt);
936 				}
937 			}
938 			auto author = p.authors.join(", ");
939 			while (true) {
940 				// Tries getting the name until a valid one is given.
941 				import std.regex;
942 				auto nameRegex = regex(`^[a-z0-9\-_]+$`);
943 				string triedName = input("Name", p.name);
944 				if (triedName.matchFirst(nameRegex).empty) {
945 					logError(`Invalid name '%s', names should consist only of lowercase alphanumeric characters, dashes ('-') and underscores ('_').`, triedName);
946 				} else {
947 					p.name = triedName;
948 					break;
949 				}
950 			}
951 			p.description = input("Description", p.description);
952 			p.authors = input("Author name", author).split(",").map!(a => a.strip).array;
953 			p.license = input("License", p.license);
954 			string copyrightString = .format("Copyright © %s, %-(%s, %)", Clock.currTime().year, p.authors);
955 			p.copyright = input("Copyright string", copyrightString);
956 
957 			while (true) {
958 				auto depspec = input("Add dependency (leave empty to skip)", null);
959 				if (!depspec.length) break;
960 				addDependency(dub, p, depspec);
961 			}
962 		}
963 
964 		if (!["vibe.d", "deimos", "minimal"].canFind(m_templateType))
965 		{
966 			free_args ~= m_templateType;
967 		}
968 		dub.createEmptyPackage(NativePath(dir), free_args, m_templateType, m_format, &depCallback, app_args);
969 
970 		logInfo("Package successfully created in %s", dir.length ? dir : ".");
971 		return 0;
972 	}
973 }
974 
975 
976 /******************************************************************************/
977 /* GENERATE / BUILD / RUN / TEST / DESCRIBE                                   */
978 /******************************************************************************/
979 
980 abstract class PackageBuildCommand : Command {
981 	protected {
982 		string m_compilerName;
983 		string m_arch;
984 		string[] m_debugVersions;
985 		string[] m_overrideConfigs;
986 		GeneratorSettings baseSettings;
987 		string m_defaultConfig;
988 		bool m_nodeps;
989 		bool m_forceRemove = false;
990 	}
991 
992 	override void prepare(scope CommandArgs args)
993 	{
994 		args.getopt("b|build", &this.baseSettings.buildType, [
995 			"Specifies the type of build to perform. Note that setting the DFLAGS environment variable will override the build type with custom flags.",
996 			"Possible names:",
997 			"  "~builtinBuildTypes.join(", ")~" and custom types"
998 		]);
999 		args.getopt("c|config", &this.baseSettings.config, [
1000 			"Builds the specified configuration. Configurations can be defined in dub.json"
1001 		]);
1002 		args.getopt("override-config", &m_overrideConfigs, [
1003 			"Uses the specified configuration for a certain dependency. Can be specified multiple times.",
1004 			"Format: --override-config=<dependency>/<config>"
1005 		]);
1006 		args.getopt("compiler", &m_compilerName, [
1007 			"Specifies the compiler binary to use (can be a path).",
1008 			"Arbitrary pre- and suffixes to the identifiers below are recognized (e.g. ldc2 or dmd-2.063) and matched to the proper compiler type:",
1009 			"  "~["dmd", "gdc", "ldc", "gdmd", "ldmd"].join(", ")
1010 		]);
1011 		args.getopt("a|arch", &m_arch, [
1012 			"Force a different architecture (e.g. x86 or x86_64)"
1013 		]);
1014 		args.getopt("d|debug", &m_debugVersions, [
1015 			"Define the specified debug version identifier when building - can be used multiple times"
1016 		]);
1017 		args.getopt("nodeps", &m_nodeps, [
1018 			"Do not resolve missing dependencies before building"
1019 		]);
1020 		args.getopt("build-mode", &this.baseSettings.buildMode, [
1021 			"Specifies the way the compiler and linker are invoked. Valid values:",
1022 			"  separate (default), allAtOnce, singleFile"
1023 		]);
1024 		args.getopt("single", &this.baseSettings.single, [
1025 			"Treats the package name as a filename. The file must contain a package recipe comment."
1026 		]);
1027 		args.getopt("force-remove", &m_forceRemove, [
1028 			"Deprecated option that does nothing."
1029 		]);
1030 		args.getopt("filter-versions", &this.baseSettings.filterVersions, [
1031 			"[Experimental] Filter version identifiers and debug version identifiers to improve build cache efficiency."
1032 		]);
1033 	}
1034 
1035 	protected void setupVersionPackage(Dub dub, string str_package_info, string default_build_type = "debug")
1036 	{
1037 		PackageAndVersion package_info = splitPackageName(str_package_info);
1038 		setupPackage(dub, package_info.name, default_build_type, package_info.version_);
1039 	}
1040 
1041 	protected void setupPackage(Dub dub, string package_name, string default_build_type = "debug", string ver = "")
1042 	{
1043 		if (!m_compilerName.length) m_compilerName = dub.defaultCompiler;
1044 		if (!m_arch.length) m_arch = dub.defaultArchitecture;
1045 		if (dub.defaultLowMemory) this.baseSettings.buildSettings.options |= BuildOption.lowmem;
1046 		if (dub.defaultEnvironments) this.baseSettings.buildSettings.addEnvironments(dub.defaultEnvironments);
1047 		if (dub.defaultBuildEnvironments) this.baseSettings.buildSettings.addBuildEnvironments(dub.defaultBuildEnvironments);
1048 		if (dub.defaultRunEnvironments) this.baseSettings.buildSettings.addRunEnvironments(dub.defaultRunEnvironments);
1049 		if (dub.defaultPreGenerateEnvironments) this.baseSettings.buildSettings.addPreGenerateEnvironments(dub.defaultPreGenerateEnvironments);
1050 		if (dub.defaultPostGenerateEnvironments) this.baseSettings.buildSettings.addPostGenerateEnvironments(dub.defaultPostGenerateEnvironments);
1051 		if (dub.defaultPreBuildEnvironments) this.baseSettings.buildSettings.addPreBuildEnvironments(dub.defaultPreBuildEnvironments);
1052 		if (dub.defaultPostBuildEnvironments) this.baseSettings.buildSettings.addPostBuildEnvironments(dub.defaultPostBuildEnvironments);
1053 		if (dub.defaultPreRunEnvironments) this.baseSettings.buildSettings.addPreRunEnvironments(dub.defaultPreRunEnvironments);
1054 		if (dub.defaultPostRunEnvironments) this.baseSettings.buildSettings.addPostRunEnvironments(dub.defaultPostRunEnvironments);
1055 		this.baseSettings.compiler = getCompiler(m_compilerName);
1056 		this.baseSettings.platform = this.baseSettings.compiler.determinePlatform(this.baseSettings.buildSettings, m_compilerName, m_arch);
1057 		this.baseSettings.buildSettings.addDebugVersions(m_debugVersions);
1058 
1059 		m_defaultConfig = null;
1060 		enforce (loadSpecificPackage(dub, package_name, ver), "Failed to load package.");
1061 
1062 		if (this.baseSettings.config.length != 0 &&
1063 			!dub.configurations.canFind(this.baseSettings.config) &&
1064 			this.baseSettings.config != "unittest")
1065 		{
1066 			string msg = "Unknown build configuration: " ~ this.baseSettings.config;
1067 			enum distance = 3;
1068 			auto match = dub.configurations.getClosestMatch(this.baseSettings.config, distance);
1069 			if (match !is null) msg ~= ". Did you mean '" ~ match ~ "'?";
1070 			enforce(0, msg);
1071 		}
1072 
1073 		if (this.baseSettings.buildType.length == 0) {
1074 			if (environment.get("DFLAGS") !is null) this.baseSettings.buildType = "$DFLAGS";
1075 			else this.baseSettings.buildType = default_build_type;
1076 		}
1077 
1078 		if (!m_nodeps) {
1079 			// retrieve missing packages
1080 			if (!dub.project.hasAllDependencies) {
1081 				logDiagnostic("Checking for missing dependencies.");
1082 				if (this.baseSettings.single)
1083 					dub.upgrade(UpgradeOptions.select | UpgradeOptions.noSaveSelections);
1084 				else dub.upgrade(UpgradeOptions.select);
1085 			}
1086 		}
1087 
1088 		dub.project.validate();
1089 
1090 		foreach (sc; m_overrideConfigs) {
1091 			auto idx = sc.indexOf('/');
1092 			enforceUsage(idx >= 0, "Expected \"<package>/<configuration>\" as argument to --override-config.");
1093 			dub.project.overrideConfiguration(sc[0 .. idx], sc[idx+1 .. $]);
1094 		}
1095 	}
1096 
1097 	private bool loadSpecificPackage(Dub dub, string package_name, string ver)
1098 	{
1099 		if (this.baseSettings.single) {
1100 			enforce(package_name.length, "Missing file name of single-file package.");
1101 			dub.loadSingleFilePackage(package_name);
1102 			return true;
1103 		}
1104 
1105 		bool from_cwd = package_name.length == 0 || package_name.startsWith(":");
1106 		// load package in root_path to enable searching for sub packages
1107 		if (loadCwdPackage(dub, from_cwd)) {
1108 			if (package_name.startsWith(":"))
1109 			{
1110 				auto pack = dub.packageManager.getSubPackage(dub.project.rootPackage, package_name[1 .. $], false);
1111 				dub.loadPackage(pack);
1112 				return true;
1113 			}
1114 			if (from_cwd) return true;
1115 		}
1116 
1117 		enforce(package_name.length, "No valid root package found - aborting.");
1118 
1119 		const vers = ver.length ? VersionRange.fromString(ver) : VersionRange.Any;
1120 		auto pack = dub.packageManager.getBestPackage(package_name, vers);
1121 
1122 		enforce(pack, format!"Failed to find a package named '%s%s' locally."(package_name,
1123 			ver == "" ? "" : ("@" ~ ver)
1124 		));
1125 		logInfo("Building package %s in %s", pack.name, pack.path.toNativeString());
1126 		dub.loadPackage(pack);
1127 		return true;
1128 	}
1129 }
1130 
1131 class GenerateCommand : PackageBuildCommand {
1132 	protected {
1133 		string m_generator;
1134 		bool m_printPlatform, m_printBuilds, m_printConfigs;
1135 	}
1136 
1137 	this() @safe pure nothrow
1138 	{
1139 		this.name = "generate";
1140 		this.argumentsPattern = "<generator> [<package>[@<version-spec>]]";
1141 		this.description = "Generates project files using the specified generator";
1142 		this.helpText = [
1143 			"Generates project files using one of the supported generators:",
1144 			"",
1145 			"visuald - VisualD project files",
1146 			"sublimetext - SublimeText project file",
1147 			"cmake - CMake build scripts",
1148 			"build - Builds the package directly",
1149 			"",
1150 			"An optional package name can be given to generate a different package than the root/CWD package."
1151 		];
1152 	}
1153 
1154 	override void prepare(scope CommandArgs args)
1155 	{
1156 		super.prepare(args);
1157 
1158 		args.getopt("combined", &this.baseSettings.combined, [
1159 			"Tries to build the whole project in a single compiler run."
1160 		]);
1161 
1162 		args.getopt("print-builds", &m_printBuilds, [
1163 			"Prints the list of available build types"
1164 		]);
1165 		args.getopt("print-configs", &m_printConfigs, [
1166 			"Prints the list of available configurations"
1167 		]);
1168 		args.getopt("print-platform", &m_printPlatform, [
1169 			"Prints the identifiers for the current build platform as used for the build fields in dub.json"
1170 		]);
1171 		args.getopt("parallel", &this.baseSettings.parallelBuild, [
1172 			"Runs multiple compiler instances in parallel, if possible."
1173 		]);
1174 	}
1175 
1176 	override int execute(Dub dub, string[] free_args, string[] app_args)
1177 	{
1178 		string str_package_info;
1179 		if (!m_generator.length) {
1180 			enforceUsage(free_args.length >= 1 && free_args.length <= 2, "Expected one or two arguments.");
1181 			m_generator = free_args[0];
1182 			if (free_args.length >= 2) str_package_info = free_args[1];
1183 		} else {
1184 			enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1185 			if (free_args.length >= 1) str_package_info = free_args[0];
1186 		}
1187 
1188 		setupVersionPackage(dub, str_package_info, "debug");
1189 
1190 		if (m_printBuilds) {
1191 			logInfo("Available build types:");
1192 			foreach (i, tp; dub.project.builds)
1193 				logInfo("  %s%s", tp, i == 0 ? " [default]" : null);
1194 			logInfo("");
1195 		}
1196 
1197 		m_defaultConfig = dub.project.getDefaultConfiguration(this.baseSettings.platform);
1198 		if (m_printConfigs) {
1199 			logInfo("Available configurations:");
1200 			foreach (tp; dub.configurations)
1201 				logInfo("  %s%s", tp, tp == m_defaultConfig ? " [default]" : null);
1202 			logInfo("");
1203 		}
1204 
1205 		GeneratorSettings gensettings = this.baseSettings;
1206 		if (!gensettings.config.length)
1207 			gensettings.config = m_defaultConfig;
1208 		gensettings.runArgs = app_args;
1209 
1210 		logDiagnostic("Generating using %s", m_generator);
1211 		dub.generateProject(m_generator, gensettings);
1212 		if (this.baseSettings.buildType == "ddox") dub.runDdox(gensettings.run, app_args);
1213 		return 0;
1214 	}
1215 }
1216 
1217 class BuildCommand : GenerateCommand {
1218 	protected {
1219 		bool m_yes; // automatic yes to prompts;
1220 		bool m_nonInteractive;
1221 	}
1222 	this() @safe pure nothrow
1223 	{
1224 		this.name = "build";
1225 		this.argumentsPattern = "[<package>[@<version-spec>]]";
1226 		this.description = "Builds a package (uses the main package in the current working directory by default)";
1227 		this.helpText = [
1228 			"Builds a package (uses the main package in the current working directory by default)"
1229 		];
1230 	}
1231 
1232 	override void prepare(scope CommandArgs args)
1233 	{
1234 		args.getopt("temp-build", &this.baseSettings.tempBuild, [
1235 			"Builds the project in the temp folder if possible."
1236 		]);
1237 
1238 		args.getopt("rdmd", &this.baseSettings.rdmd, [
1239 			"Use rdmd instead of directly invoking the compiler"
1240 		]);
1241 
1242 		args.getopt("f|force", &this.baseSettings.force, [
1243 			"Forces a recompilation even if the target is up to date"
1244 		]);
1245 		args.getopt("y|yes", &m_yes, [
1246 			`Automatic yes to prompts. Assume "yes" as answer to all interactive prompts.`
1247 		]);
1248 		args.getopt("n|non-interactive", &m_nonInteractive, [
1249 			"Don't enter interactive mode."
1250 		]);
1251 		super.prepare(args);
1252 		m_generator = "build";
1253 	}
1254 
1255 	override int execute(Dub dub, string[] free_args, string[] app_args)
1256 	{
1257 		// single package files don't need to be downloaded, they are on the disk.
1258 		if (free_args.length < 1 || this.baseSettings.single)
1259 			return super.execute(dub, free_args, app_args);
1260 
1261 		if (!m_nonInteractive)
1262 		{
1263 			const packageParts = splitPackageName(free_args[0]);
1264 			if (auto rc = fetchMissingPackages(dub, packageParts))
1265 				return rc;
1266 		}
1267 		return super.execute(dub, free_args, app_args);
1268 	}
1269 
1270 	private int fetchMissingPackages(Dub dub, in PackageAndVersion packageParts)
1271 	{
1272 
1273 		static bool input(string caption, bool default_value = true) {
1274 			writef("%s [%s]: ", caption, default_value ? "Y/n" : "y/N");
1275 			auto inp = readln();
1276 			string userInput = "y";
1277 			if (inp.length > 1)
1278 				userInput = inp[0 .. $ - 1].toLower;
1279 
1280 			switch (userInput) {
1281 				case "no", "n", "0":
1282 					return false;
1283 				case "yes", "y", "1":
1284 				default:
1285 					return true;
1286 			}
1287 		}
1288 
1289 		VersionRange dep;
1290 
1291 		if (packageParts.version_.length > 0) {
1292 			// the user provided a version manually
1293 			dep = VersionRange.fromString(packageParts.version_);
1294 		} else if (packageParts.name.startsWith(":")) {
1295 			// Subpackages are always assumed to be present
1296 			return 0;
1297 		} else if (dub.packageManager.getBestPackage(packageParts.name)) {
1298 			// found locally
1299 			return 0;
1300 		} else {
1301 			// search for the package and filter versions for exact matches
1302 			auto basePackageName = getBasePackageName(packageParts.name);
1303 			auto search = dub.searchPackages(basePackageName)
1304 				.map!(tup => tup[1].find!(p => p.name == basePackageName))
1305 				.filter!(ps => !ps.empty);
1306 			if (search.empty) {
1307 				logWarn("Package '%s' was neither found locally nor online.", packageParts.name);
1308 				return 2;
1309 			}
1310 
1311 			const p = search.front.front;
1312 			logInfo("Package '%s' was not found locally but is available online:", packageParts.name);
1313 			logInfo("---");
1314 			logInfo("Description: %s", p.description);
1315 			logInfo("Version: %s", p.version_);
1316 			logInfo("---");
1317 
1318 			const answer = m_yes ? true : input("Do you want to fetch '%s' now?".format(packageParts.name));
1319 			if (!answer)
1320 				return 0;
1321 			dep = VersionRange.fromString(p.version_);
1322 		}
1323 
1324 		dub.fetch(packageParts.name, dep, dub.defaultPlacementLocation, FetchOptions.none);
1325 		return 0;
1326 	}
1327 }
1328 
1329 class RunCommand : BuildCommand {
1330 	this() @safe pure nothrow
1331 	{
1332 		this.name = "run";
1333 		this.argumentsPattern = "[<package>[@<version-spec>]]";
1334 		this.description = "Builds and runs a package (default command)";
1335 		this.helpText = [
1336 			"Builds and runs a package (uses the main package in the current working directory by default)"
1337 		];
1338 		this.acceptsAppArgs = true;
1339 	}
1340 
1341 	override void prepare(scope CommandArgs args)
1342 	{
1343 		super.prepare(args);
1344 		this.baseSettings.run = true;
1345 	}
1346 
1347 	override int execute(Dub dub, string[] free_args, string[] app_args)
1348 	{
1349 		return super.execute(dub, free_args, app_args);
1350 	}
1351 }
1352 
1353 class TestCommand : PackageBuildCommand {
1354 	private {
1355 		string m_mainFile;
1356 	}
1357 
1358 	this() @safe pure nothrow
1359 	{
1360 		this.name = "test";
1361 		this.argumentsPattern = "[<package>[@<version-spec>]]";
1362 		this.description = "Executes the tests of the selected package";
1363 		this.helpText = [
1364 			`Builds the package and executes all contained unit tests.`,
1365 			``,
1366 			`If no explicit configuration is given, an existing "unittest" ` ~
1367 			`configuration will be preferred for testing. If none exists, the ` ~
1368 			`first library type configuration will be used, and if that doesn't ` ~
1369 			`exist either, the first executable configuration is chosen.`,
1370 			``,
1371 			`When a custom main file (--main-file) is specified, only library ` ~
1372 			`configurations can be used. Otherwise, depending on the type of ` ~
1373 			`the selected configuration, either an existing main file will be ` ~
1374 			`used (and needs to be properly adjusted to just run the unit ` ~
1375 			`tests for 'version(unittest)'), or DUB will generate one for ` ~
1376 			`library type configurations.`,
1377 			``,
1378 			`Finally, if the package contains a dependency to the "tested" ` ~
1379 			`package, the automatically generated main file will use it to ` ~
1380 			`run the unit tests.`
1381 		];
1382 		this.acceptsAppArgs = true;
1383 	}
1384 
1385 	override void prepare(scope CommandArgs args)
1386 	{
1387 		args.getopt("temp-build", &this.baseSettings.tempBuild, [
1388 			"Builds the project in the temp folder if possible."
1389 		]);
1390 
1391 		args.getopt("main-file", &m_mainFile, [
1392 			"Specifies a custom file containing the main() function to use for running the tests."
1393 		]);
1394 		args.getopt("combined", &this.baseSettings.combined, [
1395 			"Tries to build the whole project in a single compiler run."
1396 		]);
1397 		args.getopt("parallel", &this.baseSettings.parallelBuild, [
1398 			"Runs multiple compiler instances in parallel, if possible."
1399 		]);
1400 		args.getopt("f|force", &this.baseSettings.force, [
1401 			"Forces a recompilation even if the target is up to date"
1402 		]);
1403 
1404 		bool coverage = false;
1405 		args.getopt("coverage", &coverage, [
1406 			"Enables code coverage statistics to be generated."
1407 		]);
1408 		if (coverage) this.baseSettings.buildType = "unittest-cov";
1409 
1410 		bool coverageCTFE = false;
1411 		args.getopt("coverage-ctfe", &coverageCTFE, [
1412 			"Enables code coverage (including CTFE) statistics to be generated."
1413 		]);
1414 		if (coverageCTFE) this.baseSettings.buildType = "unittest-cov-ctfe";
1415 
1416 		super.prepare(args);
1417 	}
1418 
1419 	override int execute(Dub dub, string[] free_args, string[] app_args)
1420 	{
1421 		string str_package_info;
1422 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1423 		if (free_args.length >= 1) str_package_info = free_args[0];
1424 
1425 		setupVersionPackage(dub, str_package_info, "unittest");
1426 
1427 		GeneratorSettings settings = this.baseSettings;
1428 		settings.compiler = getCompiler(this.baseSettings.platform.compilerBinary);
1429 		settings.run = true;
1430 		settings.runArgs = app_args;
1431 
1432 		dub.testProject(settings, this.baseSettings.config, NativePath(m_mainFile));
1433 		return 0;
1434 	}
1435 }
1436 
1437 class LintCommand : PackageBuildCommand {
1438 	private {
1439 		bool m_syntaxCheck = false;
1440 		bool m_styleCheck = false;
1441 		string m_errorFormat;
1442 		bool m_report = false;
1443 		string m_reportFormat;
1444 		string m_reportFile;
1445 		string[] m_importPaths;
1446 		string m_config;
1447 	}
1448 
1449 	this() @safe pure nothrow
1450 	{
1451 		this.name = "lint";
1452 		this.argumentsPattern = "[<package>[@<version-spec>]]";
1453 		this.description = "Executes the linter tests of the selected package";
1454 		this.helpText = [
1455 			`Builds the package and executes D-Scanner linter tests.`
1456 		];
1457 		this.acceptsAppArgs = true;
1458 	}
1459 
1460 	override void prepare(scope CommandArgs args)
1461 	{
1462 		args.getopt("syntax-check", &m_syntaxCheck, [
1463 			"Lexes and parses sourceFile, printing the line and column number of " ~
1464 			"any syntax errors to stdout."
1465 		]);
1466 
1467 		args.getopt("style-check", &m_styleCheck, [
1468 			"Lexes and parses sourceFiles, printing the line and column number of " ~
1469 			"any static analysis check failures stdout."
1470 		]);
1471 
1472 		args.getopt("error-format", &m_errorFormat, [
1473 			"Format errors produced by the style/syntax checkers."
1474 		]);
1475 
1476 		args.getopt("report", &m_report, [
1477 			"Generate a static analysis report in JSON format."
1478 		]);
1479 
1480 		args.getopt("report-format", &m_reportFormat, [
1481 			"Specifies the format of the generated report."
1482 		]);
1483 
1484 		args.getopt("report-file", &m_reportFile, [
1485 			"Write report to file."
1486 		]);
1487 
1488 		if (m_reportFormat || m_reportFile) m_report = true;
1489 
1490 		args.getopt("import-paths", &m_importPaths, [
1491 			"Import paths"
1492 		]);
1493 
1494 		args.getopt("dscanner-config", &m_config, [
1495 			"Use the given d-scanner configuration file."
1496 		]);
1497 
1498 		super.prepare(args);
1499 	}
1500 
1501 	override int execute(Dub dub, string[] free_args, string[] app_args)
1502 	{
1503 		string str_package_info;
1504 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1505 		if (free_args.length >= 1) str_package_info = free_args[0];
1506 
1507 		string[] args;
1508 		if (!m_syntaxCheck && !m_styleCheck && !m_report && app_args.length == 0) { m_styleCheck = true; }
1509 
1510 		if (m_syntaxCheck) args ~= "--syntaxCheck";
1511 		if (m_styleCheck) args ~= "--styleCheck";
1512 		if (m_errorFormat) args ~= ["--errorFormat", m_errorFormat];
1513 		if (m_report) args ~= "--report";
1514 		if (m_reportFormat) args ~= ["--reportFormat", m_reportFormat];
1515 		if (m_reportFile) args ~= ["--reportFile", m_reportFile];
1516 		foreach (import_path; m_importPaths) args ~= ["-I", import_path];
1517 		if (m_config) args ~= ["--config", m_config];
1518 
1519 		setupVersionPackage(dub, str_package_info);
1520 		dub.lintProject(args ~ app_args);
1521 		return 0;
1522 	}
1523 }
1524 
1525 class DescribeCommand : PackageBuildCommand {
1526 	private {
1527 		bool m_importPaths = false;
1528 		bool m_stringImportPaths = false;
1529 		bool m_dataList = false;
1530 		bool m_dataNullDelim = false;
1531 		string[] m_data;
1532 	}
1533 
1534 	this() @safe pure nothrow
1535 	{
1536 		this.name = "describe";
1537 		this.argumentsPattern = "[<package>[@<version-spec>]]";
1538 		this.description = "Prints a JSON description of the project and its dependencies";
1539 		this.helpText = [
1540 			"Prints a JSON build description for the root package an all of " ~
1541 			"their dependencies in a format similar to a JSON package " ~
1542 			"description file. This is useful mostly for IDEs.",
1543 			"",
1544 			"All usual options that are also used for build/run/generate apply.",
1545 			"",
1546 			"When --data=VALUE is supplied, specific build settings for a project " ~
1547 			"will be printed instead (by default, formatted for the current compiler).",
1548 			"",
1549 			"The --data=VALUE option can be specified multiple times to retrieve " ~
1550 			"several pieces of information at once. A comma-separated list is " ~
1551 			"also acceptable (ex: --data=dflags,libs). The data will be output in " ~
1552 			"the same order requested on the command line.",
1553 			"",
1554 			"The accepted values for --data=VALUE are:",
1555 			"",
1556 			"main-source-file, dflags, lflags, libs, linker-files, " ~
1557 			"source-files, versions, debug-versions, import-paths, " ~
1558 			"string-import-paths, import-files, options",
1559 			"",
1560 			"The following are also accepted by --data if --data-list is used:",
1561 			"",
1562 			"target-type, target-path, target-name, working-directory, " ~
1563 			"copy-files, string-import-files, pre-generate-commands, " ~
1564 			"post-generate-commands, pre-build-commands, post-build-commands, " ~
1565 			"pre-run-commands, post-run-commands, requirements",
1566 		];
1567 	}
1568 
1569 	override void prepare(scope CommandArgs args)
1570 	{
1571 		super.prepare(args);
1572 
1573 		args.getopt("import-paths", &m_importPaths, [
1574 			"Shortcut for --data=import-paths --data-list"
1575 		]);
1576 
1577 		args.getopt("string-import-paths", &m_stringImportPaths, [
1578 			"Shortcut for --data=string-import-paths --data-list"
1579 		]);
1580 
1581 		args.getopt("data", &m_data, [
1582 			"Just list the values of a particular build setting, either for this "~
1583 			"package alone or recursively including all dependencies. Accepts a "~
1584 			"comma-separated list. See above for more details and accepted "~
1585 			"possibilities for VALUE."
1586 		]);
1587 
1588 		args.getopt("data-list", &m_dataList, [
1589 			"Output --data information in list format (line-by-line), instead "~
1590 			"of formatting for a compiler command line.",
1591 		]);
1592 
1593 		args.getopt("data-0", &m_dataNullDelim, [
1594 			"Output --data information using null-delimiters, rather than "~
1595 			"spaces or newlines. Result is usable with, ex., xargs -0.",
1596 		]);
1597 	}
1598 
1599 	override int execute(Dub dub, string[] free_args, string[] app_args)
1600 	{
1601 		enforceUsage(
1602 			!(m_importPaths && m_stringImportPaths),
1603 			"--import-paths and --string-import-paths may not be used together."
1604 		);
1605 
1606 		enforceUsage(
1607 			!(m_data && (m_importPaths || m_stringImportPaths)),
1608 			"--data may not be used together with --import-paths or --string-import-paths."
1609 		);
1610 
1611 		// disable all log output to stdout and use "writeln" to output the JSON description
1612 		auto ll = getLogLevel();
1613 		setLogLevel(max(ll, LogLevel.warn));
1614 		scope (exit) setLogLevel(ll);
1615 
1616 		string str_package_info;
1617 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1618 		if (free_args.length >= 1) str_package_info = free_args[0];
1619 		setupVersionPackage(dub, str_package_info);
1620 
1621 		m_defaultConfig = dub.project.getDefaultConfiguration(this.baseSettings.platform);
1622 
1623 		GeneratorSettings settings = this.baseSettings;
1624 		if (!settings.config.length)
1625 			settings.config = m_defaultConfig;
1626 		// Ignore other options
1627 		settings.buildSettings.options = this.baseSettings.buildSettings.options & BuildOption.lowmem;
1628 
1629 		// With a requested `unittest` config, switch to the special test runner
1630 		// config (which doesn't require an existing `unittest` configuration).
1631 		if (this.baseSettings.config == "unittest") {
1632 			const test_config = dub.project.addTestRunnerConfiguration(settings, !dub.dryRun);
1633 			if (test_config) settings.config = test_config;
1634 		}
1635 
1636 		if (m_importPaths) { m_data = ["import-paths"]; m_dataList = true; }
1637 		else if (m_stringImportPaths) { m_data = ["string-import-paths"]; m_dataList = true; }
1638 
1639 		if (m_data.length) {
1640 			ListBuildSettingsFormat lt;
1641 			with (ListBuildSettingsFormat)
1642 				lt = m_dataList ? (m_dataNullDelim ? listNul : list) : (m_dataNullDelim ? commandLineNul : commandLine);
1643 			dub.listProjectData(settings, m_data, lt);
1644 		} else {
1645 			auto desc = dub.project.describe(settings);
1646 			writeln(desc.serializeToPrettyJson());
1647 		}
1648 
1649 		return 0;
1650 	}
1651 }
1652 
1653 class CleanCommand : Command {
1654 	private {
1655 		bool m_allPackages;
1656 	}
1657 
1658 	this() @safe pure nothrow
1659 	{
1660 		this.name = "clean";
1661 		this.argumentsPattern = "[<package>]";
1662 		this.description = "Removes intermediate build files and cached build results";
1663 		this.helpText = [
1664 			"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.",
1665 			"Without arguments, the package in the current working directory will be cleaned."
1666 		];
1667 	}
1668 
1669 	override void prepare(scope CommandArgs args)
1670 	{
1671 		args.getopt("all-packages", &m_allPackages, [
1672 			"Cleans up *all* known packages (dub list)"
1673 		]);
1674 	}
1675 
1676 	override int execute(Dub dub, string[] free_args, string[] app_args)
1677 	{
1678 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1679 		enforceUsage(app_args.length == 0, "Application arguments are not supported for the clean command.");
1680 		enforceUsage(!m_allPackages || !free_args.length, "The --all-packages flag may not be used together with an explicit package name.");
1681 
1682 		enforce(free_args.length == 0, "Cleaning a specific package isn't possible right now.");
1683 
1684 		if (m_allPackages) {
1685 			bool any_error = false;
1686 
1687 			foreach (p; dub.packageManager.getPackageIterator()) {
1688 				try dub.cleanPackage(p.path);
1689 				catch (Exception e) {
1690 					logWarn("Failed to clean package %s at %s: %s", p.name, p.path, e.msg);
1691 					any_error = true;
1692 				}
1693 
1694 				foreach (sp; p.subPackages.filter!(sp => !sp.path.empty)) {
1695 					try dub.cleanPackage(p.path ~ sp.path);
1696 					catch (Exception e) {
1697 						logWarn("Failed to clean sub package of %s at %s: %s", p.name, p.path ~ sp.path, e.msg);
1698 						any_error = true;
1699 					}
1700 				}
1701 			}
1702 
1703 			if (any_error) return 2;
1704 		} else {
1705 			dub.cleanPackage(dub.rootPath);
1706 		}
1707 
1708 		return 0;
1709 	}
1710 }
1711 
1712 
1713 /******************************************************************************/
1714 /* FETCH / ADD / REMOVE / UPGRADE                                             */
1715 /******************************************************************************/
1716 
1717 class AddCommand : Command {
1718 	this() @safe pure nothrow
1719 	{
1720 		this.name = "add";
1721 		this.argumentsPattern = "<package>[@<version-spec>] [<packages...>]";
1722 		this.description = "Adds dependencies to the package file.";
1723 		this.helpText = [
1724 			"Adds <packages> as dependencies.",
1725 			"",
1726 			"Running \"dub add <package>\" is the same as adding <package> to the \"dependencies\" section in dub.json/dub.sdl.",
1727 			"If no version is specified for one of the packages, dub will query the registry for the latest version."
1728 		];
1729 	}
1730 
1731 	override void prepare(scope CommandArgs args) {}
1732 
1733 	override int execute(Dub dub, string[] free_args, string[] app_args)
1734 	{
1735 		import dub.recipe.io : readPackageRecipe, writePackageRecipe;
1736 		import dub.internal.vibecompat.core.file : existsFile;
1737 		enforceUsage(free_args.length != 0, "Expected one or more arguments.");
1738 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1739 
1740 		if (!loadCwdPackage(dub, true)) return 2;
1741 		auto recipe = dub.project.rootPackage.rawRecipe.clone;
1742 
1743 		foreach (depspec; free_args) {
1744 			if (!addDependency(dub, recipe, depspec))
1745 				return 2;
1746 		}
1747 		writePackageRecipe(dub.project.rootPackage.recipePath, recipe);
1748 
1749 		return 0;
1750 	}
1751 }
1752 
1753 class UpgradeCommand : Command {
1754 	private {
1755 		bool m_prerelease = false;
1756 		bool m_includeSubPackages = false;
1757 		bool m_forceRemove = false;
1758 		bool m_missingOnly = false;
1759 		bool m_verify = false;
1760 		bool m_dryRun = false;
1761 	}
1762 
1763 	this() @safe pure nothrow
1764 	{
1765 		this.name = "upgrade";
1766 		this.argumentsPattern = "[<packages...>]";
1767 		this.description = "Forces an upgrade of the dependencies";
1768 		this.helpText = [
1769 			"Upgrades all dependencies of the package by querying the package registry(ies) for new versions.",
1770 			"",
1771 			"This will update the versions stored in the selections file ("~SelectedVersions.defaultFile~") accordingly.",
1772 			"",
1773 			"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."
1774 		];
1775 	}
1776 
1777 	override void prepare(scope CommandArgs args)
1778 	{
1779 		args.getopt("prerelease", &m_prerelease, [
1780 			"Uses the latest pre-release version, even if release versions are available"
1781 		]);
1782 		args.getopt("s|sub-packages", &m_includeSubPackages, [
1783 			"Also upgrades dependencies of all directory based sub packages"
1784 		]);
1785 		args.getopt("verify", &m_verify, [
1786 			"Updates the project and performs a build. If successful, rewrites the selected versions file <to be implemented>."
1787 		]);
1788 		args.getopt("dry-run", &m_dryRun, [
1789 			"Only print what would be upgraded, but don't actually upgrade anything."
1790 		]);
1791 		args.getopt("missing-only", &m_missingOnly, [
1792 			"Performs an upgrade only for dependencies that don't yet have a version selected. This is also done automatically before each build."
1793 		]);
1794 		args.getopt("force-remove", &m_forceRemove, [
1795 			"Deprecated option that does nothing."
1796 		]);
1797 	}
1798 
1799 	override int execute(Dub dub, string[] free_args, string[] app_args)
1800 	{
1801 		enforceUsage(free_args.length <= 1, "Unexpected arguments.");
1802 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1803 		enforceUsage(!m_verify, "--verify is not yet implemented.");
1804 		enforce(loadCwdPackage(dub, true), "Failed to load package.");
1805 		logInfo("Upgrading", Color.cyan, "project in %s", dub.projectPath.toNativeString().color(Mode.bold));
1806 		auto options = UpgradeOptions.upgrade|UpgradeOptions.select;
1807 		if (m_missingOnly) options &= ~UpgradeOptions.upgrade;
1808 		if (m_prerelease) options |= UpgradeOptions.preRelease;
1809 		if (m_dryRun) options |= UpgradeOptions.dryRun;
1810 		dub.upgrade(options, free_args);
1811 
1812 		auto spacks = dub.project.rootPackage
1813 			.subPackages
1814 			.filter!(sp => sp.path.length);
1815 
1816 		if (m_includeSubPackages) {
1817 			bool any_error = false;
1818 
1819 			// Go through each path based sub package, load it as a new instance
1820 			// and perform an upgrade as if the upgrade had been run from within
1821 			// the sub package folder. Note that we have to use separate Dub
1822 			// instances, because the upgrade always works on the root package
1823 			// of a project, which in this case are the individual sub packages.
1824 			foreach (sp; spacks) {
1825 				try {
1826 					auto fullpath = (dub.projectPath ~ sp.path).toNativeString();
1827 					logInfo("Upgrading", Color.cyan, "sub package in %s", fullpath);
1828 					auto sdub = new Dub(fullpath, dub.packageSuppliers, SkipPackageSuppliers.all);
1829 					sdub.defaultPlacementLocation = dub.defaultPlacementLocation;
1830 					sdub.loadPackage();
1831 					sdub.upgrade(options, free_args);
1832 				} catch (Exception e) {
1833 					logError("Failed to update sub package at %s: %s",
1834 						sp.path, e.msg);
1835 					any_error = true;
1836 				}
1837 			}
1838 
1839 			if (any_error) return 1;
1840 		} else if (!spacks.empty) {
1841 			foreach (sp; spacks)
1842 				logInfo("Not upgrading sub package in %s", sp.path);
1843 			logInfo("\nNote: specify -s to also upgrade sub packages.");
1844 		}
1845 
1846 		return 0;
1847 	}
1848 }
1849 
1850 class FetchRemoveCommand : Command {
1851 	protected {
1852 		string m_version;
1853 		bool m_forceRemove = false;
1854 	}
1855 
1856 	override void prepare(scope CommandArgs args)
1857 	{
1858 		args.getopt("version", &m_version, [
1859 			"Use the specified version/branch instead of the latest available match",
1860 			"The remove command also accepts \"*\" here as a wildcard to remove all versions of the package from the specified location"
1861 		], true); // hide --version from help
1862 
1863 		args.getopt("force-remove", &m_forceRemove, [
1864 			"Deprecated option that does nothing"
1865 		]);
1866 	}
1867 
1868 	abstract override int execute(Dub dub, string[] free_args, string[] app_args);
1869 }
1870 
1871 class FetchCommand : FetchRemoveCommand {
1872 	this() @safe pure nothrow
1873 	{
1874 		this.name = "fetch";
1875 		this.argumentsPattern = "<package>[@<version-spec>]";
1876 		this.description = "Manually retrieves and caches a package";
1877 		this.helpText = [
1878 			"Note: Use \"dub add <dependency>\" if you just want to use a certain package as a dependency, you don't have to explicitly fetch packages.",
1879 			"",
1880 			"Explicit retrieval/removal of packages is only needed when you want to put packages in a place where several applications can share them. If you just have a dependency to add, use the `add` command. Dub will do the rest for you.",
1881 			"",
1882 			"Without specified options, placement/removal will default to a user wide shared location.",
1883 			"",
1884 			"Complete applications can be retrieved and run easily by e.g.",
1885 			"$ dub fetch vibelog --cache=local",
1886 			"$ dub run vibelog --cache=local",
1887 			"",
1888 			"This will grab all needed dependencies and compile and run the application.",
1889 		];
1890 	}
1891 
1892 	override void prepare(scope CommandArgs args)
1893 	{
1894 		super.prepare(args);
1895 	}
1896 
1897 	override int execute(Dub dub, string[] free_args, string[] app_args)
1898 	{
1899 		enforceUsage(free_args.length == 1, "Expecting exactly one argument.");
1900 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1901 
1902 		auto location = dub.defaultPlacementLocation;
1903 
1904 		auto name = free_args[0];
1905 
1906 		FetchOptions fetchOpts;
1907 		fetchOpts |= FetchOptions.forceBranchUpgrade;
1908 		if (m_version.length) { // remove then --version removed
1909 			enforceUsage(!name.canFindVersionSplitter, "Double version spec not allowed.");
1910 			logWarn("The '--version' parameter was deprecated, use %s@%s. Please update your scripts.", name, m_version);
1911 			dub.fetch(name, VersionRange.fromString(m_version), location, fetchOpts);
1912 		} else if (name.canFindVersionSplitter) {
1913 			const parts = name.splitPackageName;
1914 			dub.fetch(parts.name, VersionRange.fromString(parts.version_), location, fetchOpts);
1915 		} else {
1916 			try {
1917 				dub.fetch(name, VersionRange.Any, location, fetchOpts);
1918 				logInfo("Finished", Color.green, "%s fetched", name.color(Mode.bold));
1919 				logInfo(
1920 					"Please note that you need to use `dub run <pkgname>` " ~
1921 					"or add it to dependencies of your package to actually use/run it. "
1922 				);
1923 			}
1924 			catch(Exception e){
1925 				logInfo("Getting a release version failed: %s", e.msg);
1926 				logInfo("Retry with ~master...");
1927 				dub.fetch(name, VersionRange.fromString("~master"), location, fetchOpts);
1928 			}
1929 		}
1930 		return 0;
1931 	}
1932 }
1933 
1934 class RemoveCommand : FetchRemoveCommand {
1935 	private {
1936 		bool m_nonInteractive;
1937 	}
1938 
1939 	this() @safe pure nothrow
1940 	{
1941 		this.name = "remove";
1942 		this.argumentsPattern = "<package>[@<version-spec>]";
1943 		this.description = "Removes a cached package";
1944 		this.helpText = [
1945 			"Removes a package that is cached on the local system."
1946 		];
1947 	}
1948 
1949 	override void prepare(scope CommandArgs args)
1950 	{
1951 		super.prepare(args);
1952 		args.getopt("n|non-interactive", &m_nonInteractive, ["Don't enter interactive mode."]);
1953 	}
1954 
1955 	override int execute(Dub dub, string[] free_args, string[] app_args)
1956 	{
1957 		enforceUsage(free_args.length == 1, "Expecting exactly one argument.");
1958 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1959 
1960 		auto package_id = free_args[0];
1961 		auto location = dub.defaultPlacementLocation;
1962 
1963 		size_t resolveVersion(in Package[] packages) {
1964 			// just remove only package version
1965 			if (packages.length == 1)
1966 				return 0;
1967 
1968 			writeln("Select version of '", package_id, "' to remove from location '", location, "':");
1969 			foreach (i, pack; packages)
1970 				writefln("%s) %s", i + 1, pack.version_);
1971 			writeln(packages.length + 1, ") ", "all versions");
1972 			while (true) {
1973 				writef("> ");
1974 				auto inp = readln();
1975 				if (!inp.length) // Ctrl+D
1976 					return size_t.max;
1977 				inp = inp.stripRight;
1978 				if (!inp.length) // newline or space
1979 					continue;
1980 				try {
1981 					immutable selection = inp.to!size_t - 1;
1982 					if (selection <= packages.length)
1983 						return selection;
1984 				} catch (ConvException e) {
1985 				}
1986 				logError("Please enter a number between 1 and %s.", packages.length + 1);
1987 			}
1988 		}
1989 
1990 		if (!m_version.empty) { // remove then --version removed
1991 			enforceUsage(!package_id.canFindVersionSplitter, "Double version spec not allowed.");
1992 			logWarn("The '--version' parameter was deprecated, use %s@%s. Please update your scripts.", package_id, m_version);
1993 			dub.remove(package_id, m_version, location);
1994 		} else {
1995 			const parts = package_id.splitPackageName;
1996 			if (m_nonInteractive || parts.version_.length) {
1997 				dub.remove(parts.name, parts.version_, location);
1998 			} else {
1999 				dub.remove(package_id, location, &resolveVersion);
2000 			}
2001 		}
2002 		return 0;
2003 	}
2004 }
2005 
2006 /******************************************************************************/
2007 /* ADD/REMOVE PATH/LOCAL                                                      */
2008 /******************************************************************************/
2009 
2010 abstract class RegistrationCommand : Command {
2011 	private {
2012 		bool m_system;
2013 	}
2014 
2015 	override void prepare(scope CommandArgs args)
2016 	{
2017 		args.getopt("system", &m_system, [
2018 			"Register system-wide instead of user-wide"
2019 		]);
2020 	}
2021 
2022 	abstract override int execute(Dub dub, string[] free_args, string[] app_args);
2023 }
2024 
2025 class AddPathCommand : RegistrationCommand {
2026 	this() @safe pure nothrow
2027 	{
2028 		this.name = "add-path";
2029 		this.argumentsPattern = "<path>";
2030 		this.description = "Adds a default package search path";
2031 		this.helpText = [
2032 			"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.",
2033 			"",
2034 			"Any packages registered using add-path will be preferred over packages downloaded from the package registry when searching for dependencies during a build operation.",
2035 			"",
2036 			"The version of the packages will be determined by one of the following:",
2037 			"  - For GIT working copies, the last tag (git describe) is used to determine the version",
2038 			"  - If the package contains a \"version\" field in the package description, this is used",
2039 			"  - If neither of those apply, \"~master\" is assumed"
2040 		];
2041 	}
2042 
2043 	override int execute(Dub dub, string[] free_args, string[] app_args)
2044 	{
2045 		enforceUsage(free_args.length == 1, "Missing search path.");
2046 		dub.addSearchPath(free_args[0], m_system);
2047 		return 0;
2048 	}
2049 }
2050 
2051 class RemovePathCommand : RegistrationCommand {
2052 	this() @safe pure nothrow
2053 	{
2054 		this.name = "remove-path";
2055 		this.argumentsPattern = "<path>";
2056 		this.description = "Removes a package search path";
2057 		this.helpText = ["Removes a package search path previously added with add-path."];
2058 	}
2059 
2060 	override int execute(Dub dub, string[] free_args, string[] app_args)
2061 	{
2062 		enforceUsage(free_args.length == 1, "Expected one argument.");
2063 		dub.removeSearchPath(free_args[0], m_system);
2064 		return 0;
2065 	}
2066 }
2067 
2068 class AddLocalCommand : RegistrationCommand {
2069 	this() @safe pure nothrow
2070 	{
2071 		this.name = "add-local";
2072 		this.argumentsPattern = "<path> [<version>]";
2073 		this.description = "Adds a local package directory (e.g. a git repository)";
2074 		this.helpText = [
2075 			"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.",
2076 			"",
2077 			"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.",
2078 			"",
2079 			"See 'dub add-path -h' for a way to register multiple local packages at once."
2080 		];
2081 	}
2082 
2083 	override int execute(Dub dub, string[] free_args, string[] app_args)
2084 	{
2085 		enforceUsage(free_args.length == 1 || free_args.length == 2, "Expecting one or two arguments.");
2086 		string ver = free_args.length == 2 ? free_args[1] : null;
2087 		dub.addLocalPackage(free_args[0], ver, m_system);
2088 		return 0;
2089 	}
2090 }
2091 
2092 class RemoveLocalCommand : RegistrationCommand {
2093 	this() @safe pure nothrow
2094 	{
2095 		this.name = "remove-local";
2096 		this.argumentsPattern = "<path>";
2097 		this.description = "Removes a local package directory";
2098 		this.helpText = ["Removes a local package directory"];
2099 	}
2100 
2101 	override int execute(Dub dub, string[] free_args, string[] app_args)
2102 	{
2103 		enforceUsage(free_args.length >= 1, "Missing package path argument.");
2104 		enforceUsage(free_args.length <= 1, "Expected the package path to be the only argument.");
2105 		dub.removeLocalPackage(free_args[0], m_system);
2106 		return 0;
2107 	}
2108 }
2109 
2110 class ListCommand : Command {
2111 	this() @safe pure nothrow
2112 	{
2113 		this.name = "list";
2114 		this.argumentsPattern = "[<package>[@<version-spec>]]";
2115 		this.description = "Prints a list of all or selected local packages dub is aware of";
2116 		this.helpText = [
2117 			"Prints a list of all or selected local packages. This includes all cached "~
2118 			"packages (user or system wide), all packages in the package search paths "~
2119 			"(\"dub add-path\") and all manually registered packages (\"dub add-local\"). "~
2120 			"If a package (and optionally a version spec) is specified, only matching packages are shown."
2121 		];
2122 	}
2123 	override void prepare(scope CommandArgs args) {}
2124 	override int execute(Dub dub, string[] free_args, string[] app_args)
2125 	{
2126 		enforceUsage(free_args.length <= 1, "Expecting zero or one extra arguments.");
2127 		const pinfo = free_args.length ? splitPackageName(free_args[0]) : PackageAndVersion("","*");
2128 		const pname = pinfo.name;
2129 		const pvlim = Dependency(pinfo.version_ == "" ? "*" : pinfo.version_);
2130 		enforceUsage(app_args.length == 0, "The list command supports no application arguments.");
2131 		logInfoNoTag("Packages present in the system and known to dub:");
2132 		foreach (p; dub.packageManager.getPackageIterator()) {
2133 			if ((pname == "" || pname == p.name) && pvlim.matches(p.version_))
2134 				logInfoNoTag("  %s %s: %s", p.name.color(Mode.bold), p.version_, p.path.toNativeString());
2135 		}
2136 		logInfo("");
2137 		return 0;
2138 	}
2139 }
2140 
2141 class SearchCommand : Command {
2142 	this() @safe pure nothrow
2143 	{
2144 		this.name = "search";
2145 		this.argumentsPattern = "<package-name>";
2146 		this.description = "Search for available packages.";
2147 		this.helpText = [
2148 			"Search all specified providers for matching packages."
2149 		];
2150 	}
2151 	override void prepare(scope CommandArgs args) {}
2152 	override int execute(Dub dub, string[] free_args, string[] app_args)
2153 	{
2154 		enforce(free_args.length == 1, "Expected one argument.");
2155 		auto res = dub.searchPackages(free_args[0]);
2156 		if (res.empty)
2157 		{
2158 			logError("No matches found.");
2159 			return 2;
2160 		}
2161 		auto justify = res
2162 			.map!((descNmatches) => descNmatches[1])
2163 			.joiner
2164 			.map!(m => m.name.length + m.version_.length)
2165 			.reduce!max + " ()".length;
2166 		justify += (~justify & 3) + 1; // round to next multiple of 4
2167 		int colorDifference = cast(int)"a".color(Mode.bold).length - 1;
2168 		justify += colorDifference;
2169 		foreach (desc, matches; res)
2170 		{
2171 			logInfoNoTag("==== %s ====", desc);
2172 			foreach (m; matches)
2173 				logInfoNoTag("  %s%s", leftJustify(m.name.color(Mode.bold)
2174 					~ " (" ~ m.version_ ~ ")", justify), m.description);
2175 		}
2176 		return 0;
2177 	}
2178 }
2179 
2180 
2181 /******************************************************************************/
2182 /* OVERRIDES                                                                  */
2183 /******************************************************************************/
2184 
2185 class AddOverrideCommand : Command {
2186 	private {
2187 		bool m_system = false;
2188 	}
2189 
2190 	static immutable string DeprecationMessage =
2191 		"This command is deprecated. Use path based dependency, custom cache path, " ~
2192 		"or edit `dub.selections.json` to achieve the same results.";
2193 
2194 
2195 	this() @safe pure nothrow
2196 	{
2197 		this.name = "add-override";
2198 		this.argumentsPattern = "<package> <version-spec> <target-path/target-version>";
2199 		this.description = "Adds a new package override.";
2200 
2201 		this.hidden = true;
2202 		this.helpText = [ DeprecationMessage ];
2203 	}
2204 
2205 	override void prepare(scope CommandArgs args)
2206 	{
2207 		args.getopt("system", &m_system, [
2208 			"Register system-wide instead of user-wide"
2209 		]);
2210 	}
2211 
2212 	override int execute(Dub dub, string[] free_args, string[] app_args)
2213 	{
2214 		logWarn(DeprecationMessage);
2215 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
2216 		enforceUsage(free_args.length == 3, "Expected three arguments, not "~free_args.length.to!string);
2217 		auto scope_ = m_system ? PlacementLocation.system : PlacementLocation.user;
2218 		auto pack = free_args[0];
2219 		auto source = VersionRange.fromString(free_args[1]);
2220 		if (existsFile(NativePath(free_args[2]))) {
2221 			auto target = NativePath(free_args[2]);
2222 			if (!target.absolute) target = NativePath(getcwd()) ~ target;
2223 			dub.packageManager.addOverride_(scope_, pack, source, target);
2224 			logInfo("Added override %s %s => %s", pack, source, target);
2225 		} else {
2226 			auto target = Version(free_args[2]);
2227 			dub.packageManager.addOverride_(scope_, pack, source, target);
2228 			logInfo("Added override %s %s => %s", pack, source, target);
2229 		}
2230 		return 0;
2231 	}
2232 }
2233 
2234 class RemoveOverrideCommand : Command {
2235 	private {
2236 		bool m_system = false;
2237 	}
2238 
2239 	this() @safe pure nothrow
2240 	{
2241 		this.name = "remove-override";
2242 		this.argumentsPattern = "<package> <version-spec>";
2243 		this.description = "Removes an existing package override.";
2244 
2245 		this.hidden = true;
2246 		this.helpText = [ AddOverrideCommand.DeprecationMessage ];
2247 	}
2248 
2249 	override void prepare(scope CommandArgs args)
2250 	{
2251 		args.getopt("system", &m_system, [
2252 			"Register system-wide instead of user-wide"
2253 		]);
2254 	}
2255 
2256 	override int execute(Dub dub, string[] free_args, string[] app_args)
2257 	{
2258 		logWarn(AddOverrideCommand.DeprecationMessage);
2259 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
2260 		enforceUsage(free_args.length == 2, "Expected two arguments, not "~free_args.length.to!string);
2261 		auto scope_ = m_system ? PlacementLocation.system : PlacementLocation.user;
2262 		auto source = VersionRange.fromString(free_args[1]);
2263 		dub.packageManager.removeOverride_(scope_, free_args[0], source);
2264 		return 0;
2265 	}
2266 }
2267 
2268 class ListOverridesCommand : Command {
2269 	this() @safe pure nothrow
2270 	{
2271 		this.name = "list-overrides";
2272 		this.argumentsPattern = "";
2273 		this.description = "Prints a list of all local package overrides";
2274 
2275 		this.hidden = true;
2276 		this.helpText = [ AddOverrideCommand.DeprecationMessage ];
2277 	}
2278 	override void prepare(scope CommandArgs args) {}
2279 	override int execute(Dub dub, string[] free_args, string[] app_args)
2280 	{
2281 		logWarn(AddOverrideCommand.DeprecationMessage);
2282 
2283 		void printList(in PackageOverride_[] overrides, string caption)
2284 		{
2285 			if (overrides.length == 0) return;
2286 			logInfoNoTag("# %s", caption);
2287 			foreach (ovr; overrides)
2288 				ovr.target.match!(
2289 					t => logInfoNoTag("%s %s => %s", ovr.package_.color(Mode.bold), ovr.version_, t));
2290 		}
2291 		printList(dub.packageManager.getOverrides_(PlacementLocation.user), "User wide overrides");
2292 		printList(dub.packageManager.getOverrides_(PlacementLocation.system), "System wide overrides");
2293 		return 0;
2294 	}
2295 }
2296 
2297 /******************************************************************************/
2298 /* Cache cleanup                                                              */
2299 /******************************************************************************/
2300 
2301 class CleanCachesCommand : Command {
2302 	this() @safe pure nothrow
2303 	{
2304 		this.name = "clean-caches";
2305 		this.argumentsPattern = "";
2306 		this.description = "Removes cached metadata";
2307 		this.helpText = [
2308 			"This command removes any cached metadata like the list of available packages and their latest version."
2309 		];
2310 	}
2311 
2312 	override void prepare(scope CommandArgs args) {}
2313 
2314 	override int execute(Dub dub, string[] free_args, string[] app_args)
2315 	{
2316 		return 0;
2317 	}
2318 }
2319 
2320 /******************************************************************************/
2321 /* DUSTMITE                                                                   */
2322 /******************************************************************************/
2323 
2324 class DustmiteCommand : PackageBuildCommand {
2325 	private {
2326 		int m_compilerStatusCode = int.min;
2327 		int m_linkerStatusCode = int.min;
2328 		int m_programStatusCode = int.min;
2329 		string m_compilerRegex;
2330 		string m_linkerRegex;
2331 		string m_programRegex;
2332 		string m_testPackage;
2333 		bool m_noRedirect;
2334 		string m_strategy;
2335 		uint m_jobCount;		// zero means not specified
2336 		bool m_trace;
2337 	}
2338 
2339 	this() @safe pure nothrow
2340 	{
2341 		this.name = "dustmite";
2342 		this.argumentsPattern = "<destination-path>";
2343 		this.acceptsAppArgs = true;
2344 		this.description = "Create reduced test cases for build errors";
2345 		this.helpText = [
2346 			"This command uses the Dustmite utility to isolate the cause of build errors in a DUB project.",
2347 			"",
2348 			"It will create a copy of all involved packages and run dustmite on this copy, leaving a reduced test case.",
2349 			"",
2350 			"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."
2351 		];
2352 	}
2353 
2354 	override void prepare(scope CommandArgs args)
2355 	{
2356 		args.getopt("compiler-status", &m_compilerStatusCode, ["The expected status code of the compiler run"]);
2357 		args.getopt("compiler-regex", &m_compilerRegex, ["A regular expression used to match against the compiler output"]);
2358 		args.getopt("linker-status", &m_linkerStatusCode, ["The expected status code of the linker run"]);
2359 		args.getopt("linker-regex", &m_linkerRegex, ["A regular expression used to match against the linker output"]);
2360 		args.getopt("program-status", &m_programStatusCode, ["The expected status code of the built executable"]);
2361 		args.getopt("program-regex", &m_programRegex, ["A regular expression used to match against the program output"]);
2362 		args.getopt("test-package", &m_testPackage, ["Perform a test run - usually only used internally"]);
2363 		args.getopt("combined", &this.baseSettings.combined, ["Builds multiple packages with one compiler run"]);
2364 		args.getopt("no-redirect", &m_noRedirect, ["Don't redirect stdout/stderr streams of the test command"]);
2365 		args.getopt("strategy", &m_strategy, ["Set strategy (careful/lookback/pingpong/indepth/inbreadth)"]);
2366 		args.getopt("j", &m_jobCount, ["Set number of look-ahead processes"]);
2367 		args.getopt("trace", &m_trace, ["Save all attempted reductions to DIR.trace"]);
2368 		super.prepare(args);
2369 
2370 		// speed up loading when in test mode
2371 		if (m_testPackage.length) {
2372 			m_nodeps = true;
2373 		}
2374 	}
2375 
2376 	/// Returns: A minimally-initialized dub instance in test mode
2377 	override Dub prepareDub(CommonOptions options)
2378 	{
2379 		if (!m_testPackage.length)
2380 			return super.prepareDub(options);
2381 		return new Dub(NativePath(options.root_path), NativePath(getcwd()));
2382 	}
2383 
2384 	override int execute(Dub dub, string[] free_args, string[] app_args)
2385 	{
2386 		import std.format : formattedWrite;
2387 
2388 		if (m_testPackage.length) {
2389 			setupPackage(dub, m_testPackage);
2390 			m_defaultConfig = dub.project.getDefaultConfiguration(this.baseSettings.platform);
2391 
2392 			GeneratorSettings gensettings = this.baseSettings;
2393 			if (!gensettings.config.length)
2394 				gensettings.config = m_defaultConfig;
2395 			gensettings.run = m_programStatusCode != int.min || m_programRegex.length;
2396 			gensettings.runArgs = app_args;
2397 			gensettings.force = true;
2398 			gensettings.compileCallback = check(m_compilerStatusCode, m_compilerRegex);
2399 			gensettings.linkCallback = check(m_linkerStatusCode, m_linkerRegex);
2400 			gensettings.runCallback = check(m_programStatusCode, m_programRegex);
2401 			try dub.generateProject("build", gensettings);
2402 			catch (DustmiteMismatchException) {
2403 				logInfoNoTag("Dustmite test doesn't match.");
2404 				return 3;
2405 			}
2406 			catch (DustmiteMatchException) {
2407 				logInfoNoTag("Dustmite test matches.");
2408 				return 0;
2409 			}
2410 		} else {
2411 			enforceUsage(free_args.length == 1, "Expected destination path.");
2412 			auto path = NativePath(free_args[0]);
2413 			path.normalize();
2414 			enforceUsage(!path.empty, "Destination path must not be empty.");
2415 			if (!path.absolute) path = NativePath(getcwd()) ~ path;
2416 			enforceUsage(!path.startsWith(dub.rootPath), "Destination path must not be a sub directory of the tested package!");
2417 
2418 			setupPackage(dub, null);
2419 			auto prj = dub.project;
2420 			if (this.baseSettings.config.empty)
2421 				this.baseSettings.config = prj.getDefaultConfiguration(this.baseSettings.platform);
2422 
2423 			void copyFolderRec(NativePath folder, NativePath dstfolder)
2424 			{
2425 				mkdirRecurse(dstfolder.toNativeString());
2426 				foreach (de; iterateDirectory(folder.toNativeString())) {
2427 					if (de.name.startsWith(".")) continue;
2428 					if (de.isDirectory) {
2429 						copyFolderRec(folder ~ de.name, dstfolder ~ de.name);
2430 					} else {
2431 						if (de.name.endsWith(".o") || de.name.endsWith(".obj")) continue;
2432 						if (de.name.endsWith(".exe")) continue;
2433 						try copyFile(folder ~ de.name, dstfolder ~ de.name);
2434 						catch (Exception e) {
2435 							logWarn("Failed to copy file %s: %s", (folder ~ de.name).toNativeString(), e.msg);
2436 						}
2437 					}
2438 				}
2439 			}
2440 
2441 			static void fixPathDependency(string pack, ref Dependency dep) {
2442 				dep.visit!(
2443 					(NativePath path) {
2444 						auto mainpack = getBasePackageName(pack);
2445 						dep = Dependency(NativePath("../") ~ mainpack);
2446 					},
2447 					(any) { /* Nothing to do */ },
2448 				);
2449 			}
2450 
2451 			void fixPathDependencies(ref PackageRecipe recipe, NativePath base_path)
2452 			{
2453 				foreach (name, ref dep; recipe.buildSettings.dependencies)
2454 					fixPathDependency(name, dep);
2455 
2456 				foreach (ref cfg; recipe.configurations)
2457 					foreach (name, ref dep; cfg.buildSettings.dependencies)
2458 						fixPathDependency(name, dep);
2459 
2460 				foreach (ref subp; recipe.subPackages)
2461 					if (subp.path.length) {
2462 						auto sub_path = base_path ~ NativePath(subp.path);
2463 						auto pack = prj.packageManager.getOrLoadPackage(sub_path);
2464 						fixPathDependencies(pack.recipe, sub_path);
2465 						pack.storeInfo(sub_path);
2466 					} else fixPathDependencies(subp.recipe, base_path);
2467 			}
2468 
2469 			bool[string] visited;
2470 			foreach (pack_; prj.getTopologicalPackageList()) {
2471 				auto pack = pack_.basePackage;
2472 				if (pack.name in visited) continue;
2473 				visited[pack.name] = true;
2474 				auto dst_path = path ~ pack.name;
2475 				logInfo("Prepare", Color.light_blue, "Copy package %s to destination folder...", pack.name.color(Mode.bold));
2476 				copyFolderRec(pack.path, dst_path);
2477 
2478 				// adjust all path based dependencies
2479 				fixPathDependencies(pack.recipe, dst_path);
2480 
2481 				// overwrite package description file with additional version information
2482 				pack.storeInfo(dst_path);
2483 			}
2484 
2485 			logInfo("Starting", Color.light_green, "Executing dustmite...");
2486 			auto testcmd = appender!string();
2487 			testcmd.formattedWrite("%s dustmite --test-package=%s --build=%s --config=%s",
2488 				thisExePath, prj.name, this.baseSettings.buildType, this.baseSettings.config);
2489 
2490 			if (m_compilerName.length) testcmd.formattedWrite(" \"--compiler=%s\"", m_compilerName);
2491 			if (m_arch.length) testcmd.formattedWrite(" --arch=%s", m_arch);
2492 			if (m_compilerStatusCode != int.min) testcmd.formattedWrite(" --compiler-status=%s", m_compilerStatusCode);
2493 			if (m_compilerRegex.length) testcmd.formattedWrite(" \"--compiler-regex=%s\"", m_compilerRegex);
2494 			if (m_linkerStatusCode != int.min) testcmd.formattedWrite(" --linker-status=%s", m_linkerStatusCode);
2495 			if (m_linkerRegex.length) testcmd.formattedWrite(" \"--linker-regex=%s\"", m_linkerRegex);
2496 			if (m_programStatusCode != int.min) testcmd.formattedWrite(" --program-status=%s", m_programStatusCode);
2497 			if (m_programRegex.length) testcmd.formattedWrite(" \"--program-regex=%s\"", m_programRegex);
2498 			if (this.baseSettings.combined) testcmd ~= " --combined";
2499 
2500 			// --vquiet swallows dustmite's output ...
2501 			if (!m_noRedirect) testcmd ~= " --vquiet";
2502 
2503 			// TODO: pass *all* original parameters
2504 			logDiagnostic("Running dustmite: %s", testcmd);
2505 
2506 			string[] extraArgs;
2507 			if (m_noRedirect) extraArgs ~= "--no-redirect";
2508 			if (m_strategy.length) extraArgs ~= "--strategy=" ~ m_strategy;
2509 			if (m_jobCount) extraArgs ~= "-j" ~ m_jobCount.to!string;
2510 			if (m_trace) extraArgs ~= "--trace";
2511 
2512 			const cmd = "dustmite" ~ extraArgs ~ [path.toNativeString(), testcmd.data];
2513 			auto dmpid = spawnProcess(cmd);
2514 			return dmpid.wait();
2515 		}
2516 		return 0;
2517 	}
2518 
2519 	void delegate(int, string) check(int code_match, string regex_match)
2520 	{
2521 		return (code, output) {
2522 			import std.encoding;
2523 			import std.regex;
2524 
2525 			logInfo("%s", output);
2526 
2527 			if (code_match != int.min && code != code_match) {
2528 				logInfo("Exit code %s doesn't match expected value %s", code, code_match);
2529 				throw new DustmiteMismatchException;
2530 			}
2531 
2532 			if (regex_match.length > 0 && !match(output.sanitize, regex_match)) {
2533 				logInfo("Output doesn't match regex:");
2534 				logInfo("%s", output);
2535 				throw new DustmiteMismatchException;
2536 			}
2537 
2538 			if (code != 0 && code_match != int.min || regex_match.length > 0) {
2539 				logInfo("Tool failed, but matched either exit code or output - counting as match.");
2540 				throw new DustmiteMatchException;
2541 			}
2542 		};
2543 	}
2544 
2545 	static class DustmiteMismatchException : Exception {
2546 		this(string message = "", string file = __FILE__, int line = __LINE__, Throwable next = null)
2547 		{
2548 			super(message, file, line, next);
2549 		}
2550 	}
2551 
2552 	static class DustmiteMatchException : Exception {
2553 		this(string message = "", string file = __FILE__, int line = __LINE__, Throwable next = null)
2554 		{
2555 			super(message, file, line, next);
2556 		}
2557 	}
2558 }
2559 
2560 
2561 /******************************************************************************/
2562 /* CONVERT command                                                               */
2563 /******************************************************************************/
2564 
2565 class ConvertCommand : Command {
2566 	private {
2567 		string m_format;
2568 		bool m_stdout;
2569 	}
2570 
2571 	this() @safe pure nothrow
2572 	{
2573 		this.name = "convert";
2574 		this.argumentsPattern = "";
2575 		this.description = "Converts the file format of the package recipe.";
2576 		this.helpText = [
2577 			"This command will convert between JSON and SDLang formatted package recipe files.",
2578 			"",
2579 			"Warning: Beware that any formatting and comments within the package recipe will get lost in the conversion process."
2580 		];
2581 	}
2582 
2583 	override void prepare(scope CommandArgs args)
2584 	{
2585 		args.getopt("f|format", &m_format, ["Specifies the target package recipe format. Possible values:", "  json, sdl"]);
2586 		args.getopt("s|stdout", &m_stdout, ["Outputs the converted package recipe to stdout instead of writing to disk."]);
2587 	}
2588 
2589 	override int execute(Dub dub, string[] free_args, string[] app_args)
2590 	{
2591 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
2592 		enforceUsage(free_args.length == 0, "Unexpected arguments: "~free_args.join(" "));
2593 		enforceUsage(m_format.length > 0, "Missing target format file extension (--format=...).");
2594 		if (!loadCwdPackage(dub, true)) return 2;
2595 		dub.convertRecipe(m_format, m_stdout);
2596 		return 0;
2597 	}
2598 }
2599 
2600 
2601 /******************************************************************************/
2602 /* HELP                                                                       */
2603 /******************************************************************************/
2604 
2605 private {
2606 	enum shortArgColumn = 2;
2607 	enum longArgColumn = 6;
2608 	enum descColumn = 24;
2609 	enum lineWidth = 80 - 1;
2610 }
2611 
2612 private void showHelp(in CommandGroup[] commands, CommandArgs common_args)
2613 {
2614 	writeln(
2615 `USAGE: dub [--version] [<command>] [<options...>] [-- [<application arguments...>]]
2616 
2617 Manages the DUB project in the current directory. If the command is omitted,
2618 DUB will default to "run". When running an application, "--" can be used to
2619 separate DUB options from options passed to the application.
2620 
2621 Run "dub <command> --help" to get help for a specific command.
2622 
2623 You can use the "http_proxy" environment variable to configure a proxy server
2624 to be used for fetching packages.
2625 
2626 
2627 Available commands
2628 ==================`);
2629 
2630 	foreach (grp; commands) {
2631 		writeln();
2632 		writeWS(shortArgColumn);
2633 		writeln(grp.caption);
2634 		writeWS(shortArgColumn);
2635 		writerep!'-'(grp.caption.length);
2636 		writeln();
2637 		foreach (cmd; grp.commands) {
2638 			if (cmd.hidden) continue;
2639 			writeWS(shortArgColumn);
2640 			writef("%s %s", cmd.name, cmd.argumentsPattern);
2641 			auto chars_output = cmd.name.length + cmd.argumentsPattern.length + shortArgColumn + 1;
2642 			if (chars_output < descColumn) {
2643 				writeWS(descColumn - chars_output);
2644 			} else {
2645 				writeln();
2646 				writeWS(descColumn);
2647 			}
2648 			writeWrapped(cmd.description, descColumn, descColumn);
2649 		}
2650 	}
2651 	writeln();
2652 	writeln();
2653 	writeln(`Common options`);
2654 	writeln(`==============`);
2655 	writeln();
2656 	writeOptions(common_args);
2657 	writeln();
2658 	showVersion();
2659 }
2660 
2661 private void showVersion()
2662 {
2663 	writefln("DUB version %s, built on %s", getDUBVersion(), __DATE__);
2664 }
2665 
2666 private void showCommandHelp(Command cmd, CommandArgs args, CommandArgs common_args)
2667 {
2668 	writefln(`USAGE: dub %s %s [<options...>]%s`, cmd.name, cmd.argumentsPattern, cmd.acceptsAppArgs ? " [-- <application arguments...>]": null);
2669 	writeln();
2670 	foreach (ln; cmd.helpText)
2671 		ln.writeWrapped();
2672 
2673 	if (args.recognizedArgs.length) {
2674 		writeln();
2675 		writeln();
2676 		writeln("Command specific options");
2677 		writeln("========================");
2678 		writeln();
2679 		writeOptions(args);
2680 	}
2681 
2682 	writeln();
2683 	writeln();
2684 	writeln("Common options");
2685 	writeln("==============");
2686 	writeln();
2687 	writeOptions(common_args);
2688 	writeln();
2689 	writefln("DUB version %s, built on %s", getDUBVersion(), __DATE__);
2690 }
2691 
2692 private void writeOptions(CommandArgs args)
2693 {
2694 	foreach (arg; args.recognizedArgs) {
2695 		if (arg.hidden) continue;
2696 		auto names = arg.names.split("|");
2697 		assert(names.length == 1 || names.length == 2);
2698 		string sarg = names[0].length == 1 ? names[0] : null;
2699 		string larg = names[0].length > 1 ? names[0] : names.length > 1 ? names[1] : null;
2700 		if (sarg !is null) {
2701 			writeWS(shortArgColumn);
2702 			writef("-%s", sarg);
2703 			writeWS(longArgColumn - shortArgColumn - 2);
2704 		} else writeWS(longArgColumn);
2705 		size_t col = longArgColumn;
2706 		if (larg !is null) {
2707 			if (arg.defaultValue.peek!bool) {
2708 				writef("--%s", larg);
2709 				col += larg.length + 2;
2710 			} else {
2711 				writef("--%s=VALUE", larg);
2712 				col += larg.length + 8;
2713 			}
2714 		}
2715 		if (col < descColumn) {
2716 			writeWS(descColumn - col);
2717 		} else {
2718 			writeln();
2719 			writeWS(descColumn);
2720 		}
2721 		foreach (i, ln; arg.helpText) {
2722 			if (i > 0) writeWS(descColumn);
2723 			ln.writeWrapped(descColumn, descColumn);
2724 		}
2725 	}
2726 }
2727 
2728 private void writeWrapped(string string, size_t indent = 0, size_t first_line_pos = 0)
2729 {
2730 	// handle pre-indented strings and bullet lists
2731 	size_t first_line_indent = 0;
2732 	while (string.startsWith(" ")) {
2733 		string = string[1 .. $];
2734 		indent++;
2735 		first_line_indent++;
2736 	}
2737 	if (string.startsWith("- ")) indent += 2;
2738 
2739 	auto wrapped = string.wrap(lineWidth, getRepString!' '(first_line_pos+first_line_indent), getRepString!' '(indent));
2740 	wrapped = wrapped[first_line_pos .. $];
2741 	foreach (ln; wrapped.splitLines())
2742 		writeln(ln);
2743 }
2744 
2745 private void writeWS(size_t num) { writerep!' '(num); }
2746 private void writerep(char ch)(size_t num) { write(getRepString!ch(num)); }
2747 
2748 private string getRepString(char ch)(size_t len)
2749 {
2750 	static string buf;
2751 	if (len > buf.length) buf ~= [ch].replicate(len-buf.length);
2752 	return buf[0 .. len];
2753 }
2754 
2755 /***
2756 */
2757 
2758 
2759 private void enforceUsage(bool cond, string text)
2760 {
2761 	if (!cond) throw new UsageException(text);
2762 }
2763 
2764 private class UsageException : Exception {
2765 	this(string message, string file = __FILE__, int line = __LINE__, Throwable next = null)
2766 	{
2767 		super(message, file, line, next);
2768 	}
2769 }
2770 
2771 private bool addDependency(Dub dub, ref PackageRecipe recipe, string depspec)
2772 {
2773 	Dependency dep;
2774 	const parts = splitPackageName(depspec);
2775 	const depname = parts.name;
2776 	if (parts.version_)
2777 		dep = Dependency(parts.version_);
2778 	else
2779 	{
2780 		try {
2781 			const ver = dub.getLatestVersion(depname);
2782 			dep = ver.isBranch ? Dependency(ver) : Dependency("~>" ~ ver.toString());
2783 		} catch (Exception e) {
2784 			logError("Could not find package '%s'.", depname);
2785 			logDebug("Full error: %s", e.toString().sanitize);
2786 			return false;
2787 		}
2788 	}
2789 	recipe.buildSettings.dependencies[depname] = dep;
2790 	logInfo("Adding dependency %s %s", depname, dep.toString());
2791 	return true;
2792 }
2793 
2794 private struct PackageAndVersion
2795 {
2796 	string name;
2797 	string version_;
2798 }
2799 
2800 /* Split <package>=<version-specifier> and <package>@<version-specifier>
2801    into `name` and `version_`. */
2802 private PackageAndVersion splitPackageName(string packageName)
2803 {
2804 	// split <package>@<version-specifier>
2805 	auto parts = packageName.findSplit("@");
2806 	if (parts[1].empty) {
2807 		// split <package>=<version-specifier>
2808 		parts = packageName.findSplit("=");
2809 	}
2810 
2811 	PackageAndVersion p;
2812 	p.name = parts[0];
2813 	if (!parts[1].empty)
2814 		p.version_ = parts[2];
2815 	return p;
2816 }
2817 
2818 unittest
2819 {
2820 	// https://github.com/dlang/dub/issues/1681
2821 	assert(splitPackageName("") == PackageAndVersion("", null));
2822 
2823 	assert(splitPackageName("foo") == PackageAndVersion("foo", null));
2824 	assert(splitPackageName("foo=1.0.1") == PackageAndVersion("foo", "1.0.1"));
2825 	assert(splitPackageName("foo@1.0.1") == PackageAndVersion("foo", "1.0.1"));
2826 	assert(splitPackageName("foo@==1.0.1") == PackageAndVersion("foo", "==1.0.1"));
2827 	assert(splitPackageName("foo@>=1.0.1") == PackageAndVersion("foo", ">=1.0.1"));
2828 	assert(splitPackageName("foo@~>1.0.1") == PackageAndVersion("foo", "~>1.0.1"));
2829 	assert(splitPackageName("foo@<1.0.1") == PackageAndVersion("foo", "<1.0.1"));
2830 }
2831 
2832 private ulong canFindVersionSplitter(string packageName)
2833 {
2834 	// see splitPackageName
2835 	return packageName.canFind("@", "=");
2836 }
2837 
2838 unittest
2839 {
2840 	assert(!canFindVersionSplitter("foo"));
2841 	assert(canFindVersionSplitter("foo=1.0.1"));
2842 	assert(canFindVersionSplitter("foo@1.0.1"));
2843 	assert(canFindVersionSplitter("foo@==1.0.1"));
2844 	assert(canFindVersionSplitter("foo@>=1.0.1"));
2845 	assert(canFindVersionSplitter("foo@~>1.0.1"));
2846 	assert(canFindVersionSplitter("foo@<1.0.1"));
2847 }