Esegui un comando terminale da un'app Cocoa


204

Come posso eseguire un comando terminale (come grep) dalla mia applicazione Objective-C Cocoa?


2
Sto solo affermando l'ovvio: con il sandboxing non puoi semplicemente avviare le app che non sono nel tuo sandbox E devono essere firmate da te per consentire questo
Daij-Djan

@ Daij-Djan non è affatto vero, almeno non in macOS. Un'app macOS in modalità sandbox può eseguire uno qualsiasi dei file binari in luoghi come /usr/bindove grepvive.
jeff-h,

No. Per favore, dimostrami che ho torto;) su ist nstask non riuscirà a eseguire nulla che non sia nella tua sandbox.
Daij-Djan,

Risposte:


282

È possibile utilizzare NSTask. Ecco un esempio che verrebbe eseguito ' /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);

NSPipee NSFileHandlevengono utilizzati per reindirizzare l'output standard dell'attività.

Per informazioni più dettagliate sull'interazione con il sistema operativo dall'applicazione Objective-C, è possibile consultare questo documento nel Centro di sviluppo di Apple: Interazione con il sistema operativo .

Modifica: correzione inclusa per il problema NSLog

Se si utilizza NSTask per eseguire un'utilità della riga di comando tramite bash, è necessario includere questa linea magica per far funzionare NSLog:

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

Una spiegazione è qui: https://web.archive.org/web/20141121094204/https://cocoadev.com/HowToPipeCommandsWithNSTask


1
Sì, 'argomenti = [NSArray arrayWithObjects: @ "- e", @ "foo", @ "bar.txt", nil];'
Gordon Wilson,

14
C'è un piccolo problema nella tua risposta. NSPipe ha un buffer (impostato a livello di sistema operativo), che viene scaricato quando viene letto. Se il buffer si riempie, NSTask si bloccherà e anche la tua app si bloccherà, a tempo indeterminato. Non verrà visualizzato alcun messaggio di errore. Questo può accadere se NSTask restituisce molte informazioni. La soluzione è quella di utilizzare NSMutableData *data = [NSMutableData dataWithCapacity:512];. Poi, while ([task isRunning]) { [data appendData:[file readDataToEndOfFile]]; }. E "credo" che dovresti averne uno in più [data appendData:[file readDataToEndOfFile]];dopo l'uscita del ciclo while.
Dave Gallagher,

2
Gli errori non verranno visualizzati a meno che tu non lo faccia (vengono appena stampati nel registro): [task setStandardError: pipe];
Mike Sprague,

1
Questo potrebbe essere aggiornato con ARC e con valori letterali array Obj-C. Ad esempio pastebin.com/sRvs3CqD
bames53

1
È anche una buona idea convogliare gli errori. task.standardError = pipe;
vqdave,

43

L'articolo di Kent mi ha dato una nuova idea. questo metodo runCommand non ha bisogno di un file di script, esegue solo un comando da una riga:

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

Puoi usare questo metodo in questo modo:

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

1
Questo gestisce bene la maggior parte dei casi, ma se lo esegui in un ciclo, alla fine genera un'eccezione a causa di troppi handle di file aperti. Può essere risolto aggiungendo: [file closeFile]; dopo readDataToEndOfFile.
David Stein,

@DavidStein: Penso che usare autoreleasepool per concludere il metodo runCommand sembra essere piuttosto che. In realtà, anche il codice sopra non considera non-ARC.
Kenial,

@Kenial: Oh, questa è una soluzione molto migliore. Rilascia inoltre prontamente le risorse all'uscita dall'ambito.
David Stein,

/ bin / ps: operazione non consentita, non ottengo alcun successo, piombo?
Naman Vaishnav,

40

nello spirito di condivisione ... questo è un metodo che uso frequentemente per eseguire script di shell. puoi aggiungere uno script al tuo pacchetto di prodotti (nella fase di copia della build) e quindi far leggere ed eseguire lo script in fase di esecuzione. nota: questo codice cerca lo script nel sotto-percorso privateFrameworks. attenzione: questo potrebbe essere un rischio per la sicurezza dei prodotti distribuiti, ma per il nostro sviluppo interno è un modo semplice per personalizzare cose semplici (come l'host da cui sincronizzare rsync a ...) senza ricompilare l'applicazione, ma semplicemente modificando il shell script nel 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);    
}
//------------------------------------------------------

Modifica: correzione inclusa per il problema NSLog

Se si utilizza NSTask per eseguire un'utilità della riga di comando tramite bash, è necessario includere questa linea magica per far funzionare NSLog:

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

Nel contesto:

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

Una spiegazione è qui: http://www.cocoadev.com/index.pl?NSTask


2
Il link alla spiegazione è morto.
Jonny,

Voglio eseguire questo comando "system_profiler SPApplicationsDataType -xml" ma visualizzo questo errore "Percorso di avvio non accessibile"
Vikas Bansal,

25

Ecco come farlo in Swift

Modifiche per Swift 3.0:

  • NSPipe è stato rinominato Pipe

  • NSTask è stato rinominato Process


Questo si basa sulla risposta Objective-C di inkit sopra. Lo ha scritto come una categoria su NSString- Per Swift, diventa un'estensione di String.

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

Uso:

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

    o semplicemente:

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

Esempio:

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

}

Nota il Processrisultato come letto da Pipeè un NSStringoggetto. Potrebbe essere una stringa di errore e può anche essere una stringa vuota, ma dovrebbe sempre essere una stringa NSString.

Quindi, fintanto che non è zero, il risultato può essere lanciato come Swift Stringe restituito.

Se per qualche motivo non è NSStringpossibile inizializzare affatto dai dati del file, la funzione restituisce un messaggio di errore. La funzione avrebbe potuto essere scritta per restituire un facoltativo String?, ma sarebbe scomodo da usare e non servirebbe a uno scopo utile perché è così improbabile che ciò accada.


