Thực thi lệnh đầu cuối từ ứng dụng Cacao


204

Làm cách nào tôi có thể thực thi lệnh đầu cuối (như grep) từ ứng dụng Objective-C Ca cao của mình?


2
Tôi chỉ nói rõ ràng: với hộp cát, bạn không thể khởi động các ứng dụng không có trong hộp cát của mình VÀ chúng cần được bạn ký để cho phép điều này
Daij-Djan 27/8/2015

@ Daij-Djan hoàn toàn không đúng, ít nhất là không phải trong macOS. Một ứng dụng macOS có hộp cát có thể chạy bất kỳ nhị phân nào ở những nơi như /usr/binnơi grepsống.
jeff-h

Không. Vui lòng chứng minh tôi sai;) trên ist nstask sẽ không chạy bất cứ thứ gì không có trong hộp cát của bạn.
Daij-Djan

Câu trả lời:


282

Bạn có thể sử dụng NSTask. Đây là một ví dụ sẽ chạy ' /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được sử dụng để chuyển hướng đầu ra tiêu chuẩn của nhiệm vụ.

Để biết thêm thông tin chi tiết về việc tương tác với hệ điều hành từ trong ứng dụng Objective-C của bạn, bạn có thể xem tài liệu này trên Trung tâm phát triển của Apple: Tương tác với Hệ điều hành .

Chỉnh sửa: Bao gồm sửa lỗi cho vấn đề NSLog

Nếu bạn đang sử dụng NSTask để chạy tiện ích dòng lệnh thông qua bash, thì bạn cần bao gồm dòng ma thuật này để giữ cho NSLog hoạt động:

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

Một lời giải thích có tại đây: https://web.archive.org/web/20141121094204/https://cocoadev.com/HowToPipeCommandsWithNSTask


1
Yup, 'argument = [NSArray mảngWithObjects: @ "- e", @ "foo", @ "bar.txt", nil];'
Gordon Wilson

14
Có một trục trặc nhỏ trong câu trả lời của bạn. NSPipe có bộ đệm (được đặt ở cấp HĐH), được xóa khi đọc. Nếu bộ đệm đầy, NSTask sẽ bị treo và ứng dụng của bạn cũng sẽ bị treo, vô thời hạn. Không có thông báo lỗi sẽ xuất hiện. Điều này có thể xảy ra nếu NSTask trả về nhiều thông tin. Giải pháp là sử dụng NSMutableData *data = [NSMutableData dataWithCapacity:512];. Sau đó , while ([task isRunning]) { [data appendData:[file readDataToEndOfFile]]; }. Và tôi "tin" bạn nên có thêm một lần nữa [data appendData:[file readDataToEndOfFile]];sau khi thoát khỏi vòng lặp while.
Dave Gallagher

2
Lỗi sẽ không xuất hiện trừ khi bạn làm điều này (chúng chỉ được in trong nhật ký): [task setSt ChuẩnError: pipe];
Mike Sprague

1
Điều này có thể được cập nhật với ARC và với các mảng bằng Obj-C. Ví dụ: pastebin.com/sRvs3CqD
bames53

1
Đó cũng là một ý tưởng tốt để khắc phục lỗi. task.standardError = pipe;
vqdave

43

Bài viết của kent đã cho tôi một ý tưởng mới. phương thức runCommand này không cần tệp script, chỉ chạy một dòng lệnh:

- (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;
}

Bạn có thể sử dụng phương pháp này như thế này:

NSString *output = runCommand(@"ps -A | grep mysql");

1
Điều này xử lý tốt hầu hết các trường hợp, nhưng nếu bạn chạy nó trong một vòng lặp, cuối cùng nó sẽ phát sinh một ngoại lệ do có quá nhiều xử lý tệp mở. Có thể được sửa bằng cách thêm: [file closeFile]; sau khi đọcDataToEndOfFile.
David Stein

@DavidStein: Tôi nghĩ rằng sử dụng autoreleasepool để bọc phương thức runCommand dường như là đúng hơn. Trên thực tế, mã trên không xem xét cả phi ARC.
Kenial

@Kenial: Ồ, đó là một giải pháp tốt hơn nhiều. Nó cũng giải phóng các tài nguyên kịp thời khi rời khỏi phạm vi.
David Stein

/ bin / ps: Hoạt động không được phép, tôi không nhận được bất kỳ thành công nào, dẫn đầu?
Naman Vaishnav

40

trên tinh thần chia sẻ ... đây là phương pháp tôi thường xuyên sử dụng để chạy shell script. bạn có thể thêm tập lệnh vào gói sản phẩm của mình (trong giai đoạn sao chép của bản dựng) và sau đó để tập lệnh được đọc và chạy khi chạy. lưu ý: mã này tìm tập lệnh trong đường dẫn con privateFrameworks. cảnh báo: đây có thể là rủi ro bảo mật cho các sản phẩm được triển khai, nhưng đối với sự phát triển nội bộ của chúng tôi, đây là một cách dễ dàng để tùy chỉnh những thứ đơn giản (như máy chủ nào để rsync thành ...) mà không cần biên dịch lại ứng dụng, mà chỉ cần chỉnh sửa kịch bản shell trong gói.

