Jalankan perintah terminal dari aplikasi Cocoa

204

Bagaimana saya bisa menjalankan perintah terminal (seperti grep) dari aplikasi Kakao Objective-C saya?

hilangInTransit
sumber
2
Saya hanya menyatakan yang sudah jelas: dengan sandboxing Anda tidak bisa langsung menjalankan aplikasi yang tidak ada di sandbox Anda DAN mereka perlu ditandatangani oleh Anda untuk mengizinkan ini
Daij-Djan
@ Daij-Djan itu tidak benar sama sekali, setidaknya tidak di macOS. Aplikasi MacOS kotak pasir dapat menjalankan binari apa saja di tempat-tempat seperti /usr/bintempat greptinggal.
jeff-h
Tidak. Tolong buktikan saya salah;) pada ist nstask akan gagal menjalankan apa pun yang tidak ada di kotak pasir Anda.
Daij-Djan

Jawaban:

282

Anda bisa menggunakannya NSTask. Berikut ini contoh yang akan dijalankan ' /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);

NSPipedan NSFileHandledigunakan untuk mengarahkan output standar dari tugas tersebut.

Untuk informasi lebih rinci tentang interaksi dengan sistem operasi dari dalam aplikasi Objective-C Anda, Anda dapat melihat dokumen ini di Pusat Pengembangan Apple: Berinteraksi dengan Sistem Operasi .

Sunting: Termasuk perbaikan untuk masalah NSLog

Jika Anda menggunakan NSTask untuk menjalankan utilitas baris perintah melalui bash, maka Anda perlu menyertakan jalur ajaib ini agar NSLog berfungsi:

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

Penjelasannya ada di sini: https://web.archive.org/web/20141121094204/https://cocoadev.com/HowToPipeCommandsWithNSTask

Gordon Wilson
sumber
1
Yup, 'argumen = [NSArray arrayWithObjects: @ "- e", @ "foo", @ "bar.txt", nil];'
Gordon Wilson
14
Ada kesalahan kecil dalam jawaban Anda. NSPipe memiliki buffer (diatur pada level OS), yang memerah ketika dibaca. Jika buffer terisi, NSTask akan hang, dan aplikasi Anda akan hang juga, tanpa batas. Tidak ada pesan kesalahan akan muncul. Ini dapat terjadi jika NSTask mengembalikan banyak info. Solusinya adalah menggunakan NSMutableData *data = [NSMutableData dataWithCapacity:512];. Lalu while ([task isRunning]) { [data appendData:[file readDataToEndOfFile]]; },. Dan saya "percaya" Anda harus memiliki satu lagi [data appendData:[file readDataToEndOfFile]];setelah loop sementara keluar.
Dave Gallagher
2
Kesalahan tidak akan muncul kecuali Anda melakukan ini (hanya dicetak di log): [task setStandardError: pipe];
Mike Sprague
1
Ini dapat diperbarui dengan ARC dan dengan literal array Obj-C. Misalnya pastebin.com/sRvs3CqD
bames53
1
Ini juga merupakan ide bagus untuk menyalurkan kesalahan. task.standardError = pipe;
vqdave
43

Artikel kent memberi saya ide baru. Metode runCommand ini tidak memerlukan file skrip, cukup jalankan perintah dengan satu baris:

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

Anda dapat menggunakan metode ini seperti ini:

NSString *output = runCommand(@"ps -A | grep mysql");
Kenial
sumber
1
Ini menangani sebagian besar kasus dengan baik, tetapi jika Anda menjalankannya dalam satu lingkaran, itu pada akhirnya menimbulkan pengecualian karena terlalu banyak pegangan file yang terbuka. Dapat diperbaiki dengan menambahkan: [file closeFile]; setelah membaca DataToEndOfFile.
David Stein
@ DavidStein: Saya pikir menggunakan autoreleasepool untuk membungkus metode runCommand tampaknya bukan. Sebenarnya, kode di atas tidak mempertimbangkan non-ARC juga.
Kenial
@ Kenial: Oh, itu solusi yang jauh lebih baik. Ini juga melepaskan sumber daya segera setelah meninggalkan ruang lingkup.
David Stein
/ bin / ps: Operasi tidak diizinkan, saya tidak mendapatkan kesuksesan apa pun, memimpin?
Naman Vaishnav
40

