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

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

Ответ 1

Вы можете использовать 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

Ответ 2

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

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

Ответ 3

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

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

Ответ 4

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

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

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

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


Это основано на чернильном Objective-C ответе выше. Он написал это как категорию на NSString - Для Swift это расширение String.

расширение   String.runAsCommand()   →   Строка

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 и возвращен.

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

Ответ 5

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

Ответ 6

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

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

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

Ответ 7

Существует также старый добрый POSIX system ( "echo -en '\ 007'" );

Ответ 8

Я написал эту функцию "С", потому что 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 ..
…

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

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

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

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

Ответ 9

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

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

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

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

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

Ответ 10

Или, поскольку Objective C - это просто C с некоторым слоем OO сверху, вы можете использовать posix conterparts:

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.

Ответ 11

Если команде терминала требуется привилегия администратора (aka sudo), используйте AuthorizationExecuteWithPrivileges. Ниже будет создан файл с именем "com.stackoverflow.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.stackoverflow.test", nil};

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

Ответ 12

В дополнение к нескольким превосходным ответам выше, я использую следующий код для обработки вывода команды в фоновом режиме и избегаю механизма блокировки [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];
}