diff options
-rw-r--r-- | Foundation/GTMRegex.m | 61 | ||||
-rw-r--r-- | Foundation/GTMRegexTest.m | 110 | ||||
-rw-r--r-- | Foundation/GTMScriptRunner.h | 134 | ||||
-rw-r--r-- | Foundation/GTMScriptRunner.m | 246 | ||||
-rw-r--r-- | Foundation/GTMScriptRunnerTest.m | 244 | ||||
-rw-r--r-- | GTM.xcodeproj/project.pbxproj | 16 | ||||
-rw-r--r-- | ReleaseNotes.txt | 8 |
7 files changed, 813 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 diff --git a/GTM.xcodeproj/project.pbxproj b/GTM.xcodeproj/project.pbxproj index 9dfd689..7439100 100644 --- a/GTM.xcodeproj/project.pbxproj +++ b/GTM.xcodeproj/project.pbxproj @@ -75,6 +75,9 @@ F43E4E620D4E5EC90041161F /* GTMNSData+zlib.m in Sources */ = {isa = PBXBuildFile; fileRef = F43E4E5F0D4E5EC90041161F /* GTMNSData+zlib.m */; }; F43E4E660D4E5ED40041161F /* GTMNSData+zlibTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F43E4E600D4E5EC90041161F /* GTMNSData+zlibTest.m */; }; F43E4F6D0D4E60C50041161F /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = F43E4F6C0D4E60C50041161F /* libz.dylib */; }; + F47A79880D746EE9002302AB /* GTMScriptRunner.h in Headers */ = {isa = PBXBuildFile; fileRef = F47A79850D746EE9002302AB /* GTMScriptRunner.h */; }; + F47A79890D746EE9002302AB /* GTMScriptRunner.m in Sources */ = {isa = PBXBuildFile; fileRef = F47A79860D746EE9002302AB /* GTMScriptRunner.m */; }; + F47A798B0D746EFC002302AB /* GTMScriptRunnerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F47A79870D746EE9002302AB /* GTMScriptRunnerTest.m */; }; F47F1C120D490BC000925B8F /* GTMNSBezierPath+Shading.h in Headers */ = {isa = PBXBuildFile; fileRef = F47F1C0D0D490BC000925B8F /* GTMNSBezierPath+Shading.h */; }; F47F1C130D490BC000925B8F /* GTMNSBezierPath+Shading.m in Sources */ = {isa = PBXBuildFile; fileRef = F47F1C0E0D490BC000925B8F /* GTMNSBezierPath+Shading.m */; }; F47F1C190D490BD200925B8F /* GTMNSBezierPath+ShadingTest.10.4.tif in Resources */ = {isa = PBXBuildFile; fileRef = F47F1C0F0D490BC000925B8F /* GTMNSBezierPath+ShadingTest.10.4.tif */; }; @@ -156,6 +159,9 @@ F43E4E5F0D4E5EC90041161F /* GTMNSData+zlib.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSData+zlib.m"; sourceTree = "<group>"; }; F43E4E600D4E5EC90041161F /* GTMNSData+zlibTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSData+zlibTest.m"; sourceTree = "<group>"; }; F43E4F6C0D4E60C50041161F /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = /usr/lib/libz.dylib; sourceTree = "<absolute>"; }; + F47A79850D746EE9002302AB /* GTMScriptRunner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMScriptRunner.h; sourceTree = "<group>"; }; + F47A79860D746EE9002302AB /* GTMScriptRunner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMScriptRunner.m; sourceTree = "<group>"; }; + F47A79870D746EE9002302AB /* GTMScriptRunnerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMScriptRunnerTest.m; sourceTree = "<group>"; }; F47F1C0D0D490BC000925B8F /* GTMNSBezierPath+Shading.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GTMNSBezierPath+Shading.h"; sourceTree = "<group>"; }; F47F1C0E0D490BC000925B8F /* GTMNSBezierPath+Shading.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSBezierPath+Shading.m"; sourceTree = "<group>"; }; F47F1C0F0D490BC000925B8F /* GTMNSBezierPath+ShadingTest.10.4.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "GTMNSBezierPath+ShadingTest.10.4.tif"; sourceTree = "<group>"; }; @@ -199,6 +205,7 @@ F48FE29E0D198D36009257D2 /* GTMNSView+UnitTesting.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSView+UnitTesting.m"; sourceTree = "<group>"; }; F48FE29F0D198D36009257D2 /* GTMSenTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMSenTestCase.h; sourceTree = "<group>"; }; F48FE2E10D198E4C009257D2 /* GTMSystemVersionTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMSystemVersionTest.m; sourceTree = "<group>"; }; + F4C978090D5B79C7001C29A6 /* ReleaseNotes.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ReleaseNotes.txt; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -249,6 +256,7 @@ isa = PBXGroup; children = ( 32DBCF5E0370ADEE00C91783 /* GTM_Prefix.pch */, + F4C978090D5B79C7001C29A6 /* ReleaseNotes.txt */, F48FE26F0D198CBA009257D2 /* AppKit */, F48FE2720D198CCE009257D2 /* Foundation */, F48FE2770D198CEA009257D2 /* UnitTesting */, @@ -349,6 +357,9 @@ F437F55A0D50BC0A00F5C3A4 /* GTMRegex.h */, F437F55B0D50BC0A00F5C3A4 /* GTMRegex.m */, F437F55C0D50BC0A00F5C3A4 /* GTMRegexTest.m */, + F47A79850D746EE9002302AB /* GTMScriptRunner.h */, + F47A79860D746EE9002302AB /* GTMScriptRunner.m */, + F47A79870D746EE9002302AB /* GTMScriptRunnerTest.m */, F48FE2920D198D24009257D2 /* GTMSystemVersion.h */, F48FE2930D198D24009257D2 /* GTMSystemVersion.m */, F48FE2E10D198E4C009257D2 /* GTMSystemVersionTest.m */, @@ -394,6 +405,7 @@ F43E4DD90D4E56320041161F /* GTMNSEnumerator+Filter.h in Headers */, F43E4E610D4E5EC90041161F /* GTMNSData+zlib.h in Headers */, F437F55D0D50BC0A00F5C3A4 /* GTMRegex.h in Headers */, + F47A79880D746EE9002302AB /* GTMScriptRunner.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -462,10 +474,12 @@ 0867D690FE84028FC02AAC07 /* Project object */ = { isa = PBXProject; buildConfigurationList = 1DEB918108733D990010E9CD /* Build configuration list for PBXProject "GTM" */; + compatibilityVersion = "Xcode 2.4"; hasScannedForEncodings = 1; mainGroup = 0867D691FE84028FC02AAC07 /* GTM */; productRefGroup = 034768DFFF38A50411DB9C8B /* Products */; projectDirPath = ""; + projectRoot = ""; targets = ( F42E086C0D199A5B00D5DDE0 /* GTM */, F472042B0D33BEAF00E9F378 /* All UnitTests */, @@ -543,6 +557,7 @@ F43E4C2D0D4E36230041161F /* GTMNSString+XMLTest.m in Sources */, F43E4DDE0D4E56380041161F /* GTMNSEnumerator+FilterTest.m in Sources */, F437F5620D50BC1D00F5C3A4 /* GTMRegexTest.m in Sources */, + F47A798B0D746EFC002302AB /* GTMScriptRunnerTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -566,6 +581,7 @@ F43E4DDA0D4E56320041161F /* GTMNSEnumerator+Filter.m in Sources */, F43E4E620D4E5EC90041161F /* GTMNSData+zlib.m in Sources */, F437F55E0D50BC0A00F5C3A4 /* GTMRegex.m in Sources */, + F47A79890D746EE9002302AB /* GTMScriptRunner.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ReleaseNotes.txt b/ReleaseNotes.txt index f197504..fee7f61 100644 --- a/ReleaseNotes.txt +++ b/ReleaseNotes.txt @@ -7,12 +7,20 @@ Discussion group: http://groups.google.com/group/google-toolbox-for-mac Release ?.?.? Changes since 1.0.0 +- Updated the project so Xcode 3 is also happy. + - Fixed up the prefix header of the project and prefix handing in the Unittest Xcode Config. (thanks schafdog) + - Fixed error in handling default compression for NSData+zlib + - Changed name on API in NSString+XML and added another api to make this a litte more clear. (thanks Kent) +- 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. Release 1.0.0 |