11use crate :: fakers:: { Command , CommandRunner } ;
2+ use std:: collections:: HashMap ;
23use std:: io;
34
4- /// Resolve an environment variable on the host via a `CommandRunner`.
5- ///
6- /// Returns `Ok(Some(value))` if present, `Ok(None)` if unset/empty, or `Err(e)` on I/O/command errors.
7- pub async fn resolve_host_env_via_runner (
8- runner : & CommandRunner ,
9- key : & str ,
10- ) -> io:: Result < Option < String > > {
11- // Try `printenv KEY` first — simpler when available
12- let cmd = Command :: new_with_args ( "printenv" , [ key] ) ;
13- match runner. output_string ( cmd. clone ( ) ) . await {
14- Ok ( out) => {
15- let trimmed = out. trim ( ) . to_string ( ) ;
16- if !trimmed. is_empty ( ) {
17- return Ok ( Some ( trimmed) ) ;
18- }
19- // fallthrough to sh -c printf if empty
20- }
21- Err ( _) => {
22- // fallback below
23- }
24- }
25-
26- // Fallback: sh -c 'printf "%s" "$KEY"'
27- let mut cmd = Command :: new ( "sh" ) ;
28- cmd. arg ( "-c" ) ;
29- // Only interpolate the variable name to avoid injection
30- cmd. arg ( format ! ( "printf '%s' \" ${}\" " , key) ) ;
31-
32- let out = runner. output_string ( cmd) . await ?;
33- let trimmed = out. trim ( ) . to_string ( ) ;
34- if trimmed. is_empty ( ) {
35- Ok ( None )
36- } else {
37- Ok ( Some ( trimmed) )
38- }
39- }
40-
41- /// Resolve all host environment variables via a `CommandRunner` as `KEY=VALUE` entries.
42- ///
43- /// This is useful when spawning processes that need the host `PATH` and other vars, e.g. VTE.
44- pub async fn resolve_host_env_list_via_runner ( runner : & CommandRunner ) -> io:: Result < Vec < String > > {
5+ /// Resolve all host environment variables via a `CommandRunner`.
6+ pub async fn resolve_host_env ( runner : & CommandRunner ) -> io:: Result < HashMap < String , String > > {
457 // Prefer NUL-separated output to preserve values containing newlines.
468 let mut cmd = Command :: new ( "env" ) ;
479 cmd. arg ( "-0" ) ;
@@ -56,9 +18,9 @@ pub async fn resolve_host_env_list_via_runner(runner: &CommandRunner) -> io::Res
5618 return None ;
5719 }
5820 let s = String :: from_utf8_lossy ( entry) . to_string ( ) ;
59- if s . contains ( '=' ) { Some ( s ) } else { None }
21+ s . split_once ( '=' ) . map ( | ( k , v ) | ( k . to_string ( ) , v . to_string ( ) ) )
6022 } )
61- . collect :: < Vec < _ > > ( ) ;
23+ . collect :: < HashMap < _ , _ > > ( ) ;
6224 if !vars. is_empty ( ) {
6325 return Ok ( vars) ;
6426 }
@@ -73,43 +35,55 @@ pub async fn resolve_host_env_list_via_runner(runner: &CommandRunner) -> io::Res
7335
7436 Ok ( String :: from_utf8_lossy ( & output. stdout )
7537 . lines ( )
76- . filter ( |line| line. contains ( '=' ) )
77- . map ( ToString :: to_string)
38+ . filter_map ( |line| {
39+ line. split_once ( '=' )
40+ . map ( |( k, v) | ( k. to_string ( ) , v. to_string ( ) ) )
41+ } )
7842 . collect ( ) )
7943}
8044
45+ pub fn host_env_to_list ( env : & HashMap < String , String > ) -> Vec < String > {
46+ let mut list = env
47+ . iter ( )
48+ . map ( |( key, value) | format ! ( "{key}={value}" ) )
49+ . collect :: < Vec < _ > > ( ) ;
50+ list. sort_unstable ( ) ;
51+ list
52+ }
53+
8154#[ cfg( test) ]
8255mod tests {
8356 use super :: * ;
8457 use crate :: fakers:: NullCommandRunnerBuilder ;
8558 use smol:: block_on;
8659
8760 #[ test]
88- fn test_resolve_host_env_with_null_runner ( ) {
89- let runner = NullCommandRunnerBuilder :: new ( )
90- . cmd ( & [ "printenv" , "HOME" ] , "/fake/home" )
91- . build ( ) ;
92-
93- let res = block_on ( resolve_host_env_via_runner ( & runner, "HOME" ) ) . unwrap ( ) ;
94- assert_eq ! ( res, Some ( "/fake/home" . to_string( ) ) ) ;
95- }
96-
97- #[ test]
98- fn test_resolve_host_env_unset_with_null_runner ( ) {
61+ fn test_resolve_host_env_empty_with_null_runner ( ) {
9962 let runner = NullCommandRunnerBuilder :: new ( ) . build ( ) ;
100- let res = block_on ( resolve_host_env_via_runner ( & runner, "HOME" ) ) . unwrap ( ) ;
101- assert_eq ! ( res, None ) ;
63+ let res = block_on ( resolve_host_env ( & runner) ) . unwrap ( ) ;
64+ assert ! ( res. is_empty ( ) ) ;
10265 }
10366
10467 #[ test]
105- fn test_resolve_host_env_list_with_null_runner_nul_separated ( ) {
68+ fn test_resolve_host_env_with_null_runner_nul_separated ( ) {
10669 let mut builder = NullCommandRunnerBuilder :: new ( ) ;
10770 let mut cmd = Command :: new ( "env" ) ;
10871 cmd. arg ( "-0" ) ;
10972 builder. cmd_full ( cmd, || Ok ( "HOME=/fake/home\0 PATH=/nix/store/bin\0 " . to_string ( ) ) ) ;
11073 let runner = builder. build ( ) ;
11174
112- let res = block_on ( resolve_host_env_list_via_runner ( & runner) ) . unwrap ( ) ;
113- assert_eq ! ( res, vec![ "HOME=/fake/home" , "PATH=/nix/store/bin" ] ) ;
75+ let res = block_on ( resolve_host_env ( & runner) ) . unwrap ( ) ;
76+ assert_eq ! ( res. get( "HOME" ) , Some ( & "/fake/home" . to_string( ) ) ) ;
77+ assert_eq ! ( res. get( "PATH" ) , Some ( & "/nix/store/bin" . to_string( ) ) ) ;
78+ }
79+
80+ #[ test]
81+ fn test_host_env_to_list ( ) {
82+ let env = HashMap :: from ( [
83+ ( "PATH" . to_string ( ) , "/nix/store/bin" . to_string ( ) ) ,
84+ ( "HOME" . to_string ( ) , "/fake/home" . to_string ( ) ) ,
85+ ] ) ;
86+ let list = host_env_to_list ( & env) ;
87+ assert_eq ! ( list, vec![ "HOME=/fake/home" , "PATH=/nix/store/bin" ] ) ;
11488 }
11589}
0 commit comments