├── .github └── workflows │ ├── jazzy.yml │ └── swift.yml ├── .gitignore ├── Commands.podspec ├── Examples └── main.swift ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Package.swift ├── README.md ├── README_cn.md ├── Scripts └── deploy.sh ├── Sources └── Commands │ ├── Commands.swift │ ├── CommandsAlias.swift │ ├── CommandsArguments.swift │ ├── CommandsDashcTransformer.swift │ ├── CommandsENV.swift │ ├── CommandsRequest.swift │ ├── CommandsResponse.swift │ ├── CommandsResult.swift │ └── CommandsTask.swift └── Tests ├── CommandsTests ├── RequestParseTests.swift ├── Resources │ └── python │ │ └── main.py ├── XCTestManifests.swift └── swift_commandsTests.swift └── LinuxMain.swift /.github/workflows/jazzy.yml: -------------------------------------------------------------------------------- 1 | name: Jazzy 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | Jazzy: 11 | runs-on: macos-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: ENV 15 | run: | 16 | uname -a 17 | swift --version 18 | - uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: 3.3 21 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 22 | - name: Build 23 | run: | 24 | swift build -v 25 | - name: Generate documentation json 26 | run: | 27 | brew install sourcekitten 28 | sourcekitten doc --spm --module-name Commands > commands.json 29 | - name: Run jazzy 30 | run: | 31 | bundle exec jazzy --clean --sourcekitten-sourcefile commands.json 32 | - uses: actions/upload-artifact@v4 33 | with: 34 | name: API Docs 35 | path: docs 36 | - name: Push to gh-pages 37 | if: github.event_name == 'push' 38 | run: | 39 | git config --global user.email "${GITHUB_ACTOR}" 40 | git config --global user.name "${GITHUB_ACTOR}@users.noreply.github.com" 41 | git clone "https://x-access-token:${PRIVATE_GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" out 42 | cd out 43 | git checkout -B gh-pages 44 | git rm -rf . 45 | cd .. 46 | cp -a docs/. out/. 47 | cd out 48 | git add -A 49 | git commit -m "Automated deployment to GitHub Pages: ${GITHUB_SHA}" --allow-empty 50 | git remote -v 51 | git config --global user.name 52 | git push origin gh-pages -f 53 | env: 54 | PRIVATE_GITHUB_TOKEN: ${{ secrets.PRIVATE_GITHUB_TOKEN }} 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: [ main, dev ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: ENV 17 | run: | 18 | uname -a 19 | xcodebuild -version 20 | swift --version 21 | - name: Build 22 | run: swift build -v 23 | - name: Run tests 24 | run: swift test -v 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm 8 | .vscode -------------------------------------------------------------------------------- /Commands.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "Commands" 3 | s.version = "0.7.0" 4 | s.summary = "Swift utilities for running commands." 5 | s.homepage = "https://github.com/qiuzhifei/swift-commands" 6 | s.license = { :type => "MIT" } 7 | s.authors = { "lingoer" => "qiuzhifei521@gmail.com", "tangplin" => "qiuzhifei521@gmail.com" } 8 | 9 | s.requires_arc = true 10 | s.swift_versions = ['5.1', '5.2', '5.3'] 11 | s.osx.deployment_target = "11.0" 12 | s.source = { :git => "https://github.com/qiuzhifei/swift-commands.git", :tag => s.version } 13 | s.source_files = "Sources/*/*.swift" 14 | end 15 | -------------------------------------------------------------------------------- /Examples/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // 4 | // 5 | // Created by zhifei qiu on 2021/8/4. 6 | // 7 | 8 | import Commands 9 | 10 | fileprivate extension Commands.Result { 11 | func log() { 12 | print("---------------") 13 | print(">>> \(request.absoluteCommand)") 14 | print("\(self.response.output)") 15 | print("---------------") 16 | } 17 | } 18 | 19 | // Bash 20 | Commands.Bash.run("ls /bin/ls").log() 21 | 22 | // Ruby 23 | Commands.Ruby.run("require 'base64'; puts Base64.encode64('qiuzhifei')").log() 24 | 25 | // Python 26 | Commands.Python.run("import base64; print(base64.b64encode(b'qiuzhifei').decode('ascii'))").log() 27 | 28 | // Custom 29 | let node = Commands.Alias("node", dashc: "-e") 30 | node.run("console.log('qiuzhifei')").log() 31 | 32 | // Task 33 | Commands.Task.run("bash -c pwd").log() 34 | Commands.Task.run("python main.py").log() 35 | Commands.Task.run("ruby -v").log() 36 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem "jazzy" -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.7) 5 | base64 6 | nkf 7 | rexml 8 | activesupport (7.1.5) 9 | base64 10 | benchmark (>= 0.3) 11 | bigdecimal 12 | concurrent-ruby (~> 1.0, >= 1.0.2) 13 | connection_pool (>= 2.2.5) 14 | drb 15 | i18n (>= 1.6, < 2) 16 | logger (>= 1.4.2) 17 | minitest (>= 5.1) 18 | mutex_m 19 | securerandom (>= 0.3) 20 | tzinfo (~> 2.0) 21 | addressable (2.8.7) 22 | public_suffix (>= 2.0.2, < 7.0) 23 | algoliasearch (1.27.5) 24 | httpclient (~> 2.8, >= 2.8.3) 25 | json (>= 1.5.1) 26 | atomos (0.1.3) 27 | base64 (0.2.0) 28 | benchmark (0.4.0) 29 | bigdecimal (3.1.8) 30 | claide (1.1.0) 31 | cocoapods (1.16.2) 32 | addressable (~> 2.8) 33 | claide (>= 1.0.2, < 2.0) 34 | cocoapods-core (= 1.16.2) 35 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 36 | cocoapods-downloader (>= 2.1, < 3.0) 37 | cocoapods-plugins (>= 1.0.0, < 2.0) 38 | cocoapods-search (>= 1.0.0, < 2.0) 39 | cocoapods-trunk (>= 1.6.0, < 2.0) 40 | cocoapods-try (>= 1.1.0, < 2.0) 41 | colored2 (~> 3.1) 42 | escape (~> 0.0.4) 43 | fourflusher (>= 2.3.0, < 3.0) 44 | gh_inspector (~> 1.0) 45 | molinillo (~> 0.8.0) 46 | nap (~> 1.0) 47 | ruby-macho (>= 2.3.0, < 3.0) 48 | xcodeproj (>= 1.27.0, < 2.0) 49 | cocoapods-core (1.16.2) 50 | activesupport (>= 5.0, < 8) 51 | addressable (~> 2.8) 52 | algoliasearch (~> 1.0) 53 | concurrent-ruby (~> 1.1) 54 | fuzzy_match (~> 2.0.4) 55 | nap (~> 1.0) 56 | netrc (~> 0.11) 57 | public_suffix (~> 4.0) 58 | typhoeus (~> 1.0) 59 | cocoapods-deintegrate (1.0.5) 60 | cocoapods-downloader (2.1) 61 | cocoapods-plugins (1.0.0) 62 | nap 63 | cocoapods-search (1.0.1) 64 | cocoapods-trunk (1.6.0) 65 | nap (>= 0.8, < 2.0) 66 | netrc (~> 0.11) 67 | cocoapods-try (1.2.0) 68 | colored2 (3.1.2) 69 | concurrent-ruby (1.3.4) 70 | connection_pool (2.4.1) 71 | drb (2.2.1) 72 | escape (0.0.4) 73 | ethon (0.16.0) 74 | ffi (>= 1.15.0) 75 | ffi (1.17.0-arm64-darwin) 76 | ffi (1.17.0-x86_64-darwin) 77 | fourflusher (2.3.1) 78 | fuzzy_match (2.0.4) 79 | gh_inspector (1.1.3) 80 | httpclient (2.8.3) 81 | i18n (1.14.6) 82 | concurrent-ruby (~> 1.0) 83 | jazzy (0.15.3) 84 | cocoapods (~> 1.5) 85 | mustache (~> 1.1) 86 | open4 (~> 1.3) 87 | redcarpet (~> 3.4) 88 | rexml (>= 3.2.7, < 4.0) 89 | rouge (>= 2.0.6, < 5.0) 90 | sassc (~> 2.1) 91 | sqlite3 (~> 1.3) 92 | xcinvoke (~> 0.3.0) 93 | json (2.9.0) 94 | liferaft (0.0.6) 95 | logger (1.6.2) 96 | minitest (5.25.4) 97 | molinillo (0.8.0) 98 | mustache (1.1.1) 99 | mutex_m (0.3.0) 100 | nanaimo (0.4.0) 101 | nap (1.1.0) 102 | netrc (0.11.0) 103 | nkf (0.2.0) 104 | open4 (1.3.4) 105 | public_suffix (4.0.7) 106 | redcarpet (3.6.0) 107 | rexml (3.3.9) 108 | rouge (4.5.1) 109 | ruby-macho (2.5.1) 110 | sassc (2.4.0) 111 | ffi (~> 1.9) 112 | securerandom (0.3.2) 113 | sqlite3 (1.7.3-arm64-darwin) 114 | sqlite3 (1.7.3-x86_64-darwin) 115 | typhoeus (1.4.1) 116 | ethon (>= 0.9.0) 117 | tzinfo (2.0.6) 118 | concurrent-ruby (~> 1.0) 119 | xcinvoke (0.3.0) 120 | liferaft (~> 0.0.6) 121 | xcodeproj (1.27.0) 122 | CFPropertyList (>= 2.3.3, < 4.0) 123 | atomos (~> 0.1.3) 124 | claide (>= 1.0.2, < 2.0) 125 | colored2 (~> 3.1) 126 | nanaimo (~> 0.4.0) 127 | rexml (>= 3.3.6, < 4.0) 128 | 129 | PLATFORMS 130 | arm64-darwin 131 | x86_64-darwin 132 | 133 | DEPENDENCIES 134 | jazzy 135 | 136 | BUNDLED WITH 137 | 2.5.23 138 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 qiuzhifei 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "swift-commands", 8 | platforms: [ 9 | .macOS(.v11) 10 | ], 11 | products: [ 12 | .library(name: "Commands", 13 | targets: ["Commands"]), 14 | ], 15 | dependencies: [], 16 | targets: [ 17 | .target(name: "Commands", 18 | dependencies: []), 19 | .target(name: "Examples", 20 | dependencies: ["Commands"], 21 | path: "Examples/"), 22 | .testTarget(name: "CommandsTests", 23 | dependencies: ["Commands"], 24 | resources: [.process("Resources")]) 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Commands 2 | ![SPM](https://img.shields.io/badge/SPM-compatible-brightgreen.svg) 3 | ![CocoaPods](https://img.shields.io/cocoapods/v/Commands?color=green) 4 | [![CI Status](https://img.shields.io/github/actions/workflow/status/qiuzhifei/swift-commands/swift.yml)](https://github.com/qiuzhifei/swift-commands/actions) 5 | [![License](https://img.shields.io/github/license/qiuzhifei/swift-commands)](https://github.com/qiuzhifei/swift-commands/blob/main/LICENSE) 6 | ![Platform](https://img.shields.io/badge/platforms-macOS%2011.0-orange) 7 | 8 | Swift utilities for running commands. 9 | 10 | The `Commands` module allows you to take a system command as a string and return the standard output. 11 | 12 | [API documentation can be found here](https://qiuzhifei.github.io/swift-commands/). 13 | 14 | ## Usage 15 | ``` 16 | import Commands 17 | ``` 18 | 19 | ### Bash 20 | Execute shell commands. 21 | ```swift 22 | let result = Commands.Task.run("bash -c ls") 23 | ``` 24 | Or 25 | ```swift 26 | let result = Commands.Task.run(["bash", "-c", "ls"]) 27 | ``` 28 | Or 29 | ```swift 30 | let result = Commands.Bash.run("ls") 31 | ``` 32 | 33 | ### Python 34 | Execute python scripts. 35 | ```swift 36 | let result = Commands.Task.run("python main.py") 37 | ``` 38 | Execute python commands. 39 | ```swift 40 | let result = Commands.Task.run("python -c import base64; print(base64.b64encode(b'qiuzhifei').decode('ascii'))") 41 | ``` 42 | Or 43 | ```swift 44 | let result = Commands.Python.run("import base64; print(base64.b64encode(b'qiuzhifei').decode('ascii'))") 45 | ``` 46 | 47 | ### Ruby 48 | Execute ruby scripts. 49 | ```swift 50 | let result = Commands.Task.run("ruby main.rb") 51 | ``` 52 | Execute ruby commands. 53 | ```swift 54 | let result = Commands.Task.run("ruby -e require 'base64'; puts Base64.encode64('qiuzhifei')") 55 | ``` 56 | Or 57 | ```swift 58 | let result = Commands.Ruby.run("require 'base64'; puts Base64.encode64('qiuzhifei')") 59 | ``` 60 | 61 | ### Alias 62 | Create a shortcut name for a command. 63 | ```swift 64 | let node = Commands.Alias("/usr/local/bin/node", dashc: "-e") 65 | let result = node.run("console.log('qiuzhifei')") 66 | ``` 67 | 68 | ### Setting global environment variables 69 | ```swift 70 | Commands.ENV.global["http_proxy"] = "http://127.0.0.1:7890" 71 | ``` 72 | ```swift 73 | Commands.ENV.global.add(PATH: "/Users/zhifeiqiu/.rvm/bin") 74 | ``` 75 | 76 | ### Making Commands 77 | ```swift 78 | let request: Commands.Request = "ruby -v" 79 | ``` 80 | Or 81 | ```swift 82 | let request: Commands.Request = ["ruby", "-v"] 83 | ``` 84 | Or 85 | ```swift 86 | let request = Commands.Request(executableURL: "ruby", arguments: "-v") 87 | ``` 88 | Change environment variables 89 | ```swift 90 | var request: Commands.Request = "ruby -v" 91 | request.environment?.add(PATH: "/usr/local/bin") 92 | request.environment?["http_proxy"] = "http://127.0.0.1:7890" 93 | request.environment?["https_proxy"] = "http://127.0.0.1:7890" 94 | request.environment?["all_proxy"] = "socks5://127.0.0.1:7890" 95 | 96 | let result = Commands.Task.run(request) 97 | ``` 98 | 99 | ### Result Handler 100 | Returns the `Commands.Result` of running cmd in a subprocess. 101 | ```swift 102 | let result = Commands.Task.run("ruby -v") 103 | switch result { 104 | case .Success(let request, let response): 105 | debugPrint("command: \(request.absoluteCommand), success output: \(response.output)") 106 | case .Failure(let request, let response): 107 | debugPrint("command: \(request.absoluteCommand), failure output: \(response.errorOutput)") 108 | } 109 | ``` 110 | 111 | ## Adding Commands as a Dependency 112 | To use the `Commands` library in a SwiftPM project, 113 | add the following line to the dependencies in your `Package.swift` file: 114 | 115 | ```swift 116 | let package = Package( 117 | // name, platforms, products, etc. 118 | dependencies: [ 119 | .package(url: "https://github.com/qiuzhifei/swift-commands", from: "0.6.0"), 120 | // other dependencies 121 | ], 122 | targets: [ 123 | .target(name: "", dependencies: [ 124 | .product(name: "Commands", package: "swift-commands"), 125 | ]), 126 | // other targets 127 | ] 128 | ) 129 | ``` 130 | 131 | ## CocoaPods (OS X 11.0+) 132 | You can use [CocoaPods](http://cocoapods.org/) to install `Commands` by adding it to your `Podfile`: 133 | ```ruby 134 | pod 'Commands', '~> 0.7.0' 135 | ``` 136 | 137 | ## QuickStart 138 | ```shell 139 | git clone https://github.com/QiuZhiFei/swift-commands 140 | cd swift-commands && open Package.swift 141 | ``` 142 | 143 | ## References 144 | - [https://github.com/apple/swift-argument-parser](https://github.com/apple/swift-argument-parser) 145 | -------------------------------------------------------------------------------- /README_cn.md: -------------------------------------------------------------------------------- 1 | # Swift Commands 2 | ![SPM](https://img.shields.io/badge/SPM-compatible-brightgreen.svg) 3 | ![CocoaPods](https://img.shields.io/cocoapods/v/Commands.svg) 4 | [![CI Status](https://img.shields.io/github/workflow/status/qiuzhifei/swift-commands/Swift)](https://github.com/qiuzhifei/swift-commands/actions) 5 | [![License](https://img.shields.io/github/license/qiuzhifei/swift-commands)](https://github.com/qiuzhifei/swift-commands/blob/main/LICENSE) 6 | ![Platform](https://img.shields.io/badge/platforms-macOS%2010.9-orange) 7 | 8 | Swift调用系统命令。 9 | 10 | `Commands`模块,使用系统命令字符串,并返回标准输出。 11 | 12 | [API documentation can be found here](https://qiuzhifei.github.io/swift-commands/). 13 | 14 | ## Usage 15 | ``` 16 | import Commands 17 | ``` 18 | 19 | ### Bash 20 | Execute shell commands. 21 | ```swift 22 | let result = Commands.Task.run("bash -c ls") 23 | ``` 24 | Or 25 | ```swift 26 | let result = Commands.Task.run(["bash", "-c", "ls"]) 27 | ``` 28 | Or 29 | ```swift 30 | let result = Commands.Bash.run("ls") 31 | ``` 32 | 33 | ### Python 34 | Execute python scripts. 35 | ```swift 36 | let result = Commands.Task.run("python main.py") 37 | ``` 38 | Execute python commands. 39 | ```swift 40 | let result = Commands.Task.run("python -c import base64; print(base64.b64encode(b'qiuzhifei').decode('ascii'))") 41 | ``` 42 | Or 43 | ```swift 44 | let result = Commands.Python.run("import base64; print(base64.b64encode(b'qiuzhifei').decode('ascii'))") 45 | ``` 46 | 47 | ### Ruby 48 | Execute ruby scripts. 49 | ```swift 50 | let result = Commands.Task.run("ruby main.rb") 51 | ``` 52 | Execute ruby commands. 53 | ```swift 54 | let result = Commands.Task.run("ruby -e require 'base64'; puts Base64.encode64('qiuzhifei')") 55 | ``` 56 | Or 57 | ```swift 58 | let result = Commands.Ruby.run("require 'base64'; puts Base64.encode64('qiuzhifei')") 59 | ``` 60 | 61 | ### Alias 62 | Create a shortcut name for a command. 63 | ```swift 64 | let node = Commands.Alias("/usr/local/bin/node", dashc: "-e") 65 | let result = node.run("console.log('qiuzhifei')") 66 | ``` 67 | 68 | ### Setting global environment variables 69 | ```swift 70 | Commands.ENV.global["http_proxy"] = "http://127.0.0.1:7890" 71 | ``` 72 | ```swift 73 | Commands.ENV.global.add(PATH: "/Users/zhifeiqiu/.rvm/bin") 74 | ``` 75 | 76 | ### Making Commands 77 | ```swift 78 | let request: Commands.Request = "ruby -v" 79 | ``` 80 | Or 81 | ```swift 82 | let request: Commands.Request = ["ruby", "-v"] 83 | ``` 84 | Or 85 | ```swift 86 | let request = Commands.Request(executableURL: "ruby", arguments: "-v") 87 | ``` 88 | Change environment variables 89 | ```swift 90 | var request: Commands.Request = "ruby -v" 91 | request.environment?.add(PATH: "/usr/local/bin") 92 | request.environment?["http_proxy"] = "http://127.0.0.1:7890" 93 | request.environment?["https_proxy"] = "http://127.0.0.1:7890" 94 | request.environment?["all_proxy"] = "socks5://127.0.0.1:7890" 95 | 96 | let result = Commands.Task.run(request) 97 | ``` 98 | 99 | ### Result Handler 100 | Returns the `Commands.Result` of running cmd in a subprocess. 101 | ```swift 102 | let result = Commands.Task.run("ruby -v") 103 | switch result { 104 | case .Success(let request, let response): 105 | debugPrint("command: \(request.absoluteCommand), success output: \(response.output)") 106 | case .Failure(let request, let response): 107 | debugPrint("command: \(request.absoluteCommand), failure output: \(response.errorOutput)") 108 | } 109 | ``` 110 | 111 | ## Adding Commands as a Dependency 112 | To use the `Commands` library in a SwiftPM project, 113 | add the following line to the dependencies in your `Package.swift` file: 114 | 115 | ```swift 116 | let package = Package( 117 | // name, platforms, products, etc. 118 | dependencies: [ 119 | .package(url: "https://github.com/qiuzhifei/swift-commands", from: "0.6.0"), 120 | // other dependencies 121 | ], 122 | targets: [ 123 | .target(name: "", dependencies: [ 124 | .product(name: "Commands", package: "swift-commands"), 125 | ]), 126 | // other targets 127 | ] 128 | ) 129 | ``` 130 | 131 | ## CocoaPods (OS X 11.0+) 132 | You can use [CocoaPods](http://cocoapods.org/) to install `Commands` by adding it to your `Podfile`: 133 | ```ruby 134 | pod 'Commands', '~> 0.6.0' 135 | ``` 136 | 137 | ## QuickStart 138 | ```shell 139 | git clone https://github.com/QiuZhiFei/swift-commands 140 | cd swift-commands && open Package.swift 141 | ``` 142 | 143 | ## References 144 | - [https://github.com/apple/swift-argument-parser](https://github.com/apple/swift-argument-parser) 145 | -------------------------------------------------------------------------------- /Scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | # test 6 | ## 1. Build and run tests, Run `swift test -v` 7 | ## 2. Validates a Pod, Run `pod lib lint --verbose` 8 | swift test -v 9 | pod lib lint --verbose 10 | 11 | # deploy 12 | ## 1. Bump the version in https://github.com/QiuZhiFei/swift-commands/blob/dev/Commands.podspec#L3, and https://github.com/QiuZhiFei/swift-commands/blob/main/README.md. 13 | ## 2. Bump the git tag and push tag to github, create a release in https://github.com/QiuZhiFei/swift-commands/tags. 14 | ## 3. Publish a cocoapods podspec, Run `pod trunk push Commands.podspec --allow-warnings --verbose`. 15 | -------------------------------------------------------------------------------- /Sources/Commands/Commands.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Commands.swift 3 | // 4 | // 5 | // Created by zhifei qiu on 2021/8/2. 6 | // 7 | 8 | public enum Commands { } 9 | -------------------------------------------------------------------------------- /Sources/Commands/CommandsAlias.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandsAlias.swift 3 | // 4 | // 5 | // Created by zhifei qiu on 2021/8/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Commands { 11 | public struct Alias { 12 | public let executableURL: String 13 | public let dashc: Commands.Arguments? 14 | 15 | public init(_ executableURL: String, dashc: Commands.Arguments? = nil) { 16 | self.executableURL = executableURL 17 | self.dashc = dashc 18 | } 19 | } 20 | } 21 | 22 | public extension Commands.Alias { 23 | @discardableResult 24 | func run(_ arguments: Commands.Arguments? = nil, 25 | environment: Commands.ENV? = Commands.ENV.global) -> Commands.Result { 26 | let request = prepare(arguments, environment: environment) 27 | return Commands.Task.run(request) 28 | } 29 | 30 | func system(_ arguments: Commands.Arguments? = nil, 31 | environment: Commands.ENV? = Commands.ENV.global) { 32 | let request = prepare(arguments, environment: environment) 33 | Commands.Task.system(request) 34 | } 35 | 36 | func system(_ arguments: Commands.Arguments? = nil, 37 | environment: Commands.ENV? = Commands.ENV.global, 38 | output: ((String) -> Void)?, 39 | errorOutput: ((String) -> Void)?) { 40 | let request = prepare(arguments, environment: environment) 41 | Commands.Task.system(request, output: output, errorOutput: errorOutput) 42 | } 43 | } 44 | 45 | private extension Commands.Alias { 46 | func prepare(_ arguments: Commands.Arguments?, 47 | environment: Commands.ENV?) -> Commands.Request { 48 | return Commands.Request(environment, 49 | executableURL: executableURL, 50 | dashc: dashc, 51 | arguments: arguments) 52 | } 53 | } 54 | 55 | public extension Commands { 56 | static let Bash = Commands.Alias("bash", dashc: "-c") 57 | static let Ruby = Commands.Alias("ruby", dashc: "-e") 58 | static let Python = Commands.Alias("python", dashc: "-c") 59 | static let Node = Commands.Alias("node", dashc: "-e") 60 | } 61 | -------------------------------------------------------------------------------- /Sources/Commands/CommandsArguments.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandsArguments.swift 3 | // 4 | // 5 | // Created by zhifei qiu on 2021/8/6. 6 | // 7 | 8 | public extension Commands { 9 | struct Arguments { 10 | public let raw: [String] 11 | 12 | public init(_ value: String) { 13 | self.init([value]) 14 | } 15 | 16 | public init(_ elements: [String]) { 17 | raw = elements 18 | } 19 | } 20 | } 21 | 22 | extension Commands.Arguments: Swift.ExpressibleByStringLiteral { 23 | public init(stringLiteral value: StringLiteralType) { 24 | self.init(value) 25 | } 26 | 27 | public init(extendedGraphemeClusterLiteral value: String) { 28 | self.init(value) 29 | } 30 | 31 | public init(unicodeScalarLiteral value: String) { 32 | self.init(value) 33 | } 34 | } 35 | 36 | extension Commands.Arguments: Swift.ExpressibleByStringInterpolation { 37 | public init(stringInterpolation: DefaultStringInterpolation) { 38 | self.init(stringInterpolation.description) 39 | } 40 | } 41 | 42 | extension Commands.Arguments: Swift.ExpressibleByArrayLiteral { 43 | public init(arrayLiteral elements: String...) { 44 | self.init(elements) 45 | } 46 | } 47 | 48 | extension Commands.Arguments: Equatable { 49 | public static func == (lhs: Self, rhs: Self) -> Bool { 50 | return lhs.raw == rhs.raw 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Commands/CommandsDashcTransformer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandsDashcTransformer.swift 3 | // 4 | // 5 | // Created by zhifei qiu on 2021/9/11. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol CommandsDashcTransformerHandler { 11 | static func canHandle(executableURL: String, dashc: Commands.Arguments?) -> Bool 12 | static func handle(executableURL: String, dashc: Commands.Arguments?) -> Commands.Arguments? 13 | } 14 | 15 | extension Commands { 16 | public enum DashcTransformer { } 17 | } 18 | 19 | extension Commands.DashcTransformer { 20 | private static var handlers: [CommandsDashcTransformerHandler.Type] = [ 21 | Bash.self, 22 | Ruby.self, 23 | Python.self, 24 | Node.self, 25 | ] 26 | } 27 | 28 | extension Commands.DashcTransformer { 29 | public static func register(_ handler: CommandsDashcTransformerHandler.Type) { 30 | if handlers.contains(where: { it in return type(of: it) == type(of: handler) }) { 31 | return 32 | } 33 | handlers.append(handler) 34 | } 35 | 36 | public static func unregister(_ handler: CommandsDashcTransformerHandler.Type) { 37 | while let index = handlers.firstIndex(where: { it in return type(of: it) == type(of: handler) }) { 38 | handlers.remove(at: index) 39 | } 40 | } 41 | } 42 | 43 | extension Commands.DashcTransformer { 44 | static func transformer(executableURL: String, dashc: Commands.Arguments?) -> Commands.Arguments? { 45 | for handler in handlers { 46 | if handler.canHandle(executableURL: executableURL, dashc: dashc) { 47 | return handler.handle(executableURL: executableURL, dashc: dashc) 48 | } 49 | } 50 | return nil 51 | } 52 | } 53 | 54 | extension Commands.DashcTransformer { 55 | public struct Bash: CommandsDashcTransformerHandler { 56 | public static func canHandle(executableURL: String, 57 | dashc: Commands.Arguments?) -> Bool { 58 | if executableURL.hasSuffix("bash"), dashc?.raw == ["-c"] { 59 | return true 60 | } 61 | return false 62 | } 63 | 64 | public static func handle(executableURL: String, 65 | dashc: Commands.Arguments?) -> Commands.Arguments? { 66 | return dashc?.raw == ["-c"] ? dashc : nil 67 | } 68 | } 69 | } 70 | 71 | extension Commands.DashcTransformer { 72 | public struct Ruby: CommandsDashcTransformerHandler { 73 | public static func canHandle(executableURL: String, 74 | dashc: Commands.Arguments?) -> Bool { 75 | if executableURL.hasSuffix("ruby"), dashc?.raw == ["-e"] { 76 | return true 77 | } 78 | return false 79 | } 80 | 81 | public static func handle(executableURL: String, 82 | dashc: Commands.Arguments?) -> Commands.Arguments? { 83 | return dashc?.raw == ["-e"] ? dashc : nil 84 | } 85 | } 86 | } 87 | 88 | 89 | extension Commands.DashcTransformer { 90 | public struct Python: CommandsDashcTransformerHandler { 91 | public static func canHandle(executableURL: String, 92 | dashc: Commands.Arguments?) -> Bool { 93 | if executableURL.hasSuffix("python"), dashc?.raw == ["-c"] { 94 | return true 95 | } 96 | return false 97 | } 98 | 99 | public static func handle(executableURL: String, 100 | dashc: Commands.Arguments?) -> Commands.Arguments? { 101 | return dashc?.raw == ["-c"] ? dashc : nil 102 | } 103 | } 104 | } 105 | 106 | extension Commands.DashcTransformer { 107 | public struct Node: CommandsDashcTransformerHandler { 108 | public static func canHandle(executableURL: String, 109 | dashc: Commands.Arguments?) -> Bool { 110 | if executableURL.hasSuffix("node"), dashc?.raw == ["-e"] { 111 | return true 112 | } 113 | return false 114 | } 115 | 116 | public static func handle(executableURL: String, 117 | dashc: Commands.Arguments?) -> Commands.Arguments? { 118 | return dashc?.raw == ["-e"] ? dashc : nil 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Sources/Commands/CommandsENV.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandsENV.swift 3 | // 4 | // 5 | // Created by zhifei qiu on 2021/8/25. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Commands { 11 | public struct ENV { 12 | public private(set) var data: [String: String] = [:] 13 | 14 | public init(_ data: [String: String]? = ProcessInfo.processInfo.environment) { 15 | self.data = data ?? [:] 16 | } 17 | } 18 | } 19 | 20 | extension Commands.ENV { 21 | public static var global = Commands.ENV() 22 | } 23 | 24 | public extension Commands.ENV { 25 | mutating func add(PATH: String) { 26 | data["PATH"] = data["PATH"] == nil ? PATH : "\(PATH):\(data["PATH"]!)" 27 | } 28 | } 29 | 30 | public extension Commands.ENV { 31 | subscript(_ key: String) -> String? { 32 | set(newValue) { 33 | data[key] = newValue 34 | } 35 | get { 36 | return data[key] 37 | } 38 | } 39 | } 40 | 41 | extension Commands.ENV: Swift.ExpressibleByDictionaryLiteral { 42 | public init(dictionaryLiteral elements: (String, String)...) { 43 | let dictionary = elements.reduce(into: [String: String](), { $0[$1.0] = $1.1}) 44 | self.init(dictionary) 45 | } 46 | } 47 | 48 | extension Commands.ENV: Equatable { 49 | public static func == (lhs: Self, rhs: Self) -> Bool { 50 | lhs.data == rhs.data 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Commands/CommandsRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandsRequest.swift 3 | // 4 | // 5 | // Created by zhifei qiu on 2021/8/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Commands { 11 | struct Request { 12 | public var environment: ENV? { 13 | didSet { 14 | if audited { 15 | self.executableURL = getAbsoluteExecutableURL() 16 | } 17 | } 18 | } 19 | public var executableURL: String { 20 | didSet { 21 | if audited { 22 | self.executableURL = getAbsoluteExecutableURL() 23 | } 24 | } 25 | } 26 | public var dashc: Arguments? 27 | public var arguments: Arguments? 28 | public let audited: Bool 29 | public var absoluteCommand: String { 30 | return getAbsoluteCommand() 31 | } 32 | 33 | public init(_ environment: ENV? = ENV.global, 34 | executableURL: String, 35 | dashc: Arguments? = nil, 36 | arguments: Arguments? = nil, 37 | audited: Bool = true) { 38 | self.executableURL = executableURL 39 | self.environment = environment 40 | self.dashc = dashc 41 | self.arguments = arguments 42 | self.audited = audited 43 | if audited { 44 | self.executableURL = getAbsoluteExecutableURL() 45 | } 46 | } 47 | 48 | public init(_ string: String) { 49 | self = Self.create([string]) 50 | } 51 | 52 | public init(_ elements: [String]) { 53 | self = Self.create(elements) 54 | } 55 | } 56 | } 57 | 58 | extension Commands.Request: Swift.ExpressibleByStringLiteral { 59 | public init(stringLiteral value: StringLiteralType) { 60 | self.init(value) 61 | } 62 | 63 | public init(extendedGraphemeClusterLiteral value: String) { 64 | self.init(value) 65 | } 66 | 67 | public init(unicodeScalarLiteral value: String) { 68 | self.init(value) 69 | } 70 | } 71 | 72 | extension Commands.Request: Swift.ExpressibleByStringInterpolation { 73 | public init(stringInterpolation: DefaultStringInterpolation) { 74 | self.init(stringInterpolation.description) 75 | } 76 | } 77 | 78 | extension Commands.Request: Swift.ExpressibleByArrayLiteral { 79 | public init(arrayLiteral elements: String...) { 80 | self = Self.create(elements) 81 | } 82 | } 83 | 84 | private extension Commands.Request { 85 | func getAbsoluteExecutableURL() -> String { 86 | if FileManager.default.fileExists(atPath: executableURL) { 87 | return executableURL 88 | } 89 | guard let environment = environment else { return executableURL } 90 | let paths = environment["PATH"]?.split(separator: ":").map{ String($0) } ?? [] 91 | guard paths.count > 0 else { return executableURL } 92 | for path in paths { 93 | let absoluteExecutableURL = "\(path)/\(executableURL)" 94 | if FileManager.default.fileExists(atPath: absoluteExecutableURL) { 95 | return absoluteExecutableURL 96 | } 97 | } 98 | return executableURL 99 | } 100 | 101 | func getAbsoluteCommand() -> String { 102 | var result: [String] = [] 103 | if let environment = environment { 104 | let defaultENV = ProcessInfo.processInfo.environment 105 | result += environment.data 106 | .filter{ defaultENV[$0.0] != $0.1 } 107 | .map{ "\($0)=\($1)" } 108 | } 109 | result += [executableURL] 110 | result += dashc?.raw ?? [] 111 | result += arguments?.raw ?? [] 112 | return result.joined(separator: " ") 113 | } 114 | 115 | static func create(_ data: [String], 116 | prepareArguments: (([String]) -> ([String]))? = nil) -> Self { 117 | if data.count == 0 { 118 | return Commands.Request(executableURL: "") 119 | } 120 | 121 | let result = parse(data) 122 | 123 | let env = result.0.env 124 | let executableURL = result.1.executableURL 125 | let dashc = result.2.dashc 126 | let arguments = result.3.arguments 127 | 128 | var environment = Commands.ENV.global 129 | for arg in env { 130 | let key = String(arg[arg.startIndex.. Bool { 144 | lhs.environment == rhs.environment 145 | && lhs.executableURL == rhs.executableURL 146 | && lhs.dashc == rhs.dashc 147 | && lhs.arguments == rhs.arguments 148 | && lhs.audited == rhs.audited 149 | } 150 | } 151 | 152 | extension Commands.Request { 153 | private struct ENVResult { 154 | var env: [String] = [] 155 | var finished: Bool = false 156 | } 157 | 158 | private struct ExecutableURLResult { 159 | var executableURL: String = "" 160 | var finished: Bool = false 161 | } 162 | 163 | private struct DashcResult { 164 | var dashc: Commands.Arguments? = nil 165 | var finished: Bool = false 166 | } 167 | 168 | private struct ArgumentsResult { 169 | var arguments: Commands.Arguments? = nil 170 | var finished: Bool = false 171 | } 172 | 173 | private static func parse(_ data: [String]) -> ( 174 | ENVResult, ExecutableURLResult, DashcResult, ArgumentsResult) { 175 | var data = data.filter{ $0.split(separator: " ").map{ String($0) }.count > 0 } 176 | 177 | var envResult = ENVResult() 178 | var executableURLResult = ExecutableURLResult() 179 | var dashcResult = DashcResult() 180 | var argumentsResult = ArgumentsResult() 181 | 182 | while data.count > 0 { 183 | // env 184 | if !envResult.finished { 185 | let parameters = data.first!.split(separator: " ").map{ String($0) } 186 | if !parameters.first!.contains("=") { 187 | envResult.finished = true 188 | } else { 189 | envResult.env.append(parameters.first!) 190 | data = shift(parameters, data) 191 | } 192 | continue 193 | } 194 | 195 | // executableURL 196 | if !executableURLResult.finished { 197 | let parameters = data.first!.split(separator: " ").map{ String($0) } 198 | executableURLResult.executableURL = parameters.first! 199 | data = shift(parameters, data) 200 | executableURLResult.finished = true 201 | continue 202 | } 203 | 204 | // dashc 205 | if !dashcResult.finished { 206 | let parameters = data.first!.split(separator: " ").map{ String($0) } 207 | if parameters.first!.starts(with: "-"), 208 | let dashc = Commands.DashcTransformer.transformer(executableURL: executableURLResult.executableURL, dashc: Commands.Arguments(parameters.first!)) { 209 | dashcResult.dashc = dashc 210 | data = shift(parameters, data) 211 | } 212 | dashcResult.finished = true 213 | continue 214 | } 215 | 216 | // arguments 217 | if !argumentsResult.finished { 218 | if data.count > 0 { 219 | argumentsResult.arguments = Commands.Arguments(data) 220 | } 221 | data.removeAll() 222 | argumentsResult.finished = true 223 | continue 224 | } 225 | } 226 | 227 | return (envResult, executableURLResult, dashcResult, argumentsResult) 228 | } 229 | 230 | private static func shift(_ parameters: [String], 231 | _ data: [String]) -> [String] { 232 | var data = data 233 | let command = parameters.dropFirst().joined(separator: " ") 234 | if command.count == 0 { 235 | data.removeFirst() 236 | } else { 237 | data.replaceSubrange(0...0, with: [command]) 238 | } 239 | data = data.filter{ $0.split(separator: " ").map{ String($0) }.count > 0 } 240 | return data 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /Sources/Commands/CommandsResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandsResponse.swift 3 | // 4 | // 5 | // Created by zhifei qiu on 2021/8/25. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Commands { 11 | public struct Response { 12 | public let statusCode: Int32 13 | public let output: String 14 | public let errorOutput: String 15 | } 16 | } 17 | 18 | extension Commands.Response: Equatable { 19 | public static func == (lhs: Self, rhs: Self) -> Bool { 20 | lhs.statusCode == rhs.statusCode 21 | && lhs.output == rhs.output 22 | && lhs.errorOutput == rhs.errorOutput 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Commands/CommandsResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandsResult.swift 3 | // 4 | // 5 | // Created by zhifei qiu on 2021/8/2. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Commands { 11 | enum Result { 12 | case Success(_ request: Request, response: Response) 13 | case Failure(_ request: Request, response: Response) 14 | } 15 | } 16 | 17 | public extension Commands.Result { 18 | var request: Commands.Request { 19 | switch self { 20 | case .Success(let request, _): 21 | return request 22 | case .Failure(let request, _): 23 | return request 24 | } 25 | } 26 | 27 | var response: Commands.Response { 28 | switch self { 29 | case .Success(_, let response): 30 | return response 31 | case .Failure(_, let response): 32 | return response 33 | } 34 | } 35 | 36 | var statusCode: Int32 { 37 | return response.statusCode 38 | } 39 | 40 | var output: String { 41 | return response.output 42 | } 43 | 44 | var errorOutput: String { 45 | return response.errorOutput 46 | } 47 | } 48 | 49 | public extension Commands.Result { 50 | var isSuccess: Bool { 51 | switch self { 52 | case .Success(_, _): 53 | return true 54 | default: 55 | return false 56 | } 57 | } 58 | 59 | var isFailure: Bool { 60 | !isSuccess 61 | } 62 | } 63 | 64 | extension Commands.Result: Equatable { 65 | public static func == (lhs: Self, rhs: Self) -> Bool { 66 | lhs.request == rhs.request 67 | && lhs.response == rhs.response 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/Commands/CommandsTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandsTask.swift 3 | // 4 | // 5 | // Created by zhifei qiu on 2021/8/4. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Commands { 11 | enum Task { } 12 | } 13 | 14 | public extension Commands.Task { 15 | @discardableResult 16 | static func run(_ request: Commands.Request) -> Commands.Result { 17 | let process = prepare(request) 18 | 19 | let outputPipe = Pipe() 20 | process.standardOutput = outputPipe 21 | 22 | let errorPipe = Pipe() 23 | process.standardError = errorPipe 24 | 25 | do { 26 | try run(process) 27 | 28 | let outputActual = try fileHandleData(fileHandle: outputPipe.fileHandleForReading) ?? "" 29 | let errorActual = try fileHandleData(fileHandle: errorPipe.fileHandleForReading) ?? "" 30 | 31 | if process.terminationStatus == EXIT_SUCCESS { 32 | let response = Commands.Response(statusCode: process.terminationStatus, 33 | output: outputActual, 34 | errorOutput: errorActual) 35 | return Commands.Result.Success(request, response: response) 36 | } 37 | 38 | let response = Commands.Response(statusCode: process.terminationStatus, 39 | output: outputActual, 40 | errorOutput: errorActual) 41 | return Commands.Result.Failure(request, response: response) 42 | } catch let error { 43 | let response = Commands.Response(statusCode: EXIT_FAILURE, 44 | output: "", 45 | errorOutput: error.localizedDescription) 46 | return Commands.Result.Failure(request, response: response) 47 | } 48 | } 49 | 50 | static func system(_ request: Commands.Request, 51 | output: ((String) -> Void)?, 52 | errorOutput: ((String) -> Void)?) { 53 | let process = prepare(request) 54 | do { 55 | if let output = output { 56 | let outputPipe = Pipe() 57 | process.standardOutput = outputPipe 58 | outputPipe.fileHandleForReading.readabilityHandler = { (handler) in 59 | let data = handler.availableData 60 | if data.count > 0, 61 | let result = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) { 62 | DispatchQueue.main.async { 63 | output(result) 64 | } 65 | } 66 | } 67 | } 68 | if let errorOutput = errorOutput { 69 | let errorPipe = Pipe() 70 | process.standardError = errorPipe 71 | errorPipe.fileHandleForReading.readabilityHandler = { (handler) in 72 | let data = handler.availableData 73 | if data.count > 0, 74 | let result = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) { 75 | DispatchQueue.main.async { 76 | errorOutput(result) 77 | } 78 | } 79 | } 80 | } 81 | 82 | try run(process) 83 | 84 | if let _ = output, let outputPipe = process.standardOutput as? Pipe { 85 | outputPipe.fileHandleForReading.readabilityHandler = nil 86 | } 87 | if let _ = errorOutput, let errorPipe = process.standardError as? Pipe { 88 | errorPipe.fileHandleForReading.readabilityHandler = nil 89 | } 90 | } catch let error { 91 | print(error.localizedDescription) 92 | } 93 | } 94 | 95 | static func system(_ request: Commands.Request) { 96 | let process = prepare(request) 97 | do { 98 | try run(process) 99 | } catch let error { 100 | print(error.localizedDescription) 101 | } 102 | } 103 | } 104 | 105 | public extension Commands.Task { 106 | static func prepare(_ request: Commands.Request) -> Process { 107 | let process = Process() 108 | if #available(macOS 10.13, *) { 109 | process.executableURL = URL(fileURLWithPath: request.executableURL) 110 | } else { 111 | process.launchPath = request.executableURL 112 | } 113 | if let environment = request.environment?.data { 114 | process.environment = environment 115 | } 116 | if let dashc = request.dashc { 117 | var arguments = dashc.raw 118 | arguments.append(contentsOf: request.arguments?.raw ?? []) 119 | process.arguments = arguments 120 | } else { 121 | process.arguments = request.arguments?.raw ?? [] 122 | } 123 | return process 124 | } 125 | 126 | static func run(_ process: Process) throws { 127 | if #available(macOS 10.13, *) { 128 | try process.run() 129 | } else { 130 | process.launch() 131 | } 132 | process.waitUntilExit() 133 | } 134 | } 135 | 136 | private extension Commands.Task { 137 | static func fileHandleData(fileHandle: FileHandle) throws -> String? { 138 | var outputData: Data? 139 | if #available(macOS 10.15.4, *) { 140 | outputData = try fileHandle.readToEnd() 141 | } else { 142 | outputData = fileHandle.readDataToEndOfFile() 143 | } 144 | if let outputData = outputData { 145 | return String(data: outputData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) 146 | } 147 | return nil 148 | } 149 | } 150 | 151 | -------------------------------------------------------------------------------- /Tests/CommandsTests/RequestParseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestParseTests.swift 3 | // 4 | // 5 | // Created by zhifei qiu on 2021/9/10. 6 | // 7 | 8 | import XCTest 9 | import Commands 10 | 11 | final class RequestParseTests: XCTestCase { 12 | func testRequestParseWhitespace() throws { 13 | XCTAssertEqual(Commands.Request(""), 14 | Commands.Request(Commands.ENV(), 15 | executableURL: "", 16 | dashc: nil, 17 | arguments: nil, 18 | audited: true)) 19 | 20 | XCTAssertEqual(Commands.Request(" "), 21 | Commands.Request(Commands.ENV(), 22 | executableURL: "", 23 | dashc: nil, 24 | arguments: nil, 25 | audited: true)) 26 | 27 | XCTAssertEqual(Commands.Request(["", " "]), 28 | Commands.Request(Commands.ENV(), 29 | executableURL: "", 30 | dashc: nil, 31 | arguments: nil, 32 | audited: true)) 33 | } 34 | 35 | func testRequestParse() { 36 | do { 37 | let request = Commands.Request("bash -c ls") 38 | let requestX = Commands.Request(["bash", "-c", "ls"]) 39 | XCTAssert(request == requestX) 40 | XCTAssertEqual(Commands.Task.run(request), Commands.Task.run(requestX)) 41 | } 42 | 43 | do { 44 | let request = Commands.Request("https_proxy=http://127.0.0.1:7891 http_proxy=http://127.0.0.1:7891 all_proxy=socks5://127.0.0.1:7891 curl https://api64.ipify.org?format=json") 45 | 46 | var env = Commands.ENV() 47 | env["https_proxy"] = "http://127.0.0.1:7891" 48 | env["http_proxy"] = "http://127.0.0.1:7891" 49 | env["all_proxy"] = "socks5://127.0.0.1:7891" 50 | let requestX = Commands.Request(env, 51 | executableURL: "curl", 52 | dashc: nil, 53 | arguments: "https://api64.ipify.org?format=json", 54 | audited: true) 55 | XCTAssert(request == requestX) 56 | XCTAssertEqual(Commands.Task.run(request).statusCode, Commands.Task.run(requestX).statusCode) 57 | } 58 | 59 | do { 60 | let request = Commands.Request(["curl", "-i", "https://api64.ipify.org?format=json"]) 61 | let requestX = Commands.Request(Commands.ENV(), 62 | executableURL: "curl", 63 | dashc: nil, 64 | arguments: ["-i", "https://api64.ipify.org?format=json"], 65 | audited: true) 66 | XCTAssert(request == requestX) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Tests/CommandsTests/Resources/python/main.py: -------------------------------------------------------------------------------- 1 | import base64; 2 | print(base64.b64encode(b'qiuzhifei').decode('ascii')); 3 | -------------------------------------------------------------------------------- /Tests/CommandsTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(ObjectiveC) 2 | import XCTest 3 | 4 | extension swift_commandsTests { 5 | // DO NOT MODIFY: This is autogenerated, use: 6 | // `swift test --generate-linuxmain` 7 | // to regenerate. 8 | static let __allTests__swift_commandsTests = [ 9 | ("testCommandsBash", testCommandsBash), 10 | ("testCommandsCustom", testCommandsCustom), 11 | ("testCommandsENV", testCommandsENV), 12 | ("testCommandsNode", testCommandsNode), 13 | ("testCommandsPython", testCommandsPython), 14 | ("testCommandsRuby", testCommandsRuby), 15 | ("testCommansAlias", testCommansAlias), 16 | ("testCommansTask", testCommansTask), 17 | ] 18 | } 19 | 20 | public func __allTests() -> [XCTestCaseEntry] { 21 | return [ 22 | testCase(swift_commandsTests.__allTests__swift_commandsTests), 23 | ] 24 | } 25 | #endif 26 | -------------------------------------------------------------------------------- /Tests/CommandsTests/swift_commandsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Commands 3 | 4 | final class swift_commandsTests: XCTestCase { 5 | func testCommandsBash() throws { 6 | XCTAssert(Commands.Bash.run("pwd").output == FileManager.default.currentDirectoryPath) 7 | 8 | let mktempResult = Commands.Bash.run("mktemp -d -t swift-commands") 9 | XCTAssert(mktempResult.isSuccess) 10 | let dir = mktempResult.output 11 | XCTAssert(dir.contains("swift-commands")) 12 | 13 | let rmDirResult = Commands.Bash.run("rm -rf \(dir)") 14 | XCTAssert(rmDirResult.isSuccess) 15 | 16 | let lsResult = Commands.Bash.run("ls \(dir)") 17 | XCTAssert(lsResult.isFailure) 18 | XCTAssert(lsResult.errorOutput.contains("No such file or directory")) 19 | } 20 | 21 | func testCommandsRuby() throws { 22 | let ruby = Commands.Ruby 23 | 24 | let base64Result = ruby.run("require 'base64'; puts Base64.encode64('qiuzhifei')") 25 | XCTAssert(base64Result.isSuccess) 26 | XCTAssert(base64Result.output == "qiuzhifei".data(using: .utf8)!.base64EncodedString()) 27 | XCTAssert(base64Result.output == "cWl1emhpZmVp") 28 | 29 | XCTAssert(ruby.run("import 'qiuzhifei'").isFailure) 30 | } 31 | 32 | func testCommandsPython() throws { 33 | let python = Commands.Python 34 | 35 | let base64Result = python.run("import base64; print(base64.b64encode(b'qiuzhifei').decode('ascii'))") 36 | XCTAssert(base64Result.isSuccess) 37 | XCTAssert(base64Result.output == "qiuzhifei".data(using: .utf8)!.base64EncodedString()) 38 | XCTAssert(base64Result.output == "cWl1emhpZmVp") 39 | 40 | XCTAssert(python.run("import qiuzhifei").isFailure) 41 | } 42 | 43 | func testCommandsNode() throws { 44 | let node = Commands.Node 45 | 46 | let base64Result = node.run("console.log(Buffer.from('qiuzhifei').toString('base64'))") 47 | debugPrint(base64Result) 48 | XCTAssert(base64Result.isSuccess) 49 | XCTAssert(base64Result.output == "qiuzhifei".data(using: .utf8)!.base64EncodedString()) 50 | XCTAssert(base64Result.output == "cWl1emhpZmVp") 51 | 52 | XCTAssert(node.run("import qiuzhifei").isFailure) 53 | } 54 | 55 | func testCommandsCustom() throws { 56 | let python = Commands.Alias("python", dashc: "-c") 57 | let pythonLogResult = python.run("print('qiuzhifei')") 58 | XCTAssert(pythonLogResult.isSuccess) 59 | XCTAssert(pythonLogResult.output == "qiuzhifei") 60 | 61 | let node = Commands.Alias("node1", dashc: "-e") 62 | let nodeLogResult = node.run("console.log('qiuzhifei')") 63 | XCTAssert(nodeLogResult.isFailure) 64 | } 65 | 66 | func testCommandsENV() throws { 67 | let bashDebugResult = Commands.Bash.run("echo $commands_debug") 68 | XCTAssert(bashDebugResult.isSuccess) 69 | XCTAssert(bashDebugResult.output == "") 70 | 71 | var env = Commands.ENV() 72 | env["commands_debug"] = "1" 73 | let bashDebugENVResult = Commands.Bash.run("echo $commands_debug", environment: env) 74 | XCTAssert(bashDebugENVResult.isSuccess) 75 | XCTAssert(bashDebugENVResult.output == "1") 76 | } 77 | 78 | func testCommansAlias() throws { 79 | // bash 80 | XCTAssertEqual( 81 | Commands.Bash.run("ls"), 82 | Commands.Task.run("bash -c ls") 83 | ) 84 | 85 | // ruby 86 | XCTAssertEqual( 87 | Commands.Ruby.run("require 'base64'; puts Base64.encode64('qiuzhifei')"), 88 | Commands.Task.run("ruby -e require 'base64'; puts Base64.encode64('qiuzhifei')") 89 | ) 90 | 91 | // python 92 | XCTAssertEqual( 93 | Commands.Python.run("import base64; print(base64.b64encode(b'qiuzhifei').decode('ascii'))"), 94 | Commands.Task.run("python -c import base64; print(base64.b64encode(b'qiuzhifei').decode('ascii'))") 95 | ) 96 | 97 | // node 98 | XCTAssertEqual( 99 | Commands.Node.run("console.log(Buffer.from('qiuzhifei').toString('base64'))"), 100 | Commands.Task.run("node -e console.log(Buffer.from('qiuzhifei').toString('base64'))") 101 | ) 102 | } 103 | 104 | func testCommansTask() throws { 105 | XCTAssert(Commands.Task.run([]).isFailure) 106 | 107 | XCTAssert(Commands.Task.run("bash -c pwd").output == FileManager.default.currentDirectoryPath) 108 | 109 | let path = Bundle.module.path(forResource: "main", ofType: "py")! 110 | let pythonResult = Commands.Task.run("python \(path)") 111 | XCTAssert(pythonResult.isSuccess) 112 | XCTAssert(pythonResult.output == "cWl1emhpZmVp") 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import CommandsTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += CommandsTests.__allTests() 7 | 8 | XCTMain(tests) 9 | --------------------------------------------------------------------------------