- Change help version to 5.4DEV
[nedit.git] / source / shell.c
blob7e0e8c4fe676c2707a7dfd6a35679931e1e4e2cf
1 static const char CVSID[] = "$Id: shell.c,v 1.23 2002/07/11 21:18:10 slobasso 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) {
217 DialogF(DF_ERR, window->shell, 1,
218 "Shell command is too long due to\nfilename substitutions with '%%' or\n" \
219 "line number substitutions with '#'",
220 "OK");
221 return;
224 /* issue the command */
225 issueCommand(window, subsCommand, NULL, 0, flags, window->lastFocus, left,
226 right, fromMacro);
227 free(subsCommand);
231 ** Execute shell command "command", on input string "input", depositing the
232 ** in a macro string (via a call back to ReturnShellCommandOutput).
234 void ShellCmdToMacroString(WindowInfo *window, const char *command,
235 const char *input)
237 char *inputCopy;
239 /* Make a copy of the input string for issueCommand to hold and free
240 upon completion */
241 inputCopy = *input == '\0' ? NULL : XtNewString(input);
243 /* fork the command and begin processing input/output */
244 issueCommand(window, command, inputCopy, strlen(input),
245 ACCUMULATE | OUTPUT_TO_STRING, NULL, 0, 0, True);
249 ** Execute the line of text where the the insertion cursor is positioned
250 ** as a shell command.
252 void ExecCursorLine(WindowInfo *window, int fromMacro)
254 char *cmdText;
255 int left, right, insertPos;
256 char *subsCommand, fullName[MAXPATHLEN];
257 int pos, line, column;
258 char lineNumber[11];
260 /* Can't do two shell commands at once in the same window */
261 if (window->shellCmdData != NULL) {
262 XBell(TheDisplay, 0);
263 return;
266 /* get all of the text on the line with the insert position */
267 pos = TextGetCursorPos(window->lastFocus);
268 if (!GetSimpleSelection(window->buffer, &left, &right)) {
269 left = right = pos;
270 left = BufStartOfLine(window->buffer, left);
271 right = BufEndOfLine(window->buffer, right);
272 insertPos = right;
273 } else
274 insertPos = BufEndOfLine(window->buffer, right);
275 cmdText = BufGetRange(window->buffer, left, right);
276 BufUnsubstituteNullChars(cmdText, window->buffer);
278 /* insert a newline after the entire line */
279 BufInsert(window->buffer, insertPos, "\n");
281 /* Substitute the current file name for % and the current line number
282 for # in the shell command */
283 strcpy(fullName, window->path);
284 strcat(fullName, window->filename);
285 TextPosToLineAndCol(window->lastFocus, pos, &line, &column);
286 sprintf(lineNumber, "%d", line);
288 subsCommand = shellCommandSubstitutes(cmdText, fullName, lineNumber);
289 if (subsCommand == NULL) {
290 DialogF(DF_ERR, window->shell, 1,
291 "Shell command is too long due to\nfilename substitutions with '%%' or\n" \
292 "line number substitutions with '#'",
293 "OK");
294 return;
297 /* issue the command */
298 issueCommand(window, subsCommand, NULL, 0, 0, window->lastFocus, insertPos+1,
299 insertPos+1, fromMacro);
300 free(subsCommand);
301 XtFree(cmdText);
305 ** Do a shell command, with the options allowed to users (input source,
306 ** output destination, save first and load after) in the shell commands
307 ** menu.
309 void DoShellMenuCmd(WindowInfo *window, const char *command,
310 int input, int output,
311 int outputReplacesInput, int saveFirst, int loadAfter, int fromMacro)
313 int flags = 0;
314 char *text;
315 char *subsCommand, fullName[MAXPATHLEN];
316 int left, right, textLen;
317 int pos, line, column;
318 char lineNumber[11];
319 WindowInfo *inWindow = window;
320 Widget outWidget;
322 /* Can't do two shell commands at once in the same window */
323 if (window->shellCmdData != NULL) {
324 XBell(TheDisplay, 0);
325 return;
328 /* Substitute the current file name for % and the current line number
329 for # in the shell command */
330 strcpy(fullName, window->path);
331 strcat(fullName, window->filename);
332 pos = TextGetCursorPos(window->lastFocus);
333 TextPosToLineAndCol(window->lastFocus, pos, &line, &column);
334 sprintf(lineNumber, "%d", line);
336 subsCommand = shellCommandSubstitutes(command, fullName, lineNumber);
337 if (subsCommand == NULL) {
338 DialogF(DF_ERR, window->shell, 1,
339 "Shell command is too long due to\nfilename substitutions with '%%' or\n" \
340 "line number substitutions with '#'",
341 "OK");
342 return;
345 /* Get the command input as a text string. If there is input, errors
346 shouldn't be mixed in with output, so set flags to ERROR_DIALOGS */
347 if (input == FROM_SELECTION) {
348 text = BufGetSelectionText(window->buffer);
349 if (*text == '\0') {
350 XtFree(text);
351 free(subsCommand);
352 XBell(TheDisplay, 0);
353 return;
355 flags |= ACCUMULATE | ERROR_DIALOGS;
356 } else if (input == FROM_WINDOW) {
357 text = BufGetAll(window->buffer);
358 flags |= ACCUMULATE | ERROR_DIALOGS;
359 } else if (input == FROM_EITHER) {
360 text = BufGetSelectionText(window->buffer);
361 if (*text == '\0') {
362 XtFree(text);
363 text = BufGetAll(window->buffer);
365 flags |= ACCUMULATE | ERROR_DIALOGS;
366 } else /* FROM_NONE */
367 text = NULL;
369 /* If the buffer was substituting another character for ascii-nuls,
370 put the nuls back in before exporting the text */
371 if (text != NULL) {
372 textLen = strlen(text);
373 BufUnsubstituteNullChars(text, window->buffer);
374 } else
375 textLen = 0;
377 /* Assign the output destination. If output is to a new window,
378 create it, and run the command from it instead of the current
379 one, to free the current one from waiting for lengthy execution */
380 if (output == TO_DIALOG) {
381 outWidget = NULL;
382 flags |= OUTPUT_TO_DIALOG;
383 left = right = 0;
384 } else if (output == TO_NEW_WINDOW) {
385 EditNewFile(NULL, False, NULL, window->path);
386 outWidget = WindowList->textArea;
387 inWindow = WindowList;
388 left = right = 0;
389 } else { /* TO_SAME_WINDOW */
390 outWidget = window->lastFocus;
391 if (outputReplacesInput && input != FROM_NONE) {
392 if (input == FROM_WINDOW) {
393 left = 0;
394 right = window->buffer->length;
395 } else if (input == FROM_SELECTION) {
396 GetSimpleSelection(window->buffer, &left, &right);
397 flags |= ACCUMULATE | REPLACE_SELECTION;
398 } else if (input == FROM_EITHER) {
399 if (GetSimpleSelection(window->buffer, &left, &right))
400 flags |= ACCUMULATE | REPLACE_SELECTION;
401 else {
402 left = 0;
403 right = window->buffer->length;
406 } else {
407 if (GetSimpleSelection(window->buffer, &left, &right))
408 flags |= ACCUMULATE | REPLACE_SELECTION;
409 else
410 left = right = TextGetCursorPos(window->lastFocus);
414 /* If the command requires the file be saved first, save it */
415 if (saveFirst) {
416 if (!SaveWindow(window)) {
417 if (input != FROM_NONE)
418 XtFree(text);
419 free(subsCommand);
420 return;
424 /* If the command requires the file to be reloaded after execution, set
425 a flag for issueCommand to deal with it when execution is complete */
426 if (loadAfter)
427 flags |= RELOAD_FILE_AFTER;
429 /* issue the command */
430 issueCommand(inWindow, subsCommand, text, textLen, flags, outWidget, left,
431 right, fromMacro);
432 free(subsCommand);
436 ** Cancel the shell command in progress
438 void AbortShellCommand(WindowInfo *window)
440 shellCmdInfo *cmdData = window->shellCmdData;
442 if (cmdData == NULL)
443 return;
444 kill(- cmdData->childPid, SIGTERM);
445 finishCmdExecution(window, True);
449 ** Issue a shell command and feed it the string "input". Output can be
450 ** directed either to text widget "textW" where it replaces the text between
451 ** the positions "replaceLeft" and "replaceRight", to a separate pop-up dialog
452 ** (OUTPUT_TO_DIALOG), or to a macro-language string (OUTPUT_TO_STRING). If
453 ** "input" is NULL, no input is fed to the process. If an input string is
454 ** provided, it is freed when the command completes. Flags:
456 ** ACCUMULATE Causes output from the command to be saved up until
457 ** the command completes.
458 ** ERROR_DIALOGS Presents stderr output separately in popup a dialog,
459 ** and also reports failed exit status as a popup dialog
460 ** including the command output.
461 ** REPLACE_SELECTION Causes output to replace the selection in textW.
462 ** RELOAD_FILE_AFTER Causes the file to be completely reloaded after the
463 ** command completes.
464 ** OUTPUT_TO_DIALOG Send output to a pop-up dialog instead of textW
465 ** OUTPUT_TO_STRING Output to a macro-language string instead of a text
466 ** widget or dialog.
468 ** REPLACE_SELECTION, ERROR_DIALOGS, and OUTPUT_TO_STRING can only be used
469 ** along with ACCUMULATE (these operations can't be done incrementally).
471 static void issueCommand(WindowInfo *window, const char *command, char *input,
472 int inputLen, int flags, Widget textW, int replaceLeft,
473 int replaceRight, int fromMacro)
475 int stdinFD, stdoutFD, stderrFD;
476 XtAppContext context = XtWidgetToApplicationContext(window->shell);
477 shellCmdInfo *cmdData;
478 pid_t childPid;
480 /* verify consistency of input parameters */
481 if ((flags & ERROR_DIALOGS || flags & REPLACE_SELECTION ||
482 flags & OUTPUT_TO_STRING) && !(flags & ACCUMULATE))
483 return;
485 /* a shell command called from a macro must be executed in the same
486 window as the macro, regardless of where the output is directed,
487 so the user can cancel them as a unit */
488 if (fromMacro)
489 window = MacroRunWindow();
491 /* put up a watch cursor over the waiting window */
492 if (!fromMacro)
493 BeginWait(window->shell);
495 /* enable the cancel menu item */
496 if (!fromMacro)
497 XtSetSensitive(window->cancelShellItem, True);
499 /* fork the subprocess and issue the command */
500 childPid = forkCommand(window->shell, command, window->path, &stdinFD,
501 &stdoutFD, (flags & ERROR_DIALOGS) ? &stderrFD : NULL);
503 /* set the pipes connected to the process for non-blocking i/o */
504 if (fcntl(stdinFD, F_SETFL, O_NONBLOCK) < 0)
505 perror("NEdit: Internal error (fcntl)");
506 if (fcntl(stdoutFD, F_SETFL, O_NONBLOCK) < 0)
507 perror("NEdit: Internal error (fcntl1)");
508 if (flags & ERROR_DIALOGS) {
509 if (fcntl(stderrFD, F_SETFL, O_NONBLOCK) < 0)
510 perror("NEdit: Internal error (fcntl2)");
513 /* if there's nothing to write to the process' stdin, close it now */
514 if (input == NULL)
515 close(stdinFD);
517 /* Create a data structure for passing process information around
518 amongst the callback routines which will process i/o and completion */
519 cmdData = (shellCmdInfo *)XtMalloc(sizeof(shellCmdInfo));
520 window->shellCmdData = cmdData;
521 cmdData->flags = flags;
522 cmdData->stdinFD = stdinFD;
523 cmdData->stdoutFD = stdoutFD;
524 cmdData->stderrFD = stderrFD;
525 cmdData->childPid = childPid;
526 cmdData->outBufs = NULL;
527 cmdData->errBufs = NULL;
528 cmdData->input = input;
529 cmdData->inPtr = input;
530 cmdData->textW = textW;
531 cmdData->bannerIsUp = False;
532 cmdData->fromMacro = fromMacro;
533 cmdData->leftPos = replaceLeft;
534 cmdData->rightPos = replaceRight;
535 cmdData->inLength = inputLen;
537 /* Set up timer proc for putting up banner when process takes too long */
538 if (fromMacro)
539 cmdData->bannerTimeoutID = 0;
540 else
541 cmdData->bannerTimeoutID = XtAppAddTimeOut(context, BANNER_WAIT_TIME,
542 bannerTimeoutProc, window);
544 /* Set up timer proc for flushing output buffers periodically */
545 if ((flags & ACCUMULATE) || textW == NULL)
546 cmdData->flushTimeoutID = 0;
547 else
548 cmdData->flushTimeoutID = XtAppAddTimeOut(context, OUTPUT_FLUSH_FREQ,
549 flushTimeoutProc, window);
551 /* set up callbacks for activity on the file descriptors */
552 cmdData->stdoutInputID = XtAppAddInput(context, stdoutFD,
553 (XtPointer)XtInputReadMask, stdoutReadProc, window);
554 if (input != NULL)
555 cmdData->stdinInputID = XtAppAddInput(context, stdinFD,
556 (XtPointer)XtInputWriteMask, stdinWriteProc, window);
557 else
558 cmdData->stdinInputID = 0;
559 if (flags & ERROR_DIALOGS)
560 cmdData->stderrInputID = XtAppAddInput(context, stderrFD,
561 (XtPointer)XtInputReadMask, stderrReadProc, window);
562 else
563 cmdData->stderrInputID = 0;
565 /* If this was called from a macro, preempt the macro untill shell
566 command completes */
567 if (fromMacro)
568 PreemptMacro();
572 ** Called when the shell sub-process stdout stream has data. Reads data into
573 ** the "outBufs" buffer chain in the window->shellCommandData data structure.
575 static void stdoutReadProc(XtPointer clientData, int *source, XtInputId *id)
577 WindowInfo *window = (WindowInfo *)clientData;
578 shellCmdInfo *cmdData = window->shellCmdData;
579 buffer *buf;
580 int nRead;
582 /* read from the process' stdout stream */
583 buf = (buffer *)XtMalloc(sizeof(buffer));
584 nRead = read(cmdData->stdoutFD, buf->contents, IO_BUF_SIZE);
586 /* error in read */
587 if (nRead == -1) { /* error */
588 if (errno != EWOULDBLOCK && errno != EAGAIN) {
589 perror("NEdit: Error reading shell command output");
590 XtFree((char *)buf);
591 finishCmdExecution(window, True);
593 return;
596 /* end of data. If the stderr stream is done too, execution of the
597 shell process is complete, and we can display the results */
598 if (nRead == 0) {
599 XtFree((char *)buf);
600 XtRemoveInput(cmdData->stdoutInputID);
601 cmdData->stdoutInputID = 0;
602 if (cmdData->stderrInputID == 0)
603 finishCmdExecution(window, False);
604 return;
607 /* characters were read successfully, add buf to linked list of buffers */
608 buf->length = nRead;
609 addOutput(&cmdData->outBufs, buf);
613 ** Called when the shell sub-process stderr stream has data. Reads data into
614 ** the "errBufs" buffer chain in the window->shellCommandData data structure.
616 static void stderrReadProc(XtPointer clientData, int *source, XtInputId *id)
618 WindowInfo *window = (WindowInfo *)clientData;
619 shellCmdInfo *cmdData = window->shellCmdData;
620 buffer *buf;
621 int nRead;
623 /* read from the process' stderr stream */
624 buf = (buffer *)XtMalloc(sizeof(buffer));
625 nRead = read(cmdData->stderrFD, buf->contents, IO_BUF_SIZE);
627 /* error in read */
628 if (nRead == -1) {
629 if (errno != EWOULDBLOCK && errno != EAGAIN) {
630 perror("NEdit: Error reading shell command error stream");
631 XtFree((char *)buf);
632 finishCmdExecution(window, True);
634 return;
637 /* end of data. If the stdout stream is done too, execution of the
638 shell process is complete, and we can display the results */
639 if (nRead == 0) {
640 XtFree((char *)buf);
641 XtRemoveInput(cmdData->stderrInputID);
642 cmdData->stderrInputID = 0;
643 if (cmdData->stdoutInputID == 0)
644 finishCmdExecution(window, False);
645 return;
648 /* characters were read successfully, add buf to linked list of buffers */
649 buf->length = nRead;
650 addOutput(&cmdData->errBufs, buf);
654 ** Called when the shell sub-process stdin stream is ready for input. Writes
655 ** data from the "input" text string passed to issueCommand.
657 static void stdinWriteProc(XtPointer clientData, int *source, XtInputId *id)
659 WindowInfo *window = (WindowInfo *)clientData;
660 shellCmdInfo *cmdData = window->shellCmdData;
661 int nWritten;
663 nWritten = write(cmdData->stdinFD, cmdData->inPtr, cmdData->inLength);
664 if (nWritten == -1) {
665 if (errno == EPIPE) {
666 /* Just shut off input to broken pipes. User is likely feeding
667 it to a command which does not take input */
668 XtRemoveInput(cmdData->stdinInputID);
669 cmdData->stdinInputID = 0;
670 close(cmdData->stdinFD);
671 cmdData->inPtr = NULL;
672 } else if (errno != EWOULDBLOCK && errno != EAGAIN) {
673 perror("NEdit: Write to shell command failed");
674 finishCmdExecution(window, True);
676 } else {
677 cmdData->inPtr += nWritten;
678 cmdData->inLength -= nWritten;
679 if (cmdData->inLength <= 0) {
680 XtRemoveInput(cmdData->stdinInputID);
681 cmdData->stdinInputID = 0;
682 close(cmdData->stdinFD);
683 cmdData->inPtr = NULL;
689 ** Timer proc for putting up the "Shell Command in Progress" banner if
690 ** the process is taking too long.
692 static void bannerTimeoutProc(XtPointer clientData, XtIntervalId *id)
694 WindowInfo *window = (WindowInfo *)clientData;
695 shellCmdInfo *cmdData = window->shellCmdData;
697 cmdData->bannerIsUp = True;
698 SetModeMessage(window,
699 "Shell Command in Progress -- Press Ctrl+. to Cancel");
700 cmdData->bannerTimeoutID = 0;
704 ** Buffer replacement wrapper routine to be used for inserting output from
705 ** a command into the buffer, which takes into account that the buffer may
706 ** have been shrunken by the user (eg, by Undo). If necessary, the starting
707 ** and ending positions (part of the state of the command) are corrected.
709 static void safeBufReplace(textBuffer *buf, int *start, int *end,
710 const char *text)
712 if (*start > buf->length)
713 *start = buf->length;
714 if (*end > buf->length)
715 *end = buf->length;
716 BufReplace(buf, *start, *end, text);
720 ** Timer proc for flushing output buffers periodically when the process
721 ** takes too long.
723 static void flushTimeoutProc(XtPointer clientData, XtIntervalId *id)
725 WindowInfo *window = (WindowInfo *)clientData;
726 shellCmdInfo *cmdData = window->shellCmdData;
727 textBuffer *buf = TextGetBuffer(cmdData->textW);
728 int len;
729 char *outText;
731 /* shouldn't happen, but it would be bad if it did */
732 if (cmdData->textW == NULL)
733 return;
735 outText = coalesceOutput(&cmdData->outBufs, &len);
736 if (len != 0) {
737 if (BufSubstituteNullChars(outText, len, buf)) {
738 safeBufReplace(buf, &cmdData->leftPos, &cmdData->rightPos, outText);
739 TextSetCursorPos(cmdData->textW, cmdData->leftPos+strlen(outText));
740 cmdData->leftPos += len;
741 cmdData->rightPos = cmdData->leftPos;
742 } else
743 fprintf(stderr, "NEdit: Too much binary data\n");
745 XtFree(outText);
747 /* re-establish the timer proc (this routine) to continue processing */
748 cmdData->flushTimeoutID = XtAppAddTimeOut(
749 XtWidgetToApplicationContext(window->shell),
750 OUTPUT_FLUSH_FREQ, flushTimeoutProc, clientData);
754 ** Clean up after the execution of a shell command sub-process and present
755 ** the output/errors to the user as requested in the initial issueCommand
756 ** call. If "terminatedOnError" is true, don't bother trying to read the
757 ** output, just close the i/o descriptors, free the memory, and restore the
758 ** user interface state.
760 static void finishCmdExecution(WindowInfo *window, int terminatedOnError)
762 shellCmdInfo *cmdData = window->shellCmdData;
763 textBuffer *buf;
764 int status, failure, errorReport, reselectStart, outTextLen, errTextLen;
765 int resp, cancel = False, fromMacro = cmdData->fromMacro;
766 char *outText, *errText = NULL;
768 /* Cancel any pending i/o on the file descriptors */
769 if (cmdData->stdoutInputID != 0)
770 XtRemoveInput(cmdData->stdoutInputID);
771 if (cmdData->stdinInputID != 0)
772 XtRemoveInput(cmdData->stdinInputID);
773 if (cmdData->stderrInputID != 0)
774 XtRemoveInput(cmdData->stderrInputID);
776 /* Close any file descriptors remaining open */
777 close(cmdData->stdoutFD);
778 if (cmdData->flags & ERROR_DIALOGS)
779 close(cmdData->stderrFD);
780 if (cmdData->inPtr != NULL)
781 close(cmdData->stdinFD);
783 /* Free the provided input text */
784 if (cmdData->input != NULL)
785 XtFree(cmdData->input);
787 /* Cancel pending timeouts */
788 if (cmdData->flushTimeoutID != 0)
789 XtRemoveTimeOut(cmdData->flushTimeoutID);
790 if (cmdData->bannerTimeoutID != 0)
791 XtRemoveTimeOut(cmdData->bannerTimeoutID);
793 /* Clean up waiting-for-shell-command-to-complete mode */
794 if (!cmdData->fromMacro) {
795 EndWait(window->shell);
796 XtSetSensitive(window->cancelShellItem, False);
797 if (cmdData->bannerIsUp)
798 ClearModeMessage(window);
801 /* If the process was killed or became inaccessable, give up */
802 if (terminatedOnError) {
803 freeBufList(&cmdData->outBufs);
804 freeBufList(&cmdData->errBufs);
805 waitpid(cmdData->childPid, &status, 0);
806 goto cmdDone;
809 /* Assemble the output from the process' stderr and stdout streams into
810 null terminated strings, and free the buffer lists used to collect it */
811 outText = coalesceOutput(&cmdData->outBufs, &outTextLen);
812 if (cmdData->flags & ERROR_DIALOGS)
813 errText = coalesceOutput(&cmdData->errBufs, &errTextLen);
815 /* Wait for the child process to complete and get its return status */
816 waitpid(cmdData->childPid, &status, 0);
818 /* Present error and stderr-information dialogs. If a command returned
819 error output, or if the process' exit status indicated failure,
820 present the information to the user. */
821 if (cmdData->flags & ERROR_DIALOGS) {
822 failure = WIFEXITED(status) && WEXITSTATUS(status) != 0;
823 errorReport = *errText != '\0';
824 if (failure && errorReport) {
825 removeTrailingNewlines(errText);
826 truncateString(errText, DF_MAX_MSG_LENGTH);
827 resp = DialogF(DF_WARN, window->shell, 2, "%s",
828 "Cancel", "Proceed", errText);
829 cancel = resp == 1;
830 } else if (failure) {
831 truncateString(outText, DF_MAX_MSG_LENGTH-70);
832 resp = DialogF(DF_WARN, window->shell, 2,
833 "Command reported failed exit status.\nOutput from command:\n%s",
834 "Cancel", "Proceed", outText);
835 cancel = resp == 1;
836 } else if (errorReport) {
837 removeTrailingNewlines(errText);
838 truncateString(errText, DF_MAX_MSG_LENGTH);
839 resp = DialogF(DF_INF, window->shell, 2, "%s",
840 "Proceed", "Cancel", errText);
841 cancel = resp == 2;
843 XtFree(errText);
844 if (cancel) {
845 XtFree(outText);
846 goto cmdDone;
850 /* If output is to a dialog, present the dialog. Otherwise insert the
851 (remaining) output in the text widget as requested, and move the
852 insert point to the end */
853 if (cmdData->flags & OUTPUT_TO_DIALOG) {
854 removeTrailingNewlines(outText);
855 if (*outText != '\0')
856 createOutputDialog(window->shell, outText);
857 } else if (cmdData->flags & OUTPUT_TO_STRING) {
858 ReturnShellCommandOutput(window,outText, WEXITSTATUS(status));
859 } else {
860 buf = TextGetBuffer(cmdData->textW);
861 if (!BufSubstituteNullChars(outText, outTextLen, buf)) {
862 fprintf(stderr,"NEdit: Too much binary data in shell cmd output\n");
863 outText[0] = '\0';
865 if (cmdData->flags & REPLACE_SELECTION) {
866 reselectStart = buf->primary.rectangular ? -1 : buf->primary.start;
867 BufReplaceSelected(buf, outText);
868 TextSetCursorPos(cmdData->textW, buf->cursorPosHint);
869 if (reselectStart != -1)
870 BufSelect(buf, reselectStart, reselectStart + strlen(outText));
871 } else {
872 safeBufReplace(buf, &cmdData->leftPos, &cmdData->rightPos, outText);
873 TextSetCursorPos(cmdData->textW, cmdData->leftPos+strlen(outText));
877 /* If the command requires the file to be reloaded afterward, reload it */
878 if (cmdData->flags & RELOAD_FILE_AFTER)
879 RevertToSaved(window);
881 /* Command is complete, free data structure and continue macro execution */
882 XtFree(outText);
883 cmdDone:
884 XtFree((char *)cmdData);
885 window->shellCmdData = NULL;
886 if (fromMacro)
887 ResumeMacroExecution(window);
891 ** Fork a subprocess to execute a command, return file descriptors for pipes
892 ** connected to the subprocess' stdin, stdout, and stderr streams. cmdDir
893 ** sets the default directory for the subprocess. If stderrFD is passed as
894 ** NULL, the pipe represented by stdoutFD is connected to both stdin and
895 ** stderr. The function value returns the pid of the new subprocess, or -1
896 ** if an error occured.
898 static pid_t forkCommand(Widget parent, const char *command, const char *cmdDir,
899 int *stdinFD, int *stdoutFD, int *stderrFD)
901 int childStdoutFD, childStdinFD, childStderrFD, pipeFDs[2];
902 int dupFD;
903 pid_t childPid;
905 /* Ignore SIGPIPE signals generated when user attempts to provide
906 input for commands which don't take input */
907 signal(SIGPIPE, SIG_IGN);
909 /* Create pipes to communicate with the sub process. One end of each is
910 returned to the caller, the other half is spliced to stdin, stdout
911 and stderr in the child process */
912 if (pipe(pipeFDs) != 0) {
913 perror("NEdit: Internal error (opening stdout pipe)");
914 return -1;
916 *stdoutFD = pipeFDs[0];
917 childStdoutFD = pipeFDs[1];
918 if (pipe(pipeFDs) != 0) {
919 perror("NEdit: Internal error (opening stdin pipe)");
920 return -1;
922 *stdinFD = pipeFDs[1];
923 childStdinFD = pipeFDs[0];
924 if (stderrFD == NULL)
925 childStderrFD = childStdoutFD;
926 else {
927 if (pipe(pipeFDs) != 0) {
928 perror("NEdit: Internal error (opening stdin pipe)");
929 return -1;
931 *stderrFD = pipeFDs[0];
932 childStderrFD = pipeFDs[1];
935 /* Fork the process */
936 childPid = fork();
939 ** Child process context (fork returned 0), clean up the
940 ** child ends of the pipes and execute the command
942 if (0 == childPid) {
944 /* close the parent end of the pipes in the child process */
945 close(*stdinFD);
946 close(*stdoutFD);
947 if (stderrFD != NULL)
948 close(*stderrFD);
950 /* close current stdin, stdout, and stderr file descriptors before
951 substituting pipes */
952 close(fileno(stdin));
953 close(fileno(stdout));
954 close(fileno(stderr));
956 /* duplicate the child ends of the pipes to have the same numbers
957 as stdout & stderr, so it can substitute for stdout & stderr */
958 dupFD = dup2(childStdinFD, fileno(stdin));
959 if (dupFD == -1)
960 perror("dup of stdin failed");
961 dupFD = dup2(childStdoutFD, fileno(stdout));
962 if (dupFD == -1)
963 perror("dup of stdout failed");
964 dupFD = dup2(childStderrFD, fileno(stderr));
965 if (dupFD == -1)
966 perror("dup of stderr failed");
968 /* make this process the leader of a new process group, so the sub
969 processes can be killed, if necessary, with a killpg call */
970 #ifndef __EMX__ /* OS/2 doesn't have this */
971 setsid();
972 #endif
974 /* change the current working directory to the directory of the current
975 file. */
976 if(cmdDir[0] != 0)
977 if(chdir(cmdDir) == -1)
978 perror("chdir to directory of current file failed");
980 /* execute the command using the shell specified by preferences */
981 execl(GetPrefShell(), GetPrefShell(), "-c", command, (char *)0);
983 /* if we reach here, execl failed */
984 fprintf(stderr, "Error starting shell: %s\n", GetPrefShell());
985 exit(EXIT_FAILURE);
988 /* Parent process context, check if fork succeeded */
989 if (childPid == -1)
990 DialogF(DF_ERR, parent, 1,
991 "Error starting shell command process\n(fork failed)",
992 "Dismiss");
994 /* close the child ends of the pipes */
995 close(childStdinFD);
996 close(childStdoutFD);
997 if (stderrFD != NULL)
998 close(childStderrFD);
1000 return childPid;
1004 ** Add a buffer full of output to a buffer list
1006 static void addOutput(buffer **bufList, buffer *buf)
1008 buf->next = *bufList;
1009 *bufList = buf;
1013 ** coalesce the contents of a list of buffers into a contiguous memory block,
1014 ** freeing the memory occupied by the buffer list. Returns the memory block
1015 ** as the function result, and its length as parameter "length".
1017 static char *coalesceOutput(buffer **bufList, int *outLength)
1019 buffer *buf, *rBufList = NULL;
1020 char *outBuf, *outPtr, *p;
1021 int i, length = 0;
1023 /* find the total length of data read */
1024 for (buf=*bufList; buf!=NULL; buf=buf->next)
1025 length += buf->length;
1027 /* allocate contiguous memory for returning data */
1028 outBuf = XtMalloc(length+1);
1030 /* reverse the buffer list */
1031 while (*bufList != NULL) {
1032 buf = *bufList;
1033 *bufList = buf->next;
1034 buf->next = rBufList;
1035 rBufList = buf;
1038 /* copy the buffers into the output buffer */
1039 outPtr = outBuf;
1040 for (buf=rBufList; buf!=NULL; buf=buf->next) {
1041 p = buf->contents;
1042 for (i=0; i<buf->length; i++)
1043 *outPtr++ = *p++;
1046 /* terminate with a null */
1047 *outPtr = '\0';
1049 /* free the buffer list */
1050 freeBufList(&rBufList);
1052 *outLength = outPtr - outBuf;
1053 return outBuf;
1056 static void freeBufList(buffer **bufList)
1058 buffer *buf;
1060 while (*bufList != NULL) {
1061 buf = *bufList;
1062 *bufList = buf->next;
1063 XtFree((char *)buf);
1068 ** Remove trailing newlines from a string by substituting nulls
1070 static void removeTrailingNewlines(char *string)
1072 char *endPtr = &string[strlen(string)-1];
1074 while (endPtr >= string && *endPtr == '\n')
1075 *endPtr-- = '\0';
1079 ** Create a dialog for the output of a shell command. The dialog lives until
1080 ** the user presses the Dismiss button, and is then destroyed
1082 static void createOutputDialog(Widget parent, char *text)
1084 Arg al[50];
1085 int ac, rows, cols, hasScrollBar, wrapped;
1086 Widget form, textW, button;
1087 XmString st1;
1089 /* measure the width and height of the text to determine size for dialog */
1090 measureText(text, MAX_OUT_DIALOG_COLS, &rows, &cols, &wrapped);
1091 if (rows > MAX_OUT_DIALOG_ROWS) {
1092 rows = MAX_OUT_DIALOG_ROWS;
1093 hasScrollBar = True;
1094 } else
1095 hasScrollBar = False;
1096 if (cols > MAX_OUT_DIALOG_COLS)
1097 cols = MAX_OUT_DIALOG_COLS;
1098 if (cols == 0)
1099 cols = 1;
1100 /* Without completely emulating Motif's wrapping algorithm, we can't
1101 be sure that we haven't underestimated the number of lines in case
1102 a line has wrapped, so let's assume that some lines could be obscured
1104 if (wrapped)
1105 hasScrollBar = True;
1106 ac = 0;
1107 form = CreateFormDialog(parent, "shellOutForm", al, ac);
1109 ac = 0;
1110 XtSetArg(al[ac], XmNlabelString, st1=MKSTRING("Dismiss")); ac++;
1111 XtSetArg(al[ac], XmNhighlightThickness, 0); ac++;
1112 XtSetArg(al[ac], XmNbottomAttachment, XmATTACH_FORM); ac++;
1113 XtSetArg(al[ac], XmNtopAttachment, XmATTACH_NONE); ac++;
1114 button = XmCreatePushButtonGadget(form, "dismiss", al, ac);
1115 XtManageChild(button);
1116 XtVaSetValues(form, XmNdefaultButton, button, NULL);
1117 XmStringFree(st1);
1118 XtAddCallback(button, XmNactivateCallback, destroyOutDialogCB,
1119 XtParent(form));
1121 ac = 0;
1122 XtSetArg(al[ac], XmNrows, rows); ac++;
1123 XtSetArg(al[ac], XmNcolumns, cols); ac++;
1124 XtSetArg(al[ac], XmNresizeHeight, False); ac++;
1125 XtSetArg(al[ac], XmNtraversalOn, False); ac++;
1126 XtSetArg(al[ac], XmNwordWrap, True); ac++;
1127 XtSetArg(al[ac], XmNscrollHorizontal, False); ac++;
1128 XtSetArg(al[ac], XmNscrollVertical, hasScrollBar); ac++;
1129 XtSetArg(al[ac], XmNhighlightThickness, 0); ac++;
1130 XtSetArg(al[ac], XmNspacing, 0); ac++;
1131 XtSetArg(al[ac], XmNeditMode, XmMULTI_LINE_EDIT); ac++;
1132 XtSetArg(al[ac], XmNeditable, False); ac++;
1133 XtSetArg(al[ac], XmNvalue, text); ac++;
1134 XtSetArg(al[ac], XmNtopAttachment, XmATTACH_FORM); ac++;
1135 XtSetArg(al[ac], XmNleftAttachment, XmATTACH_FORM); ac++;
1136 XtSetArg(al[ac], XmNbottomAttachment, XmATTACH_WIDGET); ac++;
1137 XtSetArg(al[ac], XmNrightAttachment, XmATTACH_FORM); ac++;
1138 XtSetArg(al[ac], XmNbottomWidget, button); ac++;
1139 textW = XmCreateScrolledText(form, "outText", al, ac);
1140 XtManageChild(textW);
1142 XtVaSetValues(XtParent(form), XmNtitle, "Output from Command", NULL);
1143 ManageDialogCenteredOnPointer(form);
1147 ** Dispose of the command output dialog when user presses Dismiss button
1149 static void destroyOutDialogCB(Widget w, XtPointer callback, XtPointer closure)
1151 XtDestroyWidget((Widget)callback);
1155 ** Measure the width and height of a string of text. Assumes 8 character
1156 ** tabs. wrapWidth specifies a number of columns at which text wraps.
1158 static void measureText(char *text, int wrapWidth, int *rows, int *cols,
1159 int *wrapped)
1161 int maxCols = 0, line = 1, col = 0, wrapCol;
1162 char *c;
1164 *wrapped = 0;
1165 for (c=text; *c!='\0'; c++) {
1166 if (*c=='\n') {
1167 line++;
1168 col = 0;
1169 continue;
1172 if (*c == '\t') {
1173 col += 8 - (col % 8);
1174 wrapCol = 0; /* Tabs at end of line are not drawn when wrapped */
1175 } else if (*c == ' ') {
1176 col++;
1177 wrapCol = 0; /* Spaces at end of line are not drawn when wrapped */
1178 } else {
1179 col++;
1180 wrapCol = 1;
1183 /* Note: there is a small chance that the number of lines is
1184 over-estimated when a line ends with a space or a tab (ie, followed
1185 by a newline) and that whitespace crosses the boundary, because
1186 whitespace at the end of a line does not cause wrapping. Taking
1187 this into account is very hard, but an over-estimation is harmless.
1188 The worst that can happen is that some extra blank lines are shown
1189 at the end of the dialog (in contrast to an under-estimation, which
1190 could make the last lines invisible).
1191 On the other hand, without emulating Motif's wrapping algorithm
1192 completely, we can't be sure that we don't underestimate the number
1193 of lines (Motif uses word wrap, and this counting algorithm uses
1194 character wrap). Therefore, we remember whether there is a line
1195 that has wrapped. In that case we allways install a scroll bar.
1197 if (col > wrapWidth) {
1198 line++;
1199 *wrapped = 1;
1200 col = wrapCol;
1201 } else if (col > maxCols) {
1202 maxCols = col;
1205 *rows = line;
1206 *cols = maxCols;
1210 ** Truncate a string to a maximum of length characters. If it shortens the
1211 ** string, it appends "..." to show that it has been shortened. It assumes
1212 ** that the string that it is passed is writeable.
1214 static void truncateString(char *string, int length)
1216 if ((int)strlen(string) > length)
1217 memcpy(&string[length-3], "...", 4);
1221 ** Substitute the string fileStr in inStr wherever % appears and
1222 ** lineStr in inStr wherever # appears, storing the
1223 ** result in outStr. If predictOnly is non-zero, the result string length
1224 ** is predicted without creating the string. Returns the length of the result
1225 ** string or -1 in case of an error.
1228 static int shellSubstituter(char *outStr, const char *inStr, const char *fileStr,
1229 const char *lineStr, int outLen, int predictOnly)
1231 const char *inChar;
1232 char *outChar = NULL;
1233 int outWritten = 0;
1234 int fileLen, lineLen;
1236 inChar = inStr;
1237 if (!predictOnly) {
1238 outChar = outStr;
1240 fileLen = strlen(fileStr);
1241 lineLen = strlen(lineStr);
1243 while (*inChar != '\0') {
1245 if (!predictOnly && outWritten >= outLen) {
1246 return(-1);
1249 if (*inChar == '%') {
1250 if (*(inChar + 1) == '%') {
1251 inChar += 2;
1252 if (!predictOnly) {
1253 *outChar++ = '%';
1255 outWritten++;
1256 } else {
1257 if (!predictOnly) {
1258 if (outWritten + fileLen >= outLen) {
1259 return(-1);
1261 strncpy(outChar, fileStr, fileLen);
1262 outChar += fileLen;
1264 outWritten += fileLen;
1265 inChar++;
1267 } else if (*inChar == '#') {
1268 if (*(inChar + 1) == '#') {
1269 inChar += 2;
1270 if (!predictOnly) {
1271 *outChar++ = '#';
1273 outWritten++;
1274 } else {
1275 if (!predictOnly) {
1276 if (outWritten + lineLen >= outLen) {
1277 return(-1);
1279 strncpy(outChar, lineStr, lineLen);
1280 outChar += lineLen;
1282 outWritten += lineLen;
1283 inChar++;
1285 } else {
1286 if (!predictOnly) {
1287 *outChar++ = *inChar;
1289 inChar++;
1290 outWritten++;
1294 if (!predictOnly) {
1295 if (outWritten >= outLen) {
1296 return(-1);
1298 *outChar = '\0';
1300 ++outWritten;
1301 return(outWritten);
1304 static char *shellCommandSubstitutes(const char *inStr, const char *fileStr,
1305 const char *lineStr)
1307 int cmdLen;
1308 char *subsCmdStr = NULL;
1310 cmdLen = shellSubstituter(NULL, inStr, fileStr, lineStr, 0, 1);
1311 if (cmdLen >= 0) {
1312 subsCmdStr = malloc(cmdLen);
1313 if (subsCmdStr) {
1314 cmdLen = shellSubstituter(subsCmdStr, inStr, fileStr, lineStr, cmdLen, 0);
1315 if (cmdLen < 0) {
1316 free(subsCmdStr);
1317 subsCmdStr = NULL;
1321 return(subsCmdStr);