##// END OF EJS Templates
rhg: Add support for --config CLI arguments...
Simon Sapin -
r47232:87fd7f47 default draft
parent child Browse files
Show More
@@ -1,341 +1,344 b''
1 // config.rs
1 // config.rs
2 //
2 //
3 // Copyright 2020
3 // Copyright 2020
4 // Valentin Gatien-Baron,
4 // Valentin Gatien-Baron,
5 // Raphaël Gomès <rgomes@octobus.net>
5 // Raphaël Gomès <rgomes@octobus.net>
6 //
6 //
7 // This software may be used and distributed according to the terms of the
7 // This software may be used and distributed according to the terms of the
8 // GNU General Public License version 2 or any later version.
8 // GNU General Public License version 2 or any later version.
9
9
10 use super::layer;
10 use super::layer;
11 use crate::config::layer::{
11 use crate::config::layer::{
12 ConfigError, ConfigLayer, ConfigParseError, ConfigValue,
12 ConfigError, ConfigLayer, ConfigParseError, ConfigValue,
13 };
13 };
14 use crate::utils::files::get_bytes_from_path;
14 use crate::utils::files::get_bytes_from_path;
15 use format_bytes::{write_bytes, DisplayBytes};
15 use format_bytes::{write_bytes, DisplayBytes};
16 use std::env;
16 use std::env;
17 use std::path::{Path, PathBuf};
17 use std::path::{Path, PathBuf};
18
18
19 use crate::errors::{HgResultExt, IoResultExt};
19 use crate::errors::{HgResultExt, IoResultExt};
20
20
21 /// Holds the config values for the current repository
21 /// Holds the config values for the current repository
22 /// TODO update this docstring once we support more sources
22 /// TODO update this docstring once we support more sources
23 pub struct Config {
23 pub struct Config {
24 layers: Vec<layer::ConfigLayer>,
24 layers: Vec<layer::ConfigLayer>,
25 }
25 }
26
26
27 impl DisplayBytes for Config {
27 impl DisplayBytes for Config {
28 fn display_bytes(
28 fn display_bytes(
29 &self,
29 &self,
30 out: &mut dyn std::io::Write,
30 out: &mut dyn std::io::Write,
31 ) -> std::io::Result<()> {
31 ) -> std::io::Result<()> {
32 for (index, layer) in self.layers.iter().rev().enumerate() {
32 for (index, layer) in self.layers.iter().rev().enumerate() {
33 write_bytes!(
33 write_bytes!(
34 out,
34 out,
35 b"==== Layer {} (trusted: {}) ====\n{}",
35 b"==== Layer {} (trusted: {}) ====\n{}",
36 index,
36 index,
37 if layer.trusted {
37 if layer.trusted {
38 &b"yes"[..]
38 &b"yes"[..]
39 } else {
39 } else {
40 &b"no"[..]
40 &b"no"[..]
41 },
41 },
42 layer
42 layer
43 )?;
43 )?;
44 }
44 }
45 Ok(())
45 Ok(())
46 }
46 }
47 }
47 }
48
48
49 pub enum ConfigSource {
49 pub enum ConfigSource {
50 /// Absolute path to a config file
50 /// Absolute path to a config file
51 AbsPath(PathBuf),
51 AbsPath(PathBuf),
52 /// Already parsed (from the CLI, env, Python resources, etc.)
52 /// Already parsed (from the CLI, env, Python resources, etc.)
53 Parsed(layer::ConfigLayer),
53 Parsed(layer::ConfigLayer),
54 }
54 }
55
55
56 pub fn parse_bool(v: &[u8]) -> Option<bool> {
56 pub fn parse_bool(v: &[u8]) -> Option<bool> {
57 match v.to_ascii_lowercase().as_slice() {
57 match v.to_ascii_lowercase().as_slice() {
58 b"1" | b"yes" | b"true" | b"on" | b"always" => Some(true),
58 b"1" | b"yes" | b"true" | b"on" | b"always" => Some(true),
59 b"0" | b"no" | b"false" | b"off" | b"never" => Some(false),
59 b"0" | b"no" | b"false" | b"off" | b"never" => Some(false),
60 _ => None,
60 _ => None,
61 }
61 }
62 }
62 }
63
63
64 impl Config {
64 impl Config {
65 /// Load system and user configuration from various files.
65 /// Load system and user configuration from various files.
66 ///
66 ///
67 /// This is also affected by some environment variables.
67 /// This is also affected by some environment variables.
68 ///
68 pub fn load(
69 /// TODO: add a parameter for `--config` CLI arguments
69 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
70 pub fn load() -> Result<Self, ConfigError> {
70 ) -> Result<Self, ConfigError> {
71 let mut config = Self { layers: Vec::new() };
71 let mut config = Self { layers: Vec::new() };
72 let opt_rc_path = env::var_os("HGRCPATH");
72 let opt_rc_path = env::var_os("HGRCPATH");
73 // HGRCPATH replaces system config
73 // HGRCPATH replaces system config
74 if opt_rc_path.is_none() {
74 if opt_rc_path.is_none() {
75 config.add_system_config()?
75 config.add_system_config()?
76 }
76 }
77 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
77 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
78 config.add_for_environment_variable("VISUAL", b"ui", b"editor");
78 config.add_for_environment_variable("VISUAL", b"ui", b"editor");
79 config.add_for_environment_variable("PAGER", b"pager", b"pager");
79 config.add_for_environment_variable("PAGER", b"pager", b"pager");
80 // HGRCPATH replaces user config
80 // HGRCPATH replaces user config
81 if opt_rc_path.is_none() {
81 if opt_rc_path.is_none() {
82 config.add_user_config()?
82 config.add_user_config()?
83 }
83 }
84 if let Some(rc_path) = &opt_rc_path {
84 if let Some(rc_path) = &opt_rc_path {
85 for path in env::split_paths(rc_path) {
85 for path in env::split_paths(rc_path) {
86 if !path.as_os_str().is_empty() {
86 if !path.as_os_str().is_empty() {
87 if path.is_dir() {
87 if path.is_dir() {
88 config.add_trusted_dir(&path)?
88 config.add_trusted_dir(&path)?
89 } else {
89 } else {
90 config.add_trusted_file(&path)?
90 config.add_trusted_file(&path)?
91 }
91 }
92 }
92 }
93 }
93 }
94 }
94 }
95 if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? {
96 config.layers.push(layer)
97 }
95 Ok(config)
98 Ok(config)
96 }
99 }
97
100
98 fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
101 fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
99 if let Some(entries) = std::fs::read_dir(path)
102 if let Some(entries) = std::fs::read_dir(path)
100 .for_file(path)
103 .for_file(path)
101 .io_not_found_as_none()?
104 .io_not_found_as_none()?
102 {
105 {
103 for entry in entries {
106 for entry in entries {
104 let file_path = entry.for_file(path)?.path();
107 let file_path = entry.for_file(path)?.path();
105 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
108 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
106 self.add_trusted_file(&file_path)?
109 self.add_trusted_file(&file_path)?
107 }
110 }
108 }
111 }
109 }
112 }
110 Ok(())
113 Ok(())
111 }
114 }
112
115
113 fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
116 fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
114 if let Some(data) =
117 if let Some(data) =
115 std::fs::read(path).for_file(path).io_not_found_as_none()?
118 std::fs::read(path).for_file(path).io_not_found_as_none()?
116 {
119 {
117 self.layers.extend(ConfigLayer::parse(path, &data)?)
120 self.layers.extend(ConfigLayer::parse(path, &data)?)
118 }
121 }
119 Ok(())
122 Ok(())
120 }
123 }
121
124
122 fn add_for_environment_variable(
125 fn add_for_environment_variable(
123 &mut self,
126 &mut self,
124 var: &str,
127 var: &str,
125 section: &[u8],
128 section: &[u8],
126 key: &[u8],
129 key: &[u8],
127 ) {
130 ) {
128 if let Some(value) = env::var_os(var) {
131 if let Some(value) = env::var_os(var) {
129 let origin = layer::ConfigOrigin::Environment(var.into());
132 let origin = layer::ConfigOrigin::Environment(var.into());
130 let mut layer = ConfigLayer::new(origin);
133 let mut layer = ConfigLayer::new(origin);
131 layer.add(
134 layer.add(
132 section.to_owned(),
135 section.to_owned(),
133 key.to_owned(),
136 key.to_owned(),
134 // `value` is not a path but this works for any `OsStr`:
137 // `value` is not a path but this works for any `OsStr`:
135 get_bytes_from_path(value),
138 get_bytes_from_path(value),
136 None,
139 None,
137 );
140 );
138 self.layers.push(layer)
141 self.layers.push(layer)
139 }
142 }
140 }
143 }
141
144
142 #[cfg(unix)] // TODO: other platforms
145 #[cfg(unix)] // TODO: other platforms
143 fn add_system_config(&mut self) -> Result<(), ConfigError> {
146 fn add_system_config(&mut self) -> Result<(), ConfigError> {
144 let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
147 let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
145 let etc = prefix.join("etc").join("mercurial");
148 let etc = prefix.join("etc").join("mercurial");
146 self.add_trusted_file(&etc.join("hgrc"))?;
149 self.add_trusted_file(&etc.join("hgrc"))?;
147 self.add_trusted_dir(&etc.join("hgrc.d"))
150 self.add_trusted_dir(&etc.join("hgrc.d"))
148 };
151 };
149 let root = Path::new("/");
152 let root = Path::new("/");
150 // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
153 // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
151 // instead? TODO: can this be a relative path?
154 // instead? TODO: can this be a relative path?
152 let hg = crate::utils::current_exe()?;
155 let hg = crate::utils::current_exe()?;
153 // TODO: this order (per-installation then per-system) matches
156 // TODO: this order (per-installation then per-system) matches
154 // `systemrcpath()` in `mercurial/scmposix.py`, but
157 // `systemrcpath()` in `mercurial/scmposix.py`, but
155 // `mercurial/helptext/config.txt` suggests it should be reversed
158 // `mercurial/helptext/config.txt` suggests it should be reversed
156 if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
159 if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
157 if installation_prefix != root {
160 if installation_prefix != root {
158 add_for_prefix(&installation_prefix)?
161 add_for_prefix(&installation_prefix)?
159 }
162 }
160 }
163 }
161 add_for_prefix(root)?;
164 add_for_prefix(root)?;
162 Ok(())
165 Ok(())
163 }
166 }
164
167
165 #[cfg(unix)] // TODO: other plateforms
168 #[cfg(unix)] // TODO: other plateforms
166 fn add_user_config(&mut self) -> Result<(), ConfigError> {
169 fn add_user_config(&mut self) -> Result<(), ConfigError> {
167 let opt_home = home::home_dir();
170 let opt_home = home::home_dir();
168 if let Some(home) = &opt_home {
171 if let Some(home) = &opt_home {
169 self.add_trusted_file(&home.join(".hgrc"))?
172 self.add_trusted_file(&home.join(".hgrc"))?
170 }
173 }
171 let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
174 let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
172 if !darwin {
175 if !darwin {
173 if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
176 if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
174 .map(PathBuf::from)
177 .map(PathBuf::from)
175 .or_else(|| opt_home.map(|home| home.join(".config")))
178 .or_else(|| opt_home.map(|home| home.join(".config")))
176 {
179 {
177 self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
180 self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
178 }
181 }
179 }
182 }
180 Ok(())
183 Ok(())
181 }
184 }
182
185
183 /// Loads in order, which means that the precedence is the same
186 /// Loads in order, which means that the precedence is the same
184 /// as the order of `sources`.
187 /// as the order of `sources`.
185 pub fn load_from_explicit_sources(
188 pub fn load_from_explicit_sources(
186 sources: Vec<ConfigSource>,
189 sources: Vec<ConfigSource>,
187 ) -> Result<Self, ConfigError> {
190 ) -> Result<Self, ConfigError> {
188 let mut layers = vec![];
191 let mut layers = vec![];
189
192
190 for source in sources.into_iter() {
193 for source in sources.into_iter() {
191 match source {
194 match source {
192 ConfigSource::Parsed(c) => layers.push(c),
195 ConfigSource::Parsed(c) => layers.push(c),
193 ConfigSource::AbsPath(c) => {
196 ConfigSource::AbsPath(c) => {
194 // TODO check if it should be trusted
197 // TODO check if it should be trusted
195 // mercurial/ui.py:427
198 // mercurial/ui.py:427
196 let data = match std::fs::read(&c) {
199 let data = match std::fs::read(&c) {
197 Err(_) => continue, // same as the python code
200 Err(_) => continue, // same as the python code
198 Ok(data) => data,
201 Ok(data) => data,
199 };
202 };
200 layers.extend(ConfigLayer::parse(&c, &data)?)
203 layers.extend(ConfigLayer::parse(&c, &data)?)
201 }
204 }
202 }
205 }
203 }
206 }
204
207
205 Ok(Config { layers })
208 Ok(Config { layers })
206 }
209 }
207
210
208 /// Loads the per-repository config into a new `Config` which is combined
211 /// Loads the per-repository config into a new `Config` which is combined
209 /// with `self`.
212 /// with `self`.
210 pub(crate) fn combine_with_repo(
213 pub(crate) fn combine_with_repo(
211 &self,
214 &self,
212 repo_config_files: &[PathBuf],
215 repo_config_files: &[PathBuf],
213 ) -> Result<Self, ConfigError> {
216 ) -> Result<Self, ConfigError> {
214 let (cli_layers, other_layers) = self
217 let (cli_layers, other_layers) = self
215 .layers
218 .layers
216 .iter()
219 .iter()
217 .cloned()
220 .cloned()
218 .partition(ConfigLayer::is_from_command_line);
221 .partition(ConfigLayer::is_from_command_line);
219
222
220 let mut repo_config = Self {
223 let mut repo_config = Self {
221 layers: other_layers,
224 layers: other_layers,
222 };
225 };
223 for path in repo_config_files {
226 for path in repo_config_files {
224 // TODO: check if this file should be trusted:
227 // TODO: check if this file should be trusted:
225 // `mercurial/ui.py:427`
228 // `mercurial/ui.py:427`
226 repo_config.add_trusted_file(path)?;
229 repo_config.add_trusted_file(path)?;
227 }
230 }
228 repo_config.layers.extend(cli_layers);
231 repo_config.layers.extend(cli_layers);
229 Ok(repo_config)
232 Ok(repo_config)
230 }
233 }
231
234
232 /// Returns an `Err` if the first value found is not a valid boolean.
235 /// Returns an `Err` if the first value found is not a valid boolean.
233 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
236 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
234 /// found, or `None`.
237 /// found, or `None`.
235 pub fn get_option(
238 pub fn get_option(
236 &self,
239 &self,
237 section: &[u8],
240 section: &[u8],
238 item: &[u8],
241 item: &[u8],
239 ) -> Result<Option<bool>, ConfigParseError> {
242 ) -> Result<Option<bool>, ConfigParseError> {
240 match self.get_inner(&section, &item) {
243 match self.get_inner(&section, &item) {
241 Some((layer, v)) => match parse_bool(&v.bytes) {
244 Some((layer, v)) => match parse_bool(&v.bytes) {
242 Some(b) => Ok(Some(b)),
245 Some(b) => Ok(Some(b)),
243 None => Err(ConfigParseError {
246 None => Err(ConfigParseError {
244 origin: layer.origin.to_owned(),
247 origin: layer.origin.to_owned(),
245 line: v.line,
248 line: v.line,
246 bytes: v.bytes.to_owned(),
249 bytes: v.bytes.to_owned(),
247 }),
250 }),
248 },
251 },
249 None => Ok(None),
252 None => Ok(None),
250 }
253 }
251 }
254 }
252
255
253 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
256 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
254 /// if the value is not found, an `Err` if it's not a valid boolean.
257 /// if the value is not found, an `Err` if it's not a valid boolean.
255 pub fn get_bool(
258 pub fn get_bool(
256 &self,
259 &self,
257 section: &[u8],
260 section: &[u8],
258 item: &[u8],
261 item: &[u8],
259 ) -> Result<bool, ConfigError> {
262 ) -> Result<bool, ConfigError> {
260 Ok(self.get_option(section, item)?.unwrap_or(false))
263 Ok(self.get_option(section, item)?.unwrap_or(false))
261 }
264 }
262
265
263 /// Returns the raw value bytes of the first one found, or `None`.
266 /// Returns the raw value bytes of the first one found, or `None`.
264 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
267 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
265 self.get_inner(section, item)
268 self.get_inner(section, item)
266 .map(|(_, value)| value.bytes.as_ref())
269 .map(|(_, value)| value.bytes.as_ref())
267 }
270 }
268
271
269 /// Returns the layer and the value of the first one found, or `None`.
272 /// Returns the layer and the value of the first one found, or `None`.
270 fn get_inner(
273 fn get_inner(
271 &self,
274 &self,
272 section: &[u8],
275 section: &[u8],
273 item: &[u8],
276 item: &[u8],
274 ) -> Option<(&ConfigLayer, &ConfigValue)> {
277 ) -> Option<(&ConfigLayer, &ConfigValue)> {
275 for layer in self.layers.iter().rev() {
278 for layer in self.layers.iter().rev() {
276 if !layer.trusted {
279 if !layer.trusted {
277 continue;
280 continue;
278 }
281 }
279 if let Some(v) = layer.get(&section, &item) {
282 if let Some(v) = layer.get(&section, &item) {
280 return Some((&layer, v));
283 return Some((&layer, v));
281 }
284 }
282 }
285 }
283 None
286 None
284 }
287 }
285
288
286 /// Get raw values bytes from all layers (even untrusted ones) in order
289 /// Get raw values bytes from all layers (even untrusted ones) in order
287 /// of precedence.
290 /// of precedence.
288 #[cfg(test)]
291 #[cfg(test)]
289 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
292 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
290 let mut res = vec![];
293 let mut res = vec![];
291 for layer in self.layers.iter().rev() {
294 for layer in self.layers.iter().rev() {
292 if let Some(v) = layer.get(&section, &item) {
295 if let Some(v) = layer.get(&section, &item) {
293 res.push(v.bytes.as_ref());
296 res.push(v.bytes.as_ref());
294 }
297 }
295 }
298 }
296 res
299 res
297 }
300 }
298 }
301 }
299
302
300 #[cfg(test)]
303 #[cfg(test)]
301 mod tests {
304 mod tests {
302 use super::*;
305 use super::*;
303 use pretty_assertions::assert_eq;
306 use pretty_assertions::assert_eq;
304 use std::fs::File;
307 use std::fs::File;
305 use std::io::Write;
308 use std::io::Write;
306
309
307 #[test]
310 #[test]
308 fn test_include_layer_ordering() {
311 fn test_include_layer_ordering() {
309 let tmpdir = tempfile::tempdir().unwrap();
312 let tmpdir = tempfile::tempdir().unwrap();
310 let tmpdir_path = tmpdir.path();
313 let tmpdir_path = tmpdir.path();
311 let mut included_file =
314 let mut included_file =
312 File::create(&tmpdir_path.join("included.rc")).unwrap();
315 File::create(&tmpdir_path.join("included.rc")).unwrap();
313
316
314 included_file.write_all(b"[section]\nitem=value1").unwrap();
317 included_file.write_all(b"[section]\nitem=value1").unwrap();
315 let base_config_path = tmpdir_path.join("base.rc");
318 let base_config_path = tmpdir_path.join("base.rc");
316 let mut config_file = File::create(&base_config_path).unwrap();
319 let mut config_file = File::create(&base_config_path).unwrap();
317 let data =
320 let data =
318 b"[section]\nitem=value0\n%include included.rc\nitem=value2";
321 b"[section]\nitem=value0\n%include included.rc\nitem=value2";
319 config_file.write_all(data).unwrap();
322 config_file.write_all(data).unwrap();
320
323
321 let sources = vec![ConfigSource::AbsPath(base_config_path)];
324 let sources = vec![ConfigSource::AbsPath(base_config_path)];
322 let config = Config::load_from_explicit_sources(sources)
325 let config = Config::load_from_explicit_sources(sources)
323 .expect("expected valid config");
326 .expect("expected valid config");
324
327
325 let (_, value) = config.get_inner(b"section", b"item").unwrap();
328 let (_, value) = config.get_inner(b"section", b"item").unwrap();
326 assert_eq!(
329 assert_eq!(
327 value,
330 value,
328 &ConfigValue {
331 &ConfigValue {
329 bytes: b"value2".to_vec(),
332 bytes: b"value2".to_vec(),
330 line: Some(4)
333 line: Some(4)
331 }
334 }
332 );
335 );
333
336
334 let value = config.get(b"section", b"item").unwrap();
337 let value = config.get(b"section", b"item").unwrap();
335 assert_eq!(value, b"value2",);
338 assert_eq!(value, b"value2",);
336 assert_eq!(
339 assert_eq!(
337 config.get_all(b"section", b"item"),
340 config.get_all(b"section", b"item"),
338 [b"value2", b"value1", b"value0"]
341 [b"value2", b"value1", b"value0"]
339 );
342 );
340 }
343 }
341 }
344 }
@@ -1,255 +1,298 b''
1 // layer.rs
1 // layer.rs
2 //
2 //
3 // Copyright 2020
3 // Copyright 2020
4 // Valentin Gatien-Baron,
4 // Valentin Gatien-Baron,
5 // Raphaël Gomès <rgomes@octobus.net>
5 // Raphaël Gomès <rgomes@octobus.net>
6 //
6 //
7 // This software may be used and distributed according to the terms of the
7 // This software may be used and distributed according to the terms of the
8 // GNU General Public License version 2 or any later version.
8 // GNU General Public License version 2 or any later version.
9
9
10 use crate::errors::{HgError, IoResultExt};
10 use crate::errors::{HgError, IoResultExt};
11 use crate::utils::files::{get_bytes_from_path, get_path_from_bytes};
11 use crate::utils::files::{get_bytes_from_path, get_path_from_bytes};
12 use format_bytes::{write_bytes, DisplayBytes};
12 use format_bytes::{write_bytes, DisplayBytes};
13 use lazy_static::lazy_static;
13 use lazy_static::lazy_static;
14 use regex::bytes::Regex;
14 use regex::bytes::Regex;
15 use std::collections::HashMap;
15 use std::collections::HashMap;
16 use std::path::{Path, PathBuf};
16 use std::path::{Path, PathBuf};
17
17
18 lazy_static! {
18 lazy_static! {
19 static ref SECTION_RE: Regex = make_regex(r"^\[([^\[]+)\]");
19 static ref SECTION_RE: Regex = make_regex(r"^\[([^\[]+)\]");
20 static ref ITEM_RE: Regex = make_regex(r"^([^=\s][^=]*?)\s*=\s*((.*\S)?)");
20 static ref ITEM_RE: Regex = make_regex(r"^([^=\s][^=]*?)\s*=\s*((.*\S)?)");
21 /// Continuation whitespace
21 /// Continuation whitespace
22 static ref CONT_RE: Regex = make_regex(r"^\s+(\S|\S.*\S)\s*$");
22 static ref CONT_RE: Regex = make_regex(r"^\s+(\S|\S.*\S)\s*$");
23 static ref EMPTY_RE: Regex = make_regex(r"^(;|#|\s*$)");
23 static ref EMPTY_RE: Regex = make_regex(r"^(;|#|\s*$)");
24 static ref COMMENT_RE: Regex = make_regex(r"^(;|#)");
24 static ref COMMENT_RE: Regex = make_regex(r"^(;|#)");
25 /// A directive that allows for removing previous entries
25 /// A directive that allows for removing previous entries
26 static ref UNSET_RE: Regex = make_regex(r"^%unset\s+(\S+)");
26 static ref UNSET_RE: Regex = make_regex(r"^%unset\s+(\S+)");
27 /// A directive that allows for including other config files
27 /// A directive that allows for including other config files
28 static ref INCLUDE_RE: Regex = make_regex(r"^%include\s+(\S|\S.*\S)\s*$");
28 static ref INCLUDE_RE: Regex = make_regex(r"^%include\s+(\S|\S.*\S)\s*$");
29 }
29 }
30
30
31 /// All config values separated by layers of precedence.
31 /// All config values separated by layers of precedence.
32 /// Each config source may be split in multiple layers if `%include` directives
32 /// Each config source may be split in multiple layers if `%include` directives
33 /// are used.
33 /// are used.
34 /// TODO detail the general precedence
34 /// TODO detail the general precedence
35 #[derive(Clone)]
35 #[derive(Clone)]
36 pub struct ConfigLayer {
36 pub struct ConfigLayer {
37 /// Mapping of the sections to their items
37 /// Mapping of the sections to their items
38 sections: HashMap<Vec<u8>, ConfigItem>,
38 sections: HashMap<Vec<u8>, ConfigItem>,
39 /// All sections (and their items/values) in a layer share the same origin
39 /// All sections (and their items/values) in a layer share the same origin
40 pub origin: ConfigOrigin,
40 pub origin: ConfigOrigin,
41 /// Whether this layer comes from a trusted user or group
41 /// Whether this layer comes from a trusted user or group
42 pub trusted: bool,
42 pub trusted: bool,
43 }
43 }
44
44
45 impl ConfigLayer {
45 impl ConfigLayer {
46 pub fn new(origin: ConfigOrigin) -> Self {
46 pub fn new(origin: ConfigOrigin) -> Self {
47 ConfigLayer {
47 ConfigLayer {
48 sections: HashMap::new(),
48 sections: HashMap::new(),
49 trusted: true, // TODO check
49 trusted: true, // TODO check
50 origin,
50 origin,
51 }
51 }
52 }
52 }
53
53
54 /// Parse `--config` CLI arguments and return a layer if there’s any
55 pub(crate) fn parse_cli_args(
56 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
57 ) -> Result<Option<Self>, ConfigError> {
58 fn parse_one(arg: &[u8]) -> Option<(Vec<u8>, Vec<u8>, Vec<u8>)> {
59 use crate::utils::SliceExt;
60
61 let (section_and_item, value) = split_2(arg, b'=')?;
62 let (section, item) = split_2(section_and_item.trim(), b'.')?;
63 Some((
64 section.to_owned(),
65 item.to_owned(),
66 value.trim().to_owned(),
67 ))
68 }
69
70 fn split_2(bytes: &[u8], separator: u8) -> Option<(&[u8], &[u8])> {
71 let mut iter = bytes.splitn(2, |&byte| byte == separator);
72 let a = iter.next()?;
73 let b = iter.next()?;
74 Some((a, b))
75 }
76
77 let mut layer = Self::new(ConfigOrigin::CommandLine);
78 for arg in cli_config_args {
79 let arg = arg.as_ref();
80 if let Some((section, item, value)) = parse_one(arg) {
81 layer.add(section, item, value, None);
82 } else {
83 Err(HgError::abort(format!(
84 "malformed --config option: \"{}\" \
85 (use --config section.name=value)",
86 String::from_utf8_lossy(arg),
87 )))?
88 }
89 }
90 if layer.sections.is_empty() {
91 Ok(None)
92 } else {
93 Ok(Some(layer))
94 }
95 }
96
54 /// Returns whether this layer comes from `--config` CLI arguments
97 /// Returns whether this layer comes from `--config` CLI arguments
55 pub(crate) fn is_from_command_line(&self) -> bool {
98 pub(crate) fn is_from_command_line(&self) -> bool {
56 if let ConfigOrigin::CommandLine = self.origin {
99 if let ConfigOrigin::CommandLine = self.origin {
57 true
100 true
58 } else {
101 } else {
59 false
102 false
60 }
103 }
61 }
104 }
62
105
63 /// Add an entry to the config, overwriting the old one if already present.
106 /// Add an entry to the config, overwriting the old one if already present.
64 pub fn add(
107 pub fn add(
65 &mut self,
108 &mut self,
66 section: Vec<u8>,
109 section: Vec<u8>,
67 item: Vec<u8>,
110 item: Vec<u8>,
68 value: Vec<u8>,
111 value: Vec<u8>,
69 line: Option<usize>,
112 line: Option<usize>,
70 ) {
113 ) {
71 self.sections
114 self.sections
72 .entry(section)
115 .entry(section)
73 .or_insert_with(|| HashMap::new())
116 .or_insert_with(|| HashMap::new())
74 .insert(item, ConfigValue { bytes: value, line });
117 .insert(item, ConfigValue { bytes: value, line });
75 }
118 }
76
119
77 /// Returns the config value in `<section>.<item>` if it exists
120 /// Returns the config value in `<section>.<item>` if it exists
78 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&ConfigValue> {
121 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&ConfigValue> {
79 Some(self.sections.get(section)?.get(item)?)
122 Some(self.sections.get(section)?.get(item)?)
80 }
123 }
81
124
82 pub fn is_empty(&self) -> bool {
125 pub fn is_empty(&self) -> bool {
83 self.sections.is_empty()
126 self.sections.is_empty()
84 }
127 }
85
128
86 /// Returns a `Vec` of layers in order of precedence (so, in read order),
129 /// Returns a `Vec` of layers in order of precedence (so, in read order),
87 /// recursively parsing the `%include` directives if any.
130 /// recursively parsing the `%include` directives if any.
88 pub fn parse(src: &Path, data: &[u8]) -> Result<Vec<Self>, ConfigError> {
131 pub fn parse(src: &Path, data: &[u8]) -> Result<Vec<Self>, ConfigError> {
89 let mut layers = vec![];
132 let mut layers = vec![];
90
133
91 // Discard byte order mark if any
134 // Discard byte order mark if any
92 let data = if data.starts_with(b"\xef\xbb\xbf") {
135 let data = if data.starts_with(b"\xef\xbb\xbf") {
93 &data[3..]
136 &data[3..]
94 } else {
137 } else {
95 data
138 data
96 };
139 };
97
140
98 // TODO check if it's trusted
141 // TODO check if it's trusted
99 let mut current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
142 let mut current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
100
143
101 let mut lines_iter =
144 let mut lines_iter =
102 data.split(|b| *b == b'\n').enumerate().peekable();
145 data.split(|b| *b == b'\n').enumerate().peekable();
103 let mut section = b"".to_vec();
146 let mut section = b"".to_vec();
104
147
105 while let Some((index, bytes)) = lines_iter.next() {
148 while let Some((index, bytes)) = lines_iter.next() {
106 if let Some(m) = INCLUDE_RE.captures(&bytes) {
149 if let Some(m) = INCLUDE_RE.captures(&bytes) {
107 let filename_bytes = &m[1];
150 let filename_bytes = &m[1];
108 // `Path::parent` only fails for the root directory,
151 // `Path::parent` only fails for the root directory,
109 // which `src` can’t be since we’ve managed to open it as a
152 // which `src` can’t be since we’ve managed to open it as a
110 // file.
153 // file.
111 let dir = src
154 let dir = src
112 .parent()
155 .parent()
113 .expect("Path::parent fail on a file we’ve read");
156 .expect("Path::parent fail on a file we’ve read");
114 // `Path::join` with an absolute argument correctly ignores the
157 // `Path::join` with an absolute argument correctly ignores the
115 // base path
158 // base path
116 let filename = dir.join(&get_path_from_bytes(&filename_bytes));
159 let filename = dir.join(&get_path_from_bytes(&filename_bytes));
117 let data = std::fs::read(&filename).for_file(&filename)?;
160 let data = std::fs::read(&filename).for_file(&filename)?;
118 layers.push(current_layer);
161 layers.push(current_layer);
119 layers.extend(Self::parse(&filename, &data)?);
162 layers.extend(Self::parse(&filename, &data)?);
120 current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
163 current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
121 } else if let Some(_) = EMPTY_RE.captures(&bytes) {
164 } else if let Some(_) = EMPTY_RE.captures(&bytes) {
122 } else if let Some(m) = SECTION_RE.captures(&bytes) {
165 } else if let Some(m) = SECTION_RE.captures(&bytes) {
123 section = m[1].to_vec();
166 section = m[1].to_vec();
124 } else if let Some(m) = ITEM_RE.captures(&bytes) {
167 } else if let Some(m) = ITEM_RE.captures(&bytes) {
125 let item = m[1].to_vec();
168 let item = m[1].to_vec();
126 let mut value = m[2].to_vec();
169 let mut value = m[2].to_vec();
127 loop {
170 loop {
128 match lines_iter.peek() {
171 match lines_iter.peek() {
129 None => break,
172 None => break,
130 Some((_, v)) => {
173 Some((_, v)) => {
131 if let Some(_) = COMMENT_RE.captures(&v) {
174 if let Some(_) = COMMENT_RE.captures(&v) {
132 } else if let Some(_) = CONT_RE.captures(&v) {
175 } else if let Some(_) = CONT_RE.captures(&v) {
133 value.extend(b"\n");
176 value.extend(b"\n");
134 value.extend(&m[1]);
177 value.extend(&m[1]);
135 } else {
178 } else {
136 break;
179 break;
137 }
180 }
138 }
181 }
139 };
182 };
140 lines_iter.next();
183 lines_iter.next();
141 }
184 }
142 current_layer.add(
185 current_layer.add(
143 section.clone(),
186 section.clone(),
144 item,
187 item,
145 value,
188 value,
146 Some(index + 1),
189 Some(index + 1),
147 );
190 );
148 } else if let Some(m) = UNSET_RE.captures(&bytes) {
191 } else if let Some(m) = UNSET_RE.captures(&bytes) {
149 if let Some(map) = current_layer.sections.get_mut(&section) {
192 if let Some(map) = current_layer.sections.get_mut(&section) {
150 map.remove(&m[1]);
193 map.remove(&m[1]);
151 }
194 }
152 } else {
195 } else {
153 return Err(ConfigParseError {
196 return Err(ConfigParseError {
154 origin: ConfigOrigin::File(src.to_owned()),
197 origin: ConfigOrigin::File(src.to_owned()),
155 line: Some(index + 1),
198 line: Some(index + 1),
156 bytes: bytes.to_owned(),
199 bytes: bytes.to_owned(),
157 }
200 }
158 .into());
201 .into());
159 }
202 }
160 }
203 }
161 if !current_layer.is_empty() {
204 if !current_layer.is_empty() {
162 layers.push(current_layer);
205 layers.push(current_layer);
163 }
206 }
164 Ok(layers)
207 Ok(layers)
165 }
208 }
166 }
209 }
167
210
168 impl DisplayBytes for ConfigLayer {
211 impl DisplayBytes for ConfigLayer {
169 fn display_bytes(
212 fn display_bytes(
170 &self,
213 &self,
171 out: &mut dyn std::io::Write,
214 out: &mut dyn std::io::Write,
172 ) -> std::io::Result<()> {
215 ) -> std::io::Result<()> {
173 let mut sections: Vec<_> = self.sections.iter().collect();
216 let mut sections: Vec<_> = self.sections.iter().collect();
174 sections.sort_by(|e0, e1| e0.0.cmp(e1.0));
217 sections.sort_by(|e0, e1| e0.0.cmp(e1.0));
175
218
176 for (section, items) in sections.into_iter() {
219 for (section, items) in sections.into_iter() {
177 let mut items: Vec<_> = items.into_iter().collect();
220 let mut items: Vec<_> = items.into_iter().collect();
178 items.sort_by(|e0, e1| e0.0.cmp(e1.0));
221 items.sort_by(|e0, e1| e0.0.cmp(e1.0));
179
222
180 for (item, config_entry) in items {
223 for (item, config_entry) in items {
181 write_bytes!(
224 write_bytes!(
182 out,
225 out,
183 b"{}.{}={} # {}\n",
226 b"{}.{}={} # {}\n",
184 section,
227 section,
185 item,
228 item,
186 &config_entry.bytes,
229 &config_entry.bytes,
187 &self.origin,
230 &self.origin,
188 )?
231 )?
189 }
232 }
190 }
233 }
191 Ok(())
234 Ok(())
192 }
235 }
193 }
236 }
194
237
195 /// Mapping of section item to value.
238 /// Mapping of section item to value.
196 /// In the following:
239 /// In the following:
197 /// ```text
240 /// ```text
198 /// [ui]
241 /// [ui]
199 /// paginate=no
242 /// paginate=no
200 /// ```
243 /// ```
201 /// "paginate" is the section item and "no" the value.
244 /// "paginate" is the section item and "no" the value.
202 pub type ConfigItem = HashMap<Vec<u8>, ConfigValue>;
245 pub type ConfigItem = HashMap<Vec<u8>, ConfigValue>;
203
246
204 #[derive(Clone, Debug, PartialEq)]
247 #[derive(Clone, Debug, PartialEq)]
205 pub struct ConfigValue {
248 pub struct ConfigValue {
206 /// The raw bytes of the value (be it from the CLI, env or from a file)
249 /// The raw bytes of the value (be it from the CLI, env or from a file)
207 pub bytes: Vec<u8>,
250 pub bytes: Vec<u8>,
208 /// Only present if the value comes from a file, 1-indexed.
251 /// Only present if the value comes from a file, 1-indexed.
209 pub line: Option<usize>,
252 pub line: Option<usize>,
210 }
253 }
211
254
212 #[derive(Clone, Debug)]
255 #[derive(Clone, Debug)]
213 pub enum ConfigOrigin {
256 pub enum ConfigOrigin {
214 /// From a configuration file
257 /// From a configuration file
215 File(PathBuf),
258 File(PathBuf),
216 /// From a `--config` CLI argument
259 /// From a `--config` CLI argument
217 CommandLine,
260 CommandLine,
218 /// From environment variables like `$PAGER` or `$EDITOR`
261 /// From environment variables like `$PAGER` or `$EDITOR`
219 Environment(Vec<u8>),
262 Environment(Vec<u8>),
220 /* TODO cli
263 /* TODO cli
221 * TODO defaults (configitems.py)
264 * TODO defaults (configitems.py)
222 * TODO extensions
265 * TODO extensions
223 * TODO Python resources?
266 * TODO Python resources?
224 * Others? */
267 * Others? */
225 }
268 }
226
269
227 impl DisplayBytes for ConfigOrigin {
270 impl DisplayBytes for ConfigOrigin {
228 fn display_bytes(
271 fn display_bytes(
229 &self,
272 &self,
230 out: &mut dyn std::io::Write,
273 out: &mut dyn std::io::Write,
231 ) -> std::io::Result<()> {
274 ) -> std::io::Result<()> {
232 match self {
275 match self {
233 ConfigOrigin::File(p) => out.write_all(&get_bytes_from_path(p)),
276 ConfigOrigin::File(p) => out.write_all(&get_bytes_from_path(p)),
234 ConfigOrigin::CommandLine => out.write_all(b"--config"),
277 ConfigOrigin::CommandLine => out.write_all(b"--config"),
235 ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e),
278 ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e),
236 }
279 }
237 }
280 }
238 }
281 }
239
282
240 #[derive(Debug)]
283 #[derive(Debug)]
241 pub struct ConfigParseError {
284 pub struct ConfigParseError {
242 pub origin: ConfigOrigin,
285 pub origin: ConfigOrigin,
243 pub line: Option<usize>,
286 pub line: Option<usize>,
244 pub bytes: Vec<u8>,
287 pub bytes: Vec<u8>,
245 }
288 }
246
289
247 #[derive(Debug, derive_more::From)]
290 #[derive(Debug, derive_more::From)]
248 pub enum ConfigError {
291 pub enum ConfigError {
249 Parse(ConfigParseError),
292 Parse(ConfigParseError),
250 Other(HgError),
293 Other(HgError),
251 }
294 }
252
295
253 fn make_regex(pattern: &'static str) -> Regex {
296 fn make_regex(pattern: &'static str) -> Regex {
254 Regex::new(pattern).expect("expected a valid regex")
297 Regex::new(pattern).expect("expected a valid regex")
255 }
298 }
@@ -1,116 +1,137 b''
1 extern crate log;
1 extern crate log;
2 use clap::App;
2 use clap::App;
3 use clap::AppSettings;
3 use clap::AppSettings;
4 use clap::Arg;
4 use clap::Arg;
5 use clap::ArgMatches;
5 use clap::ArgMatches;
6 use format_bytes::format_bytes;
6 use format_bytes::format_bytes;
7 use std::path::Path;
7 use std::path::Path;
8
8
9 mod error;
9 mod error;
10 mod exitcode;
10 mod exitcode;
11 mod ui;
11 mod ui;
12 use error::CommandError;
12 use error::CommandError;
13
13
14 fn add_global_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
14 fn add_global_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
15 app.arg(
15 app.arg(
16 Arg::with_name("repository")
16 Arg::with_name("repository")
17 .help("repository root directory")
17 .help("repository root directory")
18 .short("-R")
18 .short("-R")
19 .long("--repository")
19 .long("--repository")
20 .value_name("REPO")
20 .value_name("REPO")
21 .takes_value(true),
21 .takes_value(true),
22 )
22 )
23 .arg(
24 Arg::with_name("config")
25 .help("set/override config option (use 'section.name=value')")
26 .long("--config")
27 .value_name("CONFIG")
28 .takes_value(true)
29 // Ok: `--config section.key1=val --config section.key2=val2`
30 .multiple(true)
31 // Not ok: `--config section.key1=val section.key2=val2`
32 .number_of_values(1),
33 )
23 }
34 }
24
35
25 fn main() {
36 fn main() {
26 env_logger::init();
37 env_logger::init();
27 let app = App::new("rhg")
38 let app = App::new("rhg")
28 .setting(AppSettings::AllowInvalidUtf8)
39 .setting(AppSettings::AllowInvalidUtf8)
29 .setting(AppSettings::SubcommandRequired)
40 .setting(AppSettings::SubcommandRequired)
30 .setting(AppSettings::VersionlessSubcommands)
41 .setting(AppSettings::VersionlessSubcommands)
31 .version("0.0.1");
42 .version("0.0.1");
32 let app = add_global_args(app);
43 let app = add_global_args(app);
33 let app = add_subcommand_args(app);
44 let app = add_subcommand_args(app);
34
45
35 let ui = ui::Ui::new();
46 let ui = ui::Ui::new();
36
47
37 let matches = app.clone().get_matches_safe().unwrap_or_else(|err| {
48 let matches = app.clone().get_matches_safe().unwrap_or_else(|err| {
38 let _ = ui.writeln_stderr_str(&err.message);
49 let _ = ui.writeln_stderr_str(&err.message);
39 std::process::exit(exitcode::UNIMPLEMENTED)
50 std::process::exit(exitcode::UNIMPLEMENTED)
40 });
51 });
41
52
42 let (subcommand_name, subcommand_matches) = matches.subcommand();
53 let (subcommand_name, subcommand_matches) = matches.subcommand();
43 let run = subcommand_run_fn(subcommand_name)
54 let run = subcommand_run_fn(subcommand_name)
44 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
55 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
45 let args = subcommand_matches
56 let args = subcommand_matches
46 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
57 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
47
58
48 // Global arguments can be in either based on e.g. `hg -R ./foo log` v.s.
59 // Global arguments can be in either based on e.g. `hg -R ./foo log` v.s.
49 // `hg log -R ./foo`
60 // `hg log -R ./foo`
50 let global_arg =
61 let value_of_global_arg =
51 |name| args.value_of_os(name).or_else(|| matches.value_of_os(name));
62 |name| args.value_of_os(name).or_else(|| matches.value_of_os(name));
63 // For arguments where multiple occurences are allowed, return a
64 // possibly-iterator of all values.
65 let values_of_global_arg = |name: &str| {
66 let a = matches.values_of_os(name).into_iter().flatten();
67 let b = args.values_of_os(name).into_iter().flatten();
68 a.chain(b)
69 };
52
70
53 let repo_path = global_arg("repository").map(Path::new);
71 let repo_path = value_of_global_arg("repository").map(Path::new);
54 let result = (|| -> Result<(), CommandError> {
72 let result = (|| -> Result<(), CommandError> {
55 let config = hg::config::Config::load()?;
73 let config_args = values_of_global_arg("config")
74 // `get_bytes_from_path` works for OsStr the same as for Path
75 .map(hg::utils::files::get_bytes_from_path);
76 let config = hg::config::Config::load(config_args)?;
56 run(&ui, &config, repo_path, args)
77 run(&ui, &config, repo_path, args)
57 })();
78 })();
58
79
59 let exit_code = match result {
80 let exit_code = match result {
60 Ok(_) => exitcode::OK,
81 Ok(_) => exitcode::OK,
61
82
62 // Exit with a specific code and no error message to let a potential
83 // Exit with a specific code and no error message to let a potential
63 // wrapper script fallback to Python-based Mercurial.
84 // wrapper script fallback to Python-based Mercurial.
64 Err(CommandError::Unimplemented) => exitcode::UNIMPLEMENTED,
85 Err(CommandError::Unimplemented) => exitcode::UNIMPLEMENTED,
65
86
66 Err(CommandError::Abort { message }) => {
87 Err(CommandError::Abort { message }) => {
67 if !message.is_empty() {
88 if !message.is_empty() {
68 // Ignore errors when writing to stderr, we’re already exiting
89 // Ignore errors when writing to stderr, we’re already exiting
69 // with failure code so there’s not much more we can do.
90 // with failure code so there’s not much more we can do.
70 let _ =
91 let _ =
71 ui.write_stderr(&format_bytes!(b"abort: {}\n", message));
92 ui.write_stderr(&format_bytes!(b"abort: {}\n", message));
72 }
93 }
73 exitcode::ABORT
94 exitcode::ABORT
74 }
95 }
75 };
96 };
76 std::process::exit(exit_code)
97 std::process::exit(exit_code)
77 }
98 }
78
99
79 macro_rules! subcommands {
100 macro_rules! subcommands {
80 ($( $command: ident )+) => {
101 ($( $command: ident )+) => {
81 mod commands {
102 mod commands {
82 $(
103 $(
83 pub mod $command;
104 pub mod $command;
84 )+
105 )+
85 }
106 }
86
107
87 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
108 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
88 app
109 app
89 $(
110 $(
90 .subcommand(add_global_args(commands::$command::args()))
111 .subcommand(add_global_args(commands::$command::args()))
91 )+
112 )+
92 }
113 }
93
114
94 fn subcommand_run_fn(name: &str) -> Option<fn(
115 fn subcommand_run_fn(name: &str) -> Option<fn(
95 &ui::Ui,
116 &ui::Ui,
96 &hg::config::Config,
117 &hg::config::Config,
97 Option<&Path>,
118 Option<&Path>,
98 &ArgMatches,
119 &ArgMatches,
99 ) -> Result<(), CommandError>> {
120 ) -> Result<(), CommandError>> {
100 match name {
121 match name {
101 $(
122 $(
102 stringify!($command) => Some(commands::$command::run),
123 stringify!($command) => Some(commands::$command::run),
103 )+
124 )+
104 _ => None,
125 _ => None,
105 }
126 }
106 }
127 }
107 };
128 };
108 }
129 }
109
130
110 subcommands! {
131 subcommands! {
111 cat
132 cat
112 debugdata
133 debugdata
113 debugrequirements
134 debugrequirements
114 files
135 files
115 root
136 root
116 }
137 }
General Comments 0
You need to be logged in to leave comments. Login now