POSIXコマンドをObjective-Cで使用しようと試行錯誤した。


概要

今回は例として、POSIXコマンドのchflagsを使って下記のフォルダ内のファイルを隠しファイルにすることを考える。

chflagsのパスは/usr/bin/chflagsである。

コマンド 説明
chflags hidden <ファイル名> 隠し属性を付与する
chflags nohidden <ファイル名> 隠し属性を取る

ターミナルでの呼び出し方は以下の通り。(~には自分の環境のパスを挿入)

ターミナルでの実行例
chflags hidden ~/Downloads/instance/SampleFolder/sample.txt

実装

これと同じことObjective-Cのコードで、NSTaskを用いて実行する。

呼び出し部
NSString *sampleFilePathWithTilde = @"~/Downloads/instance/SampleFolder/sample.txt";
NSString *sampleFilePath          = [sampleFilePathWithTilde stringByExpandingTildeInPath];

NSURL *chflags = [NSURL fileURLWithPath:@"/usr/bin/chflags"];
[self executeSelectCommand:chflags args:@[@"hidden", sampleFilePath]];
関数部分
/**
 @brief  POSIXコマンドのパスとオプションを格納した配列を渡して実行する
 @param  commandURL POSIXコマンドのパス args 引数
 @return 正常終了でYES
 */
- (BOOL)executeSelectCommand:(NSURL *)commandURL args:(NSArray *)args {
    // タスクオブジェクトを準備する
    NSTask *task = [[NSTask alloc] init];
    [task setLaunchPath:[commandURL path]];
    [task setArguments:args];

    // 読み込み元のパイプを作成する
    NSPipe *outPipe = [[NSPipe alloc] init];
    [task setStandardOutput:outPipe];

    // プロセスを起動する
    [task launch];

    // タスクが正常に終了することを確認する
    [task waitUntilExit];
    int status = [task terminationStatus];

    // ステータスを確認する
    if (status != 0) {
        return NO;
    }
    return YES;
}

実行後、隠し属性になっていることを確認。

上記のコードの改善

SampleFolder内のファイルを全て隠し属性にしようと考える。
ターミナルでの呼び出し方は以下の通り。(~には自分の環境のパスを挿入)

ターミナルでの実行例
chflags hidden ~/Downloads/instance/SampleFolder/*

しかし同じことを上記のコードで行おうとすると、ワイルドカードである「*」を展開できないという不都合が生じる。

[self executeSelectCommand:chflags args:@[@"hidden", @"*"]];
ログ
chflags: *: No such file or directory

ディレクトリが見つからないというエラー。
「*」を展開できていないようである。

問題の詳細

NSTask doesn't expand the * in your path. But if you invoke the command through the shell (i.e. /bin/bash), it will:

NSTaskで外部コマンドを実行する場合、外部コマンドをフルパスで指定しないといけないらしい。マシン環境によってコマンドはマシンによって"/opt/local/bin"だったり"/usr/local/bin"だったりするので、フルパスでコマンドを指定すると動かなかったりするので、"/bin/sh -c"を経由させてコマンドの実行パスを解決すると幸せになれるみたいです。

  • 以上より、下記の通り手を加えた。
    • よりターミナルの操作に近い形で呼び出せるように引数を変更。
      • 第一引数はNSArrayからNSStringにした。
      • 第二引数はコマンド実行前にcdしてカレントパスを変更のと同義。
    • POSIXコマンドの呼び出しにShellを通すようにした。

改善後の実装

呼び出し部
NSString *sampleFolderPathWithTilde = @"~/Downloads/instance/SampleFolder";
NSString *sampleFolderPath          = [sampleFolderPathWithTilde stringByExpandingTildeInPath];
NSURL    *sampleFolderURL           = [NSURL fileURLWithPath:sampleFolderPath];
[self executePosixThroughBashWithStatement:@"chflags hidden *" posixExecutingURL:sampleFolderURL];
関数部分
/**
 @brief Shellを通じてPosixコマンドを実行する(*を展開するために用意)
 @param statement 引数の文字列 ex.@"chflags hidden *"
 @param aCurrentDirectory Posixコマンドの実行上のパス(空文字可)
 @return BOOL Posix実行正常終了でYES
 */
- (BOOL)executePosixThroughBashWithStatement:(NSString *)statement posixExecutingURL:(NSURL *)aCurrentDirectory {
    NSArray *args = @[@"-c", statement];

    // タスクオブジェクトを準備する
    NSTask *task = [[NSTask alloc] init];
    task.launchPath = @"/bin/sh"; // ワイルドカードを展開するためbashを経由する
    task.arguments  = args;
    // 読み込み元のパイプを作成する
    NSPipe *pOutput = [NSPipe pipe];  // 標準出力先
    NSPipe *pError  = [NSPipe pipe];  // エラー出力先
    task.standardOutput = pOutput;
    task.standardError  = pError;
    if (aCurrentDirectory.path.length != 0) {
        task.currentDirectoryURL = aCurrentDirectory;
    }

    // プロセスを起動する
    [task launch];
    // タスクが正常に終了することを確認する
    [task waitUntilExit];

    NSData *dataError = [[pError fileHandleForReading] availableData]; //エラーのデータ
    if (dataError.length != 0) {
        NSLog(@"error-%s", [dataError bytes]); //エラー出力
    }

    NSData   *dataOutput   = [[pOutput fileHandleForReading] availableData];    //標準出力データ
    NSString *stringOutput = [[NSString alloc] initWithData:dataOutput encoding:NSShiftJISStringEncoding]; //標準出力
    if (stringOutput.length != 0) {
        NSLog(@"\n*** UnixOutput ***\n%@", stringOutput);
    }

    // ステータスを確認する
    if ([task terminationStatus] != 0) {
        return NO;
    }
    return YES;
}

全て隠し属性になっていることを確認。

第一引数ですが、argumentsの要素を小分けにするとうまく行かなかったので、NSStringで書き直した。

NSArray *args = @[@"-c", @"chflags hidden *"];

下記のコードで実行したところエラーがでた。(最初の例ではうまく行っていたが何故だろうか)

NSArray *args = @[@"-c", @"chflags", @"hidden", @"*"];
エラーログ
error-usage: chflags [-fhv] [-R [-H | -L | -P]] flags file ...
G…

余談:ログへのアウトプットとかは返り値にしたりしたほうが、デバッグしやすいかもなとも思う。