1/*
2 * Copyright 2013, Haiku, Inc. All Rights Reserved.
3 * Distributed under the terms of the MIT License.
4 *
5 * Authors:
6 *		Ingo Weinhold <ingo_weinhold@gmx.de>
7 */
8
9
10#include <errno.h>
11#include <getopt.h>
12#include <stdio.h>
13#include <stdlib.h>
14#include <sys/ioctl.h>
15#include <unistd.h>
16
17#include <algorithm>
18#include <set>
19
20#include <Locale.h>
21
22#include <package/solver/SolverPackage.h>
23#include <TextTable.h>
24
25#include "Command.h"
26#include "PackageManager.h"
27#include "pkgman.h"
28
29
30// TODO: internationalization!
31// The table code doesn't support full-width characters yet.
32
33
34using namespace BPackageKit;
35
36
37static const char* const kShortUsage =
38	"  %command% ( <search-string> | --all | -a )\n"
39	"    Searches for packages matching <search-string>.\n";
40
41static const char* const kLongUsage =
42	"Usage: %program% %command% ( <search-string> | --all | -a )\n"
43	"Searches for packages matching <search-string>.\n"
44	"\n"
45	"Options:\n"
46	"  -a, --all\n"
47	"    List all packages. Specified instead of <search-string>.\n"
48	"  --debug <level>\n"
49	"    Print debug output. <level> should be between 0 (no debug output,\n"
50	"    the default) and 10 (most debug output).\n"
51	"  -D, --details\n"
52	"    Print more details. Matches in each installation location and each\n"
53	"    repository will be listed individually with their version.\n"
54	"  -i, --installed-only\n"
55	"    Only find installed packages.\n"
56	"  -u, --uninstalled-only\n"
57	"    Only find not installed packages.\n"
58	"  -r, --requirements\n"
59	"    Search packages with <search-string> as requirements.\n"
60	"  -s <scope>, --search-scope=<scope>\n"
61	"    Search for packages containing <search-string> only on the given scope.\n"
62	"    <scope> must be either \"name\" or \"full\"."
63	"\n"
64	"Status flags in non-detailed listings:\n"
65	"  S - installed in system with a matching version in a repository\n"
66	"  s - installed in system without a matching version in a repository\n"
67	"  H - installed in home with a matching version in a repository\n"
68	"  h - installed in home without a matching version in a repository\n"
69	"  v - multiple different versions available in repositories\n"
70	"\n";
71
72
73DEFINE_COMMAND(SearchCommand, "search", kShortUsage, kLongUsage,
74	COMMAND_CATEGORY_PACKAGES)
75
76
77static int
78get_terminal_width()
79{
80    int fd = fileno(stdout);
81    struct winsize windowSize;
82	if (isatty(fd) == 1 && ioctl(fd, TIOCGWINSZ, &windowSize) == 0)
83		return windowSize.ws_col;
84
85    return INT_MAX;
86}
87
88
89struct PackageComparator {
90	PackageComparator(const BSolverRepository* systemRepository,
91		const BSolverRepository* homeRepository)
92		:
93		fSystemRepository(systemRepository),
94		fHomeRepository(homeRepository)
95	{
96		BLocale::Default()->GetCollator(&fCollator);
97		fCollator.SetNumericSorting(true);
98	}
99
100	int operator()(const BSolverPackage* a, const BSolverPackage* b) const
101	{
102		int cmp = fCollator.Compare(a->Name().String(), b->Name().String());
103		if (cmp != 0)
104			return cmp;
105
106		// Names are equal. Sort by installation location and then by repository
107		// name.
108		if (a->Repository() == b->Repository())
109			return 0;
110
111		if (a->Repository() == fSystemRepository)
112			return -1;
113		if (b->Repository() == fSystemRepository)
114			return 1;
115		if (a->Repository() == fHomeRepository)
116			return -1;
117		if (b->Repository() == fHomeRepository)
118			return 1;
119
120		return a->Repository()->Name().Compare(b->Repository()->Name());
121	}
122
123private:
124	const BSolverRepository*	fSystemRepository;
125	const BSolverRepository*	fHomeRepository;
126
127	BCollator					fCollator;
128};
129
130
131static int
132compare_packages(const BSolverPackage* a, const BSolverPackage* b,
133	void* comparator)
134{
135	return (*(PackageComparator*)comparator)(a, b);
136}
137
138
139int
140SearchCommand::Execute(int argc, const char* const* argv)
141{
142	bool installedOnly = false;
143	bool uninstalledOnly = false;
144	bool nameOnly = false;
145	bool fullSearch = false;
146	bool listAll = false;
147	bool details = false;
148	bool requirements = false;
149
150	while (true) {
151		static struct option sLongOptions[] = {
152			{ "all", no_argument, 0, 'a' },
153			{ "debug", required_argument, 0, OPTION_DEBUG },
154			{ "details", no_argument, 0, 'D' },
155			{ "help", no_argument, 0, 'h' },
156			{ "installed-only", no_argument, 0, 'i' },
157			{ "uninstalled-only", no_argument, 0, 'u' },
158			{ "requirements", no_argument, 0, 'r' },
159			{ "search-scope", required_argument, NULL, 's' },
160			{ 0, 0, 0, 0 }
161		};
162
163		opterr = 0; // don't print errors
164		int c = getopt_long(argc, (char**)argv, "aDhiurs:", sLongOptions, NULL);
165		if (c == -1)
166			break;
167
168		if (fCommonOptions.HandleOption(c))
169			continue;
170
171		switch (c) {
172			case 'a':
173				listAll = true;
174				break;
175
176			case 'D':
177				details = true;
178				break;
179
180			case 'h':
181				PrintUsageAndExit(false);
182				break;
183
184			case 'i':
185				installedOnly = true;
186				uninstalledOnly = false;
187				break;
188
189			case 'u':
190				uninstalledOnly = true;
191				installedOnly = false;
192				break;
193
194			case 'r':
195				requirements = true;
196				break;
197
198			case 's':
199				if (strcmp(optarg, "name") == 0)
200					nameOnly = true;
201				else if (strcmp(optarg, "full") == 0)
202					fullSearch = true;
203				else
204					fprintf(stderr, "Warning: Invalid search scope (%s). Using default.\n",
205						optarg);
206				break;
207
208			default:
209				PrintUsageAndExit(true);
210				break;
211		}
212	}
213
214	// The remaining argument is the search string. Ignored when --all has been
215	// specified.
216	if ((!listAll && argc != optind + 1) || (listAll && requirements))
217		PrintUsageAndExit(true);
218
219	const char* searchString = listAll ? "" : argv[optind++];
220
221	// create the solver
222	PackageManager packageManager(B_PACKAGE_INSTALLATION_LOCATION_HOME);
223	packageManager.SetDebugLevel(fCommonOptions.DebugLevel());
224	packageManager.Init(
225		(!uninstalledOnly ? PackageManager::B_ADD_INSTALLED_REPOSITORIES : 0)
226			| (!installedOnly ? PackageManager::B_ADD_REMOTE_REPOSITORIES : 0));
227
228	uint32 flags = BSolver::B_FIND_CASE_INSENSITIVE | BSolver::B_FIND_IN_NAME
229		| BSolver::B_FIND_IN_SUMMARY | BSolver::B_FIND_IN_PROVIDES;
230
231	if (nameOnly)
232		flags = BSolver::B_FIND_CASE_INSENSITIVE | BSolver::B_FIND_IN_NAME;
233
234	if (fullSearch)
235		flags = BSolver::B_FIND_CASE_INSENSITIVE | BSolver::B_FIND_IN_NAME
236			| BSolver::B_FIND_IN_SUMMARY | BSolver::B_FIND_IN_DESCRIPTION
237			| BSolver::B_FIND_IN_PROVIDES;
238
239	if (requirements)
240		flags = BSolver::B_FIND_IN_REQUIRES;
241
242	// search
243	BObjectList<BSolverPackage> packages;
244	status_t error = packageManager.Solver()->FindPackages(searchString,
245		flags, packages);
246	if (error != B_OK)
247		DIE(error, "searching packages failed");
248
249	if (packages.IsEmpty()) {
250		printf("No matching packages found.\n");
251		return 0;
252	}
253
254	// sort packages by name and installation location/repository
255	const BSolverRepository* systemRepository
256		= static_cast<const BSolverRepository*>(
257			packageManager.SystemRepository());
258	const BSolverRepository* homeRepository
259		= static_cast<const BSolverRepository*>(
260			packageManager.HomeRepository());
261	PackageComparator comparator(systemRepository, homeRepository);
262	packages.SortItems(&compare_packages, &comparator);
263
264	// print table
265	TextTable table;
266
267	if (details) {
268		table.AddColumn("Repository");
269		table.AddColumn("Name");
270		table.AddColumn("Version");
271		table.AddColumn("Arch");
272
273		int32 packageCount = packages.CountItems();
274		for (int32 i = 0; i < packageCount; i++) {
275			BSolverPackage* package = packages.ItemAt(i);
276
277			BString repository = "";
278			if (package->Repository() == systemRepository)
279				repository = "<system>";
280			else if (package->Repository() == homeRepository)
281				repository = "<home>";
282			else
283				repository = package->Repository()->Name();
284
285			table.SetTextAt(i, 0, repository);
286			table.SetTextAt(i, 1, package->Name());
287			table.SetTextAt(i, 2, package->Version().ToString());
288			table.SetTextAt(i, 3, package->Info().ArchitectureName());
289		}
290	} else {
291		table.AddColumn("Status");
292		table.AddColumn("Name");
293		table.AddColumn("Description", B_ALIGN_LEFT, true);
294
295		int32 packageCount = packages.CountItems();
296		for (int32 i = 0; i < packageCount;) {
297			// find the next group of equally named packages
298			int32 groupStart = i;
299			std::set<BPackageVersion> versions;
300			BSolverPackage* systemPackage = NULL;
301			BSolverPackage* homePackage = NULL;
302			while (i < packageCount) {
303				BSolverPackage* package = packages.ItemAt(i);
304				if (i > groupStart
305					&& package->Name() != packages.ItemAt(groupStart)->Name()) {
306					break;
307				}
308
309				if (package->Repository() == systemRepository)
310					systemPackage = package;
311				else if (package->Repository() == homeRepository)
312					homePackage = package;
313				else
314					versions.insert(package->Version());
315
316				i++;
317			}
318
319			// add a table row for the group
320			BString status;
321			if (systemPackage != NULL) {
322				status << (versions.find(systemPackage->Version())
323					!= versions.end() ? 'S' : 's');
324			}
325			if (homePackage != NULL) {
326				status << (versions.find(homePackage->Version())
327					!= versions.end() ? 'H' : 'h');
328			}
329			if (versions.size() > 1)
330				status << 'v';
331
332			int32 rowIndex = table.CountRows();
333			BSolverPackage* package = packages.ItemAt(groupStart);
334			table.SetTextAt(rowIndex, 0, status);
335			table.SetTextAt(rowIndex, 1, package->Name());
336			table.SetTextAt(rowIndex, 2, package->Info().Summary());
337		}
338	}
339
340	table.Print(get_terminal_width());
341
342	return 0;
343}
344