Exécuter une commande de terminal à partir d'une application Cocoa

204

Comment puis-je exécuter une commande de terminal (comme grep) à partir de mon application Objective-C Cocoa?

lostInTransit
la source
2
Je dis simplement l'évidence: avec le bac à sable, vous ne pouvez pas simplement démarrer des applications qui ne sont pas dans votre bac à sable ET elles doivent être signées par vous pour permettre cela
Daij-Djan
@ Daij-Djan ce n'est pas vrai du tout, du moins pas sous macOS. Une application macOS en bac à sable peut exécuter n'importe lequel des fichiers binaires dans des endroits tels que l' /usr/binendroit où grepvit.
jeff-h
Non. Veuillez me prouver le contraire;) sur ist nstask ne réussira pas à exécuter quoi que ce soit qui ne se trouve pas dans votre bac à sable.
Daij-Djan

Réponses:

282

Vous pouvez utiliser NSTask. Voici un exemple qui fonctionnerait ' /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);

NSPipeet NSFileHandlesont utilisés pour rediriger la sortie standard de la tâche.

Pour plus d'informations sur l'interaction avec le système d'exploitation à partir de votre application Objective-C, vous pouvez voir ce document sur le Centre de développement d'Apple: Interagir avec le système d'exploitation .

Edit: correction incluse pour le problème NSLog

Si vous utilisez NSTask pour exécuter un utilitaire de ligne de commande via bash, vous devez inclure cette ligne magique pour que NSLog fonctionne:

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

Une explication est ici: https://web.archive.org/web/20141121094204/https://cocoadev.com/HowToPipeCommandsWithNSTask

Gordon Wilson
la source
1
Yup, 'arguments = [NSArray arrayWithObjects: @ "- e", @ "foo", @ "bar.txt", nil];'
Gordon Wilson
14
Il y a un petit problème dans votre réponse. NSPipe a un tampon (défini au niveau du système d'exploitation), qui est vidé lors de sa lecture. Si le tampon se remplit, NSTask se bloque et votre application se bloque aussi indéfiniment. Aucun message d'erreur n'apparaîtra. Cela peut se produire si le NSTask retourne beaucoup d'informations. La solution est d'utiliser NSMutableData *data = [NSMutableData dataWithCapacity:512];. Ensuite, while ([task isRunning]) { [data appendData:[file readDataToEndOfFile]]; }. Et je "crois" que vous devriez en avoir un de plus [data appendData:[file readDataToEndOfFile]];après la sortie de la boucle while.
Dave Gallagher
2
Les erreurs ne surviendront que si vous faites cela (elles sont simplement imprimées dans le journal): [task setStandardError: pipe];
Mike Sprague
1
Cela pourrait être mis à jour avec ARC et avec les littéraux de tableau Obj-C. Par exemple pastebin.com/sRvs3CqD
bames53
1
C'est aussi une bonne idée de transmettre les erreurs. task.standardError = pipe;
vqdave
43

L'article de Kent m'a donné une nouvelle idée. cette méthode runCommand n'a pas besoin d'un fichier de script, exécute simplement une commande par une ligne:

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

Vous pouvez utiliser cette méthode comme ceci:

NSString *output = runCommand(@"ps -A | grep mysql");
Kenial
la source
1
Cela gère bien la plupart des cas, mais si vous l'exécutez en boucle, il déclenche finalement une exception en raison d'un trop grand nombre de descripteurs de fichiers ouverts. Peut être corrigé en ajoutant: [file closeFile]; après readDataToEndOfFile.
David Stein
@DavidStein: Je pense que l'utilisation d'autoreleasepool pour envelopper la méthode runCommand semble être plutôt que. En fait, le code ci-dessus ne considère pas non-ARC également.
Kenial
@Kenial: Oh, c'est une bien meilleure solution. Il libère également les ressources rapidement en quittant la portée.
David Stein
/ bin / ps: opération non autorisée, je ne réussis pas, chef de file?
Naman Vaishnav
40

dans un esprit de partage ... c'est une méthode que j'utilise fréquemment pour exécuter des scripts shell. vous pouvez ajouter un script à votre bundle de produits (dans la phase de copie de la build), puis faire lire et exécuter le script au moment de l'exécution. remarque: ce code recherche le script dans le sous-chemin privateFrameworks. avertissement: cela pourrait être un risque pour la sécurité des produits déployés, mais pour notre développement en interne, c'est un moyen facile de personnaliser des choses simples (comme l'hôte à rsync ...) sans recompiler l'application, mais simplement éditer le script shell dans le 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);    
}
//------------------------------------------------------

