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.
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())
}
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:
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.
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.