run_clang_format/cli/
mod.rs

1use std::{path, process};
2
3mod handlers;
4mod logging;
5pub mod utils;
6
7use clap::{arg, crate_authors, crate_description, crate_name, crate_version};
8#[allow(unused_imports)]
9use color_eyre::{eyre::eyre, eyre::WrapErr, Help};
10use schemars::{schema_for, JsonSchema};
11use serde::Deserialize;
12
13#[derive(Deserialize, Debug, JsonSchema)]
14#[serde(rename_all = "camelCase")] // removed: deny_unknown_fields
15pub struct JsonModel {
16    /// List of paths and/or globs.
17    /// This list may contain paths or shell-style globs to define the files that should be
18    /// filtered. Paths or globs that resolve to folders will be silently ignored. Any path
19    /// contained in this list must be specified relative to the configuration file.
20    pub paths: Vec<String>,
21    /// Optional list of globs used for efficiently pre-filtering paths.
22    /// In contrast to the post-filter, searching will completely skip all paths and its siblings
23    /// for any match with any pattern. E.g., [".git"] will skip all ".git" folders completely.
24    /// By default, i.e., if this field is not present in the configuration, the tool will skip all
25    /// hidden paths and files. Set this entry to an empty list to prevent any kind of
26    /// pre-filtering.
27    pub filter_pre: Option<Vec<String>>,
28    /// Optional list of globs to use for post-filtering.
29    /// This filter will be applied for all paths _after_ they have been resolved. In contrast to
30    /// the pre-filter, siblings of paths will not be filtered without the corresponding glob. E.g.,
31    /// ".git" will not filter any files, only ".git/**" would. Notice that only
32    pub filter_post: Option<Vec<String>>,
33    /// Optional path to a `.clang-format` style file (can be specified via --style)
34    pub style_file: Option<path::PathBuf>,
35    /// Optional path where the `.clang-format` file should be copied to while executing
36    pub style_root: Option<path::PathBuf>,
37    /// Optional path to the `clang-format` executable or command name
38    pub command: Option<path::PathBuf>,
39
40    #[serde(skip)]
41    /// Parent directory of the Json file, used to resolve paths specified within
42    pub root: path::PathBuf,
43    #[serde(skip)]
44    /// Lossy Json filename
45    pub name: String,
46}
47
48#[derive(Debug)]
49pub enum Command {
50    Format,
51    Check,
52}
53
54#[derive(Debug)]
55pub struct Data {
56    /// Json input data
57    pub json: JsonModel,
58    /// Command-line override for the style file
59    pub style: Option<path::PathBuf>,
60    /// Command-line override for the clang-format executable
61    pub command: Option<path::PathBuf>,
62    /// Command-line parameter for the number of jobs to use for executing clang-format
63    /// If `None` then all available jobs should be used, else the specified number of jobs.
64    pub jobs: Option<u8>,
65    /// Command to execute.
66    pub cmd: Command,
67    /// Check that all files are within the .clang-format root directory.
68    pub strict_root: bool,
69}
70
71#[derive(Debug)]
72pub struct Builder {
73    pub matches: clap::ArgMatches,
74}
75
76impl Builder {
77    fn app() -> clap::Command {
78        clap::Command::new(crate_name!())
79            .arg_required_else_help(true)
80            .version(crate_version!())
81            .author(crate_authors!())
82            .about(crate_description!())
83            .arg(
84                arg!(<JSON>)
85                    .help("Path/configuration as .json")
86                    .value_parser(clap::value_parser!(std::path::PathBuf)),
87            )
88            .arg(
89                arg!(-s --style ... "Optional path to .clang-format style file. \
90                                     Overrides <JSON> configuration")
91                .value_parser(clap::value_parser!(std::path::PathBuf))
92                .required(false)
93                .action(clap::ArgAction::Set),
94            )
95            .arg(
96                arg!(-c --command ... "Optional path to executable or clang-format command. \
97                                       Overrides <JSON> configuration, defaults to `clang-format`")
98                // .default_value("clang-format")
99                .value_parser(clap::value_parser!(std::path::PathBuf))
100                .required(false)
101                .action(clap::ArgAction::Set),
102            )
103            .arg(
104                arg!(-j --jobs ... "Optional parameter to define the number of jobs to use. \
105                                    If provided without value (e.g., '-j') all available logical \
106                                    cores are used. Maximum value is 255")
107                .required(false)
108                .num_args(0..=1)
109                .action(clap::ArgAction::Set),
110            )
111            .arg(arg!(-v --verbose ... "Verbosity, use -vv... for verbose output.").global(true))
112            .arg(
113                arg!(--check "Run in check mode instead of formatting. Use -vv to \
114                              log the output of clang-format for each mismatch. \
115                              Requires clang-format 10 or higher.")
116                .action(clap::ArgAction::SetTrue),
117            )
118            .arg(
119                arg!(-q --quiet "Suppress all output except for errors; overrides -v")
120                    .action(clap::ArgAction::SetTrue),
121            )
122            .arg(
123                // See https://github.com/clap-rs/clap/issues/2468
124                arg!(--"strict-root"
125                    "Checks that all files are siblings to the .clang-format root directory. \
126                     Without this option, no checks are performed and files that are not within \
127                     the root directory will be ignored and thus not formatted by clang-format. \
128                     This option should only be enabled if no symlinks, etc., are used since such \
129                     paths may not be resolved reliably. This check is only available if a \
130                     style file or style root directory is specified.")
131                .action(clap::ArgAction::SetTrue),
132            )
133            .subcommand_negates_reqs(true)
134            .subcommand(
135                clap::Command::new("schema")
136                    .about("Print the schema used for the <JSON> configuration file"),
137            )
138    }
139
140    pub fn build() -> Builder {
141        let cmd = Builder::app();
142        let builder = Builder {
143            matches: cmd.get_matches(),
144        };
145        logging::setup(&builder.matches);
146        builder
147    }
148
149    pub fn parse(self) -> eyre::Result<Data> {
150        if self.matches.subcommand_matches("schema").is_some() {
151            println!("{}", JsonModel::schema(),);
152            process::exit(0);
153        }
154
155        let json_path = self.path_for_key("JSON", true)?;
156        let json = JsonModel::load(json_path).wrap_err("Invalid parameter for <JSON>")?;
157
158        let style = match self.matches.contains_id("style") {
159            false => None,
160            true => {
161                let style_path = self
162                    .path_for_key("style", true)
163                    .wrap_err("Invalid parameter for option --style")?;
164                let path = utils::file_with_name_or_ext(style_path, ".clang-format")
165                    .wrap_err("Invalid parameter for option --style")?;
166                Some(path)
167            }
168        };
169
170        let command = match self.matches.get_one::<std::path::PathBuf>("command") {
171            None => None,
172            Some(_) => Some(
173                utils::executable_or_exists(self.path_for_key("command", false)?, None)
174                    .wrap_err("Invalid parameter for option --command")
175                    .suggestion(
176                        "Please make sure that '--command' is either a valid absolute path, \
177                            a valid path relative to the current working directory \
178                            or a known application",
179                    )?,
180            ),
181        };
182
183        // cannot use "and" since it is not lazily evaluated, and cannot use "and_then" nicely
184        // since the question mark operator does not work in closures
185        // let command = self
186        //     .matches
187        //     .value_of_os("command")
188        //     .and_then(|_| Some(self.path_for_key("command", false)?));
189
190        let jobs = {
191            if let Some(val) = self.matches.get_one::<String>("jobs") {
192                let val: u8 = val
193                    .parse()
194                    .map_err(|_| eyre!("Invalid parameter for option --jobs"))
195                    .suggestion("Please provide a number in the range [0 .. 255]")?;
196                Some(val)
197            } else {
198                None
199            }
200        };
201
202        let cmd = if self.matches.get_flag("check") {
203            Command::Check
204        } else {
205            Command::Format
206        };
207
208        let strict_root = self.matches.get_flag("strict-root");
209
210        Ok(Data {
211            json,
212            style,
213            command,
214            jobs,
215            cmd,
216            strict_root,
217        })
218    }
219
220    fn path_for_key(&self, key: &str, check_exists: bool) -> eyre::Result<path::PathBuf> {
221        let path = self
222            .matches
223            .get_one::<std::path::PathBuf>(key)
224            .map(std::path::PathBuf::from)
225            .ok_or(eyre!(format!(
226                "Could not convert parameter '{key}' to path"
227            )))?;
228
229        if check_exists {
230            return utils::path_or_err(path);
231        }
232        Ok(path)
233    }
234}
235
236impl JsonModel {
237    fn schema() -> String {
238        let schema = schema_for!(JsonModel);
239        serde_json::to_string_pretty(&schema).unwrap()
240    }
241
242    fn load(path: impl AsRef<path::Path>) -> eyre::Result<JsonModel> {
243        let json_path = utils::file_with_ext(path.as_ref(), "json", true)?;
244        let json_name = json_path.to_string_lossy();
245
246        let f = std::fs::File::open(path.as_ref())
247            .wrap_err(format!("Failed to open provided JSON file '{json_name}'"))?;
248
249        let mut json: JsonModel = serde_json::from_reader(std::io::BufReader::new(f))
250            .wrap_err(format!("Validation failed for '{json_name}'"))
251            .suggestion(format!(
252        "Please make sure that '{json_name}' is a valid .json file and the contents match the required schema."))?;
253
254        json.root = json_path
255            .canonicalize()
256            .unwrap()
257            .parent()
258            .unwrap()
259            .to_path_buf();
260
261        json.name = json_path.to_string_lossy().into();
262        Ok(json)
263    }
264}