¿Cómo puedo ejecutar un comando de terminal (como grep
) desde mi aplicación Objective-C Cocoa?
/usr/bin
donde grep
vive.
¿Cómo puedo ejecutar un comando de terminal (como grep
) desde mi aplicación Objective-C Cocoa?
/usr/bin
donde grep
vive.
Respuestas:
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);
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: 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
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.
task.standardError = pipe;
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");
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
Cambios para Swift 3.0:
NSPipe
ha sido renombradoPipe
NSTask
ha sido renombradoProcess
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
.
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 ---"
}
}
}
let input = "echo hello"
let output = input.runAsCommand()
print(output) // prints "hello"
o solo:
print("echo hello".runAsCommand()) // prints "hello"
@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 Process
resultado que se lee del Pipe
es un NSString
objeto. 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 String
y volver.
Si por alguna razón no NSString
se 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.
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!)
He aquí un ejemplo Swift haciendo uso de Pipe
, Process
yString
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()
fork , exec y wait deberían funcionar, si realmente no estás buscando una forma específica de Objective-C. fork
crea una copia del programa actualmente en ejecución, exec
reemplaza el programa actualmente en ejecución por uno nuevo y wait
espera 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.
También hay un buen sistema POSIX antiguo ("echo -en '\ 007'");
Incorrect NSStringEncoding value 0x0000 detected. Assuming NSStringEncodingASCII. Will stop this compatibility mapping behavior in the near future.
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 en aras de ser completo / inequívoco ...
#define $UTF8(A) ((NSString*)[NSS stringWithUTF8String:A])
Años más tarde, C
sigue 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]; });
}
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 NSTask
leer 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 .
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];
}
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.
Si el comando Terminal requiere privilegio de administrador (también conocido como sudo
), úselo 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);