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.