Ejecutar un comando de terminal desde una aplicación Cocoa


¿Cómo puedo ejecutar un comando terminal (como grep) desde mi aplicación Objective-C Cocoa?

Author: Jonas, 2009-01-05

11 answers

Puede usar NSTask. Aquí hay un ejemplo que ejecutaría ' /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);

NSPipe y NSFileHandle se utilizan para redirigir la salida estándar de la tarea.

Para obtener información más detallada sobre cómo interactuar con el sistema operativo desde su aplicación Objective-C, puede ver este documento en el Centro de Desarrollo de Apple: Interactuar con el Sistema Operativo.

Editar: Solución incluida para el problema NSLog

Si está utilizando NSTask para ejecutar una línea de comandos utilidad a través de bash, entonces necesita incluir esta línea mágica para mantener NSLog funcionando:

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

Una explicación está aquí: https://web.archive.org/web/20141121094204/https://cocoadev.com/HowToPipeCommandsWithNSTask

 277
Author: Gordon Wilson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-26 08:29:51

En el espíritu de compartir... este es un método que uso con frecuencia para ejecutar scripts de shell. puede agregar un script a su paquete de productos (en la fase de copia de la compilación) y luego haga que el script se lea y ejecute en tiempo de ejecución. nota: este código busca el script en la sub-ruta PrivateFrameworks. advertencia: esto podría ser un riesgo de seguridad para los productos implementados, pero para nuestro desarrollo interno es una manera fácil de personalizar cosas simples (como a qué host se debe rsync...) sin volver a compilar el aplicación, pero simplemente editando el script de shell en el paquete.

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

Editar: Solución incluida para el problema NSLog

Si está utilizando NSTask para ejecutar una utilidad de línea de comandos a través de bash, entonces necesita incluir esta línea mágica para mantener NSLog funcionando:

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

En contexto:

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

Una explicación está aquí: http://www.cocoadev.com/index.pl?NSTask

 40
Author: kent,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-20 11:19:57

El artículo de Kent me dio una nueva idea. este método runCommand no necesita un archivo de script, solo ejecuta un comando por una línea:

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

Puedes usar este método de la siguiente manera:

NSString *output = runCommand(@"ps -A | grep mysql");
 38
Author: Kenial,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-05-17 00:31:11

Aquí está cómo hacerlo en Swift

Cambios para Swift 3.0:

  • NSPipe ha sido renombrado Pipe

  • NSTask ha sido renombrado Process


Esto se basa en la respuesta de Objective-C de inkit anterior. Lo escribió como un categoría el NSString - Para Swift, se convierte en un prórroga de String.

Cadena de extensión.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 simplemente:

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

Ejemplo:

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

}

Tenga en cuenta que el resultado Process leído desde el Pipe es un objeto NSString. Puede ser una cadena de error y también puede ser una cadena vacía, pero siempre debe ser un NSString.

Así que, mientras no sea nil, el resultado puede ser lanzado como un Swift String y devuelto.

Si por alguna razón no NSString en absoluto puede ser inicializada desde los datos del archivo, la función devuelve un mensaje de error. La función podría haber sido escrita para devolver un String? opcional, pero eso sería incómodo de usar y no serviría para un propósito útil porque es muy poco probable que esto ocurra.

 20
Author: ElmerCat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-09-26 00:34:49

Tenedor, exec , y wait debería funcionar, si realmente no estás buscando una forma específica de Objective-C. fork crea una copia del programa en ejecución, exec reemplaza el programa en ejecución por uno nuevo, y wait espera a que salga el subproceso. Por ejemplo (sin ninguna comprobación de errores):

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

También está system, que ejecuta el comando como si lo hubiera escrito desde la línea de comandos del shell. Es más simple, pero tienes menos control sobre la situación.

Asumo que está trabajando en una aplicación Mac, por lo que los enlaces son a la documentación de Apple para estas funciones, pero son todos POSIX, por lo que debe usarlos en cualquier sistema compatible con POSIX.

 14
Author: Zach Hirsch,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-03 16:35:03

Objective-C (see below for Swift)

Limpió el código en la respuesta superior para hacerlo más legible, menos redundante, agregó los beneficios de el método de una línea y se convirtió en una categoría NSString

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

Aplicación:

@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];

Y si tiene problemas con la codificación de salida:

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

Espero que sea tan útil para ti como lo será para mi futuro. (¡Hola, tú!)


Swift 4

Aquí hay un ejemplo de Swift haciendo uso de Pipe, Process, y String

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()
 14
Author: inket,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-20 06:36:50

También hay un buen sistema POSIX ("echo-es '\007'");

 11
Author: nes1983,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-05-25 13:15:03

Escribí esta función "C", porque NSTask es desagradable..

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, y por el bien de ser completo / inequívoco {

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

Años después, C sigue siendo un desastre desconcertante, para mí.. y con poca fe en mi capacidad para corregir mis graves defectos anteriores, la única rama de olivo que ofrezco es una versión rezhuzhed de la respuesta de @inket que es barrest of bones, para mis compañeros puristas / odiadores de la verbosidad...

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]; });
}
 7
Author: Alex Gray,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-04-01 08:54:16

Custos Mortem dijo:

Me sorprende que nadie realmente se metió en problemas de llamadas de bloqueo/no bloqueo

Para problemas de bloqueo/no bloqueo de llamadas con respecto a NSTask lea a continuación:

Asynctask.m code código de ejemplo que muestra cómo implementar flujos asincrónicos stdin, stdout y stderr para procesar datos con NSTask

Código fuente de asynctask.m está disponible en GitHub .

 3
Author: jon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-06-06 08:25:34

O como Objective C es solo C con alguna capa OO en la parte superior, puede usar las partes conterparts 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[]); 

Se incluyen desde unistd.h archivo de cabecera.

 2
Author: Paulo Lopes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-01-05 08:32:25

Si el comando Terminal requiere Privilegios de Administrador (también conocido como sudo), use AuthorizationExecuteWithPrivileges en su lugar. Lo siguiente creará un archivo llamado " com.stackoverflow.test "es el directorio raíz" / 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); 
 2
Author: SwiftArchitect,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-17 05:14:14