mz_ore/cli.rs
1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License in the LICENSE file at the
6// root of this repository, or online at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Command-line parsing utilities.
17
18use std::ffi::OsString;
19use std::fmt::{self, Display};
20use std::str::FromStr;
21
22use clap::Parser;
23
24/// A help template for use with clap that does not include the name of the
25/// binary or the version in the help output.
26const NO_VERSION_HELP_TEMPLATE: &str = "{about}
27
28USAGE:
29 {usage}
30
31{all-args}";
32
33/// Configures command-line parsing via [`parse_args`].
34#[derive(Debug, Default, Clone)]
35pub struct CliConfig<'a> {
36 /// An optional prefix to apply to the environment variable name for all
37 /// arguments with an environment variable fallback.
38 //
39 // TODO(benesch): switch to the clap-native `env_prefix` option if that
40 // gets implemented: https://github.com/clap-rs/clap/issues/3221.
41 pub env_prefix: Option<&'a str>,
42 /// Enable clap's built-in `--version` flag.
43 ///
44 /// We disable this by default because most of our binaries are not
45 /// meaningfully versioned.
46 pub enable_version_flag: bool,
47}
48
49/// Parses command-line arguments according to a clap `Parser` after
50/// applying Materialize-specific customizations.
51pub fn parse_args<O>(config: CliConfig) -> O
52where
53 O: Parser,
54{
55 // Construct the prefixed environment variable names for all
56 // environment-enabled arguments, if requested. We have to construct these
57 // names before constructing `clap` below to get the lifetimes to work out.
58 let command = O::command();
59 let arg_envs: Vec<_> = command
60 .get_arguments()
61 .filter_map(|arg| match (config.env_prefix, arg.get_env()) {
62 (Some(prefix), Some(env)) => {
63 let mut prefixed_env = OsString::from(prefix);
64 prefixed_env.push(env);
65 Some((arg.get_id(), prefixed_env))
66 }
67 _ => None,
68 })
69 .collect();
70
71 let mut clap = O::command().args_override_self(true);
72
73 if !config.enable_version_flag {
74 clap = clap.disable_version_flag(true);
75 clap = clap.help_template(NO_VERSION_HELP_TEMPLATE);
76 }
77
78 for (arg, env) in &arg_envs {
79 clap = clap.mut_arg(*arg, |arg| arg.env_os(env));
80 }
81
82 O::from_arg_matches(&clap.get_matches()).unwrap()
83}
84
85/// A command-line argument of the form `KEY=VALUE`.
86#[derive(Debug, Clone)]
87pub struct KeyValueArg<K, V> {
88 /// The key of the command-line argument.
89 pub key: K,
90 /// The value of the command-line argument.
91 pub value: V,
92}
93
94impl<K, V> FromStr for KeyValueArg<K, V>
95where
96 K: FromStr,
97 K::Err: Display,
98 V: FromStr,
99 V::Err: Display,
100{
101 type Err = String;
102
103 fn from_str(s: &str) -> Result<KeyValueArg<K, V>, String> {
104 let mut parts = s.splitn(2, '=');
105 let key = parts.next().expect("always one part");
106 let value = parts
107 .next()
108 .ok_or_else(|| "must have format KEY=VALUE".to_string())?;
109 Ok(KeyValueArg {
110 key: key.parse().map_err(|e| format!("parsing key: {}", e))?,
111 value: value.parse().map_err(|e| format!("parsing value: {}", e))?,
112 })
113 }
114}
115
116/// A command-line argument that defaults to `true`
117/// that can be falsified with `--flag=false`
118// TODO: replace with `SetTrue` when available in clap.
119#[derive(Debug, Clone)]
120pub struct DefaultTrue {
121 /// The value for this flag
122 pub value: bool,
123}
124
125impl Default for DefaultTrue {
126 fn default() -> Self {
127 Self { value: true }
128 }
129}
130
131impl fmt::Display for DefaultTrue {
132 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
133 self.value.fmt(f)
134 }
135}
136
137impl FromStr for DefaultTrue {
138 type Err = std::str::ParseBoolError;
139
140 fn from_str(s: &str) -> Result<Self, Self::Err> {
141 Ok(Self { value: s.parse()? })
142 }
143}