remove swift-argument-parser - it was adding well over 1Mb of bloat to release builds

This commit is contained in:
Nick Payne
2025-07-02 20:58:28 +01:00
parent d10f16a119
commit a0902f6eec
7 changed files with 347 additions and 81 deletions
-15
View File
@@ -1,15 +0,0 @@
{
"originHash" : "5f2b81278809343fed36ed8c17e7d6930bfd5b85261cdf5dadb17ab7ffdfc0e3",
"pins" : [
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "011f0c765fb46d9cac61bca19be0527e99c98c8b",
"version" : "1.5.1"
}
}
],
"version" : 3
}
+1 -11
View File
@@ -8,17 +8,7 @@ let package = Package(
platforms: [
.macOS("14.2")
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0")
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.executableTarget(
name: "audiotee",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser")
]
)
.executableTarget(name: "audiotee")
]
)
+234
View File
@@ -0,0 +1,234 @@
import Foundation
// MARK: - Error Types
enum ArgumentParserError: Error, CustomStringConvertible {
case unknownOption(String)
case missingValue(String)
case invalidValue(String, String)
case validationFailed(String)
case helpRequested
var description: String {
switch self {
case .unknownOption(let option):
return "Unknown option: \(option)"
case .missingValue(let option):
return "Missing value for option: \(option)"
case .invalidValue(let option, let value):
return "Invalid value '\(value)' for option: \(option)"
case .validationFailed(let message):
return message
case .helpRequested:
return "" // Help is handled separately
}
}
}
// MARK: - Argument Configuration
struct ArgumentConfig {
let name: String
let shortName: String?
let help: String
let isFlag: Bool
let isArray: Bool
let defaultValue: String?
init(
name: String, shortName: String? = nil, help: String, isFlag: Bool = false,
isArray: Bool = false, defaultValue: String? = nil
) {
self.name = name
self.shortName = shortName
self.help = help
self.isFlag = isFlag
self.isArray = isArray
self.defaultValue = defaultValue
}
}
// MARK: - Simple Argument Parser
class SimpleArgumentParser {
private let programName: String
private let abstract: String
private let discussion: String
private var configs: [ArgumentConfig] = []
private var parsedValues: [String: [String]] = [:]
init(programName: String, abstract: String, discussion: String = "") {
self.programName = programName
self.abstract = abstract
self.discussion = discussion
}
func addOption(name: String, shortName: String? = nil, help: String, defaultValue: String? = nil)
{
configs.append(
ArgumentConfig(name: name, shortName: shortName, help: help, defaultValue: defaultValue))
}
func addArrayOption(name: String, shortName: String? = nil, help: String) {
configs.append(ArgumentConfig(name: name, shortName: shortName, help: help, isArray: true))
}
func addFlag(name: String, shortName: String? = nil, help: String) {
configs.append(ArgumentConfig(name: name, shortName: shortName, help: help, isFlag: true))
}
func parse(_ arguments: [String] = Array(CommandLine.arguments.dropFirst())) throws {
var i = 0
while i < arguments.count {
let arg = arguments[i]
if arg == "--help" || arg == "-h" {
throw ArgumentParserError.helpRequested
}
guard arg.hasPrefix("-") else {
throw ArgumentParserError.unknownOption(arg)
}
let optionName = findOptionName(arg)
guard let config = findConfig(optionName) else {
throw ArgumentParserError.unknownOption(arg)
}
if config.isFlag {
parsedValues[config.name] = ["true"]
i += 1
} else {
// Need a value
i += 1
guard i < arguments.count else {
throw ArgumentParserError.missingValue(arg)
}
if config.isArray {
// Collect all values until next option or end
var values: [String] = []
while i < arguments.count && !arguments[i].hasPrefix("-") {
values.append(arguments[i])
i += 1
}
if values.isEmpty {
throw ArgumentParserError.missingValue(arg)
}
parsedValues[config.name] = values
} else {
let value = arguments[i]
parsedValues[config.name] = [value]
i += 1
}
}
}
// Set default values for missing options
for config in configs {
if parsedValues[config.name] == nil, let defaultValue = config.defaultValue {
parsedValues[config.name] = [defaultValue]
}
}
}
private func findOptionName(_ arg: String) -> String {
if arg.hasPrefix("--") {
return String(arg.dropFirst(2))
} else if arg.hasPrefix("-") {
return String(arg.dropFirst(1))
}
return arg
}
private func findConfig(_ optionName: String) -> ArgumentConfig? {
return configs.first { config in
config.name == optionName || config.shortName == optionName
}
}
func getValue<T>(_ name: String, as type: T.Type) throws -> T {
guard let values = parsedValues[name], let value = values.first else {
throw ArgumentParserError.missingValue(name)
}
return try convertValue(value, to: type, optionName: name)
}
func getOptionalValue<T>(_ name: String, as type: T.Type) throws -> T? {
guard let values = parsedValues[name], let value = values.first else {
return nil
}
return try convertValue(value, to: type, optionName: name)
}
func getArrayValue<T>(_ name: String, as type: T.Type) throws -> [T] {
guard let values = parsedValues[name] else {
return []
}
return try values.map { try convertValue($0, to: type, optionName: name) }
}
func getFlag(_ name: String) -> Bool {
return parsedValues[name]?.first == "true"
}
private func convertValue<T>(_ value: String, to type: T.Type, optionName: String) throws -> T {
if type == String.self {
return value as! T
} else if type == Int32.self {
guard let intValue = Int32(value) else {
throw ArgumentParserError.invalidValue(optionName, value)
}
return intValue as! T
} else if type == Double.self {
guard let doubleValue = Double(value) else {
throw ArgumentParserError.invalidValue(optionName, value)
}
return doubleValue as! T
} else if type == OutputFormat.self {
guard let format = OutputFormat(rawValue: value) else {
throw ArgumentParserError.invalidValue(optionName, value)
}
return format as! T
}
throw ArgumentParserError.invalidValue(optionName, value)
}
func printHelp() {
print(abstract)
if !discussion.isEmpty {
print("\n\(discussion)")
}
print("\nUSAGE:")
print(" \(programName) [OPTIONS]")
let optionConfigs = configs.filter { !$0.isFlag }
let flagConfigs = configs.filter { $0.isFlag }
if !optionConfigs.isEmpty {
print("\nOPTIONS:")
for config in optionConfigs {
let shortName = config.shortName.map { "-\($0), " } ?? ""
let defaultDesc = config.defaultValue.map { " (default: \($0))" } ?? ""
print(" \(shortName)--\(config.name) \(config.help)\(defaultDesc)")
}
}
if !flagConfigs.isEmpty {
print("\nFLAGS:")
for config in flagConfigs {
let shortName = config.shortName.map { "-\($0), " } ?? ""
print(" \(shortName)--\(config.name) \(config.help)")
}
}
print("\n -h, --help Show this help message")
}
}
+109 -48
View File
@@ -1,63 +1,100 @@
import ArgumentParser
import CoreAudio
import Foundation
struct AudioTee: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Capture system audio and stream to stdout",
discussion: """
AudioTee captures system audio using Core Audio taps and streams it as structured output.
Output formats:
• json: Base64-encoded audio in JSON messages (safe for terminals)
• binary: Raw binary audio with JSON metadata headers (efficient for pipes)
• auto: Automatically choose based on whether stdout is a terminal (default)
Process filtering:
• include-processes: Only tap specified process IDs (empty = all processes)
• exclude-processes: Tap all processes except specified ones
• mute: How to handle processes being tapped
Examples:
audiotee # Auto format, tap all processes
audiotee --format=json # Always use JSON format
audiotee --format=binary # Always use binary format
audiotee --sample-rate=16000 # Convert to 16kHz mono for ASR
audiotee --sample-rate=8000 # Convert to 8kHz for telephony
audiotee --include-processes 1234 # Only tap process 1234
audiotee --include-processes 1234 5678 9012 # Tap only these processes
audiotee --exclude-processes 1234 5678 # Tap everything except these
audiotee --mute # Mute processes being tapped
"""
)
@Option(name: .shortAndLong, help: "Output format")
struct AudioTee {
var format: OutputFormat = .auto
@Option(
name: .long, help: "Process IDs to include (space-separated, empty = all processes)")
var includeProcesses: [Int32] = []
@Option(
name: .long, help: "Process IDs to exclude (space-separated)")
var excludeProcesses: [Int32] = []
@Flag(name: .long, help: "Mute processes being tapped")
var mute: Bool = false
@Option(
name: .long,
help: "Target sample rate (8000, 16000, 22050, 24000, 32000, 44100, 48000)")
var sampleRate: Double?
@Option(
name: .long,
help: "Audio chunk duration in seconds (default: 0.2)")
var chunkDuration: Double = 0.2
init() {}
static func main() {
let parser = SimpleArgumentParser(
programName: "audiotee",
abstract: "Capture system audio and stream to stdout",
discussion: """
AudioTee captures system audio using Core Audio taps and streams it as structured output.
Output formats:
• json: Base64-encoded audio in JSON messages (safe for terminals)
• binary: Raw binary audio with JSON metadata headers (efficient for pipes)
• auto: Automatically choose based on whether stdout is a terminal (default)
Process filtering:
• include-processes: Only tap specified process IDs (empty = all processes)
• exclude-processes: Tap all processes except specified ones
• mute: How to handle processes being tapped
Examples:
audiotee # Auto format, tap all processes
audiotee --format=json # Always use JSON format
audiotee --format=binary # Always use binary format
audiotee --sample-rate=16000 # Convert to 16kHz mono for ASR
audiotee --sample-rate=8000 # Convert to 8kHz for telephony
audiotee --include-processes 1234 # Only tap process 1234
audiotee --include-processes 1234 5678 9012 # Tap only these processes
audiotee --exclude-processes 1234 5678 # Tap everything except these
audiotee --mute # Mute processes being tapped
"""
)
// Configure arguments
parser.addOption(name: "format", shortName: "f", help: "Output format", defaultValue: "auto")
parser.addArrayOption(
name: "include-processes",
help: "Process IDs to include (space-separated, empty = all processes)")
parser.addArrayOption(
name: "exclude-processes", help: "Process IDs to exclude (space-separated)")
parser.addFlag(name: "mute", help: "Mute processes being tapped")
parser.addOption(
name: "sample-rate",
help: "Target sample rate (8000, 16000, 22050, 24000, 32000, 44100, 48000)")
parser.addOption(
name: "chunk-duration", help: "Audio chunk duration in seconds", defaultValue: "0.2")
// Parse arguments
do {
try parser.parse()
var audioTee = AudioTee()
// Extract values
audioTee.format = try parser.getValue("format", as: OutputFormat.self)
audioTee.includeProcesses = try parser.getArrayValue("include-processes", as: Int32.self)
audioTee.excludeProcesses = try parser.getArrayValue("exclude-processes", as: Int32.self)
audioTee.mute = parser.getFlag("mute")
audioTee.sampleRate = try parser.getOptionalValue("sample-rate", as: Double.self)
audioTee.chunkDuration = try parser.getValue("chunk-duration", as: Double.self)
// Validate
try audioTee.validate()
// Run
try audioTee.run()
} catch ArgumentParserError.helpRequested {
parser.printHelp()
exit(0)
} catch ArgumentParserError.validationFailed(let message) {
print("Error: \(message)", to: &standardError)
exit(1)
} catch let error as ArgumentParserError {
print("Error: \(error.description)", to: &standardError)
parser.printHelp()
exit(1)
} catch {
print("Error: \(error)", to: &standardError)
exit(1)
}
}
func validate() throws {
if !includeProcesses.isEmpty && !excludeProcesses.isEmpty {
throw ValidationError("Cannot specify both --include-processes and --exclude-processes")
throw ArgumentParserError.validationFailed(
"Cannot specify both --include-processes and --exclude-processes")
}
}
@@ -159,3 +196,27 @@ struct AudioTee: ParsableCommand {
}
}
}
// Helper for stderr output
var standardError = FileHandle.standardError
extension FileHandle: @retroactive TextOutputStream {
public func write(_ string: String) {
let data = Data(string.utf8)
self.write(data)
}
}
// Exit code handling
enum ExitCode: Error {
case failure
}
extension ExitCode {
var code: Int32 {
switch self {
case .failure:
return 1
}
}
}
+1 -3
View File
@@ -1,6 +1,4 @@
import ArgumentParser
enum OutputFormat: String, CaseIterable, ExpressibleByArgument {
enum OutputFormat: String, CaseIterable {
case json = "json"
case binary = "binary"
case auto = "auto"
+1 -2
View File
@@ -1,7 +1,6 @@
import ArgumentParser
import CoreAudio
public enum TapMuteBehavior: String, CaseIterable, ExpressibleByArgument {
public enum TapMuteBehavior: String, CaseIterable {
case unmuted = "unmuted"
case muted = "muted"
+1 -2
View File
@@ -1,5 +1,4 @@
import ArgumentParser
import AudioToolbox
import Foundation
AudioTee.main()
AudioTee.main()