Remove some superfluous blank lines
[xapian.git] / xapian-core / bin / xapian-inspect.cc
blobfe3266e05cc2b70473d7e2604ed9d1808c86c14f
1 /** @file xapian-inspect.cc
2 * @brief Inspect the contents of a glass table for development or debugging.
3 */
4 /* Copyright (C) 2007,2008,2009,2010,2011,2012,2017,2018 Olly Betts
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
21 #include <config.h>
23 #include <iomanip>
24 #include <iostream>
25 #include <string>
26 #include <cstdio> // For sprintf().
28 #include "glass_cursor.h"
29 #include "glass_table.h"
30 #include "glass_version.h"
31 #include "filetests.h"
32 #include "stringutils.h"
34 #include <xapian.h>
36 #include "gnu_getopt.h"
38 using namespace std;
40 #define PROG_NAME "xapian-inspect"
41 #define PROG_DESC "Inspect the contents of a glass table for development or debugging"
43 #define OPT_HELP 1
44 #define OPT_VERSION 2
46 static bool keys = true, tags = true;
48 static void show_usage() {
49 cout << "Usage: " PROG_NAME " [OPTIONS] TABLE\n"
50 " " PROG_NAME " [OPTIONS] -t TABLE DB\n\n"
51 "Options:\n"
52 " -t, --table=TABLE which table to inspect\n"
53 " --help display this help and exit\n"
54 " --version output version information and exit" << endl;
57 static void
58 display_nicely(const string & data) {
59 string::const_iterator i;
60 for (i = data.begin(); i != data.end(); ++i) {
61 unsigned char ch = *i;
62 if (ch < 32 || ch >= 127) {
63 switch (ch) {
64 case '\n': cout << "\\n"; break;
65 case '\r': cout << "\\r"; break;
66 case '\t': cout << "\\t"; break;
67 default: {
68 char buf[20];
69 sprintf(buf, "\\x%02x", int(ch));
70 cout << buf;
73 } else if (ch == '\\') {
74 cout << "\\\\";
75 } else {
76 cout << ch;
81 // Reverse display_nicely() encoding.
82 static string
83 unescape(const string& s)
85 auto bslash = s.find('\\');
86 if (bslash == string::npos)
87 return s;
88 string r(s, 0, bslash);
89 for (auto i = s.begin() + bslash; i != s.end(); ++i) {
90 char ch = *i;
91 if (ch == '\\') {
92 if (++i == s.end())
93 goto bad_escaping;
94 ch = *i;
95 switch (ch) {
96 case '\\':
97 break;
98 case '0':
99 // \0 is not output by display_nicely(), but would
100 // reasonably be expected to work.
101 ch = '\0';
102 break;
103 case 'n':
104 ch = '\n';
105 break;
106 case 'r':
107 ch = '\r';
108 break;
109 case 't':
110 ch = '\t';
111 break;
112 case 'x': {
113 if (++i == s.end())
114 goto bad_escaping;
115 char ch1 = *i;
116 if (++i == s.end())
117 goto bad_escaping;
118 char ch2 = *i;
119 if (!C_isxdigit(ch1) || !C_isxdigit(ch2))
120 goto bad_escaping;
121 ch = hex_digit(ch1) << 4 | hex_digit(ch2);
122 break;
124 default:
125 goto bad_escaping;
128 r += ch;
130 return r;
132 bad_escaping:
133 cout << "Bad escaping in specified key value, assuming literal"
134 << endl;
135 return s;
138 static void
139 show_help()
141 cout << "Commands:\n"
142 "next : Next entry (alias 'n' or '')\n"
143 "prev : Previous entry (alias 'p')\n"
144 "first : First entry (alias 'f')\n"
145 "last : Last entry (alias 'l')\n"
146 "goto K : Goto first entry with key >= K (alias 'g')\n"
147 "until K: Display entries until key >= K (alias 'u')\n"
148 "until : Display entries until end (alias 'u')\n"
149 "open T : Open table T instead (alias 'o') - e.g. open postlist\n"
150 "keys : Toggle showing keys (default: true) (alias 'k')\n"
151 "tags : Toggle showing tags (default: true) (alias 't')\n"
152 "help : Show this (alias 'h' or '?')\n"
153 "quit : Quit this utility (alias 'q')" << endl;
156 static void
157 show_entry(GlassCursor & cursor)
159 if (cursor.after_end()) {
160 cout << "After end" << endl;
161 return;
163 if (cursor.current_key.empty()) {
164 cout << "Before start" << endl;
165 return;
167 if (keys) {
168 cout << "Key: ";
169 display_nicely(cursor.current_key);
170 cout << endl;
172 if (tags) {
173 cout << "Tag: ";
174 cursor.read_tag();
175 display_nicely(cursor.current_tag);
176 cout << endl;
180 static void
181 do_until(GlassCursor & cursor, const string & target)
183 if (cursor.after_end()) {
184 cout << "At end already." << endl;
185 return;
188 if (!target.empty()) {
189 int cmp = target.compare(cursor.current_key);
190 if (cmp <= 0) {
191 if (cmp)
192 cout << "Already after specified key." << endl;
193 else
194 cout << "Already at specified key." << endl;
195 return;
199 size_t count = 0;
200 while (cursor.next()) {
201 ++count;
202 show_entry(cursor);
204 if (target.empty())
205 continue;
207 int cmp = target.compare(cursor.current_key);
208 if (cmp < 0) {
209 cout << "No exact match, stopping at entry after, "
210 "having advanced by " << count << " entries." << endl;
211 return;
213 if (cmp == 0) {
214 cout << "Advanced by " << count << " entries." << endl;
215 return;
219 cout << "Reached end, having advanced by " << count << " entries." << endl;
222 static void
223 goto_last(GlassCursor& cursor)
225 // To position on the last key we just do a < search for a key greater than
226 // any possible key - one longer than the longest possible length and
227 // consisting entirely of the highest sorting byte value.
228 cursor.find_entry_lt(string(GLASS_BTREE_MAX_KEY_LEN + 1, '\xff'));
232 main(int argc, char **argv)
234 static const struct option long_opts[] = {
235 {"table", required_argument, 0, 't'},
236 {"help", no_argument, 0, OPT_HELP},
237 {"version", no_argument, 0, OPT_VERSION},
238 {NULL, 0, 0, 0}
241 string table_name;
243 int c;
244 while ((c = gnu_getopt_long(argc, argv, "t:", long_opts, 0)) != -1) {
245 switch (c) {
246 case 't':
247 table_name = optarg;
248 break;
249 case OPT_HELP:
250 cout << PROG_NAME " - " PROG_DESC "\n\n";
251 show_usage();
252 exit(0);
253 case OPT_VERSION:
254 cout << PROG_NAME " - " PACKAGE_STRING << endl;
255 exit(0);
256 default:
257 show_usage();
258 exit(1);
262 if (argc - optind != 1) {
263 show_usage();
264 exit(1);
267 // Path to the DB to inspect (possibly with a table name appended).
268 string db_path(argv[optind]);
269 bool arg_is_directory = dir_exists(db_path);
270 if (arg_is_directory && table_name.empty()) {
271 cerr << argv[0]
272 << ": You need to specify a table name to inspect with --table."
273 << endl;
274 exit(1);
276 int single_file_fd = -1;
277 if (table_name.empty()) {
278 // db_path should be a path to a table, possibly without the extension
279 // or with just a trailing '.' (supported mostly for historical
280 // reasons). First normalise away any extension or trailing '.'.
281 if (endswith(db_path, "." GLASS_TABLE_EXTENSION)) {
282 db_path.resize(db_path.size() -
283 CONST_STRLEN(GLASS_TABLE_EXTENSION) - 1);
284 } else if (endswith(db_path, '.')) {
285 db_path.resize(db_path.size() - 1);
287 size_t slash = db_path.find_last_of(DIR_SEPS);
288 // If slash is std::string::npos, this assigns the whole of db_path to
289 // table_name, which is what we want.
290 table_name.assign(db_path, slash + 1, string::npos);
291 if (slash != string::npos) {
292 db_path.resize(slash);
293 } else {
294 db_path.resize(0);
296 } else if (!arg_is_directory) {
297 single_file_fd = open(db_path.c_str(), O_RDONLY | O_BINARY);
298 if (single_file_fd < 0) {
299 cerr << argv[0] << ": Couldn't open file '" << db_path << "'"
300 << endl;
301 exit(1);
305 GlassVersion* version_file_ptr;
306 if (single_file_fd < 0) {
307 version_file_ptr = new GlassVersion(db_path);
308 } else {
309 version_file_ptr = new GlassVersion(single_file_fd);
311 GlassVersion& version_file = *version_file_ptr;
313 version_file.read();
314 glass_revision_number_t rev = version_file.get_revision();
316 show_help();
317 cout << endl;
319 open_different_table:
320 try {
321 Glass::table_type table_code;
322 if (table_name == "docdata") {
323 table_code = Glass::DOCDATA;
324 } else if (table_name == "spelling") {
325 table_code = Glass::SPELLING;
326 } else if (table_name == "synonym") {
327 table_code = Glass::SYNONYM;
328 } else if (table_name == "termlist") {
329 table_code = Glass::TERMLIST;
330 } else if (table_name == "position") {
331 table_code = Glass::POSITION;
332 } else if (table_name == "postlist") {
333 table_code = Glass::POSTLIST;
334 } else {
335 cerr << "Unknown table: '" << table_name << "'" << endl;
336 exit(1);
339 GlassTable* table_ptr;
340 if (single_file_fd < 0) {
341 string table_path = db_path;
342 table_path += '/';
343 table_path += table_name;
344 table_path += '.';
345 table_ptr = new GlassTable("", table_path, true);
346 } else {
347 auto offset = version_file.get_offset();
348 table_ptr = new GlassTable("", single_file_fd, offset, true);
350 GlassTable& table = *table_ptr;
352 table.open(0, version_file.get_root(table_code), rev);
353 if (table.empty()) {
354 cout << "No entries!" << endl;
355 exit(0);
357 cout << "Table has " << table.get_entry_count() << " entries" << endl;
359 GlassCursor cursor(&table);
360 cursor.rewind();
361 cursor.next();
363 while (!cin.eof()) {
364 show_entry(cursor);
365 wait_for_input:
366 cout << "? " << flush;
368 string input;
369 getline(cin, input);
370 if (cin.eof()) break;
372 if (endswith(input, '\r'))
373 input.resize(input.size() - 1);
375 if (input.empty() || input == "n" || input == "next") {
376 if (cursor.after_end()) {
377 cout << "At end already." << endl;
378 goto wait_for_input;
380 (void)cursor.next();
381 continue;
382 } else if (input == "p" || input == "prev") {
383 if (cursor.current_key.empty()) {
384 cout << "Before start already." << endl;
385 goto wait_for_input;
387 // If the cursor has fallen off the end, point it back at the
388 // last entry.
389 if (cursor.after_end()) {
390 goto_last(cursor);
391 continue;
393 cursor.find_entry_lt(cursor.current_key);
394 continue;
395 } else if (startswith(input, "u ")) {
396 do_until(cursor, unescape(input.substr(2)));
397 goto wait_for_input;
398 } else if (startswith(input, "until ")) {
399 do_until(cursor, unescape(input.substr(6)));
400 goto wait_for_input;
401 } else if (input == "u" || input == "until") {
402 do_until(cursor, string());
403 goto wait_for_input;
404 } else if (input == "f" || input == "first") {
405 cursor.rewind();
406 cursor.next();
407 continue;
408 } else if (input == "l" || input == "last") {
409 goto_last(cursor);
410 continue;
411 } else if (startswith(input, "g ")) {
412 if (!cursor.find_entry_ge(unescape(input.substr(2)))) {
413 cout << "No exact match, going to entry after." << endl;
415 continue;
416 } else if (startswith(input, "goto ")) {
417 if (!cursor.find_entry_ge(unescape(input.substr(5)))) {
418 cout << "No exact match, going to entry after." << endl;
420 continue;
421 } else if (startswith(input, "o ") || startswith(input, "open ")) {
422 size_t trim = (input[1] == ' ' ? 2 : 5);
423 table_name.assign(input, trim, string::npos);
424 if (endswith(table_name, "." GLASS_TABLE_EXTENSION))
425 table_name.resize(table_name.size() -
426 CONST_STRLEN(GLASS_TABLE_EXTENSION) - 1);
427 else if (endswith(table_name, '.'))
428 table_name.resize(table_name.size() - 1);
429 goto open_different_table;
430 } else if (input == "t" || input == "tags") {
431 tags = !tags;
432 cout << "Showing tags: " << boolalpha << tags << endl;
433 } else if (input == "k" || input == "keys") {
434 keys = !keys;
435 cout << "Showing keys: " << boolalpha << keys << endl;
436 } else if (input == "q" || input == "quit") {
437 break;
438 } else if (input == "h" || input == "help" || input == "?") {
439 show_help();
440 goto wait_for_input;
441 } else {
442 cout << "Unknown command." << endl;
443 goto wait_for_input;
446 } catch (const Xapian::Error &error) {
447 cerr << argv[0] << ": " << error.get_description() << endl;
448 exit(1);