shell.c 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  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;
  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;
  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;
  48. const char* path = getenv("PATH");
  49. FILE *file, *fp = NULL;
  50. char sys_command[CHUNK];
  51. static char temp_file[] = TEMPFILE;
  52. oline = curbp->b_line;
  53. if(is_file_modified(curbp->b_fname) && !file_was_modified_prompt()) {
  54. return;
  55. }
  56. if(io == 1) {
  57. sprintf(prompt, "%s", openp);
  58. if(opcmdtext[0] != '\0') {
  59. strcat(prompt, " (default ");
  60. strcat(prompt, opcmdtext);
  61. strcat(prompt, ")");
  62. }
  63. } else {
  64. sprintf(prompt, "%s", insertp);
  65. if(ipcmdtext[0] != '\0') {
  66. strcat(prompt, " (default ");
  67. strcat(prompt, ipcmdtext);
  68. strcat(prompt, ")");
  69. }
  70. }
  71. strcat(prompt, ": ");
  72. start_col = strlen(prompt);
  73. display_prompt_and_response(prompt, cmdtext);
  74. cpos = strlen(cmdtext);
  75. for (;;) {
  76. didtry = (k == 0x09); /* Was last command tab-completion? */
  77. tb_present();
  78. if(execute_kbd_macro) {
  79. use_kbd_macro(&ev);
  80. } else if(tb_poll_event(&ev) != TB_OK) return;
  81. if(msgline_editor(ev, prompt, cmdtext, STRBUF_L, &cpos)) {
  82. continue;
  83. }
  84. if(!ev.mod)
  85. k = ev.ch;
  86. else
  87. k = ev.key;
  88. if(record_input) {
  89. record_buffer[record_buffer_index] = ev;
  90. record_buffer_index++;
  91. }
  92. /* ignore control keys other than return, C-g, backspace, CR, C-s, C-R, ESC, tab */
  93. if (k < 32 &&
  94. k != TB_KEY_CTRL_G &&
  95. k != TB_KEY_BACKSPACE &&
  96. k != TB_KEY_BACKSPACE2 &&
  97. k != TB_KEY_ENTER &&
  98. k != TB_KEY_ESC &&
  99. k != TB_KEY_TAB)
  100. continue;
  101. switch(k) {
  102. case TB_KEY_ENTER: /* return */
  103. done = TRUE;
  104. break;
  105. case TB_KEY_ESC: /* esc */
  106. case TB_KEY_CTRL_G: /* ctrl-g */
  107. if (fp != NULL) fclose(fp);
  108. tb_set_cursor(0, MSGLINE);
  109. clrtoeol(0, MSGLINE);
  110. return;
  111. case TB_KEY_BACKSPACE2: /* del, erase */
  112. case TB_KEY_BACKSPACE: /* backspace */
  113. if (cpos == 0)
  114. continue;
  115. cmdtext[--cpos] = '\0';
  116. tb_set_cursor(start_col + cpos, MSGLINE);
  117. display_prompt_and_response(prompt, cmdtext);
  118. break;
  119. do_tab:
  120. case TB_KEY_TAB: {
  121. char curpath[PATH_MAX], pcmd[PATH_MAX], cu;
  122. int ii = 0;
  123. for(cu = cmdtext[cpos]; !isspace(cu) && cpos > 0; cpos--, cu = cmdtext[cpos])
  124. ;;
  125. for(int iii = 0; cmdtext[cpos+iii] != '\0'; iii++) {
  126. cu = cmdtext[cpos+iii];
  127. if(!isspace(cu)) {
  128. pcmd[ii] = cu;
  129. ii++;
  130. }
  131. }
  132. if(cpos > 0)
  133. cpos++;
  134. pcmd[ii] = '\0';
  135. if(!didtry) {
  136. if (fp != NULL) fclose(fp);
  137. strcpy(temp_file, TEMPFILE);
  138. if (-1 == (fd = mkstemp(temp_file)))
  139. fatal("%s: Failed to create temp file\n");
  140. strcpy(sys_command, "printf \"%s\\n\" ");
  141. for(int i = 0, ii = 0; path[i] != '\0'; i++, ii++) {
  142. if(path[i] == ':') {
  143. curpath[ii] = '\0';
  144. strcat(sys_command, curpath);
  145. strcat(sys_command, "/");
  146. strcat(sys_command, pcmd);
  147. strcat(sys_command, "* ");
  148. ii = 0;
  149. i++;
  150. } else
  151. curpath[ii] = path[i];
  152. }
  153. strcat(sys_command, "| awk '$0 !~ \"\\\\*\" { split($0, a, \"/\"); print a[length(a)] }' | sort -u >");
  154. strcat(sys_command, temp_file);
  155. // strcat(sys_command, " 2>&1");
  156. (void) ! system(sys_command); /* stop compiler unused result warning */
  157. fp = fdopen(fd, "r");
  158. unlink(temp_file);
  159. }
  160. while ((c = getc(fp)) != EOF && c != '\n') {
  161. if (cpos < PATH_MAX - 1 && c != '*') {
  162. cmdtext[cpos++] = c;
  163. cmdtext[cpos] = '\0';
  164. }
  165. }
  166. cmdtext[cpos] = '\0';
  167. for(int i = cpos+1; cmdtext[i] != '\0'; i++)
  168. cmdtext[i] = 0;
  169. if (c != '\n' || c == -1) rewind(fp);
  170. if(c == -1) goto do_tab;
  171. didtry = 1;
  172. tb_set_cursor(start_col, MSGLINE);
  173. clrtoeol(start_col, MSGLINE);
  174. addstr(cmdtext);
  175. break;
  176. }
  177. default:
  178. if (cpos < STRBUF_M - 1) {
  179. for(int i = strlen(cmdtext); i > cpos; i--) {
  180. cmdtext[i] = cmdtext[i - 1];
  181. }
  182. cmdtext[cpos] = k;
  183. cmdtext[strlen(cmdtext)] = '\0';
  184. tb_set_cursor(start_col, MSGLINE);
  185. addstr(cmdtext);
  186. cpos++;
  187. tb_set_cursor(start_col + cpos, MSGLINE);
  188. }
  189. break;
  190. }
  191. if(done)
  192. break;
  193. }
  194. if(cmdtext[0] == '\0') {
  195. if(io == 1 && opcmdtext[0] != '\0')
  196. strncpy(cmdtext, opcmdtext, STRBUF_M);
  197. else if(io == 0 && ipcmdtext[0] != '\0')
  198. strncpy(cmdtext, ipcmdtext, STRBUF_M);
  199. else {
  200. clrtoeol(0, MSGLINE);
  201. return;
  202. }
  203. }
  204. if(io == 1)
  205. strncpy(opcmdtext, cmdtext, STRBUF_M);
  206. else if(io == 0)
  207. strncpy(ipcmdtext, cmdtext, STRBUF_M);
  208. int ncmd = 0, cncmd = 0;
  209. char n;
  210. for(int i = 0; cmdtext[i] != '\0'; i++, ncmd++) {
  211. n = cmdtext[i+1];
  212. if(cmdtext[i] == '@' && cmdtext[i-1] != '\\') {
  213. ncmd += strlen(curbp->b_fname);
  214. }
  215. if(cmdtext[i] == '#') {
  216. char *s;
  217. asprintf(&s, "%d", curbp->b_line);
  218. ncmd += strlen(s);
  219. free(s);
  220. s = NULL;
  221. }
  222. if(cmdtext[i] == '$' && !isalpha(n) &&
  223. cmdtext[i-1] != '\\') {
  224. char *s;
  225. asprintf(&s, "%d", curbp->b_col);
  226. ncmd += strlen(s);
  227. free(s);
  228. s = NULL;
  229. }
  230. }
  231. char cmd[ncmd];
  232. if(cmdtext[0] == '!') {
  233. ots = TRUE;
  234. cmdtext[0] = ' ';
  235. }
  236. for(int i = 0; cmdtext[i] != '\0'; i++, cncmd++) {
  237. n = cmdtext[i+1];
  238. if(cmdtext[i] == '@' && cmdtext[i-1] != '\\') {
  239. cncmd += strlen(curbp->b_fname) - 1;
  240. strcat(cmd, curbp->b_fname);
  241. } else if(cmdtext[i] == '#') {
  242. char *s;
  243. asprintf(&s, "%d", curbp->b_line);
  244. cncmd += strlen(s) - 1;
  245. strcat(cmd, s);
  246. free(s);
  247. s = NULL;
  248. } else if(cmdtext[i] == '$' && !isalpha(n) &&
  249. cmdtext[i-1] != '\\') {
  250. char *s;
  251. asprintf(&s, "%d", curbp->b_col);
  252. cncmd += strlen(s) - 1;
  253. strcat(cmd, s);
  254. free(s);
  255. s = NULL;
  256. } else {
  257. cmd[cncmd] = cmdtext[i];
  258. }
  259. cmd[cncmd+1] = '\0';
  260. }
  261. if (curbp->b_mark != NOMARK && curbp->b_point != curbp->b_mark) {
  262. if(io == 0) {
  263. oscrap = (char_t*) malloc(scrap.len);
  264. onscrap = scrap.len;
  265. (void) memcpy(oscrap, scrap.data, scrap.len * sizeof (char_t));
  266. copy_cut(TRUE, TRUE, FALSE);
  267. }
  268. hasregion = TRUE;
  269. }
  270. tb_shutdown();
  271. if(hasregion && io == 0) {
  272. /* Find all dollar signs and increase the size by one for each sign. */
  273. for(int i = 0; scrap.data[i] != '\0'; i++) {
  274. if(scrap.data[i] == '$' || scrap.data[i] == '`' || scrap.data[i] == '"')
  275. escaped_size += 2;
  276. else
  277. escaped_size++;
  278. }
  279. escaped_region = malloc(sizeof(char_t *)*escaped_size+1);
  280. /* Escape all $ with \$, ` with \`, and " with \". This prevents
  281. the echo command from trying to do a variable substitution,
  282. command execution, and removal of double quotes.
  283. */
  284. for(int i = 0, k = 0; scrap.data[i] != '\0'; i++, k++) {
  285. if(scrap.data[i] == '$' || scrap.data[i] == '`' || scrap.data[i] == '"') {
  286. escaped_region[k] = '\\';
  287. k++;
  288. escaped_region[k] = scrap.data[i];
  289. } else {
  290. escaped_region[k] = scrap.data[i];
  291. }
  292. }
  293. escaped_region[escaped_size] = '\0';
  294. asprintf(&command, "echo \"%s\" | %s > /tmp/ait-temp.txt", (char *)escaped_region, cmd);
  295. } else {
  296. asprintf(&command, "%s%s", cmd, ots ? "" : " > /tmp/ait-temp.txt");
  297. }
  298. /* Using system(3) fixes some issues with programs such as xclip(1).
  299. With the ! support to control output, it also fixes issues with
  300. interactive shell scripts.
  301. It also makes the code a lot simpler.
  302. */
  303. system(command);
  304. file = fopen("/tmp/ait-temp.txt", "r");
  305. if (!file) return;
  306. fseek(file, 0, SEEK_END);
  307. ngtemp = ftell(file);
  308. fseek(file, 0, SEEK_SET);
  309. if(gtemp != NULL) {
  310. free(gtemp);
  311. gtemp = NULL;
  312. }
  313. if(ngtemp > 0) {
  314. gtemp = calloc(ngtemp + 1, sizeof(char));
  315. fread(gtemp, sizeof(char), ngtemp, file);
  316. gtemp[ngtemp-1] = '\0'; // there is usually a trailing newline
  317. }
  318. fclose(file);
  319. remove("/tmp/ait-temp.txt");
  320. memset(cmd, 0, STRBUF_L);
  321. memset(cmdtext, 0, STRBUF_L);
  322. tb_init();
  323. LINES = tb_height();
  324. COLS = tb_width();
  325. MSGLINE = LINES-1;
  326. tb_set_input_mode(TB_INPUT_ALT);
  327. /* Mark the log for update */
  328. redraw();
  329. /* check if canceled command */
  330. if(gtemp == NULL || gtemp[0] == -1 || gtemp[0] == 0) {
  331. if(io == 0) {
  332. /* put the original contents back in the buffer and reset scrap */
  333. paste_internal(FALSE);
  334. free(scrap.data);
  335. scrap.len = onscrap;
  336. scrap.data = (char_t*) malloc(scrap.len);
  337. (void) memcpy(scrap.data, oscrap, scrap.len * sizeof (char_t));
  338. }
  339. } else {
  340. switch(io) {
  341. case 0: {
  342. clipboard();
  343. if(oscrap != NULL) {
  344. free(oscrap);
  345. oscrap = NULL;
  346. }
  347. break;
  348. }
  349. case 1: {
  350. gtemp[ngtemp-1] = '\0';
  351. /* Find the file name and find the line number */
  352. if(gtemp[0] == '\0')
  353. goto do_finish;
  354. cbuf = strtok(gtemp, ":");
  355. /* sometimes the editor_dir is lost */
  356. if(editor_dir[0] == '\0') {
  357. getcwd(editor_dir, PATH_MAX+1);
  358. strcat(editor_dir, "/");
  359. }
  360. strcpy(temp, editor_dir);
  361. strcat(temp, cbuf);
  362. cbuf = strtok(NULL, ":");
  363. if(cbuf != NULL && (line = atoi(cbuf)) == 0) {
  364. strcat(temp, ":");
  365. strcat(temp, cbuf);
  366. }
  367. strcat(temp, "\0");
  368. if(line < 1)
  369. free(cbuf);
  370. bp = find_buffer(temp, TRUE, FALSE);
  371. disassociate_b(curwp);
  372. curbp = bp;
  373. associate_b2w(curbp, curwp);
  374. if (!growgap(curbp, CHUNK))
  375. fatal("%s: Failed to allocate required memory.\n");
  376. movegap(curbp, 0);
  377. /* load the file if not already loaded */
  378. if (bp != NULL) {
  379. if (!load_file(temp)) {
  380. msg("New file %s", temp);
  381. }
  382. if(line > 0) {
  383. point = line_to_point(line);
  384. if (point != -1) {
  385. curbp->b_point = point;
  386. if (curbp->b_epage < pos(curbp, curbp->b_ebuf))
  387. curbp->b_reframe = 1;
  388. msg("Line %d", line);
  389. } else {
  390. msg("Line %d, not found", line);
  391. }
  392. update_display();
  393. }
  394. }
  395. break;
  396. }
  397. }
  398. }
  399. do_finish:
  400. /* Some commands, such as ones that copy from region, will mess up
  401. curbp->b_line due to the cut that is preformed. We want to reset
  402. that mistake.
  403. */
  404. if(curbp->b_line != oline && newlines == 0 && io == 0)
  405. curbp->b_line = oline;
  406. if(command != NULL) {
  407. free(command);
  408. command = NULL;
  409. }
  410. if(gtemp != NULL) {
  411. free(gtemp);
  412. gtemp = NULL;
  413. ngtemp = 0;
  414. }
  415. }