Ejecute un comando de terminal desde una aplicación Cocoa

204

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

perdido en el transito
fuente
2
Solo digo lo obvio: con sandboxing no puedes simplemente iniciar aplicaciones que no están en tu sandbox Y deben ser firmadas por ti para permitir esto
Daij-Djan
@ Daij-Djan eso no es cierto en absoluto, al menos no en macOS. Una aplicación de macOS sandboxed puede ejecutar cualquiera de los archivos binarios en lugares como /usr/bindonde grepvive.
jeff-h
No. Por favor demuestre que estoy equivocado;) en ist nstask no se ejecutará nada que no esté en su caja de arena.
Daij-Djan

Respuestas:

282

Puedes usar NSTask. Aquí hay un ejemplo que se 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);

NSPipey NSFileHandlese 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: interacción con el sistema operativo .

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 debe incluir esta línea mágica para mantener el 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

Gordon Wilson
fuente
1
Sí, 'argumentos = [NSArray arrayWithObjects: @ "- e", @ "foo", @ "bar.txt", nil];'
Gordon Wilson el
14
Hay una pequeña falla en tu respuesta. NSPipe tiene un búfer (establecido en el nivel del sistema operativo), que se vacía cuando se lee. Si el búfer se llena, NSTask se bloqueará y su aplicación también se bloqueará, indefinidamente. No aparecerá un mensaje de error. Esto puede suceder si NSTask devuelve mucha información. La solución es usar NSMutableData *data = [NSMutableData dataWithCapacity:512];. A continuación, while ([task isRunning]) { [data appendData:[file readDataToEndOfFile]]; }. Y "creo" que debería tener uno más [data appendData:[file readDataToEndOfFile]];después de las salidas del bucle while.
Dave Gallagher
2
Los errores no aparecerán a menos que haga esto (simplemente se imprimen en el registro): [task setStandardError: pipe];
Mike Sprague
1
Esto podría actualizarse con ARC y con los literales de matriz Obj-C. Por ejemplo, pastebin.com/sRvs3CqD
bames53
1
También es una buena idea canalizar los errores. task.standardError = pipe;
vqdave
43

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 así:

NSString *output = runCommand(@"ps -A | grep mysql");
Kenial
fuente
1
Esto maneja bien la mayoría de los casos, pero si lo ejecuta en un bucle, eventualmente genera una excepción debido a demasiados identificadores de archivos abiertos. Se puede arreglar agregando: [archivo closeFile]; después de readDataToEndOfFile.
David Stein
@DavidStein: Creo que usar autoreleasepool para envolver el método runCommand parece ser algo más que no. En realidad, el código anterior no considera también el no ARC.
Kenial
@ Kenial: Oh, esa es una solución mucho mejor. También libera los recursos rápidamente al salir del alcance.
David Stein
/ bin / ps: Operación no permitida, no obtengo ningún éxito, ¿líder?
Naman Vaishnav
40

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 hacer que el script se lea y se ejecute en tiempo de ejecución. nota: este código busca el script en la subruta 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 qué host rsync para ...) sin volver a compilar la aplicación, sino simplemente editar 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 debe incluir esta línea mágica para mantener el 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

kent
fuente
2
El enlace de explicación está muerto.
Jonny
Quiero ejecutar este comando "system_profiler SPApplicationsDataType -xml" pero recibo este error "ruta de inicio no accesible"
Vikas Bansal
25

Aquí se explica 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 Objective-C anterior de inkit. Lo escribió como una categoría en NSString- Para Swift, se convierte en una extensión deString .

extensión 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 solo:

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 Processresultado que se lee del Pipees un NSStringobjeto. Puede ser una cadena de error y también puede ser una cadena vacía, pero siempre debe ser una NSString.

Por lo tanto, siempre que no sea nulo, el resultado puede lanzarse como un Swift Stringy volver.

Si por alguna razón no NSStringse puede inicializar nada a partir de los datos del archivo, la función devuelve un mensaje de error. La función podría haberse escrito para devolver un opcional String?, pero sería difícil de usar y no tendría un propósito útil porque es muy poco probable que esto ocurra.

ElmerCat
fuente
1
Realmente agradable y elegante! Esta respuesta debería tener más votos a favor.
XueYu
Si no necesita la salida. Agregue el argumento @discardableResult delante o encima del método runCommand. Esto le permitirá llamar al método sin tener que ponerlo en una variable.
Lloyd Keijzer
let result = String (bytes: fileHandle.readDataToEndOfFile (), codificación: String.Encoding.utf8) está bien
cleexiang
18

