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

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

132937 次浏览

你可以使用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应用程序中与操作系统交互的更详细信息,您可以在Apple的开发中心上查看此文档:与操作系统交互

编辑:包括修复NSLog问题

如果你正在使用NSTask通过bash运行命令行实用程序,那么你需要包括这条神奇的行来保持NSLog工作:

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

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

或者因为Objective C只是C,上面有一些OO层,你可以使用posix对等物:

int execl(const char *path, const char *arg0, ..., const char *argn, (char *)0);
int execle(const char *path, const char *arg0, ..., const char *argn, (char *)0, char *const envp[]);
int execlp(const char *file, const char *arg0, ..., const char *argn, (char *)0);
int execlpe(const char *file, const char *arg0, ..., const char *argn, (char *)0, char *const envp[]);
int execv(const char *path, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

它们包含在unistd.h头文件中。

执行,和等待应该工作,如果你不是真的在寻找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. */

还有系统,它运行命令,就像你从shell的命令行输入它一样。这样更简单,但你对情况的控制力更弱。

我假设你在一个Mac应用程序上工作,所以链接是到苹果的这些函数的文档,但它们都是POSIX,所以你应该在任何posix兼容的系统上使用它们。

还有老POSIX 系统("echo -en '\007'");

本着分享的精神…这是我经常使用的一种方法来运行shell脚本。 您可以将一个脚本添加到您的产品包(在构建的复制阶段),然后 在运行时读取并运行脚本。注意:这段代码在privateFrameworks子路径中查找脚本。 警告:对于已部署的产品来说,这可能是一个安全风险,但对于我们的内部开发来说,这是一种简单的自定义方法(比如rsync到哪个主机…),而不需要重新编译应用程序,只需要编辑bundle中的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

验尸官说:

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

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

asynctask。M——示例代码,演示如何实现异步stdin, stdout &用NSTask处理数据的stderr流

asynctask的源代码。m在GitHub上可用。

如果Terminal命令需要管理员权限(又名sudo),请改用AuthorizationExecuteWithPrivileges。 下面将创建一个名为“com.stackoverflow”的文件。

. test是根目录“/System/Library/Caches”
AuthorizationRef authorizationRef;
FILE *pipe = NULL;
OSStatus err = AuthorizationCreate(nil,
kAuthorizationEmptyEnvironment,
kAuthorizationFlagDefaults,
&authorizationRef);


char *command= "/usr/bin/touch";
char *args[] = {"/System/Library/Caches/com.stackoverflow.test", nil};


err = AuthorizationExecuteWithPrivileges(authorizationRef,
command,
kAuthorizationFlagDefaults,
args,
&pipe);

肯特的文章给了我一个新想法。这个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");

我写了这个“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 ..
…

哦,为了完整/明确起见……

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

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

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


斯威夫特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()

下面是如何在Swift中做到这一点

Swift 3.0的变化:

  • NSPipe已被重命名为Pipe

  • NSTask已被重命名为Process


这是基于inkit的Objective-C答案上面。他把它写成< em > < / em >类别NSString - 对于Swift,它变成String< em > < / em >扩展

extension , String.runAsCommand(),, - - - - - - >,字符串

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?,但这将是尴尬的使用,也不会起到有用的作用,因为这种情况不太可能发生。

除了上面的几个很好的答案,我使用下面的代码在后台处理命令的输出,并避免[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];
}