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