Skip to content

Instantly share code, notes, and snippets.

@NSExceptional
Created June 25, 2022 23:31
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save NSExceptional/33837b97966ed95b5a84f4aa3a5027f6 to your computer and use it in GitHub Desktop.
Save NSExceptional/33837b97966ed95b5a84f4aa3a5027f6 to your computer and use it in GitHub Desktop.
Leverage @dynamicMemberLookup to invoke shell commands dynamically
//
// Shell.swift
//
// Created by Tanner Bennett on 6/25/22.
// Copyright Tanner Bennett (c) 2022
//
import Foundation
extension StringProtocol {
func split(first separator: Character) -> [String] {
return self.split(separator: separator, maxSplits: 1, omittingEmptySubsequences: false)
.map { String($0) }
}
func split(on separator: Character) -> [String] {
return self.split(separator: separator)
.map { String($0) }
}
}
enum ShellBuiltins {
typealias Command = (_ args: [String]) throws -> String
static let lookup: [String: Command] = [
"cd": cd(args:),
// TODO: add more
]
static func cd(args: [String]) throws -> String {
FileManager.default.changeCurrentDirectoryPath(args.first!)
return ""
}
}
struct ShellImpl {
enum Error: Swift.Error {
case executableNotFound
}
private static var userShell: String {
return ProcessInfo.processInfo.environment["SHELL"]!
}
private static var userEnv: [String: String] {
let env = try! self.invoke(executable: self.userShell, args: ["-c", "env"])
let pairs = env.split(separator: "\n")
.map { $0.split(first: "=") }
.map { ($0[0], $0[1]) }
return pairs.reduce(into: [:], { $0[$1.0] = $1.1 })
}
private static var userPATH: String {
return self.userEnv["PATH"]!
}
private static var runtimeSearchPaths: [String] {
return self.userPATH.split(on: ":")
}
/// Just for 'foo' or 'bar'
private static func resolveExecutable(named name: String) -> String? {
for path in self.runtimeSearchPaths {
let fullPath = (path as NSString).appendingPathComponent(name)
if FileManager.default.fileExists(atPath: fullPath) {
return fullPath
}
}
return nil
}
/// Just for '../foo' or './subdir/bar' etc
private static func resolve(relativePath: String, in directory: String) -> String? {
return nil // TODO, but not really needed
}
/// Anything
private static func resolve(inputPath: String) -> String? {
if inputPath.hasPrefix("/") {
return inputPath
}
if inputPath.hasPrefix("./") || inputPath.hasPrefix("../") {
return self.resolve(relativePath: inputPath, in: FileManager.default.currentDirectoryPath)
}
return self.resolveExecutable(named: inputPath)
}
static func invoke(executable: String, args: [String]) throws -> String {
// Case: invoking a shell builtin such as cd
if let builtin = ShellBuiltins.lookup[executable] {
return try builtin(args)
}
guard let exePath = self.resolve(inputPath: executable) else {
throw Error.executableNotFound
}
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.arguments = args
task.launchPath = exePath
task.standardInput = nil
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
if let output = String(data: data, encoding: .utf8) {
return output
}
return ""
}
}
@dynamicMemberLookup
struct Shell {
subscript(dynamicMember executable: String) -> (_ args: String...) throws -> String {
return { (args: String...) in
return try ShellImpl.invoke(executable: executable, args: args)
}
}
var cwd: String {
return FileManager.default.currentDirectoryPath
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment