Führen Sie einen Terminalbefehl über eine Cocoa-App aus

204

Wie kann ich einen Terminalbefehl (wie grep) in meiner Objective-C Cocoa-Anwendung ausführen ?

lostInTransit
quelle
2
Ich sage
@ Daij-Djan das stimmt überhaupt nicht, zumindest nicht in macOS. Eine MacOS-App mit Sandbox kann alle Binärdateien beispielsweise an Orten ausführen, an /usr/bindenen sie grepleben.
Jeff-h
Bitte beweise mir das Gegenteil;) auf seiner nstask kann nichts ausgeführt werden, was sich nicht in deiner Sandbox befindet.
Daij-Djan

Antworten:

282

Sie können verwenden NSTask. Hier ist ein Beispiel, das ' /usr/bin/grep foo bar.txt' ausführen würde .

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

NSPipeund NSFileHandlewerden verwendet, um die Standardausgabe der Aufgabe umzuleiten.

Ausführlichere Informationen zur Interaktion mit dem Betriebssystem in Ihrer Objective-C-Anwendung finden Sie in diesem Dokument im Apple Development Center: Interaktion mit dem Betriebssystem .

Bearbeiten: Enthaltene Korrektur für NSLog-Problem

Wenn Sie NSTask verwenden, um ein Befehlszeilenprogramm über Bash auszuführen, müssen Sie diese magische Zeile einfügen, damit NSLog funktioniert:

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

Eine Erklärung finden Sie hier: https://web.archive.org/web/20141121094204/https://cocoadev.com/HowToPipeCommandsWithNSTask

Gordon Wilson
quelle
1
Ja, 'Argumente = [NSArray arrayWithObjects: @ "- e", @ "foo", @ "bar.txt", nil];'
Gordon Wilson
14
Ihre Antwort enthält einen kleinen Fehler. NSPipe verfügt über einen Puffer (auf Betriebssystemebene festgelegt), der beim Lesen gelöscht wird. Wenn der Puffer voll ist, bleibt NSTask hängen, und auch Ihre App bleibt auf unbestimmte Zeit hängen. Es wird keine Fehlermeldung angezeigt. Dies kann passieren, wenn die NSTask viele Informationen zurückgibt. Die Lösung ist zu verwenden NSMutableData *data = [NSMutableData dataWithCapacity:512];. Dann while ([task isRunning]) { [data appendData:[file readDataToEndOfFile]]; }. Und ich "glaube", dass Sie [data appendData:[file readDataToEndOfFile]];nach dem Beenden der while-Schleife noch eine haben sollten .
Dave Gallagher
2
Fehler treten erst auf, wenn Sie dies tun (sie werden nur im Protokoll gedruckt): [task setStandardError: pipe];
Mike Sprague
1
Dies könnte mit ARC und mit Obj-C-Array-Literalen aktualisiert werden. ZB pastebin.com/sRvs3CqD
bames53
1
Es ist auch eine gute Idee, die Fehler weiterzuleiten. task.standardError = pipe;
VQDave
43

Kents Artikel brachte mich auf eine neue Idee. Diese runCommand-Methode benötigt keine Skriptdatei, sondern führt nur einen Befehl in einer Zeile aus:

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

Sie können diese Methode folgendermaßen verwenden:

NSString *output = runCommand(@"ps -A | grep mysql");
Kenial
quelle
1
Dies behandelt die meisten Fälle gut, aber wenn Sie es in einer Schleife ausführen, wird möglicherweise eine Ausnahme ausgelöst, da zu viele offene Dateihandles vorhanden sind. Kann behoben werden durch Hinzufügen von: [Datei closeFile]; nach readDataToEndOfFile.
David Stein
@DavidStein: Ich denke, die Verwendung von Autoreleasepool zum Umschließen der runCommand-Methode scheint eher zu sein als. Tatsächlich berücksichtigt der obige Code nicht auch Nicht-ARC.
Kenial
@ Kenial: Oh, das ist eine viel bessere Lösung. Außerdem werden die Ressourcen sofort nach Verlassen des Bereichs freigegeben.
David Stein
/ bin / ps: Operation nicht erlaubt, ich bekomme keinen Erfolg, führen?
Naman Vaishnav
40

