Cocoaアプリから端末コマンドを実行する


204

grepObjective-C Cocoaアプリケーションからターミナルコマンド(など)を実行するにはどうすればよいですか?


2
明らかなことを言っているだけです。サンドボックスでは、サンドボックスにないアプリを起動することはできません。これを許可するには、署名する必要があります
Daij-Djan

@ Daij-Djanは、少なくともmacOSではまったく真実ではありません。サンドボックス化されたmacOSアプリは、/usr/binどこにgrep住んでいるかなどの場所で任意のバイナリを実行できます。
jeff-h

いいえ。間違っていることを証明してください;)ist nstaskでサンドボックスにないものは実行できません。
Daij-Djan 2018年

回答:


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にはバッファ(OSレベルで設定)があり、読み取られるとフラッシュされます。バッファーがいっぱいになると、NSTaskがハングし、アプリも無期限にハングします。エラーメッセージは表示されません。これは、NSTaskが多くの情報を返す場合に発生する可能性があります。解決策はを使用することNSMutableData *data = [NSMutableData dataWithCapacity:512];です。その後、while ([task isRunning]) { [data appendData:[file readDataToEndOfFile]]; }。そして[data appendData:[file readDataToEndOfFile]];、whileループが終了すると、もう1つ必要になると "信じています" 。
Dave Gallagher

2
これを行わない限り、エラーは発生しません(エラーはログに出力されるだけです):[task setStandardError:pipe];
Mike Sprague

1
これは、ARCおよびObj-C配列リテラルで更新できます。例えばpastebin.com/sRvs3CqD
bames53

1
エラーをパイプ処理することもお勧めします。task.standardError = pipe;
vqdave 2014年

43

ケントの記事は私に新しい考えを与えました。このrunCommandメソッドはスクリプトファイルを必要とせず、コマンドを1行で実行します。

- (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
これはほとんどのケースをうまく処理しますが、ループで実行すると、開いているファイルハンドルが多すぎるために、最終的に例外が発生します。以下を追加することで修正できます:[file closeFile]; readDataToEndOfFileの後。
David Stein、

@DavidStein:autoCommandを使用してrunCommandメソッドをラップするのではなく、実際、上記のコードは非ARCも考慮していません。
ケニアル2016年

@ケニアル:ああ、それははるかに良い解決策です。また、スコープを離れるとすぐにリソースを解放します。
デビッドスタイン

/ bin / ps:操作は許可されていません。成功していません、リード?
Naman Vaishnav

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
説明リンクは死んでいる。
ジョニー2014

このコマンド「system_profiler SPApplicationsDataType -xml」を実行したいのですが、このエラー「起動パスにアクセスできません」が表示されます
Vikas Bansal

25

これはSwiftでそれを行う方法です

Swift 3.0の変更点:

  • NSPipe 名前が変更されました Pipe

  • NSTask 名前が変更されました Process


これは、上記のinkitのObjective-Cの回答に基づいています。彼はそれをカテゴリーとして書いた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から読み取った結果PipeNSStringオブジェクトであることに注意してください。エラー文字列の場合や空の文字列の場合もありますが、常にである必要がありNSStringます。

したがって、nilでない限り、結果をSwiftとしてキャストして返すことができStringます。

何らかの理由NSStringでファイルデータから初期化できない場合、関数はエラーメッセージを返します。この関数は、オプションのを返すように作成することもできますがString?、これを使用するのは面倒であり、これが発生する可能性が非常に低いため、有用な目的に役立ちません。


1
本当に素敵でエレガントな方法!この回答にはもっと賛成票が必要です。
XueYu 2017

出力が必要ない場合。@discardableResult引数をrunCommandメソッドの前または上に追加します。これにより、メソッドを変数に配置せずに呼び出すことができます。
ロイドケイツァー

let result = String(bytes:fileHandle.readDataToEndOfFile()、encoding:String.Encoding.utf8)は
問題ありません

18

Objective-C(Swiftについては下記を参照)

上部の回答のコードを整理して読みやすくし、冗長性を減らし、1行のメソッドの利点を追加して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

これは、、、およびを使用したSwiftの例ですPipeProcessString

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 2015

14

実際にObjective-C固有の方法を探しているのでなければ、forkexec、およびwaitが機能するはずです。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. */

また、systemもあり、シェルのコマンドラインから入力したかのようにコマンドを実行します。単純ですが、状況を制御する能力が低くなります。

私はあなたがMacアプリケーションで作業していると仮定しているので、リンクはこれらの関数に関するAppleのドキュメントへのリンクですが、それらはすべてなPOSIXので、POSIX準拠のシステムで使用する必要があります。


私はこれが非常に古い答えであることを知っていますが、私はこれを言う必要があります:これは、実行を処理するためにtrheadを使用する優れた方法です。唯一の欠点は、プログラム全体のコピーが作成されることです。ココアアプリケーションの場合、@ GordonWilsonを使用してより良いアプローチを行います。コマンドラインアプリケーションで作業している場合は、これが最善の方法です。おかげで(申し訳ありませんが私の悪い英語)
ニコスカラリス

11

また、古き良きPOSIX システム( "echo -en '\ 007'")もあります。


6
このコマンドを実行しないでください。(ケースでは、このコマンドが何をするか分からない)
ジャスティン・

4
少し安全なものに変更しました…(ビープ音)
nes1983 '08 / 12/09

これはコンソールでエラーをスローしませんか?Incorrect NSStringEncoding value 0x0000 detected. Assuming NSStringEncodingASCII. Will stop this compatibility mapping behavior in the near future.
cwd 2011

1
うーん。多分あなたはバックスラッシュをダブルエスケープする必要があります。
nes1983

/ usr / bin / echoなどを実行するだけです。rm -rfは苛酷で、コンソールのUnicodeは依然として問題です:)
Maxim Veksler

8

NSTask煩わしいのでこの「C」関数を書きました。

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の回答のrezhuzhedバージョンです純粋主義者/冗長性嫌い...

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は未定義です。sizeof(ssize_t)!= sizeof(int)のアーキテクチャでは、chars_readはfread()の戻り値に対して小さすぎます。BUFSIZバイトよりも多くの出力が必要な場合はどうでしょうか。出力がUTF-8でない場合はどうなりますか?pclose()がエラーを返すとどうなりますか?fread()のエラーをどのように報告しますか?
ObjectiveC-oder 2014年

@ ObjectiveC-oder D'oh-わかりません。教えてください。
Alex Grey 14

3

Custos Mortemさんのコメント:

ブロッキング/ノンブロッキングの通話の問題に誰も本当に夢中にならなかったのには驚きました

NSTask下記の読み取りに関するブロッキング/非ブロッキングの呼び出しの問題について:

asynctask.m-NSTaskでデータを処理するための非同期stdin、stdout、stderrストリームを実装する方法を示すサンプルコード

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

ターミナルコマンドで管理者権限(別名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以降、正式に廃止されました
Sam Washburn

2
..しかし、これはこれを行う唯一の方法であり、多くのインストーラーがそれに依存しているため、関係なく動作し続けます。
Thomas Tempelmann、2016
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.