run_clang_format/cli/
mod.rs1use 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")] pub struct JsonModel {
16 pub paths: Vec<String>,
21 pub filter_pre: Option<Vec<String>>,
28 pub filter_post: Option<Vec<String>>,
33 pub style_file: Option<path::PathBuf>,
35 pub style_root: Option<path::PathBuf>,
37 pub command: Option<path::PathBuf>,
39
40 #[serde(skip)]
41 pub root: path::PathBuf,
43 #[serde(skip)]
44 pub name: String,
46}
47
48#[derive(Debug)]
49pub enum Command {
50 Format,
51 Check,
52}
53
54#[derive(Debug)]
55pub struct Data {
56 pub json: JsonModel,
58 pub style: Option<path::PathBuf>,
60 pub command: Option<path::PathBuf>,
62 pub jobs: Option<u8>,
65 pub cmd: Command,
67 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 .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 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 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}