Objetivo-C (ver abajo para Swift)

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

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

Implementació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!)


Swift 4

He aquí un ejemplo Swift haciendo uso de Pipe, ProcessyString

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()
Inket
fuente
2
De hecho, ¡tu código me fue muy útil! Lo cambié a Swift y lo publiqué como otra respuesta a continuación.
ElmerCat
14

fork , exec y wait deberían funcionar, si realmente no estás buscando una forma específica de Objective-C. forkcrea una copia del programa actualmente en ejecución, execreemplaza el programa actualmente en ejecución por uno nuevo y waitespera a que salga el subproceso. Por ejemplo (sin verificació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 hay un sistema , 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.

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

Zach Hirsch
fuente
Sé que esta es una respuesta muy antigua, pero necesito decir esto: esta es una excelente manera de usar trheads para manejar la ejecución. El único inconveniente es que crea una copia de todo el programa. entonces, para una aplicación de cacao, iría con @GordonWilson para un mejor enfoque, y si estoy trabajando en una aplicación de línea de comandos, esta es la mejor manera de hacerlo. gracias (perdón mi mal inglés)
Nicos Karalis
11

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

nes1983
fuente
66
NO EJECUTE ESTE MANDO. (En caso de que no sepa lo que hace este comando)
justin
44
Cambiado a algo un poco más segura ... (se oye un pitido)
nes1983
¿No arrojará esto un error en la consola? Incorrect NSStringEncoding value 0x0000 detected. Assuming NSStringEncodingASCII. Will stop this compatibility mapping behavior in the near future.
cwd
1
Hmm Tal vez tengas que escapar dos veces de la barra invertida.
nes1983
simplemente ejecute / usr / bin / echo o algo así. rm -rf es duro, y unicode en la consola todavía apesta :)
Maxim Veksler
8

Escribí esta función "C", porque NSTaskes 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 en aras de ser completo / inequívoco ...

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

Años más tarde, Csigue siendo un desastre desconcertante para mí ... y con poca fe en mi capacidad para corregir mis graves deficiencias anteriores: la única rama de olivo que ofrezco es una versión rezhuzhed de la respuesta de @inket que es más que huesos , para mi compañero puristas / detestadores de 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]; });
}
Alex Gray
fuente
1
outP no está definido en ningún error, chars_read es demasiado pequeño para el valor de retorno de fread () en cualquier arquitectura donde sizeof (ssize_t)! = sizeof (int), ¿qué sucede si queremos más resultados que bytes BUFSIZ? ¿Qué pasa si la salida no es UTF-8? ¿Qué pasa si pclose () devuelve un error? ¿Cómo reportamos el error de fread ()?
ObjectiveC-oder
@ ObjectiveC-oder D'oh - No lo sé. ¡Por favor, dime (como en ... edita)!
Alex Gray
3

El Custodio Mortem dijo:

Me sorprende que nadie realmente haya tenido problemas con las llamadas de bloqueo / no bloqueo

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

asynctask.m: código de muestra que muestra cómo implementar transmisiones asíncronas stdin, stdout y stderr para procesar datos con NSTask

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

jon
fuente
Vea mi contribución para una versión sin bloqueo
Guruniverse
3

Además de las varias respuestas excelentes anteriores, utilizo el siguiente código para procesar la salida del comando en segundo plano y evitar el mecanismo de bloqueo 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
fuente
Para mí, la línea que marcó la diferencia fue [self performSelectorInBackground: @selector (collectTaskOutput :) withObject: file];
neowinston
2

O como el objetivo C es solo C con alguna capa OO en la parte superior, puede usar las contrapartidas 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 el archivo de encabezado unistd.h.

Paulo Lopes
fuente
2

Si el comando Terminal requiere privilegio de administrador (también conocido como sudo), úselo AuthorizationExecuteWithPrivilegesen 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); 
Arquitecto Swift
fuente
44
Esto ha quedado oficialmente en desuso desde OS X 10.7
Sam Washburn
2
... pero sigue funcionando independientemente, ya que es la única forma de hacerlo, y muchos instaladores confían en eso, creo.
Thomas Tempelmann