Executar um comando do terminal a partir de um aplicativo Cocoa

204

Como posso executar um comando de terminal (como grep) do meu aplicativo Objective-C Cocoa?

lostInTransit
fonte
2
Im apenas afirmando o óbvio: com sandboxing você não pode simplesmente ir iniciar aplicativos que não estão na sua caixa de areia e eles precisam ser assinado por você para permitir que este
Daij-Djan
@ Daij-Djan isso não é verdade, pelo menos não no macOS. Um aplicativo macOS em área restrita pode executar qualquer um dos binários em locais como /usr/binonde grepmora.
Jeff-h
Não. Por favor, prove que estou errado;) no ist nstask falhará ao executar qualquer coisa que não esteja na sua caixa de areia.
Daij-Djan

Respostas:

282

Você pode usar NSTask. Aqui está um exemplo que seria executado ' /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);

NSPipee NSFileHandlesão usados ​​para redirecionar a saída padrão da tarefa.

Para obter informações mais detalhadas sobre como interagir com o sistema operacional no aplicativo Objective-C, consulte este documento no Centro de Desenvolvimento da Apple: Interagindo com o sistema operacional .

Editar: Correção incluída para o problema NSLog

Se você estiver usando o NSTask para executar um utilitário de linha de comando via bash, precisará incluir esta linha mágica para manter o NSLog funcionando:

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

Uma explicação está aqui: https://web.archive.org/web/20141121094204/https://cocoadev.com/HowToPipeCommandsWithNSTask

Gordon Wilson
fonte
1
Sim, 'argumentos = [NSArray arrayWithObjects: @ "- e", @ "foo", @ "bar.txt", nulo];'
Gordon Wilson
14
Há uma pequena falha na sua resposta. O NSPipe possui um buffer (definido no nível do SO), que é liberado quando é lido. Se o buffer ficar cheio, o NSTask travará e o aplicativo travará indefinidamente. Nenhuma mensagem de erro será exibida. Isso pode acontecer se o NSTask retornar muitas informações. A solução é usar NSMutableData *data = [NSMutableData dataWithCapacity:512];. Então while ([task isRunning]) { [data appendData:[file readDataToEndOfFile]]; },. E eu "acredito" que você deve ter mais um [data appendData:[file readDataToEndOfFile]];após o ciclo while terminar.
Dave Gallagher
2
Os erros não serão exibidos, a menos que você faça isso (eles apenas serão impressos no log): [task setStandardError: pipe];
8268 Mike Sprague
1
Isso pode ser atualizado com o ARC e com os literais da matriz Obj-C. Por exemplo, pastebin.com/sRvs3CqD
bames53
1
Também é uma boa ideia canalizar os erros. task.standardError = pipe;
vqdave
43

O artigo de Kent me deu uma nova idéia. esse método runCommand não precisa de um arquivo de script, apenas executa um comando por uma linha:

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

Você pode usar este método como este:

NSString *output = runCommand(@"ps -A | grep mysql");
Kenial
fonte
1
Isso lida bem com a maioria dos casos, mas se você executá-lo em um loop, eventualmente gera uma exceção devido a muitos identificadores de arquivos abertos. Pode ser corrigido adicionando: [file closeFile]; após readDataToEndOfFile.
David Stein
@ DavidStein: Eu acho que usando autoreleasepool para embrulhar o método runCommand parece ser um pouco do que. Na verdade, o código acima também não considera o ARC.
Kenial 17/05
@ Kenial: Oh, essa é uma solução muito melhor. Ele também libera os recursos imediatamente após sair do escopo.
David Stein
/ bin / ps: operação não permitida, não estou obtendo sucesso, lidera?
Naman Vaishnav
40

no espírito de compartilhar ... esse é um método que eu uso frequentemente para executar scripts de shell. você pode adicionar um script ao seu pacote de produtos (na fase de cópia da construção) e, em seguida, fazer com que o script seja lido e executado em tempo de execução. nota: esse código procura o script no subcaminho privateFrameworks. aviso: isso pode ser um risco à segurança dos produtos implantados, mas, para o nosso desenvolvimento interno, é uma maneira fácil de personalizar coisas simples (como qual host do rsync ...) sem recompilar o aplicativo, mas apenas editar o script de shell no pacote.

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

Editar: Correção incluída para o problema NSLog

Se você estiver usando o NSTask para executar um utilitário de linha de comando via bash, precisará incluir esta linha mágica para manter o NSLog funcionando:

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

No contexto:

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

Uma explicação está aqui: http://www.cocoadev.com/index.pl?NSTask

Kent
fonte
2
O link de explicação está morto.
Jonny
Eu quero executar este comando "system_profiler SPApplicationsDataType -xml", mas eu estou recebendo este erro "caminho de inicialização não acessível"
Vikas Bansal
25

Veja como fazer isso no Swift

Alterações no Swift 3.0:

  • NSPipe foi renomeado Pipe

  • NSTask foi renomeado Process


Isto é baseado na resposta Objective-C do inkit acima. Ele escreveu como uma categoria em NSString- Para Swift, isso se torna uma extensão de String.

extensão 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 ---"
        }
    }
}

Uso:

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

    ou apenas:

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

Exemplo:

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

}

Observe o Processresultado como lido a partir de Pipeum NSStringobjeto. Pode ser uma sequência de erros e também pode ser uma sequência vazia, mas sempre deve ser uma NSString.

Portanto, desde que não seja nulo, o resultado pode ser convertido em Swift Stringe retornado.

Se, por algum motivo, não for NSStringpossível inicializar a partir dos dados do arquivo, a função retornará uma mensagem de erro. A função poderia ter sido escrita para retornar um opcional String?, mas seria difícil de usar e não serviria a um propósito útil, porque é muito improvável que isso ocorra.

