Cocoa 앱에서 터미널 명령 실행


204

grepObjective-C Cocoa 애플리케이션에서 터미널 명령 (예 :)을 어떻게 실행할 수 있습니까?


2
샌드 박스를 사용하면 샌드 박스에없는 앱을 시작할 수 없으며이를 허용하려면 서명해야합니다.
Daij-Djan

@ Daij-Djan 적어도 사실은 아니지만 macOS에서는 그렇지 않습니다. 샌드 박스 맥 OS 응용 프로그램은 같은 장소에서 바이너리 중 하나를 실행할 수있는 /usr/bin곳에 grep살고있다.
jeff-h

아니요. 잘못 알려주십시오.) ist nstask에서 샌드 박스에없는 것을 실행하지 못합니다.
Daij-Djan

답변:


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
예, 'arguments = [NSArray arrayWithObjects : @ "-e", @ "foo", @ "bar.txt", nil];'
Gordon Wilson

14
답변에 작은 결함이 있습니다. NSPipe에는 버퍼 (OS 레벨로 설정)가 있으며, 읽을 때 플러시됩니다. 버퍼가 가득 차면 NSTask가 중단되고 앱도 무한정 중단됩니다. 오류 메시지가 나타나지 않습니다. NSTask가 많은 정보를 리턴하는 경우 발생할 수 있습니다. 해결책은 NSMutableData *data = [NSMutableData dataWithCapacity:512];입니다. 그런 다음, while ([task isRunning]) { [data appendData:[file readDataToEndOfFile]]; }. 그리고 나는 " [data appendData:[file readDataToEndOfFile]];루프 "하고 당신은 while 루프 종료 후 하나 더 가져야합니다 .
Dave Gallagher

2
이렇게하지 않으면 오류가 발생하지 않습니다 (그냥 로그에 인쇄됩니다) : [task setStandardError : pipe];
Mike Sprague

1
ARC 및 Obj-C 배열 리터럴로 업데이트 할 수 있습니다. 예 : pastebin.com/sRvs3CqD
bames53

1
오류를 파이프하는 것도 좋습니다. task.standardError = pipe;
vqdave

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 이후.
David Stein

@DavidStein : autoreleasepool을 사용하여 runCommand 메소드를 래핑하는 것이 아니라고 생각합니다. 실제로 위의 코드는 비 ARC도 고려하지 않습니다.
Kenial

@ Kenial : 아, 그것은 훨씬 더 나은 해결책입니다. 또한 범위를 벗어나면 즉시 리소스를 해제합니다.
David Stein

/ bin / ps : 작업이 허용되지 않습니다. 성공하지 못했습니다.
Naman Vaishnav

40

공유의 정신에서 ... 이것은 쉘 스크립트를 실행하기 위해 자주 사용하는 방법입니다. 빌드의 복사 단계에서 제품 번들에 스크립트를 추가 한 후 런타임에 스크립트를 읽고 실행할 수 있습니다. 참고 :이 코드는 privateFrameworks 하위 경로에서 스크립트를 찾습니다. 경고 : 배포 된 제품의 경우 보안 위험이 발생할 수 있지만 자체 개발의 경우 응용 프로그램을 다시 컴파일하지 않고 간단한 작업 (예 : rsync to host ... 등)을 사용자 지정하는 쉬운 방법입니다. 번들의 쉘 스크립트.

//------------------------------------------------------
-(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"을 실행하고 싶지만 "시작 경로를 액세스 할 수 없습니다"라는 오류가 표시됩니다.
Vikas Bansal

25

Swift에서 수행하는 방법은 다음과 같습니다.

스위프트 3.0의 변경 사항 :

  • NSPipe 이름이 변경되었습니다 Pipe

  • NSTask 이름이 변경되었습니다 Process


이것은 위의 inkit의 Objective-C 답변을 기반으로합니다. 그는로 쓴 카테고리NSString- 스위프트를 들어,이된다 확장String.

확장 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()

}

메모 Process(가)에서 읽을 결과를 Pipe입니다 NSString객체입니다. 오류 문자열 일 수도 있고 빈 문자열 일 수도 있지만 항상이어야합니다 NSString.

