从Cocoa应用执行终端命令


204

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


2
我只是在说一个显而易见的事实:使用沙箱,您不能只启动不在沙箱中的应用,并且需要您签名才能允许这样做
Daij-Djan 2015年

@ Daij-Djan根本不是真的,至少在macOS中不是。装有沙盒的macOS应用程序可以/usr/bingrep居住的地方运行任何二进制文件。
jeff-h

否。请证明我错了;)在ist nstask上将无法运行沙箱中未运行的任何内容。
Daij-Djan

Answers:


282

您可以使用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);

NSPipe并且NSFileHandle用于重定向任务的标准输出。

有关从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


1
是的,“参数= [NSArray arrayWithObjects:@“-e”,@“ foo”,@“ bar.txt”,nil];“
戈登·威尔逊2009年

14
您的答案有一个小故障。NSPipe具有一个缓冲区(在操作系统级别设置),在读取时将刷新该缓冲区。如果缓冲区已满,NSTask将挂起,您的应用也将无限期挂起。没有错误消息出现。如果NSTask返回大量信息,则可能发生这种情况。解决方法是使用NSMutableData *data = [NSMutableData dataWithCapacity:512];。然后,while ([task isRunning]) { [data appendData:[file readDataToEndOfFile]]; }。我“相信” [data appendData:[file readDataToEndOfFile]];在while循环退出后,您应该再拥有一个。
戴夫·加拉格尔

2
除非您执行此操作,否则错误不会出现(它们只会在日志中显示):[task setStandardError:pipe];
Mike Sprague

1
这可以使用ARC和Obj-C数组文字进行更新。例如,pastebin.com
sRvs3CqD

1
传送错误也是一个好主意。task.standardError = pipe;
vqdave 2014年

43

肯特的文章给了我一个新的主意。这个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
这可以很好地处理大多数情况,但是如果以循环方式运行它,则由于打开的文件句柄过多,最终会引发异常。可以通过添加以下内容来修复:[file closeFile]; 在readDataToEndOfFile之后。
大卫·斯坦

@DavidStein:我认为使用autoreleasepool来包装runCommand方法似乎不是。实际上,以上代码也不会考虑非ARC。
Kenial '16

@Kenial:哦,这是一个更好的解决方案。它还会在离开范围时立即释放资源。
大卫·斯坦

/ 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
说明链接已死。
强尼2014年

我想运行此命令“ system_profiler SPApplicationsDataType -xml”,但出现此错误“无法访问启动路径”
Vikas Bansal 2015年

25

这是在Swift中执行的方法

Swift 3.0的更改:

  • NSPipe 已重命名 Pipe

  • NSTask 已重命名 Process


这是基于上面inkit的Objective-C答案。他写了它作为一个类别NSString-对于斯威夫特,它成为一个扩展String

扩展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()

}

请注意,Process从中读取的结果Pipe是一个NSString对象。它可能是错误字符串,也可能是空字符串,但应始终为NSString

因此,只要不为零,结果就可以转换为Swift String并返回。

如果由于某种原因根本NSString无法从文件数据中初始化任何内容,则该函数将返回错误消息。该函数可以编写为返回一个optional String?,但是使用起来很尴尬,并且没有用处,因为它不太可能发生。


1
真是一种优雅的方式!这个答案应该有更多的支持。

如果不需要输出。在runCommand方法的前面或上方添加@discardableResult参数。这将使您无需将其放在变量中即可调用该方法。
劳埃德·凯伊泽

让结果= String(bytes:fileHandle.readDataToEndOfFile(),编码:String.Encoding.utf8)没问题
cleexiang

18

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

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

14

如果您并不是真的在寻找一种Objective-C特定的方式,那么forkexecwait应该可以工作。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,它可以像在shell命令行中键入命令一样运行命令。这比较简单,但是您对情况的控制较少。

我假设您正在使用Mac应用程序,因此这些功能的链接均指向Apple的文档,但它们都是POSIX,因此您应该在任何POSIX兼容系统上使用它们。


我知道这是一个非常古老的答案,但是我需要说:这是使用trheads处理执行的一种极好的方法。唯一的缺点是它将创建整个程序的副本。因此对于可可应用程序,我将与@GordonWilson一起使用以获得更好的方法,如果我在命令行应用程序上工作,则这是最好的方法。谢谢(对不起,我的英语不好)
Nicos Karalis 2013年

11

还有一个很好的旧POSIX 系统(“ echo -en'\ 007'”);


6
请勿运行此命令。(以防您不知道此命令的作用)
贾斯丁于2009年

4
将其更改为稍微更安全的事物…(它发出哔哔声)
nes1983

这不会在控制台中引发错误吗?Incorrect NSStringEncoding value 0x0000 detected. Assuming NSStringEncodingASCII. Will stop this compatibility mapping behavior in the near future.
cwd

1
嗯 也许您必须两次转义反斜杠。
nes1983

只需运行/ usr / bin / echo之类的东西。rm -rf很苛刻,控制台中的unicode仍然很烂:)
Maxim Veksler

8

我写了这个“ 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]; });
}

1
在发生任何错误时,outP是未定义的,在sizeof(ssize_t)!= sizeof(int)的任何体系结构上,chars_read对于fread()的返回值而言太小,如果我们想要的输出多于BUFSIZ字节怎么办?如果输出不是UTF-8怎么办?如果pclose()返回错误怎么办?我们如何报告fread()错误?
ObjectiveC-oder 2014年

@ ObjectiveC-oder D'oh-我不知道。请告诉我(如在..编辑后)!
亚历克斯·格雷

3

Custos Mortem说:

令我惊讶的是,没有人真正遇到阻塞/非阻塞呼叫问题

有关阻塞/非阻塞呼叫的问题,NSTask请参阅以下内容:

asynctask.m-示例代码,显示如何实现异步stdin,stdout和stderr流以使用NSTask处理数据

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


请参阅我非阻塞版本的贡献
Guruniverse

3

除了上述几个出色的答案外,我还使用以下代码在后台处理命令的输出,并避免了的阻塞机制[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];
}

对我而言,造成所有差异的那一行是[self performSelectorInBackground:@selector(collectTaskOutput :) withObject:file];
neowinston

2

或者,因为目标C只是在顶部具有OO层的C,所以可以使用posix conterparts:

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头文件中。


2

如果“终端”命令需要管理员特权(aka 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); 

4
自OS X 10.7起
Sam Washburn

2
..但无论如何它都可以继续工作,因为这是唯一的方法,我相信许多安装程序都依赖它。
Thomas Tempelmann '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.