dalam semangat berbagi ... ini adalah metode yang sering saya gunakan untuk menjalankan skrip shell. Anda dapat menambahkan skrip ke bundel produk Anda (dalam fase salin build) dan kemudian meminta skrip dibaca dan dijalankan pada saat runtime. catatan: kode ini mencari skrip di sub-jalur privateFrameworks. peringatan: ini bisa menjadi risiko keamanan untuk produk yang digunakan, tetapi untuk pengembangan in-house kami, ini adalah cara mudah untuk menyesuaikan hal-hal sederhana (seperti host mana untuk rsync ke ...) tanpa mengkompilasi ulang aplikasi, tetapi hanya mengedit skrip shell dalam bundel.

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

Sunting: Termasuk perbaikan untuk masalah NSLog

Jika Anda menggunakan NSTask untuk menjalankan utilitas baris perintah melalui bash, maka Anda perlu menyertakan jalur ajaib ini agar NSLog berfungsi:

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

Dalam konteks:

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

Penjelasannya ada di sini: http://www.cocoadev.com/index.pl?NSTask

kent
sumber
2
Tautan penjelasan sudah mati.
Jonny
Saya ingin menjalankan perintah ini "system_profiler SPApplicationsDataType -xml" tetapi saya mendapatkan kesalahan ini "luncurkan jalur tidak dapat diakses"
Vikas Bansal
25

Inilah cara melakukannya di Swift

Perubahan untuk Swift 3.0:

  • NSPipe telah diganti namanya Pipe

  • NSTask telah diganti namanya Process


Ini didasarkan pada jawaban Objectit-C di atas. Dia menulisnya sebagai kategori di NSString- Untuk Swift, itu menjadi perpanjangan dari String.

ekstensi 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 ---"
        }
    }
}

Pemakaian:

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

    atau hanya:

print("echo hello".runAsCommand())   // prints "hello" 

Contoh:

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

}

Perhatikan Processhasil sebagai membaca dari Pipesebuah NSStringobjek. Mungkin string kesalahan dan juga bisa berupa string kosong, tetapi harus selalu berupa NSString.

Jadi, selama tidak nihil, hasilnya bisa berubah menjadi Swift Stringdan dikembalikan.

Jika karena alasan tertentu tidak ada NSStringsama sekali dapat diinisialisasi dari data file, fungsi mengembalikan pesan kesalahan. Fungsi itu bisa saja ditulis untuk mengembalikan opsional String?, tetapi itu akan menjadi canggung untuk digunakan dan tidak akan melayani tujuan yang bermanfaat karena sangat tidak mungkin hal ini terjadi.

ElmerCat
sumber
1
Sangat Bagus dan Elegan! Jawaban ini seharusnya memiliki lebih banyak suara positif.
XueYu
Jika Anda tidak membutuhkan output. Tambahkan argumen @discardableResult di depan atau di atas metode runCommand. Ini akan memungkinkan Anda memanggil metode tanpa harus memasukkannya ke dalam variabel.
Lloyd Keijzer
biarkan hasilnya = String (byte: fileHandle.readDataToEndOfFile (), penyandian: String.Encoding.utf8) ok
cleexiang
18

Objective-C (lihat di bawah untuk Swift)

Membersihkan kode di jawaban atas untuk membuatnya lebih mudah dibaca, kurang berlebihan, menambahkan manfaat dari metode satu baris dan dibuat menjadi kategori NSString

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

Penerapan:

@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

Pemakaian:

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

Dan jika Anda mengalami masalah dengan pengkodean keluaran:

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

Semoga bermanfaat bagi Anda dan masa depan saya. (Hai kamu!)


Cepat 4

Berikut adalah Swift contoh pembuatan penggunaan Pipe, ProcessdanString

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

Pemakaian:

let output = "echo hello".run()
inket
sumber
2
Memang, kode Anda sangat berguna bagi saya! Saya mengubahnya menjadi Swift dan mempostingnya sebagai jawaban lain di bawah ini.
ElmerCat
14

fork , exec , dan wait harus berfungsi, jika Anda tidak benar-benar mencari cara spesifik Objective-C. forkmembuat salinan dari program yang sedang berjalan, execmenggantikan program yang sedang berjalan dengan yang baru, dan waitmenunggu subproses untuk keluar. Misalnya (tanpa memeriksa kesalahan):

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

Ada juga sistem , yang menjalankan perintah seolah-olah Anda mengetiknya dari baris perintah shell. Ini lebih sederhana, tetapi Anda memiliki sedikit kendali atas situasi.

Saya berasumsi Anda sedang mengerjakan aplikasi Mac, jadi tautannya adalah ke dokumentasi Apple untuk fungsi-fungsi ini, tetapi semuanya POSIX, jadi Anda harus menggunakannya pada sistem yang mendukung POSIX.

