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