shell.rs

  1use schemars::JsonSchema;
  2use serde::{Deserialize, Serialize};
  3use std::{borrow::Cow, fmt, path::Path, sync::LazyLock};
  4
  5/// Shell configuration to open the terminal with.
  6#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
  7#[serde(rename_all = "snake_case")]
  8pub enum Shell {
  9    /// Use the system's default terminal configuration in /etc/passwd
 10    #[default]
 11    System,
 12    /// Use a specific program with no arguments.
 13    Program(String),
 14    /// Use a specific program with arguments.
 15    WithArguments {
 16        /// The program to run.
 17        program: String,
 18        /// The arguments to pass to the program.
 19        args: Vec<String>,
 20        /// An optional string to override the title of the terminal tab
 21        title_override: Option<String>,
 22    },
 23}
 24
 25impl Shell {
 26    pub fn program(&self) -> String {
 27        match self {
 28            Shell::Program(program) => program.clone(),
 29            Shell::WithArguments { program, .. } => program.clone(),
 30            Shell::System => get_system_shell(),
 31        }
 32    }
 33
 34    pub fn program_and_args(&self) -> (String, &[String]) {
 35        match self {
 36            Shell::Program(program) => (program.clone(), &[]),
 37            Shell::WithArguments { program, args, .. } => (program.clone(), args),
 38            Shell::System => (get_system_shell(), &[]),
 39        }
 40    }
 41
 42    pub fn shell_kind(&self, is_windows: bool) -> ShellKind {
 43        match self {
 44            Shell::Program(program) => ShellKind::new(program, is_windows),
 45            Shell::WithArguments { program, .. } => ShellKind::new(program, is_windows),
 46            Shell::System => ShellKind::system(),
 47        }
 48    }
 49}
 50
 51#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
 52pub enum ShellKind {
 53    #[default]
 54    Posix,
 55    Csh,
 56    Tcsh,
 57    Rc,
 58    Fish,
 59    PowerShell,
 60    Nushell,
 61    Cmd,
 62    Xonsh,
 63    Elvish,
 64}
 65
 66pub fn get_system_shell() -> String {
 67    if cfg!(windows) {
 68        get_windows_system_shell()
 69    } else {
 70        std::env::var("SHELL").unwrap_or("/bin/sh".to_string())
 71    }
 72}
 73
 74pub fn get_default_system_shell() -> String {
 75    if cfg!(windows) {
 76        get_windows_system_shell()
 77    } else {
 78        "/bin/sh".to_string()
 79    }
 80}
 81
 82/// Get the default system shell, preferring bash on Windows.
 83pub fn get_default_system_shell_preferring_bash() -> String {
 84    if cfg!(windows) {
 85        get_windows_bash().unwrap_or_else(|| get_windows_system_shell())
 86    } else {
 87        "/bin/sh".to_string()
 88    }
 89}
 90
 91pub fn get_windows_bash() -> Option<String> {
 92    use std::path::PathBuf;
 93
 94    fn find_bash_in_scoop() -> Option<PathBuf> {
 95        let bash_exe =
 96            PathBuf::from(std::env::var_os("USERPROFILE")?).join("scoop\\shims\\bash.exe");
 97        bash_exe.exists().then_some(bash_exe)
 98    }
 99
100    fn find_bash_in_git() -> Option<PathBuf> {
101        // /path/to/git/cmd/git.exe/../../bin/bash.exe
102        let git = which::which("git").ok()?;
103        let git_bash = git.parent()?.parent()?.join("bin").join("bash.exe");
104        git_bash.exists().then_some(git_bash)
105    }
106
107    static BASH: LazyLock<Option<String>> = LazyLock::new(|| {
108        let bash = find_bash_in_scoop()
109            .or_else(|| find_bash_in_git())
110            .map(|p| p.to_string_lossy().into_owned());
111        if let Some(ref path) = bash {
112            log::info!("Found bash at {}", path);
113        }
114        bash
115    });
116
117    (*BASH).clone()
118}
119
120pub fn get_windows_system_shell() -> String {
121    use std::path::PathBuf;
122
123    fn find_pwsh_in_programfiles(find_alternate: bool, find_preview: bool) -> Option<PathBuf> {
124        #[cfg(target_pointer_width = "64")]
125        let env_var = if find_alternate {
126            "ProgramFiles(x86)"
127        } else {
128            "ProgramFiles"
129        };
130
131        #[cfg(target_pointer_width = "32")]
132        let env_var = if find_alternate {
133            "ProgramW6432"
134        } else {
135            "ProgramFiles"
136        };
137
138        let install_base_dir = PathBuf::from(std::env::var_os(env_var)?).join("PowerShell");
139        install_base_dir
140            .read_dir()
141            .ok()?
142            .filter_map(Result::ok)
143            .filter(|entry| matches!(entry.file_type(), Ok(ft) if ft.is_dir()))
144            .filter_map(|entry| {
145                let dir_name = entry.file_name();
146                let dir_name = dir_name.to_string_lossy();
147
148                let version = if find_preview {
149                    let dash_index = dir_name.find('-')?;
150                    if &dir_name[dash_index + 1..] != "preview" {
151                        return None;
152                    };
153                    dir_name[..dash_index].parse::<u32>().ok()?
154                } else {
155                    dir_name.parse::<u32>().ok()?
156                };
157
158                let exe_path = entry.path().join("pwsh.exe");
159                if exe_path.exists() {
160                    Some((version, exe_path))
161                } else {
162                    None
163                }
164            })
165            .max_by_key(|(version, _)| *version)
166            .map(|(_, path)| path)
167    }
168
169    fn find_pwsh_in_msix(find_preview: bool) -> Option<PathBuf> {
170        let msix_app_dir =
171            PathBuf::from(std::env::var_os("LOCALAPPDATA")?).join("Microsoft\\WindowsApps");
172        if !msix_app_dir.exists() {
173            return None;
174        }
175
176        let prefix = if find_preview {
177            "Microsoft.PowerShellPreview_"
178        } else {
179            "Microsoft.PowerShell_"
180        };
181        msix_app_dir
182            .read_dir()
183            .ok()?
184            .filter_map(|entry| {
185                let entry = entry.ok()?;
186                if !matches!(entry.file_type(), Ok(ft) if ft.is_dir()) {
187                    return None;
188                }
189
190                if !entry.file_name().to_string_lossy().starts_with(prefix) {
191                    return None;
192                }
193
194                let exe_path = entry.path().join("pwsh.exe");
195                exe_path.exists().then_some(exe_path)
196            })
197            .next()
198    }
199
200    fn find_pwsh_in_scoop() -> Option<PathBuf> {
201        let pwsh_exe =
202            PathBuf::from(std::env::var_os("USERPROFILE")?).join("scoop\\shims\\pwsh.exe");
203        pwsh_exe.exists().then_some(pwsh_exe)
204    }
205
206    static SYSTEM_SHELL: LazyLock<String> = LazyLock::new(|| {
207        let locations = [
208            || find_pwsh_in_programfiles(false, false),
209            || find_pwsh_in_programfiles(true, false),
210            || find_pwsh_in_msix(false),
211            || find_pwsh_in_programfiles(false, true),
212            || find_pwsh_in_msix(true),
213            || find_pwsh_in_programfiles(true, true),
214            || find_pwsh_in_scoop(),
215            || which::which_global("pwsh.exe").ok(),
216            || which::which_global("powershell.exe").ok(),
217        ];
218
219        locations
220            .into_iter()
221            .find_map(|f| f())
222            .map(|p| p.to_string_lossy().trim().to_owned())
223            .inspect(|shell| log::info!("Found powershell in: {}", shell))
224            .unwrap_or_else(|| {
225                log::warn!("Powershell not found, falling back to `cmd`");
226                "cmd.exe".to_string()
227            })
228    });
229
230    (*SYSTEM_SHELL).clone()
231}
232
233impl fmt::Display for ShellKind {
234    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235        match self {
236            ShellKind::Posix => write!(f, "sh"),
237            ShellKind::Csh => write!(f, "csh"),
238            ShellKind::Tcsh => write!(f, "tcsh"),
239            ShellKind::Fish => write!(f, "fish"),
240            ShellKind::PowerShell => write!(f, "powershell"),
241            ShellKind::Nushell => write!(f, "nu"),
242            ShellKind::Cmd => write!(f, "cmd"),
243            ShellKind::Rc => write!(f, "rc"),
244            ShellKind::Xonsh => write!(f, "xonsh"),
245            ShellKind::Elvish => write!(f, "elvish"),
246        }
247    }
248}
249
250impl ShellKind {
251    pub fn system() -> Self {
252        Self::new(&get_system_shell(), cfg!(windows))
253    }
254
255    pub fn new(program: impl AsRef<Path>, is_windows: bool) -> Self {
256        let program = program.as_ref();
257        let program = program
258            .file_stem()
259            .unwrap_or_else(|| program.as_os_str())
260            .to_string_lossy();
261
262        match &*program {
263            "powershell" | "pwsh" => ShellKind::PowerShell,
264            "cmd" => ShellKind::Cmd,
265            "nu" => ShellKind::Nushell,
266            "fish" => ShellKind::Fish,
267            "csh" => ShellKind::Csh,
268            "tcsh" => ShellKind::Tcsh,
269            "rc" => ShellKind::Rc,
270            "xonsh" => ShellKind::Xonsh,
271            "elvish" => ShellKind::Elvish,
272            "sh" | "bash" | "zsh" => ShellKind::Posix,
273            _ if is_windows => ShellKind::PowerShell,
274            // Some other shell detected, the user might install and use a
275            // unix-like shell.
276            _ => ShellKind::Posix,
277        }
278    }
279
280    pub fn to_shell_variable(self, input: &str) -> String {
281        match self {
282            Self::PowerShell => Self::to_powershell_variable(input),
283            Self::Cmd => Self::to_cmd_variable(input),
284            Self::Posix => input.to_owned(),
285            Self::Fish => input.to_owned(),
286            Self::Csh => input.to_owned(),
287            Self::Tcsh => input.to_owned(),
288            Self::Rc => input.to_owned(),
289            Self::Nushell => Self::to_nushell_variable(input),
290            Self::Xonsh => input.to_owned(),
291            Self::Elvish => input.to_owned(),
292        }
293    }
294
295    fn to_cmd_variable(input: &str) -> String {
296        if let Some(var_str) = input.strip_prefix("${") {
297            if var_str.find(':').is_none() {
298                // If the input starts with "${", remove the trailing "}"
299                format!("%{}%", &var_str[..var_str.len() - 1])
300            } else {
301                // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
302                // which will result in the task failing to run in such cases.
303                input.into()
304            }
305        } else if let Some(var_str) = input.strip_prefix('$') {
306            // If the input starts with "$", directly append to "$env:"
307            format!("%{}%", var_str)
308        } else {
309            // If no prefix is found, return the input as is
310            input.into()
311        }
312    }
313
314    fn to_powershell_variable(input: &str) -> String {
315        if let Some(var_str) = input.strip_prefix("${") {
316            if var_str.find(':').is_none() {
317                // If the input starts with "${", remove the trailing "}"
318                format!("$env:{}", &var_str[..var_str.len() - 1])
319            } else {
320                // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
321                // which will result in the task failing to run in such cases.
322                input.into()
323            }
324        } else if let Some(var_str) = input.strip_prefix('$') {
325            // If the input starts with "$", directly append to "$env:"
326            format!("$env:{}", var_str)
327        } else {
328            // If no prefix is found, return the input as is
329            input.into()
330        }
331    }
332
333    fn to_nushell_variable(input: &str) -> String {
334        let mut result = String::new();
335        let mut source = input;
336        let mut is_start = true;
337
338        loop {
339            match source.chars().next() {
340                None => return result,
341                Some('$') => {
342                    source = Self::parse_nushell_var(&source[1..], &mut result, is_start);
343                    is_start = false;
344                }
345                Some(_) => {
346                    is_start = false;
347                    let chunk_end = source.find('$').unwrap_or(source.len());
348                    let (chunk, rest) = source.split_at(chunk_end);
349                    result.push_str(chunk);
350                    source = rest;
351                }
352            }
353        }
354    }
355
356    fn parse_nushell_var<'a>(source: &'a str, text: &mut String, is_start: bool) -> &'a str {
357        if source.starts_with("env.") {
358            text.push('$');
359            return source;
360        }
361
362        match source.chars().next() {
363            Some('{') => {
364                let source = &source[1..];
365                if let Some(end) = source.find('}') {
366                    let var_name = &source[..end];
367                    if !var_name.is_empty() {
368                        if !is_start {
369                            text.push_str("(");
370                        }
371                        text.push_str("$env.");
372                        text.push_str(var_name);
373                        if !is_start {
374                            text.push_str(")");
375                        }
376                        &source[end + 1..]
377                    } else {
378                        text.push_str("${}");
379                        &source[end + 1..]
380                    }
381                } else {
382                    text.push_str("${");
383                    source
384                }
385            }
386            Some(c) if c.is_alphabetic() || c == '_' => {
387                let end = source
388                    .find(|c: char| !c.is_alphanumeric() && c != '_')
389                    .unwrap_or(source.len());
390                let var_name = &source[..end];
391                if !is_start {
392                    text.push_str("(");
393                }
394                text.push_str("$env.");
395                text.push_str(var_name);
396                if !is_start {
397                    text.push_str(")");
398                }
399                &source[end..]
400            }
401            _ => {
402                text.push('$');
403                source
404            }
405        }
406    }
407
408    pub fn args_for_shell(&self, interactive: bool, combined_command: String) -> Vec<String> {
409        match self {
410            ShellKind::PowerShell => vec!["-C".to_owned(), combined_command],
411            ShellKind::Cmd => vec!["/C".to_owned(), combined_command],
412            ShellKind::Posix
413            | ShellKind::Nushell
414            | ShellKind::Fish
415            | ShellKind::Csh
416            | ShellKind::Tcsh
417            | ShellKind::Rc
418            | ShellKind::Xonsh
419            | ShellKind::Elvish => interactive
420                .then(|| "-i".to_owned())
421                .into_iter()
422                .chain(["-c".to_owned(), combined_command])
423                .collect(),
424        }
425    }
426
427    pub const fn command_prefix(&self) -> Option<char> {
428        match self {
429            ShellKind::PowerShell => Some('&'),
430            ShellKind::Nushell => Some('^'),
431            ShellKind::Posix
432            | ShellKind::Csh
433            | ShellKind::Tcsh
434            | ShellKind::Rc
435            | ShellKind::Fish
436            | ShellKind::Cmd
437            | ShellKind::Xonsh
438            | ShellKind::Elvish => None,
439        }
440    }
441
442    pub fn prepend_command_prefix<'a>(&self, command: &'a str) -> Cow<'a, str> {
443        match self.command_prefix() {
444            Some(prefix) if !command.starts_with(prefix) => {
445                Cow::Owned(format!("{prefix}{command}"))
446            }
447            _ => Cow::Borrowed(command),
448        }
449    }
450
451    pub const fn sequential_commands_separator(&self) -> char {
452        match self {
453            ShellKind::Cmd => '&',
454            ShellKind::Posix
455            | ShellKind::Csh
456            | ShellKind::Tcsh
457            | ShellKind::Rc
458            | ShellKind::Fish
459            | ShellKind::PowerShell
460            | ShellKind::Nushell
461            | ShellKind::Xonsh
462            | ShellKind::Elvish => ';',
463        }
464    }
465
466    pub const fn sequential_and_commands_separator(&self) -> &'static str {
467        match self {
468            ShellKind::Cmd
469            | ShellKind::Posix
470            | ShellKind::Csh
471            | ShellKind::Tcsh
472            | ShellKind::Rc
473            | ShellKind::Fish
474            | ShellKind::PowerShell
475            | ShellKind::Xonsh => "&&",
476            ShellKind::Nushell | ShellKind::Elvish => ";",
477        }
478    }
479
480    pub fn try_quote<'a>(&self, arg: &'a str) -> Option<Cow<'a, str>> {
481        shlex::try_quote(arg).ok().map(|arg| match self {
482            // If we are running in PowerShell, we want to take extra care when escaping strings.
483            // In particular, we want to escape strings with a backtick (`) rather than a backslash (\).
484            ShellKind::PowerShell => Cow::Owned(arg.replace("\\\"", "`\"").replace("\\\\", "\\")),
485            ShellKind::Cmd => Cow::Owned(arg.replace("\\\\", "\\")),
486            ShellKind::Posix
487            | ShellKind::Csh
488            | ShellKind::Tcsh
489            | ShellKind::Rc
490            | ShellKind::Fish
491            | ShellKind::Nushell
492            | ShellKind::Xonsh
493            | ShellKind::Elvish => arg,
494        })
495    }
496
497    /// Quotes the given argument if necessary, taking into account the command prefix.
498    ///
499    /// In other words, this will consider quoting arg without its command prefix to not break the command.
500    /// You should use this over `try_quote` when you want to quote a shell command.
501    pub fn try_quote_prefix_aware<'a>(&self, arg: &'a str) -> Option<Cow<'a, str>> {
502        if let Some(char) = self.command_prefix() {
503            if let Some(arg) = arg.strip_prefix(char) {
504                // we have a command that is prefixed
505                for quote in ['\'', '"'] {
506                    if let Some(arg) = arg
507                        .strip_prefix(quote)
508                        .and_then(|arg| arg.strip_suffix(quote))
509                    {
510                        // and the command itself is wrapped as a literal, that
511                        // means the prefix exists to interpret a literal as a
512                        // command. So strip the quotes, quote the command, and
513                        // re-add the quotes if they are missing after requoting
514                        let quoted = self.try_quote(arg)?;
515                        return Some(if quoted.starts_with(['\'', '"']) {
516                            Cow::Owned(self.prepend_command_prefix(&quoted).into_owned())
517                        } else {
518                            Cow::Owned(
519                                self.prepend_command_prefix(&format!("{quote}{quoted}{quote}"))
520                                    .into_owned(),
521                            )
522                        });
523                    }
524                }
525                return self
526                    .try_quote(arg)
527                    .map(|quoted| Cow::Owned(self.prepend_command_prefix(&quoted).into_owned()));
528            }
529        }
530        self.try_quote(arg)
531    }
532
533    pub fn split(&self, input: &str) -> Option<Vec<String>> {
534        shlex::split(input)
535    }
536
537    pub const fn activate_keyword(&self) -> &'static str {
538        match self {
539            ShellKind::Cmd => "",
540            ShellKind::Nushell => "overlay use",
541            ShellKind::PowerShell => ".",
542            ShellKind::Fish
543            | ShellKind::Csh
544            | ShellKind::Tcsh
545            | ShellKind::Posix
546            | ShellKind::Rc
547            | ShellKind::Xonsh
548            | ShellKind::Elvish => "source",
549        }
550    }
551
552    pub const fn clear_screen_command(&self) -> &'static str {
553        match self {
554            ShellKind::Cmd => "cls",
555            ShellKind::Posix
556            | ShellKind::Csh
557            | ShellKind::Tcsh
558            | ShellKind::Rc
559            | ShellKind::Fish
560            | ShellKind::PowerShell
561            | ShellKind::Nushell
562            | ShellKind::Xonsh
563            | ShellKind::Elvish => "clear",
564        }
565    }
566
567    #[cfg(windows)]
568    /// We do not want to escape arguments if we are using CMD as our shell.
569    /// If we do we end up with too many quotes/escaped quotes for CMD to handle.
570    pub const fn tty_escape_args(&self) -> bool {
571        match self {
572            ShellKind::Cmd => false,
573            ShellKind::Posix
574            | ShellKind::Csh
575            | ShellKind::Tcsh
576            | ShellKind::Rc
577            | ShellKind::Fish
578            | ShellKind::PowerShell
579            | ShellKind::Nushell
580            | ShellKind::Xonsh
581            | ShellKind::Elvish => true,
582        }
583    }
584}
585
586#[cfg(test)]
587mod tests {
588    use super::*;
589
590    // Examples
591    // WSL
592    // wsl.exe --distribution NixOS --cd /home/user -- /usr/bin/zsh -c "echo hello"
593    // wsl.exe --distribution NixOS --cd /home/user -- /usr/bin/zsh -c "\"echo hello\"" | grep hello"
594    // wsl.exe --distribution NixOS --cd ~ env RUST_LOG=info,remote=debug .zed_wsl_server/zed-remote-server-dev-build proxy --identifier dev-workspace-53
595    // PowerShell from Nushell
596    // nu -c overlay use "C:\Users\kubko\dev\python\39007\tests\.venv\Scripts\activate.nu"; ^"C:\Program Files\PowerShell\7\pwsh.exe" -C "C:\Users\kubko\dev\python\39007\tests\.venv\Scripts\python.exe -m pytest \"test_foo.py::test_foo\""
597    // PowerShell from CMD
598    // cmd /C \" \"C:\\\\Users\\\\kubko\\\\dev\\\\python\\\\39007\\\\tests\\\\.venv\\\\Scripts\\\\activate.bat\"& \"C:\\\\Program Files\\\\PowerShell\\\\7\\\\pwsh.exe\" -C \"C:\\\\Users\\\\kubko\\\\dev\\\\python\\\\39007\\\\tests\\\\.venv\\\\Scripts\\\\python.exe -m pytest \\\"test_foo.py::test_foo\\\"\"\"
599
600    #[test]
601    fn test_try_quote_powershell() {
602        let shell_kind = ShellKind::PowerShell;
603        assert_eq!(
604            shell_kind
605                .try_quote("C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \"test_foo.py::test_foo\"")
606                .unwrap()
607                .into_owned(),
608            "\"C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest `\"test_foo.py::test_foo`\"\"".to_string()
609        );
610    }
611
612    #[test]
613    fn test_try_quote_cmd() {
614        let shell_kind = ShellKind::Cmd;
615        assert_eq!(
616            shell_kind
617                .try_quote("C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \"test_foo.py::test_foo\"")
618                .unwrap()
619                .into_owned(),
620            "\"C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \\\"test_foo.py::test_foo\\\"\"".to_string()
621        );
622    }
623
624    #[test]
625    fn test_try_quote_nu_command() {
626        let shell_kind = ShellKind::Nushell;
627        assert_eq!(
628            shell_kind.try_quote("'uname'").unwrap().into_owned(),
629            "\"'uname'\"".to_string()
630        );
631        assert_eq!(
632            shell_kind
633                .try_quote_prefix_aware("'uname'")
634                .unwrap()
635                .into_owned(),
636            "\"'uname'\"".to_string()
637        );
638        assert_eq!(
639            shell_kind.try_quote("^uname").unwrap().into_owned(),
640            "'^uname'".to_string()
641        );
642        assert_eq!(
643            shell_kind
644                .try_quote_prefix_aware("^uname")
645                .unwrap()
646                .into_owned(),
647            "^uname".to_string()
648        );
649        assert_eq!(
650            shell_kind.try_quote("^'uname'").unwrap().into_owned(),
651            "'^'\"'uname\'\"".to_string()
652        );
653        assert_eq!(
654            shell_kind
655                .try_quote_prefix_aware("^'uname'")
656                .unwrap()
657                .into_owned(),
658            "^'uname'".to_string()
659        );
660        assert_eq!(
661            shell_kind.try_quote("'uname a'").unwrap().into_owned(),
662            "\"'uname a'\"".to_string()
663        );
664        assert_eq!(
665            shell_kind
666                .try_quote_prefix_aware("'uname a'")
667                .unwrap()
668                .into_owned(),
669            "\"'uname a'\"".to_string()
670        );
671        assert_eq!(
672            shell_kind.try_quote("^'uname a'").unwrap().into_owned(),
673            "'^'\"'uname a'\"".to_string()
674        );
675        assert_eq!(
676            shell_kind
677                .try_quote_prefix_aware("^'uname a'")
678                .unwrap()
679                .into_owned(),
680            "^'uname a'".to_string()
681        );
682        assert_eq!(
683            shell_kind.try_quote("uname").unwrap().into_owned(),
684            "uname".to_string()
685        );
686        assert_eq!(
687            shell_kind
688                .try_quote_prefix_aware("uname")
689                .unwrap()
690                .into_owned(),
691            "uname".to_string()
692        );
693    }
694}