Execute a terminal command from a Cocoa app

后端 未结 12 2141
既然无缘
既然无缘 2020-11-22 06:34

How can I execute a terminal command (like grep) from my Objective-C Cocoa application?

相关标签:
12条回答
  • 2020-11-22 06:48

    Objective-C (see below for Swift)

    Cleaned up the code in the top answer to make it more readable, less redundant, added the benefits of the one-line method and made into an NSString category

    @interface NSString (ShellExecution)
    - (NSString*)runAsCommand;
    @end
    

    Implementation:

    @implementation NSString (ShellExecution)
    
    - (NSString*)runAsCommand {
        NSPipe* pipe = [NSPipe pipe];
    
        NSTask* task = [[NSTask alloc] init];
        [task setLaunchPath: @"/bin/sh"];
        [task setArguments:@[@"-c", [NSString stringWithFormat:@"%@", self]]];
        [task setStandardOutput:pipe];
    
        NSFileHandle* file = [pipe fileHandleForReading];
        [task launch];
    
        return [[NSString alloc] initWithData:[file readDataToEndOfFile] encoding:NSUTF8StringEncoding];
    }
    
    @end
    

    Usage:

    NSString* output = [@"echo hello" runAsCommand];
    

    And if you're having problems with output encoding:

    // Had problems with `lsof` output and Japanese-named files, this fixed it
    NSString* output = [@"export LANG=en_US.UTF-8;echo hello" runAsCommand];
    

    Hope it's as useful to you as it will be to future me. (Hi, you!)


    Swift 4

    Here's a Swift example making use of Pipe, Process, and String

    extension String {
        func run() -> String? {
            let pipe = Pipe()
            let process = Process()
            process.launchPath = "/bin/sh"
            process.arguments = ["-c", self]
            process.standardOutput = pipe
    
            let fileHandle = pipe.fileHandleForReading
            process.launch()
    
            return String(data: fileHandle.readDataToEndOfFile(), encoding: .utf8)
        }
    }
    

    Usage:

    let output = "echo hello".run()
    
    0 讨论(0)
  • 2020-11-22 06:49

    I wrote this "C" function, because NSTask is obnoxious..

    NSString * runCommand(NSString* c) {
    
        NSString* outP; FILE *read_fp;  char buffer[BUFSIZ + 1];
        int chars_read; memset(buffer, '\0', sizeof(buffer));
        read_fp = popen(c.UTF8String, "r");
        if (read_fp != NULL) {
            chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
            if (chars_read > 0) outP = $UTF8(buffer);
            pclose(read_fp);
        }   
        return outP;
    }
    
    NSLog(@"%@", runCommand(@"ls -la /")); 
    
    total 16751
    drwxrwxr-x+ 60 root        wheel     2108 May 24 15:19 .
    drwxrwxr-x+ 60 root        wheel     2108 May 24 15:19 ..
    …
    

    oh, and for the sake of being complete / unambiguous…

    #define $UTF8(A) ((NSString*)[NSS stringWithUTF8String:A])
    

    Years later, C is still a bewildering mess, to me.. and with little faith in my ability to correct my gross shortcomings above - the only olive branch I offer is a rezhuzhed version of @inket's answer that is barest of bones, for my fellow purists / verbosity-haters...

    id _system(id cmd) { 
       return !cmd ? nil : ({ NSPipe* pipe; NSTask * task;
      [task = NSTask.new setValuesForKeysWithDictionary: 
        @{ @"launchPath" : @"/bin/sh", 
            @"arguments" : @[@"-c", cmd],
       @"standardOutput" : pipe = NSPipe.pipe}]; [task launch];
      [NSString.alloc initWithData:
         pipe.fileHandleForReading.readDataToEndOfFile
                          encoding:NSUTF8StringEncoding]; });
    }
    
    0 讨论(0)
  • 2020-11-22 06:54

    Here's how to do it in Swift

    Changes for Swift 3.0:

    • NSPipe has been renamed Pipe

    • NSTask has been renamed Process


    This is based on inkit's Objective-C answer above. He wrote it as a category on NSString — For Swift, it becomes an extension of String.

    extension  String.runAsCommand()  ->  String

    extension String {
        func runAsCommand() -> String {
            let pipe = Pipe()
            let task = Process()
            task.launchPath = "/bin/sh"
            task.arguments = ["-c", String(format:"%@", self)]
            task.standardOutput = pipe
            let file = pipe.fileHandleForReading
            task.launch()
            if let result = NSString(data: file.readDataToEndOfFile(), encoding: String.Encoding.utf8.rawValue) {
                return result as String
            }
            else {
                return "--- Error running command - Unable to initialize string from file data ---"
            }
        }
    }
    

    Usage:

    let input = "echo hello"
    let output = input.runAsCommand()
    print(output)                        // prints "hello"
    

        or just:

    print("echo hello".runAsCommand())   // prints "hello" 
    

    Example:

    @IBAction func toggleFinderShowAllFiles(_ sender: AnyObject) {
    
        var newSetting = ""
        let readDefaultsCommand = "defaults read com.apple.finder AppleShowAllFiles"
    
        let oldSetting = readDefaultsCommand.runAsCommand()
    
        // Note: the Command results are terminated with a newline character
    
        if (oldSetting == "0\n") { newSetting = "1" }
        else { newSetting = "0" }
    
        let writeDefaultsCommand = "defaults write com.apple.finder AppleShowAllFiles \(newSetting) ; killall Finder"
    
        _ = writeDefaultsCommand.runAsCommand()
    
    }
    

    Note the Process result as read from the Pipe is an NSString object. It might be an error string and it can also be an empty string, but it should always be an NSString.

    So, as long as it's not nil, the result can cast as a Swift String and returned.

    If for some reason no NSString at all can be initialized from the file data, the function returns an error message. The function could have been written to return an optional String?, but that would be awkward to use and wouldn't serve a useful purpose because it's so unlikely for this to occur.

    0 讨论(0)
  • 2020-11-22 06:56

    You can use NSTask. Here's an example that would run '/usr/bin/grep foo bar.txt'.

    int pid = [[NSProcessInfo processInfo] processIdentifier];
    NSPipe *pipe = [NSPipe pipe];
    NSFileHandle *file = pipe.fileHandleForReading;
    
    NSTask *task = [[NSTask alloc] init];
    task.launchPath = @"/usr/bin/grep";
    task.arguments = @[@"foo", @"bar.txt"];
    task.standardOutput = pipe;
    
    [task launch];
    
    NSData *data = [file readDataToEndOfFile];
    [file closeFile];
    
    NSString *grepOutput = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
    NSLog (@"grep returned:\n%@", grepOutput);
    

    NSPipe and NSFileHandle are used to redirect the standard output of the task.

    For more detailed information on interacting with the operating system from within your Objective-C application, you can see this document on Apple's Development Center: Interacting with the Operating System.

    Edit: Included fix for NSLog problem

    If you are using NSTask to run a command-line utility via bash, then you need to include this magic line to keep NSLog working:

    //The magic line that keeps your log where it belongs
    task.standardOutput = pipe;
    

    An explanation is here: https://web.archive.org/web/20141121094204/https://cocoadev.com/HowToPipeCommandsWithNSTask

    0 讨论(0)
  • 2020-11-22 06:56

    There is also good old POSIX system("echo -en '\007'");

    0 讨论(0)
  • 2020-11-22 06:58

    fork, exec, and wait should work, if you're not really looking for a Objective-C specific way. fork creates a copy of the currently running program, exec replaces the currently running program with a new one, and wait waits for the subprocess to exit. For example (without any error checking):

    #include <stdlib.h>
    #include <unistd.h>
    


    pid_t p = fork();
    if (p == 0) {
        /* fork returns 0 in the child process. */
        execl("/other/program/to/run", "/other/program/to/run", "foo", NULL);
    } else {
        /* fork returns the child's PID in the parent. */
        int status;
        wait(&status);
        /* The child has exited, and status contains the way it exited. */
    }
    
    /* The child has run and exited by the time execution gets to here. */
    

    There's also system, which runs the command as if you typed it from the shell's command line. It's simpler, but you have less control over the situation.

    I'm assuming you're working on a Mac application, so the links are to Apple's documentation for these functions, but they're all POSIX, so you should be to use them on any POSIX-compliant system.

    0 讨论(0)
提交回复
热议问题