im Geiste des Teilens ... ist dies eine Methode, die ich häufig verwende, um Shell-Skripte auszuführen. Sie können Ihrem Produktpaket ein Skript hinzufügen (in der Kopierphase des Builds) und das Skript dann zur Laufzeit lesen und ausführen lassen. Hinweis: Dieser Code sucht nach dem Skript im Unterpfad privateFrameworks. Warnung: Dies könnte ein Sicherheitsrisiko für bereitgestellte Produkte darstellen. Für unsere interne Entwicklung ist es jedoch eine einfache Möglichkeit, einfache Dinge (z. B. den Host, auf den synchronisiert werden soll ...) anzupassen, ohne die Anwendung neu zu kompilieren, sondern nur die zu bearbeiten Shell-Skript im Bundle.

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

Bearbeiten: Enthaltene Korrektur für NSLog-Problem

Wenn Sie NSTask verwenden, um ein Befehlszeilenprogramm über Bash auszuführen, müssen Sie diese magische Zeile einfügen, damit NSLog funktioniert:

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

Im Zusammenhang:

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

Eine Erklärung finden Sie hier: http://www.cocoadev.com/index.pl?NSTask

Kent
quelle
2
Der Erklärungslink ist tot.
Jonny
Ich möchte diesen Befehl "system_profiler SPApplicationsDataType -xml" ausführen, erhalte aber die Fehlermeldung "Startpfad nicht zugänglich"
Vikas Bansal
25

So geht's in Swift

Änderungen für Swift 3.0:

  • NSPipe wurde umbenannt Pipe

  • NSTask wurde umbenannt Process


Dies basiert auf der obigen Objective-C-Antwort von inkit. Er schrieb es als Kategorie auf NSString- Für Swift wird es eine Erweiterung von String.

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

Verwendung:

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

    oder nur:

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

Beispiel:

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

}

Beachten Sie, dass das Processausgelesene Ergebnis Pipeein NSStringObjekt ist. Es kann eine Fehlerzeichenfolge sein und es kann auch eine leere Zeichenfolge sein, aber es sollte immer eine sein NSString.

Solange es nicht Null ist, kann das Ergebnis als Swift gewertet Stringund zurückgegeben werden.

Wenn aus irgendeinem Grund überhaupt nichts NSStringaus den Dateidaten initialisiert werden kann, gibt die Funktion eine Fehlermeldung zurück. Die Funktion hätte geschrieben werden können, um ein optionales Element zurückzugeben String?, aber das wäre umständlich zu verwenden und würde keinen nützlichen Zweck erfüllen, da dies so unwahrscheinlich ist.

ElmerCat
quelle
1
Wirklich schön und elegant! Diese Antwort sollte mehr positive Stimmen haben.
XueYu
Wenn Sie die Ausgabe nicht benötigen. Fügen Sie das Argument @discardableResult vor oder über der runCommand-Methode hinzu. Auf diese Weise können Sie die Methode aufrufen, ohne sie in eine Variable einfügen zu müssen.
Lloyd Keijzer
let result = String (Bytes: fileHandle.readDataToEndOfFile (), Kodierung: String.Encoding.utf8) ist in
Ordnung
18

Ziel-C (siehe unten für Swift)

Der Code in der oberen Antwort wurde bereinigt, um ihn lesbarer und weniger redundant zu machen. Außerdem wurden die Vorteile der einzeiligen Methode hinzugefügt und wurde zu einer NSString-Kategorie

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

Implementierung:

@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

Verwendung:

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

Und wenn Sie Probleme mit der Ausgabecodierung haben:

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

Ich hoffe, es ist für Sie genauso nützlich wie für die Zukunft von mir. (Hallo Du!)


Swift 4

Hier ist ein Swift Beispiel macht Verwendung von Pipe, ProcessundString

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

Verwendung:

let output = "echo hello".run()
inket
quelle
2
In der Tat war Ihr Code für mich sehr nützlich! Ich habe es in Swift geändert und unten als weitere Antwort gepostet.
ElmerCat
14

Fork , Exec und Wait sollten funktionieren, wenn Sie nicht wirklich nach einem Objective-C-spezifischen Weg suchen. forkErstellt eine Kopie des aktuell ausgeführten Programms, execersetzt das aktuell ausgeführte Programm durch ein neues und waitwartet, bis der Unterprozess beendet ist. Zum Beispiel (ohne Fehlerprüfung):

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

Es gibt auch ein System , das den Befehl so ausführt, als hätten Sie ihn über die Befehlszeile der Shell eingegeben. Es ist einfacher, aber Sie haben weniger Kontrolle über die Situation.

Ich gehe davon aus, dass Sie an einer Mac-Anwendung arbeiten. Die Links beziehen sich also auf die Apple-Dokumentation für diese Funktionen, aber sie sind alle vorhanden POSIX. Sie sollten sie daher auf jedem POSIX-kompatiblen System verwenden.

