UNIXworkcode

1 /******************************************************************************* 2 * * 3 * shell.c -- Nirvana Editor shell command execution * 4 * * 5 * Copyright (C) 1999 Mark Edel * 6 * * 7 * This is free software; you can redistribute it and/or modify it under the * 8 * terms of the GNU General Public License as published by the Free Software * 9 * Foundation; either version 2 of the License, or (at your option) any later * 10 * version. In addition, you may distribute version of this program linked to * 11 * Motif or Open Motif. See README for details. * 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 *******************************************************************************/ 28 29 #ifdef HAVE_CONFIG_H 30 #include "../config.h" 31 #endif 32 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" 44 #include "../util/nedit_malloc.h" 45 #include "menu.h" 46 47 #include <stdio.h> 48 #include <stdlib.h> 49 #include <string.h> 50 #include <signal.h> 51 #include <sys/types.h> 52 #include <sys/param.h> 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 68 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> 74 75 #ifdef HAVE_DEBUG_H 76 #include "../debug.h" 77 #endif 78 79 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 */ 88 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 96 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; 103 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; 121 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); 148 149 /* 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. 153 */ 154 void FilterSelection(WindowInfo *window, const char *command, int fromMacro) 155 { 156 int left, right, textLen; 157 char *text; 158 159 /* Can't do two shell commands at once in the same window */ 160 if (window->shellCmdData != NULL) { 161 XBell(TheDisplay, 0); 162 return; 163 } 164 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 NEditFree(text); 170 XBell(TheDisplay, 0); 171 return; 172 } 173 textLen = strlen(text); 174 BufUnsubstituteNullChars(text, window->buffer); 175 left = window->buffer->primary.start; 176 right = window->buffer->primary.end; 177 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); 181 } 182 183 /* 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. 187 */ 188 void ExecShellCommand(WindowInfo *window, const char *command, int fromMacro) 189 { 190 int left, right, flags = 0; 191 char *subsCommand, fullName[MAXPATHLEN]; 192 int pos, line, column; 193 char lineNumber[16]; 194 195 /* Can't do two shell commands at once in the same window */ 196 if (window->shellCmdData != NULL) { 197 XBell(TheDisplay, 0); 198 return; 199 } 200 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; 207 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 if(!TextPosToLineAndCol(window->lastFocus, pos, &line, &column)) { 213 line = BufCountLines(window->buffer, 0, pos) + 1; 214 } 215 snprintf(lineNumber, 16, "%d", line); 216 subsCommand = shellCommandSubstitutes(command, fullName, lineNumber); 217 if (subsCommand == NULL) 218 { 219 DialogF(DF_ERR, window->shell, 1, "Shell Command", 220 "Shell command is too long due to\n" 221 "filename substitutions with ''%%'' or\n" 222 "line number substitutions with ''#''", "OK"); 223 return; 224 } 225 226 /* issue the command */ 227 issueCommand(window, subsCommand, NULL, 0, flags, window->lastFocus, left, 228 right, fromMacro); 229 free(subsCommand); 230 } 231 232 /* 233 ** Execute shell command "command", on input string "input", depositing the 234 ** in a macro string (via a call back to ReturnShellCommandOutput). 235 */ 236 void ShellCmdToMacroString(WindowInfo *window, const char *command, 237 const char *input) 238 { 239 char *inputCopy; 240 241 /* Make a copy of the input string for issueCommand to hold and free 242 upon completion */ 243 inputCopy = *input == '\0' ? NULL : NEditStrdup(input); 244 245 /* fork the command and begin processing input/output */ 246 issueCommand(window, command, inputCopy, strlen(input), 247 ACCUMULATE | OUTPUT_TO_STRING, NULL, 0, 0, True); 248 } 249 250 /* 251 ** Execute the line of text where the the insertion cursor is positioned 252 ** as a shell command. 253 */ 254 void ExecCursorLine(WindowInfo *window, int fromMacro) 255 { 256 char *cmdText; 257 int left, right, insertPos; 258 char *subsCommand, fullName[MAXPATHLEN]; 259 int pos, line, column; 260 char lineNumber[16]; 261 262 /* Can't do two shell commands at once in the same window */ 263 if (window->shellCmdData != NULL) { 264 XBell(TheDisplay, 0); 265 return; 266 } 267 268 /* get all of the text on the line with the insert position */ 269 pos = TextGetCursorPos(window->lastFocus); 270 if (!GetSimpleSelection(window->buffer, &left, &right)) { 271 left = right = pos; 272 left = BufStartOfLine(window->buffer, left); 273 right = BufEndOfLine(window->buffer, right); 274 insertPos = right; 275 } else 276 insertPos = BufEndOfLine(window->buffer, right); 277 cmdText = BufGetRange(window->buffer, left, right); 278 BufUnsubstituteNullChars(cmdText, window->buffer); 279 280 /* insert a newline after the entire line */ 281 BufInsert(window->buffer, insertPos, "\n"); 282 283 /* Substitute the current file name for % and the current line number 284 for # in the shell command */ 285 strcpy(fullName, window->path); 286 strcat(fullName, window->filename); 287 if(!TextPosToLineAndCol(window->lastFocus, pos, &line, &column)) { 288 line = BufCountLines(window->buffer, 0, pos) + 1; 289 } 290 snprintf(lineNumber, 16, "%d", line); 291 292 subsCommand = shellCommandSubstitutes(cmdText, fullName, lineNumber); 293 if (subsCommand == NULL) 294 { 295 DialogF(DF_ERR, window->shell, 1, "Shell Command", 296 "Shell command is too long due to\n" 297 "filename substitutions with ''%%'' or\n" 298 "line number substitutions with ''#''", "OK"); 299 return; 300 } 301 302 /* issue the command */ 303 issueCommand(window, subsCommand, NULL, 0, 0, window->lastFocus, insertPos+1, 304 insertPos+1, fromMacro); 305 free(subsCommand); 306 NEditFree(cmdText); 307 } 308 309 /* 310 ** Do a shell command, with the options allowed to users (input source, 311 ** output destination, save first and load after) in the shell commands 312 ** menu. 313 */ 314 void DoShellMenuCmd(WindowInfo *window, const char *command, 315 int input, int output, 316 int outputReplacesInput, int saveFirst, int loadAfter, int fromMacro) 317 { 318 int flags = 0; 319 char *text; 320 char *subsCommand, fullName[MAXPATHLEN]; 321 int left = 0, right = 0, textLen; 322 int pos, line, column; 323 char lineNumber[16]; 324 WindowInfo *inWindow = window; 325 Widget outWidget; 326 327 /* Can't do two shell commands at once in the same window */ 328 if (window->shellCmdData != NULL) { 329 XBell(TheDisplay, 0); 330 return; 331 } 332 333 /* Substitute the current file name for % and the current line number 334 for # in the shell command */ 335 strcpy(fullName, window->path); 336 strcat(fullName, window->filename); 337 pos = TextGetCursorPos(window->lastFocus); 338 if(!TextPosToLineAndCol(window->lastFocus, pos, &line, &column)) { 339 line = BufCountLines(window->buffer, 0, pos) + 1; 340 } 341 snprintf(lineNumber, 16, "%d", line); 342 343 subsCommand = shellCommandSubstitutes(command, fullName, lineNumber); 344 if (subsCommand == NULL) 345 { 346 DialogF(DF_ERR, window->shell, 1, "Shell Command", 347 "Shell command is too long due to\n" 348 "filename substitutions with ''%%'' or\n" 349 "line number substitutions with ''#''", "OK"); 350 return; 351 } 352 353 /* Get the command input as a text string. If there is input, errors 354 shouldn't be mixed in with output, so set flags to ERROR_DIALOGS */ 355 if (input == FROM_SELECTION) { 356 text = BufGetSelectionText(window->buffer); 357 if (*text == '\0') { 358 NEditFree(text); 359 NEditFree(subsCommand); 360 XBell(TheDisplay, 0); 361 return; 362 } 363 flags |= ACCUMULATE | ERROR_DIALOGS; 364 } else if (input == FROM_WINDOW) { 365 text = BufGetAll(window->buffer); 366 flags |= ACCUMULATE | ERROR_DIALOGS; 367 } else if (input == FROM_EITHER) { 368 text = BufGetSelectionText(window->buffer); 369 if (*text == '\0') { 370 NEditFree(text); 371 text = BufGetAll(window->buffer); 372 } 373 flags |= ACCUMULATE | ERROR_DIALOGS; 374 } else /* FROM_NONE */ 375 text = NULL; 376 377 /* If the buffer was substituting another character for ascii-nuls, 378 put the nuls back in before exporting the text */ 379 if (text != NULL) { 380 textLen = strlen(text); 381 BufUnsubstituteNullChars(text, window->buffer); 382 } else 383 textLen = 0; 384 385 /* Assign the output destination. If output is to a new window, 386 create it, and run the command from it instead of the current 387 one, to free the current one from waiting for lengthy execution */ 388 if (output == TO_DIALOG) { 389 outWidget = NULL; 390 flags |= OUTPUT_TO_DIALOG; 391 left = right = 0; 392 } else if (output == TO_NEW_WINDOW) { 393 EditNewFile(GetPrefOpenInTab()?inWindow:NULL, NULL, False, NULL, window->path); 394 outWidget = WindowList->textArea; 395 inWindow = WindowList; 396 left = right = 0; 397 CheckCloseDim(); 398 } else { /* TO_SAME_WINDOW */ 399 outWidget = window->lastFocus; 400 if (outputReplacesInput && input != FROM_NONE) { 401 if (input == FROM_WINDOW) { 402 left = 0; 403 right = window->buffer->length; 404 } else if (input == FROM_SELECTION) { 405 GetSimpleSelection(window->buffer, &left, &right); 406 flags |= ACCUMULATE | REPLACE_SELECTION; 407 } else if (input == FROM_EITHER) { 408 if (GetSimpleSelection(window->buffer, &left, &right)) 409 flags |= ACCUMULATE | REPLACE_SELECTION; 410 else { 411 left = 0; 412 right = window->buffer->length; 413 } 414 } 415 } else { 416 if (GetSimpleSelection(window->buffer, &left, &right)) 417 flags |= ACCUMULATE | REPLACE_SELECTION; 418 else 419 left = right = TextGetCursorPos(window->lastFocus); 420 } 421 } 422 423 /* If the command requires the file be saved first, save it */ 424 if (saveFirst) { 425 if (!SaveWindow(window)) { 426 if (input != FROM_NONE) 427 NEditFree(text); 428 free(subsCommand); 429 return; 430 } 431 } 432 433 /* If the command requires the file to be reloaded after execution, set 434 a flag for issueCommand to deal with it when execution is complete */ 435 if (loadAfter) 436 flags |= RELOAD_FILE_AFTER; 437 438 /* issue the command */ 439 issueCommand(inWindow, subsCommand, text, textLen, flags, outWidget, left, 440 right, fromMacro); 441 free(subsCommand); 442 } 443 444 /* 445 ** Cancel the shell command in progress 446 */ 447 void AbortShellCommand(WindowInfo *window) 448 { 449 shellCmdInfo *cmdData = window->shellCmdData; 450 451 if (cmdData == NULL) 452 return; 453 kill(- cmdData->childPid, SIGTERM); 454 finishCmdExecution(window, True); 455 } 456 457 /* 458 ** Issue a shell command and feed it the string "input". Output can be 459 ** directed either to text widget "textW" where it replaces the text between 460 ** the positions "replaceLeft" and "replaceRight", to a separate pop-up dialog 461 ** (OUTPUT_TO_DIALOG), or to a macro-language string (OUTPUT_TO_STRING). If 462 ** "input" is NULL, no input is fed to the process. If an input string is 463 ** provided, it is freed when the command completes. Flags: 464 ** 465 ** ACCUMULATE Causes output from the command to be saved up until 466 ** the command completes. 467 ** ERROR_DIALOGS Presents stderr output separately in popup a dialog, 468 ** and also reports failed exit status as a popup dialog 469 ** including the command output. 470 ** REPLACE_SELECTION Causes output to replace the selection in textW. 471 ** RELOAD_FILE_AFTER Causes the file to be completely reloaded after the 472 ** command completes. 473 ** OUTPUT_TO_DIALOG Send output to a pop-up dialog instead of textW 474 ** OUTPUT_TO_STRING Output to a macro-language string instead of a text 475 ** widget or dialog. 476 ** 477 ** REPLACE_SELECTION, ERROR_DIALOGS, and OUTPUT_TO_STRING can only be used 478 ** along with ACCUMULATE (these operations can't be done incrementally). 479 */ 480 static void issueCommand(WindowInfo *window, const char *command, char *input, 481 int inputLen, int flags, Widget textW, int replaceLeft, 482 int replaceRight, int fromMacro) 483 { 484 int stdinFD, stdoutFD, stderrFD = 0; 485 XtAppContext context = XtWidgetToApplicationContext(window->shell); 486 shellCmdInfo *cmdData; 487 pid_t childPid; 488 489 /* verify consistency of input parameters */ 490 if ((flags & ERROR_DIALOGS || flags & REPLACE_SELECTION || 491 flags & OUTPUT_TO_STRING) && !(flags & ACCUMULATE)) 492 return; 493 494 /* a shell command called from a macro must be executed in the same 495 window as the macro, regardless of where the output is directed, 496 so the user can cancel them as a unit */ 497 if (fromMacro) 498 window = MacroRunWindow(); 499 500 /* put up a watch cursor over the waiting window */ 501 if (!fromMacro) 502 BeginWait(window->shell); 503 504 /* enable the cancel menu item */ 505 if (!fromMacro) 506 SetSensitive(window, window->cancelShellItem, True); 507 508 /* fork the subprocess and issue the command */ 509 childPid = forkCommand(window->shell, command, window->path, &stdinFD, 510 &stdoutFD, (flags & ERROR_DIALOGS) ? &stderrFD : NULL); 511 512 /* set the pipes connected to the process for non-blocking i/o */ 513 if (fcntl(stdinFD, F_SETFL, O_NONBLOCK) < 0) 514 perror("xnedit: Internal error (fcntl)"); 515 if (fcntl(stdoutFD, F_SETFL, O_NONBLOCK) < 0) 516 perror("xnedit: Internal error (fcntl1)"); 517 if (flags & ERROR_DIALOGS) { 518 if (fcntl(stderrFD, F_SETFL, O_NONBLOCK) < 0) 519 perror("xnedit: Internal error (fcntl2)"); 520 } 521 522 /* if there's nothing to write to the process' stdin, close it now */ 523 if (input == NULL) 524 close(stdinFD); 525 526 /* Create a data structure for passing process information around 527 amongst the callback routines which will process i/o and completion */ 528 cmdData = (shellCmdInfo *)NEditMalloc(sizeof(shellCmdInfo)); 529 window->shellCmdData = cmdData; 530 cmdData->flags = flags; 531 cmdData->stdinFD = stdinFD; 532 cmdData->stdoutFD = stdoutFD; 533 cmdData->stderrFD = stderrFD; 534 cmdData->childPid = childPid; 535 cmdData->outBufs = NULL; 536 cmdData->errBufs = NULL; 537 cmdData->input = input; 538 cmdData->inPtr = input; 539 cmdData->textW = textW; 540 cmdData->bannerIsUp = False; 541 cmdData->fromMacro = fromMacro; 542 cmdData->leftPos = replaceLeft; 543 cmdData->rightPos = replaceRight; 544 cmdData->inLength = inputLen; 545 546 /* Set up timer proc for putting up banner when process takes too long */ 547 if (fromMacro) 548 cmdData->bannerTimeoutID = 0; 549 else 550 cmdData->bannerTimeoutID = XtAppAddTimeOut(context, BANNER_WAIT_TIME, 551 bannerTimeoutProc, window); 552 553 /* Set up timer proc for flushing output buffers periodically */ 554 if ((flags & ACCUMULATE) || textW == NULL) 555 cmdData->flushTimeoutID = 0; 556 else 557 cmdData->flushTimeoutID = XtAppAddTimeOut(context, OUTPUT_FLUSH_FREQ, 558 flushTimeoutProc, window); 559 560 /* set up callbacks for activity on the file descriptors */ 561 cmdData->stdoutInputID = XtAppAddInput(context, stdoutFD, 562 (XtPointer)XtInputReadMask, stdoutReadProc, window); 563 if (input != NULL) 564 cmdData->stdinInputID = XtAppAddInput(context, stdinFD, 565 (XtPointer)XtInputWriteMask, stdinWriteProc, window); 566 else 567 cmdData->stdinInputID = 0; 568 if (flags & ERROR_DIALOGS) 569 cmdData->stderrInputID = XtAppAddInput(context, stderrFD, 570 (XtPointer)XtInputReadMask, stderrReadProc, window); 571 else 572 cmdData->stderrInputID = 0; 573 574 /* If this was called from a macro, preempt the macro untill shell 575 command completes */ 576 if (fromMacro) 577 PreemptMacro(); 578 } 579 580 /* 581 ** Called when the shell sub-process stdout stream has data. Reads data into 582 ** the "outBufs" buffer chain in the window->shellCommandData data structure. 583 */ 584 static void stdoutReadProc(XtPointer clientData, int *source, XtInputId *id) 585 { 586 WindowInfo *window = (WindowInfo *)clientData; 587 shellCmdInfo *cmdData = window->shellCmdData; 588 buffer *buf; 589 int nRead; 590 591 /* read from the process' stdout stream */ 592 buf = (buffer *)NEditMalloc(sizeof(buffer)); 593 nRead = read(cmdData->stdoutFD, buf->contents, IO_BUF_SIZE); 594 595 /* error in read */ 596 if (nRead == -1) { /* error */ 597 if (errno != EWOULDBLOCK && errno != EAGAIN) { 598 perror("xnedit: Error reading shell command output"); 599 NEditFree(buf); 600 finishCmdExecution(window, True); 601 } 602 return; 603 } 604 605 /* end of data. If the stderr stream is done too, execution of the 606 shell process is complete, and we can display the results */ 607 if (nRead == 0) { 608 NEditFree(buf); 609 XtRemoveInput(cmdData->stdoutInputID); 610 cmdData->stdoutInputID = 0; 611 if (cmdData->stderrInputID == 0) 612 finishCmdExecution(window, False); 613 return; 614 } 615 616 /* characters were read successfully, add buf to linked list of buffers */ 617 buf->length = nRead; 618 addOutput(&cmdData->outBufs, buf); 619 } 620 621 /* 622 ** Called when the shell sub-process stderr stream has data. Reads data into 623 ** the "errBufs" buffer chain in the window->shellCommandData data structure. 624 */ 625 static void stderrReadProc(XtPointer clientData, int *source, XtInputId *id) 626 { 627 WindowInfo *window = (WindowInfo *)clientData; 628 shellCmdInfo *cmdData = window->shellCmdData; 629 buffer *buf; 630 int nRead; 631 632 /* read from the process' stderr stream */ 633 buf = (buffer *)NEditMalloc(sizeof(buffer)); 634 nRead = read(cmdData->stderrFD, buf->contents, IO_BUF_SIZE); 635 636 /* error in read */ 637 if (nRead == -1) { 638 if (errno != EWOULDBLOCK && errno != EAGAIN) { 639 perror("xnedit: Error reading shell command error stream"); 640 NEditFree(buf); 641 finishCmdExecution(window, True); 642 } 643 return; 644 } 645 646 /* end of data. If the stdout stream is done too, execution of the 647 shell process is complete, and we can display the results */ 648 if (nRead == 0) { 649 NEditFree(buf); 650 XtRemoveInput(cmdData->stderrInputID); 651 cmdData->stderrInputID = 0; 652 if (cmdData->stdoutInputID == 0) 653 finishCmdExecution(window, False); 654 return; 655 } 656 657 /* characters were read successfully, add buf to linked list of buffers */ 658 buf->length = nRead; 659 addOutput(&cmdData->errBufs, buf); 660 } 661 662 /* 663 ** Called when the shell sub-process stdin stream is ready for input. Writes 664 ** data from the "input" text string passed to issueCommand. 665 */ 666 static void stdinWriteProc(XtPointer clientData, int *source, XtInputId *id) 667 { 668 WindowInfo *window = (WindowInfo *)clientData; 669 shellCmdInfo *cmdData = window->shellCmdData; 670 int nWritten; 671 672 nWritten = write(cmdData->stdinFD, cmdData->inPtr, cmdData->inLength); 673 if (nWritten == -1) { 674 if (errno == EPIPE) { 675 /* Just shut off input to broken pipes. User is likely feeding 676 it to a command which does not take input */ 677 XtRemoveInput(cmdData->stdinInputID); 678 cmdData->stdinInputID = 0; 679 close(cmdData->stdinFD); 680 cmdData->inPtr = NULL; 681 } else if (errno != EWOULDBLOCK && errno != EAGAIN) { 682 perror("xnedit: Write to shell command failed"); 683 finishCmdExecution(window, True); 684 } 685 } else { 686 cmdData->inPtr += nWritten; 687 cmdData->inLength -= nWritten; 688 if (cmdData->inLength <= 0) { 689 XtRemoveInput(cmdData->stdinInputID); 690 cmdData->stdinInputID = 0; 691 close(cmdData->stdinFD); 692 cmdData->inPtr = NULL; 693 } 694 } 695 } 696 697 /* 698 ** Timer proc for putting up the "Shell Command in Progress" banner if 699 ** the process is taking too long. 700 */ 701 #define MAX_TIMEOUT_MSG_LEN (MAX_ACCEL_LEN + 60) 702 static void bannerTimeoutProc(XtPointer clientData, XtIntervalId *id) 703 { 704 WindowInfo *window = (WindowInfo *)clientData; 705 shellCmdInfo *cmdData = window->shellCmdData; 706 XmString xmCancel; 707 char* cCancel; 708 char message[MAX_TIMEOUT_MSG_LEN]; 709 710 cmdData->bannerIsUp = True; 711 712 /* Extract accelerator text from menu PushButtons */ 713 XtVaGetValues(window->cancelShellItem, XmNacceleratorText, &xmCancel, NULL); 714 715 /* Translate Motif string to char* */ 716 cCancel = GetXmStringText(xmCancel); 717 718 /* Free Motif String */ 719 XmStringFree(xmCancel); 720 721 /* Create message */ 722 if ('\0' == cCancel[0]) 723 { 724 strncpy(message, "Shell Command in Progress", MAX_TIMEOUT_MSG_LEN); 725 message[MAX_TIMEOUT_MSG_LEN - 1] = '\0'; 726 } else 727 { 728 snprintf(message, 729 MAX_TIMEOUT_MSG_LEN, 730 "Shell Command in Progress -- Press %s to Cancel", 731 cCancel); 732 } 733 734 /* Free C-string */ 735 NEditFree(cCancel); 736 737 SetModeMessage(window, message); 738 cmdData->bannerTimeoutID = 0; 739 } 740 741 /* 742 ** Buffer replacement wrapper routine to be used for inserting output from 743 ** a command into the buffer, which takes into account that the buffer may 744 ** have been shrunken by the user (eg, by Undo). If necessary, the starting 745 ** and ending positions (part of the state of the command) are corrected. 746 */ 747 static void safeBufReplace(textBuffer *buf, int *start, int *end, 748 const char *text) 749 { 750 if (*start > buf->length) 751 *start = buf->length; 752 if (*end > buf->length) 753 *end = buf->length; 754 BufReplace(buf, *start, *end, text); 755 } 756 757 /* 758 ** Timer proc for flushing output buffers periodically when the process 759 ** takes too long. 760 */ 761 static void flushTimeoutProc(XtPointer clientData, XtIntervalId *id) 762 { 763 WindowInfo *window = (WindowInfo *)clientData; 764 shellCmdInfo *cmdData = window->shellCmdData; 765 textBuffer *buf = TextGetBuffer(cmdData->textW); 766 int len; 767 char *outText; 768 769 /* shouldn't happen, but it would be bad if it did */ 770 if (cmdData->textW == NULL) 771 return; 772 773 outText = coalesceOutput(&cmdData->outBufs, &len); 774 if (len != 0) { 775 if (BufSubstituteNullChars(outText, len, buf)) { 776 safeBufReplace(buf, &cmdData->leftPos, &cmdData->rightPos, outText); 777 TextSetCursorPos(cmdData->textW, cmdData->leftPos+strlen(outText)); 778 cmdData->leftPos += len; 779 cmdData->rightPos = cmdData->leftPos; 780 } else 781 fprintf(stderr, "xnedit: Too much binary data\n"); 782 } 783 NEditFree(outText); 784 785 /* re-establish the timer proc (this routine) to continue processing */ 786 cmdData->flushTimeoutID = XtAppAddTimeOut( 787 XtWidgetToApplicationContext(window->shell), 788 OUTPUT_FLUSH_FREQ, flushTimeoutProc, clientData); 789 } 790 791 /* 792 ** Clean up after the execution of a shell command sub-process and present 793 ** the output/errors to the user as requested in the initial issueCommand 794 ** call. If "terminatedOnError" is true, don't bother trying to read the 795 ** output, just close the i/o descriptors, free the memory, and restore the 796 ** user interface state. 797 */ 798 static void finishCmdExecution(WindowInfo *window, int terminatedOnError) 799 { 800 shellCmdInfo *cmdData = window->shellCmdData; 801 textBuffer *buf; 802 int status, failure, errorReport, reselectStart, outTextLen, errTextLen; 803 int resp, cancel = False, fromMacro = cmdData->fromMacro; 804 char *outText, *errText = NULL; 805 806 /* Cancel any pending i/o on the file descriptors */ 807 if (cmdData->stdoutInputID != 0) 808 XtRemoveInput(cmdData->stdoutInputID); 809 if (cmdData->stdinInputID != 0) 810 XtRemoveInput(cmdData->stdinInputID); 811 if (cmdData->stderrInputID != 0) 812 XtRemoveInput(cmdData->stderrInputID); 813 814 /* Close any file descriptors remaining open */ 815 close(cmdData->stdoutFD); 816 if (cmdData->flags & ERROR_DIALOGS) 817 close(cmdData->stderrFD); 818 if (cmdData->inPtr != NULL) 819 close(cmdData->stdinFD); 820 821 /* Free the provided input text */ 822 NEditFree(cmdData->input); 823 824 /* Cancel pending timeouts */ 825 if (cmdData->flushTimeoutID != 0) 826 XtRemoveTimeOut(cmdData->flushTimeoutID); 827 if (cmdData->bannerTimeoutID != 0) 828 XtRemoveTimeOut(cmdData->bannerTimeoutID); 829 830 /* Clean up waiting-for-shell-command-to-complete mode */ 831 if (!cmdData->fromMacro) { 832 EndWait(window->shell); 833 SetSensitive(window, window->cancelShellItem, False); 834 if (cmdData->bannerIsUp) 835 ClearModeMessage(window); 836 } 837 838 /* If the process was killed or became inaccessable, give up */ 839 if (terminatedOnError) { 840 freeBufList(&cmdData->outBufs); 841 freeBufList(&cmdData->errBufs); 842 waitpid(cmdData->childPid, &status, 0); 843 goto cmdDone; 844 } 845 846 /* Assemble the output from the process' stderr and stdout streams into 847 null terminated strings, and free the buffer lists used to collect it */ 848 outText = coalesceOutput(&cmdData->outBufs, &outTextLen); 849 if (cmdData->flags & ERROR_DIALOGS) 850 errText = coalesceOutput(&cmdData->errBufs, &errTextLen); 851 852 /* Wait for the child process to complete and get its return status */ 853 waitpid(cmdData->childPid, &status, 0); 854 855 /* Present error and stderr-information dialogs. If a command returned 856 error output, or if the process' exit status indicated failure, 857 present the information to the user. */ 858 if (cmdData->flags & ERROR_DIALOGS) 859 { 860 failure = WIFEXITED(status) && WEXITSTATUS(status) != 0; 861 errorReport = *errText != '\0'; 862 863 if (failure && errorReport) 864 { 865 removeTrailingNewlines(errText); 866 truncateString(errText, DF_MAX_MSG_LENGTH); 867 resp = DialogF(DF_WARN, window->shell, 2, "Warning", "%s", "Cancel", 868 "Proceed", errText); 869 cancel = resp == 1; 870 } else if (failure) 871 { 872 truncateString(outText, DF_MAX_MSG_LENGTH-70); 873 resp = DialogF(DF_WARN, window->shell, 2, "Command Failure", 874 "Command reported failed exit status.\n" 875 "Output from command:\n%s", "Cancel", "Proceed", outText); 876 cancel = resp == 1; 877 } else if (errorReport) 878 { 879 removeTrailingNewlines(errText); 880 truncateString(errText, DF_MAX_MSG_LENGTH); 881 resp = DialogF(DF_INF, window->shell, 2, "Information", "%s", 882 "Proceed", "Cancel", errText); 883 cancel = resp == 2; 884 } 885 886 NEditFree(errText); 887 if (cancel) 888 { 889 NEditFree(outText); 890 goto cmdDone; 891 } 892 } 893 894 /* If output is to a dialog, present the dialog. Otherwise insert the 895 (remaining) output in the text widget as requested, and move the 896 insert point to the end */ 897 if (cmdData->flags & OUTPUT_TO_DIALOG) { 898 removeTrailingNewlines(outText); 899 if (*outText != '\0') 900 createOutputDialog(window->shell, outText); 901 } else if (cmdData->flags & OUTPUT_TO_STRING) { 902 ReturnShellCommandOutput(window,outText, WEXITSTATUS(status)); 903 } else { 904 buf = TextGetBuffer(cmdData->textW); 905 if (!BufSubstituteNullChars(outText, outTextLen, buf)) { 906 fprintf(stderr,"xnedit: Too much binary data in shell cmd output\n"); 907 outText[0] = '\0'; 908 } 909 if (cmdData->flags & REPLACE_SELECTION) { 910 reselectStart = buf->primary.rectangular ? -1 : buf->primary.start; 911 BufReplaceSelected(buf, outText); 912 TextSetCursorPos(cmdData->textW, buf->cursorPosHint); 913 if (reselectStart != -1) 914 BufSelect(buf, reselectStart, reselectStart + strlen(outText)); 915 } else { 916 safeBufReplace(buf, &cmdData->leftPos, &cmdData->rightPos, outText); 917 TextSetCursorPos(cmdData->textW, cmdData->leftPos+strlen(outText)); 918 } 919 } 920 921 /* If the command requires the file to be reloaded afterward, reload it */ 922 if (cmdData->flags & RELOAD_FILE_AFTER) 923 RevertToSaved(window, NULL); 924 925 /* Command is complete, free data structure and continue macro execution */ 926 NEditFree(outText); 927 cmdDone: 928 NEditFree(cmdData); 929 window->shellCmdData = NULL; 930 if (fromMacro) 931 ResumeMacroExecution(window); 932 } 933 934 /* 935 ** Fork a subprocess to execute a command, return file descriptors for pipes 936 ** connected to the subprocess' stdin, stdout, and stderr streams. cmdDir 937 ** sets the default directory for the subprocess. If stderrFD is passed as 938 ** NULL, the pipe represented by stdoutFD is connected to both stdin and 939 ** stderr. The function value returns the pid of the new subprocess, or -1 940 ** if an error occured. 941 */ 942 static pid_t forkCommand(Widget parent, const char *command, const char *cmdDir, 943 int *stdinFD, int *stdoutFD, int *stderrFD) 944 { 945 int childStdoutFD, childStdinFD, childStderrFD, pipeFDs[2]; 946 int dupFD; 947 pid_t childPid; 948 949 /* Ignore SIGPIPE signals generated when user attempts to provide 950 input for commands which don't take input */ 951 signal(SIGPIPE, SIG_IGN); 952 953 /* Create pipes to communicate with the sub process. One end of each is 954 returned to the caller, the other half is spliced to stdin, stdout 955 and stderr in the child process */ 956 if (pipe(pipeFDs) != 0) { 957 perror("xnedit: Internal error (opening stdout pipe)"); 958 return -1; 959 } 960 *stdoutFD = pipeFDs[0]; 961 childStdoutFD = pipeFDs[1]; 962 if (pipe(pipeFDs) != 0) { 963 perror("xnedit: Internal error (opening stdin pipe)"); 964 return -1; 965 } 966 *stdinFD = pipeFDs[1]; 967 childStdinFD = pipeFDs[0]; 968 if (stderrFD == NULL) 969 childStderrFD = childStdoutFD; 970 else { 971 if (pipe(pipeFDs) != 0) { 972 perror("xnedit: Internal error (opening stdin pipe)"); 973 return -1; 974 } 975 *stderrFD = pipeFDs[0]; 976 childStderrFD = pipeFDs[1]; 977 } 978 979 /* Fork the process */ 980 #ifdef VMS 981 childPid = vfork(); 982 #else 983 childPid = fork(); 984 #endif 985 986 /* 987 ** Child process context (fork returned 0), clean up the 988 ** child ends of the pipes and execute the command 989 */ 990 if (0 == childPid) { 991 /* close the parent end of the pipes in the child process */ 992 close(*stdinFD); 993 close(*stdoutFD); 994 if (stderrFD != NULL) 995 close(*stderrFD); 996 997 /* close current stdin, stdout, and stderr file descriptors before 998 substituting pipes */ 999 close(fileno(stdin)); 1000 close(fileno(stdout)); 1001 close(fileno(stderr)); 1002 1003 /* duplicate the child ends of the pipes to have the same numbers 1004 as stdout & stderr, so it can substitute for stdout & stderr */ 1005 dupFD = dup2(childStdinFD, fileno(stdin)); 1006 if (dupFD == -1) { 1007 perror("dup of stdin failed"); 1008 } 1009 dupFD = dup2(childStdoutFD, fileno(stdout)); 1010 if (dupFD == -1) { 1011 perror("dup of stdout failed"); 1012 } 1013 dupFD = dup2(childStderrFD, fileno(stderr)); 1014 if (dupFD == -1) { 1015 perror("dup of stderr failed"); 1016 } 1017 1018 /* now close the original child end of the pipes 1019 (we now have the 0, 1 and 2 descriptors in their place) */ 1020 close(childStdinFD); 1021 close(childStdoutFD); 1022 close(childStderrFD); 1023 1024 /* make this process the leader of a new process group, so the sub 1025 processes can be killed, if necessary, with a killpg call */ 1026 setsid(); 1027 1028 /* change the current working directory to the directory of the 1029 current file. */ 1030 if (cmdDir[0] != 0) { 1031 if (chdir(cmdDir) == -1) { 1032 perror("chdir to directory of current file failed"); 1033 } 1034 } 1035 1036 /* execute the command using the shell specified by preferences */ 1037 execlp(GetPrefShell(), GetPrefShell(), "-c", command, NULL); 1038 1039 /* if we reach here, execlp failed */ 1040 fprintf(stderr, "Error starting shell: %s\n", GetPrefShell()); 1041 exit(EXIT_FAILURE); 1042 } 1043 1044 /* Parent process context, check if fork succeeded */ 1045 if (childPid == -1) 1046 { 1047 DialogF(DF_ERR, parent, 1, "Shell Command", 1048 "Error starting shell command process\n(fork failed)", 1049 "OK"); 1050 } 1051 1052 /* close the child ends of the pipes */ 1053 close(childStdinFD); 1054 close(childStdoutFD); 1055 if (stderrFD != NULL) 1056 close(childStderrFD); 1057 1058 return childPid; 1059 } 1060 1061 /* 1062 ** Add a buffer full of output to a buffer list 1063 */ 1064 static void addOutput(buffer **bufList, buffer *buf) 1065 { 1066 buf->next = *bufList; 1067 *bufList = buf; 1068 } 1069 1070 /* 1071 ** coalesce the contents of a list of buffers into a contiguous memory block, 1072 ** freeing the memory occupied by the buffer list. Returns the memory block 1073 ** as the function result, and its length as parameter "length". 1074 */ 1075 static char *coalesceOutput(buffer **bufList, int *outLength) 1076 { 1077 buffer *buf, *rBufList = NULL; 1078 char *outBuf, *outPtr, *p; 1079 int i, length = 0; 1080 1081 /* find the total length of data read */ 1082 for (buf=*bufList; buf!=NULL; buf=buf->next) 1083 length += buf->length; 1084 1085 /* allocate contiguous memory for returning data */ 1086 outBuf = (char*)NEditMalloc(length+1); 1087 1088 /* reverse the buffer list */ 1089 while (*bufList != NULL) { 1090 buf = *bufList; 1091 *bufList = buf->next; 1092 buf->next = rBufList; 1093 rBufList = buf; 1094 } 1095 1096 /* copy the buffers into the output buffer */ 1097 outPtr = outBuf; 1098 for (buf=rBufList; buf!=NULL; buf=buf->next) { 1099 p = buf->contents; 1100 for (i=0; i<buf->length; i++) 1101 *outPtr++ = *p++; 1102 } 1103 1104 /* terminate with a null */ 1105 *outPtr = '\0'; 1106 1107 /* free the buffer list */ 1108 freeBufList(&rBufList); 1109 1110 *outLength = outPtr - outBuf; 1111 return outBuf; 1112 } 1113 1114 static void freeBufList(buffer **bufList) 1115 { 1116 buffer *buf; 1117 1118 while (*bufList != NULL) { 1119 buf = *bufList; 1120 *bufList = buf->next; 1121 NEditFree(buf); 1122 } 1123 } 1124 1125 /* 1126 ** Remove trailing newlines from a string by substituting nulls 1127 */ 1128 static void removeTrailingNewlines(char *string) 1129 { 1130 char *endPtr = &string[strlen(string)-1]; 1131 1132 while (endPtr >= string && *endPtr == '\n') 1133 *endPtr-- = '\0'; 1134 } 1135 1136 /* 1137 ** Create a dialog for the output of a shell command. The dialog lives until 1138 ** the user presses the Dismiss button, and is then destroyed 1139 */ 1140 static void createOutputDialog(Widget parent, char *text) 1141 { 1142 Arg al[50]; 1143 int ac, rows, cols, hasScrollBar, wrapped; 1144 Widget form, textW, button; 1145 XmString st1; 1146 1147 /* measure the width and height of the text to determine size for dialog */ 1148 measureText(text, MAX_OUT_DIALOG_COLS, &rows, &cols, &wrapped); 1149 if (rows > MAX_OUT_DIALOG_ROWS) { 1150 rows = MAX_OUT_DIALOG_ROWS; 1151 hasScrollBar = True; 1152 } else 1153 hasScrollBar = False; 1154 if (cols > MAX_OUT_DIALOG_COLS) 1155 cols = MAX_OUT_DIALOG_COLS; 1156 if (cols == 0) 1157 cols = 1; 1158 /* Without completely emulating Motif's wrapping algorithm, we can't 1159 be sure that we haven't underestimated the number of lines in case 1160 a line has wrapped, so let's assume that some lines could be obscured 1161 */ 1162 if (wrapped) 1163 hasScrollBar = True; 1164 ac = 0; 1165 form = CreateFormDialog(parent, "shellOutForm", al, ac); 1166 1167 ac = 0; 1168 XtSetArg(al[ac], XmNlabelString, st1=MKSTRING("OK")); ac++; 1169 XtSetArg(al[ac], XmNmarginWidth, BUTTON_WIDTH_MARGIN); ac++; 1170 XtSetArg(al[ac], XmNhighlightThickness, 2); ac++; 1171 XtSetArg(al[ac], XmNbottomAttachment, XmATTACH_FORM); ac++; 1172 XtSetArg(al[ac], XmNtopAttachment, XmATTACH_NONE); ac++; 1173 button = XmCreatePushButtonGadget(form, "ok", al, ac); 1174 XtManageChild(button); 1175 XtVaSetValues(form, XmNdefaultButton, button, NULL); 1176 XtVaSetValues(form, XmNcancelButton, button, NULL); 1177 XmStringFree(st1); 1178 XtAddCallback(button, XmNactivateCallback, destroyOutDialogCB, 1179 XtParent(form)); 1180 1181 ac = 0; 1182 XtSetArg(al[ac], XmNrows, rows); ac++; 1183 XtSetArg(al[ac], XmNcolumns, cols); ac++; 1184 XtSetArg(al[ac], XmNresizeHeight, False); ac++; 1185 XtSetArg(al[ac], XmNtraversalOn, True); ac++; 1186 XtSetArg(al[ac], XmNwordWrap, True); ac++; 1187 XtSetArg(al[ac], XmNscrollHorizontal, False); ac++; 1188 XtSetArg(al[ac], XmNscrollVertical, hasScrollBar); ac++; 1189 XtSetArg(al[ac], XmNhighlightThickness, 2); ac++; 1190 XtSetArg(al[ac], XmNspacing, 0); ac++; 1191 XtSetArg(al[ac], XmNeditMode, XmMULTI_LINE_EDIT); ac++; 1192 XtSetArg(al[ac], XmNeditable, False); ac++; 1193 XtSetArg(al[ac], XmNvalue, text); ac++; 1194 XtSetArg(al[ac], XmNtopAttachment, XmATTACH_FORM); ac++; 1195 XtSetArg(al[ac], XmNleftAttachment, XmATTACH_FORM); ac++; 1196 XtSetArg(al[ac], XmNbottomAttachment, XmATTACH_WIDGET); ac++; 1197 XtSetArg(al[ac], XmNrightAttachment, XmATTACH_FORM); ac++; 1198 XtSetArg(al[ac], XmNbottomWidget, button); ac++; 1199 textW = XmCreateScrolledText(form, "outText", al, ac); 1200 AddMouseWheelSupport(textW); 1201 MakeSingleLineTextW(textW); /* Binds <Return> to activate() */ 1202 XtManageChild(textW); 1203 1204 XtVaSetValues(XtParent(form), XmNtitle, "Output from Command", NULL); 1205 ManageDialogCenteredOnPointer(form); 1206 1207 #ifdef LESSTIF_VERSION 1208 /* 1209 * The Lesstif text widget blocks activate() calls in multi-line mode, 1210 * so we put the original focus on the Ok button such that the user 1211 * can simply hit Return to dismiss the dialog. 1212 */ 1213 XmProcessTraversal(button, XmTRAVERSE_CURRENT); 1214 #else 1215 XmProcessTraversal(textW, XmTRAVERSE_CURRENT); 1216 #endif 1217 } 1218 1219 /* 1220 ** Dispose of the command output dialog when user presses Dismiss button 1221 */ 1222 static void destroyOutDialogCB(Widget w, XtPointer callback, XtPointer closure) 1223 { 1224 XtDestroyWidget((Widget)callback); 1225 } 1226 1227 /* 1228 ** Measure the width and height of a string of text. Assumes 8 character 1229 ** tabs. wrapWidth specifies a number of columns at which text wraps. 1230 */ 1231 static void measureText(char *text, int wrapWidth, int *rows, int *cols, 1232 int *wrapped) 1233 { 1234 int maxCols = 0, line = 1, col = 0, wrapCol; 1235 char *c; 1236 1237 *wrapped = 0; 1238 for (c=text; *c!='\0'; c++) { 1239 if (*c=='\n') { 1240 line++; 1241 col = 0; 1242 continue; 1243 } 1244 1245 if (*c == '\t') { 1246 col += 8 - (col % 8); 1247 wrapCol = 0; /* Tabs at end of line are not drawn when wrapped */ 1248 } else if (*c == ' ') { 1249 col++; 1250 wrapCol = 0; /* Spaces at end of line are not drawn when wrapped */ 1251 } else { 1252 col++; 1253 wrapCol = 1; 1254 } 1255 1256 /* Note: there is a small chance that the number of lines is 1257 over-estimated when a line ends with a space or a tab (ie, followed 1258 by a newline) and that whitespace crosses the boundary, because 1259 whitespace at the end of a line does not cause wrapping. Taking 1260 this into account is very hard, but an over-estimation is harmless. 1261 The worst that can happen is that some extra blank lines are shown 1262 at the end of the dialog (in contrast to an under-estimation, which 1263 could make the last lines invisible). 1264 On the other hand, without emulating Motif's wrapping algorithm 1265 completely, we can't be sure that we don't underestimate the number 1266 of lines (Motif uses word wrap, and this counting algorithm uses 1267 character wrap). Therefore, we remember whether there is a line 1268 that has wrapped. In that case we allways install a scroll bar. 1269 */ 1270 if (col > wrapWidth) { 1271 line++; 1272 *wrapped = 1; 1273 col = wrapCol; 1274 } else if (col > maxCols) { 1275 maxCols = col; 1276 } 1277 } 1278 *rows = line; 1279 *cols = maxCols; 1280 } 1281 1282 /* 1283 ** Truncate a string to a maximum of length characters. If it shortens the 1284 ** string, it appends "..." to show that it has been shortened. It assumes 1285 ** that the string that it is passed is writeable. 1286 */ 1287 static void truncateString(char *string, int length) 1288 { 1289 if ((int)strlen(string) > length) 1290 memcpy(&string[length-3], "...", 4); 1291 } 1292 1293 /* 1294 ** Substitute the string fileStr in inStr wherever % appears and 1295 ** lineStr in inStr wherever # appears, storing the 1296 ** result in outStr. If predictOnly is non-zero, the result string length 1297 ** is predicted without creating the string. Returns the length of the result 1298 ** string or -1 in case of an error. 1299 ** 1300 */ 1301 static int shellSubstituter(char *outStr, const char *inStr, const char *fileStr, 1302 const char *lineStr, int outLen, int predictOnly) 1303 { 1304 const char *inChar; 1305 char *outChar = NULL; 1306 int outWritten = 0; 1307 int fileLen, lineLen; 1308 1309 inChar = inStr; 1310 if (!predictOnly) { 1311 outChar = outStr; 1312 } 1313 fileLen = strlen(fileStr); 1314 lineLen = strlen(lineStr); 1315 1316 while (*inChar != '\0') { 1317 1318 if (!predictOnly && outWritten >= outLen) { 1319 return(-1); 1320 } 1321 1322 if (*inChar == '%') { 1323 if (*(inChar + 1) == '%') { 1324 inChar += 2; 1325 if (!predictOnly) { 1326 *outChar++ = '%'; 1327 } 1328 outWritten++; 1329 } else { 1330 if (!predictOnly) { 1331 if (outWritten + fileLen >= outLen) { 1332 return(-1); 1333 } 1334 strncpy(outChar, fileStr, fileLen); 1335 outChar += fileLen; 1336 } 1337 outWritten += fileLen; 1338 inChar++; 1339 } 1340 } else if (*inChar == '#') { 1341 if (*(inChar + 1) == '#') { 1342 inChar += 2; 1343 if (!predictOnly) { 1344 *outChar++ = '#'; 1345 } 1346 outWritten++; 1347 } else { 1348 if (!predictOnly) { 1349 if (outWritten + lineLen >= outLen) { 1350 return(-1); 1351 } 1352 strncpy(outChar, lineStr, lineLen); 1353 outChar += lineLen; 1354 } 1355 outWritten += lineLen; 1356 inChar++; 1357 } 1358 } else { 1359 if (!predictOnly) { 1360 *outChar++ = *inChar; 1361 } 1362 inChar++; 1363 outWritten++; 1364 } 1365 } 1366 1367 if (!predictOnly) { 1368 if (outWritten >= outLen) { 1369 return(-1); 1370 } 1371 *outChar = '\0'; 1372 } 1373 ++outWritten; 1374 return(outWritten); 1375 } 1376 1377 static char *shellCommandSubstitutes(const char *inStr, const char *fileStr, 1378 const char *lineStr) 1379 { 1380 int cmdLen; 1381 char *subsCmdStr = NULL; 1382 1383 cmdLen = shellSubstituter(NULL, inStr, fileStr, lineStr, 0, 1); 1384 if (cmdLen >= 0) { 1385 subsCmdStr = (char*)NEditMalloc(cmdLen); 1386 if (subsCmdStr) { 1387 cmdLen = shellSubstituter(subsCmdStr, inStr, fileStr, lineStr, cmdLen, 0); 1388 if (cmdLen < 0) { 1389 free(subsCmdStr); 1390 subsCmdStr = NULL; 1391 } 1392 } 1393 } 1394 return(subsCmdStr); 1395 } 1396