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 dub.internal.dyaml.stdsumtype;
25 
26 import std.algorithm;
27 import std.array;
28 import std.conv;
29 import std.encoding;
30 import std.exception;
31 import std.file;
32 import std.getopt;
33 import std.path : absolutePath, buildNormalizedPath, expandTilde, setExtension;
34 import std.process : environment, spawnProcess, wait;
35 import std.stdio;
36 import std.string;
37 import std.typecons : Tuple, tuple;
38 import std.variant;
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), getWorkingDirectory());
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.",
885 			"By default, the current working directory is used.",
886 			"",
887 			"Custom templates can be defined by packages by providing a sub-package called \"init-exec\". No default source files are added in this case.",
888 			"The \"init-exec\" subpackage is compiled and executed inside the destination folder after the base project directory has been created.",
889 			"Free arguments \"dub init -t custom -- free args\" are passed into the \"init-exec\" subpackage as app arguments."
890 		];
891 		this.acceptsAppArgs = true;
892 	}
893 
894 	override void prepare(scope CommandArgs args)
895 	{
896 		args.getopt("t|type", &m_templateType, [
897 			"Set the type of project to generate. Available types:",
898 			"",
899 			"minimal - simple \"hello world\" project (default)",
900 			"vibe.d  - minimal HTTP server based on vibe.d",
901 			"deimos  - skeleton for C header bindings",
902 			"custom  - custom project provided by dub package",
903 		]);
904 		args.getopt("f|format", &m_format, [
905 			"Sets the format to use for the package description file. Possible values:",
906 			"  " ~ [__traits(allMembers, PackageFormat)].map!(f => f == m_format.init.to!string ? f ~ " (default)" : f).join(", ")
907 		]);
908 		args.getopt("n|non-interactive", &m_nonInteractive, ["Don't enter interactive mode."]);
909 	}
910 
911 	override int execute(Dub dub, string[] free_args, string[] app_args)
912 	{
913 		string dir;
914 		if (free_args.length)
915 		{
916 			dir = free_args[0];
917 			free_args = free_args[1 .. $];
918 		}
919 
920 		static string input(string caption, string default_value)
921 		{
922 			writef("%s [%s]: ", caption, default_value);
923 			stdout.flush();
924 			auto inp = readln();
925 			return inp.length > 1 ? inp[0 .. $-1] : default_value;
926 		}
927 
928 		void depCallback(ref PackageRecipe p, ref PackageFormat fmt) {
929 			import std.datetime: Clock;
930 
931 			if (m_nonInteractive) return;
932 
933 			while (true) {
934 				string rawfmt = input("Package recipe format (sdl/json)", fmt.to!string);
935 				if (!rawfmt.length) break;
936 				try {
937 					fmt = rawfmt.to!PackageFormat;
938 					break;
939 				} catch (Exception) {
940 					logError(`Invalid format '%s', enter either 'sdl' or 'json'.`, rawfmt);
941 				}
942 			}
943 			auto author = p.authors.join(", ");
944 			while (true) {
945 				// Tries getting the name until a valid one is given.
946 				import std.regex;
947 				auto nameRegex = regex(`^[a-z0-9\-_]+$`);
948 				string triedName = input("Name", p.name);
949 				if (triedName.matchFirst(nameRegex).empty) {
950 					logError(`Invalid name '%s', names should consist only of lowercase alphanumeric characters, dashes ('-') and underscores ('_').`, triedName);
951 				} else {
952 					p.name = triedName;
953 					break;
954 				}
955 			}
956 			p.description = input("Description", p.description);
957 			p.authors = input("Author name", author).split(",").map!(a => a.strip).array;
958 			p.license = input("License", p.license);
959 			string copyrightString = .format("Copyright © %s, %-(%s, %)", Clock.currTime().year, p.authors);
960 			p.copyright = input("Copyright string", copyrightString);
961 
962 			while (true) {
963 				auto depspec = input("Add dependency (leave empty to skip)", null);
964 				if (!depspec.length) break;
965 				addDependency(dub, p, depspec);
966 			}
967 		}
968 
969 		if (!["vibe.d", "deimos", "minimal"].canFind(m_templateType))
970 		{
971 			free_args ~= m_templateType;
972 		}
973 		dub.createEmptyPackage(NativePath(dir), free_args, m_templateType, m_format, &depCallback, app_args);
974 
975 		logInfo("Package successfully created in %s", dir.length ? dir : ".");
976 		return 0;
977 	}
978 }
979 
980 
981 /******************************************************************************/
982 /* GENERATE / BUILD / RUN / TEST / DESCRIBE                                   */
983 /******************************************************************************/
984 
985 abstract class PackageBuildCommand : Command {
986 	protected {
987 		string m_compilerName;
988 		string m_arch;
989 		string[] m_debugVersions;
990 		string[] m_overrideConfigs;
991 		GeneratorSettings baseSettings;
992 		string m_defaultConfig;
993 		bool m_nodeps;
994 		bool m_forceRemove = false;
995 	}
996 
997 	override void prepare(scope CommandArgs args)
998 	{
999 		args.getopt("b|build", &this.baseSettings.buildType, [
1000 			"Specifies the type of build to perform. Note that setting the DFLAGS environment variable will override the build type with custom flags.",
1001 			"Possible names:",
1002 			"  "~builtinBuildTypes.join(", ")~" and custom types"
1003 		]);
1004 		args.getopt("c|config", &this.baseSettings.config, [
1005 			"Builds the specified configuration. Configurations can be defined in dub.json"
1006 		]);
1007 		args.getopt("override-config", &m_overrideConfigs, [
1008 			"Uses the specified configuration for a certain dependency. Can be specified multiple times.",
1009 			"Format: --override-config=<dependency>/<config>"
1010 		]);
1011 		args.getopt("compiler", &m_compilerName, [
1012 			"Specifies the compiler binary to use (can be a path).",
1013 			"Arbitrary pre- and suffixes to the identifiers below are recognized (e.g. ldc2 or dmd-2.063) and matched to the proper compiler type:",
1014 			"  "~["dmd", "gdc", "ldc", "gdmd", "ldmd"].join(", ")
1015 		]);
1016 		args.getopt("a|arch", &m_arch, [
1017 			"Force a different architecture (e.g. x86 or x86_64)"
1018 		]);
1019 		args.getopt("d|debug", &m_debugVersions, [
1020 			"Define the specified debug version identifier when building - can be used multiple times"
1021 		]);
1022 		args.getopt("nodeps", &m_nodeps, [
1023 			"Do not resolve missing dependencies before building"
1024 		]);
1025 		args.getopt("build-mode", &this.baseSettings.buildMode, [
1026 			"Specifies the way the compiler and linker are invoked. Valid values:",
1027 			"  separate (default), allAtOnce, singleFile"
1028 		]);
1029 		args.getopt("single", &this.baseSettings.single, [
1030 			"Treats the package name as a filename. The file must contain a package recipe comment."
1031 		]);
1032 		args.getopt("force-remove", &m_forceRemove, [
1033 			"Deprecated option that does nothing."
1034 		]);
1035 		args.getopt("filter-versions", &this.baseSettings.filterVersions, [
1036 			"[Experimental] Filter version identifiers and debug version identifiers to improve build cache efficiency."
1037 		]);
1038 	}
1039 
1040 	protected void setupVersionPackage(Dub dub, string str_package_info, string default_build_type = "debug")
1041 	{
1042 		PackageAndVersion package_info = splitPackageName(str_package_info);
1043 		setupPackage(dub, package_info.name, default_build_type, package_info.version_);
1044 	}
1045 
1046 	protected void setupPackage(Dub dub, string package_name, string default_build_type = "debug", string ver = "")
1047 	{
1048 		if (!m_compilerName.length) m_compilerName = dub.defaultCompiler;
1049 		if (!m_arch.length) m_arch = dub.defaultArchitecture;
1050 		if (dub.defaultLowMemory) this.baseSettings.buildSettings.options |= BuildOption.lowmem;
1051 		if (dub.defaultEnvironments) this.baseSettings.buildSettings.addEnvironments(dub.defaultEnvironments);
1052 		if (dub.defaultBuildEnvironments) this.baseSettings.buildSettings.addBuildEnvironments(dub.defaultBuildEnvironments);
1053 		if (dub.defaultRunEnvironments) this.baseSettings.buildSettings.addRunEnvironments(dub.defaultRunEnvironments);
1054 		if (dub.defaultPreGenerateEnvironments) this.baseSettings.buildSettings.addPreGenerateEnvironments(dub.defaultPreGenerateEnvironments);
1055 		if (dub.defaultPostGenerateEnvironments) this.baseSettings.buildSettings.addPostGenerateEnvironments(dub.defaultPostGenerateEnvironments);
1056 		if (dub.defaultPreBuildEnvironments) this.baseSettings.buildSettings.addPreBuildEnvironments(dub.defaultPreBuildEnvironments);
1057 		if (dub.defaultPostBuildEnvironments) this.baseSettings.buildSettings.addPostBuildEnvironments(dub.defaultPostBuildEnvironments);
1058 		if (dub.defaultPreRunEnvironments) this.baseSettings.buildSettings.addPreRunEnvironments(dub.defaultPreRunEnvironments);
1059 		if (dub.defaultPostRunEnvironments) this.baseSettings.buildSettings.addPostRunEnvironments(dub.defaultPostRunEnvironments);
1060 		this.baseSettings.compiler = getCompiler(m_compilerName);
1061 		this.baseSettings.platform = this.baseSettings.compiler.determinePlatform(this.baseSettings.buildSettings, m_compilerName, m_arch);
1062 		this.baseSettings.buildSettings.addDebugVersions(m_debugVersions);
1063 
1064 		m_defaultConfig = null;
1065 		enforce (loadSpecificPackage(dub, package_name, ver), "Failed to load package.");
1066 
1067 		if (this.baseSettings.config.length != 0 &&
1068 			!dub.configurations.canFind(this.baseSettings.config) &&
1069 			this.baseSettings.config != "unittest")
1070 		{
1071 			string msg = "Unknown build configuration: " ~ this.baseSettings.config;
1072 			enum distance = 3;
1073 			auto match = dub.configurations.getClosestMatch(this.baseSettings.config, distance);
1074 			if (match !is null) msg ~= ". Did you mean '" ~ match ~ "'?";
1075 			enforce(0, msg);
1076 		}
1077 
1078 		if (this.baseSettings.buildType.length == 0) {
1079 			if (environment.get("DFLAGS") !is null) this.baseSettings.buildType = "$DFLAGS";
1080 			else this.baseSettings.buildType = default_build_type;
1081 		}
1082 
1083 		if (!m_nodeps) {
1084 			// retrieve missing packages
1085 			if (!dub.project.hasAllDependencies) {
1086 				logDiagnostic("Checking for missing dependencies.");
1087 				if (this.baseSettings.single)
1088 					dub.upgrade(UpgradeOptions.select | UpgradeOptions.noSaveSelections);
1089 				else dub.upgrade(UpgradeOptions.select);
1090 			}
1091 		}
1092 
1093 		dub.project.validate();
1094 
1095 		foreach (sc; m_overrideConfigs) {
1096 			auto idx = sc.indexOf('/');
1097 			enforceUsage(idx >= 0, "Expected \"<package>/<configuration>\" as argument to --override-config.");
1098 			dub.project.overrideConfiguration(sc[0 .. idx], sc[idx+1 .. $]);
1099 		}
1100 	}
1101 
1102 	private bool loadSpecificPackage(Dub dub, string package_name, string ver)
1103 	{
1104 		if (this.baseSettings.single) {
1105 			enforce(package_name.length, "Missing file name of single-file package.");
1106 			dub.loadSingleFilePackage(package_name);
1107 			return true;
1108 		}
1109 
1110 		bool from_cwd = package_name.length == 0 || package_name.startsWith(":");
1111 		// load package in root_path to enable searching for sub packages
1112 		if (loadCwdPackage(dub, from_cwd)) {
1113 			if (package_name.startsWith(":"))
1114 			{
1115 				auto pack = dub.packageManager.getSubPackage(dub.project.rootPackage, package_name[1 .. $], false);
1116 				dub.loadPackage(pack);
1117 				return true;
1118 			}
1119 			if (from_cwd) return true;
1120 		}
1121 
1122 		enforce(package_name.length, "No valid root package found - aborting.");
1123 
1124 		const vers = ver.length ? VersionRange.fromString(ver) : VersionRange.Any;
1125 		auto pack = dub.packageManager.getBestPackage(package_name, vers);
1126 
1127 		enforce(pack, format!"Failed to find a package named '%s%s' locally."(package_name,
1128 			ver == "" ? "" : ("@" ~ ver)
1129 		));
1130 		logInfo("Building package %s in %s", pack.name, pack.path.toNativeString());
1131 		dub.loadPackage(pack);
1132 		return true;
1133 	}
1134 }
1135 
1136 class GenerateCommand : PackageBuildCommand {
1137 	protected {
1138 		string m_generator;
1139 		bool m_printPlatform, m_printBuilds, m_printConfigs;
1140 	}
1141 
1142 	this() @safe pure nothrow
1143 	{
1144 		this.name = "generate";
1145 		this.argumentsPattern = "<generator> [<package>[@<version-spec>]]";
1146 		this.description = "Generates project files using the specified generator";
1147 		this.helpText = [
1148 			"Generates project files using one of the supported generators:",
1149 			"",
1150 			"visuald - VisualD project files",
1151 			"sublimetext - SublimeText project file",
1152 			"cmake - CMake build scripts",
1153 			"build - Builds the package directly",
1154 			"",
1155 			"An optional package name can be given to generate a different package than the root/CWD package."
1156 		];
1157 	}
1158 
1159 	override void prepare(scope CommandArgs args)
1160 	{
1161 		super.prepare(args);
1162 
1163 		args.getopt("combined", &this.baseSettings.combined, [
1164 			"Tries to build the whole project in a single compiler run."
1165 		]);
1166 
1167 		args.getopt("print-builds", &m_printBuilds, [
1168 			"Prints the list of available build types"
1169 		]);
1170 		args.getopt("print-configs", &m_printConfigs, [
1171 			"Prints the list of available configurations"
1172 		]);
1173 		args.getopt("print-platform", &m_printPlatform, [
1174 			"Prints the identifiers for the current build platform as used for the build fields in dub.json"
1175 		]);
1176 		args.getopt("parallel", &this.baseSettings.parallelBuild, [
1177 			"Runs multiple compiler instances in parallel, if possible."
1178 		]);
1179 	}
1180 
1181 	override int execute(Dub dub, string[] free_args, string[] app_args)
1182 	{
1183 		string str_package_info;
1184 		if (!m_generator.length) {
1185 			enforceUsage(free_args.length >= 1 && free_args.length <= 2, "Expected one or two arguments.");
1186 			m_generator = free_args[0];
1187 			if (free_args.length >= 2) str_package_info = free_args[1];
1188 		} else {
1189 			enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1190 			if (free_args.length >= 1) str_package_info = free_args[0];
1191 		}
1192 
1193 		setupVersionPackage(dub, str_package_info, "debug");
1194 
1195 		if (m_printBuilds) {
1196 			logInfo("Available build types:");
1197 			foreach (i, tp; dub.project.builds)
1198 				logInfo("  %s%s", tp, i == 0 ? " [default]" : null);
1199 			logInfo("");
1200 		}
1201 
1202 		m_defaultConfig = dub.project.getDefaultConfiguration(this.baseSettings.platform);
1203 		if (m_printConfigs) {
1204 			logInfo("Available configurations:");
1205 			foreach (tp; dub.configurations)
1206 				logInfo("  %s%s", tp, tp == m_defaultConfig ? " [default]" : null);
1207 			logInfo("");
1208 		}
1209 
1210 		GeneratorSettings gensettings = this.baseSettings;
1211 		if (!gensettings.config.length)
1212 			gensettings.config = m_defaultConfig;
1213 		gensettings.runArgs = app_args;
1214 
1215 		logDiagnostic("Generating using %s", m_generator);
1216 		dub.generateProject(m_generator, gensettings);
1217 		if (this.baseSettings.buildType == "ddox") dub.runDdox(gensettings.run, app_args);
1218 		return 0;
1219 	}
1220 }
1221 
1222 class BuildCommand : GenerateCommand {
1223 	protected {
1224 		bool m_yes; // automatic yes to prompts;
1225 		bool m_nonInteractive;
1226 	}
1227 	this() @safe pure nothrow
1228 	{
1229 		this.name = "build";
1230 		this.argumentsPattern = "[<package>[@<version-spec>]]";
1231 		this.description = "Builds a package (uses the main package in the current working directory by default)";
1232 		this.helpText = [
1233 			"Builds a package (uses the main package in the current working directory by default)"
1234 		];
1235 	}
1236 
1237 	override void prepare(scope CommandArgs args)
1238 	{
1239 		args.getopt("temp-build", &this.baseSettings.tempBuild, [
1240 			"Builds the project in the temp folder if possible."
1241 		]);
1242 
1243 		args.getopt("rdmd", &this.baseSettings.rdmd, [
1244 			"Use rdmd instead of directly invoking the compiler"
1245 		]);
1246 
1247 		args.getopt("f|force", &this.baseSettings.force, [
1248 			"Forces a recompilation even if the target is up to date"
1249 		]);
1250 		args.getopt("y|yes", &m_yes, [
1251 			`Automatic yes to prompts. Assume "yes" as answer to all interactive prompts.`
1252 		]);
1253 		args.getopt("n|non-interactive", &m_nonInteractive, [
1254 			"Don't enter interactive mode."
1255 		]);
1256 		super.prepare(args);
1257 		m_generator = "build";
1258 	}
1259 
1260 	override int execute(Dub dub, string[] free_args, string[] app_args)
1261 	{
1262 		// single package files don't need to be downloaded, they are on the disk.
1263 		if (free_args.length < 1 || this.baseSettings.single)
1264 			return super.execute(dub, free_args, app_args);
1265 
1266 		if (!m_nonInteractive)
1267 		{
1268 			const packageParts = splitPackageName(free_args[0]);
1269 			if (auto rc = fetchMissingPackages(dub, packageParts))
1270 				return rc;
1271 		}
1272 		return super.execute(dub, free_args, app_args);
1273 	}
1274 
1275 	private int fetchMissingPackages(Dub dub, in PackageAndVersion packageParts)
1276 	{
1277 
1278 		static bool input(string caption, bool default_value = true) {
1279 			writef("%s [%s]: ", caption, default_value ? "Y/n" : "y/N");
1280 			auto inp = readln();
1281 			string userInput = "y";
1282 			if (inp.length > 1)
1283 				userInput = inp[0 .. $ - 1].toLower;
1284 
1285 			switch (userInput) {
1286 				case "no", "n", "0":
1287 					return false;
1288 				case "yes", "y", "1":
1289 				default:
1290 					return true;
1291 			}
1292 		}
1293 
1294 		VersionRange dep;
1295 
1296 		if (packageParts.version_.length > 0) {
1297 			// the user provided a version manually
1298 			dep = VersionRange.fromString(packageParts.version_);
1299 		} else if (packageParts.name.startsWith(":")) {
1300 			// Subpackages are always assumed to be present
1301 			return 0;
1302 		} else if (dub.packageManager.getBestPackage(packageParts.name)) {
1303 			// found locally
1304 			return 0;
1305 		} else {
1306 			// search for the package and filter versions for exact matches
1307 			auto basePackageName = getBasePackageName(packageParts.name);
1308 			auto search = dub.searchPackages(basePackageName)
1309 				.map!(tup => tup[1].find!(p => p.name == basePackageName))
1310 				.filter!(ps => !ps.empty);
1311 			if (search.empty) {
1312 				logWarn("Package '%s' was neither found locally nor online.", packageParts.name);
1313 				return 2;
1314 			}
1315 
1316 			const p = search.front.front;
1317 			logInfo("Package '%s' was not found locally but is available online:", packageParts.name);
1318 			logInfo("---");
1319 			logInfo("Description: %s", p.description);
1320 			logInfo("Version: %s", p.version_);
1321 			logInfo("---");
1322 
1323 			const answer = m_yes ? true : input("Do you want to fetch '%s' now?".format(packageParts.name));
1324 			if (!answer)
1325 				return 0;
1326 			dep = VersionRange.fromString(p.version_);
1327 		}
1328 
1329 		dub.fetch(packageParts.name, dep, dub.defaultPlacementLocation, FetchOptions.none);
1330 		return 0;
1331 	}
1332 }
1333 
1334 class RunCommand : BuildCommand {
1335 	this() @safe pure nothrow
1336 	{
1337 		this.name = "run";
1338 		this.argumentsPattern = "[<package>[@<version-spec>]]";
1339 		this.description = "Builds and runs a package (default command)";
1340 		this.helpText = [
1341 			"Builds and runs a package (uses the main package in the current working directory by default)"
1342 		];
1343 		this.acceptsAppArgs = true;
1344 	}
1345 
1346 	override void prepare(scope CommandArgs args)
1347 	{
1348 		super.prepare(args);
1349 		this.baseSettings.run = true;
1350 	}
1351 
1352 	override int execute(Dub dub, string[] free_args, string[] app_args)
1353 	{
1354 		return super.execute(dub, free_args, app_args);
1355 	}
1356 }
1357 
1358 class TestCommand : PackageBuildCommand {
1359 	private {
1360 		string m_mainFile;
1361 	}
1362 
1363 	this() @safe pure nothrow
1364 	{
1365 		this.name = "test";
1366 		this.argumentsPattern = "[<package>[@<version-spec>]]";
1367 		this.description = "Executes the tests of the selected package";
1368 		this.helpText = [
1369 			`Builds the package and executes all contained unit tests.`,
1370 			``,
1371 			`If no explicit configuration is given, an existing "unittest" ` ~
1372 			`configuration will be preferred for testing. If none exists, the ` ~
1373 			`first library type configuration will be used, and if that doesn't ` ~
1374 			`exist either, the first executable configuration is chosen.`,
1375 			``,
1376 			`When a custom main file (--main-file) is specified, only library ` ~
1377 			`configurations can be used. Otherwise, depending on the type of ` ~
1378 			`the selected configuration, either an existing main file will be ` ~
1379 			`used (and needs to be properly adjusted to just run the unit ` ~
1380 			`tests for 'version(unittest)'), or DUB will generate one for ` ~
1381 			`library type configurations.`,
1382 			``,
1383 			`Finally, if the package contains a dependency to the "tested" ` ~
1384 			`package, the automatically generated main file will use it to ` ~
1385 			`run the unit tests.`
1386 		];
1387 		this.acceptsAppArgs = true;
1388 	}
1389 
1390 	override void prepare(scope CommandArgs args)
1391 	{
1392 		args.getopt("temp-build", &this.baseSettings.tempBuild, [
1393 			"Builds the project in the temp folder if possible."
1394 		]);
1395 
1396 		args.getopt("main-file", &m_mainFile, [
1397 			"Specifies a custom file containing the main() function to use for running the tests."
1398 		]);
1399 		args.getopt("combined", &this.baseSettings.combined, [
1400 			"Tries to build the whole project in a single compiler run."
1401 		]);
1402 		args.getopt("parallel", &this.baseSettings.parallelBuild, [
1403 			"Runs multiple compiler instances in parallel, if possible."
1404 		]);
1405 		args.getopt("f|force", &this.baseSettings.force, [
1406 			"Forces a recompilation even if the target is up to date"
1407 		]);
1408 
1409 		bool coverage = false;
1410 		args.getopt("coverage", &coverage, [
1411 			"Enables code coverage statistics to be generated."
1412 		]);
1413 		if (coverage) this.baseSettings.buildType = "unittest-cov";
1414 
1415 		bool coverageCTFE = false;
1416 		args.getopt("coverage-ctfe", &coverageCTFE, [
1417 			"Enables code coverage (including CTFE) statistics to be generated."
1418 		]);
1419 		if (coverageCTFE) this.baseSettings.buildType = "unittest-cov-ctfe";
1420 
1421 		super.prepare(args);
1422 	}
1423 
1424 	override int execute(Dub dub, string[] free_args, string[] app_args)
1425 	{
1426 		string str_package_info;
1427 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1428 		if (free_args.length >= 1) str_package_info = free_args[0];
1429 
1430 		setupVersionPackage(dub, str_package_info, "unittest");
1431 
1432 		GeneratorSettings settings = this.baseSettings;
1433 		settings.compiler = getCompiler(this.baseSettings.platform.compilerBinary);
1434 		settings.run = true;
1435 		settings.runArgs = app_args;
1436 
1437 		dub.testProject(settings, this.baseSettings.config, NativePath(m_mainFile));
1438 		return 0;
1439 	}
1440 }
1441 
1442 class LintCommand : PackageBuildCommand {
1443 	private {
1444 		bool m_syntaxCheck = false;
1445 		bool m_styleCheck = false;
1446 		string m_errorFormat;
1447 		bool m_report = false;
1448 		string m_reportFormat;
1449 		string m_reportFile;
1450 		string[] m_importPaths;
1451 		string m_config;
1452 	}
1453 
1454 	this() @safe pure nothrow
1455 	{
1456 		this.name = "lint";
1457 		this.argumentsPattern = "[<package>[@<version-spec>]]";
1458 		this.description = "Executes the linter tests of the selected package";
1459 		this.helpText = [
1460 			`Builds the package and executes D-Scanner linter tests.`
1461 		];
1462 		this.acceptsAppArgs = true;
1463 	}
1464 
1465 	override void prepare(scope CommandArgs args)
1466 	{
1467 		args.getopt("syntax-check", &m_syntaxCheck, [
1468 			"Lexes and parses sourceFile, printing the line and column number of " ~
1469 			"any syntax errors to stdout."
1470 		]);
1471 
1472 		args.getopt("style-check", &m_styleCheck, [
1473 			"Lexes and parses sourceFiles, printing the line and column number of " ~
1474 			"any static analysis check failures stdout."
1475 		]);
1476 
1477 		args.getopt("error-format", &m_errorFormat, [
1478 			"Format errors produced by the style/syntax checkers."
1479 		]);
1480 
1481 		args.getopt("report", &m_report, [
1482 			"Generate a static analysis report in JSON format."
1483 		]);
1484 
1485 		args.getopt("report-format", &m_reportFormat, [
1486 			"Specifies the format of the generated report."
1487 		]);
1488 
1489 		args.getopt("report-file", &m_reportFile, [
1490 			"Write report to file."
1491 		]);
1492 
1493 		if (m_reportFormat || m_reportFile) m_report = true;
1494 
1495 		args.getopt("import-paths", &m_importPaths, [
1496 			"Import paths"
1497 		]);
1498 
1499 		args.getopt("dscanner-config", &m_config, [
1500 			"Use the given d-scanner configuration file."
1501 		]);
1502 
1503 		super.prepare(args);
1504 	}
1505 
1506 	override int execute(Dub dub, string[] free_args, string[] app_args)
1507 	{
1508 		string str_package_info;
1509 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1510 		if (free_args.length >= 1) str_package_info = free_args[0];
1511 
1512 		string[] args;
1513 		if (!m_syntaxCheck && !m_styleCheck && !m_report && app_args.length == 0) { m_styleCheck = true; }
1514 
1515 		if (m_syntaxCheck) args ~= "--syntaxCheck";
1516 		if (m_styleCheck) args ~= "--styleCheck";
1517 		if (m_errorFormat) args ~= ["--errorFormat", m_errorFormat];
1518 		if (m_report) args ~= "--report";
1519 		if (m_reportFormat) args ~= ["--reportFormat", m_reportFormat];
1520 		if (m_reportFile) args ~= ["--reportFile", m_reportFile];
1521 		foreach (import_path; m_importPaths) args ~= ["-I", import_path];
1522 		if (m_config) args ~= ["--config", m_config];
1523 
1524 		setupVersionPackage(dub, str_package_info);
1525 		dub.lintProject(args ~ app_args);
1526 		return 0;
1527 	}
1528 }
1529 
1530 class DescribeCommand : PackageBuildCommand {
1531 	private {
1532 		bool m_importPaths = false;
1533 		bool m_stringImportPaths = false;
1534 		bool m_dataList = false;
1535 		bool m_dataNullDelim = false;
1536 		string[] m_data;
1537 	}
1538 
1539 	this() @safe pure nothrow
1540 	{
1541 		this.name = "describe";
1542 		this.argumentsPattern = "[<package>[@<version-spec>]]";
1543 		this.description = "Prints a JSON description of the project and its dependencies";
1544 		this.helpText = [
1545 			"Prints a JSON build description for the root package an all of " ~
1546 			"their dependencies in a format similar to a JSON package " ~
1547 			"description file. This is useful mostly for IDEs.",
1548 			"",
1549 			"All usual options that are also used for build/run/generate apply.",
1550 			"",
1551 			"When --data=VALUE is supplied, specific build settings for a project " ~
1552 			"will be printed instead (by default, formatted for the current compiler).",
1553 			"",
1554 			"The --data=VALUE option can be specified multiple times to retrieve " ~
1555 			"several pieces of information at once. A comma-separated list is " ~
1556 			"also acceptable (ex: --data=dflags,libs). The data will be output in " ~
1557 			"the same order requested on the command line.",
1558 			"",
1559 			"The accepted values for --data=VALUE are:",
1560 			"",
1561 			"main-source-file, dflags, lflags, libs, linker-files, " ~
1562 			"source-files, versions, debug-versions, import-paths, " ~
1563 			"string-import-paths, import-files, options",
1564 			"",
1565 			"The following are also accepted by --data if --data-list is used:",
1566 			"",
1567 			"target-type, target-path, target-name, working-directory, " ~
1568 			"copy-files, string-import-files, pre-generate-commands, " ~
1569 			"post-generate-commands, pre-build-commands, post-build-commands, " ~
1570 			"pre-run-commands, post-run-commands, requirements",
1571 		];
1572 	}
1573 
1574 	override void prepare(scope CommandArgs args)
1575 	{
1576 		super.prepare(args);
1577 
1578 		args.getopt("import-paths", &m_importPaths, [
1579 			"Shortcut for --data=import-paths --data-list"
1580 		]);
1581 
1582 		args.getopt("string-import-paths", &m_stringImportPaths, [
1583 			"Shortcut for --data=string-import-paths --data-list"
1584 		]);
1585 
1586 		args.getopt("data", &m_data, [
1587 			"Just list the values of a particular build setting, either for this "~
1588 			"package alone or recursively including all dependencies. Accepts a "~
1589 			"comma-separated list. See above for more details and accepted "~
1590 			"possibilities for VALUE."
1591 		]);
1592 
1593 		args.getopt("data-list", &m_dataList, [
1594 			"Output --data information in list format (line-by-line), instead "~
1595 			"of formatting for a compiler command line.",
1596 		]);
1597 
1598 		args.getopt("data-0", &m_dataNullDelim, [
1599 			"Output --data information using null-delimiters, rather than "~
1600 			"spaces or newlines. Result is usable with, ex., xargs -0.",
1601 		]);
1602 	}
1603 
1604 	override int execute(Dub dub, string[] free_args, string[] app_args)
1605 	{
1606 		enforceUsage(
1607 			!(m_importPaths && m_stringImportPaths),
1608 			"--import-paths and --string-import-paths may not be used together."
1609 		);
1610 
1611 		enforceUsage(
1612 			!(m_data && (m_importPaths || m_stringImportPaths)),
1613 			"--data may not be used together with --import-paths or --string-import-paths."
1614 		);
1615 
1616 		// disable all log output to stdout and use "writeln" to output the JSON description
1617 		auto ll = getLogLevel();
1618 		setLogLevel(max(ll, LogLevel.warn));
1619 		scope (exit) setLogLevel(ll);
1620 
1621 		string str_package_info;
1622 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1623 		if (free_args.length >= 1) str_package_info = free_args[0];
1624 		setupVersionPackage(dub, str_package_info);
1625 
1626 		m_defaultConfig = dub.project.getDefaultConfiguration(this.baseSettings.platform);
1627 
1628 		GeneratorSettings settings = this.baseSettings;
1629 		if (!settings.config.length)
1630 			settings.config = m_defaultConfig;
1631 		settings.cache = dub.cachePathDontUse(); // See function's description
1632 		// Ignore other options
1633 		settings.buildSettings.options = this.baseSettings.buildSettings.options & BuildOption.lowmem;
1634 
1635 		// With a requested `unittest` config, switch to the special test runner
1636 		// config (which doesn't require an existing `unittest` configuration).
1637 		if (this.baseSettings.config == "unittest") {
1638 			const test_config = dub.project.addTestRunnerConfiguration(settings, !dub.dryRun);
1639 			if (test_config) settings.config = test_config;
1640 		}
1641 
1642 		if (m_importPaths) { m_data = ["import-paths"]; m_dataList = true; }
1643 		else if (m_stringImportPaths) { m_data = ["string-import-paths"]; m_dataList = true; }
1644 
1645 		if (m_data.length) {
1646 			ListBuildSettingsFormat lt;
1647 			with (ListBuildSettingsFormat)
1648 				lt = m_dataList ? (m_dataNullDelim ? listNul : list) : (m_dataNullDelim ? commandLineNul : commandLine);
1649 			dub.listProjectData(settings, m_data, lt);
1650 		} else {
1651 			auto desc = dub.project.describe(settings);
1652 			writeln(desc.serializeToPrettyJson());
1653 		}
1654 
1655 		return 0;
1656 	}
1657 }
1658 
1659 class CleanCommand : Command {
1660 	private {
1661 		bool m_allPackages;
1662 	}
1663 
1664 	this() @safe pure nothrow
1665 	{
1666 		this.name = "clean";
1667 		this.argumentsPattern = "[<package>]";
1668 		this.description = "Removes intermediate build files and cached build results";
1669 		this.helpText = [
1670 			"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.",
1671 			"Without arguments, the package in the current working directory will be cleaned."
1672 		];
1673 	}
1674 
1675 	override void prepare(scope CommandArgs args)
1676 	{
1677 		args.getopt("all-packages", &m_allPackages, [
1678 			"Cleans up *all* known packages (dub list)"
1679 		]);
1680 	}
1681 
1682 	override int execute(Dub dub, string[] free_args, string[] app_args)
1683 	{
1684 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1685 		enforceUsage(app_args.length == 0, "Application arguments are not supported for the clean command.");
1686 		enforceUsage(!m_allPackages || !free_args.length, "The --all-packages flag may not be used together with an explicit package name.");
1687 
1688 		enforce(free_args.length == 0, "Cleaning a specific package isn't possible right now.");
1689 
1690 		if (m_allPackages) {
1691 			dub.clean();
1692 		} else {
1693 			dub.loadPackage();
1694 			dub.clean(dub.project.rootPackage);
1695 		}
1696 
1697 		return 0;
1698 	}
1699 }
1700 
1701 
1702 /******************************************************************************/
1703 /* FETCH / ADD / REMOVE / UPGRADE                                             */
1704 /******************************************************************************/
1705 
1706 class AddCommand : Command {
1707 	this() @safe pure nothrow
1708 	{
1709 		this.name = "add";
1710 		this.argumentsPattern = "<package>[@<version-spec>] [<packages...>]";
1711 		this.description = "Adds dependencies to the package file.";
1712 		this.helpText = [
1713 			"Adds <packages> as dependencies.",
1714 			"",
1715 			"Running \"dub add <package>\" is the same as adding <package> to the \"dependencies\" section in dub.json/dub.sdl.",
1716 			"If no version is specified for one of the packages, dub will query the registry for the latest version."
1717 		];
1718 	}
1719 
1720 	override void prepare(scope CommandArgs args) {}
1721 
1722 	override int execute(Dub dub, string[] free_args, string[] app_args)
1723 	{
1724 		import dub.recipe.io : readPackageRecipe, writePackageRecipe;
1725 		import dub.internal.vibecompat.core.file : existsFile;
1726 		enforceUsage(free_args.length != 0, "Expected one or more arguments.");
1727 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1728 
1729 		if (!loadCwdPackage(dub, true)) return 2;
1730 		auto recipe = dub.project.rootPackage.rawRecipe.clone;
1731 
1732 		foreach (depspec; free_args) {
1733 			if (!addDependency(dub, recipe, depspec))
1734 				return 2;
1735 		}
1736 		writePackageRecipe(dub.project.rootPackage.recipePath, recipe);
1737 
1738 		return 0;
1739 	}
1740 }
1741 
1742 class UpgradeCommand : Command {
1743 	private {
1744 		bool m_prerelease = false;
1745 		bool m_includeSubPackages = false;
1746 		bool m_forceRemove = false;
1747 		bool m_missingOnly = false;
1748 		bool m_verify = false;
1749 		bool m_dryRun = false;
1750 	}
1751 
1752 	this() @safe pure nothrow
1753 	{
1754 		this.name = "upgrade";
1755 		this.argumentsPattern = "[<packages...>]";
1756 		this.description = "Forces an upgrade of the dependencies";
1757 		this.helpText = [
1758 			"Upgrades all dependencies of the package by querying the package registry(ies) for new versions.",
1759 			"",
1760 			"This will update the versions stored in the selections file ("~SelectedVersions.defaultFile~") accordingly.",
1761 			"",
1762 			"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."
1763 		];
1764 	}
1765 
1766 	override void prepare(scope CommandArgs args)
1767 	{
1768 		args.getopt("prerelease", &m_prerelease, [
1769 			"Uses the latest pre-release version, even if release versions are available"
1770 		]);
1771 		args.getopt("s|sub-packages", &m_includeSubPackages, [
1772 			"Also upgrades dependencies of all directory based sub packages"
1773 		]);
1774 		args.getopt("verify", &m_verify, [
1775 			"Updates the project and performs a build. If successful, rewrites the selected versions file <to be implemented>."
1776 		]);
1777 		args.getopt("dry-run", &m_dryRun, [
1778 			"Only print what would be upgraded, but don't actually upgrade anything."
1779 		]);
1780 		args.getopt("missing-only", &m_missingOnly, [
1781 			"Performs an upgrade only for dependencies that don't yet have a version selected. This is also done automatically before each build."
1782 		]);
1783 		args.getopt("force-remove", &m_forceRemove, [
1784 			"Deprecated option that does nothing."
1785 		]);
1786 	}
1787 
1788 	override int execute(Dub dub, string[] free_args, string[] app_args)
1789 	{
1790 		enforceUsage(free_args.length <= 1, "Unexpected arguments.");
1791 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1792 		enforceUsage(!m_verify, "--verify is not yet implemented.");
1793 		enforce(loadCwdPackage(dub, true), "Failed to load package.");
1794 		logInfo("Upgrading", Color.cyan, "project in %s", dub.projectPath.toNativeString().color(Mode.bold));
1795 		auto options = UpgradeOptions.upgrade|UpgradeOptions.select;
1796 		if (m_missingOnly) options &= ~UpgradeOptions.upgrade;
1797 		if (m_prerelease) options |= UpgradeOptions.preRelease;
1798 		if (m_dryRun) options |= UpgradeOptions.dryRun;
1799 		dub.upgrade(options, free_args);
1800 
1801 		auto spacks = dub.project.rootPackage
1802 			.subPackages
1803 			.filter!(sp => sp.path.length);
1804 
1805 		if (m_includeSubPackages) {
1806 			bool any_error = false;
1807 
1808 			// Go through each path based sub package, load it as a new instance
1809 			// and perform an upgrade as if the upgrade had been run from within
1810 			// the sub package folder. Note that we have to use separate Dub
1811 			// instances, because the upgrade always works on the root package
1812 			// of a project, which in this case are the individual sub packages.
1813 			foreach (sp; spacks) {
1814 				try {
1815 					auto fullpath = (dub.projectPath ~ sp.path).toNativeString();
1816 					logInfo("Upgrading", Color.cyan, "sub package in %s", fullpath);
1817 					auto sdub = new Dub(fullpath, dub.packageSuppliers, SkipPackageSuppliers.all);
1818 					sdub.defaultPlacementLocation = dub.defaultPlacementLocation;
1819 					sdub.loadPackage();
1820 					sdub.upgrade(options, free_args);
1821 				} catch (Exception e) {
1822 					logError("Failed to update sub package at %s: %s",
1823 						sp.path, e.msg);
1824 					any_error = true;
1825 				}
1826 			}
1827 
1828 			if (any_error) return 1;
1829 		} else if (!spacks.empty) {
1830 			foreach (sp; spacks)
1831 				logInfo("Not upgrading sub package in %s", sp.path);
1832 			logInfo("\nNote: specify -s to also upgrade sub packages.");
1833 		}
1834 
1835 		return 0;
1836 	}
1837 }
1838 
1839 class FetchRemoveCommand : Command {
1840 	protected {
1841 		string m_version;
1842 		bool m_forceRemove = false;
1843 	}
1844 
1845 	override void prepare(scope CommandArgs args)
1846 	{
1847 		args.getopt("version", &m_version, [
1848 			"Use the specified version/branch instead of the latest available match",
1849 			"The remove command also accepts \"*\" here as a wildcard to remove all versions of the package from the specified location"
1850 		], true); // hide --version from help
1851 
1852 		args.getopt("force-remove", &m_forceRemove, [
1853 			"Deprecated option that does nothing"
1854 		]);
1855 	}
1856 
1857 	abstract override int execute(Dub dub, string[] free_args, string[] app_args);
1858 }
1859 
1860 class FetchCommand : FetchRemoveCommand {
1861 	this() @safe pure nothrow
1862 	{
1863 		this.name = "fetch";
1864 		this.argumentsPattern = "<package>[@<version-spec>]";
1865 		this.description = "Manually retrieves and caches a package";
1866 		this.helpText = [
1867 			"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.",
1868 			"",
1869 			"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.",
1870 			"",
1871 			"Without specified options, placement/removal will default to a user wide shared location.",
1872 			"",
1873 			"Complete applications can be retrieved and run easily by e.g.",
1874 			"$ dub fetch vibelog --cache=local",
1875 			"$ dub run vibelog --cache=local",
1876 			"",
1877 			"This will grab all needed dependencies and compile and run the application.",
1878 		];
1879 	}
1880 
1881 	override void prepare(scope CommandArgs args)
1882 	{
1883 		super.prepare(args);
1884 	}
1885 
1886 	override int execute(Dub dub, string[] free_args, string[] app_args)
1887 	{
1888 		enforceUsage(free_args.length == 1, "Expecting exactly one argument.");
1889 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1890 
1891 		auto location = dub.defaultPlacementLocation;
1892 
1893 		auto name = free_args[0];
1894 
1895 		FetchOptions fetchOpts;
1896 		fetchOpts |= FetchOptions.forceBranchUpgrade;
1897 		if (m_version.length) { // remove then --version removed
1898 			enforceUsage(!name.canFindVersionSplitter, "Double version spec not allowed.");
1899 			logWarn("The '--version' parameter was deprecated, use %s@%s. Please update your scripts.", name, m_version);
1900 			dub.fetch(name, VersionRange.fromString(m_version), location, fetchOpts);
1901 		} else if (name.canFindVersionSplitter) {
1902 			const parts = name.splitPackageName;
1903 			dub.fetch(parts.name, VersionRange.fromString(parts.version_), location, fetchOpts);
1904 		} else {
1905 			try {
1906 				dub.fetch(name, VersionRange.Any, location, fetchOpts);
1907 				logInfo("Finished", Color.green, "%s fetched", name.color(Mode.bold));
1908 				logInfo(
1909 					"Please note that you need to use `dub run <pkgname>` " ~
1910 					"or add it to dependencies of your package to actually use/run it. "
1911 				);
1912 			}
1913 			catch(Exception e){
1914 				logInfo("Getting a release version failed: %s", e.msg);
1915 				logInfo("Retry with ~master...");
1916 				dub.fetch(name, VersionRange.fromString("~master"), location, fetchOpts);
1917 			}
1918 		}
1919 		return 0;
1920 	}
1921 }
1922 
1923 class RemoveCommand : FetchRemoveCommand {
1924 	private {
1925 		bool m_nonInteractive;
1926 	}
1927 
1928 	this() @safe pure nothrow
1929 	{
1930 		this.name = "remove";
1931 		this.argumentsPattern = "<package>[@<version-spec>]";
1932 		this.description = "Removes a cached package";
1933 		this.helpText = [
1934 			"Removes a package that is cached on the local system."
1935 		];
1936 	}
1937 
1938 	override void prepare(scope CommandArgs args)
1939 	{
1940 		super.prepare(args);
1941 		args.getopt("n|non-interactive", &m_nonInteractive, ["Don't enter interactive mode."]);
1942 	}
1943 
1944 	override int execute(Dub dub, string[] free_args, string[] app_args)
1945 	{
1946 		enforceUsage(free_args.length == 1, "Expecting exactly one argument.");
1947 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1948 
1949 		auto package_id = free_args[0];
1950 		auto location = dub.defaultPlacementLocation;
1951 
1952 		size_t resolveVersion(in Package[] packages) {
1953 			// just remove only package version
1954 			if (packages.length == 1)
1955 				return 0;
1956 
1957 			writeln("Select version of '", package_id, "' to remove from location '", location, "':");
1958 			foreach (i, pack; packages)
1959 				writefln("%s) %s", i + 1, pack.version_);
1960 			writeln(packages.length + 1, ") ", "all versions");
1961 			while (true) {
1962 				writef("> ");
1963 				auto inp = readln();
1964 				if (!inp.length) // Ctrl+D
1965 					return size_t.max;
1966 				inp = inp.stripRight;
1967 				if (!inp.length) // newline or space
1968 					continue;
1969 				try {
1970 					immutable selection = inp.to!size_t - 1;
1971 					if (selection <= packages.length)
1972 						return selection;
1973 				} catch (ConvException e) {
1974 				}
1975 				logError("Please enter a number between 1 and %s.", packages.length + 1);
1976 			}
1977 		}
1978 
1979 		if (!m_version.empty) { // remove then --version removed
1980 			enforceUsage(!package_id.canFindVersionSplitter, "Double version spec not allowed.");
1981 			logWarn("The '--version' parameter was deprecated, use %s@%s. Please update your scripts.", package_id, m_version);
1982 			dub.remove(package_id, m_version, location);
1983 		} else {
1984 			const parts = package_id.splitPackageName;
1985 			if (m_nonInteractive || parts.version_.length) {
1986 				dub.remove(parts.name, parts.version_, location);
1987 			} else {
1988 				dub.remove(package_id, location, &resolveVersion);
1989 			}
1990 		}
1991 		return 0;
1992 	}
1993 }
1994 
1995 /******************************************************************************/
1996 /* ADD/REMOVE PATH/LOCAL                                                      */
1997 /******************************************************************************/
1998 
1999 abstract class RegistrationCommand : Command {
2000 	private {
2001 		bool m_system;
2002 	}
2003 
2004 	override void prepare(scope CommandArgs args)
2005 	{
2006 		args.getopt("system", &m_system, [
2007 			"Register system-wide instead of user-wide"
2008 		]);
2009 	}
2010 
2011 	abstract override int execute(Dub dub, string[] free_args, string[] app_args);
2012 }
2013 
2014 class AddPathCommand : RegistrationCommand {
2015 	this() @safe pure nothrow
2016 	{
2017 		this.name = "add-path";
2018 		this.argumentsPattern = "<path>";
2019 		this.description = "Adds a default package search path";
2020 		this.helpText = [
2021 			"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.",
2022 			"",
2023 			"Any packages registered using add-path will be preferred over packages downloaded from the package registry when searching for dependencies during a build operation.",
2024 			"",
2025 			"The version of the packages will be determined by one of the following:",
2026 			"  - For GIT working copies, the last tag (git describe) is used to determine the version",
2027 			"  - If the package contains a \"version\" field in the package description, this is used",
2028 			"  - If neither of those apply, \"~master\" is assumed"
2029 		];
2030 	}
2031 
2032 	override int execute(Dub dub, string[] free_args, string[] app_args)
2033 	{
2034 		enforceUsage(free_args.length == 1, "Missing search path.");
2035 		dub.addSearchPath(free_args[0], m_system);
2036 		return 0;
2037 	}
2038 }
2039 
2040 class RemovePathCommand : RegistrationCommand {
2041 	this() @safe pure nothrow
2042 	{
2043 		this.name = "remove-path";
2044 		this.argumentsPattern = "<path>";
2045 		this.description = "Removes a package search path";
2046 		this.helpText = ["Removes a package search path previously added with add-path."];
2047 	}
2048 
2049 	override int execute(Dub dub, string[] free_args, string[] app_args)
2050 	{
2051 		enforceUsage(free_args.length == 1, "Expected one argument.");
2052 		dub.removeSearchPath(free_args[0], m_system);
2053 		return 0;
2054 	}
2055 }
2056 
2057 class AddLocalCommand : RegistrationCommand {
2058 	this() @safe pure nothrow
2059 	{
2060 		this.name = "add-local";
2061 		this.argumentsPattern = "<path> [<version>]";
2062 		this.description = "Adds a local package directory (e.g. a git repository)";
2063 		this.helpText = [
2064 			"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.",
2065 			"",
2066 			"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.",
2067 			"",
2068 			"See 'dub add-path -h' for a way to register multiple local packages at once."
2069 		];
2070 	}
2071 
2072 	override int execute(Dub dub, string[] free_args, string[] app_args)
2073 	{
2074 		enforceUsage(free_args.length == 1 || free_args.length == 2, "Expecting one or two arguments.");
2075 		string ver = free_args.length == 2 ? free_args[1] : null;
2076 		dub.addLocalPackage(free_args[0], ver, m_system);
2077 		return 0;
2078 	}
2079 }
2080 
2081 class RemoveLocalCommand : RegistrationCommand {
2082 	this() @safe pure nothrow
2083 	{
2084 		this.name = "remove-local";
2085 		this.argumentsPattern = "<path>";
2086 		this.description = "Removes a local package directory";
2087 		this.helpText = ["Removes a local package directory"];
2088 	}
2089 
2090 	override int execute(Dub dub, string[] free_args, string[] app_args)
2091 	{
2092 		enforceUsage(free_args.length >= 1, "Missing package path argument.");
2093 		enforceUsage(free_args.length <= 1, "Expected the package path to be the only argument.");
2094 		dub.removeLocalPackage(free_args[0], m_system);
2095 		return 0;
2096 	}
2097 }
2098 
2099 class ListCommand : Command {
2100 	this() @safe pure nothrow
2101 	{
2102 		this.name = "list";
2103 		this.argumentsPattern = "[<package>[@<version-spec>]]";
2104 		this.description = "Prints a list of all or selected local packages dub is aware of";
2105 		this.helpText = [
2106 			"Prints a list of all or selected local packages. This includes all cached "~
2107 			"packages (user or system wide), all packages in the package search paths "~
2108 			"(\"dub add-path\") and all manually registered packages (\"dub add-local\"). "~
2109 			"If a package (and optionally a version spec) is specified, only matching packages are shown."
2110 		];
2111 	}
2112 	override void prepare(scope CommandArgs args) {}
2113 	override int execute(Dub dub, string[] free_args, string[] app_args)
2114 	{
2115 		enforceUsage(free_args.length <= 1, "Expecting zero or one extra arguments.");
2116 		const pinfo = free_args.length ? splitPackageName(free_args[0]) : PackageAndVersion("","*");
2117 		const pname = pinfo.name;
2118 		const pvlim = Dependency(pinfo.version_ == "" ? "*" : pinfo.version_);
2119 		enforceUsage(app_args.length == 0, "The list command supports no application arguments.");
2120 		logInfoNoTag("Packages present in the system and known to dub:");
2121 		foreach (p; dub.packageManager.getPackageIterator()) {
2122 			if ((pname == "" || pname == p.name) && pvlim.matches(p.version_))
2123 				logInfoNoTag("  %s %s: %s", p.name.color(Mode.bold), p.version_, p.path.toNativeString());
2124 		}
2125 		logInfo("");
2126 		return 0;
2127 	}
2128 }
2129 
2130 class SearchCommand : Command {
2131 	this() @safe pure nothrow
2132 	{
2133 		this.name = "search";
2134 		this.argumentsPattern = "<package-name>";
2135 		this.description = "Search for available packages.";
2136 		this.helpText = [
2137 			"Search all specified providers for matching packages."
2138 		];
2139 	}
2140 	override void prepare(scope CommandArgs args) {}
2141 	override int execute(Dub dub, string[] free_args, string[] app_args)
2142 	{
2143 		enforce(free_args.length == 1, "Expected one argument.");
2144 		auto res = dub.searchPackages(free_args[0]);
2145 		if (res.empty)
2146 		{
2147 			logError("No matches found.");
2148 			return 2;
2149 		}
2150 		auto justify = res
2151 			.map!((descNmatches) => descNmatches[1])
2152 			.joiner
2153 			.map!(m => m.name.length + m.version_.length)
2154 			.reduce!max + " ()".length;
2155 		justify += (~justify & 3) + 1; // round to next multiple of 4
2156 		int colorDifference = cast(int)"a".color(Mode.bold).length - 1;
2157 		justify += colorDifference;
2158 		foreach (desc, matches; res)
2159 		{
2160 			logInfoNoTag("==== %s ====", desc);
2161 			foreach (m; matches)
2162 				logInfoNoTag("  %s%s", leftJustify(m.name.color(Mode.bold)
2163 					~ " (" ~ m.version_ ~ ")", justify), m.description);
2164 		}
2165 		return 0;
2166 	}
2167 }
2168 
2169 
2170 /******************************************************************************/
2171 /* OVERRIDES                                                                  */
2172 /******************************************************************************/
2173 
2174 class AddOverrideCommand : Command {
2175 	private {
2176 		bool m_system = false;
2177 	}
2178 
2179 	static immutable string DeprecationMessage =
2180 		"This command is deprecated. Use path based dependency, custom cache path, " ~
2181 		"or edit `dub.selections.json` to achieve the same results.";
2182 
2183 
2184 	this() @safe pure nothrow
2185 	{
2186 		this.name = "add-override";
2187 		this.argumentsPattern = "<package> <version-spec> <target-path/target-version>";
2188 		this.description = "Adds a new package override.";
2189 
2190 		this.hidden = true;
2191 		this.helpText = [ DeprecationMessage ];
2192 	}
2193 
2194 	override void prepare(scope CommandArgs args)
2195 	{
2196 		args.getopt("system", &m_system, [
2197 			"Register system-wide instead of user-wide"
2198 		]);
2199 	}
2200 
2201 	override int execute(Dub dub, string[] free_args, string[] app_args)
2202 	{
2203 		logWarn(DeprecationMessage);
2204 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
2205 		enforceUsage(free_args.length == 3, "Expected three arguments, not "~free_args.length.to!string);
2206 		auto scope_ = m_system ? PlacementLocation.system : PlacementLocation.user;
2207 		auto pack = free_args[0];
2208 		auto source = VersionRange.fromString(free_args[1]);
2209 		if (existsFile(NativePath(free_args[2]))) {
2210 			auto target = NativePath(free_args[2]);
2211 			if (!target.absolute) target = getWorkingDirectory() ~ target;
2212 			dub.packageManager.addOverride_(scope_, pack, source, target);
2213 			logInfo("Added override %s %s => %s", pack, source, target);
2214 		} else {
2215 			auto target = Version(free_args[2]);
2216 			dub.packageManager.addOverride_(scope_, pack, source, target);
2217 			logInfo("Added override %s %s => %s", pack, source, target);
2218 		}
2219 		return 0;
2220 	}
2221 }
2222 
2223 class RemoveOverrideCommand : Command {
2224 	private {
2225 		bool m_system = false;
2226 	}
2227 
2228 	this() @safe pure nothrow
2229 	{
2230 		this.name = "remove-override";
2231 		this.argumentsPattern = "<package> <version-spec>";
2232 		this.description = "Removes an existing package override.";
2233 
2234 		this.hidden = true;
2235 		this.helpText = [ AddOverrideCommand.DeprecationMessage ];
2236 	}
2237 
2238 	override void prepare(scope CommandArgs args)
2239 	{
2240 		args.getopt("system", &m_system, [
2241 			"Register system-wide instead of user-wide"
2242 		]);
2243 	}
2244 
2245 	override int execute(Dub dub, string[] free_args, string[] app_args)
2246 	{
2247 		logWarn(AddOverrideCommand.DeprecationMessage);
2248 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
2249 		enforceUsage(free_args.length == 2, "Expected two arguments, not "~free_args.length.to!string);
2250 		auto scope_ = m_system ? PlacementLocation.system : PlacementLocation.user;
2251 		auto source = VersionRange.fromString(free_args[1]);
2252 		dub.packageManager.removeOverride_(scope_, free_args[0], source);
2253 		return 0;
2254 	}
2255 }
2256 
2257 class ListOverridesCommand : Command {
2258 	this() @safe pure nothrow
2259 	{
2260 		this.name = "list-overrides";
2261 		this.argumentsPattern = "";
2262 		this.description = "Prints a list of all local package overrides";
2263 
2264 		this.hidden = true;
2265 		this.helpText = [ AddOverrideCommand.DeprecationMessage ];
2266 	}
2267 	override void prepare(scope CommandArgs args) {}
2268 	override int execute(Dub dub, string[] free_args, string[] app_args)
2269 	{
2270 		logWarn(AddOverrideCommand.DeprecationMessage);
2271 
2272 		void printList(in PackageOverride_[] overrides, string caption)
2273 		{
2274 			if (overrides.length == 0) return;
2275 			logInfoNoTag("# %s", caption);
2276 			foreach (ovr; overrides)
2277 				ovr.target.match!(
2278 					t => logInfoNoTag("%s %s => %s", ovr.package_.color(Mode.bold), ovr.version_, t));
2279 		}
2280 		printList(dub.packageManager.getOverrides_(PlacementLocation.user), "User wide overrides");
2281 		printList(dub.packageManager.getOverrides_(PlacementLocation.system), "System wide overrides");
2282 		return 0;
2283 	}
2284 }
2285 
2286 /******************************************************************************/
2287 /* Cache cleanup                                                              */
2288 /******************************************************************************/
2289 
2290 class CleanCachesCommand : Command {
2291 	this() @safe pure nothrow
2292 	{
2293 		this.name = "clean-caches";
2294 		this.argumentsPattern = "";
2295 		this.description = "Removes cached metadata";
2296 		this.helpText = [
2297 			"This command removes any cached metadata like the list of available packages and their latest version."
2298 		];
2299 	}
2300 
2301 	override void prepare(scope CommandArgs args) {}
2302 
2303 	override int execute(Dub dub, string[] free_args, string[] app_args)
2304 	{
2305 		return 0;
2306 	}
2307 }
2308 
2309 /******************************************************************************/
2310 /* DUSTMITE                                                                   */
2311 /******************************************************************************/
2312 
2313 class DustmiteCommand : PackageBuildCommand {
2314 	private {
2315 		int m_compilerStatusCode = int.min;
2316 		int m_linkerStatusCode = int.min;
2317 		int m_programStatusCode = int.min;
2318 		string m_compilerRegex;
2319 		string m_linkerRegex;
2320 		string m_programRegex;
2321 		string m_testPackage;
2322 		bool m_noRedirect;
2323 		string m_strategy;
2324 		uint m_jobCount;		// zero means not specified
2325 		bool m_trace;
2326 	}
2327 
2328 	this() @safe pure nothrow
2329 	{
2330 		this.name = "dustmite";
2331 		this.argumentsPattern = "<destination-path>";
2332 		this.acceptsAppArgs = true;
2333 		this.description = "Create reduced test cases for build errors";
2334 		this.helpText = [
2335 			"This command uses the Dustmite utility to isolate the cause of build errors in a DUB project.",
2336 			"",
2337 			"It will create a copy of all involved packages and run dustmite on this copy, leaving a reduced test case.",
2338 			"",
2339 			"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."
2340 		];
2341 	}
2342 
2343 	override void prepare(scope CommandArgs args)
2344 	{
2345 		args.getopt("compiler-status", &m_compilerStatusCode, ["The expected status code of the compiler run"]);
2346 		args.getopt("compiler-regex", &m_compilerRegex, ["A regular expression used to match against the compiler output"]);
2347 		args.getopt("linker-status", &m_linkerStatusCode, ["The expected status code of the linker run"]);
2348 		args.getopt("linker-regex", &m_linkerRegex, ["A regular expression used to match against the linker output"]);
2349 		args.getopt("program-status", &m_programStatusCode, ["The expected status code of the built executable"]);
2350 		args.getopt("program-regex", &m_programRegex, ["A regular expression used to match against the program output"]);
2351 		args.getopt("test-package", &m_testPackage, ["Perform a test run - usually only used internally"]);
2352 		args.getopt("combined", &this.baseSettings.combined, ["Builds multiple packages with one compiler run"]);
2353 		args.getopt("no-redirect", &m_noRedirect, ["Don't redirect stdout/stderr streams of the test command"]);
2354 		args.getopt("strategy", &m_strategy, ["Set strategy (careful/lookback/pingpong/indepth/inbreadth)"]);
2355 		args.getopt("j", &m_jobCount, ["Set number of look-ahead processes"]);
2356 		args.getopt("trace", &m_trace, ["Save all attempted reductions to DIR.trace"]);
2357 		super.prepare(args);
2358 
2359 		// speed up loading when in test mode
2360 		if (m_testPackage.length) {
2361 			m_nodeps = true;
2362 		}
2363 	}
2364 
2365 	/// Returns: A minimally-initialized dub instance in test mode
2366 	override Dub prepareDub(CommonOptions options)
2367 	{
2368 		if (!m_testPackage.length)
2369 			return super.prepareDub(options);
2370 		return new Dub(NativePath(options.root_path), getWorkingDirectory());
2371 	}
2372 
2373 	override int execute(Dub dub, string[] free_args, string[] app_args)
2374 	{
2375 		import std.format : formattedWrite;
2376 
2377 		if (m_testPackage.length) {
2378 			setupPackage(dub, m_testPackage);
2379 			m_defaultConfig = dub.project.getDefaultConfiguration(this.baseSettings.platform);
2380 
2381 			GeneratorSettings gensettings = this.baseSettings;
2382 			if (!gensettings.config.length)
2383 				gensettings.config = m_defaultConfig;
2384 			gensettings.run = m_programStatusCode != int.min || m_programRegex.length;
2385 			gensettings.runArgs = app_args;
2386 			gensettings.force = true;
2387 			gensettings.compileCallback = check(m_compilerStatusCode, m_compilerRegex);
2388 			gensettings.linkCallback = check(m_linkerStatusCode, m_linkerRegex);
2389 			gensettings.runCallback = check(m_programStatusCode, m_programRegex);
2390 			try dub.generateProject("build", gensettings);
2391 			catch (DustmiteMismatchException) {
2392 				logInfoNoTag("Dustmite test doesn't match.");
2393 				return 3;
2394 			}
2395 			catch (DustmiteMatchException) {
2396 				logInfoNoTag("Dustmite test matches.");
2397 				return 0;
2398 			}
2399 		} else {
2400 			enforceUsage(free_args.length == 1, "Expected destination path.");
2401 			auto path = NativePath(free_args[0]);
2402 			path.normalize();
2403 			enforceUsage(!path.empty, "Destination path must not be empty.");
2404 			if (!path.absolute) path = getWorkingDirectory() ~ path;
2405 			enforceUsage(!path.startsWith(dub.rootPath), "Destination path must not be a sub directory of the tested package!");
2406 
2407 			setupPackage(dub, null);
2408 			auto prj = dub.project;
2409 			if (this.baseSettings.config.empty)
2410 				this.baseSettings.config = prj.getDefaultConfiguration(this.baseSettings.platform);
2411 
2412 			void copyFolderRec(NativePath folder, NativePath dstfolder)
2413 			{
2414 				ensureDirectory(dstfolder);
2415 				foreach (de; iterateDirectory(folder.toNativeString())) {
2416 					if (de.name.startsWith(".")) continue;
2417 					if (de.isDirectory) {
2418 						copyFolderRec(folder ~ de.name, dstfolder ~ de.name);
2419 					} else {
2420 						if (de.name.endsWith(".o") || de.name.endsWith(".obj")) continue;
2421 						if (de.name.endsWith(".exe")) continue;
2422 						try copyFile(folder ~ de.name, dstfolder ~ de.name);
2423 						catch (Exception e) {
2424 							logWarn("Failed to copy file %s: %s", (folder ~ de.name).toNativeString(), e.msg);
2425 						}
2426 					}
2427 				}
2428 			}
2429 
2430 			static void fixPathDependency(string pack, ref Dependency dep) {
2431 				dep.visit!(
2432 					(NativePath path) {
2433 						auto mainpack = getBasePackageName(pack);
2434 						dep = Dependency(NativePath("../") ~ mainpack);
2435 					},
2436 					(any) { /* Nothing to do */ },
2437 				);
2438 			}
2439 
2440 			void fixPathDependencies(ref PackageRecipe recipe, NativePath base_path)
2441 			{
2442 				foreach (name, ref dep; recipe.buildSettings.dependencies)
2443 					fixPathDependency(name, dep);
2444 
2445 				foreach (ref cfg; recipe.configurations)
2446 					foreach (name, ref dep; cfg.buildSettings.dependencies)
2447 						fixPathDependency(name, dep);
2448 
2449 				foreach (ref subp; recipe.subPackages)
2450 					if (subp.path.length) {
2451 						auto sub_path = base_path ~ NativePath(subp.path);
2452 						auto pack = prj.packageManager.getOrLoadPackage(sub_path);
2453 						fixPathDependencies(pack.recipe, sub_path);
2454 						pack.storeInfo(sub_path);
2455 					} else fixPathDependencies(subp.recipe, base_path);
2456 			}
2457 
2458 			bool[string] visited;
2459 			foreach (pack_; prj.getTopologicalPackageList()) {
2460 				auto pack = pack_.basePackage;
2461 				if (pack.name in visited) continue;
2462 				visited[pack.name] = true;
2463 				auto dst_path = path ~ pack.name;
2464 				logInfo("Prepare", Color.light_blue, "Copy package %s to destination folder...", pack.name.color(Mode.bold));
2465 				copyFolderRec(pack.path, dst_path);
2466 
2467 				// adjust all path based dependencies
2468 				fixPathDependencies(pack.recipe, dst_path);
2469 
2470 				// overwrite package description file with additional version information
2471 				pack.storeInfo(dst_path);
2472 			}
2473 
2474 			logInfo("Starting", Color.light_green, "Executing dustmite...");
2475 			auto testcmd = appender!string();
2476 			testcmd.formattedWrite("%s dustmite --test-package=%s --build=%s --config=%s",
2477 				thisExePath, prj.name, this.baseSettings.buildType, this.baseSettings.config);
2478 
2479 			if (m_compilerName.length) testcmd.formattedWrite(" \"--compiler=%s\"", m_compilerName);
2480 			if (m_arch.length) testcmd.formattedWrite(" --arch=%s", m_arch);
2481 			if (m_compilerStatusCode != int.min) testcmd.formattedWrite(" --compiler-status=%s", m_compilerStatusCode);
2482 			if (m_compilerRegex.length) testcmd.formattedWrite(" \"--compiler-regex=%s\"", m_compilerRegex);
2483 			if (m_linkerStatusCode != int.min) testcmd.formattedWrite(" --linker-status=%s", m_linkerStatusCode);
2484 			if (m_linkerRegex.length) testcmd.formattedWrite(" \"--linker-regex=%s\"", m_linkerRegex);
2485 			if (m_programStatusCode != int.min) testcmd.formattedWrite(" --program-status=%s", m_programStatusCode);
2486 			if (m_programRegex.length) testcmd.formattedWrite(" \"--program-regex=%s\"", m_programRegex);
2487 			if (this.baseSettings.combined) testcmd ~= " --combined";
2488 
2489 			// --vquiet swallows dustmite's output ...
2490 			if (!m_noRedirect) testcmd ~= " --vquiet";
2491 
2492 			// TODO: pass *all* original parameters
2493 			logDiagnostic("Running dustmite: %s", testcmd);
2494 
2495 			string[] extraArgs;
2496 			if (m_noRedirect) extraArgs ~= "--no-redirect";
2497 			if (m_strategy.length) extraArgs ~= "--strategy=" ~ m_strategy;
2498 			if (m_jobCount) extraArgs ~= "-j" ~ m_jobCount.to!string;
2499 			if (m_trace) extraArgs ~= "--trace";
2500 
2501 			const cmd = "dustmite" ~ extraArgs ~ [path.toNativeString(), testcmd.data];
2502 			auto dmpid = spawnProcess(cmd);
2503 			return dmpid.wait();
2504 		}
2505 		return 0;
2506 	}
2507 
2508 	void delegate(int, string) check(int code_match, string regex_match)
2509 	{
2510 		return (code, output) {
2511 			import std.encoding;
2512 			import std.regex;
2513 
2514 			logInfo("%s", output);
2515 
2516 			if (code_match != int.min && code != code_match) {
2517 				logInfo("Exit code %s doesn't match expected value %s", code, code_match);
2518 				throw new DustmiteMismatchException;
2519 			}
2520 
2521 			if (regex_match.length > 0 && !match(output.sanitize, regex_match)) {
2522 				logInfo("Output doesn't match regex:");
2523 				logInfo("%s", output);
2524 				throw new DustmiteMismatchException;
2525 			}
2526 
2527 			if (code != 0 && code_match != int.min || regex_match.length > 0) {
2528 				logInfo("Tool failed, but matched either exit code or output - counting as match.");
2529 				throw new DustmiteMatchException;
2530 			}
2531 		};
2532 	}
2533 
2534 	static class DustmiteMismatchException : Exception {
2535 		this(string message = "", string file = __FILE__, int line = __LINE__, Throwable next = null)
2536 		{
2537 			super(message, file, line, next);
2538 		}
2539 	}
2540 
2541 	static class DustmiteMatchException : Exception {
2542 		this(string message = "", string file = __FILE__, int line = __LINE__, Throwable next = null)
2543 		{
2544 			super(message, file, line, next);
2545 		}
2546 	}
2547 }
2548 
2549 
2550 /******************************************************************************/
2551 /* CONVERT command                                                               */
2552 /******************************************************************************/
2553 
2554 class ConvertCommand : Command {
2555 	private {
2556 		string m_format;
2557 		bool m_stdout;
2558 	}
2559 
2560 	this() @safe pure nothrow
2561 	{
2562 		this.name = "convert";
2563 		this.argumentsPattern = "";
2564 		this.description = "Converts the file format of the package recipe.";
2565 		this.helpText = [
2566 			"This command will convert between JSON and SDLang formatted package recipe files.",
2567 			"",
2568 			"Warning: Beware that any formatting and comments within the package recipe will get lost in the conversion process."
2569 		];
2570 	}
2571 
2572 	override void prepare(scope CommandArgs args)
2573 	{
2574 		args.getopt("f|format", &m_format, ["Specifies the target package recipe format. Possible values:", "  json, sdl"]);
2575 		args.getopt("s|stdout", &m_stdout, ["Outputs the converted package recipe to stdout instead of writing to disk."]);
2576 	}
2577 
2578 	override int execute(Dub dub, string[] free_args, string[] app_args)
2579 	{
2580 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
2581 		enforceUsage(free_args.length == 0, "Unexpected arguments: "~free_args.join(" "));
2582 		enforceUsage(m_format.length > 0, "Missing target format file extension (--format=...).");
2583 		if (!loadCwdPackage(dub, true)) return 2;
2584 		dub.convertRecipe(m_format, m_stdout);
2585 		return 0;
2586 	}
2587 }
2588 
2589 
2590 /******************************************************************************/
2591 /* HELP                                                                       */
2592 /******************************************************************************/
2593 
2594 private {
2595 	enum shortArgColumn = 2;
2596 	enum longArgColumn = 6;
2597 	enum descColumn = 24;
2598 	enum lineWidth = 80 - 1;
2599 }
2600 
2601 private void showHelp(in CommandGroup[] commands, CommandArgs common_args)
2602 {
2603 	writeln(
2604 `USAGE: dub [--version] [<command>] [<options...>] [-- [<application arguments...>]]
2605 
2606 Manages the DUB project in the current directory. If the command is omitted,
2607 DUB will default to "run". When running an application, "--" can be used to
2608 separate DUB options from options passed to the application.
2609 
2610 Run "dub <command> --help" to get help for a specific command.
2611 
2612 You can use the "http_proxy" environment variable to configure a proxy server
2613 to be used for fetching packages.
2614 
2615 
2616 Available commands
2617 ==================`);
2618 
2619 	foreach (grp; commands) {
2620 		writeln();
2621 		writeWS(shortArgColumn);
2622 		writeln(grp.caption);
2623 		writeWS(shortArgColumn);
2624 		writerep!'-'(grp.caption.length);
2625 		writeln();
2626 		foreach (cmd; grp.commands) {
2627 			if (cmd.hidden) continue;
2628 			writeWS(shortArgColumn);
2629 			writef("%s %s", cmd.name, cmd.argumentsPattern);
2630 			auto chars_output = cmd.name.length + cmd.argumentsPattern.length + shortArgColumn + 1;
2631 			if (chars_output < descColumn) {
2632 				writeWS(descColumn - chars_output);
2633 			} else {
2634 				writeln();
2635 				writeWS(descColumn);
2636 			}
2637 			writeWrapped(cmd.description, descColumn, descColumn);
2638 		}
2639 	}
2640 	writeln();
2641 	writeln();
2642 	writeln(`Common options`);
2643 	writeln(`==============`);
2644 	writeln();
2645 	writeOptions(common_args);
2646 	writeln();
2647 	showVersion();
2648 }
2649 
2650 private void showVersion()
2651 {
2652 	writefln("DUB version %s, built on %s", getDUBVersion(), __DATE__);
2653 }
2654 
2655 private void showCommandHelp(Command cmd, CommandArgs args, CommandArgs common_args)
2656 {
2657 	writefln(`USAGE: dub %s %s [<options...>]%s`, cmd.name, cmd.argumentsPattern, cmd.acceptsAppArgs ? " [-- <application arguments...>]": null);
2658 	writeln();
2659 	foreach (ln; cmd.helpText)
2660 		ln.writeWrapped();
2661 
2662 	if (args.recognizedArgs.length) {
2663 		writeln();
2664 		writeln();
2665 		writeln("Command specific options");
2666 		writeln("========================");
2667 		writeln();
2668 		writeOptions(args);
2669 	}
2670 
2671 	writeln();
2672 	writeln();
2673 	writeln("Common options");
2674 	writeln("==============");
2675 	writeln();
2676 	writeOptions(common_args);
2677 	writeln();
2678 	writefln("DUB version %s, built on %s", getDUBVersion(), __DATE__);
2679 }
2680 
2681 private void writeOptions(CommandArgs args)
2682 {
2683 	foreach (arg; args.recognizedArgs) {
2684 		if (arg.hidden) continue;
2685 		auto names = arg.names.split("|");
2686 		assert(names.length == 1 || names.length == 2);
2687 		string sarg = names[0].length == 1 ? names[0] : null;
2688 		string larg = names[0].length > 1 ? names[0] : names.length > 1 ? names[1] : null;
2689 		if (sarg !is null) {
2690 			writeWS(shortArgColumn);
2691 			writef("-%s", sarg);
2692 			writeWS(longArgColumn - shortArgColumn - 2);
2693 		} else writeWS(longArgColumn);
2694 		size_t col = longArgColumn;
2695 		if (larg !is null) {
2696 			if (arg.defaultValue.peek!bool) {
2697 				writef("--%s", larg);
2698 				col += larg.length + 2;
2699 			} else {
2700 				writef("--%s=VALUE", larg);
2701 				col += larg.length + 8;
2702 			}
2703 		}
2704 		if (col < descColumn) {
2705 			writeWS(descColumn - col);
2706 		} else {
2707 			writeln();
2708 			writeWS(descColumn);
2709 		}
2710 		foreach (i, ln; arg.helpText) {
2711 			if (i > 0) writeWS(descColumn);
2712 			ln.writeWrapped(descColumn, descColumn);
2713 		}
2714 	}
2715 }
2716 
2717 private void writeWrapped(string string, size_t indent = 0, size_t first_line_pos = 0)
2718 {
2719 	// handle pre-indented strings and bullet lists
2720 	size_t first_line_indent = 0;
2721 	while (string.startsWith(" ")) {
2722 		string = string[1 .. $];
2723 		indent++;
2724 		first_line_indent++;
2725 	}
2726 	if (string.startsWith("- ")) indent += 2;
2727 
2728 	auto wrapped = string.wrap(lineWidth, getRepString!' '(first_line_pos+first_line_indent), getRepString!' '(indent));
2729 	wrapped = wrapped[first_line_pos .. $];
2730 	foreach (ln; wrapped.splitLines())
2731 		writeln(ln);
2732 }
2733 
2734 private void writeWS(size_t num) { writerep!' '(num); }
2735 private void writerep(char ch)(size_t num) { write(getRepString!ch(num)); }
2736 
2737 private string getRepString(char ch)(size_t len)
2738 {
2739 	static string buf;
2740 	if (len > buf.length) buf ~= [ch].replicate(len-buf.length);
2741 	return buf[0 .. len];
2742 }
2743 
2744 /***
2745 */
2746 
2747 
2748 private void enforceUsage(bool cond, string text)
2749 {
2750 	if (!cond) throw new UsageException(text);
2751 }
2752 
2753 private class UsageException : Exception {
2754 	this(string message, string file = __FILE__, int line = __LINE__, Throwable next = null)
2755 	{
2756 		super(message, file, line, next);
2757 	}
2758 }
2759 
2760 private bool addDependency(Dub dub, ref PackageRecipe recipe, string depspec)
2761 {
2762 	Dependency dep;
2763 	const parts = splitPackageName(depspec);
2764 	const depname = parts.name;
2765 	if (parts.version_)
2766 		dep = Dependency(parts.version_);
2767 	else
2768 	{
2769 		try {
2770 			const ver = dub.getLatestVersion(depname);
2771 			dep = ver.isBranch ? Dependency(ver) : Dependency("~>" ~ ver.toString());
2772 		} catch (Exception e) {
2773 			logError("Could not find package '%s'.", depname);
2774 			logDebug("Full error: %s", e.toString().sanitize);
2775 			return false;
2776 		}
2777 	}
2778 	recipe.buildSettings.dependencies[depname] = dep;
2779 	logInfo("Adding dependency %s %s", depname, dep.toString());
2780 	return true;
2781 }
2782 
2783 private struct PackageAndVersion
2784 {
2785 	string name;
2786 	string version_;
2787 }
2788 
2789 /* Split <package>=<version-specifier> and <package>@<version-specifier>
2790    into `name` and `version_`. */
2791 private PackageAndVersion splitPackageName(string packageName)
2792 {
2793 	// split <package>@<version-specifier>
2794 	auto parts = packageName.findSplit("@");
2795 	if (parts[1].empty) {
2796 		// split <package>=<version-specifier>
2797 		parts = packageName.findSplit("=");
2798 	}
2799 
2800 	PackageAndVersion p;
2801 	p.name = parts[0];
2802 	if (!parts[1].empty)
2803 		p.version_ = parts[2];
2804 	return p;
2805 }
2806 
2807 unittest
2808 {
2809 	// https://github.com/dlang/dub/issues/1681
2810 	assert(splitPackageName("") == PackageAndVersion("", null));
2811 
2812 	assert(splitPackageName("foo") == PackageAndVersion("foo", null));
2813 	assert(splitPackageName("foo=1.0.1") == PackageAndVersion("foo", "1.0.1"));
2814 	assert(splitPackageName("foo@1.0.1") == PackageAndVersion("foo", "1.0.1"));
2815 	assert(splitPackageName("foo@==1.0.1") == PackageAndVersion("foo", "==1.0.1"));
2816 	assert(splitPackageName("foo@>=1.0.1") == PackageAndVersion("foo", ">=1.0.1"));
2817 	assert(splitPackageName("foo@~>1.0.1") == PackageAndVersion("foo", "~>1.0.1"));
2818 	assert(splitPackageName("foo@<1.0.1") == PackageAndVersion("foo", "<1.0.1"));
2819 }
2820 
2821 private ulong canFindVersionSplitter(string packageName)
2822 {
2823 	// see splitPackageName
2824 	return packageName.canFind("@", "=");
2825 }
2826 
2827 unittest
2828 {
2829 	assert(!canFindVersionSplitter("foo"));
2830 	assert(canFindVersionSplitter("foo=1.0.1"));
2831 	assert(canFindVersionSplitter("foo@1.0.1"));
2832 	assert(canFindVersionSplitter("foo@==1.0.1"));
2833 	assert(canFindVersionSplitter("foo@>=1.0.1"));
2834 	assert(canFindVersionSplitter("foo@~>1.0.1"));
2835 	assert(canFindVersionSplitter("foo@<1.0.1"));
2836 }