├── .editorconfig
├── .github
├── FUNDING.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── .spi.yml
├── CODE_OF_CONDUCT.md
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── ProcessEnv
│ ├── Process+Output.swift
│ ├── Process+Parameters.swift
│ └── ProcessInfo+UserEnvironment.swift
└── Tests
└── ProcessEnvTests
└── ProcessEnvTests.swift
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [mattmassicotte]
2 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths-ignore:
8 | - 'README.md'
9 | - 'CODE_OF_CONDUCT.md'
10 | - '.editorconfig'
11 | - '.spi.yml'
12 | pull_request:
13 | branches:
14 | - main
15 |
16 | concurrency:
17 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
18 | cancel-in-progress: true
19 |
20 | jobs:
21 | test:
22 | name: Test
23 | runs-on: macOS-15
24 | timeout-minutes: 30
25 | env:
26 | DEVELOPER_DIR: /Applications/Xcode_16.3.app
27 | steps:
28 | - uses: actions/checkout@v4
29 | - name: Test
30 | run: set -o pipefail && xcodebuild -scheme ProcessEnv -destination "platform=macOS" test | xcbeautify
31 |
32 | linux_test:
33 | name: Test Linux
34 | runs-on: ubuntu-latest
35 | timeout-minutes: 30
36 | strategy:
37 | matrix:
38 | swift-version:
39 | - 6.0.3
40 | - 6.1
41 | steps:
42 | - name: Checkout
43 | uses: actions/checkout@v4
44 | - name: Swiftly
45 | uses: vapor/swiftly-action@v0.2.0
46 | with:
47 | toolchain: ${{ matrix.swift-version }}
48 | - name: Test
49 | run: swift test
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Carthage
4 | /Packages
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | builder:
3 | configs:
4 | - documentation_targets: [ProcessEnv]
5 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 |
2 | # Contributor Covenant Code of Conduct
3 |
4 | ## Our Pledge
5 |
6 | We as members, contributors, and leaders pledge to make participation in our
7 | community a harassment-free experience for everyone, regardless of age, body
8 | size, visible or invisible disability, ethnicity, sex characteristics, gender
9 | identity and expression, level of experience, education, socio-economic status,
10 | nationality, personal appearance, race, caste, color, religion, or sexual
11 | identity and orientation.
12 |
13 | We pledge to act and interact in ways that contribute to an open, welcoming,
14 | diverse, inclusive, and healthy community.
15 |
16 | ## Our Standards
17 |
18 | Examples of behavior that contributes to a positive environment for our
19 | community include:
20 |
21 | * Demonstrating empathy and kindness toward other people
22 | * Being respectful of differing opinions, viewpoints, and experiences
23 | * Giving and gracefully accepting constructive feedback
24 | * Accepting responsibility and apologizing to those affected by our mistakes,
25 | and learning from the experience
26 | * Focusing on what is best not just for us as individuals, but for the overall
27 | community
28 |
29 | Examples of unacceptable behavior include:
30 |
31 | * The use of sexualized language or imagery, and sexual attention or advances of
32 | any kind
33 | * Trolling, insulting or derogatory comments, and personal or political attacks
34 | * Public or private harassment
35 | * Publishing others' private information, such as a physical or email address,
36 | without their explicit permission
37 | * Other conduct which could reasonably be considered inappropriate in a
38 | professional setting
39 |
40 | ## Enforcement Responsibilities
41 |
42 | Community leaders are responsible for clarifying and enforcing our standards of
43 | acceptable behavior and will take appropriate and fair corrective action in
44 | response to any behavior that they deem inappropriate, threatening, offensive,
45 | or harmful.
46 |
47 | Community leaders have the right and responsibility to remove, edit, or reject
48 | comments, commits, code, wiki edits, issues, and other contributions that are
49 | not aligned to this Code of Conduct, and will communicate reasons for moderation
50 | decisions when appropriate.
51 |
52 | ## Scope
53 |
54 | This Code of Conduct applies within all community spaces, and also applies when
55 | an individual is officially representing the community in public spaces.
56 | Examples of representing our community include using an official e-mail address,
57 | posting via an official social media account, or acting as an appointed
58 | representative at an online or offline event.
59 |
60 | ## Enforcement
61 |
62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
63 | reported to the community leaders responsible for enforcement at
64 | support@chimehq.com.
65 | All complaints will be reviewed and investigated promptly and fairly.
66 |
67 | All community leaders are obligated to respect the privacy and security of the
68 | reporter of any incident.
69 |
70 | ## Enforcement Guidelines
71 |
72 | Community leaders will follow these Community Impact Guidelines in determining
73 | the consequences for any action they deem in violation of this Code of Conduct:
74 |
75 | ### 1. Correction
76 |
77 | **Community Impact**: Use of inappropriate language or other behavior deemed
78 | unprofessional or unwelcome in the community.
79 |
80 | **Consequence**: A private, written warning from community leaders, providing
81 | clarity around the nature of the violation and an explanation of why the
82 | behavior was inappropriate. A public apology may be requested.
83 |
84 | ### 2. Warning
85 |
86 | **Community Impact**: A violation through a single incident or series of
87 | actions.
88 |
89 | **Consequence**: A warning with consequences for continued behavior. No
90 | interaction with the people involved, including unsolicited interaction with
91 | those enforcing the Code of Conduct, for a specified period of time. This
92 | includes avoiding interactions in community spaces as well as external channels
93 | like social media. Violating these terms may lead to a temporary or permanent
94 | ban.
95 |
96 | ### 3. Temporary Ban
97 |
98 | **Community Impact**: A serious violation of community standards, including
99 | sustained inappropriate behavior.
100 |
101 | **Consequence**: A temporary ban from any sort of interaction or public
102 | communication with the community for a specified period of time. No public or
103 | private interaction with the people involved, including unsolicited interaction
104 | with those enforcing the Code of Conduct, is allowed during this period.
105 | Violating these terms may lead to a permanent ban.
106 |
107 | ### 4. Permanent Ban
108 |
109 | **Community Impact**: Demonstrating a pattern of violation of community
110 | standards, including sustained inappropriate behavior, harassment of an
111 | individual, or aggression toward or disparagement of classes of individuals.
112 |
113 | **Consequence**: A permanent ban from any sort of public interaction within the
114 | community.
115 |
116 | ## Attribution
117 |
118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
119 | version 2.1, available at
120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
121 |
122 | Community Impact Guidelines were inspired by
123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
124 |
125 | For answers to common questions about this code of conduct, see the FAQ at
126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
127 | [https://www.contributor-covenant.org/translations][translations].
128 |
129 | [homepage]: https://www.contributor-covenant.org
130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
131 | [Mozilla CoC]: https://github.com/mozilla/diversity
132 | [FAQ]: https://www.contributor-covenant.org/faq
133 | [translations]: https://www.contributor-covenant.org/translations
134 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2020, Chime
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.8
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "ProcessEnv",
7 | platforms: [.macOS(.v10_13)],
8 | products: [
9 | .library(name: "ProcessEnv", targets: ["ProcessEnv"]),
10 | ],
11 | dependencies: [],
12 | targets: [
13 | .target(name: "ProcessEnv", dependencies: []),
14 | .testTarget(name: "ProcessEnvTests", dependencies: ["ProcessEnv"]),
15 | ]
16 | )
17 |
18 | let swiftSettings: [SwiftSetting] = [
19 | .enableExperimentalFeature("StrictConcurrency")
20 | ]
21 |
22 | for target in package.targets {
23 | var settings = target.swiftSettings ?? []
24 | settings.append(contentsOf: swiftSettings)
25 | target.swiftSettings = settings
26 | }
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [![Build Status][build status badge]][build status]
4 | [![Platforms][platforms badge]][platforms]
5 | [![Documentation][documentation badge]][documentation]
6 | [![Matrix][matrix badge]][matrix]
7 |
8 |
9 |
10 | # ProcessEnv
11 |
12 | ProcessEnv is a small library for capturing a user's shell configuration. This is very handy for launching `Process` instances with the same configuration. It also contains a few niceties for working with `Process`.
13 |
14 | ## Integration
15 |
16 | Swift Package Manager:
17 |
18 | ```swift
19 | dependencies: [
20 | .package(url: "https://github.com/ChimeHQ/ProcessEnv", branch: "main")
21 | ]
22 | ```
23 |
24 | ## Extensions
25 |
26 | The bulk of code is in the form of extensions on `ProcessInfo`, for accessing various environment variables. A single `[String : String]` dictionary of the user's environment is available as the `userEnvironment` property.
27 |
28 | ```swift
29 | ProcessInfo.processInfo.userEnvironment
30 |
31 | ProcessInfo.processInfo.path // $PATH
32 | ProcessInfo.processInfo.homePath
33 | ProcessInfo.processInfo.shellExecutablePath
34 | ```
35 |
36 | ## Contributing and Collaboration
37 |
38 | I would love to hear from you! Issues or pull requests work great. Both a [Matrix space][matrix] and [Discord][discord] are available for live help, but I have a strong bias towards answering in the form of documentation. You can also find me on [the web](https://www.massicotte.org).
39 |
40 | I prefer collaboration, and would love to find ways to work together if you have a similar project.
41 |
42 | I prefer indentation with tabs for improved accessibility. But, I'd rather you use the system you want and make a PR than hesitate because of whitespace.
43 |
44 | By participating in this project you agree to abide by the [Contributor Code of Conduct](CODE_OF_CONDUCT.md).
45 |
46 | [build status]: https://github.com/ChimeHQ/ProcessEnv/actions
47 | [build status badge]: https://github.com/ChimeHQ/ProcessEnv/workflows/CI/badge.svg
48 | [platforms]: https://swiftpackageindex.com/ChimeHQ/ProcessEnv
49 | [platforms badge]: https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FChimeHQ%2FProcessEnv%2Fbadge%3Ftype%3Dplatforms
50 | [documentation]: https://swiftpackageindex.com/ChimeHQ/ProcessEnv/main/documentation
51 | [documentation badge]: https://img.shields.io/badge/Documentation-DocC-blue
52 | [matrix]: https://matrix.to/#/%23chimehq%3Amatrix.org
53 | [matrix badge]: https://img.shields.io/matrix/chimehq%3Amatrix.org?label=Matrix
54 | [discord]: https://discord.gg/esFpX6sErJ
55 |
--------------------------------------------------------------------------------
/Sources/ProcessEnv/Process+Output.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public extension Process {
4 | static func execute(_ parameters: Process.ExecutionParameters) throws -> Data? {
5 | let task = Process(parameters: parameters)
6 |
7 | return try? task.runAndReadStdout()
8 | }
9 |
10 | static func executeAsUser(_ parameters: Process.ExecutionParameters) throws -> Data? {
11 | let userParams = parameters.userShellInvocation()
12 |
13 | let task = Process(parameters: userParams)
14 |
15 | return try? task.runAndReadStdout()
16 | }
17 |
18 | static func readOutput(from launchPath: String, arguments: [String] = [], environment: [String : String] = [:]) -> Data? {
19 | let params = Process.ExecutionParameters(path: launchPath, arguments: arguments, environment: environment)
20 |
21 | return try? execute(params)
22 | }
23 |
24 | func runAndReadStdout() throws -> Data? {
25 | let pipe = Pipe()
26 |
27 | standardOutput = pipe
28 |
29 | if #available(macOS 10.13, *) {
30 | try run()
31 | } else {
32 | if let launchPath = launchPath, FileManager.default.isExecutableFile(atPath: launchPath) == false {
33 | return nil
34 | }
35 |
36 | launch()
37 | }
38 |
39 | waitUntilExit()
40 |
41 | if #available(macOS 10.15.4, *) {
42 | return try pipe.fileHandleForReading.readToEnd()
43 | } else {
44 | return pipe.fileHandleForReading.readDataToEndOfFile()
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/ProcessEnv/Process+Parameters.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | #if os(macOS) || os(Linux)
4 |
5 | public extension Process {
6 | /// Wraps up all of the parameters needed for starting a Process into one single type.
7 | struct ExecutionParameters: Codable, Hashable, Sendable {
8 | public var path: String
9 | public var arguments: [String]
10 | public var environment: [String : String]?
11 | public var currentDirectoryURL: URL?
12 |
13 | public init(path: String, arguments: [String] = [], environment: [String : String]? = nil, currentDirectoryURL: URL? = nil) {
14 | self.path = path
15 | self.arguments = arguments
16 | self.environment = environment
17 | self.currentDirectoryURL = currentDirectoryURL
18 | }
19 |
20 | public var command: String {
21 | let escapedArgs = arguments.map { arg in
22 | let range = arg.rangeOfCharacter(from: .whitespacesAndNewlines)
23 |
24 | if let range, range.isEmpty == false {
25 | return "\"" + arg + "\""
26 | }
27 |
28 | return arg
29 | }
30 |
31 | return ([path] + escapedArgs).joined(separator: " ")
32 | }
33 |
34 | /// Returns parameters that emulate an invocation in the user's shell
35 | ///
36 | /// This is done by executing:
37 | ///
38 | /// shellExecutablePath -ilc
39 | ///
40 | /// This method executes this with the `environment` environment
41 | /// variables set. But, it also ensures that the `TERM`, `HOME`, and
42 | /// `PATH` variables have values, if aren't present in `environment`.
43 | ///
44 | /// The `-i` and `-l` flags are critical, as they control how many
45 | /// shells read configuration files.
46 | public func userShellInvocation() -> ExecutionParameters {
47 | let processInfo = ProcessInfo.processInfo
48 |
49 | let shellPath = processInfo.shellExecutablePath
50 | let args = ["-ilc", command]
51 | let cwdURL = currentDirectoryURL
52 |
53 | let defaultEnv = ["TERM": "xterm-256color",
54 | "HOME": processInfo.homePath,
55 | "PATH": processInfo.path]
56 |
57 | let baseEnv = environment ?? defaultEnv
58 |
59 | let env = baseEnv.merging(defaultEnv, uniquingKeysWith: { (a, _) in a })
60 |
61 | return ExecutionParameters(path: shellPath,
62 | arguments: args,
63 | environment: env,
64 | currentDirectoryURL: cwdURL)
65 | }
66 | }
67 |
68 | private var compatibleCurrentDirectoryURL: URL? {
69 | get {
70 | if #available(macOS 10.13, *) {
71 | return currentDirectoryURL
72 | } else {
73 | return URL(fileURLWithPath: currentDirectoryPath, isDirectory: true)
74 | }
75 | }
76 | set {
77 | if #available(macOS 10.13, *) {
78 | currentDirectoryURL = newValue
79 | return
80 | }
81 |
82 | if let cwdPath = newValue?.path {
83 | self.currentDirectoryPath = cwdPath
84 | }
85 | }
86 | }
87 |
88 | var parameters: ExecutionParameters {
89 | get {
90 | return ExecutionParameters(path: self.launchPath ?? "",
91 | arguments: arguments ?? [],
92 | environment: self.environment,
93 | currentDirectoryURL: self.compatibleCurrentDirectoryURL)
94 | }
95 | set {
96 | self.launchPath = newValue.path
97 | self.arguments = newValue.arguments
98 | self.environment = newValue.environment
99 | self.compatibleCurrentDirectoryURL = newValue.currentDirectoryURL
100 |
101 | }
102 | }
103 |
104 | convenience init(parameters: ExecutionParameters) {
105 | self.init()
106 |
107 | self.parameters = parameters
108 | }
109 | }
110 |
111 | #endif
112 |
--------------------------------------------------------------------------------
/Sources/ProcessEnv/ProcessInfo+UserEnvironment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProcessInfo+UserEnvironment.swift
3 | // ProcessEnv
4 | //
5 | // Created by Matthew Massicotte on 2019-02-12.
6 | //
7 |
8 | import Foundation
9 |
10 | public extension Process {
11 | @available(*, deprecated, renamed: "Environment")
12 | typealias Envrionment = [String : String]
13 | typealias Environment = [String : String]
14 | }
15 |
16 | extension ProcessInfo {
17 | /// The path to the current user's shell executable
18 | ///
19 | /// This attempts to query the `SHELL` environment variable, the
20 | /// password directory (via `getpwuid`), or if those fail
21 | /// falls back to "/bin/bash".
22 | public var shellExecutablePath: String {
23 | if let value = environment["SHELL"], !value.isEmpty {
24 | return value
25 | }
26 |
27 | if let value = pwShell, !value.isEmpty {
28 | return value
29 | }
30 |
31 | // this is a terrible fallback, but we need something
32 | return "/bin/bash"
33 | }
34 |
35 | public var pwShell: String? {
36 | guard let passwd = getpwuid(getuid()) else {
37 | return nil
38 | }
39 |
40 | guard let cString = passwd.pointee.pw_shell else {
41 | return nil
42 | }
43 |
44 | return String(cString: cString)
45 | }
46 |
47 | public var pwUserName: String? {
48 | guard let passwd = getpwuid(getuid()) else {
49 | return nil
50 | }
51 |
52 | guard let cString = passwd.pointee.pw_name else {
53 | return nil
54 | }
55 |
56 | return String(cString: cString)
57 | }
58 |
59 | public var pwDir: String? {
60 | guard let passwd = getpwuid(getuid()) else {
61 | return nil
62 | }
63 |
64 | guard let cString = passwd.pointee.pw_dir else {
65 | return nil
66 | }
67 |
68 | return String(cString: cString)
69 | }
70 | /// Returns the value of PATH
71 | ///
72 | /// If PATH is set in the environment, it is returned. If not,
73 | /// the fallback value of "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
74 | /// is returned.
75 | public var path: String {
76 | return environment["PATH"] ?? "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
77 | }
78 |
79 | public var homePath: String {
80 | if let path = environment["HOME"] {
81 | return path
82 | }
83 |
84 | if let path = pwDir {
85 | return path
86 | }
87 |
88 | if #available(macOS 12.0, *) {
89 | return "/Users/\(userName)"
90 | }
91 |
92 | if let name = pwUserName {
93 | return "/Users/\(name)"
94 | }
95 |
96 | // I'm not sure there is a reasonable fallback in this situation
97 | return ""
98 | }
99 |
100 | public var sandboxContainerId: String? {
101 | environment["APP_SANDBOX_CONTAINER_ID"]
102 | }
103 |
104 | /// Returns true if the process is running with sandboxing enabled.
105 | public var isSandboxed: Bool {
106 | sandboxContainerId != nil
107 | }
108 |
109 | /// Capture the interactive-login shell environment
110 | ///
111 | /// This method attempts to reconstruct the user
112 | /// environment that would be set up when logging into
113 | /// a terminal session.
114 | public var userEnvironment: [String : String] {
115 | guard let data = try? Process.executeAsUser(Process.ExecutionParameters(path: "/usr/bin/env", environment: environment)) else {
116 | return environment
117 | }
118 |
119 | return parseEnvOutput(data)
120 | }
121 |
122 | func parseEnvOutput(_ data: Data) -> [String : String] {
123 | guard let string = String(data: data, encoding: .utf8) else {
124 | return [:]
125 | }
126 |
127 | var env: [String: String] = [:]
128 | let charSet = CharacterSet.whitespaces
129 |
130 | string.enumerateLines { (line, _) in
131 | let components = line.split(separator: "=")
132 |
133 | guard let key = components.first?.trimmingCharacters(in: charSet) else {
134 | return
135 | }
136 |
137 | let value = components.dropFirst().joined(separator: "=").trimmingCharacters(in: charSet)
138 |
139 | env[key] = value
140 | }
141 |
142 | return env
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/Tests/ProcessEnvTests/ProcessEnvTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import ProcessEnv
3 |
4 | final class ProcessEnvTests: XCTestCase {
5 | func testParseEnvOutput() throws {
6 | let output = """
7 | TERM=xterm-256color
8 | TERM_PROGRAM=Apple_Terminal
9 | """
10 |
11 | let data = ProcessInfo.processInfo.parseEnvOutput(output.data(using: .utf8)!)
12 |
13 | XCTAssertEqual(data, ["TERM" : "xterm-256color", "TERM_PROGRAM" : "Apple_Terminal"])
14 | }
15 |
16 | func testEnvOutputWithEqualsInValue() throws {
17 | let output = """
18 | KEY=tricky=value
19 | """
20 |
21 | let data = ProcessInfo.processInfo.parseEnvOutput(output.data(using: .utf8)!)
22 |
23 | XCTAssertEqual(data, ["KEY" : "tricky=value"])
24 | }
25 |
26 | #if !os(Linux)
27 | func testEnvironmentVariables() throws {
28 | let env = ProcessInfo.processInfo.userEnvironment
29 |
30 | XCTAssertFalse(env.isEmpty)
31 |
32 | XCTAssertNotNil(env["SHELL"])
33 | XCTAssertNotNil(env["HOME"])
34 | }
35 | #endif
36 |
37 | func testParameterCommand() throws {
38 | let params = Process.ExecutionParameters(path: "cmd", arguments: ["-u", "-v"])
39 |
40 | XCTAssertEqual(params.command, "cmd -u -v")
41 | }
42 |
43 | func testParameterCommandWithSpaces() throws {
44 | let params = Process.ExecutionParameters(path: "cmd", arguments: ["-u", "has spaces"])
45 |
46 | XCTAssertEqual(params.command, "cmd -u \"has spaces\"") }
47 |
48 | func testUserShellParameters() throws {
49 | let params = Process.ExecutionParameters(path: "cmd", arguments: ["-u", "-v"])
50 |
51 | let userParams = params.userShellInvocation()
52 |
53 | XCTAssertEqual(userParams.path, ProcessInfo.processInfo.shellExecutablePath)
54 | XCTAssertEqual(userParams.arguments, ["-ilc", "cmd -u -v"])
55 | }
56 |
57 | func testUserShellParametersWithSpaces() throws {
58 | let params = Process.ExecutionParameters(path: "cmd", arguments: ["-u", "white\nspace"])
59 |
60 | let userParams = params.userShellInvocation()
61 |
62 | XCTAssertEqual(userParams.path, ProcessInfo.processInfo.shellExecutablePath)
63 | XCTAssertEqual(userParams.arguments, ["-ilc", "cmd -u \"white\nspace\""])
64 | }
65 | }
66 |
--------------------------------------------------------------------------------