Permalink
Browse files

Implementation of componentsSeparatedByShell.

componentsSeparatedByShell is intended to parse a
string into an array of strings, looking for
possible quoted arguments and escaped quotes.

This SenTestCase test componentsSeparatedByShell
using several test command lines with different
posibilities about quotes, spaces and those kind
of things.
  • Loading branch information...
1 parent 90072e0 commit b06121e55499727f0e50eefe655601536410fe54 @drodriguez drodriguez committed Jun 27, 2009
Showing with 278 additions and 0 deletions.
  1. +112 −0 objc/NuZip.m
  2. +166 −0 test/NZNSString+ShellSplitTest.m
View
112 objc/NuZip.m
@@ -33,6 +33,21 @@ int nuzip_printf(const char *format, ...)
int zip_main(int argc, char *argv[]);
+@interface NSString (NZShellSplit)
+
+/*
+ * Split a string as a shell command line.
+ * Split the string using the common rules used by shells. It will use
+ * spaces as shell separators, unless those spaces are in a single or
+ * double quoted argument. It will also avoid using escaped quotes as
+ * the start or end of a quoted argument.
+ */
+- (NSArray *)componentsSeparatedByShell;
+
+@end
+
+
+
@implementation NuZip
+ (int) unzip:(NSString *) command
@@ -71,6 +86,103 @@ + (int) zip:(NSString *) command
@end
+
+
+static const unichar kSpace = 0x0020;
+static const unichar kDoubleQuote = 0x0022;
+static const unichar kSingleQuote = 0x0027;
+static const unichar kBackslash = 0x005C;
+
+@implementation NSString (NZShellSplit)
+
+- (NSArray *)componentsSeparatedByShell {
+ NSUInteger current = 0;
+ NSUInteger length = [self length];
+ NSMutableArray *arguments = [[NSMutableArray alloc] init];
+ unichar ch, endingQuote;
+
+ while (current < length) {
+ NSMutableString *argument = [[NSMutableString alloc] init];
+
+ while (current < length) {
+ ch = [self characterAtIndex:current++];
+
+ CFMutableStringRef part = CFStringCreateMutable(NULL, 0);
+ if (ch == kDoubleQuote || ch == kSingleQuote) {
+ endingQuote = ch;
+
+ while (current < length) {
+ ch = [self characterAtIndex:current++];
+
+ if (ch == endingQuote) {
+ break;
+ } else if (ch == kBackslash) {
+ if (current < length) { // Slash at the end of the string?
+ ch = [self characterAtIndex:current++];
+ }
+ CFStringAppendCharacters(part, &ch, 1);
+ } else {
+ CFStringAppendCharacters(part, &ch, 1);
+ }
+ }
+
+ if (current >= length && ch != endingQuote) {
+ NSString *reason =
+ [NSString stringWithFormat:@"Unmatched quote <%C>",
+ endingQuote];
+ @throw [NSException exceptionWithName:NSInvalidArgumentException
+ reason:reason
+ userInfo:nil];
+ }
+ } else if (ch == kBackslash) {
+ if (current >= length) { // Slash at the end of the string
+ ch = kBackslash;
+ } else {
+ ch = [self characterAtIndex:current++];
+ }
+ CFStringAppendCharacters(part, &ch, 1);
+ } else if (ch != kSpace && ch != kBackslash &&
+ ch != kDoubleQuote && ch != kSingleQuote) {
+ CFStringAppendCharacters(part, &ch, 1);
+ while (current < length) {
+ ch = [self characterAtIndex:current++];
+ if (ch == kSpace || ch == kBackslash ||
+ ch == kDoubleQuote || ch == kSingleQuote) {
+ current--; // Otherwise we will jump over the character.
+ break;
+ } else {
+ CFStringAppendCharacters(part, &ch, 1);
+ }
+ }
+ } else { // it has to be a space (or more), ignore them
+ while (current < length &&
+ ((ch = [self characterAtIndex:current++]) == kSpace)) {
+ // do nothing
+ }
+ if (current < length) {
+ current--; // Unread last read character.
+ }
+ CFRelease(part);
+ break;
+ }
+
+ [argument appendString:(NSString *)part];
+ CFRelease(part);
+ }
+
+ [arguments addObject:[[argument copy] autorelease]];
+ [argument release];
+ }
+
+ NSArray *returnValue = [arguments copy];
+ [arguments release];
+ return [returnValue autorelease];
+}
+
+@end
+
+
+
/*
miniunz.c
Version 1.01e, February 12th, 2005
View
166 test/NZNSString+ShellSplitTest.m
@@ -0,0 +1,166 @@
+#import <SenTestingKit/SenTestingKit.h>
+
+@interface NZNSStringShellSplitTest : SenTestCase {}
+
+@end
+
+// This is here to avoid warnings with the compiler
+@interface NSString (NZShellSplit)
+- (NSArray *)componentsSeparatedByShell;
+@end
+
+@implementation NZNSStringShellSplitTest
+
+- (void)testOneArgumentString {
+ NSString *cmd = @"abc";
+ NSArray *args = [cmd componentsSeparatedByShell];
+
+ STAssertEquals([args count], (NSUInteger)1,
+ @"A command with one argument should have only one argument.");
+ STAssertTrue([@"abc" isEqualToString:[args objectAtIndex:0]],
+ @"The argument #0 should be 'abc'.");
+}
+
+- (void)testTwoArgumentsSeparatedByOneSpace {
+ NSString *cmd = @"abc def";
+ NSArray *args = [cmd componentsSeparatedByShell];
+
+ STAssertEquals([args count], (NSUInteger)2,
+ @"A command with two argument should have two arguments.");
+ STAssertTrue([@"abc" isEqualToString:[args objectAtIndex:0]],
+ @"The argument #0 should be 'abc'.");
+ STAssertTrue([@"def" isEqualToString:[args objectAtIndex:1]],
+ @"The argument #1 should be 'def'.");
+}
+
+- (void)testTwoArgumentsSeparatedByMoreSpaces {
+ NSString *cmd = @"abc def";
+ NSArray *args = [cmd componentsSeparatedByShell];
+
+ STAssertEquals([args count], (NSUInteger)2,
+ @"A command with two argument should have two arguments.");
+ STAssertTrue([@"abc" isEqualToString:[args objectAtIndex:0]],
+ @"The argument #0 should be 'abc'.");
+ STAssertTrue([@"def" isEqualToString:[args objectAtIndex:1]],
+ @"The argument #1 should be 'def'.");
+}
+
+- (void)testThreeArgumentsSeparatedBySpaces {
+ NSString *cmd = @"abc def ghi";
+ NSArray *args = [cmd componentsSeparatedByShell];
+
+ STAssertEquals([args count], (NSUInteger)3,
+ @"A command with three argument should have three arguments.");
+ STAssertTrue([@"abc" isEqualToString:[args objectAtIndex:0]],
+ @"The argument #0 should be 'abc'.");
+ STAssertTrue([@"def" isEqualToString:[args objectAtIndex:1]],
+ @"The argument #1 should be 'def'.");
+ STAssertTrue([@"ghi" isEqualToString:[args objectAtIndex:2]],
+ @"The argument #2 should be 'ghi'.");
+}
+
+- (void)testOneArgumentWithDoubleQuotes {
+ NSString *cmd = @"\"abc\"";
+ NSArray *args = [cmd componentsSeparatedByShell];
+
+ STAssertEquals([args count], (NSUInteger)1,
+ @"A command with one quoted argument should have one argument.");
+ STAssertTrue([@"abc" isEqualToString:[args objectAtIndex:0]],
+ @"The argument #0 should be 'abc'.");
+}
+
+- (void)testOneArgumentWithSingleQuotes {
+ NSString *cmd = @"'abc'";
+ NSArray *args = [cmd componentsSeparatedByShell];
+
+ STAssertEquals([args count], (NSUInteger)1,
+ @"A command with one quoted argument should have one argument.");
+ STAssertTrue([@"abc" isEqualToString:[args objectAtIndex:0]],
+ @"The argument #0 should be 'abc'.");
+}
+
+- (void)testOneQuotedArgumentWithEscapedQuotes {
+ NSString *cmd = @"'ab\\'c'";
+ NSArray *args = [cmd componentsSeparatedByShell];
+
+ STAssertEquals([args count], (NSUInteger)1,
+ @"A command with one quoted argument should have one argument.");
+ STAssertTrue([@"ab'c" isEqualToString:[args objectAtIndex:0]],
+ @"The argument #0 should be 'abc'.");
+}
+
+- (void)testOneQuotedArgumentWithSpaces {
+ NSString *cmd = @"'abc def ghi'";
+ NSArray *args = [cmd componentsSeparatedByShell];
+
+ STAssertEquals([args count], (NSUInteger)1,
+ @"A command with one quoted argument should have one argument.");
+ STAssertTrue([@"abc def ghi" isEqualToString:[args objectAtIndex:0]],
+ @"The argument #0 should be 'abc def ghi'.");
+}
+
+- (void)testOneQuotedArgumentWithOtherUnescapedQuotes {
+ NSString *cmd = @"'abc\"ghi'";
+ NSArray *args = [cmd componentsSeparatedByShell];
+
+ STAssertEquals([args count], (NSUInteger)1,
+ @"A command with one quoted argument should have one argument.");
+ STAssertTrue([@"abc\"ghi" isEqualToString:[args objectAtIndex:0]],
+ @"The argument #0 should be 'abc\"ghi'.");
+}
+
+- (void)testBackSlashAtEnd {
+ NSString *cmd = @"abc ghi\\";
+ NSArray *args = [cmd componentsSeparatedByShell];
+
+ STAssertEquals([args count], (NSUInteger)2,
+ @"A command with two arguments should have two arguments.");
+ STAssertTrue([@"abc" isEqualToString:[args objectAtIndex:0]],
+ @"The argument #0 should be 'abc'.");
+ STAssertTrue([@"ghi\\" isEqualToString:[args objectAtIndex:1]],
+ @"The argument #1 should be 'ghi\\'.");
+}
+
+- (void)testEscapedQuotes {
+ NSString *cmd = @"\\\" \\'";
+ NSArray *args = [cmd componentsSeparatedByShell];
+
+ STAssertEquals([args count], (NSUInteger)2,
+ @"A command with two arguments should have two arguments.");
+ STAssertTrue([@"\"" isEqualToString:[args objectAtIndex:0]],
+ @"The argument #0 should be '\"'.");
+ STAssertTrue([@"'" isEqualToString:[args objectAtIndex:1]],
+ @"The argument #1 should be '''.");
+}
+
+- (void)testUnmatchedDoubleQuotes {
+ NSString *cmd = @"abc \"def";
+ STAssertThrowsSpecificNamed([cmd componentsSeparatedByShell],
+ NSException, NSInvalidArgumentException,
+ @"A command with unmatched quotes should throw "
+ @"a unmatched quotes exception.");
+}
+
+- (void)testUnmatchedSingleQuotes {
+ NSString *cmd = @"abc 'def";
+ STAssertThrowsSpecificNamed([cmd componentsSeparatedByShell],
+ NSException, NSInvalidArgumentException,
+ @"A command with unmatched quotes should throw "
+ @"a unmatched quotes exception.");
+}
+
+- (void)testMixedQuotedAndUnquoted {
+ NSString *cmd = @"\"abc\" -d \"efg\"";
+ NSArray *args = [cmd componentsSeparatedByShell];
+
+ STAssertEquals([args count], (NSUInteger)3,
+ @"A command with two arguments should have two arguments.");
+ STAssertTrue([@"abc" isEqualToString:[args objectAtIndex:0]],
+ @"The argument #0 should be 'abc'.");
+ STAssertTrue([@"-d" isEqualToString:[args objectAtIndex:1]],
+ @"The argument #1 should be '-d'.");
+ STAssertTrue([@"efg" isEqualToString:[args objectAtIndex:2]],
+ @"The argument #2 should be 'efg'.");
+}
+
+@end

0 comments on commit b06121e

Please sign in to comment.