Zach Hirsch
sumber
Saya tahu ini adalah jawaban yang sangat lama tetapi saya harus mengatakan ini: ini adalah cara yang sangat baik untuk menggunakan trheads untuk menangani eksekusi. satu-satunya downside adalah membuat salinan seluruh program. jadi untuk aplikasi kakao saya akan pergi dengan @GordonWilson untuk pendekatan yang lebih bagus, dan jika saya bekerja pada aplikasi baris perintah ini adalah cara terbaik untuk melakukannya. terima kasih (maaf bahasa Inggris saya yang buruk)
Nicos Karalis
11

Ada juga sistem POSIX lama yang bagus ("echo -en '\ 007'");

nes1983
sumber
6
JANGAN LARI PERINTAH INI. (Jika Anda tidak tahu apa perintah ini)
justin
4
Mengubahnya menjadi sesuatu yang sedikit lebih aman ... (itu berbunyi 'bip')
nes1983
Tidakkah ini akan membuat kesalahan di konsol? Incorrect NSStringEncoding value 0x0000 detected. Assuming NSStringEncodingASCII. Will stop this compatibility mapping behavior in the near future.
cwd
1
Hmm. Mungkin Anda harus meloloskan diri dari backslash.
nes1983
jalankan saja / usr / bin / echo atau sesuatu. rm -rf keras, dan unicode di konsol masih menyebalkan :)
Maxim Veksler
8

Saya menulis fungsi "C" ini, karena NSTaskmenjengkelkan ..

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, dan demi menjadi lengkap / tidak ambigu ...

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

Bertahun-tahun kemudian, Cmasih menjadi kekacauan yang membingungkan, bagi saya .. dan dengan sedikit kepercayaan pada kemampuan saya untuk memperbaiki kekurangan kotor saya di atas - satu-satunya cabang zaitun yang saya tawarkan adalah versi rezhuzhed dari jawaban @ inket yang paling sederhana tulangnya , untuk rekan saya purists / pembenci verbositas ...

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]; });
}
Alex Gray
sumber
1
outP tidak terdefinisi pada kesalahan apa pun, chars_read terlalu kecil untuk nilai kembalinya fread () pada arsitektur apa pun di mana sizeof (ssize_t)! = sizeof (int), bagaimana jika kita menginginkan lebih banyak output daripada BUFSIZ byte? Bagaimana jika outputnya bukan UTF-8? Bagaimana jika pclose () mengembalikan kesalahan? Bagaimana kami melaporkan kesalahan fread ()?
ObjectiveC-oder
@ ObjectiveC-oder D'oh - Saya tidak tahu. Tolong, beri tahu saya (seperti dalam .. sunting)!
Alex Gray
3

Custos Mortem berkata:

Saya terkejut tidak ada yang benar-benar masuk ke masalah panggilan pemblokiran / non-pemblokiran

Untuk masalah pemblokiran / non-pemblokiran terkait NSTaskmembaca di bawah ini:

asynctask.m - kode sampel yang menunjukkan cara menerapkan asynchronous stdin, stdout & stderr stream untuk memproses data dengan NSTask

Kode sumber asynctask.m tersedia di GitHub .

jon
sumber
Lihat kontribusi saya untuk versi yang tidak menghalangi
Guruniverse
3

Selain beberapa jawaban yang sangat baik di atas, saya menggunakan kode berikut untuk memproses output dari perintah di latar belakang dan menghindari mekanisme pemblokiran [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
sumber
Bagi saya, baris yang membuat perbedaan adalah [self performSelectorInBackground: @selector (collectTaskOutput :) withObject: file];
neowinston
2

Atau karena Objective C hanya C dengan beberapa layer OO di atas Anda dapat menggunakan 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[]); 

Mereka disertakan dari file header unistd.h.

Paulo Lopes
sumber
2

Jika perintah Terminal membutuhkan Administrator Privilege (alias sudo), gunakan AuthorizationExecuteWithPrivilegessaja. Berikut ini akan membuat file bernama "com.stackoverflow.test" adalah direktori root "/ 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); 
Arsitektur Swift
sumber
4
Ini telah secara resmi dihentikan sejak OS X 10.7
Sam Washburn
2
.. tetapi tetap bekerja terlepas, karena itu satu-satunya cara untuk melakukan ini, dan banyak installer bergantung pada itu, saya percaya.
Thomas Tempelmann