2

Note: Not sure why this is marked as duplicate as I clearly stated that I don't want to use stringByReplacingOccurrencesOfString over and over again.

I have a question regarding the special character filename.

I have implemented a program, so that when you open a file or multiple files, the program will read all these filenames and local path and store them into the NSMutableArray. This part works perfectly without a problem.

My program also need to use NSTask to manipulate these files. However, the problem is, sometimes filename will contain special characters, for example, /Users/josh/Desktop/Screen Shot 2013-03-19 at 2.05.06 PM.png.

I have to replace space with backslash and space

NSString *urlPath = [[self url] path];
urlPath = [urlPath stringByReplacingOccurrencesOfString:@"(" withString:@"\\("];
urlPath = [urlPath stringByReplacingOccurrencesOfString:@")" withString:@"\\)"];
urlPath = [urlPath stringByReplacingOccurrencesOfString:@" " withString:@"\\ "];

to: /Users/josh/Desktop/Screen\ Shot\ 2013-03-19\ at\ 2.05.06\ PM.png

so that I can manipulate the file properly.

Same for the ( and ). I also need to add backslash before that.

but there are too many special characters. ie.

/Users/josh/Desktop/~!@#$?:<,.>%^&*()_+`-={}[]\|'';.txt

I need to change to:

/Users/josh/Desktop/\~\!@\#\$\?\:\<\,.\>\%^\&\*\(\)_+\`-\=\{\}\[\]\\\|\'\'\;.txt

and not to mention other special characters (ie. accent)

Is there any easy way to put a backslash in front of each special character, as I don't want to keep calling stringByReplacingOccurrencesOfString over and over again.

Mike Abdullah
  • 14,933
  • 2
  • 50
  • 75
Josh
  • 692
  • 2
  • 9
  • 38
  • Based on this SO question: http://stackoverflow.com/questions/2931942/nsstring-backslash-escaping it would appear thats one of the best methods around at the moment :/ – Ryan Poolos Mar 19 '13 at 20:49
  • 1
    Can you give an example of what type of commands you want to run in the `NSTask`? There should be no need to escape anything in file paths as they are normally handed directly as `argv` and undergo no shell espansion. – NSGod Mar 19 '13 at 20:58
  • @NSGod I always thought that escape is needed. ie. if you open the terminal and drag a file which the filename contains a special code, terminal will automatically add the escape backslash. this is how i found out ie. /Users/josh/Desktop/~!@#$?:<,.>%^&*()_+`-={}[]\|'';.txt I need to change to: /Users/josh/Desktop/\~!@#\$\?\:\<\,.>\%^\&*()_+`-\={}[]\\|\'\'\;.txt – Josh Mar 19 '13 at 21:09
  • @rmaddy this is not duplicate. I was asking if there is any other way beside using stringByReplacingOccurrencesOfString over and over again. – Josh Mar 19 '13 at 21:10
  • @Josh NSGod is right. The need for escaping arises from the shell (for instance bash). `NSTask` does not use a shell. If you would escape the paths nobody would unescape them and the files would not be found. – Nikolai Ruhe Mar 19 '13 at 21:11
  • @Josh It is a duplicate question. Both questions ask how to add backslashes to a bunch of characters. Just because you don't like one of the answers doesn't mean it's not a duplicate. Make that big ugly answer into a helpful category method on NSString. Then you get a nice simple way to do what you want. – rmaddy Mar 19 '13 at 21:17
  • I think this question's gist is about how to escape strings for use as an argument to the bash. That's a different use case than the one in the marked-as-duplicate post. So I vote against duplicate. – Nikolai Ruhe Mar 20 '13 at 15:03

3 Answers3

1

I think you may be able to use an NSRegularExpressionSearch search.

It would look something like this

