Skip to content

Instantly share code, notes, and snippets.

@andreacipriani
Last active February 15, 2024 12:50
Show Gist options
  • Star 48 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save andreacipriani/8c3af3719da31c8fae2cdfa8c21e17ba to your computer and use it in GitHub Desktop.
Save andreacipriani/8c3af3719da31c8fae2cdfa8c21e17ba to your computer and use it in GitHub Desktop.
Execute shell/bash commands from Swift
import UIKit
protocol CommandExecuting {
func run(commandName: String, arguments: [String]) throws -> String
}
enum BashError: Error {
case commandNotFound(name: String)
}
struct Bash: CommandExecuting {
func run(commandName: String, arguments: [String] = []) throws -> String {
return try run(resolve(commandName), with: arguments)
}
private func resolve(_ command: String) throws -> String {
guard var bashCommand = try? run("/bin/bash" , with: ["-l", "-c", "which \(command)"]) else {
throw BashError.commandNotFound(name: command)
}
bashCommand = bashCommand.trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines)
return bashCommand
}
private func run(_ command: String, with arguments: [String] = []) throws -> String {
let process = Process()
process.launchPath = command
process.arguments = arguments
let outputPipe = Pipe()
process.standardOutput = outputPipe
process.launch()
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
let output = String(decoding: outputData, as: UTF8.self)
return output
}
}
let bash: CommandExecuting = Bash()
if let lsOutput = try? bash.run(commandName: "ls", arguments: []) { print(lsOutput) }
if let lsWithArgumentsOutput = try? bash.run(commandName: "ls", arguments: ["-la"]) { print(lsWithArgumentsOutput) }
@0xPr0xy
Copy link

0xPr0xy commented May 12, 2017

Task is renamed to Process

@andreacipriani
Copy link
Author

andreacipriani commented Dec 11, 2018

@0xPr0xy Thanks, I've updated the gist

@SSamuelsen
Copy link

How can you execute multiple commands one after another in the same terminal window?

@VasilijSviridov
Copy link

VasilijSviridov commented Mar 12, 2019

Process renamed to CommandLine

@andreacipriani
Copy link
Author

How can you execute multiple commands one after another in the same terminal window?
You could patch the code to take a collection of commands and use && to concatenate them?

@Elwyn1979
Copy link

Really intrested in adding additional commands. For example I need to run the command with 2 different arguments.

example
//path -t "message" -z "message"

this has to be in 1 line but can't get to work. Any help would be a life saver.

@vincbeaulieu
Copy link

if let lsOutput = bash.execute(commandName: "ls") { print(lsOutput) }
if let lsWithArgumentsOutput = bash.execute(commandName: "ls", arguments: ["-la"]) { print(lsWithArgumentsOutput) }

I receive "Statements are not allowed at the top level" for these two lines.

@SLboat
Copy link

SLboat commented Dec 27, 2019

suggestion change to run(),a modal way

    private func execute(command: String, arguments: [String] = []) -> String? {
        let process = Process()
        process.launchPath = command
        process.arguments = arguments
        
        let pipe = Pipe()
        process.standardOutput = pipe
        if let _ = try? process.run(){
            let data = pipe.fileHandleForReading.readDataToEndOfFile()
            let output = String(data: data, encoding: String.Encoding.utf8)
            return output
        }else{
            return nil
        }
    }

@gary17
Copy link

gary17 commented Sep 25, 2020

I believe try process.run() at line 23 should be guard (try? process.run()) != nil else { return nil }, otherwise error: errors thrown from here are not handled.

@gabrieloc
Copy link

gabrieloc commented Sep 30, 2020

Just discovered this, I have another take on this using new dynamic Swift features which would turn something like this:

let bash: CommandExecuting = Bash()
let files = bash.run(commandName: "ls", arguments: ["-la", "~/Desktop"])

into:

let bash = Command()
let files = bash.ls(
 la: "~/Desktop"
)

https://gist.github.com/gabrieloc/a27e27434ea01f6c58e24faaad344836

@gary17
Copy link

gary17 commented Sep 30, 2020

IMHO, Swift's dynamicMember syntactic sugar blurs the line between compile-time failures and runtime failures. If you introduce the bash.ls() syntax vs. bash.run(commandName: "ls") syntax, you are kind of suggesting that the ls command is a built-in API command that is not subject to the runtime command not found error. I wouldn't use dynamicMember in an interface without a good reason (but that's just me :) ).

@gabrieloc
Copy link

With my implementation, ls() will not autocomplete if working inside Xcode, and passing it as a string argument is just doing the same thing but with more steps. Unless you define a symbol, there’s no way to avoid runtime errors.

I wouldn’t use dynamicMember in production code either, but for scripting it minimizes all the boilerplate we’re used to autocompleting away

@gary17
Copy link

gary17 commented Sep 30, 2020

Also, perhaps it would be beneficial to factor out which apart from run to allow a consumer to decide what exactly to execute:: https://gist.github.com/gary17/b6c8b662da36cefee7010cd4bdcb2ee6

@moritzluedtke
Copy link

This works great, thank you for the gist!

I do get an error when using the command, although the given command is still executed. I get /bin/bash: /etc/profile: Operation not permitted before every print out of my original command.

Example code:

let cli = Bash()
if let output = try? cli.run(commandName: "pwd") {
    print(output)
}

I did change the import from UiKit to AppKit since I'm developing for MacOS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment