Выполнить команду терминала из приложения Какао

Как я могу выполнить команду терминала (например, grep) из моего приложения Objective-C Какао?

12 ответов

Решение

Ты можешь использовать NSTask, Вот пример, который будет работать/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 а также NSFileHandle используются для перенаправления стандартного вывода задачи.

Более подробную информацию о взаимодействии с операционной системой из приложения Objective-C вы можете найти в этом документе в Центре разработки Apple: Взаимодействие с операционной системой.

Изменить: Включено исправление для проблемы NSLog

Если вы используете NSTask для запуска утилиты командной строки через bash, то вам нужно включить эту магическую строку, чтобы NSLog работал:

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

Объяснение здесь: https://web.archive.org/web/20141121094204/https://cocoadev.com/HowToPipeCommandsWithNSTask

Статья Кента дала мне новую идею. этот метод runCommand не нуждается в файле сценария, он просто запускает команду по строке:

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

Вы можете использовать этот метод следующим образом:

NSString *output = runCommand(@"ps -A | grep mysql");

В духе обмена... это метод, который я часто использую для запуска сценариев оболочки. Вы можете добавить сценарий в комплект вашего продукта (на этапе копирования сборки), а затем выполнить чтение и запуск сценария во время выполнения. примечание: этот код ищет скрипт в подпути privateFrameworks. предупреждение: это может представлять угрозу безопасности для развернутых продуктов, но для нашей собственной разработки это простой способ настроить простые вещи (например, с какого хоста на rsync...), не перекомпилируя приложение, а просто отредактировав Сценарий оболочки в комплекте.

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

Изменить: Включено исправление для проблемы NSLog

Если вы используете NSTask для запуска утилиты командной строки через bash, то вам нужно включить эту магическую строку, чтобы NSLog работал:

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

В контексте:

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

Объяснение здесь: http://www.cocoadev.com/index.pl?NSTask

Вот как это сделать в Swift

Изменения для Swift 3.0:

  • NSPipe был переименован Pipe

  • NSTask был переименован Process


Это основано на ответе Obit-C от Inkit выше. Он написал это как категорию на NSString - Для Свифта это становится продолжением String,

расширение 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 ---"
        }
    }
}

Использование:

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

или просто:

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

}

Обратите внимание Process результат, прочитанный из Pipe является NSString объект. Это может быть строка ошибки, а также пустая строка, но она всегда должна быть NSString,

Так что, пока это не ноль, результат можно разыграть как Swift String и вернулся.

Если по какой-то причине нет NSString вообще может быть инициализирован из данных файла, функция возвращает сообщение об ошибке. Функция могла быть написана для возврата необязательного String?, но это было бы неудобно для использования и не послужило бы полезной цели, потому что это вряд ли произойдет.

Objective-C (см. Ниже для Swift)

Очистили код в верхнем ответе, чтобы сделать его более читабельным, менее избыточным, добавили преимущества однострочного метода и превратили в категорию NSString

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

Реализация:

@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

Использование:

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

И если у вас есть проблемы с кодировкой вывода:

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

Надеюсь, это так же полезно для вас, как и для меня в будущем. (Привет!)


Swift 4

Вот пример Swift с использованием Pipe, Process, а также 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)
    }
}

Использование:

let output = "echo hello".run()

fork, exec и wait должны работать, если вы на самом деле не ищете специфический для Objective-C способ. fork создает копию запущенной в данный момент программы, exec заменяет текущую запущенную программу новой и wait ожидает выхода из подпроцесса. Например (без проверки ошибок):

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

Также есть система, которая запускает команду так, как будто вы набрали ее из командной строки оболочки. Это проще, но у вас меньше контроля над ситуацией.

Я предполагаю, что вы работаете над приложением Mac, так что ссылки на документацию Apple для этих функций, но они все POSIXпоэтому вы должны использовать их в любой POSIX-совместимой системе.

Существует также старая добросовестная система POSIX ("echo -en '\007'");

Я написал эту функцию "C", потому что NSTask это неприятно..

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 ..
…

о, и ради того, чтобы быть полным / однозначным...

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

Годы спустя, C для меня это все еще изумительный беспорядок... и с малой верой в мою способность исправить мои грубые недостатки выше - единственная оливковая ветвь, которую я предлагаю, это rezhuzhed версия ответа @inket, которая является чуть-чуть костей, для моих собратьев-пуристов / многословия -haters...

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

В дополнение к нескольким превосходным ответам, приведенным выше, я использую следующий код, чтобы обработать вывод команды в фоновом режиме и избежать механизма блокировки [file readDataToEndOfFile],

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

    [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];
}

Кастос Мортем сказал:

Я удивлен, что никто не сталкивался с проблемами блокировки / неблокирования вызовов.

Для блокирующих / неблокирующих вызовов, связанных с NSTask читай ниже:

asynctask.m - пример кода, показывающий, как реализовать асинхронные потоки stdin, stdout и stderr для обработки данных с помощью NSTask

Исходный код asynctask.m доступен на GitHub.

Если команда "Терминал" требует привилегий администратора sudo), используйте AuthorizationExecuteWithPrivileges вместо. Далее будет создан файл с именем "com.stackru.test", являющийся корневым каталогом "/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.stackru.test", nil};

err = AuthorizationExecuteWithPrivileges(authorizationRef,
                                         command,
                                         kAuthorizationFlagDefaults,
                                         args,
                                         &pipe); 

Или, поскольку Objective C - это просто C с некоторым слоем OO сверху, вы можете использовать составные части 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[]); 

Они включены в заголовочный файл unistd.h.

Другие вопросы по тегам