+ (NSString *) addBackslashes: (NSString *) string
{
    // First convert the name string to a pure ASCII string
    NSData *asciiData = [string dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];

    NSString *asciiString = [[[NSString alloc] initWithData:asciiData encoding:NSASCIIStringEncoding] lowercaseString];

    // Define the characters that we will replace
    NSString *searchCharacters = @"PUT IN ALL OF YOUR SPECIAL CHARACTERS HERE";
    // example NSString *searchCharacters = @"!@#$%&*()";

    // replace them
    NSString *regExPattern = [NSString stringWithFormat:@"[%@]", searchCharacters];

    string = [asciiString stringByReplacingOccurrencesOfString:regExPattern withString: [NSString stringWithFormat:@"\\%@", regExPattern] options:NSRegularExpressionSearch range:NSMakeRange(0, asciiString.length)];
    return string;
}
Dima
  • 23,484
  • 6
  • 56
  • 83
  • using regular expression is also a good way, it's much clearer than calling stringByReplacingOccurrencesOfString numerous times, thanks – Josh Mar 21 '13 at 13:09
1

As described in NSTask's documentation for the setArguments: method, there should be no need to do special quoting:

Discussion

The NSTask object converts both path and the strings in arguments to appropriate C-style strings (using fileSystemRepresentation) before passing them to the task via argv[]. The strings in arguments do not undergo shell expansion, so you do not need to do special quoting, and shell variables, such as $PWD, are not resolved.

If you feel it is necessary, can you please provide some examples of the commands you want to run in the NSTask?

[UPDATE]: I see in the comments that you indeed are using the NSTask to execute a bash shell with -c, which I had wondered about. I've generally used NSTask to execute the command directly rather than going through the shell, like this:

NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:@"/bin/ls"];
[task setArguments:[NSArray arrayWithObjects:@"-l", self.url.path, nil]];

Can you give a more accurate example of the actual command you want to run? For example, are you piping a series of commands together? Perhaps there might be an alternate way to achieve the same results without the need for using the bash shell...

NSGod
  • 22,699
  • 3
  • 58
  • 66
  • NSTask *task = [[NSTask alloc] init]; [task setLaunchPath:@"/bin/bash"]; NSString *argument = [NSString stringWithFormat:@"ls -l %@", self.url.path]; [task setArguments:[NSArray arrayWithObjects:@"-c", argument, nil]]; – Josh Mar 19 '13 at 21:19
  • so if I just pass self.url.path without adding backslash, it won't work. But as I said there are too many characters need to add backslash and keep using stringByReplacingOccurrencesOfString seems not efficient – Josh Mar 19 '13 at 21:20
  • 1
    A better solution would be not to use the shell. Why not "/bin/ls" as launch path and ["-l", self.url.path] as launch arguments? – Martin R Mar 19 '13 at 21:30
  • I remember reading an article from stackoverflow.com that, use /bin/bash if the NSTask has too many arguments. ie. | grep or | awk. That's why I use /bin/bash here. Some of the argument of my NSTask is short (ie. ls -l) but some involves grep and awk. – Josh Mar 19 '13 at 21:46
  • I guess I will use a few NSTask instead of using bash. Thanks for your help – Josh Mar 20 '13 at 14:42
0

you could maintain a set of strings that need to be escaped and use NSScanner to build the new string by iterating the the source string and each time a problematic character is found u first add \\ to a destination string and continue coping the next chars.

NSString *sourceString = @"/Users/josh/Desktop/\"Screen Shot\" 2013-03-19 at 2\\05\\06 PM.png";
NSMutableString *destString = [@"" mutableCopy];
NSCharacterSet *escapeCharsSet = [NSCharacterSet characterSetWithCharactersInString:@" ()\\"];

NSScanner *scanner = [NSScanner scannerWithString:sourceString];
while (![scanner isAtEnd]) {
    NSString *tempString;
    [scanner scanUpToCharactersFromSet:escapeCharsSet intoString:&tempString];
    if([scanner isAtEnd]){
        [destString appendString:tempString];
    }
    else {
        [destString appendFormat:@"%@\\%@", tempString, [sourceString substringWithRange:NSMakeRange([scanner scanLocation], 1)]];
        [scanner setScanLocation:[scanner scanLocation]+1];
    }
}

NSLog(@"\n%@\n%@", sourceString, destString);

result:

/Users/josh/Desktop/Screen Shot 2013-03-19 at 2.05.06 PM.png
/Users/josh/Desktop/Screen\ Shot\ 2013-03-19\ at\ 2.05.06\ PM.png
vikingosegundo
  • 52,040
  • 14
  • 137
  • 178