use std::collections::HashMap;
use winnow::ascii::escaped;
use winnow::ascii::space1;
use winnow::combinator::alt;
use winnow::combinator::delimited;
use winnow::combinator::opt;
use winnow::combinator::preceded;
use winnow::combinator::separated;
use winnow::error::StrContext;
use winnow::prelude::*;
use winnow::token::take_till;
use winnow::token::take_until;
#[derive(Debug, PartialEq, Eq)]
pub(super) enum Directive {
WS,
Source(String),
SourceAutoEnv,
#[doc(hidden)]
SourceAutoPath,
NoWarnMultipleKeyMatches,
Ignore(Matcher),
Transform(Matcher, String, HashMap<String, String>),
Set {
section: String,
key: String,
value: String,
separator: Option<String>,
},
Remove(Matcher),
AddRemove(Matcher),
AddHide(Matcher),
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum Matcher {
Section(String),
SectionRegex(String),
Literal(String, String),
Regex(String, String),
}
pub(super) fn parse_config(i: &mut &str) -> ModalResult<Vec<Directive>> {
let alternatives = (
alt((
comment.context(StrContext::Label("comment")),
chezmoi_template.context(StrContext::Label("chezmoi template")),
source.context(StrContext::Label("source")),
no_warn_multiple_key_matches.context(StrContext::Label("no-warn-multiple-key-matches")),
ignore.context(StrContext::Label("ignore")),
transform.context(StrContext::Label("transform")),
)),
alt((
set.context(StrContext::Label("set")),
remove.context(StrContext::Label("remove")),
add_remove.context(StrContext::Label("add:remove")),
add_hide.context(StrContext::Label("add:hide")),
"".map(|_| Directive::WS)
.context(StrContext::Label("whitespace")), )),
);
(separated(0.., alt(alternatives), newline), opt(newline))
.map(|(val, _)| val)
.parse_next(i)
}
fn newline(i: &mut &str) -> ModalResult<()> {
alt(("\r\n", "\n", "\r")).void().parse_next(i)
}
fn comment(i: &mut &str) -> ModalResult<Directive> {
('#', take_till(0.., ['\n', '\r']))
.void()
.map(|()| Directive::WS)
.parse_next(i)
}
fn chezmoi_template(i: &mut &str) -> ModalResult<Directive> {
delimited("{{", take_until(0.., "}}"), "}}")
.void()
.map(|()| Directive::WS)
.parse_next(i)
}
fn source(i: &mut &str) -> ModalResult<Directive> {
(
"source",
space1,
alt((
"auto-path".map(|_| Directive::SourceAutoPath),
"auto".map(|_| Directive::SourceAutoEnv),
quoted_string_nl.map(Directive::Source),
)),
)
.map(|(_, _, result)| result)
.parse_next(i)
}
fn no_warn_multiple_key_matches(i: &mut &str) -> ModalResult<Directive> {
"no-warn-multiple-key-matches"
.map(|_| Directive::NoWarnMultipleKeyMatches)
.parse_next(i)
}
fn ignore(i: &mut &str) -> ModalResult<Directive> {
("ignore", space1, matcher)
.map(|(_, _, pattern)| Directive::Ignore(pattern))
.parse_next(i)
}
fn set(i: &mut &str) -> ModalResult<Directive> {
(
"set",
space1,
quoted_string,
space1,
quoted_string,
space1,
quoted_string,
opt((space1, "separator=", quoted_string).map(|(_, _, v)| v)),
)
.map(
|(_, _, section, _, key, _, value, separator)| Directive::Set {
section,
key,
value,
separator,
},
)
.parse_next(i)
}
fn remove(i: &mut &str) -> ModalResult<Directive> {
("remove", space1, matcher)
.map(|(_, _, matcher)| Directive::Remove(matcher))
.parse_next(i)
}
fn add_remove(i: &mut &str) -> ModalResult<Directive> {
("add:remove", space1, matcher)
.map(|(_, _, matcher)| Directive::AddRemove(matcher))
.parse_next(i)
}
fn add_hide(i: &mut &str) -> ModalResult<Directive> {
("add:hide", space1, matcher)
.map(|(_, _, matcher)| Directive::AddHide(matcher))
.parse_next(i)
}
fn transform(i: &mut &str) -> ModalResult<Directive> {
(
"transform",
space1,
matcher_transform,
space1,
take_till(1.., [' ', '\r', '\n']),
opt(preceded(space1, separated(0.., transform_arg, space1))),
)
.map(|(_, _, pattern, _, transform, args)| {
Directive::Transform(pattern, transform.to_owned(), args.unwrap_or_default())
})
.parse_next(i)
}
fn transform_arg(i: &mut &str) -> ModalResult<(String, String)> {
(take_till(1.., [' ', '=']), '=', quoted_string)
.map(|(key, _, value)| (key.to_owned(), value))
.parse_next(i)
}
fn match_section_regex(i: &mut &str) -> ModalResult<Matcher> {
("section", space1, "regex", space1, quoted_string)
.map(|(_, _, _, _, section)| Matcher::SectionRegex(section))
.parse_next(i)
}
fn match_section(i: &mut &str) -> ModalResult<Matcher> {
("section", space1, quoted_string)
.map(|(_, _, section)| Matcher::Section(section))
.parse_next(i)
}
fn match_regex(i: &mut &str) -> ModalResult<Matcher> {
("regex", space1, quoted_string, space1, quoted_string)
.map(|(_, _, section, _, key)| Matcher::Regex(section, key))
.parse_next(i)
}
fn match_literal(i: &mut &str) -> ModalResult<Matcher> {
(quoted_string, space1, quoted_string)
.map(|(section, _, key)| Matcher::Literal(section, key))
.parse_next(i)
}
fn matcher(i: &mut &str) -> ModalResult<Matcher> {
alt((
match_section_regex,
match_section,
match_regex,
match_literal,
))
.parse_next(i)
}
fn matcher_transform(i: &mut &str) -> ModalResult<Matcher> {
alt((match_regex, match_literal)).parse_next(i)
}
fn quoted_string(i: &mut &str) -> ModalResult<String> {
delimited(
'"',
escaped(
take_till(1.., ['"', '\\']),
'\\',
alt(("\\".value("\\"), "\"".value("\""), "n".value("\n"))),
),
'"',
)
.parse_next(i)
}
fn quoted_string_nl(i: &mut &str) -> ModalResult<String> {
delimited(
'"',
escaped(
take_till(1.., ['\n', '\r', '\\']),
'\\',
alt(("\\".value("\\"), "\"".value("\""), "n".value("\n"))),
),
alt(('\n', '\r')),
)
.map(|mut v: String| {
while v.ends_with(['\r', '\n', ' ', '\t']) {
v.pop();
}
if v.ends_with('"') {
v.pop();
}
v
})
.parse_next(i)
}
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn check_quoted_string() {
let (rem, out) = quoted_string.parse_peek("\"test \\\" \\\\input\"").unwrap();
assert_eq!(rem, "");
assert_eq!(out, "test \" \\input");
let res = quoted_string.parse_peek("\"invalid");
assert!(res.is_err());
}
#[test]
fn check_quoted_string_nl() {
let (rem, out) = quoted_string_nl
.parse_peek("\"test \\\" \\\\input\"\n")
.unwrap();
assert_eq!(rem, "");
assert_eq!(out, "test \" \\input");
let (rem, out) = quoted_string_nl.parse_peek("\"a \" b\"\n").unwrap();
assert_eq!(rem, "");
assert_eq!(out, "a \" b");
let res = quoted_string_nl.parse_peek("\"invalid");
assert!(res.is_err());
}
#[test]
fn check_chezmoi_raw_source() {
let input = concat!(
r#"source "{{ .chezmoi.sourceDir }}/{{ .chezmoi.sourceFile | trimSuffix ".tmpl" | replace "modify_" "" }}.src.ini""#,
"\n"
);
let (rem, out) = source.parse_peek(input).unwrap();
assert_eq!(rem, "");
assert!(
matches!(out, Directive::Source(s) if s == r#"{{ .chezmoi.sourceDir }}/{{ .chezmoi.sourceFile | trimSuffix ".tmpl" | replace "modify_" "" }}.src.ini"#)
);
let input = concat!(
r#"source "{{ .chezmoi.sourceDir }}/{{ .chezmoi.sourceFile | trimSuffix ".tmpl" | replace "modify_" "" }}.src.ini" "#,
"\t \n"
);
let (rem, out) = source.parse_peek(input).unwrap();
assert_eq!(rem, "");
assert!(
matches!(out, Directive::Source(s) if s == r#"{{ .chezmoi.sourceDir }}/{{ .chezmoi.sourceFile | trimSuffix ".tmpl" | replace "modify_" "" }}.src.ini"#)
);
}
#[test]
fn check_matcher() {
let (rem, out) = matcher.parse_peek("section \"my-section\"").unwrap();
assert_eq!(rem, "");
assert!(matches!(out, Matcher::Section(s) if s == "my-section"));
let (rem, out) = matcher.parse_peek("\"my-section\" \"my-key\"").unwrap();
assert_eq!(rem, "");
assert!(matches!(out, Matcher::Literal(s, k) if s == "my-section" && k == "my-key"));
let (rem, out) = matcher
.parse_peek("regex \"my-section.*\" \"my-key.*\"")
.unwrap();
assert_eq!(rem, "");
assert!(matches!(out, Matcher::Regex(s, k) if s == "my-section.*" && k == "my-key.*"));
}
#[test]
fn check_transform_arg() {
let (rem, out) = transform_arg.parse_peek("aaa=\"bbb\"").unwrap();
assert_eq!(rem, "");
assert_eq!(out.0, "aaa");
assert_eq!(out.1, "bbb");
}
#[test]
fn check_transform() {
let (rem, out) = transform
.parse_peek("transform regex \"s.*\" \"k.*\" transform-name arg1=\"a\" arg2=\"b\"")
.unwrap();
assert_eq!(rem, "");
assert_eq!(
out,
Directive::Transform(
Matcher::Regex("s.*".into(), "k.*".into()),
"transform-name".into(),
HashMap::from([("arg1".into(), "a".into()), ("arg2".into(), "b".into())]),
)
);
}
#[test]
fn check_transform_no_args() {
let (rem, out) = transform
.parse_peek("transform regex \"s.*\" \"k.*\" transform-name")
.unwrap();
assert_eq!(rem, "");
assert_eq!(
out,
Directive::Transform(
Matcher::Regex("s.*".into(), "k.*".into()),
"transform-name".into(),
HashMap::new(),
)
);
}
const FULL_EXAMPLE: &str = indoc! {r#"
#!/path
source auto
ignore section "c"
ignore "a" "b"
transform "d" "e" unsorted-list separator=","
transform "f g" "h" keyring service="srv" user="usr"
transform "a" "b" kde-shortcut
ignore regex "a.*" "b.*"
transform regex "d.*" "e.*" kde-shortcut
# Random comment
# Test adding
add:hide "f g" "h"
add:remove regex "quux.*" "eh?"
add:remove section "very secret"
add:hide section "somewhat secret"
"#};
#[test]
fn test_parse() {
let out = parse_config.parse(FULL_EXAMPLE).unwrap();
let out: Vec<_> = out.into_iter().filter(|v| *v != Directive::WS).collect();
assert_eq!(
out,
vec![
Directive::SourceAutoEnv,
Directive::Ignore(Matcher::Section("c".into())),
Directive::Ignore(Matcher::Literal("a".into(), "b".into())),
Directive::Transform(
Matcher::Literal("d".into(), "e".into()),
"unsorted-list".into(),
HashMap::from([("separator".into(), ",".into())])
),
Directive::Transform(
Matcher::Literal("f g".into(), "h".into()),
"keyring".into(),
HashMap::from([
("service".into(), "srv".into()),
("user".into(), "usr".into())
])
),
Directive::Transform(
Matcher::Literal("a".into(), "b".into()),
"kde-shortcut".into(),
HashMap::new()
),
Directive::Ignore(Matcher::Regex("a.*".into(), "b.*".into())),
Directive::Transform(
Matcher::Regex("d.*".into(), "e.*".into()),
"kde-shortcut".into(),
HashMap::new()
),
Directive::AddHide(Matcher::Literal("f g".into(), "h".into())),
Directive::AddRemove(Matcher::Regex("quux.*".into(), "eh?".into())),
Directive::AddRemove(Matcher::Section("very secret".into())),
Directive::AddHide(Matcher::Section("somewhat secret".into())),
]
);
}
#[test]
fn test_parse_newlines() {
let out = parse_config
.parse(
"source auto\rsource \"foo\"\r\nignore section \"bar\"\nignore section \
\"quux\"\r\n",
)
.unwrap();
let out: Vec<_> = out.into_iter().filter(|v| *v != Directive::WS).collect();
assert_eq!(
out,
vec![
Directive::SourceAutoEnv,
Directive::Source("foo".into()),
Directive::Ignore(Matcher::Section("bar".into())),
Directive::Ignore(Matcher::Section("quux".into()))
]
);
}
}