Structopt/clap: don't parse positional arguments?

#1

I’m building a tool called escalator for solving a certain problem I’m encountering in Docker containers (I need USER to be unprivileged but I need ENTRYPOINT to be PID 1 and root:root). In any case, the need for such a tool is beside the point of the question :smile:

The current usage docs emitted by clap/structopt:

escalator 0.1.0
Naftuli Kay <me@naftuli.wtf>
Escalate user and group ids to root and execute a binary with arguments in place without forking.

USAGE:
    escalator [FLAGS] <binary> [args]...

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information
    -v               Verbosity; pass multiple times to increase log verbosity. By default, verbosity is set to ERROR, -v
                     yields WARN, -vv yields INFO, -vvv yields DEBUG, and -vvvv yields TRACE.

ARGS:
    <binary>     A fully-qualified path to the binary to execute.
    <args>...    A list of arguments to pass to the binary upon execution.  

The actual structopt definition:

/// Escalate user and group ids to root and execute a binary with arguments in place without forking.
#[derive(Debug, StructOpt)]
struct CLI {
    /// Verbosity; pass multiple times to increase log verbosity. By default, verbosity is set to
    /// ERROR, -v yields WARN, -vv yields INFO, -vvv yields DEBUG, and -vvvv yields TRACE.
    #[structopt(short = "v", parse(from_occurrences))]
    verbosity: u8,
    /// A fully-qualified path to the binary to execute.
    #[structopt(parse(from_os_str))]
    binary: PathBuf,
    /// A list of arguments to pass to the binary upon execution.
    args: Vec<String>,
}

The issue that I’m encountering is that I can’t pass arbitrary flags in my invocation:

$ sudo target/debug/escalator $(which id) -u -n
error: Found argument '-u' which wasn't expected, or isn't valid in this context

USAGE:
    escalator [FLAGS] <binary> [args]...

For more information try --help

Is there a way to tell structopt/clap to not parse anything including and after the args positional value? I’m climbing through the settings right now to see if this is possible. Things do just work if I call escalator $(which id) -- -u -n, but since the whole purpose of this binary is to proxy things forward, it seems like a bit of overhead to have to always pass -- to delimit between the binary and the arguments.

Any ideas?

0 Likes

#2

I think the TrailingVarArg setting should work: https://docs.rs/clap/2.32.0/clap/enum.AppSettings.html#variant.TrailingVarArg

0 Likes

#3
/// Escalate user and group ids to root and execute a binary with arguments in place without forking.
#[derive(Debug, StructOpt)]
#[structopt(raw(setting = "structopt::clap::AppSettings::TrailingVarArg"))]
struct CLI {
    /// Verbosity; pass multiple times to increase log verbosity. By default, verbosity is set to
    /// ERROR, -v yields WARN, -vv yields INFO, -vvv yields DEBUG, and -vvvv yields TRACE.
    #[structopt(short = "v", parse(from_occurrences))]
    verbosity: u8,
    /// A fully-qualified path to the binary to execute.
    #[structopt(parse(from_os_str))]
    binary: PathBuf,
    /// A list of arguments to pass to the binary upon execution.
    args: Vec<String>,
}

Still no joy unfortunately:

naftuli@axel:~/devel/src/github.com/naftulikay/escalator$ sudo $(which cargo) run -- $(which id)
    Finished dev [unoptimized + debuginfo] target(s) in 0.18s
     Running `target/debug/escalator /usr/bin/id`
uid=0(root) gid=0(root) groups=0(root)
naftuli@axel:~/devel/src/github.com/naftulikay/escalator$ sudo $(which cargo) run -- $(which id) -u -n
    Finished dev [unoptimized + debuginfo] target(s) in 0.18s
     Running `target/debug/escalator /usr/bin/id -u -n`
error: Found argument '-u' which wasn't expected, or isn't valid in this context

USAGE:
    escalator [FLAGS] <binary> [args]...

For more information try --help
naftuli@axel:~/devel/src/github.com/naftulikay/escalator$ sudo $(which cargo) run -- $(which id) -- -u -n
    Finished dev [unoptimized + debuginfo] target(s) in 0.18s
     Running `target/debug/escalator /usr/bin/id -- -u -n`
root

Any other ideas, or maybe I’m doing something wrong?

0 Likes

#4

I have reproduced the problem here on the playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f21f5bf2681f8d5e0968449aa27e62a0

extern crate clap; // 2.32.0

use clap::App;
use clap::AppSettings;
use clap::Arg;

use std::process;

fn main() {
    let args = ["app", "-v", "/path/to/binary", "-a", "-b"];

    eprintln!("Executing {:?}", args);

    let _ = App::new("app")
        .version("1.0.0")
        .author("Naftuli Kay")
        .setting(AppSettings::DontDelimitTrailingValues)
        .arg(Arg::with_name("verbosity")
            .short("v")
            .multiple(true)
            .help("Change verbosity."))
        .arg(Arg::with_name("binary")
            .value_name("BINARY")
            .takes_value(true)
            .required(true)
            .index(1)
            .help("The executable to execute."))
        .arg(Arg::with_name("args")
            .value_name("args")
            .takes_value(true)
            .multiple(true)
            .required(false)
            .last(true)
            .use_delimiter(false)
            .help("The arguments to pass to the executable."))
        .get_matches_from_safe(args.iter())
        .unwrap_or_else(|e| {
            eprintln!("{}", e);
            process::exit(1);
        });
}

The goal is to tell clap to stop all parsing once it gets past <binary>.

0 Likes

#5

This works for me:

let _ = App::new("app")
    .version("1.0.0")
    .author("Naftuli Kay")
    .setting(AppSettings::TrailingVarArg)
    .setting(AppSettings::DontDelimitTrailingValues)
    .arg(Arg::with_name("verbosity")
        .short("v")
        .multiple(true)
        .help("Change verbosity."))
    .arg(Arg::with_name("binary")
        .value_name("BINARY")
        .takes_value(true)
        .multiple(true)
        .required(true)
        .index(1)
        .help("The executable to execute."))
    .get_matches_from_safe(args.iter())
    .unwrap_or_else(|e| {
        eprintln!("{}", e);
        process::exit(1);
    });

I haven’t tried to map it back to structopt.

0 Likes

#6

@inejge I don’t see args there. To clarify a little bit, my use case is running the program like this:

escalator -vv $(which id) -u -n

escalator only knows about -v and <binary> and that anything that follows should be considered an argument, regardless of whether it’s a flag or not.

Presently, I have to run it like this:

escalator -vv $(which id) -- -u -n

I’m trying to get rid of the need for --. If this is not possible, I will open an issue for clap.

0 Likes

#7

The args are in the binary argument. The first one is the first positional argument. The rest correspond to -u and -n, in your example. Try it.

0 Likes