从Cocoa应用程序执行终端命令

211

我该如何从我的Objective-C Cocoa应用程序中执行终端命令(例如grep)?


2
我只是在说显而易见的事情:使用沙盒技术,你不能启动不在你的沙盒中的应用程序,而且它们需要由你签名才能允许这样做。 - Daij-Djan
1
@Daij-Djan,这完全不正确,至少在macOS上不是这样。一个沙盒化的macOS应用程序可以运行位于/usr/bin等位置的任何二进制文件,例如grep - jeff-h
1
不,你要证明我错了 ;) 在 NSTask 中,将无法运行不在你的沙盒中的任何内容。 - Daij-Djan
12个回答

289

你可以使用NSTask。下面是一个示例,运行命令'/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);
NSPipeNSFileHandle 用于重定向任务的标准输出。如需更详细的关于在 Objective-C 应用程序中与操作系统交互的信息,请参阅苹果开发者中心上的此文档:与操作系统交互
编辑:包含解决 NSLog 问题的修复方法。
如果您正在使用 NSTask 通过 bash 运行命令行实用程序,那么需要添加以下 "magic line" 以使 NSLog 正常工作:
//The magic line that keeps your log where it belongs
task.standardOutput = pipe;

这里有一个解释:https://web.archive.org/web/20141121094204/https://cocoadev.com/HowToPipeCommandsWithNSTask


16
你的回答中有一个小错误。NSPipe具有缓冲区(在操作系统级别设置),当读取它时会被清空。如果缓冲区已满,NSTask将会挂起,并且你的应用程序也会无限期地挂起。不会出现任何错误消息。如果NSTask返回了大量信息,这种情况可能会发生。解决方法是使用NSMutableData *data = [NSMutableData dataWithCapacity:512];。然后,while ([task isRunning]) { [data appendData:[file readDataToEndOfFile]]; }。我"相信"在while循环结束后还应该再加上一句[data appendData:[file readDataToEndOfFile]] - Dave
3
只有当你执行这个操作时,错误才会出现(它们只会在日志中被打印出来): [task setStandardError:pipe]; - Mike Sprague
1
Dave的评论很好。但是,你可能想使用availableData而不是readDataToEndOfFile,因为当没有数据时,availableData会阻塞。如果被执行的命令消耗大量CPU并且输出很少(例如gcc),这一点非常重要。 - digory doo
1
这可以使用ARC和Obj-C数组字面量进行更新。例如:http://pastebin.com/sRvs3CqD - bames53
1
cocoadev.com链接出现http 404错误 :( - GBF_Gabriel
显示剩余7条评论

46

kent的文章给了我一个新想法。这个runCommand方法不需要一个脚本文件,只需通过一行命令运行:

- (NSString *)runCommand:(NSString *)commandToRun
{
    NSTask *task = [[NSTask alloc] init];
    [task setLaunchPath:@"/bin/sh"];

    NSArray *arguments = [NSArray arrayWithObjects:
                          @"-c" ,
                          [NSString stringWithFormat:@"%@", commandToRun],
                          nil];
    NSLog(@"run command:%@", commandToRun);
    [task setArguments:arguments];

    NSPipe *pipe = [NSPipe pipe];
    [task setStandardOutput:pipe];

    NSFileHandle *file = [pipe fileHandleForReading];

    [task launch];

    NSData *data = [file readDataToEndOfFile];

    NSString *output = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    return output;
}
你可以这样使用这个方法:
NSString *output = runCommand(@"ps -A | grep mysql");

1
这个处理方式大多数情况下都很好,但如果你在一个循环中运行它,由于打开的文件句柄太多,最终会引发异常。可以通过在readDataToEndOfFile之后添加[file closeFile];来解决。 - David Stein
@DavidStein:我认为使用autoreleasepool来包装runCommand方法似乎更好。实际上,上面的代码也没有考虑非ARC。 - Kenial
@Kenial:哦,那是一个更好的解决方案。离开作用域后它也会立即释放资源。 - David Stein
/bin/ps:操作不允许,我没有获得任何成功,领导? - Naman Vaishnav

40

分享精神驱动...这是我经常用来运行shell脚本的方法。 你可以将一个脚本添加到你的产品捆绑包中(在构建的复制阶段),然后在运行时读取和执行该脚本。注意:此代码会在privateFrameworks子路径中查找脚本。 警告:这可能会对部署的产品造成安全风险,但对于我们内部开发来说,这是一种简单的定制简单事物的方法(比如要将内容同步到哪个主机...),无需重新编译应用程序,只需编辑捆绑包中的shell脚本。

//------------------------------------------------------
-(void) runScript:(NSString*)scriptName
{
    NSTask *task;
    task = [[NSTask alloc] init];
    [task setLaunchPath: @"/bin/sh"];

    NSArray *arguments;
    NSString* newpath = [NSString stringWithFormat:@"%@/%@",[[NSBundle mainBundle] privateFrameworksPath], scriptName];
    NSLog(@"shell script path: %@",newpath);
    arguments = [NSArray arrayWithObjects:newpath, nil];
    [task setArguments: arguments];

    NSPipe *pipe;
    pipe = [NSPipe pipe];
    [task setStandardOutput: pipe];

    NSFileHandle *file;
    file = [pipe fileHandleForReading];

    [task launch];

    NSData *data;
    data = [file readDataToEndOfFile];

    NSString *string;
    string = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
    NSLog (@"script returned:\n%@", string);    
}
//------------------------------------------------------

编辑:修复了NSLog的问题

如果你正在使用NSTask通过bash运行命令行工具,那么你需要包含这个“魔法”代码行以保持NSLog正常工作:

//The magic line that keeps your log where it belongs
[task setStandardInput:[NSPipe pipe]];
抱歉,我只能使用英语进行交流和回答问题。
NSPipe *pipe;
pipe = [NSPipe pipe];
[task setStandardOutput: pipe];
//The magic line that keeps your log where it belongs
[task setStandardInput:[NSPipe pipe]];

这里有一个解释:http://www.cocoadev.com/index.pl?NSTask


2
说明链接已失效。 - Jonny
我想运行这个命令 "system_profiler SPApplicationsDataType -xml",但是我得到了这个错误 "launch path not accessible"。 - Vikas Bansal

27

以下是在Swift中的操作方法

Swift 3.0的更改:

  • NSPipe已更名为Pipe

  • NSTask已更名为Process


这是基于inkit之前Objective-C的回答。他将其编写为NSStringcategory — 对于Swift,它成为了Stringextension

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 ---"
        }
    }
}

