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 		if (dub.defaultEnvironments) m_buildSettings.addEnvironments(dub.defaultEnvironments);
1003 		if (dub.defaultBuildEnvironments) m_buildSettings.addBuildEnvironments(dub.defaultBuildEnvironments);
1004 		if (dub.defaultRunEnvironments) m_buildSettings.addRunEnvironments(dub.defaultRunEnvironments);
1005 		if (dub.defaultPreGenerateEnvironments) m_buildSettings.addPreGenerateEnvironments(dub.defaultPreGenerateEnvironments);
1006 		if (dub.defaultPostGenerateEnvironments) m_buildSettings.addPostGenerateEnvironments(dub.defaultPostGenerateEnvironments);
1007 		if (dub.defaultPreBuildEnvironments) m_buildSettings.addPreBuildEnvironments(dub.defaultPreBuildEnvironments);
1008 		if (dub.defaultPostBuildEnvironments) m_buildSettings.addPostBuildEnvironments(dub.defaultPostBuildEnvironments);
1009 		if (dub.defaultPreRunEnvironments) m_buildSettings.addPreRunEnvironments(dub.defaultPreRunEnvironments);
1010 		if (dub.defaultPostRunEnvironments) m_buildSettings.addPostRunEnvironments(dub.defaultPostRunEnvironments);
1011 		m_compiler = getCompiler(m_compilerName);
1012 		m_buildPlatform = m_compiler.determinePlatform(m_buildSettings, m_compilerName, m_arch);
1013 		m_buildSettings.addDebugVersions(m_debugVersions);
1014 
1015 		m_defaultConfig = null;
1016 		enforce (loadSpecificPackage(dub, package_name, ver), "Failed to load package.");
1017 
1018 		if (m_buildConfig.length != 0 && !dub.configurations.canFind(m_buildConfig))
1019 		{
1020 			string msg = "Unknown build configuration: "~m_buildConfig;
1021 			enum distance = 3;
1022 			auto match = dub.configurations.getClosestMatch(m_buildConfig, distance);
1023 			if (match !is null) msg ~= ". Did you mean '" ~ match ~ "'?";
1024 			enforce(0, msg);
1025 		}
1026 
1027 		if (m_buildType.length == 0) {
1028 			if (environment.get("DFLAGS") !is null) m_buildType = "$DFLAGS";
1029 			else m_buildType = default_build_type;
1030 		}
1031 
1032 		if (!m_nodeps) {
1033 			// retrieve missing packages
1034 			dub.project.reinit();
1035 			if (!dub.project.hasAllDependencies) {
1036 				logDiagnostic("Checking for missing dependencies.");
1037 				if (m_single) dub.upgrade(UpgradeOptions.select | UpgradeOptions.noSaveSelections);
1038 				else dub.upgrade(UpgradeOptions.select);
1039 			}
1040 		}
1041 
1042 		dub.project.validate();
1043 
1044 		foreach (sc; m_overrideConfigs) {
1045 			auto idx = sc.indexOf('/');
1046 			enforceUsage(idx >= 0, "Expected \"<package>/<configuration>\" as argument to --override-config.");
1047 			dub.project.overrideConfiguration(sc[0 .. idx], sc[idx+1 .. $]);
1048 		}
1049 	}
1050 
1051 	private bool loadSpecificPackage(Dub dub, string package_name, string ver)
1052 	{
1053 		if (m_single) {
1054 			enforce(package_name.length, "Missing file name of single-file package.");
1055 			dub.loadSingleFilePackage(package_name);
1056 			return true;
1057 		}
1058 
1059 		bool from_cwd = package_name.length == 0 || package_name.startsWith(":");
1060 		// load package in root_path to enable searching for sub packages
1061 		if (loadCwdPackage(dub, from_cwd)) {
1062 			if (package_name.startsWith(":"))
1063 			{
1064 				auto pack = dub.packageManager.getSubPackage(dub.project.rootPackage, package_name[1 .. $], false);
1065 				dub.loadPackage(pack);
1066 				return true;
1067 			}
1068 			if (from_cwd) return true;
1069 		}
1070 
1071 		enforce(package_name.length, "No valid root package found - aborting.");
1072 
1073 		auto pack = ver == ""
1074 			? dub.packageManager.getLatestPackage(package_name)
1075 			: dub.packageManager.getBestPackage(package_name, ver);
1076 
1077 		enforce(pack, format!"Failed to find a package named '%s%s' locally."(package_name,
1078 			ver == "" ? "" : ("@" ~ ver)
1079 		));
1080 		logInfo("Building package %s in %s", pack.name, pack.path.toNativeString());
1081 		dub.loadPackage(pack);
1082 		return true;
1083 	}
1084 }
1085 
1086 class GenerateCommand : PackageBuildCommand {
1087 	protected {
1088 		string m_generator;
1089 		bool m_rdmd = false;
1090 		bool m_tempBuild = false;
1091 		bool m_run = false;
1092 		bool m_force = false;
1093 		bool m_combined = false;
1094 		bool m_parallel = false;
1095 		bool m_printPlatform, m_printBuilds, m_printConfigs;
1096 	}
1097 
1098 	this() @safe pure nothrow
1099 	{
1100 		this.name = "generate";
1101 		this.argumentsPattern = "<generator> [<package>[@<version-spec>]]";
1102 		this.description = "Generates project files using the specified generator";
1103 		this.helpText = [
1104 			"Generates project files using one of the supported generators:",
1105 			"",
1106 			"visuald - VisualD project files",
1107 			"sublimetext - SublimeText project file",
1108 			"cmake - CMake build scripts",
1109 			"build - Builds the package directly",
1110 			"",
1111 			"An optional package name can be given to generate a different package than the root/CWD package."
1112 		];
1113 	}
1114 
1115 	override void prepare(scope CommandArgs args)
1116 	{
1117 		super.prepare(args);
1118 
1119 		args.getopt("combined", &m_combined, [
1120 			"Tries to build the whole project in a single compiler run."
1121 		]);
1122 
1123 		args.getopt("print-builds", &m_printBuilds, [
1124 			"Prints the list of available build types"
1125 		]);
1126 		args.getopt("print-configs", &m_printConfigs, [
1127 			"Prints the list of available configurations"
1128 		]);
1129 		args.getopt("print-platform", &m_printPlatform, [
1130 			"Prints the identifiers for the current build platform as used for the build fields in dub.json"
1131 		]);
1132 		args.getopt("parallel", &m_parallel, [
1133 			"Runs multiple compiler instances in parallel, if possible."
1134 		]);
1135 	}
1136 
1137 	override int execute(Dub dub, string[] free_args, string[] app_args)
1138 	{
1139 		string str_package_info;
1140 		if (!m_generator.length) {
1141 			enforceUsage(free_args.length >= 1 && free_args.length <= 2, "Expected one or two arguments.");
1142 			m_generator = free_args[0];
1143 			if (free_args.length >= 2) str_package_info = free_args[1];
1144 		} else {
1145 			enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1146 			if (free_args.length >= 1) str_package_info = free_args[0];
1147 		}
1148 
1149 		setupVersionPackage(dub, str_package_info, "debug");
1150 
1151 		if (m_printBuilds) { // FIXME: use actual package data
1152 			logInfo("Available build types:");
1153 			foreach (tp; ["debug", "release", "unittest", "profile"])
1154 				logInfo("  %s", tp);
1155 			logInfo("");
1156 		}
1157 
1158 		m_defaultConfig = dub.project.getDefaultConfiguration(m_buildPlatform);
1159 		if (m_printConfigs) {
1160 			logInfo("Available configurations:");
1161 			foreach (tp; dub.configurations)
1162 				logInfo("  %s%s", tp, tp == m_defaultConfig ? " [default]" : null);
1163 			logInfo("");
1164 		}
1165 
1166 		GeneratorSettings gensettings;
1167 		gensettings.platform = m_buildPlatform;
1168 		gensettings.config = m_buildConfig.length ? m_buildConfig : m_defaultConfig;
1169 		gensettings.buildType = m_buildType;
1170 		gensettings.buildMode = m_buildMode;
1171 		gensettings.compiler = m_compiler;
1172 		gensettings.buildSettings = m_buildSettings;
1173 		gensettings.combined = m_combined;
1174 		gensettings.filterVersions = m_filterVersions;
1175 		gensettings.run = m_run;
1176 		gensettings.runArgs = app_args;
1177 		gensettings.force = m_force;
1178 		gensettings.rdmd = m_rdmd;
1179 		gensettings.tempBuild = m_tempBuild;
1180 		gensettings.parallelBuild = m_parallel;
1181 		gensettings.single = m_single;
1182 
1183 		logDiagnostic("Generating using %s", m_generator);
1184 		dub.generateProject(m_generator, gensettings);
1185 		if (m_buildType == "ddox") dub.runDdox(gensettings.run, app_args);
1186 		return 0;
1187 	}
1188 }
1189 
1190 class BuildCommand : GenerateCommand {
1191 	protected {
1192 		bool m_yes; // automatic yes to prompts;
1193 		bool m_nonInteractive;
1194 	}
1195 	this() @safe pure nothrow
1196 	{
1197 		this.name = "build";
1198 		this.argumentsPattern = "[<package>[@<version-spec>]]";
1199 		this.description = "Builds a package (uses the main package in the current working directory by default)";
1200 		this.helpText = [
1201 			"Builds a package (uses the main package in the current working directory by default)"
1202 		];
1203 	}
1204 
1205 	override void prepare(scope CommandArgs args)
1206 	{
1207 		args.getopt("rdmd", &m_rdmd, [
1208 			"Use rdmd instead of directly invoking the compiler"
1209 		]);
1210 
1211 		args.getopt("f|force", &m_force, [
1212 			"Forces a recompilation even if the target is up to date"
1213 		]);
1214 		args.getopt("y|yes", &m_yes, [
1215 			`Automatic yes to prompts. Assume "yes" as answer to all interactive prompts.`
1216 		]);
1217 		args.getopt("n|non-interactive", &m_nonInteractive, [
1218 			"Don't enter interactive mode."
1219 		]);
1220 		super.prepare(args);
1221 		m_generator = "build";
1222 	}
1223 
1224 	override int execute(Dub dub, string[] free_args, string[] app_args)
1225 	{
1226 		// single package files don't need to be downloaded, they are on the disk.
1227 		if (free_args.length < 1 || m_single)
1228 			return super.execute(dub, free_args, app_args);
1229 
1230 		if (!m_nonInteractive)
1231 		{
1232 			const packageParts = splitPackageName(free_args[0]);
1233 			if (auto rc = fetchMissingPackages(dub, packageParts))
1234 				return rc;
1235 		}
1236 		return super.execute(dub, free_args, app_args);
1237 	}
1238 
1239 	private int fetchMissingPackages(Dub dub, in PackageAndVersion packageParts)
1240 	{
1241 
1242 		static bool input(string caption, bool default_value = true) {
1243 			writef("%s [%s]: ", caption, default_value ? "Y/n" : "y/N");
1244 			auto inp = readln();
1245 			string userInput = "y";
1246 			if (inp.length > 1)
1247 				userInput = inp[0 .. $ - 1].toLower;
1248 
1249 			switch (userInput) {
1250 				case "no", "n", "0":
1251 					return false;
1252 				case "yes", "y", "1":
1253 				default:
1254 					return true;
1255 			}
1256 		}
1257 
1258 		Dependency dep;
1259 
1260 		if (packageParts.version_.length > 0) {
1261 			// the user provided a version manually
1262 			dep = Dependency(packageParts.version_);
1263 		} else {
1264 			if (packageParts.name.startsWith(":") ||
1265 				dub.packageManager.getFirstPackage(packageParts.name))
1266 				// found locally
1267 				return 0;
1268 
1269 			// search for the package and filter versions for exact matches
1270 			auto basePackageName = getBasePackageName(packageParts.name);
1271 			auto search = dub.searchPackages(basePackageName)
1272 				.map!(tup => tup[1].find!(p => p.name == basePackageName))
1273 				.filter!(ps => !ps.empty);
1274 			if (search.empty) {
1275 				logWarn("Package '%s' was neither found locally nor online.", packageParts.name);
1276 				return 2;
1277 			}
1278 
1279 			const p = search.front.front;
1280 			logInfo("Package '%s' was not found locally but is available online:", packageParts.name);
1281 			logInfo("---");
1282 			logInfo("Description: %s", p.description);
1283 			logInfo("Version: %s", p.version_);
1284 			logInfo("---");
1285 
1286 			const answer = m_yes ? true : input("Do you want to fetch '%s' now?".format(packageParts.name));
1287 			if (!answer)
1288 				return 0;
1289 			dep = Dependency(p.version_);
1290 		}
1291 
1292 		dub.fetch(packageParts.name, dep, dub.defaultPlacementLocation, FetchOptions.none);
1293 		return 0;
1294 	}
1295 }
1296 
1297 class RunCommand : BuildCommand {
1298 	this() @safe pure nothrow
1299 	{
1300 		this.name = "run";
1301 		this.argumentsPattern = "[<package>[@<version-spec>]]";
1302 		this.description = "Builds and runs a package (default command)";
1303 		this.helpText = [
1304 			"Builds and runs a package (uses the main package in the current working directory by default)"
1305 		];
1306 		this.acceptsAppArgs = true;
1307 	}
1308 
1309 	override void prepare(scope CommandArgs args)
1310 	{
1311 		args.getopt("temp-build", &m_tempBuild, [
1312 			"Builds the project in the temp folder if possible."
1313 		]);
1314 
1315 		super.prepare(args);
1316 		m_run = true;
1317 	}
1318 
1319 	override int execute(Dub dub, string[] free_args, string[] app_args)
1320 	{
1321 		return super.execute(dub, free_args, app_args);
1322 	}
1323 }
1324 
1325 class TestCommand : PackageBuildCommand {
1326 	private {
1327 		string m_mainFile;
1328 		bool m_combined = false;
1329 		bool m_parallel = false;
1330 		bool m_force = false;
1331 	}
1332 
1333 	this() @safe pure nothrow
1334 	{
1335 		this.name = "test";
1336 		this.argumentsPattern = "[<package>[@<version-spec>]]";
1337 		this.description = "Executes the tests of the selected package";
1338 		this.helpText = [
1339 			`Builds the package and executes all contained unit tests.`,
1340 			``,
1341 			`If no explicit configuration is given, an existing "unittest" ` ~
1342 			`configuration will be preferred for testing. If none exists, the ` ~
1343 			`first library type configuration will be used, and if that doesn't ` ~
1344 			`exist either, the first executable configuration is chosen.`,
1345 			``,
1346 			`When a custom main file (--main-file) is specified, only library ` ~
1347 			`configurations can be used. Otherwise, depending on the type of ` ~
1348 			`the selected configuration, either an existing main file will be ` ~
1349 			`used (and needs to be properly adjusted to just run the unit ` ~
1350 			`tests for 'version(unittest)'), or DUB will generate one for ` ~
1351 			`library type configurations.`,
1352 			``,
1353 			`Finally, if the package contains a dependency to the "tested" ` ~
1354 			`package, the automatically generated main file will use it to ` ~
1355 			`run the unit tests.`
1356 		];
1357 		this.acceptsAppArgs = true;
1358 	}
1359 
1360 	override void prepare(scope CommandArgs args)
1361 	{
1362 		args.getopt("main-file", &m_mainFile, [
1363 			"Specifies a custom file containing the main() function to use for running the tests."
1364 		]);
1365 		args.getopt("combined", &m_combined, [
1366 			"Tries to build the whole project in a single compiler run."
1367 		]);
1368 		args.getopt("parallel", &m_parallel, [
1369 			"Runs multiple compiler instances in parallel, if possible."
1370 		]);
1371 		args.getopt("f|force", &m_force, [
1372 			"Forces a recompilation even if the target is up to date"
1373 		]);
1374 		bool coverage = false;
1375 		args.getopt("coverage", &coverage, [
1376 			"Enables code coverage statistics to be generated."
1377 		]);
1378 		if (coverage) m_buildType = "unittest-cov";
1379 
1380 		super.prepare(args);
1381 	}
1382 
1383 	override int execute(Dub dub, string[] free_args, string[] app_args)
1384 	{
1385 		string str_package_info;
1386 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1387 		if (free_args.length >= 1) str_package_info = free_args[0];
1388 
1389 		setupVersionPackage(dub, str_package_info, "unittest");
1390 
1391 		GeneratorSettings settings;
1392 		settings.platform = m_buildPlatform;
1393 		settings.compiler = getCompiler(m_buildPlatform.compilerBinary);
1394 		settings.buildType = m_buildType;
1395 		settings.buildMode = m_buildMode;
1396 		settings.buildSettings = m_buildSettings;
1397 		settings.combined = m_combined;
1398 		settings.filterVersions = m_filterVersions;
1399 		settings.parallelBuild = m_parallel;
1400 		settings.force = m_force;
1401 		settings.tempBuild = m_single;
1402 		settings.run = true;
1403 		settings.runArgs = app_args;
1404 		settings.single = m_single;
1405 
1406 		dub.testProject(settings, m_buildConfig, NativePath(m_mainFile));
1407 		return 0;
1408 	}
1409 }
1410 
1411 class LintCommand : PackageBuildCommand {
1412 	private {
1413 		bool m_syntaxCheck = false;
1414 		bool m_styleCheck = false;
1415 		string m_errorFormat;
1416 		bool m_report = false;
1417 		string m_reportFormat;
1418 		string m_reportFile;
1419 		string[] m_importPaths;
1420 		string m_config;
1421 	}
1422 
1423 	this() @safe pure nothrow
1424 	{
1425 		this.name = "lint";
1426 		this.argumentsPattern = "[<package>[@<version-spec>]]";
1427 		this.description = "Executes the linter tests of the selected package";
1428 		this.helpText = [
1429 			`Builds the package and executes D-Scanner linter tests.`
1430 		];
1431 		this.acceptsAppArgs = true;
1432 	}
1433 
1434 	override void prepare(scope CommandArgs args)
1435 	{
1436 		args.getopt("syntax-check", &m_syntaxCheck, [
1437 			"Lexes and parses sourceFile, printing the line and column number of " ~
1438 			"any syntax errors to stdout."
1439 		]);
1440 
1441 		args.getopt("style-check", &m_styleCheck, [
1442 			"Lexes and parses sourceFiles, printing the line and column number of " ~
1443 			"any static analysis check failures stdout."
1444 		]);
1445 
1446 		args.getopt("error-format", &m_errorFormat, [
1447 			"Format errors produced by the style/syntax checkers."
1448 		]);
1449 
1450 		args.getopt("report", &m_report, [
1451 			"Generate a static analysis report in JSON format."
1452 		]);
1453 
1454 		args.getopt("report-format", &m_reportFormat, [
1455 			"Specifies the format of the generated report."
1456 		]);
1457 
1458 		args.getopt("report-file", &m_reportFile, [
1459 			"Write report to file."
1460 		]);
1461 
1462 		if (m_reportFormat || m_reportFile) m_report = true;
1463 
1464 		args.getopt("import-paths", &m_importPaths, [
1465 			"Import paths"
1466 		]);
1467 
1468 		args.getopt("config", &m_config, [
1469 			"Use the given configuration file."
1470 		]);
1471 
1472 		super.prepare(args);
1473 	}
1474 
1475 	override int execute(Dub dub, string[] free_args, string[] app_args)
1476 	{
1477 		string str_package_info;
1478 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1479 		if (free_args.length >= 1) str_package_info = free_args[0];
1480 
1481 		string[] args;
1482 		if (!m_syntaxCheck && !m_styleCheck && !m_report && app_args.length == 0) { m_styleCheck = true; }
1483 
1484 		if (m_syntaxCheck) args ~= "--syntaxCheck";
1485 		if (m_styleCheck) args ~= "--styleCheck";
1486 		if (m_errorFormat) args ~= ["--errorFormat", m_errorFormat];
1487 		if (m_report) args ~= "--report";
1488 		if (m_reportFormat) args ~= ["--reportFormat", m_reportFormat];
1489 		if (m_reportFile) args ~= ["--reportFile", m_reportFile];
1490 		foreach (import_path; m_importPaths) args ~= ["-I", import_path];
1491 		if (m_config) args ~= ["--config", m_config];
1492 
1493 		setupVersionPackage(dub, str_package_info);
1494 		dub.lintProject(args ~ app_args);
1495 		return 0;
1496 	}
1497 }
1498 
1499 class DescribeCommand : PackageBuildCommand {
1500 	private {
1501 		bool m_importPaths = false;
1502 		bool m_stringImportPaths = false;
1503 		bool m_dataList = false;
1504 		bool m_dataNullDelim = false;
1505 		string[] m_data;
1506 	}
1507 
1508 	this() @safe pure nothrow
1509 	{
1510 		this.name = "describe";
1511 		this.argumentsPattern = "[<package>[@<version-spec>]]";
1512 		this.description = "Prints a JSON description of the project and its dependencies";
1513 		this.helpText = [
1514 			"Prints a JSON build description for the root package an all of " ~
1515 			"their dependencies in a format similar to a JSON package " ~
1516 			"description file. This is useful mostly for IDEs.",
1517 			"",
1518 			"All usual options that are also used for build/run/generate apply.",
1519 			"",
1520 			"When --data=VALUE is supplied, specific build settings for a project " ~
1521 			"will be printed instead (by default, formatted for the current compiler).",
1522 			"",
1523 			"The --data=VALUE option can be specified multiple times to retrieve " ~
1524 			"several pieces of information at once. A comma-separated list is " ~
1525 			"also acceptable (ex: --data=dflags,libs). The data will be output in " ~
1526 			"the same order requested on the command line.",
1527 			"",
1528 			"The accepted values for --data=VALUE are:",
1529 			"",
1530 			"main-source-file, dflags, lflags, libs, linker-files, " ~
1531 			"source-files, versions, debug-versions, import-paths, " ~
1532 			"string-import-paths, import-files, options",
1533 			"",
1534 			"The following are also accepted by --data if --data-list is used:",
1535 			"",
1536 			"target-type, target-path, target-name, working-directory, " ~
1537 			"copy-files, string-import-files, pre-generate-commands, " ~
1538 			"post-generate-commands, pre-build-commands, post-build-commands, " ~
1539 			"requirements",
1540 		];
1541 	}
1542 
1543 	override void prepare(scope CommandArgs args)
1544 	{
1545 		super.prepare(args);
1546 
1547 		args.getopt("import-paths", &m_importPaths, [
1548 			"Shortcut for --data=import-paths --data-list"
1549 		]);
1550 
1551 		args.getopt("string-import-paths", &m_stringImportPaths, [
1552 			"Shortcut for --data=string-import-paths --data-list"
1553 		]);
1554 
1555 		args.getopt("data", &m_data, [
1556 			"Just list the values of a particular build setting, either for this "~
1557 			"package alone or recursively including all dependencies. Accepts a "~
1558 			"comma-separated list. See above for more details and accepted "~
1559 			"possibilities for VALUE."
1560 		]);
1561 
1562 		args.getopt("data-list", &m_dataList, [
1563 			"Output --data information in list format (line-by-line), instead "~
1564 			"of formatting for a compiler command line.",
1565 		]);
1566 
1567 		args.getopt("data-0", &m_dataNullDelim, [
1568 			"Output --data information using null-delimiters, rather than "~
1569 			"spaces or newlines. Result is usable with, ex., xargs -0.",
1570 		]);
1571 	}
1572 
1573 	override int execute(Dub dub, string[] free_args, string[] app_args)
1574 	{
1575 		enforceUsage(
1576 			!(m_importPaths && m_stringImportPaths),
1577 			"--import-paths and --string-import-paths may not be used together."
1578 		);
1579 
1580 		enforceUsage(
1581 			!(m_data && (m_importPaths || m_stringImportPaths)),
1582 			"--data may not be used together with --import-paths or --string-import-paths."
1583 		);
1584 
1585 		// disable all log output to stdout and use "writeln" to output the JSON description
1586 		auto ll = getLogLevel();
1587 		setLogLevel(max(ll, LogLevel.warn));
1588 		scope (exit) setLogLevel(ll);
1589 
1590 		string str_package_info;
1591 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1592 		if (free_args.length >= 1) str_package_info = free_args[0];
1593 		setupVersionPackage(dub, str_package_info);
1594 
1595 		m_defaultConfig = dub.project.getDefaultConfiguration(m_buildPlatform);
1596 
1597 		auto config = m_buildConfig.length ? m_buildConfig : m_defaultConfig;
1598 
1599 		GeneratorSettings settings;
1600 		settings.platform = m_buildPlatform;
1601 		settings.config = config;
1602 		settings.buildType = m_buildType;
1603 		settings.compiler = m_compiler;
1604 		settings.filterVersions = m_filterVersions;
1605 		settings.buildSettings.options |= m_buildSettings.options & BuildOption.lowmem;
1606 
1607 		if (m_importPaths) { m_data = ["import-paths"]; m_dataList = true; }
1608 		else if (m_stringImportPaths) { m_data = ["string-import-paths"]; m_dataList = true; }
1609 
1610 		if (m_data.length) {
1611 			ListBuildSettingsFormat lt;
1612 			with (ListBuildSettingsFormat)
1613 				lt = m_dataList ? (m_dataNullDelim ? listNul : list) : (m_dataNullDelim ? commandLineNul : commandLine);
1614 			dub.listProjectData(settings, m_data, lt);
1615 		} else {
1616 			auto desc = dub.project.describe(settings);
1617 			writeln(desc.serializeToPrettyJson());
1618 		}
1619 
1620 		return 0;
1621 	}
1622 }
1623 
1624 class CleanCommand : Command {
1625 	private {
1626 		bool m_allPackages;
1627 	}
1628 
1629 	this() @safe pure nothrow
1630 	{
1631 		this.name = "clean";
1632 		this.argumentsPattern = "[<package>]";
1633 		this.description = "Removes intermediate build files and cached build results";
1634 		this.helpText = [
1635 			"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.",
1636 			"Without arguments, the package in the current working directory will be cleaned."
1637 		];
1638 	}
1639 
1640 	override void prepare(scope CommandArgs args)
1641 	{
1642 		args.getopt("all-packages", &m_allPackages, [
1643 			"Cleans up *all* known packages (dub list)"
1644 		]);
1645 	}
1646 
1647 	override int execute(Dub dub, string[] free_args, string[] app_args)
1648 	{
1649 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1650 		enforceUsage(app_args.length == 0, "Application arguments are not supported for the clean command.");
1651 		enforceUsage(!m_allPackages || !free_args.length, "The --all-packages flag may not be used together with an explicit package name.");
1652 
1653 		enforce(free_args.length == 0, "Cleaning a specific package isn't possible right now.");
1654 
1655 		if (m_allPackages) {
1656 			bool any_error = false;
1657 
1658 			foreach (p; dub.packageManager.getPackageIterator()) {
1659 				try dub.cleanPackage(p.path);
1660 				catch (Exception e) {
1661 					logWarn("Failed to clean package %s at %s: %s", p.name, p.path, e.msg);
1662 					any_error = true;
1663 				}
1664 
1665 				foreach (sp; p.subPackages.filter!(sp => !sp.path.empty)) {
1666 					try dub.cleanPackage(p.path ~ sp.path);
1667 					catch (Exception e) {
1668 						logWarn("Failed to clean sub package of %s at %s: %s", p.name, p.path ~ sp.path, e.msg);
1669 						any_error = true;
1670 					}
1671 				}
1672 			}
1673 
1674 			if (any_error) return 1;
1675 		} else {
1676 			dub.cleanPackage(dub.rootPath);
1677 		}
1678 
1679 		return 0;
1680 	}
1681 }
1682 
1683 
1684 /******************************************************************************/
1685 /* FETCH / ADD / REMOVE / UPGRADE                                             */
1686 /******************************************************************************/
1687 
1688 class AddCommand : Command {
1689 	this() @safe pure nothrow
1690 	{
1691 		this.name = "add";
1692 		this.argumentsPattern = "<package>[@<version-spec>] [<packages...>]";
1693 		this.description = "Adds dependencies to the package file.";
1694 		this.helpText = [
1695 			"Adds <packages> as dependencies.",
1696 			"",
1697 			"Running \"dub add <package>\" is the same as adding <package> to the \"dependencies\" section in dub.json/dub.sdl.",
1698 			"If no version is specified for one of the packages, dub will query the registry for the latest version."
1699 		];
1700 	}
1701 
1702 	override void prepare(scope CommandArgs args) {}
1703 
1704 	override int execute(Dub dub, string[] free_args, string[] app_args)
1705 	{
1706 		import dub.recipe.io : readPackageRecipe, writePackageRecipe;
1707 		import dub.internal.vibecompat.core.file : existsFile;
1708 		enforceUsage(free_args.length != 0, "Expected one or more arguments.");
1709 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1710 
1711 		if (!loadCwdPackage(dub, true)) return 1;
1712 		auto recipe = dub.project.rootPackage.rawRecipe.clone;
1713 
1714 		foreach (depspec; free_args) {
1715 			if (!addDependency(dub, recipe, depspec))
1716 				return 1;
1717 		}
1718 		writePackageRecipe(dub.project.rootPackage.recipePath, recipe);
1719 
1720 		return 0;
1721 	}
1722 }
1723 
1724 class UpgradeCommand : Command {
1725 	private {
1726 		bool m_prerelease = false;
1727 		bool m_forceRemove = false;
1728 		bool m_missingOnly = false;
1729 		bool m_verify = false;
1730 		bool m_dryRun = false;
1731 	}
1732 
1733 	this() @safe pure nothrow
1734 	{
1735 		this.name = "upgrade";
1736 		this.argumentsPattern = "[<packages...>]";
1737 		this.description = "Forces an upgrade of the dependencies";
1738 		this.helpText = [
1739 			"Upgrades all dependencies of the package by querying the package registry(ies) for new versions.",
1740 			"",
1741 			"This will update the versions stored in the selections file ("~SelectedVersions.defaultFile~") accordingly.",
1742 			"",
1743 			"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."
1744 		];
1745 	}
1746 
1747 	override void prepare(scope CommandArgs args)
1748 	{
1749 		args.getopt("prerelease", &m_prerelease, [
1750 			"Uses the latest pre-release version, even if release versions are available"
1751 		]);
1752 		args.getopt("verify", &m_verify, [
1753 			"Updates the project and performs a build. If successful, rewrites the selected versions file <to be implemented>."
1754 		]);
1755 		args.getopt("dry-run", &m_dryRun, [
1756 			"Only print what would be upgraded, but don't actually upgrade anything."
1757 		]);
1758 		args.getopt("missing-only", &m_missingOnly, [
1759 			"Performs an upgrade only for dependencies that don't yet have a version selected. This is also done automatically before each build."
1760 		]);
1761 		args.getopt("force-remove", &m_forceRemove, [
1762 			"Deprecated option that does nothing."
1763 		]);
1764 	}
1765 
1766 	override int execute(Dub dub, string[] free_args, string[] app_args)
1767 	{
1768 		enforceUsage(free_args.length <= 1, "Unexpected arguments.");
1769 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1770 		enforceUsage(!m_verify, "--verify is not yet implemented.");
1771 		enforce(loadCwdPackage(dub, true), "Failed to load package.");
1772 		logInfo("Upgrading project in %s", dub.projectPath.toNativeString());
1773 		auto options = UpgradeOptions.upgrade|UpgradeOptions.select;
1774 		if (m_missingOnly) options &= ~UpgradeOptions.upgrade;
1775 		if (m_prerelease) options |= UpgradeOptions.preRelease;
1776 		if (m_dryRun) options |= UpgradeOptions.dryRun;
1777 		dub.upgrade(options, free_args);
1778 		return 0;
1779 	}
1780 }
1781 
1782 class FetchRemoveCommand : Command {
1783 	protected {
1784 		string m_version;
1785 		bool m_forceRemove = false;
1786 	}
1787 
1788 	override void prepare(scope CommandArgs args)
1789 	{
1790 		args.getopt("version", &m_version, [
1791 			"Use the specified version/branch instead of the latest available match",
1792 			"The remove command also accepts \"*\" here as a wildcard to remove all versions of the package from the specified location"
1793 		], true); // hide --version from help
1794 
1795 		args.getopt("force-remove", &m_forceRemove, [
1796 			"Deprecated option that does nothing"
1797 		]);
1798 	}
1799 
1800 	abstract override int execute(Dub dub, string[] free_args, string[] app_args);
1801 }
1802 
1803 class FetchCommand : FetchRemoveCommand {
1804 	this() @safe pure nothrow
1805 	{
1806 		this.name = "fetch";
1807 		this.argumentsPattern = "<package>[@<version-spec>]";
1808 		this.description = "Manually retrieves and caches a package";
1809 		this.helpText = [
1810 			"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.",
1811 			"",
1812 			"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.",
1813 			"",
1814 			"Without specified options, placement/removal will default to a user wide shared location.",
1815 			"",
1816 			"Complete applications can be retrieved and run easily by e.g.",
1817 			"$ dub fetch vibelog --cache=local",
1818 			"$ dub run vibelog --cache=local",
1819 			"",
1820 			"This will grab all needed dependencies and compile and run the application.",
1821 			"",
1822 			"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."
1823 		];
1824 	}
1825 
1826 	override void prepare(scope CommandArgs args)
1827 	{
1828 		super.prepare(args);
1829 	}
1830 
1831 	override int execute(Dub dub, string[] free_args, string[] app_args)
1832 	{
1833 		enforceUsage(free_args.length == 1, "Expecting exactly one argument.");
1834 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1835 
1836 		auto location = dub.defaultPlacementLocation;
1837 
1838 		auto name = free_args[0];
1839 
1840 		FetchOptions fetchOpts;
1841 		fetchOpts |= FetchOptions.forceBranchUpgrade;
1842 		if (m_version.length) { // remove then --version removed
1843 			enforceUsage(!name.canFindVersionSplitter, "Double version spec not allowed.");
1844 			logWarn("The '--version' parameter was deprecated, use %s@%s. Please update your scripts.", name, m_version);
1845 			dub.fetch(name, Dependency(m_version), location, fetchOpts);
1846 		} else if (name.canFindVersionSplitter) {
1847 			const parts = name.splitPackageName;
1848 			dub.fetch(parts.name, Dependency(parts.version_), location, fetchOpts);
1849 		} else {
1850 			try {
1851 				dub.fetch(name, Dependency(">=0.0.0"), location, fetchOpts);
1852 				logInfo(
1853 					"Please note that you need to use `dub run <pkgname>` " ~
1854 					"or add it to dependencies of your package to actually use/run it. " ~
1855 					"dub does not do actual installation of packages outside of its own ecosystem.");
1856 			}
1857 			catch(Exception e){
1858 				logInfo("Getting a release version failed: %s", e.msg);
1859 				logInfo("Retry with ~master...");
1860 				dub.fetch(name, Dependency("~master"), location, fetchOpts);
1861 			}
1862 		}
1863 		return 0;
1864 	}
1865 }
1866 
1867 class InstallCommand : FetchCommand {
1868 	this() @safe pure nothrow
1869     {
1870         this.name = "install";
1871         this.hidden = true;
1872     }
1873 	override void prepare(scope CommandArgs args) { super.prepare(args); }
1874 	override int execute(Dub dub, string[] free_args, string[] app_args)
1875 	{
1876 		warnRenamed("install", "fetch");
1877 		return super.execute(dub, free_args, app_args);
1878 	}
1879 }
1880 
1881 class RemoveCommand : FetchRemoveCommand {
1882 	private {
1883 		bool m_nonInteractive;
1884 	}
1885 
1886 	this() @safe pure nothrow
1887 	{
1888 		this.name = "remove";
1889 		this.argumentsPattern = "<package>[@<version-spec>]";
1890 		this.description = "Removes a cached package";
1891 		this.helpText = [
1892 			"Removes a package that is cached on the local system."
1893 		];
1894 	}
1895 
1896 	override void prepare(scope CommandArgs args)
1897 	{
1898 		super.prepare(args);
1899 		args.getopt("n|non-interactive", &m_nonInteractive, ["Don't enter interactive mode."]);
1900 	}
1901 
1902 	override int execute(Dub dub, string[] free_args, string[] app_args)
1903 	{
1904 		enforceUsage(free_args.length == 1, "Expecting exactly one argument.");
1905 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1906 
1907 		auto package_id = free_args[0];
1908 		auto location = dub.defaultPlacementLocation;
1909 
1910 		size_t resolveVersion(in Package[] packages) {
1911 			// just remove only package version
1912 			if (packages.length == 1)
1913 				return 0;
1914 
1915 			writeln("Select version of '", package_id, "' to remove from location '", location, "':");
1916 			foreach (i, pack; packages)
1917 				writefln("%s) %s", i + 1, pack.version_);
1918 			writeln(packages.length + 1, ") ", "all versions");
1919 			while (true) {
1920 				writef("> ");
1921 				auto inp = readln();
1922 				if (!inp.length) // Ctrl+D
1923 					return size_t.max;
1924 				inp = inp.stripRight;
1925 				if (!inp.length) // newline or space
1926 					continue;
1927 				try {
1928 					immutable selection = inp.to!size_t - 1;
1929 					if (selection <= packages.length)
1930 						return selection;
1931 				} catch (ConvException e) {
1932 				}
1933 				logError("Please enter a number between 1 and %s.", packages.length + 1);
1934 			}
1935 		}
1936 
1937 		if (!m_version.empty) { // remove then --version removed
1938 			enforceUsage(!package_id.canFindVersionSplitter, "Double version spec not allowed.");
1939 			logWarn("The '--version' parameter was deprecated, use %s@%s. Please update your scripts.", package_id, m_version);
1940 			dub.remove(package_id, m_version, location);
1941 		} else {
1942 			const parts = package_id.splitPackageName;
1943 			if (m_nonInteractive || parts.version_.length) {
1944 				dub.remove(parts.name, parts.version_, location);
1945 			} else {
1946 				dub.remove(package_id, location, &resolveVersion);
1947 			}
1948 		}
1949 		return 0;
1950 	}
1951 }
1952 
1953 class UninstallCommand : RemoveCommand {
1954 	this() @safe pure nothrow
1955     {
1956         this.name = "uninstall";
1957         this.hidden = true;
1958     }
1959 	override void prepare(scope CommandArgs args) { super.prepare(args); }
1960 	override int execute(Dub dub, string[] free_args, string[] app_args)
1961 	{
1962 		warnRenamed("uninstall", "remove");
1963 		return super.execute(dub, free_args, app_args);
1964 	}
1965 }
1966 
1967 
1968 /******************************************************************************/
1969 /* ADD/REMOVE PATH/LOCAL                                                      */
1970 /******************************************************************************/
1971 
1972 abstract class RegistrationCommand : Command {
1973 	private {
1974 		bool m_system;
1975 	}
1976 
1977 	override void prepare(scope CommandArgs args)
1978 	{
1979 		args.getopt("system", &m_system, [
1980 			"Register system-wide instead of user-wide"
1981 		]);
1982 	}
1983 
1984 	abstract override int execute(Dub dub, string[] free_args, string[] app_args);
1985 }
1986 
1987 class AddPathCommand : RegistrationCommand {
1988 	this() @safe pure nothrow
1989 	{
1990 		this.name = "add-path";
1991 		this.argumentsPattern = "<path>";
1992 		this.description = "Adds a default package search path";
1993 		this.helpText = [
1994 			"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.",
1995 			"",
1996 			"Any packages registered using add-path will be preferred over packages downloaded from the package registry when searching for dependencies during a build operation.",
1997 			"",
1998 			"The version of the packages will be determined by one of the following:",
1999 			"  - For GIT working copies, the last tag (git describe) is used to determine the version",
2000 			"  - If the package contains a \"version\" field in the package description, this is used",
2001 			"  - If neither of those apply, \"~master\" is assumed"
2002 		];
2003 	}
2004 
2005 	override int execute(Dub dub, string[] free_args, string[] app_args)
2006 	{
2007 		enforceUsage(free_args.length == 1, "Missing search path.");
2008 		dub.addSearchPath(free_args[0], m_system);
2009 		return 0;
2010 	}
2011 }
2012 
2013 class RemovePathCommand : RegistrationCommand {
2014 	this() @safe pure nothrow
2015 	{
2016 		this.name = "remove-path";
2017 		this.argumentsPattern = "<path>";
2018 		this.description = "Removes a package search path";
2019 		this.helpText = ["Removes a package search path previously added with add-path."];
2020 	}
2021 
2022 	override int execute(Dub dub, string[] free_args, string[] app_args)
2023 	{
2024 		enforceUsage(free_args.length == 1, "Expected one argument.");
2025 		dub.removeSearchPath(free_args[0], m_system);
2026 		return 0;
2027 	}
2028 }
2029 
2030 class AddLocalCommand : RegistrationCommand {
2031 	this() @safe pure nothrow
2032 	{
2033 		this.name = "add-local";
2034 		this.argumentsPattern = "<path> [<version>]";
2035 		this.description = "Adds a local package directory (e.g. a git repository)";
2036 		this.helpText = [
2037 			"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.",
2038 			"",
2039 			"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.",
2040 			"",
2041 			"See 'dub add-path -h' for a way to register multiple local packages at once."
2042 		];
2043 	}
2044 
2045 	override int execute(Dub dub, string[] free_args, string[] app_args)
2046 	{
2047 		enforceUsage(free_args.length == 1 || free_args.length == 2, "Expecting one or two arguments.");
2048 		string ver = free_args.length == 2 ? free_args[1] : null;
2049 		dub.addLocalPackage(free_args[0], ver, m_system);
2050 		return 0;
2051 	}
2052 }
2053 
2054 class RemoveLocalCommand : RegistrationCommand {
2055 	this() @safe pure nothrow
2056 	{
2057 		this.name = "remove-local";
2058 		this.argumentsPattern = "<path>";
2059 		this.description = "Removes a local package directory";
2060 		this.helpText = ["Removes a local package directory"];
2061 	}
2062 
2063 	override int execute(Dub dub, string[] free_args, string[] app_args)
2064 	{
2065 		enforceUsage(free_args.length >= 1, "Missing package path argument.");
2066 		enforceUsage(free_args.length <= 1, "Expected the package path to be the only argument.");
2067 		dub.removeLocalPackage(free_args[0], m_system);
2068 		return 0;
2069 	}
2070 }
2071 
2072 class ListCommand : Command {
2073 	this() @safe pure nothrow
2074 	{
2075 		this.name = "list";
2076 		this.argumentsPattern = "[<package>[@<version-spec>]]";
2077 		this.description = "Prints a list of all or selected local packages dub is aware of";
2078 		this.helpText = [
2079 			"Prints a list of all or selected local packages. This includes all cached "~
2080 			"packages (user or system wide), all packages in the package search paths "~
2081 			"(\"dub add-path\") and all manually registered packages (\"dub add-local\"). "~
2082 			"If package specified output filtered by package spec."
2083 		];
2084 	}
2085 	override void prepare(scope CommandArgs args) {}
2086 	override int execute(Dub dub, string[] free_args, string[] app_args)
2087 	{
2088 		enforceUsage(free_args.length <= 1, "Expecting zero or one extra arguments.");
2089 		const pinfo = free_args.length ? splitPackageName(free_args[0]) : PackageAndVersion("","*");
2090 		const pname = pinfo.name;
2091 		const pvlim = Dependency(pinfo.version_ == "" ? "*" : pinfo.version_);
2092 		enforceUsage(app_args.length == 0, "The list command supports no application arguments.");
2093 		logInfo("Packages present in the system and known to dub:");
2094 		foreach (p; dub.packageManager.getPackageIterator()) {
2095 			if ((pname == "" || pname == p.name) && pvlim.matches(p.version_))
2096 				logInfo("  %s %s: %s", p.name, p.version_, p.path.toNativeString());
2097 		}
2098 		logInfo("");
2099 		return 0;
2100 	}
2101 }
2102 
2103 class SearchCommand : Command {
2104 	this() @safe pure nothrow
2105 	{
2106 		this.name = "search";
2107 		this.argumentsPattern = "<query>";
2108 		this.description = "Search for available packages.";
2109 		this.helpText = [
2110 			"Search all specified DUB registries for packages matching query."
2111 		];
2112 	}
2113 	override void prepare(scope CommandArgs args) {}
2114 	override int execute(Dub dub, string[] free_args, string[] app_args)
2115 	{
2116 		enforce(free_args.length == 1, "Expected one argument.");
2117 		auto res = dub.searchPackages(free_args[0]);
2118 		if (res.empty)
2119 		{
2120 			logError("No matches found.");
2121 			return 1;
2122 		}
2123 		auto justify = res
2124 			.map!((descNmatches) => descNmatches[1])
2125 			.joiner
2126 			.map!(m => m.name.length + m.version_.length)
2127 			.reduce!max + " ()".length;
2128 		justify += (~justify & 3) + 1; // round to next multiple of 4
2129 		foreach (desc, matches; res)
2130 		{
2131 			logInfo("==== %s ====", desc);
2132 			foreach (m; matches)
2133 				logInfo("%s%s", leftJustify(m.name ~ " (" ~ m.version_ ~ ")", justify), m.description);
2134 		}
2135 		return 0;
2136 	}
2137 }
2138 
2139 
2140 /******************************************************************************/
2141 /* OVERRIDES                                                                  */
2142 /******************************************************************************/
2143 
2144 class AddOverrideCommand : Command {
2145 	private {
2146 		bool m_system = false;
2147 	}
2148 
2149 	this() @safe pure nothrow
2150 	{
2151 		this.name = "add-override";
2152 		this.argumentsPattern = "<package> <version-spec> <target-path/target-version>";
2153 		this.description = "Adds a new package override.";
2154 		this.helpText = [
2155 		];
2156 	}
2157 
2158 	override void prepare(scope CommandArgs args)
2159 	{
2160 		args.getopt("system", &m_system, [
2161 			"Register system-wide instead of user-wide"
2162 		]);
2163 	}
2164 
2165 	override int execute(Dub dub, string[] free_args, string[] app_args)
2166 	{
2167 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
2168 		enforceUsage(free_args.length == 3, "Expected three arguments, not "~free_args.length.to!string);
2169 		auto scope_ = m_system ? LocalPackageType.system : LocalPackageType.user;
2170 		auto pack = free_args[0];
2171 		auto ver = Dependency(free_args[1]);
2172 		if (existsFile(NativePath(free_args[2]))) {
2173 			auto target = NativePath(free_args[2]);
2174 			if (!target.absolute) target = NativePath(getcwd()) ~ target;
2175 			dub.packageManager.addOverride(scope_, pack, ver, target);
2176 			logInfo("Added override %s %s => %s", pack, ver, target);
2177 		} else {
2178 			auto target = Version(free_args[2]);
2179 			dub.packageManager.addOverride(scope_, pack, ver, target);
2180 			logInfo("Added override %s %s => %s", pack, ver, target);
2181 		}
2182 		return 0;
2183 	}
2184 }
2185 
2186 class RemoveOverrideCommand : Command {
2187 	private {
2188 		bool m_system = false;
2189 	}
2190 
2191 	this() @safe pure nothrow
2192 	{
2193 		this.name = "remove-override";
2194 		this.argumentsPattern = "<package> <version-spec>";
2195 		this.description = "Removes an existing package override.";
2196 		this.helpText = [
2197 		];
2198 	}
2199 
2200 	override void prepare(scope CommandArgs args)
2201 	{
2202 		args.getopt("system", &m_system, [
2203 			"Register system-wide instead of user-wide"
2204 		]);
2205 	}
2206 
2207 	override int execute(Dub dub, string[] free_args, string[] app_args)
2208 	{
2209 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
2210 		enforceUsage(free_args.length == 2, "Expected two arguments, not "~free_args.length.to!string);
2211 		auto scope_ = m_system ? LocalPackageType.system : LocalPackageType.user;
2212 		dub.packageManager.removeOverride(scope_, free_args[0], Dependency(free_args[1]));
2213 		return 0;
2214 	}
2215 }
2216 
2217 class ListOverridesCommand : Command {
2218 	this() @safe pure nothrow
2219 	{
2220 		this.name = "list-overrides";
2221 		this.argumentsPattern = "";
2222 		this.description = "Prints a list of all local package overrides";
2223 		this.helpText = [
2224 			"Prints a list of all overridden packages added via \"dub add-override\"."
2225 		];
2226 	}
2227 	override void prepare(scope CommandArgs args) {}
2228 	override int execute(Dub dub, string[] free_args, string[] app_args)
2229 	{
2230 		void printList(in PackageOverride[] overrides, string caption)
2231 		{
2232 			if (overrides.length == 0) return;
2233 			logInfo("# %s", caption);
2234 			foreach (ovr; overrides) {
2235 				if (!ovr.targetPath.empty) logInfo("%s %s => %s", ovr.package_, ovr.version_, ovr.targetPath);
2236 				else logInfo("%s %s => %s", ovr.package_, ovr.version_, ovr.targetVersion);
2237 			}
2238 		}
2239 		printList(dub.packageManager.getOverrides(LocalPackageType.user), "User wide overrides");
2240 		printList(dub.packageManager.getOverrides(LocalPackageType.system), "System wide overrides");
2241 		return 0;
2242 	}
2243 }
2244 
2245 /******************************************************************************/
2246 /* Cache cleanup                                                              */
2247 /******************************************************************************/
2248 
2249 class CleanCachesCommand : Command {
2250 	this() @safe pure nothrow
2251 	{
2252 		this.name = "clean-caches";
2253 		this.argumentsPattern = "";
2254 		this.description = "Removes cached metadata";
2255 		this.helpText = [
2256 			"This command removes any cached metadata like the list of available packages and their latest version."
2257 		];
2258 	}
2259 
2260 	override void prepare(scope CommandArgs args) {}
2261 
2262 	override int execute(Dub dub, string[] free_args, string[] app_args)
2263 	{
2264 		return 0;
2265 	}
2266 }
2267 
2268 /******************************************************************************/
2269 /* DUSTMITE                                                                   */
2270 /******************************************************************************/
2271 
2272 class DustmiteCommand : PackageBuildCommand {
2273 	private {
2274 		int m_compilerStatusCode = int.min;
2275 		int m_linkerStatusCode = int.min;
2276 		int m_programStatusCode = int.min;
2277 		string m_compilerRegex;
2278 		string m_linkerRegex;
2279 		string m_programRegex;
2280 		string m_testPackage;
2281 		bool m_combined;
2282 		bool m_noRedirect;
2283 	}
2284 
2285 	this() @safe pure nothrow
2286 	{
2287 		this.name = "dustmite";
2288 		this.argumentsPattern = "<destination-path>";
2289 		this.acceptsAppArgs = true;
2290 		this.description = "Create reduced test cases for build errors";
2291 		this.helpText = [
2292 			"This command uses the Dustmite utility to isolate the cause of build errors in a DUB project.",
2293 			"",
2294 			"It will create a copy of all involved packages and run dustmite on this copy, leaving a reduced test case.",
2295 			"",
2296 			"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."
2297 		];
2298 	}
2299 
2300 	override void prepare(scope CommandArgs args)
2301 	{
2302 		args.getopt("compiler-status", &m_compilerStatusCode, ["The expected status code of the compiler run"]);
2303 		args.getopt("compiler-regex", &m_compilerRegex, ["A regular expression used to match against the compiler output"]);
2304 		args.getopt("linker-status", &m_linkerStatusCode, ["The expected status code of the linker run"]);
2305 		args.getopt("linker-regex", &m_linkerRegex, ["A regular expression used to match against the linker output"]);
2306 		args.getopt("program-status", &m_programStatusCode, ["The expected status code of the built executable"]);
2307 		args.getopt("program-regex", &m_programRegex, ["A regular expression used to match against the program output"]);
2308 		args.getopt("test-package", &m_testPackage, ["Perform a test run - usually only used internally"]);
2309 		args.getopt("combined", &m_combined, ["Builds multiple packages with one compiler run"]);
2310 		args.getopt("no-redirect", &m_noRedirect, ["Don't redirect stdout/stderr streams of the test command"]);
2311 		super.prepare(args);
2312 
2313 		// speed up loading when in test mode
2314 		if (m_testPackage.length) {
2315 			skipDubInitialization = true;
2316 			m_nodeps = true;
2317 		}
2318 	}
2319 
2320 	override int execute(Dub dub, string[] free_args, string[] app_args)
2321 	{
2322 		import std.format : formattedWrite;
2323 
2324 		if (m_testPackage.length) {
2325 			dub = new Dub(NativePath(getcwd()));
2326 
2327 			setupPackage(dub, m_testPackage);
2328 			m_defaultConfig = dub.project.getDefaultConfiguration(m_buildPlatform);
2329 
2330 			GeneratorSettings gensettings;
2331 			gensettings.platform = m_buildPlatform;
2332 			gensettings.config = m_buildConfig.length ? m_buildConfig : m_defaultConfig;
2333 			gensettings.buildType = m_buildType;
2334 			gensettings.compiler = m_compiler;
2335 			gensettings.buildSettings = m_buildSettings;
2336 			gensettings.combined = m_combined;
2337 			gensettings.filterVersions = m_filterVersions;
2338 			gensettings.run = m_programStatusCode != int.min || m_programRegex.length;
2339 			gensettings.runArgs = app_args;
2340 			gensettings.force = true;
2341 			gensettings.compileCallback = check(m_compilerStatusCode, m_compilerRegex);
2342 			gensettings.linkCallback = check(m_linkerStatusCode, m_linkerRegex);
2343 			gensettings.runCallback = check(m_programStatusCode, m_programRegex);
2344 			try dub.generateProject("build", gensettings);
2345 			catch (DustmiteMismatchException) {
2346 				logInfo("Dustmite test doesn't match.");
2347 				return 3;
2348 			}
2349 			catch (DustmiteMatchException) {
2350 				logInfo("Dustmite test matches.");
2351 				return 0;
2352 			}
2353 		} else {
2354 			enforceUsage(free_args.length == 1, "Expected destination path.");
2355 			auto path = NativePath(free_args[0]);
2356 			path.normalize();
2357 			enforceUsage(!path.empty, "Destination path must not be empty.");
2358 			if (!path.absolute) path = NativePath(getcwd()) ~ path;
2359 			enforceUsage(!path.startsWith(dub.rootPath), "Destination path must not be a sub directory of the tested package!");
2360 
2361 			setupPackage(dub, null);
2362 			auto prj = dub.project;
2363 			if (m_buildConfig.empty)
2364 				m_buildConfig = prj.getDefaultConfiguration(m_buildPlatform);
2365 
2366 			void copyFolderRec(NativePath folder, NativePath dstfolder)
2367 			{
2368 				mkdirRecurse(dstfolder.toNativeString());
2369 				foreach (de; iterateDirectory(folder.toNativeString())) {
2370 					if (de.name.startsWith(".")) continue;
2371 					if (de.isDirectory) {
2372 						copyFolderRec(folder ~ de.name, dstfolder ~ de.name);
2373 					} else {
2374 						if (de.name.endsWith(".o") || de.name.endsWith(".obj")) continue;
2375 						if (de.name.endsWith(".exe")) continue;
2376 						try copyFile(folder ~ de.name, dstfolder ~ de.name);
2377 						catch (Exception e) {
2378 							logWarn("Failed to copy file %s: %s", (folder ~ de.name).toNativeString(), e.msg);
2379 						}
2380 					}
2381 				}
2382 			}
2383 
2384 			static void fixPathDependency(string pack, ref Dependency dep) {
2385 				if (!dep.path.empty) {
2386 					auto mainpack = getBasePackageName(pack);
2387 					dep.path = NativePath("../") ~ mainpack;
2388 				}
2389 			}
2390 
2391 			void fixPathDependencies(ref PackageRecipe recipe, NativePath base_path)
2392 			{
2393 				foreach (name, ref dep; recipe.buildSettings.dependencies)
2394 					fixPathDependency(name, dep);
2395 
2396 				foreach (ref cfg; recipe.configurations)
2397 					foreach (name, ref dep; cfg.buildSettings.dependencies)
2398 						fixPathDependency(name, dep);
2399 
2400 				foreach (ref subp; recipe.subPackages)
2401 					if (subp.path.length) {
2402 						auto sub_path = base_path ~ NativePath(subp.path);
2403 						auto pack = prj.packageManager.getOrLoadPackage(sub_path);
2404 						fixPathDependencies(pack.recipe, sub_path);
2405 						pack.storeInfo(sub_path);
2406 					} else fixPathDependencies(subp.recipe, base_path);
2407 			}
2408 
2409 			bool[string] visited;
2410 			foreach (pack_; prj.getTopologicalPackageList()) {
2411 				auto pack = pack_.basePackage;
2412 				if (pack.name in visited) continue;
2413 				visited[pack.name] = true;
2414 				auto dst_path = path ~ pack.name;
2415 				logInfo("Copy package '%s' to destination folder...", pack.name);
2416 				copyFolderRec(pack.path, dst_path);
2417 
2418 				// adjust all path based dependencies
2419 				fixPathDependencies(pack.recipe, dst_path);
2420 
2421 				// overwrite package description file with additional version information
2422 				pack.storeInfo(dst_path);
2423 			}
2424 
2425 			logInfo("Executing dustmite...");
2426 			auto testcmd = appender!string();
2427 			testcmd.formattedWrite("%s dustmite --test-package=%s --build=%s --config=%s",
2428 				thisExePath, prj.name, m_buildType, m_buildConfig);
2429 
2430 			if (m_compilerName.length) testcmd.formattedWrite(" \"--compiler=%s\"", m_compilerName);
2431 			if (m_arch.length) testcmd.formattedWrite(" --arch=%s", m_arch);
2432 			if (m_compilerStatusCode != int.min) testcmd.formattedWrite(" --compiler-status=%s", m_compilerStatusCode);
2433 			if (m_compilerRegex.length) testcmd.formattedWrite(" \"--compiler-regex=%s\"", m_compilerRegex);
2434 			if (m_linkerStatusCode != int.min) testcmd.formattedWrite(" --linker-status=%s", m_linkerStatusCode);
2435 			if (m_linkerRegex.length) testcmd.formattedWrite(" \"--linker-regex=%s\"", m_linkerRegex);
2436 			if (m_programStatusCode != int.min) testcmd.formattedWrite(" --program-status=%s", m_programStatusCode);
2437 			if (m_programRegex.length) testcmd.formattedWrite(" \"--program-regex=%s\"", m_programRegex);
2438 			if (m_combined) testcmd ~= " --combined";
2439 
2440 			// --vquiet swallows dustmite's output ...
2441 			if (!m_noRedirect) testcmd ~= " --vquiet";
2442 
2443 			// TODO: pass *all* original parameters
2444 			logDiagnostic("Running dustmite: %s", testcmd);
2445 
2446 			string[] extraArgs;
2447 			if (m_noRedirect) extraArgs ~= "--no-redirect";
2448 			const cmd = "dustmite" ~ extraArgs ~ [path.toNativeString(), testcmd.data];
2449 			auto dmpid = spawnProcess(cmd);
2450 			return dmpid.wait();
2451 		}
2452 		return 0;
2453 	}
2454 
2455 	void delegate(int, string) check(int code_match, string regex_match)
2456 	{
2457 		return (code, output) {
2458 			import std.encoding;
2459 			import std.regex;
2460 
2461 			logInfo("%s", output);
2462 
2463 			if (code_match != int.min && code != code_match) {
2464 				logInfo("Exit code %s doesn't match expected value %s", code, code_match);
2465 				throw new DustmiteMismatchException;
2466 			}
2467 
2468 			if (regex_match.length > 0 && !match(output.sanitize, regex_match)) {
2469 				logInfo("Output doesn't match regex:");
2470 				logInfo("%s", output);
2471 				throw new DustmiteMismatchException;
2472 			}
2473 
2474 			if (code != 0 && code_match != int.min || regex_match.length > 0) {
2475 				logInfo("Tool failed, but matched either exit code or output - counting as match.");
2476 				throw new DustmiteMatchException;
2477 			}
2478 		};
2479 	}
2480 
2481 	static class DustmiteMismatchException : Exception {
2482 		this(string message = "", string file = __FILE__, int line = __LINE__, Throwable next = null)
2483 		{
2484 			super(message, file, line, next);
2485 		}
2486 	}
2487 
2488 	static class DustmiteMatchException : Exception {
2489 		this(string message = "", string file = __FILE__, int line = __LINE__, Throwable next = null)
2490 		{
2491 			super(message, file, line, next);
2492 		}
2493 	}
2494 }
2495 
2496 
2497 /******************************************************************************/
2498 /* CONVERT command                                                               */
2499 /******************************************************************************/
2500 
2501 class ConvertCommand : Command {
2502 	private {
2503 		string m_format;
2504 		bool m_stdout;
2505 	}
2506 
2507 	this() @safe pure nothrow
2508 	{
2509 		this.name = "convert";
2510 		this.argumentsPattern = "";
2511 		this.description = "Converts the file format of the package recipe.";
2512 		this.helpText = [
2513 			"This command will convert between JSON and SDLang formatted package recipe files.",
2514 			"",
2515 			"Warning: Beware that any formatting and comments within the package recipe will get lost in the conversion process."
2516 		];
2517 	}
2518 
2519 	override void prepare(scope CommandArgs args)
2520 	{
2521 		args.getopt("f|format", &m_format, ["Specifies the target package recipe format. Possible values:", "  json, sdl"]);
2522 		args.getopt("s|stdout", &m_stdout, ["Outputs the converted package recipe to stdout instead of writing to disk."]);
2523 	}
2524 
2525 	override int execute(Dub dub, string[] free_args, string[] app_args)
2526 	{
2527 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
2528 		enforceUsage(free_args.length == 0, "Unexpected arguments: "~free_args.join(" "));
2529 		enforceUsage(m_format.length > 0, "Missing target format file extension (--format=...).");
2530 		if (!loadCwdPackage(dub, true)) return 1;
2531 		dub.convertRecipe(m_format, m_stdout);
2532 		return 0;
2533 	}
2534 }
2535 
2536 
2537 /******************************************************************************/
2538 /* HELP                                                                       */
2539 /******************************************************************************/
2540 
2541 private {
2542 	enum shortArgColumn = 2;
2543 	enum longArgColumn = 6;
2544 	enum descColumn = 24;
2545 	enum lineWidth = 80 - 1;
2546 }
2547 
2548 private void showHelp(in CommandGroup[] commands, CommandArgs common_args)
2549 {
2550 	writeln(
2551 `USAGE: dub [--version] [<command>] [<options...>] [-- [<application arguments...>]]
2552 
2553 Manages the DUB project in the current directory. If the command is omitted,
2554 DUB will default to "run". When running an application, "--" can be used to
2555 separate DUB options from options passed to the application.
2556 
2557 Run "dub <command> --help" to get help for a specific command.
2558 
2559 You can use the "http_proxy" environment variable to configure a proxy server
2560 to be used for fetching packages.
2561 
2562 
2563 Available commands
2564 ==================`);
2565 
2566 	foreach (grp; commands) {
2567 		writeln();
2568 		writeWS(shortArgColumn);
2569 		writeln(grp.caption);
2570 		writeWS(shortArgColumn);
2571 		writerep!'-'(grp.caption.length);
2572 		writeln();
2573 		foreach (cmd; grp.commands) {
2574 			if (cmd.hidden) continue;
2575 			writeWS(shortArgColumn);
2576 			writef("%s %s", cmd.name, cmd.argumentsPattern);
2577 			auto chars_output = cmd.name.length + cmd.argumentsPattern.length + shortArgColumn + 1;
2578 			if (chars_output < descColumn) {
2579 				writeWS(descColumn - chars_output);
2580 			} else {
2581 				writeln();
2582 				writeWS(descColumn);
2583 			}
2584 			writeWrapped(cmd.description, descColumn, descColumn);
2585 		}
2586 	}
2587 	writeln();
2588 	writeln();
2589 	writeln(`Common options`);
2590 	writeln(`==============`);
2591 	writeln();
2592 	writeOptions(common_args);
2593 	writeln();
2594 	showVersion();
2595 }
2596 
2597 private void showVersion()
2598 {
2599 	writefln("DUB version %s, built on %s", getDUBVersion(), __DATE__);
2600 }
2601 
2602 private void showCommandHelp(Command cmd, CommandArgs args, CommandArgs common_args)
2603 {
2604 	writefln(`USAGE: dub %s %s [<options...>]%s`, cmd.name, cmd.argumentsPattern, cmd.acceptsAppArgs ? " [-- <application arguments...>]": null);
2605 	writeln();
2606 	foreach (ln; cmd.helpText)
2607 		ln.writeWrapped();
2608 
2609 	if (args.recognizedArgs.length) {
2610 		writeln();
2611 		writeln();
2612 		writeln("Command specific options");
2613 		writeln("========================");
2614 		writeln();
2615 		writeOptions(args);
2616 	}
2617 
2618 	writeln();
2619 	writeln();
2620 	writeln("Common options");
2621 	writeln("==============");
2622 	writeln();
2623 	writeOptions(common_args);
2624 	writeln();
2625 	writefln("DUB version %s, built on %s", getDUBVersion(), __DATE__);
2626 }
2627 
2628 private void writeOptions(CommandArgs args)
2629 {
2630 	foreach (arg; args.recognizedArgs) {
2631 		if (arg.hidden) continue;
2632 		auto names = arg.names.split("|");
2633 		assert(names.length == 1 || names.length == 2);
2634 		string sarg = names[0].length == 1 ? names[0] : null;
2635 		string larg = names[0].length > 1 ? names[0] : names.length > 1 ? names[1] : null;
2636 		if (sarg !is null) {
2637 			writeWS(shortArgColumn);
2638 			writef("-%s", sarg);
2639 			writeWS(longArgColumn - shortArgColumn - 2);
2640 		} else writeWS(longArgColumn);
2641 		size_t col = longArgColumn;
2642 		if (larg !is null) {
2643 			if (arg.defaultValue.peek!bool) {
2644 				writef("--%s", larg);
2645 				col += larg.length + 2;
2646 			} else {
2647 				writef("--%s=VALUE", larg);
2648 				col += larg.length + 8;
2649 			}
2650 		}
2651 		if (col < descColumn) {
2652 			writeWS(descColumn - col);
2653 		} else {
2654 			writeln();
2655 			writeWS(descColumn);
2656 		}
2657 		foreach (i, ln; arg.helpText) {
2658 			if (i > 0) writeWS(descColumn);
2659 			ln.writeWrapped(descColumn, descColumn);
2660 		}
2661 	}
2662 }
2663 
2664 private void writeWrapped(string string, size_t indent = 0, size_t first_line_pos = 0)
2665 {
2666 	// handle pre-indented strings and bullet lists
2667 	size_t first_line_indent = 0;
2668 	while (string.startsWith(" ")) {
2669 		string = string[1 .. $];
2670 		indent++;
2671 		first_line_indent++;
2672 	}
2673 	if (string.startsWith("- ")) indent += 2;
2674 
2675 	auto wrapped = string.wrap(lineWidth, getRepString!' '(first_line_pos+first_line_indent), getRepString!' '(indent));
2676 	wrapped = wrapped[first_line_pos .. $];
2677 	foreach (ln; wrapped.splitLines())
2678 		writeln(ln);
2679 }
2680 
2681 private void writeWS(size_t num) { writerep!' '(num); }
2682 private void writerep(char ch)(size_t num) { write(getRepString!ch(num)); }
2683 
2684 private string getRepString(char ch)(size_t len)
2685 {
2686 	static string buf;
2687 	if (len > buf.length) buf ~= [ch].replicate(len-buf.length);
2688 	return buf[0 .. len];
2689 }
2690 
2691 /***
2692 */
2693 
2694 
2695 private void enforceUsage(bool cond, string text)
2696 {
2697 	if (!cond) throw new UsageException(text);
2698 }
2699 
2700 private class UsageException : Exception {
2701 	this(string message, string file = __FILE__, int line = __LINE__, Throwable next = null)
2702 	{
2703 		super(message, file, line, next);
2704 	}
2705 }
2706 
2707 private void warnRenamed(string prev, string curr)
2708 {
2709 	logWarn("The '%s' Command was renamed to '%s'. Please update your scripts.", prev, curr);
2710 }
2711 
2712 private bool addDependency(Dub dub, ref PackageRecipe recipe, string depspec)
2713 {
2714 	Dependency dep;
2715 	const parts = splitPackageName(depspec);
2716 	const depname = parts.name;
2717 	if (parts.version_)
2718 		dep = Dependency(parts.version_);
2719 	else
2720 	{
2721 		try {
2722 			const ver = dub.getLatestVersion(depname);
2723 			dep = ver.isBranch ? Dependency(ver) : Dependency("~>" ~ ver.toString());
2724 		} catch (Exception e) {
2725 			logError("Could not find package '%s'.", depname);
2726 			logDebug("Full error: %s", e.toString().sanitize);
2727 			return false;
2728 		}
2729 	}
2730 	recipe.buildSettings.dependencies[depname] = dep;
2731 	logInfo("Adding dependency %s %s", depname, dep.versionSpec);
2732 	return true;
2733 }
2734 
2735 private struct PackageAndVersion
2736 {
2737 	string name;
2738 	string version_;
2739 }
2740 
2741 /* Split <package>=<version-specifier> and <package>@<version-specifier>
2742    into `name` and `version_`. */
2743 private PackageAndVersion splitPackageName(string packageName)
2744 {
2745 	// split <package>@<version-specifier>
2746 	auto parts = packageName.findSplit("@");
2747 	if (parts[1].empty) {
2748 		// split <package>=<version-specifier>
2749 		parts = packageName.findSplit("=");
2750 	}
2751 
2752 	PackageAndVersion p;
2753 	p.name = parts[0];
2754 	if (!parts[1].empty)
2755 		p.version_ = parts[2];
2756 	return p;
2757 }
2758 
2759 unittest
2760 {
2761 	// https://github.com/dlang/dub/issues/1681
2762 	assert(splitPackageName("") == PackageAndVersion("", null));
2763 
2764 	assert(splitPackageName("foo") == PackageAndVersion("foo", null));
2765 	assert(splitPackageName("foo=1.0.1") == PackageAndVersion("foo", "1.0.1"));
2766 	assert(splitPackageName("foo@1.0.1") == PackageAndVersion("foo", "1.0.1"));
2767 	assert(splitPackageName("foo@==1.0.1") == PackageAndVersion("foo", "==1.0.1"));
2768 	assert(splitPackageName("foo@>=1.0.1") == PackageAndVersion("foo", ">=1.0.1"));
2769 	assert(splitPackageName("foo@~>1.0.1") == PackageAndVersion("foo", "~>1.0.1"));
2770 	assert(splitPackageName("foo@<1.0.1") == PackageAndVersion("foo", "<1.0.1"));
2771 }
2772 
2773 private ulong canFindVersionSplitter(string packageName)
2774 {
2775 	// see splitPackageName
2776 	return packageName.canFind("@", "=");
2777 }
2778 
2779 unittest
2780 {
2781 	assert(!canFindVersionSplitter("foo"));
2782 	assert(canFindVersionSplitter("foo=1.0.1"));
2783 	assert(canFindVersionSplitter("foo@1.0.1"));
2784 	assert(canFindVersionSplitter("foo@==1.0.1"));
2785 	assert(canFindVersionSplitter("foo@>=1.0.1"));
2786 	assert(canFindVersionSplitter("foo@~>1.0.1"));
2787 	assert(canFindVersionSplitter("foo@<1.0.1"));
2788 }