Edit: correction incluse pour le problème NSLog

Si vous utilisez NSTask pour exécuter un utilitaire de ligne de commande via bash, vous devez inclure cette ligne magique pour que NSLog fonctionne:

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

Dans le contexte:

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

Une explication est ici: http://www.cocoadev.com/index.pl?NSTask

Kent
la source
2
Le lien d'explication est mort.
Jonny
Je veux exécuter cette commande "system_profiler SPApplicationsDataType -xml" mais je reçois cette erreur "chemin de lancement non accessible"
Vikas Bansal
25

Voici comment le faire dans Swift

Changements pour Swift 3.0:

  • NSPipe a été renommé Pipe

  • NSTask a été renommé Process


Ceci est basé sur la réponse Objective-C de inkit ci-dessus. Il l'a écrit comme une catégorie sur NSString- Pour Swift, cela devient une extension de String.

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

Usage:

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

    ou juste:

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

Exemple:

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

}

Notez le Processrésultat tel qu'il est lu dans Pipeest un NSStringobjet. Il peut s'agir d'une chaîne d'erreur et peut également être une chaîne vide, mais elle doit toujours être un NSString.

Donc, tant qu'il n'est pas nul, le résultat peut être converti en Swift Stringet renvoyé.

Si, pour une raison quelconque, aucun ne NSStringpeut être initialisé à partir des données du fichier, la fonction renvoie un message d'erreur. La fonction aurait pu être écrite pour renvoyer une option String?, mais ce serait maladroit à utiliser et ne servirait à rien car il est peu probable que cela se produise.

ElmerCat
la source
1
Vraiment belle et élégante! Cette réponse devrait avoir plus de votes positifs.
XueYu
Si vous n'avez pas besoin de la sortie. Ajoutez l'argument @discardableResult devant ou au-dessus de la méthode runCommand. Cela vous permettra d'appeler la méthode sans avoir à la mettre dans une variable.
Lloyd Keijzer
let result = String (bytes: fileHandle.readDataToEndOfFile (), encoding: String.Encoding.utf8) is ok
cleexiang
18

Objectif-C (voir ci-dessous pour Swift)

Nettoyé le code dans la première réponse pour le rendre plus lisible, moins redondant, ajouté les avantages de la méthode à une ligne et transformé en une catégorie NSString

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

La mise en oeuvre:

@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

Usage:

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

Et si vous rencontrez des problèmes avec l'encodage de sortie:

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

J'espère que c'est aussi utile pour vous que pour moi. (Salut toi!)


Swift 4

Voici un exemple de l' utilisation de la prise Swift Pipe, ProcessetString

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

Usage:

let output = "echo hello".run()
inket
la source
2
En effet, votre code m'a été très utile! Je l'ai changé en Swift et l'ai posté comme une autre réponse ci-dessous.
ElmerCat
14