1
Modo davvero bello ed elegante! Questa risposta dovrebbe avere più voti.
XueYu,

Se non hai bisogno dell'output. Aggiungi l'argomento @discardableResult davanti o sopra il metodo runCommand. Questo ti permetterà di chiamare il metodo senza doverlo inserire in una variabile.
Lloyd Keijzer,

let result = String (byte: fileHandle.readDataToEndOfFile (), codifica: String.Encoding.utf8) è ok
cleexiang,

18

Objective-C (vedi sotto per Swift)

Ripulito il codice nella risposta in alto per renderlo più leggibile, meno ridondante, aggiunto i vantaggi del metodo a una riga e trasformato in una categoria NSString

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

Implementazione:

@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

Uso:

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

E se riscontri problemi con la codifica di output:

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

Spero ti sia utile come lo sarà per me futuro. (Ciao a te!)


Swift 4

Ecco un rapido esempio facendo uso di Pipe, ProcesseString

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

Uso:

let output = "echo hello".run()

2
In effetti, il tuo codice mi è stato molto utile! L'ho cambiato in Swift e l'ho pubblicato come un'altra risposta di seguito.
ElmerCat,

14

fork , exec e wait dovrebbero funzionare, se non stai davvero cercando un modo specifico per Objective-C. forkcrea una copia del programma attualmente in esecuzione, execsostituisce il programma attualmente in esecuzione con uno nuovo e waitattende la chiusura del sottoprocesso. Ad esempio (senza alcun controllo degli errori):

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

C'è anche sistema , che esegue il comando come se fosse stato digitato dalla riga di comando della shell. È più semplice, ma hai meno controllo sulla situazione.

Suppongo che tu stia lavorando su un'applicazione Mac, quindi i collegamenti sono alla documentazione di Apple per queste funzioni, ma sono tutti POSIX, quindi dovresti usarli su qualsiasi sistema compatibile con POSIX.


So che questa è una risposta molto vecchia, ma devo dirlo: questo è un modo eccellente di usare i trhead per gestire l'escissione. l'unico aspetto negativo è che crea una copia dell'intero programma. quindi per un'applicazione di cacao andrei con @GordonWilson per un approccio più gradevole, e se sto lavorando su un'applicazione da riga di comando, questo è il modo migliore per farlo. grazie (scusate il mio cattivo inglese)
Nicos Karalis,

11

Esiste anche un buon vecchio sistema POSIX ("echo -en '\ 007'");


6
NON ESEGUIRE QUESTO COMANDO. (Nel caso in cui non si sappia cosa fa questo comando)
justin

4
Modificato in qualcosa di leggermente più sicuro ... (emette un segnale acustico)
nes1983,

Questo non genererà un errore nella console? Incorrect NSStringEncoding value 0x0000 detected. Assuming NSStringEncodingASCII. Will stop this compatibility mapping behavior in the near future.
Cwd,

1
Hmm. Forse devi fuggire doppiamente dalla barra rovesciata.
nes1983,

basta eseguire / usr / bin / echo o qualcosa del genere. rm -rf è duro e l'unicode nella console fa ancora schifo :)
Maxim Veksler

8

Ho scritto questa funzione "C", perché NSTaskè odiosa ..

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, e per essere completo / non ambiguo ...

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

Anni dopo, Cè ancora un disastro sconcertante, per me .. e con poca fiducia nella mia capacità di correggere le mie gravi lacune sopra - l'unico ramo d'ulivo che offro è una versione rezhuzhed della risposta di @inket che è la più piccola delle ossa , per il mio collega puristi / detestatori della verbosità ...

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 non è definito su alcun errore, chars_read è troppo piccolo per il valore di ritorno di fread () su qualsiasi architettura in cui sizeof (ssize_t)! = sizeof (int), cosa succede se vogliamo più output di byte BUFSIZ? Cosa succede se l'output non è UTF-8? Cosa succede se pclose () restituisce un errore? Come riportiamo l'errore di fread ()?
Obiettivo C-

@ ObjectiveC-oder D'oh - Non lo so. Per favore, dimmi (come in .. modifica via)!
Alex Gray,

3

Custode Mortem disse:

Sono sorpreso che nessuno abbia avuto problemi di blocco / non blocco delle chiamate

Per problemi di chiamata bloccanti / non bloccanti riguardanti la NSTasklettura seguente:

asynctask.m - codice di esempio che mostra come implementare flussi stdin, stdout e stderr asincroni per l'elaborazione dei dati con NSTask

Il codice sorgente di asynctask.m è disponibile su GitHub .


Vedi il mio contributo per una versione non bloccante
Guruniverse

3

Oltre alle numerose eccellenti risposte sopra, uso il seguente codice per elaborare l'output del comando in background ed evitare il meccanismo di blocco di [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];
}

Per me la linea che ha fatto la differenza era [self performSelectorInBackground: @selector (collectTaskOutput :) withObject: file];
Neowinston,

2

O poiché l'Obiettivo C è solo C con sopra un livello di OO puoi usare le parti posteriori di 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[]); 

Sono inclusi nel file di intestazione unistd.h.


2

Se il comando Terminale richiede il privilegio di amministratore (aka sudo), utilizzare AuthorizationExecuteWithPrivilegesinvece. Di seguito verrà creato un file denominato "com.stackoverflow.test" è la directory principale "/ Sistema / Libreria / Cache".

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
Questo è stato ufficialmente deprecato da OS X 10.7
Sam Washburn il

2
.. ma continua a funzionare indipendentemente, poiché è l'unico modo per farlo, e molti installatori fanno affidamento su questo, credo.
Thomas Tempelmann,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.