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