Home > Back-end >  How to call shell functions as a child process?
How to call shell functions as a child process?

Time:04-17

I have a nvm installed by homebrew (brew install nvm). And I want to call it inside a CLI that I'm writing in Rust, to install and switch node.js versions inside my Rust program.

The problem is: nvm is a shell function, not a file entrypoint. And unfortunately the child process spawned by the Rust tokio::process::Command only inherit the parent process's environment and PATH, but not the shell functions.

Constructs a new Command for launching the program at path program, with the following default configuration:

  • No arguments to the program
  • Inherit the current process's environment
  • Inherit the current process's working directory
  • Inherit stdin/stdout/stderr for spawn or status, but create pipes for output

https://docs.rs/tokio/0.2.0/tokio/process/struct.Command.html

When I tried to call tokio::process::Command::new("nvm"); I got:

thread 'main' panicked at 'Command not fail: Os { code: 13, kind: PermissionDenied, message: "Permission denied" }'

Nothing changes if I run with sudo, because I suppose that nvm doesn't exist to my child process.

How can I access nvm, or more simply, shell functions inside my Rust program?

CodePudding user response:

Answering The Literal Question

It's very unlikely to be meaningfully useful to you, but you can export shell functions through the environment if your shell is bash or has similar extensions.

Given the following Rust program named example.rs:

use std::process::Command;
use std::process::Stdio;
fn main() {
  Command::new("bash")
    .arg("-c")
    .arg("myfunction")
    .stdout(Stdio::inherit())
    .status();
}

...the following shell program:

myfunction() { echo "This is a function call"; }
export -f myfunction
rustc example.rs && ./example

...will emit as output:

This is a function call

Sourcing In-Process

You can also have your bash interpreter source the file that defines your function, and not need to export it:

use std::process::Command;
use std::process::Stdio;
fn main() {
  Command::new("bash")
    .arg("-c")
    .arg(". \"$0\" && \"$@\"")
    .arg("/path/to/nvm.sh")
    .arg("nvm")
    .arg("install")
    .arg("12")
    .stdout(Stdio::inherit())
    .status();
}

This works for nvm install, which is run for side effects (downloading an interpreter) that aren't relevant to the calling shell. It does not do anything particularly useful in the context of nvm use, because the shell that it's run in immediately exits, so the state changes made are immediately thrown away.


Explaining Why That Answer Is Useless

(for reconfiguring the user's interactive shell)

If nvm could be called this way, it wouldn't need to be a shell function in the first place.

The whole point of nvm being a shell function is to let it modify the state of the shell in which it's invoked (modifying the shell's variables, working directory, or other internal state). You cannot modify the state of a shell from any process except that shell itself, period, full-stop.

What you can do is have a program that writes valid shell syntax to its stdout, and have the shell in which the function is defined eval that program's output. This is a fairly common pattern in such use cases where no other option exists -- see how folks run eval "$(ssh-agent -s)" for an example.

Mind, bash does let you export functions into the environment -- you can run export -f nvm, and then a child process that is also a copy of bash will have a nvm function defined -- but this is of very little value, because that new copy of bash has a separate environment; changing it won't modify the parent shell's environment, and if the nvm command didn't need to modify the parent shell's environment, it wouldn't need to be a function in the first place.

  • Related