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

204

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

lostInTransit
источник
2
Я просто констатирую очевидное: с песочницей вы не можете просто запускать приложения, которых нет в вашей песочнице, и они должны быть подписаны вами, чтобы это разрешить
Daij-Djan
@ Daij-Djan это совсем не так, по крайней мере, в macOS. Приложение macOS из песочницы может запускать любые двоичные файлы в таких местах, как, например, место, /usr/binгде они grepживут.
Джефф-ч
Нет. Пожалуйста, докажите, что я не прав;) на ist nstask не сможет запустить ничего, кроме вашей песочницы.
Дай-Джан

Ответы:

282

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

Гордон Уилсон
источник
1
Да, 'arguments = [NSArray arrayWithObjects: @ "- e", @ "foo", @ "bar.txt", nil];'
Гордон Уилсон
14
В вашем ответе есть небольшая ошибка. NSPipe имеет буфер (установлен на уровне ОС), который сбрасывается при чтении. Если буфер заполнится, NSTask будет зависать, и ваше приложение будет зависать, на неопределенный срок. Сообщение об ошибке не появится. Это может произойти, если NSTask возвращает много информации. Решение заключается в использовании NSMutableData *data = [NSMutableData dataWithCapacity:512];. Тогда while ([task isRunning]) { [data appendData:[file readDataToEndOfFile]]; }. И я «верю», что у вас должен быть еще один [data appendData:[file readDataToEndOfFile]];после выхода из цикла while.
Дэйв Галлахер
2
Ошибки не появятся, если вы не сделаете это (они просто будут напечатаны в журнале): [task setStandardError: pipe];
Майк Спраг,
1
Это можно обновить с помощью ARC и литералов массива Obj-C. Например, pastebin.com/sRvs3CqD
bames53
1
Это также хорошая идея, чтобы передать ошибки. task.standardError = pipe;
августа
43

Статья Кента дала мне новую идею. этот метод 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");
Kenial
источник
1
Это хорошо справляется с большинством случаев, но если вы запускаете его в цикле, в конечном итоге возникает исключение из-за слишком большого количества дескрипторов открытых файлов. Можно исправить, добавив: [file closeFile]; после readDataToEndOfFile.
Дэвид Стейн
@DavidStein: Я думаю, что использование autoreleasepool для переноса метода runCommand, скорее всего, не так. На самом деле, приведенный выше код также не учитывает не-ARC.
Кениал
@Kenial: О, это гораздо лучшее решение. Он также быстро освобождает ресурсы после выхода из области действия.
Дэвид Стейн
/ bin / ps: операция не разрешена, я не добиваюсь успеха, ведешь?
Наман Вайшнав
40

в духе обмена ... это метод, который я часто использую для запуска сценариев оболочки. Вы можете добавить сценарий в комплект вашего продукта (на этапе копирования сборки), а затем выполнить чтение и запуск сценария во время выполнения. примечание: этот код ищет скрипт в подпути 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

Кент
источник
2
Ссылка на объяснение мертва.
Джонни
Я хочу запустить эту команду "system_profiler SPApplicationsDataType -xml", но я получаю эту ошибку "Путь запуска недоступен"
Vikas Bansal
25

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

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

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

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


Это основано на ответе Obit-C от Inkit выше. Он написал ее в качестве категории по NSString- Для Swift, она становится продолжением из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?, но это было бы неудобно для использования и не могло бы служить полезной цели, поскольку это вряд ли произойдет.

ElmerCat
источник
1
Действительно хороший и элегантный способ! Этот ответ должен иметь больше голосов.
Сюэю
Если вам не нужен вывод. Добавьте аргумент @discardableResult перед или выше метода runCommand. Это позволит вам вызывать метод без необходимости помещать его в переменную.
Ллойд Кейзер
let result = String (bytes: fileHandle.readDataToEndOfFile (), кодировка: String.Encoding.utf8) в порядке
cleexiang
18

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

Вот Свифт использование пример изготовление 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()
inket
источник
2
Действительно, ваш код был очень полезен для меня! Я изменил его на Swift и опубликовал его как другой ответ ниже.
ElmerCat
14

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-совместимой системе.

Зак Хирш
источник
Я знаю, что это очень старый ответ, но мне нужно сказать следующее: это отличный способ использовать trheads для обработки исключения. Единственным недостатком является то, что он создает копию всей программы. поэтому для приложения с какао я бы выбрал @GordonWilson для лучшего подхода, и если я работаю над приложением командной строки, это лучший способ сделать это. спасибо (извините, мой плохой английский)
Никос Каралис
11

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

nes1983
источник
6
НЕ ПРОПУСТИТЕ ЭТУ КОМАНДУ. (Если вы не знаете, что делает эта команда)
Джастин
4
Поменял его на что-то более безопасное… (
гудит
Не приведет ли это к ошибке в консоли? Incorrect NSStringEncoding value 0x0000 detected. Assuming NSStringEncodingASCII. Will stop this compatibility mapping behavior in the near future.
cwd
1
Хм. Может быть, вы должны дважды избежать обратной косой черты.
nes1983
просто запустите / usr / bin / echo или что-то еще. rm -rf суров, а юникод в консоли все еще отстой :)
Максим Векслер
8

Я написал эту функцию "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, которая является чуть-чуть костей для моего товарища пуристы / многословные ненавистники ...

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]; });
}
Алекс Грей
источник
1
outP не определен при любой ошибке, chars_read слишком мал для возвращаемого значения fread () в любой архитектуре, где sizeof (ssize_t)! = sizeof (int), что если нам нужно больше вывода, чем байтов BUFSIZ? Что делать, если вывод не UTF-8? Что если pclose () вернет ошибку? Как мы сообщаем об ошибке fread ()?
ObjectiveC-oder
@ ObjectiveC-oder D'Oh - я не знаю. Пожалуйста, скажите мне (как в ... редактировать прочь)!
Алекс Грей
3

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

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

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

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

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

джон
источник
Смотрите мой вклад для неблокирующей версии
Guruniverse
3

В дополнение к нескольким превосходным ответам, приведенным выше, я использую следующий код, чтобы обработать вывод команды в фоновом режиме и избежать механизма блокировки [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
источник
Для меня строка, которая имела все значение, была [self executeSelectorInBackground: @selector (collectTaskOutput :) withObject: file];
Neowinston
2

Или, поскольку 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.

Пауло Лопес
источник
2

Если для команды «Терминал» требуется привилегия администратора (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); 
SwiftArchitect
источник
4
Это официально не рекомендуется с OS X 10.7
Сэм Уошберн
2
... но он продолжает работать независимо, поскольку это единственный способ сделать это, и я полагаю, что многие установщики полагаются на это.
Томас Темпельманн