shell.c 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. /* shell.c, Ait, BSD 3-Clause, Kevin Bloom, 2023-2025 */
  2. #include <stdio.h>
  3. #include <sys/wait.h>
  4. #include "header.h"
  5. #include "termbox.h"
  6. #include "util.h"
  7. #define MAX_COMMANDS 10
  8. #define MAX_ARG_LEN 100
  9. #define BUFFER_SIZE 256
  10. char opcmdtext[STRBUF_M];
  11. char ipcmdtext[STRBUF_M];
  12. int oline = 0, ib = FALSE;
  13. /* TODO: make this generic
  14. M-e will display "Shell Command: " in the msgline. You then input the command
  15. you want.
  16. Eventually maybe make it so that there are different types of commands:
  17. - input, inputs something at the point
  18. - open, runs a command and ait will open the output (this currently works)
  19. - region/replace, use the region as the input to the shell cmd and then
  20. replace the region with the output
  21. - new buffer, runs the command and the output is placed in a new buffer
  22. I probably would want some keybinds to certain commands, however.
  23. Also, I'd like to make it so that if you have a region selected, it can be
  24. executed much like how acme does it.
  25. You can input the current buffer's filename (including its path),
  26. line number, and column number by placing a single @, #, or $,
  27. respectively. If the command begins with !, it will _not_ put
  28. output anything into the temp file (which is required for ait to
  29. read the output). This will allow you to write generic interactive
  30. scripts using the shell read command.
  31. io = Insert = 0, Open = 1
  32. */
  33. void get_popen_data() {
  34. char *command = NULL;
  35. buffer_t *bp;
  36. char *insertp = "Shell Command", *openp = "Open Via";
  37. char prompt[STRBUF_M + 12 + strlen(insertp)];
  38. int cpos = 0, done = FALSE, ots = FALSE;
  39. int c = 0, k = 0, hasregion = FALSE, escaped_size = 0;
  40. int start_col = strlen(prompt), didtry, fd;
  41. int line = 0, onscrap = 0, ishellring = -1;
  42. int newlines = 0;
  43. struct tb_event ev;
  44. char cmdtext[STRBUF_L], *cbuf; /* cbuf is the colon buffer for line number */
  45. memset(cmdtext, 0, STRBUF_L);
  46. point_t point;
  47. char_t *oscrap = NULL, *escaped_region = NULL, *kr;
  48. const char* path = getenv("PATH");
  49. FILE *file, *fp = NULL;
  50. struct stat sb;
  51. char sys_command[CHUNK];
  52. static char temp_file[] = TEMPFILE;
  53. oline = curbp->b_line;
  54. if(is_file_modified(curbp->b_fname) && !file_was_modified_prompt()) {
  55. return;
  56. }
  57. if(io == 1) {
  58. sprintf(prompt, "%s", openp);
  59. if(opcmdtext[0] != '\0') {
  60. strcat(prompt, " (default ");
  61. strcat(prompt, opcmdtext);
  62. strcat(prompt, ")");
  63. }
  64. } else {
  65. sprintf(prompt, "%s", insertp);
  66. if(ipcmdtext[0] != '\0') {
  67. strcat(prompt, " (default ");
  68. strcat(prompt, ipcmdtext);
  69. strcat(prompt, ")");
  70. }
  71. }
  72. strcat(prompt, ": ");
  73. start_col = strlen(prompt);
  74. display_prompt_and_response(prompt, cmdtext);
  75. cpos = strlen(cmdtext);
  76. for (;;) {
  77. didtry = (k == 0x09); /* Was last command tab-completion? */
  78. tb_present();
  79. if(execute_kbd_macro) {
  80. use_kbd_macro(&ev);
  81. } else if(tb_poll_event(&ev) != TB_OK) return;
  82. if(msgline_editor(ev, prompt, cmdtext, STRBUF_L, &cpos)) {
  83. continue;
  84. }
  85. if(!ev.mod && ev.key != TB_KEY_ARROW_UP && ev.key != TB_KEY_ARROW_DOWN)
  86. k = ev.ch;
  87. else
  88. k = ev.key;
  89. if(record_input) {
  90. record_buffer[record_buffer_index] = ev;
  91. record_buffer_index++;
  92. }
  93. /* ignore control keys other than return, C-g, backspace, CR, C-s, C-R, ESC, tab */
  94. if (k < 32 &&
  95. k != TB_KEY_CTRL_G &&
  96. k != TB_KEY_CTRL_N &&
  97. k != TB_KEY_CTRL_P &&
  98. k != TB_KEY_ARROW_UP &&
  99. k != TB_KEY_ARROW_DOWN &&
  100. k != TB_KEY_BACKSPACE &&
  101. k != TB_KEY_BACKSPACE2 &&
  102. k != TB_KEY_ENTER &&
  103. k != TB_KEY_ESC &&
  104. k != TB_KEY_TAB)
  105. continue;
  106. switch(k) {
  107. case TB_KEY_ENTER: /* return */
  108. done = TRUE;
  109. break;
  110. case TB_KEY_ARROW_DOWN:
  111. case TB_KEY_CTRL_N: /* ctrl-n */
  112. if(ishellring > 0)
  113. ishellring--;
  114. else
  115. break;
  116. cpos = 0;
  117. for (int i = 0; i < shell_ring[ishellring].len; i++) {
  118. cmdtext[cpos++] = shell_ring[ishellring].data[i];
  119. cmdtext[cpos] = '\0';
  120. }
  121. cpos = shell_ring[ishellring].len;
  122. cmdtext[cpos] = '\0';
  123. tb_set_cursor(start_col, MSGLINE);
  124. clrtoeol(start_col, MSGLINE);
  125. addstr(cmdtext);
  126. break;
  127. case TB_KEY_ARROW_UP:
  128. case TB_KEY_CTRL_P: /* ctrl-p */
  129. if(ishellring < SHELLRING_SIZE -1 && shell_ring[ishellring+1].data != NULL)
  130. ishellring++;
  131. else
  132. break;
  133. cpos = 0;
  134. for (int i = 0; i < shell_ring[ishellring].len; i++) {
  135. cmdtext[cpos++] = shell_ring[ishellring].data[i];
  136. cmdtext[cpos] = '\0';
  137. }
  138. cpos = shell_ring[ishellring].len;
  139. cmdtext[cpos] = '\0';
  140. tb_set_cursor(start_col, MSGLINE);
  141. clrtoeol(start_col, MSGLINE);
  142. addstr(cmdtext);
  143. break;
  144. case TB_KEY_ESC: /* esc */
  145. case TB_KEY_CTRL_G: /* ctrl-g */
  146. if (fp != NULL) fclose(fp);
  147. tb_set_cursor(0, MSGLINE);
  148. clrtoeol(0, MSGLINE);
  149. return;
  150. case TB_KEY_BACKSPACE2: /* del, erase */
  151. case TB_KEY_BACKSPACE: /* backspace */
  152. if (cpos == 0)
  153. continue;
  154. cmdtext[--cpos] = '\0';
  155. tb_set_cursor(start_col + cpos, MSGLINE);
  156. display_prompt_and_response(prompt, cmdtext);
  157. break;
  158. do_tab:
  159. case TB_KEY_TAB: {
  160. char curpath[PATH_MAX], pcmd[PATH_MAX], cu;
  161. int ii = 0;
  162. for(cu = cmdtext[cpos]; !isspace(cu) && cpos > 0; cpos--, cu = cmdtext[cpos])
  163. ;;
  164. for(int iii = 0; cmdtext[cpos+iii] != '\0'; iii++) {
  165. cu = cmdtext[cpos+iii];
  166. if(!isspace(cu)) {
  167. pcmd[ii] = cu;
  168. ii++;
  169. }
  170. }
  171. if(cpos > 0)
  172. cpos++;
  173. pcmd[ii] = '\0';
  174. if(!didtry) {
  175. if (fp != NULL) fclose(fp);
  176. strcpy(temp_file, TEMPFILE);
  177. if (-1 == (fd = mkstemp(temp_file)))
  178. fatal("%s: Failed to create temp file\n");
  179. strcpy(sys_command, "printf \"%s\\n\" ");
  180. for(int i = 0, ii = 0; path[i] != '\0'; i++, ii++) {
  181. if(path[i] == ':') {
  182. curpath[ii] = '\0';
  183. strcat(sys_command, curpath);
  184. strcat(sys_command, "/");
  185. strcat(sys_command, pcmd);
  186. strcat(sys_command, "* ");
  187. ii = 0;
  188. i++;
  189. } else
  190. curpath[ii] = path[i];
  191. }
  192. strcat(sys_command, "| awk '$0 !~ \"\\\\*\" { split($0, a, \"/\"); print a[length(a)] }' | sort -u >");
  193. strcat(sys_command, temp_file);
  194. // strcat(sys_command, " 2>&1");
  195. (void) ! system(sys_command); /* stop compiler unused result warning */
  196. fp = fdopen(fd, "r");
  197. unlink(temp_file);
  198. }
  199. while ((c = getc(fp)) != EOF && c != '\n') {
  200. if (cpos < PATH_MAX - 1 && c != '*') {
  201. cmdtext[cpos++] = c;
  202. cmdtext[cpos] = '\0';
  203. }
  204. }
  205. cmdtext[cpos] = '\0';
  206. for(int i = cpos+1; cmdtext[i] != '\0'; i++)
  207. cmdtext[i] = 0;
  208. if (c != '\n' || c == -1) rewind(fp);
  209. if(c == -1) goto do_tab;
  210. didtry = 1;
  211. tb_set_cursor(start_col, MSGLINE);
  212. clrtoeol(start_col, MSGLINE);
  213. addstr(cmdtext);
  214. break;
  215. }
  216. default:
  217. if (cpos < STRBUF_M - 1) {
  218. for(int i = strlen(cmdtext); i > cpos; i--) {
  219. cmdtext[i] = cmdtext[i - 1];
  220. }
  221. cmdtext[cpos] = k;
  222. cmdtext[strlen(cmdtext)] = '\0';
  223. tb_set_cursor(start_col, MSGLINE);
  224. addstr(cmdtext);
  225. cpos++;
  226. tb_set_cursor(start_col + cpos, MSGLINE);
  227. }
  228. break;
  229. }
  230. if(done)
  231. break;
  232. }
  233. if(cmdtext[0] == '\0') {
  234. if(io == 1 && opcmdtext[0] != '\0')
  235. strncpy(cmdtext, opcmdtext, STRBUF_M);
  236. else if(io == 0 && ipcmdtext[0] != '\0')
  237. strncpy(cmdtext, ipcmdtext, STRBUF_M);
  238. else {
  239. clrtoeol(0, MSGLINE);
  240. return;
  241. }
  242. }
  243. if(io == 1)
  244. strncpy(opcmdtext, cmdtext, STRBUF_M);
  245. else if(io == 0)
  246. strncpy(ipcmdtext, cmdtext, STRBUF_M);
  247. int ncmd = 0, cncmd = 0;
  248. char n;
  249. for(int i = 0; cmdtext[i] != '\0'; i++, ncmd++) {
  250. n = cmdtext[i+1];
  251. if(cmdtext[i] == '@' && cmdtext[i-1] != '\\') {
  252. ncmd += strlen(curbp->b_fname);
  253. }
  254. if(cmdtext[i] == '#') {
  255. char *s;
  256. asprintf(&s, "%d", curbp->b_line);
  257. ncmd += strlen(s);
  258. free(s);
  259. s = NULL;
  260. }
  261. if(cmdtext[i] == '$' && !isalpha(n) &&
  262. cmdtext[i-1] != '\\') {
  263. char *s;
  264. asprintf(&s, "%d", curbp->b_col);
  265. ncmd += strlen(s);
  266. free(s);
  267. s = NULL;
  268. }
  269. }
  270. char cmd[ncmd];
  271. if(cmdtext[0] == '!') {
  272. ots = TRUE;
  273. cmdtext[0] = ' ';
  274. if(cmdtext[1] == '<') {
  275. ib = TRUE;
  276. cmdtext[1] = ' ';
  277. }
  278. } else if(cmdtext[0] == '<') {
  279. ib = TRUE;
  280. cmdtext[0] = ' ';
  281. }
  282. for(int i = 0; cmdtext[i] != '\0'; i++, cncmd++) {
  283. n = cmdtext[i+1];
  284. if(cmdtext[i] == '@' && cmdtext[i-1] != '\\') {
  285. cncmd += strlen(curbp->b_fname) - 1;
  286. strcat(cmd, curbp->b_fname);
  287. } else if(cmdtext[i] == '#') {
  288. char *s;
  289. asprintf(&s, "%d", curbp->b_line);
  290. cncmd += strlen(s) - 1;
  291. strcat(cmd, s);
  292. free(s);
  293. s = NULL;
  294. } else if(cmdtext[i] == '$' && isspace(n) &&
  295. cmdtext[i-1] != '\\') {
  296. char *s;
  297. asprintf(&s, "%d", curbp->b_col);
  298. cncmd += strlen(s) - 1;
  299. strcat(cmd, s);
  300. free(s);
  301. s = NULL;
  302. } else {
  303. cmd[cncmd] = cmdtext[i];
  304. }
  305. cmd[cncmd+1] = '\0';
  306. }
  307. if (curbp->b_mark != NOMARK && curbp->b_point != curbp->b_mark) {
  308. if(io == 0) {
  309. oscrap = (char_t*) malloc(scrap.len);
  310. onscrap = scrap.len;
  311. (void) memcpy(oscrap, scrap.data, scrap.len * sizeof (char_t));
  312. copy_cut(TRUE, TRUE, FALSE);
  313. }
  314. hasregion = TRUE;
  315. }
  316. tb_shutdown();
  317. if(hasregion && io == 0) {
  318. /* Find all dollar signs and increase the size by one for each sign. */
  319. for(int i = 0; scrap.data[i] != '\0'; i++) {
  320. if(scrap.data[i] == '$' || scrap.data[i] == '`' || scrap.data[i] == '"')
  321. escaped_size += 2;
  322. else
  323. escaped_size++;
  324. }
  325. escaped_region = malloc(sizeof(char_t *)*escaped_size+1);
  326. /* Escape all $ with \$, ` with \`, and " with \". This prevents
  327. the echo command from trying to do a variable substitution,
  328. command execution, and removal of double quotes.
  329. */
  330. for(int i = 0, k = 0; scrap.data[i] != '\0'; i++, k++) {
  331. if(scrap.data[i] == '$' || scrap.data[i] == '`' || scrap.data[i] == '"') {
  332. escaped_region[k] = '\\';
  333. k++;
  334. escaped_region[k] = scrap.data[i];
  335. } else {
  336. escaped_region[k] = scrap.data[i];
  337. }
  338. }
  339. escaped_region[escaped_size] = '\0';
  340. asprintf(&command, "echo \"%s\" | %s > /tmp/ait-temp.txt", (char *)escaped_region, cmd);
  341. } else {
  342. asprintf(&command, "%s%s", cmd, ots ? "" : " > /tmp/ait-temp.txt");
  343. }
  344. /* Using system(3) fixes some issues with programs such as xclip(1).
  345. With the ! support to control output, it also fixes issues with
  346. interactive shell scripts.
  347. It also makes the code a lot simpler.
  348. */
  349. system(command);
  350. if (stat("/tmp/ait-temp.txt", &sb) < 0) {
  351. msg("Failed to find temp file.");
  352. return;
  353. }
  354. if (MAX_SIZE_T < sb.st_size) {
  355. msg("Temp file is too big to load.");
  356. return;
  357. }
  358. if ((file = fopen("/tmp/ait-temp.txt", "r")) == NULL) {
  359. msg("Failed to open temp file.");
  360. return;
  361. }
  362. ngtemp = sb.st_size * sizeof (char_t);
  363. if(gtemp != NULL) {
  364. free(gtemp);
  365. gtemp = NULL;
  366. }
  367. if(ngtemp > 0) {
  368. gtemp = calloc(ngtemp + 1, sizeof(char));
  369. fread(gtemp, sizeof(char), ngtemp, file);
  370. gtemp[ngtemp-1] = '\0'; // there is usually a trailing newline
  371. }
  372. fclose(file);
  373. remove("/tmp/ait-temp.txt");
  374. for(int i = SHELLRING_SIZE-1; i > 0; i--) {
  375. if(shell_ring[i].data != NULL) {
  376. free(shell_ring[i].data);
  377. shell_ring[i].data = NULL;
  378. }
  379. if(shell_ring[i-1].data != NULL) {
  380. char_t *kri;
  381. kri = (char_t *)strndup((const char *)shell_ring[i-1].data, shell_ring[i-1].len);
  382. kri[shell_ring[i-1].len] = '\0';
  383. if (kri == NULL) {
  384. msg("No more memory available.");
  385. return;
  386. } else {
  387. shell_ring[i].data = kri;
  388. shell_ring[i].len = shell_ring[i-1].len;
  389. }
  390. }
  391. }
  392. if(shell_ring[0].data != NULL) {
  393. free(shell_ring[0].data);
  394. shell_ring[0].data = NULL;
  395. }
  396. kr = (char_t *)strndup((const char*)cmdtext, cpos);
  397. kr[cpos] = '\0';
  398. if (kr == NULL) {
  399. msg("No more memory available.");
  400. return;
  401. } else {
  402. shell_ring[0].data = kr;
  403. shell_ring[0].len = cpos;
  404. }
  405. memset(cmd, 0, STRBUF_L);
  406. memset(cmdtext, 0, STRBUF_L);
  407. tb_init();
  408. LINES = tb_height();
  409. COLS = tb_width();
  410. MSGLINE = LINES-1;
  411. tb_set_input_mode(TB_INPUT_ALT);
  412. /* Mark the log for update */
  413. redraw();
  414. /* check if canceled command */
  415. if(gtemp == NULL || gtemp[0] == -1 || gtemp[0] == 0) {
  416. if(io == 0) {
  417. /* put the original contents back in the buffer and reset scrap */
  418. paste_internal(FALSE);
  419. free(scrap.data);
  420. scrap.len = onscrap;
  421. scrap.data = (char_t*) malloc(scrap.len);
  422. (void) memcpy(scrap.data, oscrap, scrap.len * sizeof (char_t));
  423. }
  424. } else {
  425. switch(io) {
  426. case 0: {
  427. if(ib) {
  428. buffer_t *bp;
  429. bp = find_buffer("*Shell Command Ouput*", TRUE, FALSE);
  430. disassociate_b(curwp);
  431. curbp = bp;
  432. associate_b2w(curbp, curwp);
  433. if (!growgap(curbp, CHUNK))
  434. fatal("%s: Failed to allocate required memory.\n");
  435. movegap(curbp, 0);
  436. /* load the file if not already loaded */
  437. if (bp != NULL) {
  438. if (!load_file(temp)) {
  439. msg("");
  440. }
  441. }
  442. }
  443. clipboard();
  444. if(oscrap != NULL) {
  445. free(oscrap);
  446. oscrap = NULL;
  447. }
  448. break;
  449. }
  450. case 1: {
  451. gtemp[ngtemp-1] = '\0';
  452. /* Find the file name and find the line number */
  453. if(gtemp[0] == '\0')
  454. goto do_finish;
  455. cbuf = strtok(gtemp, ":");
  456. /* sometimes the editor_dir is lost */
  457. if(editor_dir[0] == '\0') {
  458. getcwd(editor_dir, PATH_MAX+1);
  459. strcat(editor_dir, "/");
  460. }
  461. strcpy(temp, editor_dir);
  462. strcat(temp, cbuf);
  463. cbuf = strtok(NULL, ":");
  464. if(cbuf != NULL && (line = atoi(cbuf)) == 0) {
  465. strcat(temp, ":");
  466. strcat(temp, cbuf);
  467. }
  468. strcat(temp, "\0");
  469. if(line < 1)
  470. free(cbuf);
  471. bp = find_buffer(temp, TRUE, FALSE);
  472. disassociate_b(curwp);
  473. curbp = bp;
  474. associate_b2w(curbp, curwp);
  475. if (!growgap(curbp, CHUNK))
  476. fatal("%s: Failed to allocate required memory.\n");
  477. movegap(curbp, 0);
  478. /* load the file if not already loaded */
  479. if (bp != NULL) {
  480. if (!load_file(temp)) {
  481. msg("New file %s", temp);
  482. }
  483. if(line > 0) {
  484. point = line_to_point(line);
  485. if (point != -1) {
  486. curbp->b_point = point;
  487. if (curbp->b_epage < pos(curbp, curbp->b_ebuf))
  488. curbp->b_reframe = 1;
  489. msg("Line %d", line);
  490. } else {
  491. msg("Line %d, not found", line);
  492. }
  493. update_display();
  494. }
  495. }
  496. break;
  497. }
  498. }
  499. }
  500. do_finish:
  501. /* Some commands, such as ones that copy from region, will mess up
  502. curbp->b_line due to the cut that is preformed. We want to reset
  503. that mistake.
  504. */
  505. if(curbp->b_line != oline && newlines == 0 && io == 0)
  506. curbp->b_line = oline;
  507. if(command != NULL) {
  508. free(command);
  509. command = NULL;
  510. }
  511. if(gtemp != NULL) {
  512. free(gtemp);
  513. gtemp = NULL;
  514. ngtemp = 0;
  515. }
  516. }