shell.c 16 KB

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