Виконайте команду терміналу з програми Cocoa


204

Як я можу виконати команду термінала (як grep) з мого додатка Cocoa-Objective-C?


2
Я просто констатував очевидне: з пісочницею ви не можете просто запустити програми, які не знаходяться у вашій пісочниці, і вони повинні бути підписані вами, щоб дозволити це
Daij-Djan

@ Daij-Djan, це зовсім не так, принаймні, не в macOS. Ізольований MacOS додаток може запустити будь-якого з бінарних файлів в таких місцях , як /usr/binде grepжиття.
jeff-h

Ні. Будь ласка, доведіть мене неправильно;) ist ist nstask не зможе запустити нічого, що не є у вашій пісочниці.
Daij-Djan

Відповіді:


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
Так, 'аргументи = [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]];після закінчення циклу.
Дейв Галлахер

2
Помилки не з’являться, якщо ви цього не зробите (вони просто надрукуються у журналі): [task setStandardError: pipe];
Майк Спраг

1
Це може бути оновлено ARC та літералами масиву Obj-C. Наприклад, pastebin.com/sRvs3CqD
bames53

1
Також непогано виправити помилки. task.standardError = pipe;
vqdave

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

1
Це обробляє більшість випадків добре, але якщо ви запускаєте його в циклі, це врешті-решт створює виняток через занадто багато ручок відкритого файлу. Можна виправити, додавши: [файл 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


Це ґрунтується на вищезазначеній відповіді об'єктива-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об'єктом. Це може бути рядок помилки, він також може бути порожнім рядком, але він завжди повинен бути an NSString.

Отже, доки це не буде нульовим, результат може видаватись як Швидкий Stringі повертається.

Якщо з якихось причин NSStringз даних файлу взагалі не може бути ініціалізовано, функція повертає повідомлення про помилку. Функція могла бути написана для повернення необов'язкового String?, але це було б незручно у використанні і не слугувало б корисній меті, оскільки це так малоймовірно.


1
Дійсно приємний та елегантний спосіб! Ця відповідь повинна мати більше відгуків.
XueYu

Якщо вам не потрібен вихід. Додайте аргумент @discardableResult на початку або вище методу runCommand. Це дозволить вам зателефонувати за методом без необхідності вводити його в змінну.
Ллойд Кейзер

нехай результат = String (байти: 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];

Сподіваюся, це так само корисно вам, як і майбутньому мені. (Привіт, ти!)


Швидкий 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()

2
Дійсно, ваш код був мені дуже корисний! Я змінив його на Swift і опублікував це як ще одну відповідь нижче.
ElmerCat

14

fork , exec і чекати повинні працювати, якщо ви не дуже шукаєте конкретний шлях для 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.


Я знаю, що це дуже стара відповідь, але мені потрібно сказати це: це чудовий спосіб використовувати триголовки для обробки виправдання. Єдиним недоліком є ​​те, що він створює копію всієї програми. тож для програми з какао я б пішов з @GordonWilson для кращого підходу, і якщо я працюю над додатком командного рядка, це найкращий спосіб зробити це. дякую (вибачте, моя погана англійська)
Нікос Караліс

11

Існує також стара стара система POSIX ("echo -en '\ 007'");


6
НЕ РАБОТИ ЦУ КОМАНДУ. (Якщо ви не знаєте, що робить ця команда)
Justin

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 суворий, і unicode в консолі все ще смокче :)
Максим Векслер

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

о, і заради того, щоб бути повною / однозначною ...

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

1
outP не визначено для будь-якої помилки, chars_read занадто малий, щоб повернути значення fread () для будь-якої архітектури, де sizeof (ssize_t)! Що робити, якщо вихід не UTF-8? Що робити, якщо pclose () повертає помилку? Як ми повідомляємо про помилку fread ()?
ObjectiveC-oder

@ ObjectiveC-oder D'oh - я не знаю. Скажіть, будь ласка, (як у .. редагувати)!
Алекс Грей

3

Custos Mortem сказав:

Я здивований, що ніхто насправді не стикався з питаннями блокування / незаблокування дзвінків

Для блокування / неблокування викликів щодо 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];
}

Для мене рядом, який змінив значення, було [self performSelectorInBackground: @selector (collectionTaskOutput :) withObject: file];
neowinston

2

Або оскільки ціль 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

Якщо команда Terminal вимагає привілею адміністратора (ака 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); 

4
Це було офіційно знято з моменту OS X 10.7
Сем Уошберн

2
.. але він продовжує працювати незалежно, оскільки це єдиний спосіб зробити це, і я вважаю, що багато інсталяторів покладаються на це.
Томас Темпельман
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.