Line data Source code
1 : /*
2 : * SPDX-License-Identifier: MPL-2.0
3 : *
4 : * This Source Code Form is subject to the terms of the Mozilla Public
5 : * License, v. 2.0. If a copy of the MPL was not distributed with this
6 : * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 : *
8 : * Copyright 2024 MonetDB Foundation;
9 : * Copyright August 2008 - 2023 MonetDB B.V.;
10 : * Copyright 1997 - July 2008 CWI.
11 : */
12 :
13 : /*
14 : * Readline specific stuff
15 : */
16 : #include "monetdb_config.h"
17 :
18 : #ifdef HAVE_LIBREADLINE
19 : #include <fcntl.h>
20 : #include <unistd.h>
21 :
22 : #include <readline/readline.h>
23 : #include <readline/history.h>
24 : #include "ReadlineTools.h"
25 : #define LIBMUTILS 1
26 : #include "mutils.h"
27 :
28 : #ifdef HAVE_STRINGS_H
29 : #include <strings.h> /* for strncasecmp */
30 : #endif
31 :
32 : #ifndef NATIVE_WIN32
33 : /* for umask */
34 : #include <sys/types.h>
35 : #include <sys/stat.h>
36 : #endif
37 :
38 : static const char *sql_commands[] = {
39 : "SELECT",
40 : "INSERT",
41 : "UPDATE",
42 : "SET",
43 : "DELETE",
44 : "COMMIT",
45 : "ROLLBACK",
46 : "DROP TABLE",
47 : "CREATE",
48 : "ALTER",
49 : "RELEASE SAVEPOINT",
50 : "START TRANSACTION",
51 : 0,
52 : };
53 :
54 : static Mapi _mid;
55 : static char _history_file[FILENAME_MAX];
56 : static bool _save_history = false;
57 : static const char *language;
58 :
59 : static char *
60 0 : sql_tablename_generator(const char *text, int state)
61 : {
62 :
63 0 : static int64_t seekpos, rowcount;
64 0 : static size_t len;
65 0 : static MapiHdl table_hdl;
66 :
67 0 : if (!state) {
68 0 : char *query;
69 :
70 0 : seekpos = 0;
71 0 : len = strlen(text);
72 0 : if ((query = malloc(len + 150)) == NULL)
73 : return NULL;
74 0 : snprintf(query, len + 150, "SELECT t.\"name\", s.\"name\" FROM \"sys\".\"tables\" t, \"sys\".\"schemas\" s where t.system = FALSE AND t.schema_id = s.id AND t.\"name\" like '%s%%'", text);
75 0 : table_hdl = mapi_query(_mid, query);
76 0 : free(query);
77 0 : if (table_hdl == NULL || mapi_error(_mid)) {
78 0 : if (table_hdl) {
79 0 : mapi_explain_query(table_hdl, stderr);
80 0 : mapi_close_handle(table_hdl);
81 : } else
82 0 : mapi_explain(_mid, stderr);
83 0 : return NULL;
84 : }
85 0 : mapi_fetch_all_rows(table_hdl);
86 0 : rowcount = mapi_get_row_count(table_hdl);
87 : }
88 :
89 0 : while (seekpos < rowcount) {
90 0 : if (mapi_seek_row(table_hdl, seekpos++, MAPI_SEEK_SET) != MOK ||
91 0 : mapi_fetch_row(table_hdl) <= 0)
92 0 : continue;
93 0 : return strdup(mapi_fetch_field(table_hdl, 0));
94 : }
95 :
96 : return NULL;
97 : }
98 :
99 : /* SQL commands (at start of line) */
100 : static char *
101 0 : sql_command_generator(const char *text, int state)
102 : {
103 0 : static size_t idx, len;
104 0 : const char *name;
105 :
106 0 : if (!state) {
107 0 : idx = 0;
108 0 : len = strlen(text);
109 : }
110 :
111 0 : while ((name = sql_commands[idx++])) {
112 0 : if (strncasecmp(name, text, len) == 0)
113 0 : return strdup(name);
114 : }
115 :
116 : return NULL;
117 : }
118 :
119 : static char **
120 0 : sql_completion(const char *text, int start, int end)
121 : {
122 0 : char **matches;
123 :
124 0 : matches = (char **) NULL;
125 :
126 0 : (void) end;
127 :
128 : /* FIXME: Nice, context-sensitive completion strategy should go here */
129 0 : if (strcmp(language, "sql") == 0) {
130 0 : if (start == 0) {
131 0 : matches = rl_completion_matches(text, sql_command_generator);
132 : } else {
133 0 : matches = rl_completion_matches(text, sql_tablename_generator);
134 : }
135 : }
136 0 : if (strcmp(language, "mal") == 0) {
137 0 : matches = rl_completion_matches(text, sql_tablename_generator);
138 : }
139 :
140 0 : return (matches);
141 : }
142 :
143 : /* The MAL completion help */
144 :
145 : static const char *mal_commands[] = {
146 : "address",
147 : "atom",
148 : "barrier",
149 : "catch",
150 : "command",
151 : "comment",
152 : "exit",
153 : "end",
154 : "function",
155 : "leave",
156 : "pattern",
157 : "module",
158 : "raise",
159 : "redo",
160 : 0
161 : };
162 :
163 : #ifdef illegal_ESC_binding
164 : /* see also init_readline() below */
165 : static int
166 : mal_help(int cnt, int key)
167 : {
168 : char *name, *c, *buf;
169 : int64_t seekpos = 0, rowcount;
170 : MapiHdl table_hdl;
171 :
172 : (void) cnt;
173 : (void) key;
174 :
175 : c = rl_line_buffer + strlen(rl_line_buffer) - 1;
176 : while (c > rl_line_buffer && isspace((unsigned char) *c))
177 : c--;
178 : while (c > rl_line_buffer && !isspace((unsigned char) *c))
179 : c--;
180 : if ((buf = malloc(strlen(c) + 20)) == NULL)
181 : return 0;
182 : snprintf(buf, strlen(c) + 20, "manual.help(\"%s\");", c);
183 : table_hdl = mapi_query(_mid, buf);
184 : free(buf);
185 : if (table_hdl == NULL || mapi_error(_mid)) {
186 : if (table_hdl) {
187 : mapi_explain_query(table_hdl, stderr);
188 : mapi_close_handle(table_hdl);
189 : } else
190 : mapi_explain(_mid, stderr);
191 : return 0;
192 : }
193 : mapi_fetch_all_rows(table_hdl);
194 : rowcount = mapi_get_row_count(table_hdl);
195 :
196 : printf("\n");
197 : while (seekpos < rowcount) {
198 : if (mapi_seek_row(table_hdl, seekpos++, MAPI_SEEK_SET) != MOK ||
199 : mapi_fetch_row(table_hdl) <= 0)
200 : continue;
201 : name = mapi_fetch_field(table_hdl, 0);
202 : if (name)
203 : printf("%s\n", name);
204 : }
205 : return key;
206 : }
207 : #endif
208 :
209 : static char *
210 0 : mal_command_generator(const char *text, int state)
211 : {
212 :
213 0 : static int idx;
214 0 : static int64_t seekpos, rowcount;
215 0 : static size_t len;
216 0 : static MapiHdl table_hdl;
217 0 : const char *name;
218 0 : char *buf;
219 :
220 : /* we pick our own portion of the linebuffer */
221 0 : text = rl_line_buffer + strlen(rl_line_buffer) - 1;
222 0 : while (text > rl_line_buffer && !isspace((unsigned char) *text))
223 0 : text--;
224 0 : if (!state) {
225 0 : idx = 0;
226 0 : len = strlen(text);
227 : }
228 :
229 : /* printf("expand test:%s\n",text);
230 : printf("currentline:%s\n",rl_line_buffer); */
231 :
232 0 : while (mal_commands[idx] && (name = mal_commands[idx++])) {
233 0 : if (strncasecmp(name, text, len) == 0)
234 0 : return strdup(name);
235 : }
236 : /* try the server to answer */
237 0 : if (!state) {
238 0 : char *c;
239 0 : c = strstr(text, ":=");
240 0 : if (c)
241 0 : text = c + 2;
242 0 : while (isspace((unsigned char) *text))
243 0 : text++;
244 0 : if ((buf = malloc(strlen(text) + 32)) == NULL)
245 : return NULL;
246 0 : if (strchr(text, '.') == NULL)
247 0 : snprintf(buf, strlen(text) + 32,
248 : "manual.completion(\"%s.*(\");", text);
249 : else
250 0 : snprintf(buf, strlen(text) + 32,
251 : "manual.completion(\"%s(\");", text);
252 0 : seekpos = 0;
253 0 : table_hdl = mapi_query(_mid, buf);
254 0 : free(buf);
255 0 : if (table_hdl == NULL || mapi_error(_mid)) {
256 0 : if (table_hdl) {
257 0 : mapi_explain_query(table_hdl, stderr);
258 0 : mapi_close_handle(table_hdl);
259 : } else
260 0 : mapi_explain(_mid, stderr);
261 0 : return NULL;
262 : }
263 0 : mapi_fetch_all_rows(table_hdl);
264 0 : rowcount = mapi_get_row_count(table_hdl);
265 : }
266 :
267 0 : while (seekpos < rowcount) {
268 0 : if (mapi_seek_row(table_hdl, seekpos++, MAPI_SEEK_SET) != MOK ||
269 0 : mapi_fetch_row(table_hdl) <= 0)
270 0 : continue;
271 0 : name = mapi_fetch_field(table_hdl, 0);
272 0 : if (name)
273 0 : return strdup(name);
274 : }
275 :
276 : return NULL;
277 : }
278 :
279 : static char **
280 0 : mal_completion(const char *text, int start, int end)
281 : {
282 0 : (void) start;
283 0 : (void) end;
284 :
285 : /* FIXME: Nice, context-sensitive completion strategy should go here */
286 0 : return rl_completion_matches(text, mal_command_generator);
287 : }
288 :
289 :
290 : rl_completion_func_t *
291 0 : suspend_completion(void)
292 : {
293 0 : rl_completion_func_t *func = rl_attempted_completion_function;
294 :
295 0 : rl_attempted_completion_function = NULL;
296 0 : return func;
297 : }
298 :
299 : void
300 0 : continue_completion(rl_completion_func_t * func)
301 : {
302 0 : rl_attempted_completion_function = func;
303 0 : }
304 :
305 : static void
306 0 : readline_show_error(const char *msg) {
307 0 : rl_save_prompt();
308 0 : rl_message(msg);
309 0 : rl_restore_prompt();
310 0 : rl_clear_message();
311 0 : }
312 :
313 : #ifndef BUFFER_SIZE
314 : #define BUFFER_SIZE 1024
315 : #endif
316 :
317 : static int
318 0 : invoke_editor(int cnt, int key) {
319 0 : char editor_command[BUFFER_SIZE];
320 0 : char *read_buff = NULL;
321 0 : char *editor = NULL;
322 0 : FILE *fp = NULL;
323 0 : long content_len;
324 0 : size_t read_bytes, idx;
325 :
326 0 : (void) cnt;
327 0 : (void) key;
328 :
329 : #ifdef NATIVE_WIN32
330 : char *mytemp;
331 : char template[] = "mclient_temp_XXXXXX";
332 : if ((mytemp = _mktemp(template)) == NULL) {
333 : readline_show_error("invoke_editor: Cannot create temp file\n");
334 : goto bailout;
335 : }
336 : if ((fp = MT_fopen(mytemp, "r+")) == NULL) {
337 : // Notify the user that we cannot create temp file
338 : readline_show_error("invoke_editor: Cannot create temp file\n");
339 : goto bailout;
340 : }
341 : #else
342 0 : int mytemp;
343 0 : char template[] = "/tmp/mclient_temp_XXXXXX";
344 0 : mode_t msk = umask(077);
345 0 : mytemp = mkstemp(template);
346 0 : (void) umask(msk);
347 0 : if (mytemp == -1) {
348 0 : readline_show_error("invoke_editor: Cannot create temp file\n");
349 0 : goto bailout;
350 : }
351 0 : if ((fp = fdopen(mytemp, "r+")) == NULL) {
352 : // Notify the user that we cannot create temp file
353 0 : readline_show_error("invoke_editor: Cannot create temp file\n");
354 0 : goto bailout;
355 : }
356 : #endif
357 :
358 0 : fwrite(rl_line_buffer, sizeof(char), rl_end, fp);
359 0 : fflush(fp);
360 :
361 0 : editor = getenv("VISUAL");
362 0 : if (editor == NULL) {
363 0 : editor = getenv("EDITOR");
364 0 : if (editor == NULL) {
365 0 : readline_show_error("invoke_editor: EDITOR/VISUAL env variable not set\n");
366 0 : goto bailout;
367 : }
368 : }
369 :
370 0 : snprintf(editor_command, BUFFER_SIZE, "%s %s", editor, template);
371 0 : if (system(editor_command) != 0) {
372 0 : readline_show_error("invoke_editor: Starting editor failed\n");
373 0 : goto bailout;
374 : }
375 :
376 0 : fseek(fp, 0L, SEEK_END);
377 0 : content_len = ftell(fp);
378 0 : rewind(fp);
379 :
380 0 : if (content_len > 0) {
381 0 : read_buff = (char *)malloc(content_len + 1);
382 0 : if (read_buff == NULL) {
383 0 : readline_show_error("invoke_editor: Cannot allocate memory\n");
384 0 : goto bailout;
385 : }
386 :
387 0 : read_bytes = fread(read_buff, sizeof(char), (size_t) content_len, fp);
388 0 : if (read_bytes != (size_t) content_len) {
389 0 : readline_show_error("invoke_editor: Did not read from file correctly\n");
390 0 : goto bailout;
391 : }
392 :
393 0 : read_buff[read_bytes] = 0;
394 :
395 : /* Remove trailing whitespace */
396 0 : idx = read_bytes - 1;
397 0 : while (isspace((unsigned char) read_buff[idx])) {
398 0 : read_buff[idx] = 0;
399 0 : idx--;
400 : }
401 :
402 0 : rl_replace_line(read_buff, 0);
403 0 : rl_point = (int)(idx + 1); // place the point one character after the end of the string
404 :
405 0 : free(read_buff);
406 : } else {
407 0 : rl_replace_line("", 0);
408 0 : rl_point = 0;
409 : }
410 :
411 0 : fclose(fp);
412 0 : (void) MT_remove(template);
413 :
414 0 : return 0;
415 :
416 0 : bailout:
417 0 : if (fp)
418 0 : fclose(fp);
419 0 : free(read_buff);
420 0 : (void) MT_remove(template);
421 0 : return 1;
422 : }
423 :
424 : void
425 0 : init_readline(Mapi mid, const char *lang, bool save_history)
426 : {
427 0 : language = lang;
428 0 : _mid = mid;
429 : /* Allow conditional parsing of the ~/.inputrc file. */
430 0 : rl_readline_name = "MapiClient";
431 : /* Tell the completer that we want to try our own completion
432 : * before std completion (filename) kicks in. */
433 0 : if (strcmp(language, "sql") == 0) {
434 0 : rl_attempted_completion_function = sql_completion;
435 0 : } else if (strcmp(language, "mal") == 0) {
436 : /* recognize the help function, should react to <FCN2> */
437 : #ifdef illegal_ESC_binding
438 : rl_bind_key('\033', mal_help);
439 : #endif
440 0 : rl_attempted_completion_function = mal_completion;
441 : }
442 :
443 0 : rl_add_funmap_entry("invoke-editor", invoke_editor);
444 0 : rl_bind_keyseq("\\M-e", invoke_editor);
445 :
446 0 : if (save_history) {
447 0 : int len;
448 0 : if (getenv("HOME") != NULL) {
449 0 : len = snprintf(_history_file, FILENAME_MAX,
450 : "%s/.mapiclient_history_%s",
451 : getenv("HOME"), language);
452 0 : if (len == -1 || len >= FILENAME_MAX)
453 0 : fprintf(stderr, "Warning: history filename path is too large\n");
454 : else
455 0 : _save_history = true;
456 : }
457 0 : if (_save_history) {
458 0 : FILE *f;
459 0 : switch (read_history(_history_file)) {
460 : case 0:
461 : /* success */
462 : break;
463 : case ENOENT:
464 : /* history file didn't exist, so try to create
465 : * it and then try again */
466 0 : if ((f = MT_fopen(_history_file, "w")) == NULL) {
467 : /* failed to create, don't
468 : * bother saving */
469 0 : _save_history = 0;
470 : } else {
471 0 : (void) fclose(f);
472 0 : if (read_history(_history_file) != 0) {
473 : /* still no luck, don't
474 : * bother saving */
475 0 : _save_history = 0;
476 : }
477 : }
478 : break;
479 0 : default:
480 : /* unrecognized failure, don't bother saving */
481 0 : _save_history = 0;
482 0 : break;
483 : }
484 : }
485 0 : if (!_save_history)
486 0 : fprintf(stderr, "Warning: not saving history\n");
487 : }
488 0 : }
489 :
490 : void
491 0 : deinit_readline(void)
492 : {
493 : /* nothing to do since we use append_history() */
494 0 : }
495 :
496 : void
497 0 : save_line(const char *s)
498 : {
499 0 : add_history(s);
500 0 : if (_save_history)
501 0 : append_history(1, _history_file);
502 0 : }
503 :
504 :
505 : #endif /* HAVE_LIBREADLINE */
|