1 static const char CVSID
[] = "$Id: shell.c,v 1.26 2003/07/18 15:14:16 edg Exp $";
2 /*******************************************************************************
4 * shell.c -- Nirvana Editor shell command execution *
6 * Copyright (C) 1999 Mark Edel *
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 *
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 *
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 *
22 * Nirvana Text Editor *
25 * Written by Mark Edel *
27 *******************************************************************************/
30 #include "../config.h"
38 #include "preferences.h"
41 #include "interpret.h"
42 #include "../util/DialogF.h"
43 #include "../util/misc.h"
49 #include <sys/types.h>
51 #include <sys/param.h>
61 #include <sys/select.h>
70 #include <Xm/MessageB.h>
73 #include <Xm/PushBG.h>
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 */
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
{
101 char contents
[IO_BUF_SIZE
];
104 /* data attached to window during shell command execution with
105 information for controling and communicating with the process */
108 int stdinFD
, stdoutFD
, stderrFD
;
110 XtInputId stdinInputID
, stdoutInputID
, stderrInputID
;
111 buffer
*outBufs
, *errBufs
;
115 int leftPos
, rightPos
;
117 XtIntervalId bannerTimeoutID
, flushTimeoutID
;
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
,
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
,
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
;
159 /* Can't do two shell commands at once in the same window */
160 if (window
->shellCmdData
!= NULL
) {
161 XBell(TheDisplay
, 0);
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
);
170 XBell(TheDisplay
, 0);
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
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
;
195 /* Can't do two shell commands at once in the same window */
196 if (window
->shellCmdData
!= NULL
) {
197 XBell(TheDisplay
, 0);
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
;
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");
225 /* issue the command */
226 issueCommand(window
, subsCommand
, NULL
, 0, flags
, window
->lastFocus
, left
,
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
,
240 /* Make a copy of the input string for issueCommand to hold and free
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
)
256 int left
, right
, insertPos
;
257 char *subsCommand
, fullName
[MAXPATHLEN
];
258 int pos
, line
, column
;
261 /* Can't do two shell commands at once in the same window */
262 if (window
->shellCmdData
!= NULL
) {
263 XBell(TheDisplay
, 0);
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
)) {
271 left
= BufStartOfLine(window
->buffer
, left
);
272 right
= BufEndOfLine(window
->buffer
, right
);
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");
299 /* issue the command */
300 issueCommand(window
, subsCommand
, NULL
, 0, 0, window
->lastFocus
, insertPos
+1,
301 insertPos
+1, fromMacro
);
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
311 void DoShellMenuCmd(WindowInfo
*window
, const char *command
,
312 int input
, int output
,
313 int outputReplacesInput
, int saveFirst
, int loadAfter
, int fromMacro
)
317 char *subsCommand
, fullName
[MAXPATHLEN
];
318 int left
, right
, textLen
;
319 int pos
, line
, column
;
321 WindowInfo
*inWindow
= window
;
324 /* Can't do two shell commands at once in the same window */
325 if (window
->shellCmdData
!= NULL
) {
326 XBell(TheDisplay
, 0);
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");
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
);
355 XBell(TheDisplay
, 0);
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
);
366 text
= BufGetAll(window
->buffer
);
368 flags
|= ACCUMULATE
| ERROR_DIALOGS
;
369 } else /* FROM_NONE */
372 /* If the buffer was substituting another character for ascii-nuls,
373 put the nuls back in before exporting the text */
375 textLen
= strlen(text
);
376 BufUnsubstituteNullChars(text
, window
->buffer
);
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
) {
385 flags
|= OUTPUT_TO_DIALOG
;
387 } else if (output
== TO_NEW_WINDOW
) {
388 EditNewFile(NULL
, False
, NULL
, window
->path
);
389 outWidget
= WindowList
->textArea
;
390 inWindow
= WindowList
;
392 } else { /* TO_SAME_WINDOW */
393 outWidget
= window
->lastFocus
;
394 if (outputReplacesInput
&& input
!= FROM_NONE
) {
395 if (input
== FROM_WINDOW
) {
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
;
406 right
= window
->buffer
->length
;
410 if (GetSimpleSelection(window
->buffer
, &left
, &right
))
411 flags
|= ACCUMULATE
| REPLACE_SELECTION
;
413 left
= right
= TextGetCursorPos(window
->lastFocus
);
417 /* If the command requires the file be saved first, save it */
419 if (!SaveWindow(window
)) {
420 if (input
!= FROM_NONE
)
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 */
430 flags
|= RELOAD_FILE_AFTER
;
432 /* issue the command */
433 issueCommand(inWindow
, subsCommand
, text
, textLen
, flags
, outWidget
, left
,
439 ** Cancel the shell command in progress
441 void AbortShellCommand(WindowInfo
*window
)
443 shellCmdInfo
*cmdData
= window
->shellCmdData
;
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
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
;
483 /* verify consistency of input parameters */
484 if ((flags
& ERROR_DIALOGS
|| flags
& REPLACE_SELECTION
||
485 flags
& OUTPUT_TO_STRING
) && !(flags
& ACCUMULATE
))
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 */
492 window
= MacroRunWindow();
494 /* put up a watch cursor over the waiting window */
496 BeginWait(window
->shell
);
498 /* enable the cancel menu item */
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 */
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 */
542 cmdData
->bannerTimeoutID
= 0;
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;
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
);
558 cmdData
->stdinInputID
= XtAppAddInput(context
, stdinFD
,
559 (XtPointer
)XtInputWriteMask
, stdinWriteProc
, window
);
561 cmdData
->stdinInputID
= 0;
562 if (flags
& ERROR_DIALOGS
)
563 cmdData
->stderrInputID
= XtAppAddInput(context
, stderrFD
,
564 (XtPointer
)XtInputReadMask
, stderrReadProc
, window
);
566 cmdData
->stderrInputID
= 0;
568 /* If this was called from a macro, preempt the macro untill shell
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
;
585 /* read from the process' stdout stream */
586 buf
= (buffer
*)XtMalloc(sizeof(buffer
));
587 nRead
= read(cmdData
->stdoutFD
, buf
->contents
, IO_BUF_SIZE
);
590 if (nRead
== -1) { /* error */
591 if (errno
!= EWOULDBLOCK
&& errno
!= EAGAIN
) {
592 perror("NEdit: Error reading shell command output");
594 finishCmdExecution(window
, True
);
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 */
603 XtRemoveInput(cmdData
->stdoutInputID
);
604 cmdData
->stdoutInputID
= 0;
605 if (cmdData
->stderrInputID
== 0)
606 finishCmdExecution(window
, False
);
610 /* characters were read successfully, add buf to linked list of buffers */
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
;
626 /* read from the process' stderr stream */
627 buf
= (buffer
*)XtMalloc(sizeof(buffer
));
628 nRead
= read(cmdData
->stderrFD
, buf
->contents
, IO_BUF_SIZE
);
632 if (errno
!= EWOULDBLOCK
&& errno
!= EAGAIN
) {
633 perror("NEdit: Error reading shell command error stream");
635 finishCmdExecution(window
, True
);
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 */
644 XtRemoveInput(cmdData
->stderrInputID
);
645 cmdData
->stderrInputID
= 0;
646 if (cmdData
->stdoutInputID
== 0)
647 finishCmdExecution(window
, False
);
651 /* characters were read successfully, add buf to linked list of buffers */
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
;
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
);
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
,
715 if (*start
> buf
->length
)
716 *start
= buf
->length
;
717 if (*end
> buf
->length
)
719 BufReplace(buf
, *start
, *end
, text
);
723 ** Timer proc for flushing output buffers periodically when the process
726 static void flushTimeoutProc(XtPointer clientData
, XtIntervalId
*id
)
728 WindowInfo
*window
= (WindowInfo
*)clientData
;
729 shellCmdInfo
*cmdData
= window
->shellCmdData
;
730 textBuffer
*buf
= TextGetBuffer(cmdData
->textW
);
734 /* shouldn't happen, but it would be bad if it did */
735 if (cmdData
->textW
== NULL
)
738 outText
= coalesceOutput(&cmdData
->outBufs
, &len
);
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
;
746 fprintf(stderr
, "NEdit: Too much binary data\n");
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
;
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);
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",
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
);
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
);
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
));
870 buf
= TextGetBuffer(cmdData
->textW
);
871 if (!BufSubstituteNullChars(outText
, outTextLen
, buf
)) {
872 fprintf(stderr
,"NEdit: Too much binary data in shell cmd output\n");
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
));
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 */
894 XtFree((char *)cmdData
);
895 window
->shellCmdData
= NULL
;
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];
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)");
926 *stdoutFD
= pipeFDs
[0];
927 childStdoutFD
= pipeFDs
[1];
928 if (pipe(pipeFDs
) != 0) {
929 perror("NEdit: Internal error (opening stdin pipe)");
932 *stdinFD
= pipeFDs
[1];
933 childStdinFD
= pipeFDs
[0];
934 if (stderrFD
== NULL
)
935 childStderrFD
= childStdoutFD
;
937 if (pipe(pipeFDs
) != 0) {
938 perror("NEdit: Internal error (opening stdin pipe)");
941 *stderrFD
= pipeFDs
[0];
942 childStderrFD
= pipeFDs
[1];
945 /* Fork the process */
949 ** Child process context (fork returned 0), clean up the
950 ** child ends of the pipes and execute the command
954 /* close the parent end of the pipes in the child process */
957 if (stderrFD
!= NULL
)
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
));
970 perror("dup of stdin failed");
971 dupFD
= dup2(childStdoutFD
, fileno(stdout
));
973 perror("dup of stdout failed");
974 dupFD
= dup2(childStderrFD
, fileno(stderr
));
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 */
984 /* change the current working directory to the directory of the current
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());
998 /* Parent process context, check if fork succeeded */
1001 DialogF(DF_ERR
, parent
, 1, "Shell Command",
1002 "Error starting shell command process\n(fork failed)",
1006 /* close the child ends of the pipes */
1007 close(childStdinFD
);
1008 close(childStdoutFD
);
1009 if (stderrFD
!= NULL
)
1010 close(childStderrFD
);
1016 ** Add a buffer full of output to a buffer list
1018 static void addOutput(buffer
**bufList
, buffer
*buf
)
1020 buf
->next
= *bufList
;
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
;
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
) {
1045 *bufList
= buf
->next
;
1046 buf
->next
= rBufList
;
1050 /* copy the buffers into the output buffer */
1052 for (buf
=rBufList
; buf
!=NULL
; buf
=buf
->next
) {
1054 for (i
=0; i
<buf
->length
; i
++)
1058 /* terminate with a null */
1061 /* free the buffer list */
1062 freeBufList(&rBufList
);
1064 *outLength
= outPtr
- outBuf
;
1068 static void freeBufList(buffer
**bufList
)
1072 while (*bufList
!= NULL
) {
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')
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
)
1097 int ac
, rows
, cols
, hasScrollBar
, wrapped
;
1098 Widget form
, textW
, button
;
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
;
1107 hasScrollBar
= False
;
1108 if (cols
> MAX_OUT_DIALOG_COLS
)
1109 cols
= MAX_OUT_DIALOG_COLS
;
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
1117 hasScrollBar
= True
;
1119 form
= CreateFormDialog(parent
, "shellOutForm", al
, ac
);
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
);
1131 XtAddCallback(button
, XmNactivateCallback
, destroyOutDialogCB
,
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
,
1175 int maxCols
= 0, line
= 1, col
= 0, wrapCol
;
1179 for (c
=text
; *c
!='\0'; c
++) {
1187 col
+= 8 - (col
% 8);
1188 wrapCol
= 0; /* Tabs at end of line are not drawn when wrapped */
1189 } else if (*c
== ' ') {
1191 wrapCol
= 0; /* Spaces at end of line are not drawn when wrapped */
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
) {
1215 } else if (col
> 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
)
1246 char *outChar
= NULL
;
1248 int fileLen
, lineLen
;
1254 fileLen
= strlen(fileStr
);
1255 lineLen
= strlen(lineStr
);
1257 while (*inChar
!= '\0') {
1259 if (!predictOnly
&& outWritten
>= outLen
) {
1263 if (*inChar
== '%') {
1264 if (*(inChar
+ 1) == '%') {
1272 if (outWritten
+ fileLen
>= outLen
) {
1275 strncpy(outChar
, fileStr
, fileLen
);
1278 outWritten
+= fileLen
;
1281 } else if (*inChar
== '#') {
1282 if (*(inChar
+ 1) == '#') {
1290 if (outWritten
+ lineLen
>= outLen
) {
1293 strncpy(outChar
, lineStr
, lineLen
);
1296 outWritten
+= lineLen
;
1301 *outChar
++ = *inChar
;
1309 if (outWritten
>= outLen
) {
1318 static char *shellCommandSubstitutes(const char *inStr
, const char *fileStr
,
1319 const char *lineStr
)
1322 char *subsCmdStr
= NULL
;
1324 cmdLen
= shellSubstituter(NULL
, inStr
, fileStr
, lineStr
, 0, 1);
1326 subsCmdStr
= malloc(cmdLen
);
1328 cmdLen
= shellSubstituter(subsCmdStr
, inStr
, fileStr
, lineStr
, cmdLen
, 0);