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