1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// Copyright Materialize, Inc. and contributors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License in the LICENSE file at the
// root of this repository, or online at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Command-line parsing utilities.

use std::ffi::OsString;
use std::fmt::{self, Display};
use std::str::FromStr;

use clap::Parser;

/// A help template for use with clap that does not include the name of the
/// binary or the version in the help output.
const NO_VERSION_HELP_TEMPLATE: &str = "{about}

USAGE:
    {usage}

{all-args}";

/// Configures command-line parsing via [`parse_args`].
#[derive(Debug, Default, Clone)]
pub struct CliConfig<'a> {
    /// An optional prefix to apply to the environment variable name for  all
    /// arguments with an environment variable fallback.
    //
    // TODO(benesch): switch to the clap-native `env_prefix` option if that
    // gets implemented: https://github.com/clap-rs/clap/issues/3221.
    pub env_prefix: Option<&'a str>,
    /// Enable clap's built-in `--version` flag.
    ///
    /// We disable this by default because most of our binaries are not
    /// meaningfully versioned.
    pub enable_version_flag: bool,
}

/// Parses command-line arguments according to a clap `Parser` after
/// applying Materialize-specific customizations.
pub fn parse_args<O>(config: CliConfig) -> O
where
    O: Parser,
{
    // Construct the prefixed environment variable names for all
    // environment-enabled arguments, if requested. We have to construct these
    // names before constructing `clap` below to get the lifetimes to work out.
    let arg_envs: Vec<_> = O::command()
        .get_arguments()
        .filter_map(|arg| match (config.env_prefix, arg.get_env()) {
            (Some(prefix), Some(env)) => {
                let mut prefixed_env = OsString::from(prefix);
                prefixed_env.push(env);
                Some((arg.get_id(), prefixed_env))
            }
            _ => None,
        })
        .collect();

    let mut clap = O::command().args_override_self(true);

    if !config.enable_version_flag {
        clap = clap.disable_version_flag(true);
        clap = clap.help_template(NO_VERSION_HELP_TEMPLATE);
    }

    for (arg, env) in &arg_envs {
        clap = clap.mut_arg(*arg, |arg| arg.env_os(env));
    }

    O::from_arg_matches(&clap.get_matches()).unwrap()
}

/// A command-line argument of the form `KEY=VALUE`.
#[derive(Debug, Clone)]
pub struct KeyValueArg<K, V> {
    /// The key of the command-line argument.
    pub key: K,
    /// The value of the command-line argument.
    pub value: V,
}

impl<K, V> FromStr for KeyValueArg<K, V>
where
    K: FromStr,
    K::Err: Display,
    V: FromStr,
    V::Err: Display,
{
    type Err = String;

    fn from_str(s: &str) -> Result<KeyValueArg<K, V>, String> {
        let mut parts = s.splitn(2, '=');
        let key = parts.next().expect("always one part");
        let value = parts
            .next()
            .ok_or_else(|| "must have format KEY=VALUE".to_string())?;
        Ok(KeyValueArg {
            key: key.parse().map_err(|e| format!("parsing key: {}", e))?,
            value: value.parse().map_err(|e| format!("parsing value: {}", e))?,
        })
    }
}

/// A command-line argument that defaults to `true`
/// that can be falsified with `--flag=false`
// TODO: replace with `SetTrue` when available in clap.
#[derive(Debug, Clone)]
pub struct DefaultTrue {
    /// The value for this flag
    pub value: bool,
}

impl Default for DefaultTrue {
    fn default() -> Self {
        Self { value: true }
    }
}

impl fmt::Display for DefaultTrue {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        self.value.fmt(f)
    }
}

impl FromStr for DefaultTrue {
    type Err = std::str::ParseBoolError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(Self { value: s.parse()? })
    }
}