Wykonaj polecenie terminalu z aplikacji Cocoa


204

Jak mogę wykonać polecenie terminalu (jak grep) z mojej aplikacji Objective-C Cocoa?


2
Po prostu stwierdzam oczywiste: z piaskownicą nie możesz po prostu uruchomić aplikacji, których nie ma w piaskownicy ORAZ muszą one zostać podpisane przez Ciebie, aby na to pozwolić
Daij-Djan

@ Daij-Djan to wcale nie prawda, przynajmniej nie w systemie macOS. Aplikacja systemu MacOS w trybie piaskownicy może uruchamiać dowolny plik binarny w miejscach takich jak /usr/binmiejsce grepzamieszkania.
jeff-h

Nie. Proszę udowodnić, że się mylę;) na ist nstask nie uda się uruchomić niczego, co nie jest w twojej piaskownicy.
Daij-Djan

Odpowiedzi:


282

Możesz użyć NSTask. Oto przykład, który działałby „ /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);

NSPipei NSFileHandlesłużą do przekierowania standardowego wyjścia zadania.

Bardziej szczegółowe informacje na temat interakcji z systemem operacyjnym z poziomu aplikacji Objective-C można znaleźć w tym dokumencie w Centrum programistycznym Apple: Interakcja z systemem operacyjnym .

Edycja: Dołączona poprawka dla problemu NSLog

Jeśli używasz NSTask do uruchamiania narzędzia wiersza poleceń poprzez bash, musisz dołączyć tę magiczną linię, aby NSLog działał:

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

Wyjaśnienie znajduje się tutaj: https://web.archive.org/web/20141121094204/https://cocoadev.com/HowToPipeCommandsWithNSTask


1
Tak, 'argumenty = [NSArray arrayWithObjects: @ "- e", @ "foo", @ "bar.txt", zero];'
Gordon Wilson

14
W twojej odpowiedzi jest mała usterka. NSPipe ma bufor (ustawiony na poziomie systemu operacyjnego), który jest opróżniany podczas odczytu. Jeśli bufor się zapełni, NSTask zawiesi się, a twoja aplikacja również zawiesi się na czas nieokreślony. Nie pojawi się komunikat o błędzie. Może się to zdarzyć, jeśli NSTask zwróci wiele informacji. Rozwiązaniem jest użyć NSMutableData *data = [NSMutableData dataWithCapacity:512];. Potem while ([task isRunning]) { [data appendData:[file readDataToEndOfFile]]; }. I „wierzę”, że powinieneś mieć jeszcze jeden [data appendData:[file readDataToEndOfFile]];po wyjściu z pętli while.
Dave Gallagher

2
Błędy nie pojawią się, chyba że to zrobisz (zostaną po prostu wydrukowane w dzienniku): [task setStandardError: pipe];
Mike Sprague,

1
Można to zaktualizować za pomocą ARC i literałów tablicowych Obj-C. Np pastebin.com/sRvs3CqD
bames53

1
Dobrym pomysłem jest także usuwanie błędów. task.standardError = pipe;
vqdave

43

Artykuł Kent dał mi nowy pomysł. ta metoda runCommand nie potrzebuje pliku skryptu, po prostu uruchamia polecenie za pomocą wiersza:

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

Możesz użyć tej metody w następujący sposób:

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

1
To dobrze radzi sobie z większością przypadków, ale jeśli uruchomisz go w pętli, ostatecznie pojawi się wyjątek z powodu zbyt wielu otwartych uchwytów plików. Można to naprawić poprzez dodanie: [file closeFile]; po readDataToEndOfFile.
David Stein

@DavidStein: Wydaje mi się, że używanie autoreleasepool do zawijania metody runCommand wydaje się być raczej. W rzeczywistości powyższy kod nie uwzględnia również architektury innej niż ARC.
Kenial

@Kenial: Oh, to znacznie lepsze rozwiązanie. Zwalnia także zasoby niezwłocznie po opuszczeniu zakresu.
David Stein

/ bin / ps: Operacja niedozwolona, ​​nie odnoszę żadnych sukcesów, ołowiu?
Naman Vaishnav

40