ElmerCat
fonte
1
Maneira realmente agradável e elegante! Esta resposta deve ter mais votos positivos.
XueYu
Se você não precisa da saída. Adicione o argumento @discardableResult na frente ou acima do método runCommand. Isso permitirá que você chame o método sem precisar colocá-lo em uma variável.
Lloyd Keijzer
deixar resultado = corda (bytes: fileHandle.readDataToEndOfFile (), que codifica: String.Encoding.utf8) é ok
cleexiang
18

Objective-C (veja abaixo para Swift)

Limpou o código na resposta superior para torná-lo mais legível, menos redundante, adicionou os benefícios do método de uma linha e transformado em uma categoria NSString

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

Implementação:

@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

Uso:

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

E se você estiver tendo problemas com a codificação de saída:

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

Espero que seja tão útil para você como será para o meu futuro. (Olá, você!)


Swift 4

Aqui está um exemplo Swift fazendo uso de Pipe, ProcesseString

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

Uso:

let output = "echo hello".run()
entrada
fonte
2
De fato, seu código foi muito útil para mim! Mudei para Swift e publiquei como outra resposta abaixo.
ElmerCat
14

fork , exec e wait devem funcionar, se você realmente não está procurando uma maneira específica de Objective-C. forkcria uma cópia do programa atualmente em execução, execsubstitui o programa atualmente em execução por um novo e waitaguarda a saída do subprocesso. Por exemplo (sem nenhuma verificação de erro):

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

Há também o sistema , que executa o comando como se você o digitasse na linha de comando do shell. É mais simples, mas você tem menos controle sobre a situação.

Suponho que você esteja trabalhando em um aplicativo Mac, então os links são para a documentação da Apple para essas funções, mas são todas POSIX, portanto, você deve usá-los em qualquer sistema compatível com POSIX.

Zach Hirsch
fonte
Sei que essa é uma resposta muito antiga, mas preciso dizer o seguinte: esta é uma excelente maneira de usar trheads para lidar com a execução. a única desvantagem é que ele cria uma cópia de todo o programa. portanto, para um aplicativo de cacau, eu usaria o @GordonWilson para uma abordagem mais agradável e, se estou trabalhando em um aplicativo de linha de comando, é a melhor maneira de fazê-lo. graças (desculpe meu mau Inglês)
Nicos Karalis
11

Também existe um bom sistema POSIX antigo ("echo -en '\ 007'");

nes1983
fonte
6
NÃO EXECUTE ESTE COMANDO. (Caso você não saiba o que esse comando faz)
justin
4
Mudou para algo um pouco mais seguro ... (ele emite um sinal sonoro)
nes1983
Isso não gerará um erro no console? Incorrect NSStringEncoding value 0x0000 detected. Assuming NSStringEncodingASCII. Will stop this compatibility mapping behavior in the near future.
Cwd
1
Hmm. Talvez você precise escapar duas vezes da barra invertida.
N171983 17/12
basta executar / usr / bin / eco ou algo assim. rm-rf é dura, e unicode no console ainda suga :)
Maxim Veksler
8

Eu escrevi essa função "C", porque NSTask é desagradável ..

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

ah, e por uma questão de ser completa / inequívoca…

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

Anos mais tarde, Cainda é uma confusão desconcertante, para mim .. e com pouca fé na minha capacidade de corrigir meus defeitos brutas acima - a oferta só ramo de oliveira que é uma versão rezhuzhed de resposta de @ inket que é mais desencapado dos ossos , para o meu companheiro puristas / odiadores da verbosidade ...

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
fonte
1
outP é indefinido em qualquer erro, chars_read é muito pequeno para o valor de retorno de fread () em qualquer arquitetura em que sizeof (ssize_t)! = sizeof (int), e se quisermos mais saída do que bytes BUFSIZ? E se a saída não for UTF-8? E se pclose () retornar um erro? Como reportamos o erro de fread ()?
precisa
@ ObjectiveC-oder D'oh - eu não sei. Por favor, me diga (como em .. edite)!
Alex Gray
3

Custos Mortem disse:

Estou surpreso que ninguém realmente tenha entrado em problemas de chamadas de bloqueio / não bloqueio

Para problemas de chamada de bloqueio / não bloqueio relacionados à NSTaskleitura abaixo:

asynctask.m - código de exemplo que mostra como implementar fluxos assíncronos stdin, stdout e stderr para processar dados com o NSTask

O código fonte do asynctask.m está disponível no GitHub .

Jon
fonte
Veja minha contribuição para uma versão sem bloqueio
Guruniverse
3

Além das várias excelentes respostas acima, eu uso o código a seguir para processar a saída do comando em segundo plano e evitar o mecanismo de bloqueio de [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
fonte
Para mim, a linha que fez toda a diferença foi [self performSelectorInBackground: @selector (collectTaskOutput :) withObject: file];
Neowinston 01/05/19
2

Ou como o Objective C é apenas C com uma camada OO na parte superior, você pode usar as contrapartes 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[]); 

Eles estão incluídos no arquivo de cabeçalho unistd.h.

Paulo Lopes
fonte
2

Se o comando Terminal exigir privilégio de administrador (aka sudo), use em seu AuthorizationExecuteWithPrivilegeslugar. A seguir, será criado um arquivo chamado "com.stackoverflow.test", o diretório raiz "/ 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
fonte
4
Isso foi oficialmente descontinuado desde o OS X 10.7
Sam Washburn
2
.. mas continua funcionando independentemente, pois é a única maneira de fazer isso, e muitos instaladores confiam nisso, acredito.
Thomas Tempelmann