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