Created
June 25, 2022 23:31
-
-
Save NSExceptional/33837b97966ed95b5a84f4aa3a5027f6 to your computer and use it in GitHub Desktop.
Leverage @dynamicMemberLookup to invoke shell commands dynamically
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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