w duchu udostępniania ... jest to metoda, której często używam do uruchamiania skryptów powłoki. możesz dodać skrypt do pakietu produktu (w fazie kopiowania kompilacji), a następnie odczytać skrypt i uruchomić go w czasie wykonywania. Uwaga: ten kod szuka skryptu w ścieżce podrzędnej privateFrameworks. ostrzeżenie: może to stanowić zagrożenie bezpieczeństwa dla wdrożonych produktów, ale dla naszego wewnętrznego rozwoju jest to łatwy sposób dostosowywania prostych rzeczy (takich jak host do rsync do ...) bez ponownej kompilacji aplikacji, ale po prostu edytując skrypt powłoki w pakiecie.

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

Edycja: Dołączona poprawka dla problemu NSLog

Jeśli używasz NSTask do uruchamiania narzędzia wiersza poleceń poprzez bash, musisz dołączyć tę magiczną linię, aby NSLog działał:

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

W kontekście:

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

Wyjaśnienie znajduje się tutaj: http://www.cocoadev.com/index.pl?NSTask


2
Link do wyjaśnienia nie działa.
Jonny

Chcę uruchomić polecenie „system_profiler SPApplicationsDataType -xml”, ale pojawia się błąd „ścieżka uruchamiania niedostępna”
Vikas Bansal

25

Oto jak to zrobić w Swift

Zmiany w Swift 3.0:

  • NSPipe został przemianowany Pipe

  • NSTask został przemianowany Process


Jest to oparte na powyższej odpowiedzi Inkit na Objective-C. Pisał go jako kategorii sprawie NSString- do szybkiego, staje się rozszerzenie z String.

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

Stosowanie:

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

    Lub tylko:

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

Przykład:

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

}

Zwróć uwagę, że Processwynik odczytany z Pipejest NSStringobiektem. Może to być ciąg błędu i może to być również ciąg pusty, ale zawsze powinien to być ciąg NSString.

Tak więc, dopóki nie jest zero, wynik może być rzucony jako Szybki Stringi powrócił.

Jeśli z jakiegoś powodu NSStringw ogóle nie można zainicjować z danych pliku, funkcja zwraca komunikat o błędzie. Funkcja mogła zostać napisana w celu zwrócenia opcjonalnego String?, ale byłoby to niewygodne w użyciu i nie służyłoby pożytecznemu celowi, ponieważ tak mało prawdopodobne jest, aby tak się stało.


1
Naprawdę ładny i elegancki sposób! Ta odpowiedź powinna mieć więcej głosów pozytywnych.
XueYu,

Jeśli nie potrzebujesz danych wyjściowych. Dodaj argument @discardableResult przed lub nad metodą runCommand. Umożliwi to wywołanie metody bez konieczności umieszczania jej w zmiennej.
Lloyd Keijzer

let result = String (bytes: fileHandle.readDataToEndOfFile (), kodowanie: String.Encoding.utf8) jest w porządku
cleexiang

18

Cel C (patrz Swift poniżej)

Wyczyściłem kod w górnej odpowiedzi, aby był bardziej czytelny, mniej zbędny, dodał zalety metody jednowierszowej i przekształcił się w kategorię NSString

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

Realizacja:

@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

Stosowanie:

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

A jeśli masz problemy z kodowaniem wyjściowym:

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

Mam nadzieję, że będzie to dla ciebie równie przydatne, jak dla mnie przyszłości. (Witam Cię!)


Szybki 4

Oto przykład użycie Swift making of Pipe, ProcessiString

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

Stosowanie:

let output = "echo hello".run()

2
Rzeczywiście, twój kod był dla mnie bardzo przydatny! Zmieniłem go na Swift i opublikowałem jako kolejną odpowiedź poniżej.
ElmerCat

14

fork , exec i wait powinny działać, jeśli tak naprawdę nie szukasz sposobu specyficznego dla Objective-C. forktworzy kopię aktualnie działającego programu, execzastępuje aktualnie działający program nowym i waitczeka na zakończenie podprocesu. Na przykład (bez sprawdzania błędów):

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

Istnieje również system , który uruchamia polecenie tak, jakbyś wpisał je z wiersza poleceń powłoki. To prostsze, ale masz mniejszą kontrolę nad sytuacją.

Zakładam, że pracujesz nad aplikacją na komputery Mac, więc linki prowadzą do dokumentacji Apple dla tych funkcji, ale są one wszystkie POSIX, więc powinieneś używać ich w dowolnym systemie zgodnym z POSIX.


