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