따라서 0이 아닌 한 결과는 스위프트로 캐스팅되어 String반환 될 수 있습니다 .

어떤 이유로 NSString파일 데이터에서 전혀 초기화 할 수 없는 경우 , 함수는 오류 메시지를 리턴합니다. 이 함수는 optional을 반환하도록 작성되었을 수 String?있지만 사용하기가 어려우며 이것이 발생할 가능성이 적기 때문에 유용한 목적으로 사용되지 않습니다.


1
정말 좋고 우아한 방법! 이 답변은 더 많은 투표를해야합니다.
XueYu

출력이 필요하지 않은 경우. @discardableResult 인수를 inCommand 또는 runCommand 메소드 위에 추가하십시오. 이를 통해 변수에 넣지 않고도 메소드를 호출 할 수 있습니다.
Lloyd Keijzer 5

결과 = 문자열 (바이트 : 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

다음의 스위프트 예를 만드는 사용이다 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)
    }
}

용법:

let output = "echo hello".run()

2
실제로, 당신의 코드는 나에게 매우 유용했습니다! Swift로 변경하고 아래에 다른 답변으로 게시했습니다.
ElmerCat

14

Objective-C 고유의 방법을 찾지 않는다면 fork , execwait 가 작동해야합니다. 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 응용 프로그램에서 작업하고 있다고 가정하므로 이러한 기능에 대한 Apple 설명서의 링크는 모두 있지만 POSIX모든 POSIX 호환 시스템에서 사용해야합니다.


나는 이것이 매우 오래된 대답이라는 것을 알고 있지만 이것을 말해야합니다. 이것은 trheads를 사용하여 excecution을 처리하는 탁월한 방법입니다. 유일한 단점은 전체 프로그램의 사본을 생성한다는 것입니다. 따라서 코코아 응용 프로그램의 경우 @GordonWilson과 함께 사용하면 더 좋을 것입니다. 명령 줄 응용 프로그램에서 작업하는 경우 이것이 가장 좋은 방법입니다. 덕분에 (미안 내 나쁜 영어)
니 코스 Karalis

11

오래된 POSIX 시스템도 있습니다 ( "echo -en '\ 007'").


6
이 명령을 실행하지 마십시오. (이 명령이 무엇을하는지 모르는 경우)
justin

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는 거칠고 콘솔의 유니 코드는 여전히 짜증납니다 :)
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나에게 당황 혼란은 아직 .. 위 내 총 단점을 해결하기 위해 내 능력에 대한 믿음이 적은와 - 유일한 올리브 브랜치 I의 서비스를입니다 @ inket의 대답의 rezhuzhed 버전 뼈의 barest 내 동료를 위해는, 순수 주의자 / 상세한 증오 ...

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는 sizeof (ssize_t)! = sizeof (int) 인 아키텍처에서 fread ()의 반환 값에 비해 너무 작습니다. BUFSIZ 바이트보다 더 많은 출력을 원한다면 어떻게해야합니까? 출력이 UTF-8이 아닌 경우 어떻게합니까? pclose ()가 에러를 반환하면 어떻게됩니까? fread ()의 오류를 어떻게보고합니까?
ObjectiveC-oder

@ ObjectiveC-oder D' oh-나는 몰라. 알려주세요.
Alex Grey

3

Custos Mortem은 다음과 같이 말했습니다.

아무도 통화 차단 / 비 차단 통화 문제에 빠지지 않은 것에 놀랐습니다.

NSTask아래 읽기 관련 차단 / 비 차단 통화 문제 :

asynctask.m-NSTask를 사용하여 데이터를 처리하기 위해 비동기 stdin, stdout 및 stderr 스트림을 구현하는 방법을 보여주는 샘플 코드

asynctask.m의 소스 코드는 GitHub 에서 제공됩니다 .


내보기 공헌 비 블로킹 버전
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

또는 Objective 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

터미널 명령에 관리자 권한 (일명 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 이상 사용되지 않습니다
샘 워시번

2
.. 그러나이 작업을 수행 할 수있는 유일한 방법이므로 관계없이 계속 작동하며 많은 설치 관리자가이를 사용합니다.
Thomas Tempelmann
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.