fork , exec et wait devraient fonctionner si vous ne cherchez pas vraiment une méthode spécifique à Objective-C. forkcrée une copie du programme en cours d'exécution, execremplace le programme en cours d'exécution par un nouveau et waitattend la fin du sous-processus. Par exemple (sans vérification d'erreur):

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

Il y a aussi le système , qui exécute la commande comme si vous l'avez tapée à partir de la ligne de commande du shell. C'est plus simple, mais vous avez moins de contrôle sur la situation.

Je suppose que vous travaillez sur une application Mac, donc les liens sont vers la documentation d'Apple pour ces fonctions, mais ils sont tous POSIX, vous devriez donc les utiliser sur n'importe quel système compatible POSIX.

Zach Hirsch
la source
Je sais que c'est une réponse très ancienne mais je dois dire ceci: c'est une excellente façon d'utiliser des trheads pour gérer l'exécution. le seul inconvénient est qu'il crée une copie de l'ensemble du programme. Donc, pour une application cacao, j'irais avec @GordonWilson pour une approche plus agréable, et si je travaille sur une application en ligne de commande, c'est la meilleure façon de le faire. merci (désolé mon mauvais anglais)
Nicos Karalis
11

Il y a aussi un bon vieux système POSIX ("echo -en '\ 007'");

nes1983
la source
6
N'EXÉCUTEZ PAS CETTE COMMANDE. (Au cas où vous ne savez pas ce que fait cette commande)
justin
4
Changé en quelque chose de légèrement plus sûr… (il émet un bip)
nes1983
Cela ne lancera-t-il pas une erreur dans la console? Incorrect NSStringEncoding value 0x0000 detected. Assuming NSStringEncodingASCII. Will stop this compatibility mapping behavior in the near future.
cwd
1
Hmm. Peut-être que vous devez double-échapper à la barre oblique inverse.
nes1983
exécutez simplement / usr / bin / echo ou quelque chose. rm -rf est dur, et l'unicode dans la console est toujours nul :)
Maxim Veksler
8

J'ai écrit cette fonction "C", car elle NSTaskest désagréable ..

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, et pour être complet / sans ambiguïté…

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

Des années plus tard, Cc'est toujours un gâchis déconcertant, pour moi .. et avec peu de foi en ma capacité à corriger mes défauts grossiers ci-dessus - la seule branche d'olivier que j'offre est une version rezhuzhed de la réponse de @ inket qui est la plus fine des os , pour mon collègue puristes / haineux de la 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]; });
}
Alex Gray
la source
1
outP n'est pas défini en cas d'erreur, chars_read est trop petit pour la valeur de retour de fread () sur toute architecture où sizeof (ssize_t)! = sizeof (int), que se passe-t-il si nous voulons plus de sortie que d'octets BUFSIZ? Et si la sortie n'est pas UTF-8? Que faire si pclose () renvoie une erreur? Comment signaler l'erreur de fread ()?
ObjectiveC-oder
@ ObjectiveC-oder D'oh - Je ne sais pas. S'il vous plaît, dites-moi (comme dans .. modifier)!
Alex Gray
3

Custos Mortem a déclaré:

Je suis surpris que personne n'ait vraiment eu de problèmes d'appels bloquants / non bloquants

Pour les problèmes d'appels bloquants / non bloquants concernant la NSTasklecture ci-dessous:

asynctask.m - exemple de code qui montre comment implémenter des flux stdin, stdout et stderr asynchrones pour le traitement des données avec NSTask

Le code source de asynctask.m est disponible sur GitHub .

Jon
la source
Voir ma contribution pour une version non bloquante
Guruniverse
3

En plus des nombreuses excellentes réponses ci-dessus, j'utilise le code suivant pour traiter la sortie de la commande en arrière-plan et éviter le mécanisme de blocage de [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
la source
Pour moi, la ligne qui a fait toute la différence était [self performSelectorInBackground: @selector (collectTaskOutput :) withObject: file];
neowinston
2

Ou puisque l'objectif C est juste C avec une couche OO sur le dessus, vous pouvez utiliser les contre-parties 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[]); 

Ils sont inclus dans le fichier d'en-tête unistd.h.

Paulo Lopes
la source
2

Si la commande Terminal Server requiert un privilège administrateur (aka sudo), utilisez AuthorizationExecuteWithPrivilegesplutôt. Ce qui suit va créer un fichier nommé "com.stackoverflow.test" qui est le répertoire racine "/ 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
la source
4
Cela est officiellement obsolète depuis OS X 10.7
Sam Washburn
2
.. mais cela continue de fonctionner malgré tout, car c'est la seule façon de le faire, et de nombreux installateurs en dépendent, je crois.
Thomas Tempelmann