This is MiscShell.m in view mode; [Download] [Up]
// Copyright (C) 1995 Steve Hayman
// Use is governed by the MiscKit license
#import <misckit/MiscShell.h>
/*
* $Header: /SAHayman/LocalDeveloper/Source/MiscKit/Palettes/MiscShell/MiscShell.subproj/RCS/MiscShell.m,v 1.2 94/08/04 17:39:39 shayman Exp Locker: shayman $
* $Log: MiscShell.m,v $
* Revision 1.2 94/08/04 17:39:39 shayman
* Fiddling with string values
*
*/
/*
* Note:
* In several spots we do things like
* if( [aVar isKindOfClassNamed:"DBTableView"] )
* instead of
* if ( [avar isKindOf:[DBTableView class]] )
*
* This is because you can do the first test without having to link
* DBKit into your program. Doing the second requires linking DBKit,
* including the DBKit header files in this program, etc etc, which
* we'd rather avoid if we can. Same for NXTableView. Since I want this
* object to be usable with either, this seems like the best way to
* avoid various linking problems.
*
* I have done the same thing with other AppKit classes, e.g.
* if ( [aVar isKindOfClassNamed:"Matrix"] )
* for consistency.
*/
// To avoid warnings, the following "category" defines some methods:
// Added 1/17/95, DAY
@interface Object(MiscShell_Warning_Suppressor)
- setDataSource:aSource;
- (unsigned int)columnCount;
- columnAt:(unsigned int)aPosition;
- identifier;
- setIdentifier:anIdentifier;
- reloadData:sender;
@end
#define MISC_SHELL_VERSION 3
@interface MiscShell(PrivateMethods)
- setScriptFromCString:(const char *)str;
- sendText:(const char *)buffer andNewline:(BOOL)newline to:text;
- setupEnvironment:process forSender:sender;
- addEnvVar:(const char *)varName value:(const char *)val to:env;
- handleCompleteLine;
- startOutput;
- finishOutput;
- resort:(int)keyField;
- field:(int)n ofString:s;
- splitupCurrentLine;
- (BOOL)useCustomDelimiters;
- (int) fieldsInString:(MiscString *)str;
@end
@implementation MiscShell(PrivateMethods)
- (BOOL)useCustomDelimiters
{
return [[self customDelimiters] length] > 0;
}
/*
* Add a bunch of environment variables to a process.
*/
- setupEnvironment:proc forSender:sender
{
NXBundle *mainBundle = [NXBundle mainBundle];
id env = [proc environment]; // a MiscStringArray
int i;
char varName[3];
id v;
/*
* Go through each of our v1...v9 instance variables in turn by
* making use of this clever object_getInstanceVariable runtime function.
*/
for ( i = 1; i <= 9; i++ ) {
sprintf(varName, "v%d", i);
// See if we have an instance variable by that name ..
if ( object_getInstanceVariable(self, varName, (void *)&v) ) {
// Now see if that obj responds to stringValue:, and if so,
// add an environment var representing its current stringValue.
// Special case: if the object is a browser, we write
// out the value of all its selected cells, separated
// by tabs. Rather than just the stringValue.
if ( [v isKindOfClassNamed:"NXBrowser"] ) {
List *selections = [[List alloc] init];
MiscString *str = [[MiscString alloc] init];
int i;
id aCell;
[v getSelectedCells:selections];
i = 0;
while ( aCell = [selections objectAt:i++] ) {
[str catFromFormat:"%s", [aCell stringValue]];
if ( i < [selections count] )
[str catFromFormat:"\t"];
}
[self addEnvVar:varName value:[str stringValue] to:env];
[selections free];
[str free];
} else if ( [v respondsTo:@selector(stringValue)] )
[self addEnvVar:varName value:[v stringValue] to:env];
}
}
// Add a var called "sender" containing the sender's string value
// (if known.)
// TODO - should we be using [sender selectedCell] here if it's
// a Matrix sending us this message?
if ( [sender respondsTo:@selector(stringValue)] )
[self addEnvVar:"sender" value:(const char *)[sender stringValue]
to:env];
// What the hell, add a var called "senderTag" too.
{
char tag[10];
if ( [sender respondsTo:@selector(tag)] ) {
sprintf(tag, "%d", [sender tag]);
[self addEnvVar:"senderTag" value:tag to:env];
}
}
// TODO
// Maybe if it's a Form sending us the message, we add a bunch
// of variables for each entry in the Form.
// Add a var called "mainBundle" giving our main application
// directory.
[self addEnvVar:"mainBundle" value:[mainBundle directory]
to:env];
return self;
}
// Add an environment variable to a MiscStringArray, which belongs to
// the subprocess
- addEnvVar:(const char *)varName value:(const char *)value to:env
{
MiscString *newStr = [[MiscString alloc] init];
if ( ! value )
return nil;
[newStr catFromFormat:"%s=%s", varName, value];
[env addString:[newStr stringValue]];
[newStr free];
return self;
}
// Internal method, set our script from a (char *)
- setScriptFromCString:(const char *)str
{
if ( script )
[script free];
script = [MiscString newWithString:str];
return self;
}
// Internal method that passes some text along to either a
// scrolling text object (appended), or something responding to setStringValue.
// it's appended to the end.
// The nl variable controls whether we send a newline or not.
// If true, and if the destination object is a Text object, we append
// a newline.
- sendText:(const char *)buffer andNewline:(BOOL) nl to:anObject
{
int len;
if ( !anObject )
return nil;
if ( [anObject respondsTo:@selector(setStringValue:)] ) {
return [anObject setStringValue:buffer];
}
// Otherwise it should be a text object.
if ( ! [anObject isKindOfClassNamed:"Text"] ) {
return nil;
}
len = [anObject textLength];
[anObject setSel:len:len];
[anObject replaceSel:buffer];
if ( nl ) {
len = [anObject textLength];
[anObject setSel:len:len];
[anObject replaceSel:"\n"];
}
[anObject scrollSelToVisible];
return self;
}
/*
* just read a line that looks like
* ALERT;Are you sure?;Yes;No
* Do an NXRunAlertPanel and write back the result as an integer.
*/
- doAlert
{
MiscString *message, *b1, *b2, *b3;
int r;
char rbuf[10];
message = [currentLine extractPart:1 useAsDelimiter: ';'];
b1 = [currentLine extractPart:2 useAsDelimiter: ';'];
b2 = [currentLine extractPart:3 useAsDelimiter: ';'];
b3 = [currentLine extractPart:4 useAsDelimiter: ';'];
r = NXRunAlertPanel([NXApp appName],
[message stringValue],
[b1 stringValue], [b2 stringValue], [b3 stringValue] );
sprintf(rbuf, "%d", r);
[process send:rbuf withNewline:YES];
[message free];
[b1 free];
[b2 free];
[b3 free];
return self;
}
- doOpen
{
id op = [OpenPanel new];
if ( [op runModalForDirectory:NULL file:NULL] ) {
[process send: [op filename] withNewline:YES];
} else {
[process send:"\n" withNewline:NO];
}
return self;
}
/*
* Do whatever is appropriate upon reading a complete line from the shell.
*/
- handleCompleteLine
{
/*
* Is it one of our special strings?
*/
/*
* Run special magic commands that write back answers when
* we see certain strings in the output.
*/
MiscString *var, *value;
id varObj;
// ALERT;Are you sure?;Yes;No
if ( [currentLine cmp:"ALERT" n:5] == 0 ) {
return [self doAlert];
} else if ( [currentLine cmp:"OPEN" n:4] == 0 ) {
return [self doOpen];
}
if ( [currentLine matchesRegex:"^v[0123456789]=" ] ) {
var = [currentLine extractPart:0 useAsDelimiter:'='];
value = [currentLine extractPart:1 useAsDelimiter:'='];
// ooh tricky runtime stuff - get a pointer to the
// instance var whose name is in "var"
if ( object_getInstanceVariable(self, [var stringValue], (void *)&varObj) ) {
if ( [varObj respondsTo:@selector(setStringValue:)] ) {
[varObj setStringValue:[value stringValue]];
}
}
[var free];
[value free];
return self;
}
/*
* Add the line to our lines array.
*/
[lines addString: [currentLine stringValue]];
/*
* Create a MiscStringArray out of it by splitting it into
* fields, and add it to the linesBrokenIntoFields List.
*/
[self splitupCurrentLine];
/*
* Send the target/action message
*/
if ( _target && action && [_target respondsTo:action] ) {
[_target perform:action with:self];
}
/*
* Send the line - and a newline - to standard output.
*/
[self sendText:[currentLine stringValue] andNewline:YES to:standardOutput];
/*
* If standard out is a browser, add a row to it.
*/
if ( [standardOutput isKindOfClassNamed:"NXBrowser"] ) {
id m = [standardOutput matrixInColumn:0];
id newCell;
[m renewRows: [lines count] cols:1];
newCell = [[m cellList] lastObject];
[newCell setStringValue:[currentLine stringValue]];
[newCell setLeaf:YES];
[newCell setLoaded:YES];
[m sizeToCells];
[standardOutput sizeToFit];
} else if ( [standardOutput isKindOfClassNamed:"DBTableView"]
|| [standardOutput isKindOfClassNamed:"NXTableView"]) {
; // Don't need to do anything here, all handled in finishOutput
// for now.
} else if ( [standardOutput isKindOfClassNamed:"Matrix"] ) {
[standardOutput selectCellWithTag:[self intValue]];
} else if ( [standardOutput isKindOfClassNamed:"MiscShell"] ) {
/*
* Ask the other process to read our string value and
* place it on its stdin. This is a way that you can
* pipeline two shell objects.
*/
[standardOutput takeStdinFrom:self];
}
return self;
}
- (int) fieldsInString:(MiscString *)str
{
if ( [self useCustomDelimiters] ) {
// n delimiters -> n+1 fields
return [customDelimiters numWords] + 1;
} else if ( [self delimiter] )
/*
* count delimiters.
* one delimiter = two fields.
*/
return [str numOfChar:[self delimiter]] + 1;
else
/*
* delimiter is 0 - count words delimited by whitespace
*/
return [str numWords];
}
- splitupCurrentLine
{
int i;
MiscStringArray *a = [[MiscStringArray alloc] init];
MiscString *curLine = [[lines strings] lastObject];
// Extract each field in turn, add it to the array for this line
for ( i = 0; i < [self fieldsInString:curLine]; i++ ) {
[a addString: [[self field:i ofString:curLine] stringValue]];
}
[linesBrokenIntoFields addObject:a];
return self;
}
/*
* This is here so that we can do clever things if our standardOutput
* is a table view object. This method is automatically called when
* the outlet is initialized.
* We need to tell the table view that we are its data source, and
* we need to set identifiers for each of its columns. Although those
* identifiers are normally objects, it's apparently ok to use
* other 32-bit quantities such as integers, so we'll
* just set the identifier of column "i" to be "i".
*
* TODO - is it legal to be doing this here? is the table view set up
* properly when setStandardOutput is called?
*/
- setStandardOutput:newOutput
{
int i;
standardOutput = newOutput;
if ([standardOutput isKindOfClassNamed:"DBTableView"]
|| [standardOutput isKindOfClassNamed:"NXTableView"] ) {
[standardOutput setDataSource:self];
/*
* Become its delegate so we get column moved messages
*/
[standardOutput setDelegate:self];
/*
* Put numeric identifiers on each of its columns.
*/
for ( i = 0; i < [standardOutput columnCount]; i++ )
[[standardOutput columnAt:i] setIdentifier:(void *)i];
}
return self;
}
/*
* startOutput does any special initialization of certain kinds of
* output objects.
*/
- startOutput
{
return self;
}
/*
* finishOutput is called after the subprocess has executed, and can
* be used to do any sort of output display cleanup.
*/
- finishOutput
{
if ([standardOutput isKindOfClassNamed:"DBTableView"]
|| [standardOutput isKindOfClassNamed:"NXTableView"]) {
[standardOutput reloadData:self];
} else if ( [standardOutput isKindOfClassNamed:"NXBrowser"] ) {
[standardOutput display];
}
return self;
}
/*
* Here is a shellsort function, from Kernighan & Ritchie, page 116,
* modified to pass a 3rd bonus parameter to the comparison routine.
/*
* Resort the output lines based on the value of column n.
* We do this by creating a ListSortedByFields which duplicates
* the existing string list then we tell the ListSortedByFields to
* sort itself.
*
* Multiplying by sortWhenColumnsMove does the right thing for
* ascending vs descending sorts.
*/
- (int)fieldComp:(int)n forLines:(int)a :(int)b
{
id field1 = [self line:a field:n];
id field2 = [self line:b field:n];
int rval;
// If field1 looks like a number, compare numerically.
// TODO - do the right thing if it looks like
// a time value (HH:MM)
if ( [field1 matchesRegex:":[0-9][0-9]$" ] ) {
// Replace the ":" with a ".", which will
// make the comparison work just as if it was a float.
[field1 replace: ":" with: "."];
[field2 replace: ":" with: "."];
}
if ( [field1 matchesRegex:"^[0-9]"] ) {
double n1, n2;
n1 = atof([field1 stringValue]);
n2 = atof( [field2 stringValue] );
// Check for a K or M suffix, multiply appropriately.
// This is so I can sort the output of "ps" nicely.
if ( [field1 endcmp:"M"] == 0 )
n1 *= 1024 * 1024;
if ( [field1 endcmp:"K"] == 0 )
n1 *= 1024;
if ( [field2 endcmp:"M"] == 0)
n2 *= 1024 * 1024;
if ( [field2 endcmp:"K"] == 0)
n2 *= 1024;
rval = (n1 < n2) ? -1 :
(n1 > n2) ? 1 : 0;
} else {
/*
* Do a regular string comparison.
*/
rval = [field1 compareTo: field2];
}
return rval * [self sortWhenColumnsMove];
}
- resort:(int)fieldNumber
{
List * strings = [lines strings];
id a, b;
int c, d, stride;
BOOL found;
int n = [strings count];
/*
* ShellSort, from the SortingInAction miniexample
*/
#define STRIDE_FACTOR 3
stride = 1;
while ( stride <= n )
stride = stride * STRIDE_FACTOR + 1;
while ( stride > (STRIDE_FACTOR - 1)) {
stride = stride / STRIDE_FACTOR;
for ( c = stride; c < n; c++ ) {
found = NO;
d = c - stride;
while ( (d >= 0) && !found ) {
if ( [self fieldComp:fieldNumber forLines:d:d+stride] > 0 ) {
// Swap the "lines" array ...
a = [strings objectAt:d];
b = [strings objectAt:d+stride];
[strings replaceObjectAt:d with:b];
[strings replaceObjectAt:d+stride with:a];
//and the linesBrokenIntoFields list
a = [linesBrokenIntoFields objectAt:d];
b = [linesBrokenIntoFields objectAt:d+stride];
[linesBrokenIntoFields replaceObjectAt:d with:b];
[linesBrokenIntoFields replaceObjectAt:d+stride with:a];
d -= stride;
} else
found = YES;
}
}
}
return self;
}
/*
* Utility routine to return a particular field of a particular string,
* using our delimiter. If delimiter is 0, we look for blank-separated words;
* otherwise we look for the particular delimited field.
*/
- field:(int)f ofString:s
{
id thisField = nil;
int startPos, endPos;
MiscString *delim;
if ( [self useCustomDelimiters] ) {
// customDelimiters is a string like this
// 0-4 7-9 13-22
// that defines the boundaries of each field. So, in this
// case, if we want field 2, we extract chars 13-22.
delim = [[self customDelimiters] wordNum:f];
// delim is now something like "5-9", extract the
// starting and ending positions. If it's "72-", that
// means "72 to end of line"
if ( [delim stringValue] && (sscanf( [delim stringValue], "%d-%d", &startPos, &endPos) == 2) ) {
thisField = [s midFrom:startPos to:endPos];
} else if ( [delim stringValue ] && (sscanf( [delim stringValue], "%d-", &startPos) == 1) ) {
thisField = [s midFrom:startPos to: [s length]];
}
[delim free];
} else if ( [self delimiter] )
thisField = [s extractPart:f
useAsDelimiter:[self delimiter]];
else {
/*
* take the column'th word.
* Current bug in MiscString: wordNum:n for n > numWords returns
* the last word rather than nil. So we check that.
*/
/* if ( f < [s numWords] )
thisField = [s wordNum:f];
else
thisField = nil;
*/ /* I fixed the MiscString bug, so I'm taking this out. -don */
thisField = [s wordNum:f];
}
return thisField;
}
@end
@implementation MiscShell(TableViewDelegate)
- (unsigned int) rowCount
{
return [self lineCount];
}
- (unsigned int) columnCount
{
return 1; // ??? todo - what do I put here? does it matter?
}
/*
* This is a table view asking for the value at row aPosition,
* column identifier.
*/
- getValueFor:identifier at:(unsigned int)aPosition into:aValue
{
[aValue setStringValue:
[[self line:aPosition field:(int)identifier] stringValue] ];
return self;
}
/*
* If you're the delegate of a DBTableView, you get these messages
* when the columns are resized. Not sure just how I want to deal
* with this yet.
*
* Current Plan - check the sortWhenColumnsMove variable.
* If 0, do nothing.
* If 1, resort ascending
* If -1, resort descending
*/
- tableView:sender movedColumnFrom:(unsigned int) old to:(unsigned int) new
{
/*
* Resort based on the identifier of the new first column.
*/
if ( [self sortWhenColumnsMove] ) {
[self resort: (int)[[sender columnAt:0] identifier]];
[sender reloadData:self];
}
return self;
}
@end
@implementation MiscShell
+ initialize
{
if (self == [MiscShell class]) {
/*
* **** Archiving: READ ME **** After bumping the _VERSION, it is
* considered common practice to add a comment line indicating the new
* version number, date, and modifier. Optionally, the reason for the
* change. There is no need to modify the setVersion message. BJM
* 5/24/94
*/
// version 0: initial. (sah)
// version 1: adds customDelimiters var. (sah, sep 13 1994)
// version 2: adds linesBrokenIntoFields array (sah, sep 14 1994)
// version 3: fixes a bug with archiving BOOL vars (sah, jan 4 1995)
[[MiscShell class] setVersion:MISC_SHELL_VERSION];
}
return self;
}
- init
{
self = [super init];
script = [[MiscString alloc] init];
fullOutput = [[MiscString alloc] init];
currentLine = [[MiscString alloc] init];
lines = [[MiscStringArray alloc] init];
[self setDelimiter:0]; // means "parse words"
[self setRunToCompletion:NO]; // run asynchronously
[self setSortWhenColumnsMove:NO];
linesBrokenIntoFields = [[List alloc] init];
return self;
}
/*
* Initialize, and run a non-interactive command and wait for it to terminate.
*/
- initWithCommand:(const char *)cmd
{
[self init];
[self setRunToCompletion:YES];
[self setScriptFromCString:cmd];
[self executeScript:self];
return self;
}
- free
{
if ( script )
script = [script free];
if ( process ) {
[process terminate:self];
process = [process free];
}
if ( fullOutput )
fullOutput = [fullOutput free];
[currentLine free];
return [super free];
}
// Archiving methods
- read:(NXTypedStream *)stream
{
int version;
Class myClass = [self class];
int int1, int2; // compensate for old read/write bugs
[super read:stream];
version = NXTypedStreamClassVersion(stream, "MiscShell");
switch (version) {
case MISC_SHELL_VERSION: {
/*
* Version 3 correctly reads/writes BOOL vars as "c", not "i"
*/
standardOutput = NXReadObject(stream);
standardInput = NXReadObject(stream);
standardError = NXReadObject(stream);
v1 = NXReadObject(stream);
v2 = NXReadObject(stream);
v3 = NXReadObject(stream);
v4 = NXReadObject(stream);
script = NXReadObject( stream );
_target = NXReadObject( stream );
NXReadTypes(stream, ":", &action);
NXReadTypes(stream, "@", &process);
NXReadTypes(stream, "@", &fullOutput);
NXReadTypes(stream, "c", &executionInProgress);
NXReadTypes(stream, "@", ¤tLine);
NXReadTypes(stream, "cc", ¤tLineIsComplete, &runToCompletion);
NXReadTypes(stream, "@", &lines);
NXReadTypes(stream, "c", &delimiter);
NXReadTypes(stream, "c", &sortWhenColumnsMove);
NXReadTypes(stream, "@", &customDelimiters);
NXReadTypes(stream, "@", &linesBrokenIntoFields); // new for v2
break;
}
case 2: {
/*
* Version 2 adds the linesBrokenIntoFields var
*/
standardOutput = NXReadObject(stream);
standardInput = NXReadObject(stream);
standardError = NXReadObject(stream);
v1 = NXReadObject(stream);
v2 = NXReadObject(stream);
v3 = NXReadObject(stream);
v4 = NXReadObject(stream);
script = NXReadObject( stream );
_target = NXReadObject( stream );
NXReadTypes(stream, ":", &action);
NXReadTypes(stream, "@", &process);
NXReadTypes(stream, "@", &fullOutput);
// Written as int in versions <= 2, but really a char
NXReadTypes(stream, "i", &int1); executionInProgress = int1;
NXReadTypes(stream, "@", ¤tLine);
// Written as int in versions <= 2, but really a char
NXReadTypes(stream, "ii", &int1, &int2);
currentLineIsComplete = int1;
runToCompletion = int2;
NXReadTypes(stream, "@", &lines);
NXReadTypes(stream, "c", &delimiter);
// Written as int in versions <= 2, but really a char
NXReadTypes(stream, "i", &int1); sortWhenColumnsMove = int1;
NXReadTypes(stream, "@", &customDelimiters);
NXReadTypes(stream, "@", &linesBrokenIntoFields); // new for v2
break;
}
case 1: {
/*
* Version 1 adds a customDelimiters instance var
*/
standardOutput = NXReadObject(stream);
standardInput = NXReadObject(stream);
standardError = NXReadObject(stream);
v1 = NXReadObject(stream);
v2 = NXReadObject(stream);
v3 = NXReadObject(stream);
v4 = NXReadObject(stream);
script = NXReadObject( stream );
_target = NXReadObject( stream );
NXReadTypes(stream, ":", &action);
NXReadTypes(stream, "@", &process);
NXReadTypes(stream, "@", &fullOutput);
// Written as int in versions <= 2, but really a char
NXReadTypes(stream, "i", &int1); executionInProgress = int1;
NXReadTypes(stream, "@", ¤tLine);
// Written as int in versions <= 2, but really a char
NXReadTypes(stream, "ii", &int1, &int2);
currentLineIsComplete = int1;
runToCompletion = int2;
NXReadTypes(stream, "@", &lines);
NXReadTypes(stream, "c", &delimiter);
// Written as int in versions <= 2, but really a char
NXReadTypes(stream, "i", &int1); sortWhenColumnsMove = int1;
NXReadTypes(stream, "@", &customDelimiters);
linesBrokenIntoFields = [[List alloc] init]; // new - need one
break;
}
case 0: {
standardOutput = NXReadObject(stream);
standardInput = NXReadObject(stream);
standardError = NXReadObject(stream);
v1 = NXReadObject(stream);
v2 = NXReadObject(stream);
v3 = NXReadObject(stream);
v4 = NXReadObject(stream);
script = NXReadObject( stream );
_target = NXReadObject( stream );
NXReadTypes(stream, ":", &action);
NXReadTypes(stream, "@", &process);
NXReadTypes(stream, "@", &fullOutput);
// Written as int in versions <= 2, but really a char
NXReadTypes(stream, "i", &int1); executionInProgress = int1;
NXReadTypes(stream, "@", ¤tLine);
// Written as int in versions <= 2, but really a char
NXReadTypes(stream, "ii", &int1, &int2);
currentLineIsComplete = int1;
runToCompletion = int2;
NXReadTypes(stream, "@", &lines);
NXReadTypes(stream, "c", &delimiter);
// Written as int in versions <= 2, but really a char
NXReadTypes(stream, "i", &int1); sortWhenColumnsMove = int1;
linesBrokenIntoFields = [[List alloc] init]; // new - need one
break;
}
default: {
NXLogError("[%s %s] - unknown version of %s in typed stream",
[myClass name], sel_getName(_cmd),
[myClass name]);
break;
}
}
return self;
}
- write:(NXTypedStream *)stream
{
[super write:stream];
NXWriteObjectReference( stream, standardOutput );
NXWriteObjectReference( stream, standardInput );
NXWriteObjectReference( stream, standardError );
NXWriteObjectReference( stream, v1 );
NXWriteObjectReference( stream, v2 );
NXWriteObjectReference( stream, v3 );
NXWriteObjectReference( stream, v4 );
NXWriteObject( stream, script );
NXWriteObjectReference( stream, _target );
NXWriteTypes(stream, ":", &action);
NXWriteTypes(stream, "@", &process);
NXWriteTypes(stream, "@", &fullOutput);
NXWriteTypes(stream, "c", &executionInProgress); // BOOL
NXWriteTypes(stream, "@", ¤tLine);
NXWriteTypes(stream, "cc", ¤tLineIsComplete, &runToCompletion); // BOOL
NXWriteTypes(stream, "@", &lines);
NXWriteTypes(stream, "c", &delimiter);
NXWriteTypes(stream, "c", &sortWhenColumnsMove);
NXWriteTypes(stream, "@", &customDelimiters); // added in v1
NXWriteTypes(stream, "@", &linesBrokenIntoFields); // added in v2
return self;
}
- target { return _target; }
- setTarget:aTarget
{
_target = aTarget;
return self;
}
- (SEL)action { return action; }
- setAction:(SEL)anAction
{
action = anAction;
return self;
}
/*
* Our "string value" is the "current" line we've received
* (which doesn't contain a newline.)
*/
- (const char *)stringValue
{
return [currentLine stringValue];
}
/*
* Int, double, float values are just derived from our string value.
* note: no error checking as to whether the string value really is
* a number.
*/
- (double) doubleValue
{
return ( atof([self stringValue]) );
}
- (float) floatValue
{
return ( (float) atof([self stringValue]) );
}
- (int) intValue
{
return ( atoi([self stringValue]) );
}
/*
* Set and retrieve the actual script.
* We store it internally as a MiscString
*/
- (MiscString *)script
{
return script;
}
// Inspector sends this when the script changes
- setScript:(MiscString *)newScript
{
return [self setScriptFromCString: [newScript stringValue]];
}
- (MiscString *)customDelimiters
{
return customDelimiters;
}
// Inspector sends this when the script changes
- setCustomDelimiters:(MiscString *)d
{
[customDelimiters free];
customDelimiters = [d copy];
return self;
}
// Messages from Controls
- executeScript:sender
{
if ( executionInProgress ) {
NXBeep();
return nil;
}
if ( process ) {
[process terminate:self];
[process free];
}
/*
* Get rid of old accumulated output.
*/
[fullOutput setStringValue:""];
[currentLine setStringValue:""];
currentLineIsComplete = NO;
[[lines strings] freeObjects];
[linesBrokenIntoFields freeObjects];
/*
* Here we have special pre-output checks for certain kinds of
* output objects.
*/
[self startOutput];
// Create a subprocess to execute the script. Don't run it just yet.
process = [[MiscSubprocess alloc] init:NULL withDelegate:self];
// Set up subprocess environment here - a bunch of
// environment variables that tell the process about
// the values of v1, v2, etc.
[self setupEnvironment:process forSender:sender];
// And finally start the process going.
[process execute:[script stringValue]
withPtys:NO
asynchronously: ![self runToCompletion]];
return self;
}
/*
* These messages might arrive from either a control or a matrix of
* controls.
*/
- executeFromStringValue:sender
{
if ( [sender isKindOfClassNamed:"Matrix"] )
sender = [sender selectedCell];
[self setScriptFromCString:[sender stringValue]];
return [self executeScript:sender];
}
- executeFromTitle:sender
{
if ( [sender isKindOfClassNamed:"Matrix"] )
sender = [sender selectedCell];
[self setScriptFromCString:[sender title]];
return [self executeScript:sender];
}
- executeFromAltTitle:sender
{
if ( [sender isKindOfClassNamed:"Matrix"] )
sender = [sender selectedCell];
[self setScriptFromCString:[sender altTitle]];
return [self executeScript:sender];
}
- pause:sender
{
return [process pause:sender];
}
- resume:sender
{
return [process resume:sender];
}
- terminate:sender
{
return [process terminate:sender];
}
/*
* Methods that return particular lines, or fields within lines.
*/
- (int)lineCount {
return [lines count];
}
- (int) fieldsInLine:(int)n
{
return [[linesBrokenIntoFields objectAt:n] count];
}
- (MiscStringArray *)lines { return lines; }
/*
* Return a copy of the MiscString that holds line number n.
* We return a copy for consistency with the line:field: method, so that*
* the caller is responsible for freeing both.
* Just returning [[lines strings] objectAt:n] would hand back a string
* owned by the MiscStringArray object, which the caller shouldn't free.
*
* Boy it will be nice when libFoundation_s.a is available everywhere
* and we can do this more rationally.
*/
- (MiscString *)line:(int)n
{
id str = [[lines strings] objectAt:n];
if ( str )
return [str copy];
else
return nil;
}
/*
* Return the MiscString representing field f of line n;
* the caller should NOT free it.
*/
- (MiscString *)line:(int)n field:(int)f
{
return [[[linesBrokenIntoFields objectAt:n] strings] objectAt:f];
}
// MiscSubprocess delegate methods
- subprocess:sender output:(const char *)buffer
{
/*
* fullOutput records the entire output of the script, so add
* the buffer to the end.
*/
[fullOutput cat:buffer];
/*
* Now. We have to decide how this new chunk of data, with
* possibly embedded newlines, affects the current line. We want
* to fire off a target/action message every time we receive a newline.
*/
while ( *buffer ) {
/*
* If we have previously accumulated a complete newline, it's
* no longer complete.
*/
if( currentLineIsComplete ) {
[currentLine setStringValue:""];
currentLineIsComplete = NO;
}
/*
* If we are looking at a newline, then the current output
* line is complete, so fire the target/action message.
*/
if ( *buffer == '\n' ) {
[self handleCompleteLine];
currentLineIsComplete = YES;
} else {
/*
* Add this non-newline character
*/
[currentLine addChar:*buffer];
}
buffer++;
}
return self;
}
// todo - if standardError and standardOutput are the same, why not
// just forward this message to subprocess:stdoutOutput, which would merge
// stderr and stdout handling (and allow target/action for stderr messages)
- subprocess:sender stderrOutput:(const char *)buffer
{
if ( standardError )
[self sendText:buffer andNewline:NO to:standardError];
else
fputs( buffer, stderr ); // To the console.
return self; // added to remove warning... -- DAY
}
- subprocess:sender done:(int)status :(MiscSubprocessEndCode)code
{
executionInProgress = NO;
[self finishOutput];
return self;
}
- (BOOL) runToCompletion { return runToCompletion; }
- setRunToCompletion:(BOOL)c;
{
runToCompletion = c;
return self;
}
- (int) sortWhenColumnsMove { return sortWhenColumnsMove; }
- setSortWhenColumnsMove:(int)i
{
sortWhenColumnsMove = i;
return self;
}
- (char)delimiter { return delimiter; }
- setDelimiter:(char)c
{
delimiter = c;
return self;
}
- setExecArgs:(const char *)a1:(const char *)a2:(const char *)a3
{
; // TODO - finish me
return self;
}
/*
* Methods for sending data to the standard input of a process.
*/
/*
* takeStdinFrom:sender writes [sender stringValue] followed by a newline
* to the process.
*/
- takeStdinFrom:sender
{
[self writeToStdin:[sender stringValue]];
[self writeToStdin:"\n"];
return self;
}
/*
* writeToStdin writes a string verbatim to the standard input of the
* process.
*/
- writeToStdin:(const char *)str
{
[process send:str withNewline:NO];
return self;
}
@end
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.