Inconsistency Command behavior between Linux and Windows - program path

I run my program as ,

Command::new("./myprogram")
    .current_dir("/bin")
    .spawn()
    .expect("ls command failed to start");

It works without any problem, however if I port the above code to Windows like,

Command::new(".\\myprogram")
    .current_dir("C:\\bin")
    .spawn()
    .expect("ls command failed to start");

It can't start the program and Rust is reporting that the file not found. However, if I provide a full path to my program, like,

Command::new("C:\\bin\\myprogram")
    .current_dir("C:\\bin")
    .spawn()
    .expect("ls command failed to start");

It starts normally. If I try to start the same program in Bash or cmd using .\myprogram, it starts on both systems. I think such Rust behavior is inconsistent. I still hope I missed something.

I think it's not so much Rust, as it is how programs' paths and extensions are searched.

If you try to change ".\\myprogram" to ".\\myprogram.exe", does it find it?

maybe related to Command::spawn has weird rules for finding binaries on Windows · Issue #37519 · rust-lang/rust · GitHub

windows is weird, I suggest to either add .exe if you specifiy a path, or specify only the name of a command without .exe, i.e. remove ".\".

1 Like

The first thing I would check is what the value of std::env::current_dir is. It's possible the way you run the launching process on Linux leaves you in '/bin' (I assume that's a placeholder') and 'Windows' leaves you somewhere else.

Otherwise I would expect the behavior you're seeing on Windows. Maybe there is a limitation on Linux/Posix that requires it to be different, but I would definitely expect the order to be:

  • Find the myprogram binary in the current directory.
  • Launch it using an OS specific method to set its current_dir directly.

rather than

  • Set the current_dir to the specified value.
  • Find the 'myprogram' binary in the the new current_dir.
  • Launch it.
  • (Hopefully) Set my current_dir back to its original value.

Rust works fine with Windows exe extension. Indeed, first I tried to add the extension, but later I figured out that Windows handles no extension fine, and no extension is required for running a program. If I remove .\ in front, it will launch program nicely as long as it can find it in path env. However you gave an interesting tip, if I have no myprogram in path, will it be launched from the current directory?

Yeah. I can get it to work on windows by doing:

    std::env::set_current_dir("C:\\bin");
    std::process::Command::new(".\\myprogram")
        /* the new process will inherit the spawners current_dir. */
        .spawn()
        .expect("ls command failed to start");

The other posters are referencing an issue that allows:

    std::process::Command::new("myprogram")
        .env("PATH", "C:\\bin")
        .spawn()
        .expect("ls command failed to start");

to work; but that doesn't seem to be what you are trying to do.

Thanks for confirming the issues. As for now I use the following work around,

#[cfg(target_os = "windows")]
    if prog.starts_with(".\\") {
        prog = cwd.to_owned().join(prog).display().to_string();
    }

I hope that "work around" is just temporary, for your own use and testing of how paths work.

1 Like

Unfortunately, customers deal with bad work around quite frequently even having no idea how bad can be the code behind. It's why I rather use open source, because I clear see with what I dealt in .

I cannot speak for Windows (it is the odd one out after all, most current operating systems are Unix like), but the Unix behaviour is a natural consequence of the fork+exec model that Unix traditionally used (and posix_spawn is still implemented using vfork internally in glibc, at least on Linux).

That is:

  1. Fork a child process.
  2. Change directory in the child
  3. Execute the new program in the child, using the path resolution rules of the OS.

That windows doesn't work like that (plus has a ton of arcane rules and backward compatibility exceptions: why would adding/removing .exe or .\ make a difference?) is often surprising.

A funny fact is that Windows application started with using the current directory argument, acknowledges it, and File(".\text.txt") will be correctly processed from it. So I can assume only that first , Windows tries to start the application and only after, sets the current directory for it. Linux behavior is exactly opposite.

Adding or removing ./ only makes a difference on Unix. Windows is more than happy to load a program in the current directory without it.

The .exe issue people are mentioning seems to be about this gnarly block in an extremely old version of the Rust stdlib for spawn on Windows:

        // To have the spawning semantics of unix/windows stay the same, we need
        // to read the *child's* PATH if one is provided. See #15149 for more
        // details.
        let program = self.env.as_ref().and_then(|env| {
            for (key, v) in env {
                if OsStr::new("PATH") != &**key { continue }

                // Split the value and test each path to see if the
                // program exists.
                for path in split_paths(&v) {
                    let path = path.join(self.program.to_str().unwrap())
                                   .with_extension(env::consts::EXE_EXTENSION);
                    if fs::metadata(&path).is_ok() {
                        return Some(path.into_os_string())
                    }
                }
                break
            }
            None
        });

Which, if the PATH env var was specified, could end up launching foo.exe even if the provided filename was foo.txt. But that was entirely a Rust thing and newer versions use a sane method to handle this.

If it makes a difference or not depends on if the current directory is in PATH, which it rarely is by default for security reasons.

Fair enough, I misunderstood that then.

Windows is still the odd one out by resolving the binary before the working directory change. What do I mean by being the odd one out? Well, Windows is in a minority these days, both in terms of number of mainstream OSes (various server Linux, Android, MacOS, iOS) and in number of installs. The only market segment where windows is still relevant is x86-64 desktop/laptop client computing.

(Yes there are other OSes like FreeBSD, they are a rounding error, and for the most part Unix-like. Also desktop Linux as much as it pains me to say is essentially a rounding error outside specific niches.)