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