aboutsummaryrefslogtreecommitdiff
path: root/Foundation
diff options
context:
space:
mode:
authorGravatar thomasvl <thomasvl@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3>2008-02-27 03:54:04 +0000
committerGravatar thomasvl <thomasvl@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3>2008-02-27 03:54:04 +0000
commitf38d4ee0e9157d2ac3c9a08fde37b9e19457fb0f (patch)
tree4182025226697a53d884a65a13e94adc44e60c74 /Foundation
parentb3086cfd9aead0b2900c2d942b289957837286ab (diff)
Found and fixed a bug in the regex enumerators that was causing them to
incorrectly walk a string when using '^' in an expression. Added GTMScriptRunner for spawning scripts.
Diffstat (limited to 'Foundation')
-rw-r--r--Foundation/GTMRegex.m61
-rw-r--r--Foundation/GTMRegexTest.m110
-rw-r--r--Foundation/GTMScriptRunner.h134
-rw-r--r--Foundation/GTMScriptRunner.m246
-rw-r--r--Foundation/GTMScriptRunnerTest.m244
5 files changed, 789 insertions, 6 deletions
diff --git a/Foundation/GTMRegex.m b/Foundation/GTMRegex.m
index c582b1e..d7900fa 100644
--- a/Foundation/GTMRegex.m
+++ b/Foundation/GTMRegex.m
@@ -6,9 +6,9 @@
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy
// of the License at
-//
+//
// http://www.apache.org/licenses/LICENSE-2.0
-//
+//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -21,7 +21,7 @@
// This is the pattern to use for walking replacement text when doing
// substitutions.
//
-// this pattern may look over escaped, but remember the compiler will consume
+// This pattern may look over-escaped, but remember the compiler will consume
// one layer of slashes, and then we have to escape the slashes for them to be
// seen as we want in the pattern.
static NSString *const kReplacementPattern =
@@ -43,12 +43,14 @@ static NSString *const kReplacementPattern =
GTMRegex *regex_;
NSData *utf8StrBuf_;
BOOL allSegments_;
+ BOOL treatStartOfNewSegmentAsBeginningOfString_;
regoff_t curParseIndex_;
regmatch_t *savedRegMatches_;
}
- (id)initWithRegex:(GTMRegex *)regex
processString:(NSString *)str
allSegments:(BOOL)allSegments;
+- (void)treatStartOfNewSegmentAsBeginningOfString:(BOOL)yesNo;
@end
@interface GTMRegexStringSegment (PrivateMethods)
@@ -263,9 +265,32 @@ static NSString *const kReplacementPattern =
GTMRegex *replacementRegex =
[GTMRegex regexWithPattern:kReplacementPattern
options:kGTMRegexOptionSupressNewlineSupport];
+#ifdef DEBUG
+ if (!replacementRegex)
+ NSLog(@"failed to parse out replacement regex!!!");
+#endif
+ GTMRegexEnumerator *relacementEnumerator =
+ [[[GTMRegexEnumerator alloc] initWithRegex:replacementRegex
+ processString:replacementPattern
+ allSegments:YES] autorelease];
+ // We turn on treatStartOfNewSegmentAsBeginningOfLine for this enumerator.
+ // As complex as kReplacementPattern is, it can't completely do what we want
+ // with the normal string walk. The problem is this, backreferences are a
+ // slash follow by a number ("\0"), but the replacement pattern might
+ // actually need to use backslashes (they have to be escaped). So if a
+ // replacement were "\\0", then there is no backreference, instead the
+ // replacement is a backslash and a zero. Generically this means an even
+ // number of backslashes are all escapes, and an odd are some number of
+ // literal backslashes followed by our backreference. Think of it as a "an
+ // odd number of slashes that comes after a non-backslash character." There
+ // is no way to rexpress this in re_format(7) extended expressions. Instead
+ // we look for a non-blackslash or string start followed by an optional even
+ // number of slashes followed by the backreference; and use the special
+ // flag; so after each match, we restart claiming it's the start of the
+ // string. (the problem match w/o this flag is a substition of "\2\1")
+ [relacementEnumerator treatStartOfNewSegmentAsBeginningOfString:YES];
// pull them all into an array so we can walk this as many times as needed.
- replacements =
- [[replacementRegex segmentEnumeratorForString:replacementPattern] allObjects];
+ replacements = [relacementEnumerator allObjects];
if (!replacements) {
NSLog(@"failed to create the replacements for subtituations");
return nil;
@@ -413,6 +438,21 @@ static NSString *const kReplacementPattern =
[super dealloc];
}
+- (void)treatStartOfNewSegmentAsBeginningOfString:(BOOL)yesNo {
+ // The way regexec works, it assumes the first char it's looking at to the
+ // start of the string. In normal use, this makes sense; but in this case,
+ // we're going to walk the entry string splitting it up by our pattern. That
+ // means for the first call, it is the string start, but for all future calls,
+ // it is NOT the string start, so we will pass regexec the flag to let it
+ // know. However, (you knew that was coming), there are some cases where you
+ // actually want the each pass to be considered as the start of the string
+ // (usually the cases are where a pattern can't express what's needed w/o
+ // this). There is no really good way to explain this behavior w/o all this
+ // text and lot of examples, so for now this is not in the public api, and
+ // just here. (Hint: see what w/in this file uses this for why we have it)
+ treatStartOfNewSegmentAsBeginningOfString_ = yesNo;
+}
+
- (id)nextObject {
GTMRegexStringSegment *result = nil;
@@ -446,11 +486,20 @@ static NSString *const kReplacementPattern =
nextMatches[0].rm_so = curParseIndex_;
nextMatches[0].rm_eo = [utf8StrBuf_ length];
+ // figure out our flags
+ int flags = REG_STARTEND;
+ if ((!treatStartOfNewSegmentAsBeginningOfString_) &&
+ (curParseIndex_ != 0)) {
+ // see -treatStartOfNewSegmentAsBeginningOfString: for why we have
+ // this check here.
+ flags |= REG_NOTBOL;
+ }
+
// call for the match
if ([regex_ runRegexOnUTF8:[utf8StrBuf_ bytes]
nmatch:([regex_ subPatternCount] + 1)
pmatch:nextMatches
- flags:REG_STARTEND]) {
+ flags:flags]) {
// match
if (allSegments_ &&
diff --git a/Foundation/GTMRegexTest.m b/Foundation/GTMRegexTest.m
index ef7d1e5..71c8405 100644
--- a/Foundation/GTMRegexTest.m
+++ b/Foundation/GTMRegexTest.m
@@ -503,6 +503,69 @@
NSArray *allSegments = [enumerator allObjects];
STAssertNotNil(allSegments, nil);
STAssertEquals(6U, [allSegments count], nil);
+
+ // test we are getting the flags right for newline
+ regex = [GTMRegex regexWithPattern:@"^a"];
+ STAssertNotNil(regex, nil);
+ enumerator = [regex segmentEnumeratorForString:@"aa\naa"];
+ STAssertNotNil(enumerator, nil);
+ // "a"
+ seg = [enumerator nextObject];
+ STAssertNotNil(seg, nil);
+ STAssertTrue([seg isMatch], nil);
+ STAssertEqualStrings([seg string], @"a", nil);
+ // "a\n"
+ seg = [enumerator nextObject];
+ STAssertNotNil(seg, nil);
+ STAssertFalse([seg isMatch], nil);
+ STAssertEqualStrings([seg string], @"a\n", nil);
+ // "a"
+ seg = [enumerator nextObject];
+ STAssertNotNil(seg, nil);
+ STAssertTrue([seg isMatch], nil);
+ STAssertEqualStrings([seg string], @"a", nil);
+ // "a"
+ seg = [enumerator nextObject];
+ STAssertNotNil(seg, nil);
+ STAssertFalse([seg isMatch], nil);
+ STAssertEqualStrings([seg string], @"a", nil);
+ // (end)
+ seg = [enumerator nextObject];
+ STAssertNil(seg, nil);
+
+ // test we are getting the flags right for newline, part 2
+ regex = [GTMRegex regexWithPattern:@"^a*$"];
+ STAssertNotNil(regex, nil);
+ enumerator = [regex segmentEnumeratorForString:@"aa\naa\nbb\naa"];
+ STAssertNotNil(enumerator, nil);
+ // "aa"
+ seg = [enumerator nextObject];
+ STAssertNotNil(seg, nil);
+ STAssertTrue([seg isMatch], nil);
+ STAssertEqualStrings([seg string], @"aa", nil);
+ // "\n"
+ seg = [enumerator nextObject];
+ STAssertNotNil(seg, nil);
+ STAssertFalse([seg isMatch], nil);
+ STAssertEqualStrings([seg string], @"\n", nil);
+ // "aa"
+ seg = [enumerator nextObject];
+ STAssertNotNil(seg, nil);
+ STAssertTrue([seg isMatch], nil);
+ STAssertEqualStrings([seg string], @"aa", nil);
+ // "\nbb\n"
+ seg = [enumerator nextObject];
+ STAssertNotNil(seg, nil);
+ STAssertFalse([seg isMatch], nil);
+ STAssertEqualStrings([seg string], @"\nbb\n", nil);
+ // "aa"
+ seg = [enumerator nextObject];
+ STAssertNotNil(seg, nil);
+ STAssertTrue([seg isMatch], nil);
+ STAssertEqualStrings([seg string], @"aa", nil);
+ // (end)
+ seg = [enumerator nextObject];
+ STAssertNil(seg, nil);
}
- (void)testMatchSegmentEnumeratorForString {
@@ -589,6 +652,53 @@
NSArray *allSegments = [enumerator allObjects];
STAssertNotNil(allSegments, nil);
STAssertEquals(3U, [allSegments count], nil);
+
+ // test we are getting the flags right for newline
+ regex = [GTMRegex regexWithPattern:@"^a"];
+ STAssertNotNil(regex, nil);
+ enumerator = [regex matchSegmentEnumeratorForString:@"aa\naa"];
+ STAssertNotNil(enumerator, nil);
+ // "a"
+ seg = [enumerator nextObject];
+ STAssertNotNil(seg, nil);
+ STAssertTrue([seg isMatch], nil);
+ STAssertEqualStrings([seg string], @"a", nil);
+ // "a\n" - skipped
+ // "a"
+ seg = [enumerator nextObject];
+ STAssertNotNil(seg, nil);
+ STAssertTrue([seg isMatch], nil);
+ STAssertEqualStrings([seg string], @"a", nil);
+ // "a" - skipped
+ // (end)
+ seg = [enumerator nextObject];
+ STAssertNil(seg, nil);
+
+ // test we are getting the flags right for newline, part 2
+ regex = [GTMRegex regexWithPattern:@"^a*$"];
+ STAssertNotNil(regex, nil);
+ enumerator = [regex matchSegmentEnumeratorForString:@"aa\naa\nbb\naa"];
+ STAssertNotNil(enumerator, nil);
+ // "aa"
+ seg = [enumerator nextObject];
+ STAssertNotNil(seg, nil);
+ STAssertTrue([seg isMatch], nil);
+ STAssertEqualStrings([seg string], @"aa", nil);
+ // "\n" - skipped
+ // "aa"
+ seg = [enumerator nextObject];
+ STAssertNotNil(seg, nil);
+ STAssertTrue([seg isMatch], nil);
+ STAssertEqualStrings([seg string], @"aa", nil);
+ // "\nbb\n" - skipped
+ // "aa"
+ seg = [enumerator nextObject];
+ STAssertNotNil(seg, nil);
+ STAssertTrue([seg isMatch], nil);
+ STAssertEqualStrings([seg string], @"aa", nil);
+ // (end)
+ seg = [enumerator nextObject];
+ STAssertNil(seg, nil);
}
- (void)testStringByReplacingMatchesInStringWithReplacement {
diff --git a/Foundation/GTMScriptRunner.h b/Foundation/GTMScriptRunner.h
new file mode 100644
index 0000000..226d75b
--- /dev/null
+++ b/Foundation/GTMScriptRunner.h
@@ -0,0 +1,134 @@
+//
+// GTMScriptRunner.h
+//
+// Copyright 2007-2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy
+// of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+//
+
+#import <Foundation/Foundation.h>
+
+/// Encapsulates the interaction with an interpreter for running scripts.
+// This class manages the interaction with some command-line interpreter (e.g.,
+// a shell, perl, python) and allows you to run expressions through the
+// interpreter, and even full scripts that reside in files on disk. By default,
+// the "/bin/sh" interpreter is used, but others may be explicitly specified.
+// This can be a convenient way to run quick shell commands from Cocoa, or even
+// interact with other shell tools such as "bc", or even "gdb".
+//
+// It's important to note that by default commands and scripts will have their
+// environments erased before execution. You can control the environment they
+// get with the -setEnvironment: method.
+//
+// The best way to show what this class does is to show some examples.
+//
+// Examples:
+//
+// GTMScriptRunner *sr = [GTMScriptRunner runner];
+// NSString *output = [sr run:@"ls -l /dev/null"];
+// /* output == "crw-rw-rw- 1 root wheel 3, 2 Mar 22 10:35 /dev/null" */
+//
+// GTMScriptRunner *sr = [GTMScriptRunner runner];
+// NSString *output = [sr runScript:@"/path/to/my/script.sh"];
+// /* output == the standard output from the script*/
+//
+// GTMScriptRunner *sr = [GTMScriptRunner runnerWithPerl];
+// NSString *output = [sr run:@"print 'A'x4"];
+// /* output == "AAAA" */
+//
+// See the unit test file for more examples.
+//
+@interface GTMScriptRunner : NSObject {
+ @private
+ NSString *interpreter_;
+ NSArray *interpreterArgs_;
+ NSDictionary *environment_;
+ BOOL trimsWhitespace_;
+}
+
+// Convenience methods for returning autoreleased GTMScriptRunner instances, that
+// are associated with the specified interpreter. The default interpreter
+// (returned from +runner is "/bin/sh").
++ (GTMScriptRunner *)runner;
++ (GTMScriptRunner *)runnerWithBash;
++ (GTMScriptRunner *)runnerWithPerl;
++ (GTMScriptRunner *)runnerWithPython;
+
+// Returns an autoreleased GTMScriptRunner instance associated with the specified
+// interpreter, and the given args. The specified args are the arguments that
+// should be applied to the interpreter itself, not scripts run through the
+// interpreter. For example, to start an interpreter using "perl -w", you could
+// do:
+// [GTMScriptRunner runnerWithInterpreter:@"/usr/bin/perl"
+// withArgs:[NSArray arrayWithObject:@"-w"]];
+//
++ (GTMScriptRunner *)runnerWithInterpreter:(NSString *)interp;
++ (GTMScriptRunner *)runnerWithInterpreter:(NSString *)interp
+ withArgs:(NSArray *)args;
+
+// Returns a GTMScriptRunner associated with |interp|
+- (id)initWithInterpreter:(NSString *)interp;
+
+// Returns a GTMScriptRunner associated with |interp| and |args| applied to the
+// specified interpreter. This method is the designated initializer.
+- (id)initWithInterpreter:(NSString *)interp withArgs:(NSArray *)args;
+
+// Runs the specified command string by sending it through the interpreter's
+// standard input. The standard output is returned. The standard error is
+// discarded.
+- (NSString *)run:(NSString *)cmds;
+// Same as the previous method, except the standard error is returned in |err|
+// if specified.
+- (NSString *)run:(NSString *)cmds standardError:(NSString **)err;
+
+// Runs the file at |path| using the interpreter.
+- (NSString *)runScript:(NSString *)path;
+// Runs the file at |path|, passing it |args| as arguments.
+- (NSString *)runScript:(NSString *)path withArgs:(NSArray *)args;
+// Same as above, except the standard error is returned in |err| if specified.
+- (NSString *)runScript:(NSString *)path withArgs:(NSArray *)args
+ standardError:(NSString **)err;
+
+// Returns the environment dictionary to use for the inferior process that will
+// run the interpreter. A return value of nil means that the interpreter's
+// environment should be erased.
+- (NSDictionary *)environment;
+
+// Sets the environment dictionary to use for the interpreter process. See
+// NSTask's -setEnvironment: documentation for details about the dictionary.
+// Basically, it's just a dict of key/value pairs corresponding to environment
+// keys and values. Setting a value of nil means that the environment should be
+// erased before running the interpreter.
+//
+// *** The default is nil. ***
+//
+// By default, all interpreters will run with a clean environment. If you want
+// the interpreter process to inherit your current environment you'll need to
+// do the following:
+//
+// GTMScriptRunner *sr = [GTMScriptRunner runner];
+// [sr setEnvironment:[[NSProcessInfo processInfo] environment]];
+//
+// SECURITY NOTE: That said, in general you should NOT do this because an
+// attacker can modify the environment that would then get sent to your scripts.
+// And if your binary is suid, then you ABSOLUTELY should not do this.
+//
+- (void)setEnvironment:(NSDictionary *)newEnv;
+
+// Sets (and returns) whether or not whitespace is automatically trimmed from
+// the ends of the returned strings. The default is YES, so trailing newlines
+// will be removed.
+- (BOOL)trimsWhitespace;
+- (void)setTrimsWhitespace:(BOOL)trim;
+
+@end
diff --git a/Foundation/GTMScriptRunner.m b/Foundation/GTMScriptRunner.m
new file mode 100644
index 0000000..30769fb
--- /dev/null
+++ b/Foundation/GTMScriptRunner.m
@@ -0,0 +1,246 @@
+//
+// GTMScriptRunner.m
+//
+// Copyright 2007-2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy
+// of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+//
+
+#import "GTMScriptRunner.h"
+
+
+@interface NSTask (SafeLaunching)
+- (BOOL)safeLaunch;
+@end
+
+@interface GTMScriptRunner (PrivateMethods)
+- (NSTask *)interpreterTaskWithAdditionalArgs:(NSArray *)args;
+@end
+
+@implementation GTMScriptRunner
+
++ (GTMScriptRunner *)runner {
+ return [[[self alloc] init] autorelease];
+}
+
++ (GTMScriptRunner *)runnerWithBash {
+ return [self runnerWithInterpreter:@"/bin/bash"];
+}
+
++ (GTMScriptRunner *)runnerWithPerl {
+ return [self runnerWithInterpreter:@"/usr/bin/perl"];
+}
+
++ (GTMScriptRunner *)runnerWithPython {
+ return [self runnerWithInterpreter:@"/usr/bin/python"];
+}
+
++ (GTMScriptRunner *)runnerWithInterpreter:(NSString *)interp {
+ return [self runnerWithInterpreter:interp withArgs:nil];
+}
+
++ (GTMScriptRunner *)runnerWithInterpreter:(NSString *)interp withArgs:(NSArray *)args {
+ return [[[self alloc] initWithInterpreter:interp withArgs:args] autorelease];
+}
+
+- (id)init {
+ return [self initWithInterpreter:nil];
+}
+
+- (id)initWithInterpreter:(NSString *)interp {
+ return [self initWithInterpreter:interp withArgs:nil];
+}
+
+- (id)initWithInterpreter:(NSString *)interp withArgs:(NSArray *)args {
+ if ((self = [super init])) {
+ trimsWhitespace_ = YES;
+ interpreter_ = [interp copy];
+ interpreterArgs_ = [args retain];
+ if (!interpreter_) {
+ interpreter_ = @"/bin/sh";
+ }
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [interpreter_ release];
+ [interpreterArgs_ release];
+ [super dealloc];
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"%@<%p>{ interpreter = '%@', args = %@ }",
+ [self class], self, interpreter_, interpreterArgs_];
+}
+
+- (NSString *)run:(NSString *)cmds {
+ return [self run:cmds standardError:nil];
+}
+
+- (NSString *)run:(NSString *)cmds standardError:(NSString **)err {
+ if (!cmds) return nil;
+
+ NSTask *task = [self interpreterTaskWithAdditionalArgs:nil];
+ NSFileHandle *toTask = [[task standardInput] fileHandleForWriting];
+ NSFileHandle *fromTask = [[task standardOutput] fileHandleForReading];
+
+ if (![task safeLaunch]) {
+ return nil;
+ }
+
+ [toTask writeData:[cmds dataUsingEncoding:NSUTF8StringEncoding]];
+ [toTask closeFile];
+
+ NSData *outData = [fromTask readDataToEndOfFile];
+ NSString *output = [[[NSString alloc] initWithData:outData
+ encoding:NSUTF8StringEncoding] autorelease];
+
+ // Handle returning standard error if |err| is not nil
+ if (err) {
+ NSFileHandle *stderror = [[task standardError] fileHandleForReading];
+ NSData *errData = [stderror readDataToEndOfFile];
+ *err = [[[NSString alloc] initWithData:errData
+ encoding:NSUTF8StringEncoding] autorelease];
+ if (trimsWhitespace_) {
+ *err = [*err stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
+ }
+ }
+
+ [task terminate];
+
+ if (trimsWhitespace_) {
+ output = [output stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
+ }
+
+ if ([output length] < 1) {
+ output = nil;
+ }
+
+ return output;
+}
+
+- (NSString *)runScript:(NSString *)path {
+ return [self runScript:path withArgs:nil];
+}
+
+- (NSString *)runScript:(NSString *)path withArgs:(NSArray *)args {
+ return [self runScript:path withArgs:args standardError:nil];
+}
+
+- (NSString *)runScript:(NSString *)path withArgs:(NSArray *)args standardError:(NSString **)err {
+ if (!path) return nil;
+
+ NSArray *scriptPlusArgs = [[NSArray arrayWithObject:path] arrayByAddingObjectsFromArray:args];
+ NSTask *task = [self interpreterTaskWithAdditionalArgs:scriptPlusArgs];
+ NSFileHandle *fromTask = [[task standardOutput] fileHandleForReading];
+
+ if (![task safeLaunch]) {
+ return nil;
+ }
+
+ NSData *outData = [fromTask readDataToEndOfFile];
+ NSString *output = [[[NSString alloc] initWithData:outData
+ encoding:NSUTF8StringEncoding] autorelease];
+
+ // Handle returning standard error if |err| is not nil
+ if (err) {
+ NSFileHandle *stderror = [[task standardError] fileHandleForReading];
+ NSData *errData = [stderror readDataToEndOfFile];
+ *err = [[[NSString alloc] initWithData:errData
+ encoding:NSUTF8StringEncoding] autorelease];
+ if (trimsWhitespace_) {
+ *err = [*err stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
+ }
+ }
+
+ [task terminate];
+
+ if (trimsWhitespace_) {
+ output = [output stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
+ }
+
+ if ([output length] < 1) {
+ output = nil;
+ }
+
+ return output;
+}
+
+- (NSDictionary *)environment {
+ return environment_;
+}
+
+- (void)setEnvironment:(NSDictionary *)newEnv {
+ [environment_ autorelease];
+ environment_ = [newEnv retain];
+}
+
+- (BOOL)trimsWhitespace {
+ return trimsWhitespace_;
+}
+
+- (void)setTrimsWhitespace:(BOOL)trim {
+ trimsWhitespace_ = trim;
+}
+
+@end
+
+
+@implementation GTMScriptRunner (PrivateMethods)
+
+- (NSTask *)interpreterTaskWithAdditionalArgs:(NSArray *)args {
+ NSTask *task = [[[NSTask alloc] init] autorelease];
+ [task setLaunchPath:interpreter_];
+ [task setStandardInput:[NSPipe pipe]];
+ [task setStandardOutput:[NSPipe pipe]];
+ [task setStandardError:[NSPipe pipe]];
+
+ // If |environment_| is nil, then use an empty dictionary, otherwise use
+ // environment_ exactly.
+ [task setEnvironment:(environment_
+ ? environment_
+ : [NSDictionary dictionary])];
+
+ // Build args to interpreter. The format is:
+ // interp [args-to-interp] [script-name [args-to-script]]
+ NSArray *allArgs = nil;
+ if (interpreterArgs_) {
+ allArgs = interpreterArgs_;
+ }
+ if (args) {
+ allArgs = allArgs ? [allArgs arrayByAddingObjectsFromArray:args] : args;
+ }
+ if (allArgs){
+ [task setArguments:allArgs];
+ }
+
+ return task;
+}
+
+@end
+
+@implementation NSTask (SafeLaunching)
+
+- (BOOL)safeLaunch {
+ BOOL isOK = YES;
+ @try {
+ [self launch];
+ } @catch (id ex) {
+ isOK = NO;
+ NSLog(@"Failed to launch interpreter '%@' due to: %@", [self launchPath], ex);
+ }
+ return isOK;
+}
+
+@end
diff --git a/Foundation/GTMScriptRunnerTest.m b/Foundation/GTMScriptRunnerTest.m
new file mode 100644
index 0000000..dc92ac7
--- /dev/null
+++ b/Foundation/GTMScriptRunnerTest.m
@@ -0,0 +1,244 @@
+//
+// GTMScriptRunnerTest.m
+//
+// Copyright 2007-2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy
+// of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+//
+
+#import <SenTestingKit/SenTestingKit.h>
+#import <sys/types.h>
+#import <unistd.h>
+#import "GTMScriptRunner.h"
+
+@interface GTMScriptRunnerTest : SenTestCase {
+ @private
+ NSString *shScript_;
+ NSString *perlScript_;
+}
+@end
+
+@interface GTMScriptRunnerTest (PrivateMethods)
+- (void)helperTestBourneShellUsingScriptRunner:(GTMScriptRunner *)sr;
+@end
+
+@implementation GTMScriptRunnerTest
+
+- (void)setUp {
+ shScript_ = [NSString stringWithFormat:@"/tmp/script_runner_unittest_%d_%d_sh", geteuid(), getpid()];
+ [@"#!/bin/sh\n"
+ @"i=1\n"
+ @"if [ -n \"$1\" ]; then\n"
+ @" i=$1\n"
+ @"fi\n"
+ @"echo $i\n"
+ writeToFile:shScript_ atomically:YES encoding:NSUTF8StringEncoding error:nil];
+
+ perlScript_ = [NSString stringWithFormat:@"/tmp/script_runner_unittest_%d_%d_pl", geteuid(), getpid()];
+ [@"#!/usr/bin/perl\n"
+ @"use strict;\n"
+ @"my $i = 1;\n"
+ @"if (defined $ARGV[0]) {\n"
+ @" $i = $ARGV[0];\n"
+ @"}\n"
+ @"print \"$i\n\"\n"
+ writeToFile:perlScript_ atomically:YES encoding:NSUTF8StringEncoding error:nil];
+}
+
+- (void)tearDown {
+ const char *path = [shScript_ fileSystemRepresentation];
+ if (path)
+ unlink(path);
+ path = [perlScript_ fileSystemRepresentation];
+ if (path)
+ unlink(path);
+}
+
+- (void)testShCommands {
+ GTMScriptRunner *sr = [GTMScriptRunner runner];
+ [self helperTestBourneShellUsingScriptRunner:sr];
+}
+
+- (void)testBashCommands {
+ GTMScriptRunner *sr = [GTMScriptRunner runnerWithBash];
+ [self helperTestBourneShellUsingScriptRunner:sr];
+}
+
+- (void)testZshCommands {
+ GTMScriptRunner *sr = [GTMScriptRunner runnerWithInterpreter:@"/bin/zsh"];
+ [self helperTestBourneShellUsingScriptRunner:sr];
+}
+
+- (void)testBcCommands {
+ GTMScriptRunner *sr = [GTMScriptRunner runnerWithInterpreter:@"/usr/bin/bc"
+ withArgs:[NSArray arrayWithObject:@"-lq"]];
+ STAssertNotNil(sr, @"Script runner must not be nil");
+ NSString *output = nil;
+
+ // Simple expression (NOTE that bc requires that commands end with a newline)
+ output = [sr run:@"1 + 2\n"];
+ STAssertEqualObjects(output, @"3", @"output should equal '3'");
+
+ // Simple expression with variables and multiple statements
+ output = [sr run:@"i=1; i+2\n"];
+ STAssertEqualObjects(output, @"3", @"output should equal '3'");
+
+ // Simple expression with base conversion
+ output = [sr run:@"obase=2; 2^5\n"];
+ STAssertEqualObjects(output, @"100000", @"output should equal '100000'");
+
+ // Simple expression with sine and cosine functions
+ output = [sr run:@"scale=3;s(0)+c(0)\n"];
+ STAssertEqualObjects(output, @"1.000", @"output should equal '1.000'");
+}
+
+- (void)testPerlCommands {
+ GTMScriptRunner *sr = [GTMScriptRunner runnerWithPerl];
+ STAssertNotNil(sr, @"Script runner must not be nil");
+ NSString *output = nil;
+
+ // Simple print
+ output = [sr run:@"print 'hi'"];
+ STAssertEqualObjects(output, @"hi", @"output should equal 'hi'");
+
+ // Simple print x4
+ output = [sr run:@"print 'A'x4"];
+ STAssertEqualObjects(output, @"AAAA", @"output should equal 'AAAA'");
+
+ // Simple perl-y stuff
+ output = [sr run:@"my $i=0; until ($i++==41){} print $i"];
+ STAssertEqualObjects(output, @"42", @"output should equal '42'");
+}
+
+- (void)testPythonCommands {
+ GTMScriptRunner *sr = [GTMScriptRunner runnerWithPython];
+ STAssertNotNil(sr, @"Script runner must not be nil");
+ NSString *output = nil;
+
+ // Simple print
+ output = [sr run:@"print 'hi'"];
+ STAssertEqualObjects(output, @"hi", @"output should equal 'hi'");
+
+ // Simple python expression
+ output = [sr run:@"print '-'.join(['a', 'b', 'c'])"];
+ STAssertEqualObjects(output, @"a-b-c", @"output should equal 'a-b-c'");
+}
+
+- (void)testBashScript {
+ GTMScriptRunner *sr = [GTMScriptRunner runnerWithBash];
+ STAssertNotNil(sr, @"Script runner must not be nil");
+ NSString *output = nil;
+
+ // Simple sh script
+ output = [sr runScript:shScript_];
+ STAssertEqualObjects(output, @"1", @"output should equal '1'");
+
+ // Simple sh script with 1 command line argument
+ output = [sr runScript:shScript_ withArgs:[NSArray arrayWithObject:@"2"]];
+ STAssertEqualObjects(output, @"2", @"output should equal '2'");
+}
+
+- (void)testPerlScript {
+ GTMScriptRunner *sr = [GTMScriptRunner runnerWithPerl];
+ STAssertNotNil(sr, @"Script runner must not be nil");
+ NSString *output = nil;
+
+ // Simple Perl script
+ output = [sr runScript:perlScript_];
+ STAssertEqualObjects(output, @"1", @"output should equal '1'");
+
+ // Simple perl script with 1 command line argument
+ output = [sr runScript:perlScript_ withArgs:[NSArray arrayWithObject:@"2"]];
+ STAssertEqualObjects(output, @"2", @"output should equal '2'");
+}
+
+- (void)testEnvironment {
+ GTMScriptRunner *sr = [GTMScriptRunner runner];
+ STAssertNotNil(sr, @"Script runner must not be nil");
+ NSString *output = nil;
+
+ output = [sr run:@"/usr/bin/env | wc -l"];
+ int numVars = [output intValue];
+ STAssertTrue(numVars > 0, @"numVars should be positive");
+ // By default the environment is wiped clean, however shells often add a few
+ // of their own env vars after things have been wiped. For example, sh will
+ // add about 3 env vars (PWD, _, and SHLVL).
+ STAssertTrue(numVars < 5, @"Our env should be almost empty");
+
+ NSDictionary *newEnv = [NSDictionary dictionaryWithObject:@"bar"
+ forKey:@"foo"];
+ [sr setEnvironment:newEnv];
+
+ output = [sr run:@"/usr/bin/env | wc -l"];
+ STAssertTrue([output intValue] == numVars + 1,
+ @"should have one more env var now");
+
+ [sr setEnvironment:nil];
+ output = [sr run:@"/usr/bin/env | wc -l"];
+ STAssertTrue([output intValue] == numVars,
+ @"should be back down to %d vars", numVars);
+
+ NSDictionary *currVars = [[NSProcessInfo processInfo] environment];
+ [sr setEnvironment:currVars];
+
+ output = [sr run:@"/usr/bin/env | wc -l"];
+ STAssertTrue([output intValue] == [currVars count],
+ @"should be back down to %d vars", numVars);
+}
+
+@end
+
+@implementation GTMScriptRunnerTest (PrivateMethods)
+
+- (void)helperTestBourneShellUsingScriptRunner:(GTMScriptRunner *)sr {
+ STAssertNotNil(sr, @"Script runner must not be nil");
+ NSString *output = nil;
+
+ // Simple command
+ output = [sr run:@"ls /etc/passwd"];
+ STAssertEqualObjects(output, @"/etc/passwd", @"output should equal '/etc/passwd'");
+
+ // Simple command pipe-line
+ output = [sr run:@"ls /etc/ | grep passwd | tail -1"];
+ STAssertEqualObjects(output, @"passwd", @"output should equal 'passwd'");
+
+ // Simple pipe-line with quotes and awk variables
+ output = [sr run:@"ps jaxww | awk '{print $2}' | sort -nr | tail -2 | head -1"];
+ STAssertEqualObjects(output, @"1", @"output should equal '1'");
+
+ // Simple shell loop with variables
+ output = [sr run:@"i=0; while [ $i -lt 100 ]; do i=$((i+1)); done; echo $i"];
+ STAssertEqualObjects(output, @"100", @"output should equal '100'");
+
+ // Simple command with newlines
+ output = [sr run:@"i=1\necho $i"];
+ STAssertEqualObjects(output, @"1", @"output should equal '1'");
+
+ // Simple full shell script
+ output = [sr run:@"#!/bin/sh\ni=1\necho $i\n"];
+ STAssertEqualObjects(output, @"1", @"output should equal '1'");
+
+ NSString *err = nil;
+
+ // Test getting standard error with no stdout
+ output = [sr run:@"ls /etc/does-not-exist" standardError:&err];
+ STAssertNil(output, @"output should be nil due to expected error");
+ STAssertEqualObjects(err, @"ls: /etc/does-not-exist: No such file or directory", @"");
+
+ // Test getting standard output along with some standard error
+ output = [sr run:@"ls /etc/does-not-exist /etc/passwd" standardError:&err];
+ STAssertEqualObjects(output, @"/etc/passwd", @"");
+ STAssertEqualObjects(err, @"ls: /etc/does-not-exist: No such file or directory", @"");
+}
+
+@end