1/**
2 * Describes child processes before they are started.
3 *
4 * A `Command` stores the executable, arguments, environment, standard streams,
5 * working directory, and other process options. Commands can also be piped
6 * together. A command is an `Effect`; running it asks the
7 * `ChildProcessSpawner` service to start the process and returns a
8 * `ChildProcessHandle`.
9 *
10 * @stability unstable
11 * @since 4.0.0
12 */
13import * as Context from "../Context.ts"
14import type * as Duration from "../Duration.ts"
15import type * as Effect from "../Effect.ts"
16import * as Effectable from "../Effectable.ts"
17import { dual } from "../Function.ts"
18import type * as PlatformError from "../PlatformError.ts"
19import * as Predicate from "../Predicate.ts"
20import type * as Scope from "../Scope.ts"
21import type * as Sink from "../Sink.ts"
22import type * as Stream from "../Stream.ts"
23import { type ChildProcessHandle, ChildProcessSpawner } from "./ChildProcessSpawner.ts"
24
25const TypeId = "~effect/process/ChildProcess"
26
27/**
28 * A command that can be built using `make`, combined using `pipeTo`, and executed using `exec` or `spawn`.
29 *
30 * @stability unstable
31 * @category models
32 * @since 4.0.0
33 */
34export type Command =
35 | StandardCommand
36 | PipedCommand
37
38/**
39 * A standard command with pre-parsed command and arguments.
40 *
41 * @stability unstable
42 * @category models
43 * @since 4.0.0
44 */
45export interface StandardCommand extends
46 Effect.Effect<
47 ChildProcessHandle,
48 PlatformError.PlatformError,
49 ChildProcessSpawner | Scope.Scope
50 >
51{
52 readonly _tag: "StandardCommand"
53 readonly command: string
54 readonly args: ReadonlyArray<string>
55 readonly options: CommandOptions
56}
57
58/**
59 * A pipeline of commands where the output of one is piped to the input of the
60 * next.
61 *
62 * @stability unstable
63 * @category models
64 * @since 4.0.0
65 */
66export interface PipedCommand extends
67 Effect.Effect<
68 ChildProcessHandle,
69 PlatformError.PlatformError,
70 ChildProcessSpawner | Scope.Scope
71 >
72{
73 readonly _tag: "PipedCommand"
74 readonly left: Command
75 readonly right: Command
76 readonly options: PipeOptions
77}
78
79/**
80 * Specifies which stream to pipe from the source subprocess.
81 *
82 * **Details**
83 *
84 * - `"stdout"`: Pipe stdout from the source (default)
85 * - `"stderr"`: Pipe stderr from the source
86 * - `"all"`: Pipe both stdout and stderr interleaved
87 * - `` `fd${number}` ``: Pipe from a custom file descriptor (e.g., `"fd3"`)
88 *
89 * @stability unstable
90 * @category models
91 * @since 4.0.0
92 */
93export type PipeFromOption = "stdout" | "stderr" | "all" | `fd${number}`
94
95/**
96 * Specifies which input to pipe to on the destination subprocess.
97 *
98 * **Details**
99 *
100 * - `"stdin"`: Pipe to stdin of the destination (default)
101 * - `` `fd${number}` ``: Pipe to a custom file descriptor (e.g., `"fd3"`)
102 *
103 * @stability unstable
104 * @category models
105 * @since 4.0.0
106 */
107export type PipeToOption = "stdin" | `fd${number}`
108
109/**
110 * Options for controlling how commands are piped together.
111 *
112 * **Example** (Piping stderr between commands)
113 *
114 * ```ts import.meta.vitest
115 * import { ChildProcess } from "effect/process"
116 *
117 * // Pipe stderr instead of stdout
118 * const pipeline = ChildProcess.make`my-program`.pipe(
119 * ChildProcess.pipeTo(ChildProcess.make`grep error`, { from: "stderr" })
120 * )
121 * const result = [pipeline._tag, pipeline.options.from] // => ["PipedCommand", "stderr"]
122 * ```
123 *
124 * @stability unstable
125 * @category options
126 * @since 4.0.0
127 */
128export interface PipeOptions {
129 /**
130 * Which stream to pipe from the source subprocess.
131 *
132 * **Details**
133 *
134 * - `"stdout"` (default): Pipe stdout from the source
135 * - `"stderr"`: Pipe stderr from the source
136 * - `"all"`: Pipe both stdout and stderr interleaved
137 * - `"fd3"`, `"fd4"`, etc.: Pipe from a custom file descriptor
138 */
139 readonly from?: PipeFromOption | undefined
140
141 /**
142 * Which input to pipe to on the destination subprocess.
143 *
144 * **Details**
145 *
146 * - `"stdin"` (default): Pipe to stdin of the destination
147 * - `"fd3"`, `"fd4"`, etc.: Pipe to a custom file descriptor
148 */
149 readonly to?: PipeToOption | undefined
150}
151
152/**
153 * Input type for child process stdin.
154 *
155 * @stability unstable
156 * @category models
157 * @since 4.0.0
158 */
159export type CommandInput =
160 | "pipe"
161 | "inherit"
162 | "ignore"
163 | "overlapped"
164 | Stream.Stream<Uint8Array, PlatformError.PlatformError>
165
166/**
167 * Output type for child process stdout/stderr.
168 *
169 * @stability unstable
170 * @category models
171 * @since 4.0.0
172 */
173export type CommandOutput =
174 | "pipe"
175 | "inherit"
176 | "ignore"
177 | "overlapped"
178 | Sink.Sink<Uint8Array, Uint8Array, never, PlatformError.PlatformError>
179
180/**
181 * A signal that can be sent to a child process.
182 *
183 * @stability unstable
184 * @category models
185 * @since 4.0.0
186 */
187export type Signal =
188 | "SIGABRT"
189 | "SIGALRM"
190 | "SIGBUS"
191 | "SIGCHLD"
192 | "SIGCONT"
193 | "SIGFPE"
194 | "SIGHUP"
195 | "SIGILL"
196 | "SIGINT"
197 | "SIGIO"
198 | "SIGIOT"
199 | "SIGKILL"
200 | "SIGPIPE"
201 | "SIGPOLL"
202 | "SIGPROF"
203 | "SIGPWR"
204 | "SIGQUIT"
205 | "SIGSEGV"
206 | "SIGSTKFLT"
207 | "SIGSTOP"
208 | "SIGSYS"
209 | "SIGTERM"
210 | "SIGTRAP"
211 | "SIGTSTP"
212 | "SIGTTIN"
213 | "SIGTTOU"
214 | "SIGUNUSED"
215 | "SIGURG"
216 | "SIGUSR1"
217 | "SIGUSR2"
218 | "SIGVTALRM"
219 | "SIGWINCH"
220 | "SIGXCPU"
221 | "SIGXFSZ"
222 | "SIGBREAK"
223 | "SIGLOST"
224 | "SIGINFO"
225
226/**
227 * The encoding format to use for binary data.
228 *
229 * @stability unstable
230 * @category models
231 * @since 4.0.0
232 */
233export type Encoding =
234 | "ascii"
235 | "utf8"
236 | "utf-8"
237 | "utf16le"
238 | "utf-16le"
239 | "ucs2"
240 | "ucs-2"
241 | "base64"
242 | "base64url"
243 | "latin1"
244 | "binary"
245 | "hex"
246
247/**
248 * Options that can be used to control how a child process is terminated.
249 *
250 * @stability unstable
251 * @category options
252 * @since 4.0.0
253 */
254export interface KillOptions {
255 /**
256 * The default signal used to terminate the child process. Defaults to `"SIGTERM"`.
257 */
258 readonly killSignal?: Signal | undefined
259 /**
260 * The duration of time to wait after the child process has been terminated
261 * before forcefully killing the child process by sending it the `"SIGKILL"`
262 * signal. Defaults to `undefined`, so `"SIGKILL"` is never sent.
263 *
264 * **Details**
265 *
266 * The spawner decides whether to terminate descendants and how long to wait.
267 * See its platform module documentation, such as `NodeChildProcessSpawner`.
268 */
269 readonly forceKillAfter?: Duration.Input | undefined
270}
271
272/**
273 * Configuration for the child process standard input stream.
274 *
275 * @stability unstable
276 * @category models
277 * @since 4.0.0
278 */
279export interface StdinConfig {
280 /**
281 * The configuration for the standard input stream of the child process.
282 *
283 * **Details**
284 *
285 * Can be a string indicating how the operating system should configure the
286 * pipe established between the child process `stdin` and the parent process.
287 *
288 * Can also be a `Stream`, which will pipe all elements produced into the
289 * `stdin` of the child process.
290 *
291 * Defaults to "pipe".
292 */
293 readonly stream: CommandInput
294 /**
295 * Whether or not the child process `stdin` should be closed after the input
296 * stream is finished. Defaults to `true`.
297 */
298 readonly endOnDone?: boolean | undefined
299 /**
300 * The buffer encoding to use to decode string chunks. Defaults to `utf-8`.
301 */
302 readonly encoding?: Encoding | undefined
303}
304
305/**
306 * Configuration for the child process standard output stream.
307 *
308 * @stability unstable
309 * @category models
310 * @since 4.0.0
311 */
312export interface StdoutConfig {
313 /**
314 * The configuration for the standard output stream of the child process.
315 *
316 * **Details**
317 *
318 * Can be a string indicating how the operating system should configure the
319 * pipe established between the child process `stdout` and the parent process.
320 *
321 * A `Sink` can also be passed, which will receive all elements produced by
322 * the `stdout` of the child process.
323 *
324 * Defaults to "pipe".
325 */
326 readonly stream?: CommandOutput | undefined
327}
328
329/**
330 * Configuration for the child process standard error stream.
331 *
332 * @stability unstable
333 * @category models
334 * @since 4.0.0
335 */
336export interface StderrConfig {
337 /**
338 * The configuration for the standard error stream of the child process.
339 *
340 * **Details**
341 *
342 * Can be a string indicating how the operating system should configure the
343 * pipe established between the child process `stderr` and the parent process.
344 *
345 * A `Sink` can also be passed, which will receive all elements produced by
346 * the `stderr` of the child process.
347 *
348 * Defaults to "pipe".
349 */
350 readonly stream?: CommandOutput | undefined
351}
352
353/**
354 * Configuration for additional file descriptors to expose to the child process.
355 *
356 * @stability unstable
357 * @category models
358 * @since 4.0.0
359 */
360export type AdditionalFdConfig =
361 | {
362 /**
363 * The direction of data flow for this file descriptor.
364 * - "input": Data flows from parent to child (writable by parent)
365 * - "output": Data flows from child to parent (readable by parent)
366 */
367 readonly type: "input"
368 /**
369 * For input file descriptors, an optional stream to pipe into the file
370 * descriptor..
371 */
372 readonly stream?: Stream.Stream<Uint8Array, PlatformError.PlatformError> | undefined
373 }
374 | {
375 /**
376 * The direction of data flow for this file descriptor.
377 * - "input": Data flows from parent to child (writable by parent)
378 * - "output": Data flows from child to parent (readable by parent)
379 */
380 readonly type: "output"
381 /**
382 * For output file descriptors, an optional sink which receives data from
383 * the file descriptor.
384 */
385 readonly sink?: Sink.Sink<Uint8Array, Uint8Array, never, PlatformError.PlatformError> | undefined
386 }
387
388/**
389 * Options for command execution.
390 *
391 * @stability unstable
392 * @category options
393 * @since 4.0.0
394 */
395export interface CommandOptions extends KillOptions {
396 /**
397 * The current working directory of the child process.
398 */
399 readonly cwd?: string | undefined
400 /**
401 * The environment of the child process.
402 *
403 * **Details**
404 *
405 * If `extendEnv` is set to `true`, the value of `env` will be merged with
406 * the value of `globalThis.process.env`, prioritizing the values in `env`
407 * when conflicts exist.
408 *
409 * **Gotchas**
410 *
411 * Without `extendEnv: true`, providing `env` replaces the inherited child
412 * environment. The child will not receive `PATH` unless `env` includes it.
413 */
414 readonly env?: Record<string, string | undefined> | undefined
415 /**
416 * If set to `true`, the child process uses both the values in `env` as well
417 * as the values in `globalThis.process.env`, prioritizing the values in `env`
418 * when conflicts exist.
419 *
420 * **Details**
421 *
422 * If set to `false` and `env` is provided, only the value of `env` is used.
423 *
424 * @default false
425 */
426 readonly extendEnv?: boolean | undefined
427 /**
428 * If set to `true`, runs the command inside of a shell, defaulting to `/bin/sh`
429 * on UNIX systems and `cmd.exe` on Windows.
430 *
431 * **Details**
432 *
433 * Can also be set to a string representing the absolute path to a shell to
434 * use on the system.
435 *
436 * **Gotchas**
437 *
438 * It is generally disadvised to use this option.
439 */
440 readonly shell?: boolean | string | undefined
441 /**
442 * If set to `true`, the child process will run independently of the parent
443 * process.
444 *
445 * **Details**
446 *
447 * The specific behavior of this option depends upon the platform. For
448 * example, the NodeJS documentation outlines the differences between Windows
449 * and non-Windows platforms.
450 *
451 * See https://nodejs.org/api/child_process.html#child_process_options_detached.
452 *
453 * Defaults to `true` on non-Windows platforms and `false` on Windows platforms.
454 */
455 readonly detached?: boolean | undefined
456 /**
457 * If set to `true`, prevents the child process's console or GUI window from
458 * becoming visible on Windows.
459 *
460 * **Details**
461 *
462 * Defaults to `true` unless `detached` is set to `true`. This option has no
463 * effect on non-Windows platforms.
464 */
465 readonly windowsHide?: boolean | undefined
466 /**
467 * Configuration options for the standard input stream for the child process.
468 */
469 readonly stdin?: CommandInput | StdinConfig | undefined
470 /**
471 * Configuration options for the standard output stream for the child process.
472 */
473 readonly stdout?: CommandOutput | StdoutConfig | undefined
474 /**
475 * Configuration options for the standard error stream for the child process.
476 */
477 readonly stderr?: CommandOutput | StderrConfig | undefined
478 /**
479 * Additional file descriptors to expose to the child process beyond `stdin` /
480 * `stdout` / `stderr`.
481 *
482 * **Details**
483 *
484 * Keys must be in the format `"fd3"`, `"fd4"`, etc. with a file descriptor
485 * index >= 3.
486 *
487 * The file descriptor index is determined by the numeric suffix (i.e. `fd3`
488 * has a file descriptor index of 3).
489 *
490 * **Example** (Configuring additional file descriptors)
491 *
492 * ```ts import.meta.vitest
493 * import { ChildProcess } from "effect/process"
494 *
495 * // Output fd3 - read data from child
496 * const cmd1 = ChildProcess.make("my-program", [], {
497 * additionalFds: {
498 * fd3: { type: "output" }
499 * }
500 * })
501 *
502 * // Input fd3 - write data to child
503 * const cmd2 = ChildProcess.make("my-program", [], {
504 * additionalFds: {
505 * fd3: { type: "input" }
506 * }
507 * })
508 * const result = [cmd1.options.additionalFds?.fd3?.type, cmd2.options.additionalFds?.fd3?.type]
509 * result // => ["output", "input"]
510 * ```
511 */
512 readonly additionalFds?: Record<`fd${number}`, AdditionalFdConfig> | undefined
513}
514
515/**
516 * Valid template expression item types.
517 *
518 * @stability unstable
519 * @category models
520 * @since 4.0.0
521 */
522export type TemplateExpressionItem = string | number | boolean
523
524/**
525 * Template expression type for interpolated values.
526 *
527 * @stability unstable
528 * @category models
529 * @since 4.0.0
530 */
531export type TemplateExpression = TemplateExpressionItem | ReadonlyArray<TemplateExpressionItem>
532
533// =============================================================================
534// Constructors
535// =============================================================================
536
537const Proto = {
538 ...Effectable.Prototype<Command>({
539 label: "Command",
540 evaluate(fiber) {
541 return Context.getUnsafe(fiber.context, ChildProcessSpawner).spawn(this)
542 }
543 }),
544 [TypeId]: TypeId
545}
546
547/**
548 * Checks whether a value is a `Command`.
549 *
550 * @stability unstable
551 * @category guards
552 * @since 4.0.0
553 */
554export const isCommand = (u: unknown): u is Command => Predicate.hasProperty(u, TypeId)
555
556/**
557 * Checks whether a command is a `StandardCommand`.
558 *
559 * @stability unstable
560 * @category guards
561 * @since 4.0.0
562 */
563export const isStandardCommand = (command: Command): command is StandardCommand => command._tag === "StandardCommand"
564
565/**
566 * Checks whether a command is a `PipedCommand`.
567 *
568 * @stability unstable
569 * @category guards
570 * @since 4.0.0
571 */
572export const isPipedCommand = (command: Command): command is PipedCommand => command._tag === "PipedCommand"
573
574const makeStandardCommand = (
575 command: string,
576 args: ReadonlyArray<string>,
577 options: CommandOptions
578): StandardCommand =>
579 Object.assign(Object.create(Proto), {
580 _tag: "StandardCommand",
581 command,
582 args,
583 options
584 })
585
586const makePipedCommand = (
587 left: Command,
588 right: Command,
589 options: PipeOptions = {}
590): PipedCommand =>
591 Object.assign(Object.create(Proto), {
592 _tag: "PipedCommand",
593 left,
594 right,
595 options
596 })
597
598/**
599 * Create a command from a template literal, options + template, or array form.
600 *
601 * **Details**
602 *
603 * This function supports three calling conventions:
604 * 1. Template literal: `make\`npm run build\``
605 * 2. Options + template literal: `make({ cwd: "/app" })\`npm run build\``
606 * 3. Array form: `make("npm", ["run", "build"], options?)`
607 *
608 * Template literals are not parsed until execution time, allowing parsing
609 * errors to flow through Effect's error channel.
610 *
611 * **Example** (Creating commands)
612 *
613 * ```ts import.meta.vitest
614 * import { ChildProcess } from "effect/process"
615 *
616 * // Template literal form
617 * const cmd1 = ChildProcess.make`echo "hello"`
618 *
619 * // With options
620 * const cmd2 = ChildProcess.make({ cwd: "/tmp" })`ls -la`
621 *
622 * // Array form
623 * const cmd3 = ChildProcess.make("git", ["status"])
624 *
625 * const result = [cmd1.command, cmd2.options.cwd, cmd3.args[0]] // => ["echo", "/tmp", "status"]
626 * ```
627 *
628 * @stability unstable
629 * @category constructors
630 * @since 4.0.0
631 */
632export const make: {
633 (
634 command: string,
635 options?: CommandOptions
636 ): StandardCommand
637 (
638 command: string,
639 args: ReadonlyArray<string>,
640 options?: CommandOptions
641 ): StandardCommand
642 (
643 options: CommandOptions
644 ): (
645 templates: TemplateStringsArray,
646 ...expressions: ReadonlyArray<TemplateExpression>
647 ) => StandardCommand
648 (
649 templates: TemplateStringsArray,
650 ...expressions: ReadonlyArray<TemplateExpression>
651 ): StandardCommand
652} = function make(...args: Array<unknown>): any {
653 // Template literal form: make`command`
654 if (isTemplateString(args[0])) {
655 const [templates, ...expressions] = args as [TemplateStringsArray, ...ReadonlyArray<TemplateExpression>]
656 const tokens = parseTemplates(templates, expressions)
657 return makeStandardCommand(tokens[0] ?? "", tokens.slice(1), {})
658 }
659
660 // Options form: make({ cwd: "/tmp" })`command`
661 if (typeof args[0] === "object" && !Array.isArray(args[0]) && !isTemplateString(args[0])) {
662 const options = args[0] as CommandOptions
663 return function(
664 templates: TemplateStringsArray,
665 ...expressions: ReadonlyArray<TemplateExpression>
666 ): StandardCommand {
667 const tokens = parseTemplates(templates, expressions)
668 return makeStandardCommand(tokens[0] ?? "", tokens.slice(1), options)
669 }
670 }
671
672 // Standard form without arguments: make("command", options?)
673 if (typeof args[0] === "string" && !Array.isArray(args[1])) {
674 const [command, options = {}] = args as [string, CommandOptions?]
675 return makeStandardCommand(command, [], options)
676 }
677
678 // Standard form with arguments: make("command", ["arg1", "arg2"], options?)
679 const [command, cmdArgs = [], options = {}] = args as [
680 string,
681 ReadonlyArray<string>?,
682 CommandOptions?
683 ]
684 return makeStandardCommand(command, cmdArgs, options)
685}
686
687/**
688 * Pipes the output of one command to the input of another.
689 *
690 * **Details**
691 *
692 * By default, pipes `stdout` from the source to `stdin` of the destination.
693 * Use the `options` parameter to customize which streams are connected.
694 *
695 * **Example** (Piping command output)
696 *
697 * ```ts import.meta.vitest
698 * import { ChildProcess } from "effect/process"
699 *
700 * // Pipe stdout (default)
701 * const pipeline1 = ChildProcess.make`cat file.txt`.pipe(
702 * ChildProcess.pipeTo(ChildProcess.make`grep pattern`)
703 * )
704 *
705 * // Pipe stderr instead of stdout
706 * const pipeline2 = ChildProcess.make`my-program`.pipe(
707 * ChildProcess.pipeTo(ChildProcess.make`grep error`, { from: "stderr" })
708 * )
709 *
710 * // Pipe combined stdout and stderr
711 * const pipeline3 = ChildProcess.make`my-program`.pipe(
712 * ChildProcess.pipeTo(ChildProcess.make`tee output.log`, { from: "all" })
713 * )
714 *
715 * const result = [pipeline1._tag, pipeline2.options.from, pipeline3.options.from]
716 * result // => ["PipedCommand", "stderr", "all"]
717 * ```
718 *
719 * @stability unstable
720 * @category combinators
721 * @since 4.0.0
722 */
723export const pipeTo: {
724 (that: Command, options?: PipeOptions): (self: Command) => PipedCommand
725 (self: Command, that: Command, options?: PipeOptions): PipedCommand
726} = dual(
727 (args) => isCommand(args[0]) && isCommand(args[1]),
728 (self: Command, that: Command, options?: PipeOptions) => makePipedCommand(self, that, options ?? {})
729)
730
731/**
732 * Prepends another command to a command.
733 *
734 * **Details**
735 *
736 * For pipelines, only the leftmost command is prefixed.
737 *
738 * **Example** (Prefixing commands)
739 *
740 * ```ts import.meta.vitest
741 * import { ChildProcess } from "effect/process"
742 *
743 * const command = ChildProcess.make`echo "foo"`
744 *
745 * const prefixed = command.pipe(
746 * ChildProcess.prefix`time`
747 * )
748 *
749 * // now prefixed will execute `time echo "foo"`
750 * const result = prefixed._tag === "StandardCommand" ? `${prefixed.command} ${prefixed.args[0]}` : prefixed._tag
751 * result // => "time echo"
752 * ```
753 *
754 * @stability unstable
755 * @category combinators
756 * @since 4.0.0
757 */
758export const prefix: {
759 (command: string, args?: ReadonlyArray<string>): (self: Command) => Command
760 (templates: TemplateStringsArray, ...expressions: ReadonlyArray<TemplateExpression>): (self: Command) => Command
761 (self: Command, command: string, args?: ReadonlyArray<string>): Command
762} = function prefix(...args: Array<unknown>): any {
763 if (isCommand(args[0]) && args.length > 1) {
764 const [self, ...rest] = args as [Command, ...Array<unknown>]
765 const prefixSpec = parsePrefixArgs(rest)
766 return applyPrefix(self, prefixSpec)
767 }
768 const prefixSpec = parsePrefixArgs(args)
769 return (self: Command): Command => applyPrefix(self, prefixSpec)
770}
771
772type PrefixSpec = {
773 readonly command: string
774 readonly args: ReadonlyArray<string>
775}
776
777const parsePrefixArgs = (args: ReadonlyArray<unknown>): PrefixSpec => {
778 if (isTemplateString(args[0])) {
779 const [templates, ...expressions] = args as [TemplateStringsArray, ...ReadonlyArray<TemplateExpression>]
780 const tokens = parseTemplates(templates, expressions)
781 return { command: tokens[0] ?? "", args: tokens.slice(1) }
782 }
783 const [command, cmdArgs = []] = args as [string, ReadonlyArray<string>?]
784 return { command, args: cmdArgs }
785}
786
787const applyPrefix = (self: Command, prefixSpec: PrefixSpec): Command => {
788 switch (self._tag) {
789 case "StandardCommand": {
790 return makeStandardCommand(
791 prefixSpec.command,
792 [...prefixSpec.args, self.command, ...self.args],
793 self.options
794 )
795 }
796 case "PipedCommand": {
797 return makePipedCommand(applyPrefix(self.left, prefixSpec), self.right, self.options)
798 }
799 }
800}
801
802/**
803 * Sets the current working directory for a command.
804 *
805 * **Details**
806 *
807 * For pipelines, applies to each command in the pipeline.
808 *
809 * **Example** (Setting command working directories)
810 *
811 * ```ts import.meta.vitest
812 * import { ChildProcess } from "effect/process"
813 *
814 * const cmd = ChildProcess.make`ls -la`.pipe(
815 * ChildProcess.setCwd("/tmp")
816 * )
817 * const result = cmd._tag === "StandardCommand" && cmd.options.cwd // => "/tmp"
818 * ```
819 *
820 * @stability unstable
821 * @category combinators
822 * @since 4.0.0
823 */
824export const setCwd: {
825 (cwd: string): (self: Command) => Command
826 (self: Command, cwd: string): Command
827} = dual(
828 2,
829 (self: Command, cwd: string): Command => {
830 switch (self._tag) {
831 case "StandardCommand": {
832 return makeStandardCommand(self.command, self.args, { ...self.options, cwd })
833 }
834 case "PipedCommand": {
835 return makePipedCommand(setCwd(self.left, cwd), setCwd(self.right, cwd), self.options)
836 }
837 }
838 }
839)
840
841/**
842 * Adds environment variables to a command, merging them with any existing
843 * command environment and overriding duplicate keys.
844 *
845 * **Details**
846 *
847 * For pipelines, applies to each command in the pipeline.
848 *
849 * **Example** (Setting command environment variables)
850 *
851 * ```ts import.meta.vitest
852 * import { ChildProcess } from "effect/process"
853 *
854 * const cmd = ChildProcess.make`node script.js`.pipe(
855 * ChildProcess.setEnv({ NODE_ENV: "test" })
856 * )
857 * const result = cmd._tag === "StandardCommand" && cmd.options.env?.NODE_ENV // => "test"
858 * ```
859 *
860 * @stability unstable
861 * @category combinators
862 * @since 4.0.0
863 */
864export const setEnv: {
865 (env: Record<string, string>): (self: Command) => Command
866 (self: Command, env: Record<string, string>): Command
867} = dual(
868 2,
869 (self: Command, env: Record<string, string>): Command => {
870 switch (self._tag) {
871 case "StandardCommand": {
872 const nextEnv = self.options.env === undefined
873 ? env
874 : { ...self.options.env, ...env }
875 return makeStandardCommand(self.command, self.args, { ...self.options, env: nextEnv })
876 }
877 case "PipedCommand": {
878 return makePipedCommand(setEnv(self.left, env), setEnv(self.right, env), self.options)
879 }
880 }
881 }
882)
883
884const isTemplateString = (u: unknown): u is TemplateStringsArray =>
885 Array.isArray(u) && "raw" in u && Array.isArray(u.raw)
886
887// =============================================================================
888// Utilities
889// =============================================================================
890
891/**
892 * Parses an fd name like "fd3" to its numeric index.
893 * Returns undefined if the name is invalid.
894 *
895 * @stability unstable
896 * @category converting
897 * @since 4.0.0
898 */
899export const parseFdName = (name: string): number | undefined => {
900 const match = /^fd(\d+)$/.exec(name)
901 if (match === null) return undefined
902 const fd = parseInt(match[1], 10)
903 return fd >= 3 ? fd : undefined
904}
905
906/**
907 * Create an fd name from its numeric index.
908 *
909 * @stability unstable
910 * @category converting
911 * @since 4.0.0
912 */
913export const fdName = (fd: number): string => `fd${fd}`
914
915// =============================================================================
916// Template Parsing
917// =============================================================================
918
919const parseTemplates = (
920 templates: TemplateStringsArray,
921 expressions: ReadonlyArray<TemplateExpression>
922): ReadonlyArray<string> => {
923 let tokens: ReadonlyArray<string> = []
924 for (const [index, template] of templates.entries()) {
925 tokens = parseTemplate(templates, expressions, tokens, template, index)
926 }
927 return tokens
928}
929
930const parseTemplate = (
931 templates: TemplateStringsArray,
932 expressions: ReadonlyArray<TemplateExpression>,
933 prevTokens: ReadonlyArray<string>,
934 template: string,
935 index: number
936): ReadonlyArray<string> => {
937 const rawTemplate = templates.raw[index]
938
939 if (rawTemplate === undefined) {
940 throw new Error(`Invalid backslash sequence: ${templates.raw[index]}`)
941 }
942
943 const { hasLeadingWhitespace, hasTrailingWhitespace, tokens } = splitByWhitespaces(template, rawTemplate)
944 const nextTokens = concatTokens(prevTokens, tokens, hasLeadingWhitespace)
945
946 if (index === expressions.length) {
947 return nextTokens
948 }
949
950 const expression = expressions[index]
951 const expressionTokens = Array.isArray(expression)
952 ? expression.map((expression: TemplateExpressionItem) => parseExpression(expression))
953 : [parseExpression(expression as TemplateExpressionItem)]
954
955 return concatTokens(nextTokens, expressionTokens, hasTrailingWhitespace)
956}
957
958/**
959 * Convert valid expressions defined in a template string command (i.e. using
960 * `${expression}` into strings.
961 */
962const parseExpression = (expression: TemplateExpression): string => {
963 const type = typeof expression
964 if (type === "string") {
965 return expression as string // Return strings directly
966 }
967 return String(expression) // Convert numbers to strings
968}
969
970const DELIMITERS = new Set([" ", "\t", "\r", "\n"])
971
972/**
973 * Number of characters in backslash escape sequences: \0 \xXX or \uXXXX
974 * \cX is allowed in RegExps but not in strings
975 * Octal sequences are not allowed in strict mode
976 */
977const ESCAPE_LENGTH: Record<string, number> = { x: 3, u: 5 }
978
979/**
980 * Splits a template string by whitespace while also properly handling escape
981 * sequences.
982 *
983 * As an example, let's review the following valid commands:
984 *
985 * ```ts
986 * ChildProcess.exec`echo foo\n bar`
987 * // We should run `["echo", "foo\n", "bar"]`
988 *
989 * ChildProcess.exec`echo foo
990 * bar`
991 * // We should run `["echo", "foo", "bar]`
992 * ```
993 *
994 * The problem is that when we evaluate the template string for both of the above
995 * commands, we will end up with the same string "echo foo\n bar".
996 *
997 * What we really want is to include the escaped character in the arguments for
998 * the first command, since it was written explicitly by the user.
999 *
1000 * This is why also having access to the raw template string is useful - in a
1001 * template string, there are two representations of the same string:
1002 * 1. `template` - The processed string (escape sequences are evaluated).
1003 * 2. `template.raw` - The raw string (escape sequences are literal).
1004 */
1005const splitByWhitespaces = (template: string, rawTemplate: string): {
1006 readonly tokens: ReadonlyArray<string>
1007 readonly hasLeadingWhitespace: boolean
1008 readonly hasTrailingWhitespace: boolean
1009} => {
1010 if (rawTemplate.length === 0) {
1011 return {
1012 tokens: [],
1013 hasLeadingWhitespace: false,
1014 hasTrailingWhitespace: false
1015 }
1016 }
1017
1018 const hasLeadingWhitespace = DELIMITERS.has(rawTemplate[0])
1019 const tokens: Array<string> = []
1020
1021 // Given that escape sequences will have different lengths in the template
1022 // versus the raw template, we must maintain two indices:
1023 // - One for the index into the template string
1024 // - One for the index into the raw template string
1025 // We also maintain the current cursor position for where we are in the template
1026 let templateCursor = 0
1027 for (
1028 let templateIndex = 0, rawIndex = 0;
1029 templateIndex < template.length;
1030 templateIndex += 1, rawIndex += 1
1031 ) {
1032 // Use the raw template character to check for actual whitespace
1033 const rawCharacter = rawTemplate[rawIndex]
1034
1035 if (DELIMITERS.has(rawCharacter)) {
1036 // Whitespace found, extract token from template if necessary
1037 if (templateCursor !== templateIndex) {
1038 tokens.push(template.slice(templateCursor, templateIndex))
1039 }
1040 // Advance the template start index to the current position
1041 templateCursor = templateIndex + 1
1042 } else if (rawCharacter === "\\") {
1043 // Escape sequence detected, check the next raw character
1044 const nextRawCharacter = rawTemplate[rawIndex + 1]
1045
1046 if (nextRawCharacter === "\n") {
1047 // Handle `\` character followed by a newline (i.e. a line continuation) by:
1048 // - Reversing the template index (backslash-newline is erased in template)
1049 // - Advancing the raw template index past the line continuation
1050 templateIndex -= 1
1051 rawIndex += 1
1052 } else if (nextRawCharacter === "u" && rawTemplate[rawIndex + 2] === "{") {
1053 // Handle variable-length unicode escape sequences (i.e. `\u{1F600}`) by:
1054 // - Advancing the template index an extra code unit for astral code points
1055 // - Advancing the raw template index past the unicode escape sequence
1056 const end = rawTemplate.indexOf("}", rawIndex + 3)
1057 if (parseInt(rawTemplate.slice(rawIndex + 3, end), 16) > 0xffff) {
1058 templateIndex += 1
1059 }
1060 rawIndex = end
1061 } else {
1062 // Advance raw template index past fixed-length escape sequences:
1063 // - \n → 2 chars (backslash + n)
1064 // - \t → 2 chars (backslash + t)
1065 // - \xHH → 4 chars (backslash + x + H + H)
1066 // - \uHHHH → 6 chars (backslash + u + H + H + H + H)
1067 rawIndex += ESCAPE_LENGTH[nextRawCharacter] ?? 1
1068 }
1069 }
1070 }
1071
1072 // Trailing whitespace only exists if the template cursor is equivalent to the
1073 // length of the template
1074 const hasTrailingWhitespace = templateCursor === template.length
1075
1076 // If we did not end with trailing whitespace, ensure the final token is added
1077 if (!hasTrailingWhitespace) {
1078 tokens.push(template.slice(templateCursor))
1079 }
1080
1081 return {
1082 tokens,
1083 hasLeadingWhitespace,
1084 hasTrailingWhitespace
1085 }
1086}
1087
1088/**
1089 * Concatenates two separate sets of string tokens together.
1090 *
1091 * If either set is empty or `isSeparated=false`, the last element of `prevTokens`
1092 * and the first element of `nextTokens` will be joined into a single token.
1093 */
1094const concatTokens = (
1095 prevTokens: ReadonlyArray<string>,
1096 nextTokens: ReadonlyArray<string>,
1097 isSeparated: boolean
1098): ReadonlyArray<string> =>
1099 isSeparated || prevTokens.length === 0 || nextTokens.length === 0
1100 // Keep the previous and next tokens separate from one another
1101 ? [...prevTokens, ...nextTokens]
1102 // Join the last token from the previous set and the first token from the next set
1103 : [...prevTokens.slice(0, -1), `${prevTokens.at(-1)}${nextTokens.at(0)}`, ...nextTokens.slice(1)]