//------------------------------------------------------
-(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);    
}
//------------------------------------------------------

Chỉnh sửa: Bao gồm sửa lỗi cho vấn đề NSLog

Nếu bạn đang sử dụng NSTask để chạy tiện ích dòng lệnh thông qua bash, thì bạn cần bao gồm dòng ma thuật này để giữ cho NSLog hoạt động:

//The magic line that keeps your log where it belongs
[task setStandardInput:[NSPipe pipe]];

Trong ngữ cảnh:

NSPipe *pipe;
pipe = [NSPipe pipe];
[task setStandardOutput: pipe];
//The magic line that keeps your log where it belongs
[task setStandardInput:[NSPipe pipe]];

Một lời giải thích có ở đây: http://www.cocoadev.com/index.pl?NSTask


2
Các liên kết giải thích là chết.
Jonny

Tôi muốn chạy lệnh này "system_profiler SPAppluggestDataType -xml" nhưng tôi gặp lỗi này "đường dẫn khởi chạy không truy cập được"
Vikas Bansal

25

Đây là cách thực hiện trong Swift

Thay đổi cho Swift 3.0:

  • NSPipe đã được đổi tên Pipe

  • NSTask đã được đổi tên Process


Điều này dựa trên câu trả lời Objective-C của inkit ở trên. Anh ấy đã viết nó như một thể loại trên NSString- Đối với Swift, nó trở thành một phần mở rộng của String.

tiện ích mở rộng String.runAsCommand () -> Chuỗi

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

Sử dụng:

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

    hoặc chỉ:

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

Thí dụ:

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

}

Lưu ý Processkết quả khi đọc từ Pipelà một NSStringđối tượng. Nó có thể là một chuỗi lỗi và nó cũng có thể là một chuỗi rỗng, nhưng nó phải luôn là một chuỗi NSString.

Vì vậy, miễn là nó không phải là con số không, kết quả có thể được chuyển thành Swift Stringvà được trả về.

Nếu vì một lý do nào đó, không có gì NSStringcó thể được khởi tạo từ dữ liệu tệp, hàm sẽ trả về một thông báo lỗi. Hàm này có thể đã được viết để trả về một tùy chọn String?, nhưng điều đó sẽ gây khó xử khi sử dụng và sẽ không phục vụ mục đích hữu ích vì điều này rất khó xảy ra.


1
Cách thực sự tốt đẹp và thanh lịch! Câu trả lời này nên có nhiều upvote.
XueYu

Nếu bạn không cần đầu ra. Thêm đối số @discardableResult trước hoặc trên phương thức runCommand. Điều này sẽ cho phép bạn gọi phương thức mà không cần phải đặt nó vào một biến.
Lloyd Keijzer

let result = String (byte: fileHandle.readDataToEndOfFile (), mã hóa: String.Encoding.utf8) là ok
cleexiang

18

Mục tiêu-C (xem bên dưới cho Swift)

Làm sạch mã trong câu trả lời hàng đầu để làm cho nó dễ đọc hơn, ít dư thừa hơn, thêm các lợi ích của phương pháp một dòng và được tạo thành một thể loại NSString

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

Thực hiện:

@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

Sử dụng:

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

nếu bạn gặp vấn đề với mã hóa đầu ra:

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

Hy vọng nó hữu ích cho bạn vì nó sẽ tương lai với tôi. (Chào bạn!)


Swift 4

Dưới đây là một ví dụ Swift làm sử dụng Pipe, ProcessString

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

Sử dụng:

let output = "echo hello".run()

2
Thật vậy, mã của bạn rất hữu ích với tôi! Tôi đã đổi nó thành Swift và đăng nó dưới dạng một câu trả lời khác bên dưới.
ElmerCat

14

ngã ba , exec , và chờ đợi nên làm việc, nếu bạn không thực sự tìm kiếm một cụ thể cách Objective-C. forktạo một bản sao của chương trình hiện đang chạy, execthay thế chương trình hiện đang chạy bằng chương trình mới và waitchờ quá trình con thoát ra. Ví dụ: (không có bất kỳ kiểm tra lỗi nào):

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

Ngoài ra còn có hệ thống , chạy lệnh như thể bạn gõ nó từ dòng lệnh của shell. Nó đơn giản hơn, nhưng bạn có ít quyền kiểm soát tình huống hơn.

