Applied SF patch #771878: Escape cancels all dialogs.
[nedit.git] / source / shell.c
blob816552d2de32e8bf66c3f25eab639e0f1c119858
1 static const char CVSID[] = "$Id: shell.c,v 1.26 2003/07/18 15:14:16 edg Exp $";
2 /*******************************************************************************
3 * *
4 * shell.c -- Nirvana Editor shell command execution *
5 * *
6 * Copyright (C) 1999 Mark Edel *
7 * *
8 * This is free software; you can redistribute it and/or modify it under the *
9 * terms of the GNU General Public License as published by the Free Software *
10 * Foundation; either version 2 of the License, or (at your option) any later *
11 * version. *
12 * *
13 * This software is distributed in the hope that it will be useful, but WITHOUT *
14 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
15 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License *
16 * for more details. *
17 * *
18 * You should have received a copy of the GNU General Public License along with *
19 * software; if not, write to the Free Software Foundation, Inc., 59 Temple *
20 * Place, Suite 330, Boston, MA 02111-1307 USA *
21 * *
22 * Nirvana Text Editor *
23 * December, 1993 *
24 * *
25 * Written by Mark Edel *
26 * *
27 *******************************************************************************/
29 #ifdef HAVE_CONFIG_H
30 #include "../config.h"
31 #endif
33 #include "shell.h"
34 #include "textBuf.h"
35 #include "text.h"
36 #include "nedit.h"
37 #include "window.h"
38 #include "preferences.h"
39 #include "file.h"
40 #include "macro.h"
41 #include "interpret.h"
42 #include "../util/DialogF.h"
43 #include "../util/misc.h"
45 #include <stdio.h>
46 #include <stdlib.h>
47 #include <string.h>
48 #include <signal.h>
49 #include <sys/types.h>
50 #ifndef __MVS__
51 #include <sys/param.h>
52 #endif
53 #include <sys/wait.h>
54 #include <unistd.h>
55 #include <fcntl.h>
56 #include <ctype.h>
57 #include <errno.h>
58 #ifdef notdef
59 #ifdef IBM
60 #define NBBY 8
61 #include <sys/select.h>
62 #endif
63 #include <time.h>
64 #endif
65 #ifdef __EMX__
66 #include <process.h>
67 #endif
69 #include <Xm/Xm.h>
70 #include <Xm/MessageB.h>
71 #include <Xm/Text.h>
72 #include <Xm/Form.h>
73 #include <Xm/PushBG.h>
75 #ifdef HAVE_DEBUG_H
76 #include "../debug.h"
77 #endif
80 /* Tuning parameters */
81 #define IO_BUF_SIZE 4096 /* size of buffers for collecting cmd output */
82 #define MAX_OUT_DIALOG_ROWS 30 /* max height of dialog for command output */
83 #define MAX_OUT_DIALOG_COLS 80 /* max width of dialog for command output */
84 #define OUTPUT_FLUSH_FREQ 1000 /* how often (msec) to flush output buffers
85 when process is taking too long */
86 #define BANNER_WAIT_TIME 6000 /* how long to wait (msec) before putting up
87 Shell Command Executing... banner */
89 /* flags for issueCommand */
90 #define ACCUMULATE 1
91 #define ERROR_DIALOGS 2
92 #define REPLACE_SELECTION 4
93 #define RELOAD_FILE_AFTER 8
94 #define OUTPUT_TO_DIALOG 16
95 #define OUTPUT_TO_STRING 32
97 /* element of a buffer list for collecting output from shell processes */
98 typedef struct bufElem {
99 struct bufElem *next;
100 int length;
101 char contents[IO_BUF_SIZE];
102 } buffer;
104 /* data attached to window during shell command execution with
105 information for controling and communicating with the process */
106 typedef struct {
107 int flags;
108 int stdinFD, stdoutFD, stderrFD;
109 pid_t childPid;
110 XtInputId stdinInputID, stdoutInputID, stderrInputID;
111 buffer *outBufs, *errBufs;
112 char *input;
113 char *inPtr;
114 Widget textW;
115 int leftPos, rightPos;
116 int inLength;
117 XtIntervalId bannerTimeoutID, flushTimeoutID;
118 char bannerIsUp;
119 char fromMacro;
120 } shellCmdInfo;
122 static void issueCommand(WindowInfo *window, const char *command, char *input,
123 int inputLen, int flags, Widget textW, int replaceLeft,
124 int replaceRight, int fromMacro);
125 static void stdoutReadProc(XtPointer clientData, int *source, XtInputId *id);
126 static void stderrReadProc(XtPointer clientData, int *source, XtInputId *id);
127 static void stdinWriteProc(XtPointer clientData, int *source, XtInputId *id);
128 static void finishCmdExecution(WindowInfo *window, int terminatedOnError);
129 static pid_t forkCommand(Widget parent, const char *command, const char *cmdDir,
130 int *stdinFD, int *stdoutFD, int *stderrFD);
131 static void addOutput(buffer **bufList, buffer *buf);
132 static char *coalesceOutput(buffer **bufList, int *length);
133 static void freeBufList(buffer **bufList);
134 static void removeTrailingNewlines(char *string);
135 static void createOutputDialog(Widget parent, char *text);
136 static void destroyOutDialogCB(Widget w, XtPointer callback, XtPointer closure);
137 static void measureText(char *text, int wrapWidth, int *rows, int *cols,
138 int *wrapped);
139 static void truncateString(char *string, int length);
140 static void bannerTimeoutProc(XtPointer clientData, XtIntervalId *id);
141 static void flushTimeoutProc(XtPointer clientData, XtIntervalId *id);
142 static void safeBufReplace(textBuffer *buf, int *start, int *end,
143 const char *text);
144 static char *shellCommandSubstitutes(const char *inStr, const char *fileStr,
145 const char *lineStr);
146 static int shellSubstituter(char *outStr, const char *inStr, const char *fileStr,
147 const char *lineStr, int outLen, int predictOnly);
150 ** Filter the current selection through shell command "command". The selection
151 ** is removed, and replaced by the output from the command execution. Failed
152 ** command status and output to stderr are presented in dialog form.
154 void FilterSelection(WindowInfo *window, const char *command, int fromMacro)
156 int left, right, textLen;
157 char *text;
159 /* Can't do two shell commands at once in the same window */
160 if (window->shellCmdData != NULL) {
161 XBell(TheDisplay, 0);
162 return;
165 /* Get the selection and the range in character positions that it
166 occupies. Beep and return if no selection */
167 text = BufGetSelectionText(window->buffer);
168 if (*text == '\0') {
169 XtFree(text);
170 XBell(TheDisplay, 0);
171 return;
173 textLen = strlen(text);
174 BufUnsubstituteNullChars(text, window->buffer);
175 left = window->buffer->primary.start;
176 right = window->buffer->primary.end;
178 /* Issue the command and collect its output */
179 issueCommand(window, command, text, textLen, ACCUMULATE | ERROR_DIALOGS |
180 REPLACE_SELECTION, window->lastFocus, left, right, fromMacro);
184 ** Execute shell command "command", depositing the result at the current
185 ** insert position or in the current selection if the window has a
186 ** selection.
188 void ExecShellCommand(WindowInfo *window, const char *command, int fromMacro)
190 int left, right, flags = 0;
191 char *subsCommand, fullName[MAXPATHLEN];
192 int pos, line, column;
193 char lineNumber[11];
195 /* Can't do two shell commands at once in the same window */
196 if (window->shellCmdData != NULL) {
197 XBell(TheDisplay, 0);
198 return;
201 /* get the selection or the insert position */
202 pos = TextGetCursorPos(window->lastFocus);
203 if (GetSimpleSelection(window->buffer, &left, &right))
204 flags = ACCUMULATE | REPLACE_SELECTION;
205 else
206 left = right = pos;
208 /* Substitute the current file name for % and the current line number
209 for # in the shell command */
210 strcpy(fullName, window->path);
211 strcat(fullName, window->filename);
212 TextPosToLineAndCol(window->lastFocus, pos, &line, &column);
213 sprintf(lineNumber, "%d", line);
215 subsCommand = shellCommandSubstitutes(command, fullName, lineNumber);
216 if (subsCommand == NULL)
218 DialogF(DF_ERR, window->shell, 1, "Shell Command",
219 "Shell command is too long due to\n"
220 "filename substitutions with '%%' or\n"
221 "line number substitutions with '#'", "OK");
222 return;
225 /* issue the command */
226 issueCommand(window, subsCommand, NULL, 0, flags, window->lastFocus, left,
227 right, fromMacro);
228 free(subsCommand);
232 ** Execute shell command "command", on input string "input", depositing the
233 ** in a macro string (via a call back to ReturnShellCommandOutput).
235 void ShellCmdToMacroString(WindowInfo *window, const char *command,
236 const char *input)
238 char *inputCopy;
240 /* Make a copy of the input string for issueCommand to hold and free
241 upon completion */
242 inputCopy = *input == '\0' ? NULL : XtNewString(input);
244 /* fork the command and begin processing input/output */
245 issueCommand(window, command, inputCopy, strlen(input),
246 ACCUMULATE | OUTPUT_TO_STRING, NULL, 0, 0, True);
250 ** Execute the line of text where the the insertion cursor is positioned
251 ** as a shell command.
253 void ExecCursorLine(WindowInfo *window, int fromMacro)
255 char *cmdText;
256 int left, right, insertPos;
257 char *subsCommand, fullName[MAXPATHLEN];
258 int pos, line, column;
259 char lineNumber[11];
261 /* Can't do two shell commands at once in the same window */
262 if (window->shellCmdData != NULL) {
263 XBell(TheDisplay, 0);
264 return;
267 /* get all of the text on the line with the insert position */
268 pos = TextGetCursorPos(window->lastFocus);
269 if (!GetSimpleSelection(window->buffer, &left, &right)) {
270 left = right = pos;
271 left = BufStartOfLine(window->buffer, left);
272 right = BufEndOfLine(window->buffer, right);
273 insertPos = right;
274 } else
275 insertPos = BufEndOfLine(window->buffer, right);
276 cmdText = BufGetRange(window->buffer, left, right);
277 BufUnsubstituteNullChars(cmdText, window->buffer);
279 /* insert a newline after the entire line */
280 BufInsert(window->buffer, insertPos, "\n");
282 /* Substitute the current file name for % and the current line number
283 for # in the shell command */
284 strcpy(fullName, window->path);
285 strcat(fullName, window->filename);
286 TextPosToLineAndCol(window->lastFocus, pos, &line, &column);
287 sprintf(lineNumber, "%d", line);
289 subsCommand = shellCommandSubstitutes(cmdText, fullName, lineNumber);
290 if (subsCommand == NULL)
292 DialogF(DF_ERR, window->shell, 1, "Shell Command",
293 "Shell command is too long due to\n"
294 "filename substitutions with '%%' or\n"
295 "line number substitutions with '#'", "OK");
296 return;
299 /* issue the command */
300 issueCommand(window, subsCommand, NULL, 0, 0, window->lastFocus, insertPos+1,
301 insertPos+1, fromMacro);
302 free(subsCommand);
303 XtFree(cmdText);
307 ** Do a shell command, with the options allowed to users (input source,
308 ** output destination, save first and load after) in the shell commands
309 ** menu.
311 void DoShellMenuCmd(WindowInfo *window, const char *command,
312 int input, int output,
313 int outputReplacesInput, int saveFirst, int loadAfter, int fromMacro)
315 int flags = 0;
316 char *text;
317 char *subsCommand, fullName[MAXPATHLEN];
318 int left, right, textLen;
319 int pos, line, column;
320 char lineNumber[11];
321 WindowInfo *inWindow = window;
322 Widget outWidget;
324 /* Can't do two shell commands at once in the same window */
325 if (window->shellCmdData != NULL) {
326 XBell(TheDisplay, 0);
327 return;
330 /* Substitute the current file name for % and the current line number
331 for # in the shell command */
332 strcpy(fullName, window->path);
333 strcat(fullName, window->filename);
334 pos = TextGetCursorPos(window->lastFocus);
335 TextPosToLineAndCol(window->lastFocus, pos, &line, &column);
336 sprintf(lineNumber, "%d", line);
338 subsCommand = shellCommandSubstitutes(command, fullName, lineNumber);
339 if (subsCommand == NULL)
341 DialogF(DF_ERR, window->shell, 1, "Shell Command",
342 "Shell command is too long due to\n"
343 "filename substitutions with '%%' or\n"
344 "line number substitutions with '#'", "OK");
345 return;
348 /* Get the command input as a text string. If there is input, errors
349 shouldn't be mixed in with output, so set flags to ERROR_DIALOGS */
350 if (input == FROM_SELECTION) {
351 text = BufGetSelectionText(window->buffer);
352 if (*text == '\0') {
353 XtFree(text);
354 free(subsCommand);
355 XBell(TheDisplay, 0);
356 return;
358 flags |= ACCUMULATE | ERROR_DIALOGS;
359 } else if (input == FROM_WINDOW) {
360 text = BufGetAll(window->buffer);
361 flags |= ACCUMULATE | ERROR_DIALOGS;
362 } else if (input == FROM_EITHER) {
363 text = BufGetSelectionText(window->buffer);
364 if (*text == '\0') {
365 XtFree(text);
366 text = BufGetAll(window->buffer);
368 flags |= ACCUMULATE | ERROR_DIALOGS;
369 } else /* FROM_NONE */
370 text = NULL;
372 /* If the buffer was substituting another character for ascii-nuls,
373 put the nuls back in before exporting the text */
374 if (text != NULL) {
375 textLen = strlen(text);
376 BufUnsubstituteNullChars(text, window->buffer);
377 } else
378 textLen = 0;
380 /* Assign the output destination. If output is to a new window,
381 create it, and run the command from it instead of the current
382 one, to free the current one from waiting for lengthy execution */
383 if (output == TO_DIALOG) {
384 outWidget = NULL;
385 flags |= OUTPUT_TO_DIALOG;
386 left = right = 0;
387 } else if (output == TO_NEW_WINDOW) {
388 EditNewFile(NULL, False, NULL, window->path);
389 outWidget = WindowList->textArea;
390 inWindow = WindowList;
391 left = right = 0;
392 } else { /* TO_SAME_WINDOW */
393 outWidget = window->lastFocus;
394 if (outputReplacesInput && input != FROM_NONE) {
395 if (input == FROM_WINDOW) {
396 left = 0;
397 right = window->buffer->length;
398 } else if (input == FROM_SELECTION) {
399 GetSimpleSelection(window->buffer, &left, &right);
400 flags |= ACCUMULATE | REPLACE_SELECTION;
401 } else if (input == FROM_EITHER) {
402 if (GetSimpleSelection(window->buffer, &left, &right))
403 flags |= ACCUMULATE | REPLACE_SELECTION;
404 else {
405 left = 0;
406 right = window->buffer->length;
409 } else {
410 if (GetSimpleSelection(window->buffer, &left, &right))
411 flags |= ACCUMULATE | REPLACE_SELECTION;
412 else
413 left = right = TextGetCursorPos(window->lastFocus);
417 /* If the command requires the file be saved first, save it */
418 if (saveFirst) {
419 if (!SaveWindow(window)) {
420 if (input != FROM_NONE)
421 XtFree(text);
422 free(subsCommand);
423 return;
427 /* If the command requires the file to be reloaded after execution, set
428 a flag for issueCommand to deal with it when execution is complete */
429 if (loadAfter)
430 flags |= RELOAD_FILE_AFTER;
432 /* issue the command */
433 issueCommand(inWindow, subsCommand, text, textLen, flags, outWidget, left,
434 right, fromMacro);
435 free(subsCommand);
439 ** Cancel the shell command in progress
441 void AbortShellCommand(WindowInfo *window)
443 shellCmdInfo *cmdData = window->shellCmdData;
445 if (cmdData == NULL)
446 return;
447 kill(- cmdData->childPid, SIGTERM);
448 finishCmdExecution(window, True);
452 ** Issue a shell command and feed it the string "input". Output can be
453 ** directed either to text widget "textW" where it replaces the text between
454 ** the positions "replaceLeft" and "replaceRight", to a separate pop-up dialog
455 ** (OUTPUT_TO_DIALOG), or to a macro-language string (OUTPUT_TO_STRING). If
456 ** "input" is NULL, no input is fed to the process. If an input string is
457 ** provided, it is freed when the command completes. Flags:
459 ** ACCUMULATE Causes output from the command to be saved up until
460 ** the command completes.
461 ** ERROR_DIALOGS Presents stderr output separately in popup a dialog,
462 ** and also reports failed exit status as a popup dialog
463 ** including the command output.
464 ** REPLACE_SELECTION Causes output to replace the selection in textW.
465 ** RELOAD_FILE_AFTER Causes the file to be completely reloaded after the
466 ** command completes.
467 ** OUTPUT_TO_DIALOG Send output to a pop-up dialog instead of textW
468 ** OUTPUT_TO_STRING Output to a macro-language string instead of a text
469 ** widget or dialog.
471 ** REPLACE_SELECTION, ERROR_DIALOGS, and OUTPUT_TO_STRING can only be used
472 ** along with ACCUMULATE (these operations can't be done incrementally).
474 static void issueCommand(WindowInfo *window, const char *command, char *input,
475 int inputLen, int flags, Widget textW, int replaceLeft,
476 int replaceRight, int fromMacro)
478 int stdinFD, stdoutFD, stderrFD;
479 XtAppContext context = XtWidgetToApplicationContext(window->shell);
480 shellCmdInfo *cmdData;
481 pid_t childPid;
483 /* verify consistency of input parameters */
484 if ((flags & ERROR_DIALOGS || flags & REPLACE_SELECTION ||
485 flags & OUTPUT_TO_STRING) && !(flags & ACCUMULATE))
486 return;
488 /* a shell command called from a macro must be executed in the same
489 window as the macro, regardless of where the output is directed,
490 so the user can cancel them as a unit */
491 if (fromMacro)
492 window = MacroRunWindow();
494 /* put up a watch cursor over the waiting window */
495 if (!fromMacro)
496 BeginWait(window->shell);
498 /* enable the cancel menu item */
499 if (!fromMacro)
500 XtSetSensitive(window->cancelShellItem, True);
502 /* fork the subprocess and issue the command */
503 childPid = forkCommand(window->shell, command, window->path, &stdinFD,
504 &stdoutFD, (flags & ERROR_DIALOGS) ? &stderrFD : NULL);
506 /* set the pipes connected to the process for non-blocking i/o */
507 if (fcntl(stdinFD, F_SETFL, O_NONBLOCK) < 0)
508 perror("NEdit: Internal error (fcntl)");
509 if (fcntl(stdoutFD, F_SETFL, O_NONBLOCK) < 0)
510 perror("NEdit: Internal error (fcntl1)");
511 if (flags & ERROR_DIALOGS) {
512 if (fcntl(stderrFD, F_SETFL, O_NONBLOCK) < 0)
513 perror("NEdit: Internal error (fcntl2)");
516 /* if there's nothing to write to the process' stdin, close it now */
517 if (input == NULL)
518 close(stdinFD);
520 /* Create a data structure for passing process information around
521 amongst the callback routines which will process i/o and completion */
522 cmdData = (shellCmdInfo *)XtMalloc(sizeof(shellCmdInfo));
523 window->shellCmdData = cmdData;
524 cmdData->flags = flags;
525 cmdData->stdinFD = stdinFD;
526 cmdData->stdoutFD = stdoutFD;
527 cmdData->stderrFD = stderrFD;
528 cmdData->childPid = childPid;
529 cmdData->outBufs = NULL;
530 cmdData->errBufs = NULL;
531 cmdData->input = input;
532 cmdData->inPtr = input;
533 cmdData->textW = textW;
534 cmdData->bannerIsUp = False;
535 cmdData->fromMacro = fromMacro;
536 cmdData->leftPos = replaceLeft;
537 cmdData->rightPos = replaceRight;
538 cmdData->inLength = inputLen;
540 /* Set up timer proc for putting up banner when process takes too long */
541 if (fromMacro)
542 cmdData->bannerTimeoutID = 0;
543 else
544 cmdData->bannerTimeoutID = XtAppAddTimeOut(context, BANNER_WAIT_TIME,
545 bannerTimeoutProc, window);
547 /* Set up timer proc for flushing output buffers periodically */
548 if ((flags & ACCUMULATE) || textW == NULL)
549 cmdData->flushTimeoutID = 0;
550 else
551 cmdData->flushTimeoutID = XtAppAddTimeOut(context, OUTPUT_FLUSH_FREQ,
552 flushTimeoutProc, window);
554 /* set up callbacks for activity on the file descriptors */
555 cmdData->stdoutInputID = XtAppAddInput(context, stdoutFD,
556 (XtPointer)XtInputReadMask, stdoutReadProc, window);
557 if (input != NULL)
558 cmdData->stdinInputID = XtAppAddInput(context, stdinFD,
559 (XtPointer)XtInputWriteMask, stdinWriteProc, window);
560 else
561 cmdData->stdinInputID = 0;
562 if (flags & ERROR_DIALOGS)
563 cmdData->stderrInputID = XtAppAddInput(context, stderrFD,
564 (XtPointer)XtInputReadMask, stderrReadProc, window);
565 else
566 cmdData->stderrInputID = 0;
568 /* If this was called from a macro, preempt the macro untill shell
569 command completes */
570 if (fromMacro)
571 PreemptMacro();
575 ** Called when the shell sub-process stdout stream has data. Reads data into
576 ** the "outBufs" buffer chain in the window->shellCommandData data structure.
578 static void stdoutReadProc(XtPointer clientData, int *source, XtInputId *id)
580 WindowInfo *window = (WindowInfo *)clientData;
581 shellCmdInfo *cmdData = window->shellCmdData;
582 buffer *buf;
583 int nRead;
585 /* read from the process' stdout stream */
586 buf = (buffer *)XtMalloc(sizeof(buffer));
587 nRead = read(cmdData->stdoutFD, buf->contents, IO_BUF_SIZE);
589 /* error in read */
590 if (nRead == -1) { /* error */
591 if (errno != EWOULDBLOCK && errno != EAGAIN) {
592 perror("NEdit: Error reading shell command output");
593 XtFree((char *)buf);
594 finishCmdExecution(window, True);
596 return;
599 /* end of data. If the stderr stream is done too, execution of the
600 shell process is complete, and we can display the results */
601 if (nRead == 0) {
602 XtFree((char *)buf);
603 XtRemoveInput(cmdData->stdoutInputID);
604 cmdData->stdoutInputID = 0;
605 if (cmdData->stderrInputID == 0)
606 finishCmdExecution(window, False);
607 return;
610 /* characters were read successfully, add buf to linked list of buffers */
611 buf->length = nRead;
612 addOutput(&cmdData->outBufs, buf);
616 ** Called when the shell sub-process stderr stream has data. Reads data into
617 ** the "errBufs" buffer chain in the window->shellCommandData data structure.
619 static void stderrReadProc(XtPointer clientData, int *source, XtInputId *id)
621 WindowInfo *window = (WindowInfo *)clientData;
622 shellCmdInfo *cmdData = window->shellCmdData;
623 buffer *buf;
624 int nRead;
626 /* read from the process' stderr stream */
627 buf = (buffer *)XtMalloc(sizeof(buffer));
628 nRead = read(cmdData->stderrFD, buf->contents, IO_BUF_SIZE);
630 /* error in read */
631 if (nRead == -1) {
632 if (errno != EWOULDBLOCK && errno != EAGAIN) {
633 perror("NEdit: Error reading shell command error stream");
634 XtFree((char *)buf);
635 finishCmdExecution(window, True);
637 return;
640 /* end of data. If the stdout stream is done too, execution of the
641 shell process is complete, and we can display the results */
642 if (nRead == 0) {
643 XtFree((char *)buf);
644 XtRemoveInput(cmdData->stderrInputID);
645 cmdData->stderrInputID = 0;
646 if (cmdData->stdoutInputID == 0)
647 finishCmdExecution(window, False);
648 return;
651 /* characters were read successfully, add buf to linked list of buffers */
652 buf->length = nRead;
653 addOutput(&cmdData->errBufs, buf);
657 ** Called when the shell sub-process stdin stream is ready for input. Writes
658 ** data from the "input" text string passed to issueCommand.
660 static void stdinWriteProc(XtPointer clientData, int *source, XtInputId *id)
662 WindowInfo *window = (WindowInfo *)clientData;
663 shellCmdInfo *cmdData = window->shellCmdData;
664 int nWritten;
666 nWritten = write(cmdData->stdinFD, cmdData->inPtr, cmdData->inLength);
667 if (nWritten == -1) {
668 if (errno == EPIPE) {
669 /* Just shut off input to broken pipes. User is likely feeding
670 it to a command which does not take input */
671 XtRemoveInput(cmdData->stdinInputID);
672 cmdData->stdinInputID = 0;
673 close(cmdData->stdinFD);
674 cmdData->inPtr = NULL;
675 } else if (errno != EWOULDBLOCK && errno != EAGAIN) {
676 perror("NEdit: Write to shell command failed");
677 finishCmdExecution(window, True);
679 } else {
680 cmdData->inPtr += nWritten;
681 cmdData->inLength -= nWritten;
682 if (cmdData->inLength <= 0) {
683 XtRemoveInput(cmdData->stdinInputID);
684 cmdData->stdinInputID = 0;
685 close(cmdData->stdinFD);
686 cmdData->inPtr = NULL;
692 ** Timer proc for putting up the "Shell Command in Progress" banner if
693 ** the process is taking too long.
695 static void bannerTimeoutProc(XtPointer clientData, XtIntervalId *id)
697 WindowInfo *window = (WindowInfo *)clientData;
698 shellCmdInfo *cmdData = window->shellCmdData;
700 cmdData->bannerIsUp = True;
701 SetModeMessage(window,
702 "Shell Command in Progress -- Press Ctrl+. to Cancel");
703 cmdData->bannerTimeoutID = 0;
707 ** Buffer replacement wrapper routine to be used for inserting output from
708 ** a command into the buffer, which takes into account that the buffer may
709 ** have been shrunken by the user (eg, by Undo). If necessary, the starting
710 ** and ending positions (part of the state of the command) are corrected.
712 static void safeBufReplace(textBuffer *buf, int *start, int *end,
713 const char *text)
715 if (*start > buf->length)
716 *start = buf->length;
717 if (*end > buf->length)
718 *end = buf->length;
719 BufReplace(buf, *start, *end, text);
723 ** Timer proc for flushing output buffers periodically when the process
724 ** takes too long.
726 static void flushTimeoutProc(XtPointer clientData, XtIntervalId *id)
728 WindowInfo *window = (WindowInfo *)clientData;
729 shellCmdInfo *cmdData = window->shellCmdData;
730 textBuffer *buf = TextGetBuffer(cmdData->textW);
731 int len;
732 char *outText;
734 /* shouldn't happen, but it would be bad if it did */
735 if (cmdData->textW == NULL)
736 return;
738 outText = coalesceOutput(&cmdData->outBufs, &len);
739 if (len != 0) {
740 if (BufSubstituteNullChars(outText, len, buf)) {
741 safeBufReplace(buf, &cmdData->leftPos, &cmdData->rightPos, outText);
742 TextSetCursorPos(cmdData->textW, cmdData->leftPos+strlen(outText));
743 cmdData->leftPos += len;
744 cmdData->rightPos = cmdData->leftPos;
745 } else
746 fprintf(stderr, "NEdit: Too much binary data\n");
748 XtFree(outText);
750 /* re-establish the timer proc (this routine) to continue processing */
751 cmdData->flushTimeoutID = XtAppAddTimeOut(
752 XtWidgetToApplicationContext(window->shell),
753 OUTPUT_FLUSH_FREQ, flushTimeoutProc, clientData);
757 ** Clean up after the execution of a shell command sub-process and present
758 ** the output/errors to the user as requested in the initial issueCommand
759 ** call. If "terminatedOnError" is true, don't bother trying to read the
760 ** output, just close the i/o descriptors, free the memory, and restore the
761 ** user interface state.
763 static void finishCmdExecution(WindowInfo *window, int terminatedOnError)
765 shellCmdInfo *cmdData = window->shellCmdData;
766 textBuffer *buf;
767 int status, failure, errorReport, reselectStart, outTextLen, errTextLen;
768 int resp, cancel = False, fromMacro = cmdData->fromMacro;
769 char *outText, *errText = NULL;
771 /* Cancel any pending i/o on the file descriptors */
772 if (cmdData->stdoutInputID != 0)
773 XtRemoveInput(cmdData->stdoutInputID);
774 if (cmdData->stdinInputID != 0)
775 XtRemoveInput(cmdData->stdinInputID);
776 if (cmdData->stderrInputID != 0)
777 XtRemoveInput(cmdData->stderrInputID);
779 /* Close any file descriptors remaining open */
780 close(cmdData->stdoutFD);
781 if (cmdData->flags & ERROR_DIALOGS)
782 close(cmdData->stderrFD);
783 if (cmdData->inPtr != NULL)
784 close(cmdData->stdinFD);
786 /* Free the provided input text */
787 if (cmdData->input != NULL)
788 XtFree(cmdData->input);
790 /* Cancel pending timeouts */
791 if (cmdData->flushTimeoutID != 0)
792 XtRemoveTimeOut(cmdData->flushTimeoutID);
793 if (cmdData->bannerTimeoutID != 0)
794 XtRemoveTimeOut(cmdData->bannerTimeoutID);
796 /* Clean up waiting-for-shell-command-to-complete mode */
797 if (!cmdData->fromMacro) {
798 EndWait(window->shell);
799 XtSetSensitive(window->cancelShellItem, False);
800 if (cmdData->bannerIsUp)
801 ClearModeMessage(window);
804 /* If the process was killed or became inaccessable, give up */
805 if (terminatedOnError) {
806 freeBufList(&cmdData->outBufs);
807 freeBufList(&cmdData->errBufs);
808 waitpid(cmdData->childPid, &status, 0);
809 goto cmdDone;
812 /* Assemble the output from the process' stderr and stdout streams into
813 null terminated strings, and free the buffer lists used to collect it */
814 outText = coalesceOutput(&cmdData->outBufs, &outTextLen);
815 if (cmdData->flags & ERROR_DIALOGS)
816 errText = coalesceOutput(&cmdData->errBufs, &errTextLen);
818 /* Wait for the child process to complete and get its return status */
819 waitpid(cmdData->childPid, &status, 0);
821 /* Present error and stderr-information dialogs. If a command returned
822 error output, or if the process' exit status indicated failure,
823 present the information to the user. */
824 if (cmdData->flags & ERROR_DIALOGS)
826 failure = WIFEXITED(status) && WEXITSTATUS(status) != 0;
827 errorReport = *errText != '\0';
829 if (failure && errorReport)
831 removeTrailingNewlines(errText);
832 truncateString(errText, DF_MAX_MSG_LENGTH);
833 resp = DialogF(DF_WARN, window->shell, 2, "Warning", "%s", "Cancel",
834 "Proceed", errText);
835 cancel = resp == 1;
836 } else if (failure)
838 truncateString(outText, DF_MAX_MSG_LENGTH-70);
839 resp = DialogF(DF_WARN, window->shell, 2, "Command Failure",
840 "Command reported failed exit status.\n"
841 "Output from command:\n%s", "Cancel", "Proceed", outText);
842 cancel = resp == 1;
843 } else if (errorReport)
845 removeTrailingNewlines(errText);
846 truncateString(errText, DF_MAX_MSG_LENGTH);
847 resp = DialogF(DF_INF, window->shell, 2, "Information", "%s",
848 "Proceed", "Cancel", errText);
849 cancel = resp == 2;
852 XtFree(errText);
853 if (cancel)
855 XtFree(outText);
856 goto cmdDone;
860 /* If output is to a dialog, present the dialog. Otherwise insert the
861 (remaining) output in the text widget as requested, and move the
862 insert point to the end */
863 if (cmdData->flags & OUTPUT_TO_DIALOG) {
864 removeTrailingNewlines(outText);
865 if (*outText != '\0')
866 createOutputDialog(window->shell, outText);
867 } else if (cmdData->flags & OUTPUT_TO_STRING) {
868 ReturnShellCommandOutput(window,outText, WEXITSTATUS(status));
869 } else {
870 buf = TextGetBuffer(cmdData->textW);
871 if (!BufSubstituteNullChars(outText, outTextLen, buf)) {
872 fprintf(stderr,"NEdit: Too much binary data in shell cmd output\n");
873 outText[0] = '\0';
875 if (cmdData->flags & REPLACE_SELECTION) {
876 reselectStart = buf->primary.rectangular ? -1 : buf->primary.start;
877 BufReplaceSelected(buf, outText);
878 TextSetCursorPos(cmdData->textW, buf->cursorPosHint);
879 if (reselectStart != -1)
880 BufSelect(buf, reselectStart, reselectStart + strlen(outText));
881 } else {
882 safeBufReplace(buf, &cmdData->leftPos, &cmdData->rightPos, outText);
883 TextSetCursorPos(cmdData->textW, cmdData->leftPos+strlen(outText));
887 /* If the command requires the file to be reloaded afterward, reload it */
888 if (cmdData->flags & RELOAD_FILE_AFTER)
889 RevertToSaved(window);
891 /* Command is complete, free data structure and continue macro execution */
892 XtFree(outText);
893 cmdDone:
894 XtFree((char *)cmdData);
895 window->shellCmdData = NULL;
896 if (fromMacro)
897 ResumeMacroExecution(window);
901 ** Fork a subprocess to execute a command, return file descriptors for pipes
902 ** connected to the subprocess' stdin, stdout, and stderr streams. cmdDir
903 ** sets the default directory for the subprocess. If stderrFD is passed as
904 ** NULL, the pipe represented by stdoutFD is connected to both stdin and
905 ** stderr. The function value returns the pid of the new subprocess, or -1
906 ** if an error occured.
908 static pid_t forkCommand(Widget parent, const char *command, const char *cmdDir,
909 int *stdinFD, int *stdoutFD, int *stderrFD)
911 int childStdoutFD, childStdinFD, childStderrFD, pipeFDs[2];
912 int dupFD;
913 pid_t childPid;
915 /* Ignore SIGPIPE signals generated when user attempts to provide
916 input for commands which don't take input */
917 signal(SIGPIPE, SIG_IGN);
919 /* Create pipes to communicate with the sub process. One end of each is
920 returned to the caller, the other half is spliced to stdin, stdout
921 and stderr in the child process */
922 if (pipe(pipeFDs) != 0) {
923 perror("NEdit: Internal error (opening stdout pipe)");
924 return -1;
926 *stdoutFD = pipeFDs[0];
927 childStdoutFD = pipeFDs[1];
928 if (pipe(pipeFDs) != 0) {
929 perror("NEdit: Internal error (opening stdin pipe)");
930 return -1;
932 *stdinFD = pipeFDs[1];
933 childStdinFD = pipeFDs[0];
934 if (stderrFD == NULL)
935 childStderrFD = childStdoutFD;
936 else {
937 if (pipe(pipeFDs) != 0) {
938 perror("NEdit: Internal error (opening stdin pipe)");
939 return -1;
941 *stderrFD = pipeFDs[0];
942 childStderrFD = pipeFDs[1];
945 /* Fork the process */
946 childPid = fork();
949 ** Child process context (fork returned 0), clean up the
950 ** child ends of the pipes and execute the command
952 if (0 == childPid) {
954 /* close the parent end of the pipes in the child process */
955 close(*stdinFD);
956 close(*stdoutFD);
957 if (stderrFD != NULL)
958 close(*stderrFD);
960 /* close current stdin, stdout, and stderr file descriptors before
961 substituting pipes */
962 close(fileno(stdin));
963 close(fileno(stdout));
964 close(fileno(stderr));
966 /* duplicate the child ends of the pipes to have the same numbers
967 as stdout & stderr, so it can substitute for stdout & stderr */
968 dupFD = dup2(childStdinFD, fileno(stdin));
969 if (dupFD == -1)
970 perror("dup of stdin failed");
971 dupFD = dup2(childStdoutFD, fileno(stdout));
972 if (dupFD == -1)
973 perror("dup of stdout failed");
974 dupFD = dup2(childStderrFD, fileno(stderr));
975 if (dupFD == -1)
976 perror("dup of stderr failed");
978 /* make this process the leader of a new process group, so the sub
979 processes can be killed, if necessary, with a killpg call */
980 #ifndef __EMX__ /* OS/2 doesn't have this */
981 setsid();
982 #endif
984 /* change the current working directory to the directory of the current
985 file. */
986 if(cmdDir[0] != 0)
987 if(chdir(cmdDir) == -1)
988 perror("chdir to directory of current file failed");
990 /* execute the command using the shell specified by preferences */
991 execl(GetPrefShell(), GetPrefShell(), "-c", command, (char *)0);
993 /* if we reach here, execl failed */
994 fprintf(stderr, "Error starting shell: %s\n", GetPrefShell());
995 exit(EXIT_FAILURE);
998 /* Parent process context, check if fork succeeded */
999 if (childPid == -1)
1001 DialogF(DF_ERR, parent, 1, "Shell Command",
1002 "Error starting shell command process\n(fork failed)",
1003 "Dismiss");
1006 /* close the child ends of the pipes */
1007 close(childStdinFD);
1008 close(childStdoutFD);
1009 if (stderrFD != NULL)
1010 close(childStderrFD);
1012 return childPid;
1016 ** Add a buffer full of output to a buffer list
1018 static void addOutput(buffer **bufList, buffer *buf)
1020 buf->next = *bufList;
1021 *bufList = buf;
1025 ** coalesce the contents of a list of buffers into a contiguous memory block,
1026 ** freeing the memory occupied by the buffer list. Returns the memory block
1027 ** as the function result, and its length as parameter "length".
1029 static char *coalesceOutput(buffer **bufList, int *outLength)
1031 buffer *buf, *rBufList = NULL;
1032 char *outBuf, *outPtr, *p;
1033 int i, length = 0;
1035 /* find the total length of data read */
1036 for (buf=*bufList; buf!=NULL; buf=buf->next)
1037 length += buf->length;
1039 /* allocate contiguous memory for returning data */
1040 outBuf = XtMalloc(length+1);
1042 /* reverse the buffer list */
1043 while (*bufList != NULL) {
1044 buf = *bufList;
1045 *bufList = buf->next;
1046 buf->next = rBufList;
1047 rBufList = buf;
1050 /* copy the buffers into the output buffer */
1051 outPtr = outBuf;
1052 for (buf=rBufList; buf!=NULL; buf=buf->next) {
1053 p = buf->contents;
1054 for (i=0; i<buf->length; i++)
1055 *outPtr++ = *p++;
1058 /* terminate with a null */
1059 *outPtr = '\0';
1061 /* free the buffer list */
1062 freeBufList(&rBufList);
1064 *outLength = outPtr - outBuf;
1065 return outBuf;
1068 static void freeBufList(buffer **bufList)
1070 buffer *buf;
1072 while (*bufList != NULL) {
1073 buf = *bufList;
1074 *bufList = buf->next;
1075 XtFree((char *)buf);
1080 ** Remove trailing newlines from a string by substituting nulls
1082 static void removeTrailingNewlines(char *string)
1084 char *endPtr = &string[strlen(string)-1];
1086 while (endPtr >= string && *endPtr == '\n')
1087 *endPtr-- = '\0';
1091 ** Create a dialog for the output of a shell command. The dialog lives until
1092 ** the user presses the Dismiss button, and is then destroyed
1094 static void createOutputDialog(Widget parent, char *text)
1096 Arg al[50];
1097 int ac, rows, cols, hasScrollBar, wrapped;
1098 Widget form, textW, button;
1099 XmString st1;
1101 /* measure the width and height of the text to determine size for dialog */
1102 measureText(text, MAX_OUT_DIALOG_COLS, &rows, &cols, &wrapped);
1103 if (rows > MAX_OUT_DIALOG_ROWS) {
1104 rows = MAX_OUT_DIALOG_ROWS;
1105 hasScrollBar = True;
1106 } else
1107 hasScrollBar = False;
1108 if (cols > MAX_OUT_DIALOG_COLS)
1109 cols = MAX_OUT_DIALOG_COLS;
1110 if (cols == 0)
1111 cols = 1;
1112 /* Without completely emulating Motif's wrapping algorithm, we can't
1113 be sure that we haven't underestimated the number of lines in case
1114 a line has wrapped, so let's assume that some lines could be obscured
1116 if (wrapped)
1117 hasScrollBar = True;
1118 ac = 0;
1119 form = CreateFormDialog(parent, "shellOutForm", al, ac);
1121 ac = 0;
1122 XtSetArg(al[ac], XmNlabelString, st1=MKSTRING("Dismiss")); ac++;
1123 XtSetArg(al[ac], XmNhighlightThickness, 0); ac++;
1124 XtSetArg(al[ac], XmNbottomAttachment, XmATTACH_FORM); ac++;
1125 XtSetArg(al[ac], XmNtopAttachment, XmATTACH_NONE); ac++;
1126 button = XmCreatePushButtonGadget(form, "dismiss", al, ac);
1127 XtManageChild(button);
1128 XtVaSetValues(form, XmNdefaultButton, button, NULL);
1129 XtVaSetValues(form, XmNcancelButton, button, NULL);
1130 XmStringFree(st1);
1131 XtAddCallback(button, XmNactivateCallback, destroyOutDialogCB,
1132 XtParent(form));
1134 ac = 0;
1135 XtSetArg(al[ac], XmNrows, rows); ac++;
1136 XtSetArg(al[ac], XmNcolumns, cols); ac++;
1137 XtSetArg(al[ac], XmNresizeHeight, False); ac++;
1138 XtSetArg(al[ac], XmNtraversalOn, False); ac++;
1139 XtSetArg(al[ac], XmNwordWrap, True); ac++;
1140 XtSetArg(al[ac], XmNscrollHorizontal, False); ac++;
1141 XtSetArg(al[ac], XmNscrollVertical, hasScrollBar); ac++;
1142 XtSetArg(al[ac], XmNhighlightThickness, 0); ac++;
1143 XtSetArg(al[ac], XmNspacing, 0); ac++;
1144 XtSetArg(al[ac], XmNeditMode, XmMULTI_LINE_EDIT); ac++;
1145 XtSetArg(al[ac], XmNeditable, False); ac++;
1146 XtSetArg(al[ac], XmNvalue, text); ac++;
1147 XtSetArg(al[ac], XmNtopAttachment, XmATTACH_FORM); ac++;
1148 XtSetArg(al[ac], XmNleftAttachment, XmATTACH_FORM); ac++;
1149 XtSetArg(al[ac], XmNbottomAttachment, XmATTACH_WIDGET); ac++;
1150 XtSetArg(al[ac], XmNrightAttachment, XmATTACH_FORM); ac++;
1151 XtSetArg(al[ac], XmNbottomWidget, button); ac++;
1152 textW = XmCreateScrolledText(form, "outText", al, ac);
1153 AddMouseWheelSupport(textW);
1154 XtManageChild(textW);
1156 XtVaSetValues(XtParent(form), XmNtitle, "Output from Command", NULL);
1157 ManageDialogCenteredOnPointer(form);
1161 ** Dispose of the command output dialog when user presses Dismiss button
1163 static void destroyOutDialogCB(Widget w, XtPointer callback, XtPointer closure)
1165 XtDestroyWidget((Widget)callback);
1169 ** Measure the width and height of a string of text. Assumes 8 character
1170 ** tabs. wrapWidth specifies a number of columns at which text wraps.
1172 static void measureText(char *text, int wrapWidth, int *rows, int *cols,
1173 int *wrapped)
1175 int maxCols = 0, line = 1, col = 0, wrapCol;
1176 char *c;
1178 *wrapped = 0;
1179 for (c=text; *c!='\0'; c++) {
1180 if (*c=='\n') {
1181 line++;
1182 col = 0;
1183 continue;
1186 if (*c == '\t') {
1187 col += 8 - (col % 8);
1188 wrapCol = 0; /* Tabs at end of line are not drawn when wrapped */
1189 } else if (*c == ' ') {
1190 col++;
1191 wrapCol = 0; /* Spaces at end of line are not drawn when wrapped */
1192 } else {
1193 col++;
1194 wrapCol = 1;
1197 /* Note: there is a small chance that the number of lines is
1198 over-estimated when a line ends with a space or a tab (ie, followed
1199 by a newline) and that whitespace crosses the boundary, because
1200 whitespace at the end of a line does not cause wrapping. Taking
1201 this into account is very hard, but an over-estimation is harmless.
1202 The worst that can happen is that some extra blank lines are shown
1203 at the end of the dialog (in contrast to an under-estimation, which
1204 could make the last lines invisible).
1205 On the other hand, without emulating Motif's wrapping algorithm
1206 completely, we can't be sure that we don't underestimate the number
1207 of lines (Motif uses word wrap, and this counting algorithm uses
1208 character wrap). Therefore, we remember whether there is a line
1209 that has wrapped. In that case we allways install a scroll bar.
1211 if (col > wrapWidth) {
1212 line++;
1213 *wrapped = 1;
1214 col = wrapCol;
1215 } else if (col > maxCols) {
1216 maxCols = col;
1219 *rows = line;
1220 *cols = maxCols;
1224 ** Truncate a string to a maximum of length characters. If it shortens the
1225 ** string, it appends "..." to show that it has been shortened. It assumes
1226 ** that the string that it is passed is writeable.
1228 static void truncateString(char *string, int length)
1230 if ((int)strlen(string) > length)
1231 memcpy(&string[length-3], "...", 4);
1235 ** Substitute the string fileStr in inStr wherever % appears and
1236 ** lineStr in inStr wherever # appears, storing the
1237 ** result in outStr. If predictOnly is non-zero, the result string length
1238 ** is predicted without creating the string. Returns the length of the result
1239 ** string or -1 in case of an error.
1242 static int shellSubstituter(char *outStr, const char *inStr, const char *fileStr,
1243 const char *lineStr, int outLen, int predictOnly)
1245 const char *inChar;
1246 char *outChar = NULL;
1247 int outWritten = 0;
1248 int fileLen, lineLen;
1250 inChar = inStr;
1251 if (!predictOnly) {
1252 outChar = outStr;
1254 fileLen = strlen(fileStr);
1255 lineLen = strlen(lineStr);
1257 while (*inChar != '\0') {
1259 if (!predictOnly && outWritten >= outLen) {
1260 return(-1);
1263 if (*inChar == '%') {
1264 if (*(inChar + 1) == '%') {
1265 inChar += 2;
1266 if (!predictOnly) {
1267 *outChar++ = '%';
1269 outWritten++;
1270 } else {
1271 if (!predictOnly) {
1272 if (outWritten + fileLen >= outLen) {
1273 return(-1);
1275 strncpy(outChar, fileStr, fileLen);
1276 outChar += fileLen;
1278 outWritten += fileLen;
1279 inChar++;
1281 } else if (*inChar == '#') {
1282 if (*(inChar + 1) == '#') {
1283 inChar += 2;
1284 if (!predictOnly) {
1285 *outChar++ = '#';
1287 outWritten++;
1288 } else {
1289 if (!predictOnly) {
1290 if (outWritten + lineLen >= outLen) {
1291 return(-1);
1293 strncpy(outChar, lineStr, lineLen);
1294 outChar += lineLen;
1296 outWritten += lineLen;
1297 inChar++;
1299 } else {
1300 if (!predictOnly) {
1301 *outChar++ = *inChar;
1303 inChar++;
1304 outWritten++;
1308 if (!predictOnly) {
1309 if (outWritten >= outLen) {
1310 return(-1);
1312 *outChar = '\0';
1314 ++outWritten;
1315 return(outWritten);
1318 static char *shellCommandSubstitutes(const char *inStr, const char *fileStr,
1319 const char *lineStr)
1321 int cmdLen;
1322 char *subsCmdStr = NULL;
1324 cmdLen = shellSubstituter(NULL, inStr, fileStr, lineStr, 0, 1);
1325 if (cmdLen >= 0) {
1326 subsCmdStr = malloc(cmdLen);
1327 if (subsCmdStr) {
1328 cmdLen = shellSubstituter(subsCmdStr, inStr, fileStr, lineStr, cmdLen, 0);
1329 if (cmdLen < 0) {
1330 free(subsCmdStr);
1331 subsCmdStr = NULL;
1335 return(subsCmdStr);