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