Tôi giả sử bạn đang làm việc trên một ứng dụng Mac, vì vậy các liên kết đến tài liệu của Apple cho các chức năng này, nhưng chúng là tất cả POSIX, vì vậy bạn nên sử dụng chúng trên bất kỳ hệ thống nào tuân thủ POSIX.


Tôi biết đây là một câu trả lời rất cũ nhưng tôi cần phải nói điều này: đây là một cách tuyệt vời để sử dụng các tiêu đề để xử lý việc đào thải. Nhược điểm duy nhất là nó tạo ra một bản sao của toàn bộ chương trình. Vì vậy, đối với một ứng dụng ca cao, tôi sẽ sử dụng @GordonWilson cho một ứng dụng aproach đẹp hơn và nếu tôi đang làm việc trên một ứng dụng dòng lệnh thì đây là cách tốt nhất để làm điều đó. cảm ơn (xin lỗi tiếng anh không tốt của tôi)
Nicos Karalis

11

Ngoài ra còn có hệ thống POSIX cũ tốt ("echo -en '\ 007'");


6
KHÔNG CHẠY QUY TẮC NÀY. (Trong trường hợp bạn không biết lệnh này làm gì)
justin

4
Thay đổi nó thành một cái gì đó an toàn hơn một chút (tiếng bíp)
nes1983

Điều này sẽ không gây ra lỗi trong giao diện điều khiển? Incorrect NSStringEncoding value 0x0000 detected. Assuming NSStringEncodingASCII. Will stop this compatibility mapping behavior in the near future.
cwd

1
Hừm. Có lẽ bạn phải thoát khỏi dấu gạch chéo ngược.
nes1983

chỉ cần chạy / usr / bin / echo hoặc một cái gì đó. rm -rf rất khắc nghiệt và unicode trong giao diện điều khiển vẫn còn tệ :)
Maxim Veksler

8

Tôi đã viết hàm "C" này, vì NSTasknó đáng ghét ..

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

ồ, và vì lợi ích của việc hoàn thành / không mơ hồ

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

Nhiều năm sau, Cvẫn là một mớ hỗn độn hoang mang, với tôi .. và với một chút niềm tin vào khả năng của mình để sửa chữa khuyết điểm tổng của tôi ở trên - Chi nhánh Tôi đề nghị chỉ ô liu là một phiên bản rezhuzhed của câu trả lời @ inket rằng là barest xương , cho đồng bào của tôi những người theo chủ nghĩa thuần túy / dài dòng ...

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 không được xác định trên bất kỳ lỗi nào, chars_read quá nhỏ so với giá trị trả về của fread () trên bất kỳ kiến ​​trúc nào trong đó sizeof (ssize_t)! = sizeof (int), nếu chúng ta muốn đầu ra nhiều hơn các byte BUFSIZ thì sao? Nếu đầu ra không phải là UTF-8 thì sao? Điều gì xảy ra nếu pclose () trả về lỗi? Làm thế nào để chúng tôi báo cáo lỗi của fread ()?
ObjectiveC-oder

@ ObjectiveC-oder D'oh - Tôi không biết. Xin vui lòng, cho tôi biết (như trong .. chỉnh sửa đi)!
Alex Gray

3

Custos Mortem nói:

Tôi ngạc nhiên không ai thực sự gặp phải vấn đề chặn / không chặn cuộc gọi

Để chặn / không chặn các sự cố cuộc gọi liên quan đến NSTaskđọc bên dưới:

asynctask.m - mã mẫu cho biết cách triển khai các luồng stdin, stdout & stderr không đồng bộ để xử lý dữ liệu với NSTask

Mã nguồn của asynctask.m có sẵn tại GitHub .


Xem đóng góp của tôi cho phiên bản không chặn
Guruniverse

3

Ngoài một số câu trả lời xuất sắc ở trên, tôi sử dụng đoạn mã sau để xử lý đầu ra của lệnh trong nền và tránh cơ chế chặn của [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];
}

Đối với tôi, dòng tạo ra tất cả sự khác biệt là [tự biểu diễnSelectorInBackground: @selector (collTaskOutput :) withObject: file];
neowinston

2

Hoặc vì Objective C chỉ là C với một số lớp OO ở trên, bạn có thể sử dụng các conterparts 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[]); 

Chúng được bao gồm từ tệp tiêu đề unistd.h.


2

Nếu lệnh Terminal yêu cầu Đặc quyền quản trị viên (aka sudo), AuthorizationExecuteWithPrivilegesthay vào đó hãy sử dụng . Sau đây sẽ tạo một tệp có tên "com.stackoverflow.test" là thư mục gốc "/ 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
Điều này đã chính thức bị từ chối kể từ OS X 10.7
Sam Washburn

2
.. nhưng nó vẫn hoạt động bất kể, vì đó là cách duy nhất để làm điều này, và nhiều người cài đặt dựa vào điều đó, tôi tin.
Thomas Tempelmann
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.