Wiem, że jest to bardzo stara odpowiedź, ale muszę to powiedzieć: jest to doskonały sposób na użycie głowic do obsługi wykroczenia. jedynym minusem jest to, że tworzy kopię całego programu. więc dla aplikacji kakaowej wybrałbym @GordonWilson, aby uzyskać lepsze podejście, a jeśli pracuję nad aplikacją z linii poleceń, jest to najlepszy sposób, aby to zrobić. dzięki (przepraszam za mój zły angielski)
Nicos Karalis 17.02.13

11

Istnieje również stary, dobry system POSIX („echo -en '\ 007'”);


6
NIE URUCHAMIAJ TEGO POLECENIA. (W przypadku, gdy nie wiesz, co robi to polecenie)
Justin

4
Zmieniłem go na coś nieco bezpieczniejszego… (wydaje sygnał dźwiękowy)
nes1983,

Czy nie spowoduje to błędu w konsoli? Incorrect NSStringEncoding value 0x0000 detected. Assuming NSStringEncodingASCII. Will stop this compatibility mapping behavior in the near future.
cwd

1
Hmm Może musisz podwójnie uciec od odwrotnego ukośnika.
nes1983,

po prostu uruchom / usr / bin / echo lub coś. rm -rf jest trudny, a Unicode w konsoli nadal jest do bani :)
Maxim Veksler

8

Napisałem tę funkcję „C”, ponieważ NSTaskjest wstrętna ..

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

och, a dla zachowania kompletności / jednoznaczności…

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

Wiele lat później Cdla mnie nadal jest oszałamiający bałagan… i z niewielką wiarą w moją zdolność do skorygowania moich rażących niedociągnięć powyżej - jedyną gałązką oliwną, którą oferuję, jest zmieniona wersja odpowiedzi @ inket, która jest najzupełniej kośćca , dla moich kolegów purystów / nienawistnych hejterów ...

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 jest niezdefiniowany przy każdym błędzie, chars_read jest zbyt mały dla wartości zwracanej przez fread () w dowolnej architekturze, w której sizeof (ssize_t)! = sizeof (int), co jeśli chcemy większej wydajności niż bajty BUFSIZ? Co jeśli wyjście nie jest UTF-8? Co jeśli pclose () zwraca błąd? Jak zgłaszamy błąd fread ()?
ObjectiveC-oder

@ ObjectiveC-oder D'oh - I dunno. Proszę, powiedz mi (jak w .. edytuj)!
Alex Gray

3

Custos Mortem powiedział:

Dziwi mnie, że nikt tak naprawdę nie interesował się problemami z blokowaniem / nieblokowaniem połączeń

W przypadku problemów związanych z blokowaniem / nieblokowaniem połączeń, o których NSTaskmowa poniżej:

asynctask.m - przykładowy kod pokazujący, jak zaimplementować asynchroniczne strumienie stdin, stdout i stderr do przetwarzania danych za pomocą NSTask

Kod źródłowy asynctask.m jest dostępny na GitHub .


Zobacz mój wkład w wersję nieblokującą
Guruniverse

3

Oprócz kilku doskonałych odpowiedzi powyżej, używam następującego kodu do przetwarzania danych wyjściowych polecenia w tle i unikania mechanizmu blokowania [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];
}

Dla mnie linia, która zrobiła różnicę to [self performSelectorInBackground: @selector (CollectTaskOutput :) withObject: file];
neowinston

2

Lub ponieważ celem C jest po prostu C z warstwą OO na górze, możesz użyć elementów 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[]); 

Są one zawarte w pliku nagłówkowym unistd.h.


2

Jeśli polecenie Terminal wymaga uprawnień administratora (alias sudo), użyj AuthorizationExecuteWithPrivilegeszamiast tego. Poniżej utworzy się plik o nazwie „com.stackoverflow.test” to katalog główny „/ 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
To jest oficjalnie przestarzałe od OS X 10.7
Sam Washburn 16.12

2
.. ale działa niezależnie, ponieważ jest to jedyny sposób, aby to zrobić, i wielu instalatorów polega na tym, jak sądzę.
Thomas Tempelmann,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.