使用方法:

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

    或者只需要:

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

例子:

@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()

}
请注意,从Pipe读取的Process结果是一个NSString对象。它可能是一个错误字符串,也可能是一个空字符串,但它应该始终是一个NSString
因此,只要不是nil,结果可以转换为Swift String并返回。
如果出于某种原因根本无法从文件数据初始化任何NSString,则函数将返回一个错误消息。该函数本可以编写成返回可选的String?,但这样使用起来很笨拙,并且没有任何有用的目的,因为这种情况发生的可能性非常小。

1
真的是非常好而且优雅的方式!这个答案应该有更多的赞。 - XueYu
如果你不需要输出,可以在runCommand方法之前或之上添加@discardableResult参数。这将允许你调用该方法而无需将其放入变量中。 - Lloyd Keijzer
让result等于String(bytes: fileHandle.readDataToEndOfFile(), encoding: String.Encoding.utf8)是可以的。 - cleexiang

21

Objective-C(参见下方的Swift)

清理了顶部答案中的代码,使其更易读、更少冗余,并添加了一行方法的优点,并将其制作成了NSString类别。

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

实现:

@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

使用方法:

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

如果您遇到输出编码问题:那么

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

希望它对你有用,就像对未来的我一样。 (嗨,你!)


Swift 4

这是一个使用 PipeProcessString 的 Swift 示例。

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)
    }
}

使用方法:

let output = "echo hello".run()

2
确实,你的代码对我非常有用!我将它改成了Swift并在下面发布了另一个答案。 - ElmerCat

15

forkexecwait可以使用,如果你不是真正寻求 Objective-C 特定的方法。fork 会创建一个当前运行程序的副本,exec 将当前运行的程序替换为新的程序,wait 等待子进程退出。例如(没有任何错误检查):

#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. */