Zach Hirsch
quelle
Ich weiß, dass dies eine sehr alte Antwort ist, aber ich muss dies sagen: Dies ist eine hervorragende Möglichkeit, Trheads zu verwenden, um mit der Ausführung umzugehen. Der einzige Nachteil ist, dass eine Kopie des gesamten Programms erstellt wird. Für eine Kakaoanwendung würde ich mich für einen besseren Ansatz für @GordonWilson entscheiden. Wenn ich an einer Befehlszeilenanwendung arbeite, ist dies der beste Weg, dies zu tun. danke (sorry mein schlechtes Englisch)
Nicos Karalis
11

Es gibt auch ein gutes altes POSIX- System ("echo -en '\ 007'");

nes1983
quelle
6
Führen Sie diesen Befehl nicht aus. (Falls Sie nicht wissen, was dieser Befehl bewirkt)
Justin
4
Es wurde in etwas etwas sichereres geändert… (es
piept
Wird dies nicht einen Fehler in der Konsole auslösen? Incorrect NSStringEncoding value 0x0000 detected. Assuming NSStringEncodingASCII. Will stop this compatibility mapping behavior in the near future.
CWD
1
Hmm. Vielleicht müssen Sie dem Backslash doppelt entkommen.
Nes1983
Führen Sie einfach / usr / bin / echo oder so aus. rm -rf ist hart und Unicode in der Konsole saugt immer noch :)
Maxim Veksler
8

Ich habe diese "C" -Funktion geschrieben, weil sie NSTaskwiderlich ist.

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

oh, und um vollständig / eindeutig zu sein ...

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

Jahre später Cist es für mich immer noch ein verwirrendes Durcheinander ... und mit wenig Vertrauen in meine Fähigkeit, meine oben genannten groben Mängel zu korrigieren - der einzige Olivenzweig, den ich anbiete, ist eine rezhuzhed Version von @ inkets Antwort , die für meinen Kollegen kaum Knochen enthält Puristen / Ausführlichkeitshasser ...

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]; });
}
Alex Gray
quelle
1
outP ist bei jedem Fehler undefiniert, chars_read ist zu klein für den Rückgabewert von fread () in jeder Architektur, in der sizeof (ssize_t)! = sizeof (int). Was ist, wenn wir mehr Ausgabe als BUFSIZ-Bytes wünschen? Was ist, wenn die Ausgabe nicht UTF-8 ist? Was ist, wenn pclose () einen Fehler zurückgibt? Wie melden wir den Fehler von fread ()?
ObjectiveC-oder
@ ObjectiveC-oder D'oh - ich weiß nicht. Bitte sag es mir (wie in .. weg bearbeiten)!
Alex Gray
3

Custos Mortem sagte:

Ich bin überrascht, dass niemand wirklich Probleme mit blockierenden / nicht blockierenden Anrufen hatte

Bei blockierenden / nicht blockierenden Anrufproblemen NSTasklesen Sie bitte Folgendes:

asynctask.m - Beispielcode, der zeigt, wie asynchrone stdin-, stdout- und stderr-Streams für die Datenverarbeitung mit NSTask implementiert werden

Der Quellcode von asynctask.m ist bei GitHub verfügbar .

jon
quelle
Siehe meinen Beitrag für eine nicht blockierende Version
Guruniverse
3

Zusätzlich zu den oben genannten hervorragenden Antworten verwende ich den folgenden Code, um die Ausgabe des Befehls im Hintergrund zu verarbeiten und den Blockierungsmechanismus von zu vermeiden [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];
}
Guruniverse
quelle
Für mich war die Zeile, die den Unterschied ausmachte, [self performSelectorInBackground: @selector (collectTaskOutput :) withObject: file];
Neowinston
2

Oder da Ziel C nur C mit einer OO-Schicht darüber ist, können Sie die Posix-Gegenstücke verwenden:

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

Sie sind in der Header-Datei unistd.h enthalten.

Paulo Lopes
quelle
2

Wenn für den Terminal-Befehl Administratorrechte (auch bekannt als Administratorrechte sudo) erforderlich sind , verwenden Sie AuthorizationExecuteWithPrivilegesstattdessen. Im Folgenden wird eine Datei mit dem Namen "com.stackoverflow.test" erstellt. Dies ist das Stammverzeichnis "/ 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); 
SwiftArchitect
quelle
4
Dies ist seit OS X 10.7
Sam Washburn
2
.. aber es funktioniert trotzdem weiter, da dies der einzige Weg ist, und viele Installateure verlassen sich darauf, glaube ich.
Thomas Tempelmann