77import Foundation
88import Dispatch
99import Logging
10+ import TSCBasic
11+ import Algorithms
1012
1113// MARK: - API
1214
@@ -33,30 +35,31 @@ import Logging
3335 to command: SafeString ,
3436 arguments: [ Argument ] = [ ] ,
3537 at path: String = " . " ,
36- process: Process = . init( ) ,
3738 logger: Logger ? = nil ,
3839 outputHandle: FileHandle ? = nil ,
3940 errorHandle: FileHandle ? = nil ,
40- environment: [ String : String ] ? = nil ,
41- eofTimeout: DispatchTimeInterval = . milliseconds( 10 )
42- ) throws -> ( stdout: String , stderr: String ) {
43- let command = " cd \( path. escapingSpaces) && \( command) \( arguments. map ( \. string) . joined ( separator: " " ) ) "
41+ environment: [ String : String ] ? = nil
42+ ) async throws -> ( stdout: String , stderr: String ) {
43+ let command = " \( command) \( arguments. map ( \. string) . joined ( separator: " " ) ) "
4444
45- return try process . launchBash (
45+ return try await TSCBasic . Process . launchBash (
4646 with: command,
4747 logger: logger,
4848 outputHandle: outputHandle,
4949 errorHandle: errorHandle,
5050 environment: environment,
51- eofTimeout: eofTimeout
51+ at: path == " . " ? nil :
52+ ( path == " ~ " ? TSCBasic . localFileSystem. homeDirectory. pathString :
53+ ( path. starts ( with: " ~/ " ) ? " \( TSCBasic . localFileSystem. homeDirectory. pathString) / \( path. dropFirst ( 2 ) ) " :
54+ path) )
5255 )
5356}
5457
5558@discardableResult public func shellOutOldVersion(
5659 to command: SafeString ,
5760 arguments: [ Argument ] = [ ] ,
5861 at path: String = " . " ,
59- process: Process = . init( ) ,
62+ process: Foundation . Process = . init( ) ,
6063 outputHandle: FileHandle ? = nil ,
6164 errorHandle: FileHandle ? = nil ,
6265 environment: [ String : String ] ? = nil
@@ -92,30 +95,26 @@ import Logging
9295@discardableResult public func shellOut(
9396 to command: ShellOutCommand ,
9497 at path: String = " . " ,
95- process: Process = . init( ) ,
9698 logger: Logger ? = nil ,
9799 outputHandle: FileHandle ? = nil ,
98100 errorHandle: FileHandle ? = nil ,
99- environment: [ String : String ] ? = nil ,
100- eofTimeout: DispatchTimeInterval = . milliseconds( 10 )
101- ) throws -> ( stdout: String , stderr: String ) {
102- try shellOut (
101+ environment: [ String : String ] ? = nil
102+ ) async throws -> ( stdout: String , stderr: String ) {
103+ try await shellOut (
103104 to: command. command,
104105 arguments: command. arguments,
105106 at: path,
106- process: process,
107107 logger: logger,
108108 outputHandle: outputHandle,
109109 errorHandle: errorHandle,
110- environment: environment,
111- eofTimeout: eofTimeout
110+ environment: environment
112111 )
113112}
114113
115114@discardableResult public func shellOutOldVersion(
116115 to command: ShellOutCommand ,
117116 at path: String = " . " ,
118- process: Process = . init( ) ,
117+ process: Foundation . Process = . init( ) ,
119118 outputHandle: FileHandle ? = nil ,
120119 errorHandle: FileHandle ? = nil ,
121120 environment: [ String : String ] ? = nil
@@ -437,91 +436,53 @@ extension ShellOutCommand {
437436
438437// MARK: - Private
439438
440- private extension Process {
441- @discardableResult func launchBash(
439+ private extension TSCBasic . Process {
440+ @discardableResult static func launchBash(
442441 with command: String ,
443442 logger: Logger ? = nil ,
444443 outputHandle: FileHandle ? = nil ,
445444 errorHandle: FileHandle ? = nil ,
446445 environment: [ String : String ] ? = nil ,
447- eofTimeout: DispatchTimeInterval = . milliseconds( 10 )
448- ) throws -> ( stdout: String , stderr: String ) {
449- self . executableURL = URL ( fileURLWithPath: " /bin/bash " )
450- self . arguments = [ " -c " , command]
451-
452- if let environment {
453- self . environment = environment
454- }
455-
456- let outputPipe = Pipe ( ) , errorPipe = Pipe ( )
457- self . standardOutput = outputPipe
458- self . standardError = errorPipe
459-
460- // Because FileHandle's readabilityHandler might be called from a
461- // different queue from the calling queue, avoid data races by
462- // protecting reads and writes to outputData and errorData on
463- // a single dispatch queue.
464- let outputQueue = DispatchQueue ( label: " bash-output-queue " )
465- let outputGroup = DispatchGroup ( )
466- var outputData = Data ( ) , errorData = Data ( )
467-
468- outputGroup. enter ( )
469- outputPipe. fileHandleForReading. readabilityHandler = { handler in
470- let data = handler. availableData
471-
472- if data. isEmpty { // EOF
473- handler. readabilityHandler = nil
474- outputGroup. leave ( )
475- } else {
476- outputQueue. async {
477- outputData. append ( data)
478- outputHandle? . write ( data)
479- }
446+ at: String ? = nil
447+ ) async throws -> ( stdout: String , stderr: String ) {
448+ let process = try Self . init (
449+ arguments: [ " /bin/bash " , " -c " , command] ,
450+ environment: environment ?? ProcessEnv . vars,
451+ workingDirectory: at. map { try . init( validating: $0) } ?? TSCBasic . localFileSystem. currentWorkingDirectory ?? . root,
452+ outputRedirection: . collect( redirectStderr: false ) ,
453+ startNewProcessGroup: false ,
454+ loggingHandler: nil
455+ )
456+
457+ try process. launch ( )
458+
459+ let result = try await process. waitUntilExit ( )
460+
461+ try outputHandle? . write ( contentsOf: ( try ? result. output. get ( ) ) ?? [ ] )
462+ try outputHandle? . close ( )
463+ try errorHandle? . write ( contentsOf: ( try ? result. stderrOutput. get ( ) ) ?? [ ] )
464+ try errorHandle? . close ( )
465+
466+ guard case . terminated( code: let code) = result. exitStatus, code == 0 else {
467+ let code : Int32
468+ switch result. exitStatus {
469+ case . terminated( code: let termCode) : code = termCode
470+ case . signalled( signal: let sigNo) : code = - sigNo
480471 }
472+ throw ShellOutError (
473+ terminationStatus: code,
474+ errorData: Data ( ( try ? result. stderrOutput. get ( ) ) ?? [ ] ) ,
475+ outputData: Data ( ( try ? result. output. get ( ) ) ?? [ ] )
476+ )
481477 }
482-
483- outputGroup. enter ( )
484- errorPipe. fileHandleForReading. readabilityHandler = { handler in
485- let data = handler. availableData
486-
487- if data. isEmpty { // EOF
488- handler. readabilityHandler = nil
489- outputGroup. leave ( )
490- } else {
491- outputQueue. async {
492- errorData. append ( data)
493- errorHandle? . write ( data)
494- }
495- }
496- }
497-
498- try self . run ( )
499- self . waitUntilExit ( )
500-
501- if outputGroup. wait ( timeout: . now( ) + eofTimeout) == . timedOut {
502- logger? . debug ( " ShellOut.launchBash: Timed out waiting for EOF! (command: \( command) ) " )
503- }
504-
505- // We know as of this point that either all blocks have been submitted to the
506- // queue already, or we've reached our wait timeout.
507- return try outputQueue. sync {
508- // Do not try to readToEnd() here; if we already got an EOF, there's definitely
509- // nothing to read, and if we timed out, trying to read here will just block
510- // even longer.
511- try outputHandle? . close ( )
512- try errorHandle? . close ( )
513-
514- guard self . terminationStatus == 0 , self . terminationReason == . exit else {
515- throw ShellOutError (
516- terminationStatus: terminationStatus,
517- errorData: errorData,
518- outputData: outputData
519- )
520- }
521- return ( stdout: outputData. shellOutput ( ) , stderr: errorData. shellOutput ( ) )
522- }
478+ return try (
479+ stdout: String ( result. utf8Output ( ) . trimmingSuffix ( while: \. isNewline) ) ,
480+ stderr: String ( result. utf8stderrOutput ( ) . trimmingSuffix ( while: \. isNewline) )
481+ )
523482 }
483+ }
524484
485+ extension Foundation . Process {
525486 @discardableResult func launchBashOldVersion( with command: String , outputHandle: FileHandle ? = nil , errorHandle: FileHandle ? = nil , environment: [ String : String ] ? = nil ) throws -> ( stdout: String , stderr: String ) {
526487#if os(Linux)
527488 executableURL = URL ( fileURLWithPath: " /bin/bash " )
0 commit comments