Skip to content

Commit 721e98f

Browse files
feat(core): add env, cwd to the command API, closes #1634 (#1635)
Co-authored-by: Amr Bashir <48618675+amrbashir@users.noreply.github.com>
1 parent f867e13 commit 721e98f

File tree

8 files changed

+127
-31
lines changed

8 files changed

+127
-31
lines changed

Diff for: .changes/command-options.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"api": patch
3+
"tauri": patch
4+
---
5+
6+
Adds `options` argument to the shell command API (`env` and `cwd` configuration).

Diff for: core/tauri/scripts/bundle.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: core/tauri/src/api/command.rs

+34
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
// SPDX-License-Identifier: MIT
44

55
use std::{
6+
collections::HashMap,
67
io::{BufRead, BufReader, Write},
8+
path::PathBuf,
79
process::{Command as StdCommand, Stdio},
810
sync::Arc,
911
};
@@ -52,6 +54,13 @@ macro_rules! get_std_command {
5254
command.stdout(Stdio::piped());
5355
command.stdin(Stdio::piped());
5456
command.stderr(Stdio::piped());
57+
if $self.env_clear {
58+
command.env_clear();
59+
}
60+
command.envs($self.env);
61+
if let Some(current_dir) = $self.current_dir {
62+
command.current_dir(current_dir);
63+
}
5564
#[cfg(windows)]
5665
command.creation_flags(CREATE_NO_WINDOW);
5766
command
@@ -62,6 +71,9 @@ macro_rules! get_std_command {
6271
pub struct Command {
6372
program: String,
6473
args: Vec<String>,
74+
env_clear: bool,
75+
env: HashMap<String, String>,
76+
current_dir: Option<PathBuf>,
6577
}
6678

6779
/// Child spawned.
@@ -76,6 +88,7 @@ impl CommandChild {
7688
self.stdin_writer.write_all(buf)?;
7789
Ok(())
7890
}
91+
7992
/// Send a kill signal to the child.
8093
pub fn kill(self) -> crate::api::Result<()> {
8194
self.inner.kill()?;
@@ -118,6 +131,9 @@ impl Command {
118131
Self {
119132
program: program.into(),
120133
args: Default::default(),
134+
env_clear: false,
135+
env: Default::default(),
136+
current_dir: None,
121137
}
122138
}
123139

@@ -143,6 +159,24 @@ impl Command {
143159
self
144160
}
145161

162+
/// Clears the entire environment map for the child process.
163+
pub fn env_clear(mut self) -> Self {
164+
self.env_clear = true;
165+
self
166+
}
167+
168+
/// Adds or updates multiple environment variable mappings.
169+
pub fn envs(mut self, env: HashMap<String, String>) -> Self {
170+
self.env = env;
171+
self
172+
}
173+
174+
/// Sets the working directory for the child process.
175+
pub fn current_dir(mut self, current_dir: PathBuf) -> Self {
176+
self.current_dir.replace(current_dir);
177+
self
178+
}
179+
146180
/// Spawns the command.
147181
pub fn spawn(self) -> crate::api::Result<(Receiver<CommandEvent>, CommandChild)> {
148182
let mut command = get_std_command!(self);

Diff for: core/tauri/src/endpoints/shell.rs

+30-7
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ use crate::{endpoints::InvokeResponse, Params, Window};
66
use serde::Deserialize;
77

88
#[cfg(shell_execute)]
9-
use std::{
10-
collections::HashMap,
11-
sync::{Arc, Mutex},
12-
};
9+
use std::sync::{Arc, Mutex};
10+
use std::{collections::HashMap, path::PathBuf};
1311

1412
type ChildId = u32;
1513
#[cfg(shell_execute)]
@@ -29,6 +27,23 @@ pub enum Buffer {
2927
Raw(Vec<u8>),
3028
}
3129

30+
fn default_env() -> Option<HashMap<String, String>> {
31+
Some(Default::default())
32+
}
33+
34+
#[allow(dead_code)]
35+
#[derive(Default, Deserialize)]
36+
#[serde(rename_all = "camelCase")]
37+
pub struct CommandOptions {
38+
#[serde(default)]
39+
sidecar: bool,
40+
cwd: Option<PathBuf>,
41+
// by default we don't add any env variables to the spawned process
42+
// but the env is an `Option` so when it's `None` we clear the env.
43+
#[serde(default = "default_env")]
44+
env: Option<HashMap<String, String>>,
45+
}
46+
3247
/// The API descriptor.
3348
#[derive(Deserialize)]
3449
#[serde(tag = "cmd", rename_all = "camelCase")]
@@ -40,7 +55,7 @@ pub enum Cmd {
4055
args: Vec<String>,
4156
on_event_fn: String,
4257
#[serde(default)]
43-
sidecar: bool,
58+
options: CommandOptions,
4459
},
4560
StdinWrite {
4661
pid: ChildId,
@@ -63,16 +78,24 @@ impl Cmd {
6378
program,
6479
args,
6580
on_event_fn,
66-
sidecar,
81+
options,
6782
} => {
6883
#[cfg(shell_execute)]
6984
{
70-
let mut command = if sidecar {
85+
let mut command = if options.sidecar {
7186
crate::api::command::Command::new_sidecar(program)?
7287
} else {
7388
crate::api::command::Command::new(program)
7489
};
7590
command = command.args(args);
91+
if let Some(cwd) = options.cwd {
92+
command = command.current_dir(cwd);
93+
}
94+
if let Some(env) = options.env {
95+
command = command.envs(env);
96+
} else {
97+
command = command.env_clear();
98+
}
7699
let (mut rx, child) = command.spawn()?;
77100

78101
let pid = child.pid();

Diff for: examples/api/public/build/bundle.js

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: examples/api/public/build/bundle.js.map

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: examples/api/src/components/Shell.svelte

+17-1
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,24 @@
77
export let onMessage;
88
99
let script = 'echo "hello world"'
10+
let cwd = null
11+
let env = 'SOMETHING=value ANOTHER=2'
1012
let stdin = ''
1113
let child
1214
15+
function _getEnv() {
16+
return env.split(' ').reduce((env, clause) => {
17+
let [key, value] = clause.split('=')
18+
return {
19+
...env,
20+
[key]: value
21+
}
22+
}, {})
23+
}
24+
1325
function spawn() {
1426
child = null
15-
const command = new Command(cmd, [...args, script])
27+
const command = new Command(cmd, [...args, script], { cwd: cwd || null, env: _getEnv() })
1628
1729
command.on('close', data => {
1830
onMessage(`command finished with code ${data.code} and signal ${data.signal}`)
@@ -49,4 +61,8 @@
4961
<button class="button" on:click={writeToStdin}>Write</button>
5062
{/if}
5163
</div>
64+
<div>
65+
<input bind:value={cwd} placeholder="Working directory">
66+
<input bind:value={env} placeholder="Environment variables" style="width: 300px">
67+
</div>
5268
</div>

Diff for: tooling/api/src/shell.ts

+36-19
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,24 @@
55
import { invokeTauriCommand } from './helpers/tauri'
66
import { transformCallback } from './tauri'
77

8+
interface SpawnOptions {
9+
/** Current working directory. */
10+
cwd?: string
11+
/** Environment variables. set to `null` to clear the process env. */
12+
env?: { [name: string]: string }
13+
}
14+
15+
interface InternalSpawnOptions extends SpawnOptions {
16+
sidecar?: boolean
17+
}
18+
19+
interface ChildProcess {
20+
code: number | null
21+
signal: number | null
22+
stdout: string
23+
stderr: string
24+
}
25+
826
/**
927
* Spawns a process.
1028
*
@@ -15,10 +33,10 @@ import { transformCallback } from './tauri'
1533
* @returns A promise resolving to the process id.
1634
*/
1735
async function execute(
18-
program: string,
19-
sidecar: boolean,
2036
onEvent: (event: CommandEvent) => void,
21-
args?: string | string[]
37+
program: string,
38+
args?: string | string[],
39+
options?: InternalSpawnOptions
2240
): Promise<number> {
2341
if (typeof args === 'object') {
2442
Object.freeze(args)
@@ -29,20 +47,13 @@ async function execute(
2947
message: {
3048
cmd: 'execute',
3149
program,
32-
sidecar,
33-
onEventFn: transformCallback(onEvent),
34-
args: typeof args === 'string' ? [args] : args
50+
args: typeof args === 'string' ? [args] : args,
51+
options,
52+
onEventFn: transformCallback(onEvent)
3553
}
3654
})
3755
}
3856

39-
interface ChildProcess {
40-
code: number | null
41-
signal: number | null
42-
stdout: string
43-
stderr: string
44-
}
45-
4657
class EventEmitter<E> {
4758
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
4859
eventListeners: { [key: string]: Array<(arg: any) => void> } = Object.create(
@@ -107,15 +118,20 @@ class Child {
107118
class Command extends EventEmitter<'close' | 'error'> {
108119
program: string
109120
args: string[]
110-
sidecar = false
121+
options: InternalSpawnOptions
111122
stdout = new EventEmitter<'data'>()
112123
stderr = new EventEmitter<'data'>()
113124
pid: number | null = null
114125

115-
constructor(program: string, args: string | string[] = []) {
126+
constructor(
127+
program: string,
128+
args: string | string[] = [],
129+
options?: SpawnOptions
130+
) {
116131
super()
117132
this.program = program
118133
this.args = typeof args === 'string' ? [args] : args
134+
this.options = options ?? {}
119135
}
120136

121137
/**
@@ -126,14 +142,12 @@ class Command extends EventEmitter<'close' | 'error'> {
126142
*/
127143
static sidecar(program: string, args: string | string[] = []): Command {
128144
const instance = new Command(program, args)
129-
instance.sidecar = true
145+
instance.options.sidecar = true
130146
return instance
131147
}
132148

133149
async spawn(): Promise<Child> {
134150
return execute(
135-
this.program,
136-
this.sidecar,
137151
(event) => {
138152
switch (event.event) {
139153
case 'Error':
@@ -150,7 +164,9 @@ class Command extends EventEmitter<'close' | 'error'> {
150164
break
151165
}
152166
},
153-
this.args
167+
this.program,
168+
this.args,
169+
this.options
154170
).then((pid) => new Child(pid))
155171
}
156172

@@ -214,3 +230,4 @@ async function open(path: string, openWith?: string): Promise<void> {
214230
}
215231

216232
export { Command, Child, open }
233+
export type { ChildProcess, SpawnOptions }

0 commit comments

Comments
 (0)