std::process::Comand from Clap command line arguments structure

Hi!

I want to make a program (program A) that runs another program as a child process (program B). That another program (B) could be anything, I intend to supply the first program (A) with appropriate command via command line arguments. I want to use calp (derive).

The question is -- how do I implement it in the best possible, maybe conventional, way. I could go with just Vec<String> (as a type of the member for command in the command line options struct). While it seems OK I'm sure there are other ways.

Please suggest how you would implement it.

I built something along these lines recently, for a program whose job was to populate the environment with credentials before running other commands. I've reproduced main here - I hope it helps you see some options.

use std::collections::HashMap;
use std::os::unix::process::CommandExt;
use std::process::Command;

use anyhow::Result;
use botanist_vault::cli::Vault as VaultCli;
use botanist_vault::Vault;
use clap::Args;

use crate::invoker;

/// Run a program with environment entries from a Vault KVv2 key-value entry.
///
/// The entry identified by `--mount` and `--path` must hold a value consisting
/// of string fields (eg. as set by `vault kv put -mount MOUNT PATH KEY=value
/// KEY2=value2`). Each field will correspond to an environment variable of the
/// same name, overwriting any existing environment variable with the value from
/// Vault.
///
/// Existing environment variables, other than those set as above, are not
/// altered. This command replaces itself with the target program.
#[derive(Args)]
pub struct Kvv2 {
    #[command(flatten)]
    vault: VaultCli,

    /// The Vault mount path of the KVv2 secrets engine to query.
    #[arg(long, default_value = "service-config")]
    mount: String,

    /// The path of the secret holding environment variables.
    #[arg(long)]
    path: String,

    /// The version of the secret to read. If not specified, reads the most
    /// recent version.
    #[arg(long)]
    version: Option<i32>,

    #[command(flatten)]
    command: invoker::Command,
}

impl Kvv2 {
    pub async fn run(self) -> Result<()> {
        let vault_env = self.vault.env();
        let vault = Vault::try_from(self.vault)?;

        let request = vault.kvv2(&self.mount).read(&self.path);
        let request = match self.version {
            Some(version) => request.version(version),
            None => request,
        };
        let response = request.send().await?;
        let env: HashMap<String, String> = response.data.data;

        let err = self.command.command()?.envs(env).exec();
        Err(err.into())
    }
}

invoker::Command is as follows:

use std::ffi::OsString;
use std::{env, process};

use anyhow::{anyhow, Result};
use clap::Args;
use nix::unistd;

#[derive(Args)]
pub struct Command {
    /// The program to run. If this is not specified, this will run an interactive
    /// shell.
    #[arg()]
    program: Option<OsString>,

    /// Arguments to pass to the program.
    #[arg()]
    pub args: Vec<OsString>,
}

impl Command {
    pub fn program(&self) -> Result<OsString> {
        let program = match self.program.to_owned() {
            Some(program) => program,
            None => detect_shell()?,
        };

        Ok(program)
    }

    pub fn command(&self) -> Result<process::Command> {
        let program = self.program()?;
        let mut command = process::Command::new(program);
        command.args(&self.args);

        Ok(command)
    }
}

pub fn detect_shell() -> Result<OsString> {
    match env::var("SHELL") {
        Ok(val) => Ok(val.into()),
        Err(env::VarError::NotPresent) => Ok(profile_shell()?),
        Err(e) => Err(e)?,
    }
}

fn profile_shell() -> Result<OsString> {
    let uid = unistd::getuid();
    let user = unistd::User::from_uid(uid)?;
    let user = user.ok_or_else(|| anyhow!("Current uid {uid} does not map to a user"))?;
    Ok(user.shell.into())
}

Thank you very much, that's quite helpful!

Do you know any way to check validity of Command argument? (When Clap parses the arguments that is)

Validity in what sense? Why don't you just try to run the supplied executable and find out if it runs correctly?

I meant the validity of the arguments passed like, for instance, the number of arguments. I figured out that I could mark a Vec<String> structure field with the clap attribute with num_args parameter (like clap(num_args=1..)). So in total we get something like:

#[derive(Parser)]
struct Opts {
    #[clap(num_args=1.., required=true)]
    cmd: Vec<String>,
}

This seems to work alright, though it doesn't seem conventional. But then again I suppose that kind of specific thing is far beyond the purview of conventions. However still I find myself in doubt -- would that be sufficient? Would OsString be preferable? Please let me know if you'd happen to have any suggestions.

Why would using an option provided by clap derive interface not be conventional?

Well the other part of my specific issue is that Option<String> with separate Vec<String> would imply optionality of the command (which could be just my perspective but still), and as well as that I'm trying to implement other use cases where supplying a command would be incorrect. So far I made the thing using num_args=1.. and conflicting groups.

This is how it looks all together:

#[derive(Parser)]
#[command()]
struct Opts {
    #[clap(group="aux", long, short, conflicts_with="cmd")]
    default_config: bool,
    #[clap(num_args=1.., conflicts_with="aux", required=true)]
    cmd: Vec<String>,
}

I guess it's fine? Though I'm not sure required=true and conflicts_with, but it works!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.