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