还有 system 函数,它会像在终端输入命令一样运行命令。这种方法更简单易用,但你对情况的掌控能力较小。

我假设你正在开发一款Mac应用程序,所以这些链接指向苹果提供的函数文档,但它们都是POSIX规范,所以你可以在任何符合POSIX标准的系统上使用它们。


我知道这是一个非常老的答案,但我必须说:这是一种使用线程处理执行的绝佳方式。唯一的缺点是它会创建整个程序的副本。因此,对于Cocoa应用程序,我会选择@GordonWilson提供的更好的方法,如果我正在开发命令行应用程序,则这是最好的方法。谢谢(抱歉我的英语不好)。 - Nicos Karalis

11

同样还有经典的POSIX system("echo -en '\007'");


6
不要运行此命令。(如果您不知道此命令的作用) - justin
4
将其改为稍微更安全的东西...(会发出哔哔声) - nes1983
这会不会在控制台中抛出错误?“检测到不正确的NSStringEncoding值0x0000。假设NSStringEncodingASCII。将来会停止此兼容性映射行为。” - cwd
1
嗯,也许你需要双重转义反斜杠。 - nes1983
只需运行 /usr/bin/echo 或其他命令即可。rm -rf 命令太过严厉,而控制台中的 Unicode 仍然不够完善 :) - Maxim Veksler

9

我写了这个"C"函数,因为NSTask让人感到很讨厌。

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 ..
…

为了完整/明确起见......

还有IT技术相关内容,请您提供需要翻译的具体内容。
#define $UTF8(A) ((NSString*)[NSS stringWithUTF8String:A])

多年过去了,对我来说,C仍然是一个令人困惑的混乱,而且我对于纠正上述严重缺点的能力没有太多信心 - 我唯一能提供的橄榄枝是@inket答案的简化版本,它只有最基本的要素,献给我那些追求纯粹/厌恶冗长的同行们......

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]; });
}

1
在任何错误情况下,outP未定义,chars_read对于fread()的返回值来说太小了,在sizeof(ssize_t) != sizeof(int)的任何架构上都是如此。如果我们想要比BUFSIZ字节更多的输出怎么办?如果输出不是UTF-8怎么办?如果pclose()返回一个错误怎么办?我们如何报告fread()的错误? - ObjectiveC-oder
@ObjectiveC-oder D'oh - 我不知道。请告诉我(比如说..修改一下)! - Alex Gray

5

除了上面几个出色的回答之外,我使用以下代码来处理命令输出的后台进程,并避免[file readDataToEndOfFile]的阻塞机制。

- (void)runCommand:(NSString *)commandToRun
{
    NSTask *task = [[NSTask alloc] init];
    [task setLaunchPath:@"/bin/sh"];

    NSArray *arguments = [NSArray arrayWithObjects:
                          @"-c" ,
                          [NSString stringWithFormat:@"%@", commandToRun],
                          nil];
    NSLog(@"run command:%@", commandToRun);
    [task setArguments:arguments];

    NSPipe *pipe = [NSPipe pipe];
    [task setStandardOutput:pipe];

    NSFileHandle *file = [pipe fileHandleForReading];

    [task launch];

    [self performSelectorInBackground:@selector(collectTaskOutput:) withObject:file];
}

- (void)collectTaskOutput:(NSFileHandle *)file
{
    NSData      *data;
    do
    {
        data = [file availableData];
        NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] );

    } while ([data length] > 0); // [file availableData] Returns empty data when the pipe was closed

    // Task has stopped
    [file closeFile];
}

1
对我来说,让一切变得不同的那行代码是[self performSelectorInBackground:@selector(collectTaskOutput:) withObject:file]; - neowinston
如果您正在尝试使用Swift进行此操作,则NSTask已更名为Process - Sam Soffes

3

Custos Mortem说:

我很惊讶没有人真正涉及阻塞/非阻塞调用问题

有关NSTask的阻塞/非阻塞调用问题,请阅读以下内容:

asynctask.m --示例代码,展示了如何为处理NSTask数据实现异步stdin、stdout和stderr流

asynctask.m的源代码可在GitHub上找到。


请查看我的贡献,其中包含一个非阻塞版本。 - Guruniverse

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接