Skip to content

Commit 4798228

Browse files
authored
feat!: Allow specifying many options in config profile without array (#211)
Adds the possibility to specify many options in config profile files as single strings which required arrays of strings - in the case of the array only containing 1 entry. Breaking change: Also removes the splitting of one string from `password-command` and `warmup-command`. Now the already-split command has to be given as array (or single string if there are no arguments to the command).
1 parent 7535ea3 commit 4798228

File tree

7 files changed

+45
-62
lines changed

7 files changed

+45
-62
lines changed

crates/core/Cargo.toml

-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ filetime = "0.2.23"
8585
ignore = "0.4.22"
8686
nix = { version = "0.28", default-features = false, features = ["user", "fs"] }
8787
path-dedot = "3.1.1"
88-
shell-words = "1.1.0"
8988
walkdir = "2.5.0"
9089

9190
# cache

crates/core/src/backend/ignore.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::{
66
path::{Path, PathBuf},
77
};
88

9-
use serde_with::{serde_as, DisplayFromStr};
9+
use serde_with::{serde_as, DisplayFromStr, OneOrMany};
1010

1111
use bytesize::ByteSize;
1212
#[cfg(not(windows))]
@@ -70,21 +70,25 @@ pub struct LocalSourceFilterOptions {
7070
/// Glob pattern to exclude/include (can be specified multiple times)
7171
#[cfg_attr(feature = "clap", clap(long))]
7272
#[cfg_attr(feature = "merge", merge(strategy = merge::vec::overwrite_empty))]
73+
#[serde_as(as = "OneOrMany<_>")]
7374
pub glob: Vec<String>,
7475

7576
/// Same as --glob pattern but ignores the casing of filenames
7677
#[cfg_attr(feature = "clap", clap(long, value_name = "GLOB"))]
7778
#[cfg_attr(feature = "merge", merge(strategy = merge::vec::overwrite_empty))]
79+
#[serde_as(as = "OneOrMany<_>")]
7880
pub iglob: Vec<String>,
7981

8082
/// Read glob patterns to exclude/include from this file (can be specified multiple times)
8183
#[cfg_attr(feature = "clap", clap(long, value_name = "FILE"))]
8284
#[cfg_attr(feature = "merge", merge(strategy = merge::vec::overwrite_empty))]
85+
#[serde_as(as = "OneOrMany<_>")]
8386
pub glob_file: Vec<String>,
8487

8588
/// Same as --glob-file ignores the casing of filenames in patterns
8689
#[cfg_attr(feature = "clap", clap(long, value_name = "FILE"))]
8790
#[cfg_attr(feature = "merge", merge(strategy = merge::vec::overwrite_empty))]
91+
#[serde_as(as = "OneOrMany<_>")]
8892
pub iglob_file: Vec<String>,
8993

9094
/// Ignore files based on .gitignore files
@@ -100,11 +104,13 @@ pub struct LocalSourceFilterOptions {
100104
/// Treat the provided filename like a .gitignore file (can be specified multiple times)
101105
#[cfg_attr(feature = "clap", clap(long, value_name = "FILE"))]
102106
#[cfg_attr(feature = "merge", merge(strategy = merge::vec::overwrite_empty))]
107+
#[serde_as(as = "OneOrMany<_>")]
103108
pub custom_ignorefile: Vec<String>,
104109

105110
/// Exclude contents of directories containing this filename (can be specified multiple times)
106111
#[cfg_attr(feature = "clap", clap(long, value_name = "FILE"))]
107112
#[cfg_attr(feature = "merge", merge(strategy = merge::vec::overwrite_empty))]
113+
#[serde_as(as = "OneOrMany<_>")]
108114
pub exclude_if_present: Vec<String>,
109115

110116
/// Exclude other file systems, don't cross filesystem boundaries and subvolumes

crates/core/src/commands/forget.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use chrono::{DateTime, Datelike, Duration, Local, Timelike};
44
use derivative::Derivative;
55
use derive_setters::Setters;
66
use serde_derive::{Deserialize, Serialize};
7-
use serde_with::{serde_as, DisplayFromStr};
7+
use serde_with::{serde_as, DisplayFromStr, OneOrMany};
88

99
use crate::{
1010
error::RusticResult,
@@ -107,13 +107,14 @@ pub(crate) fn get_forget_snapshots<P: ProgressBars, S: Open>(
107107
pub struct KeepOptions {
108108
/// Keep snapshots with this taglist (can be specified multiple times)
109109
#[cfg_attr(feature = "clap", clap(long, value_name = "TAG[,TAG,..]"))]
110-
#[serde_as(as = "Vec<DisplayFromStr>")]
110+
#[serde_as(as = "OneOrMany<DisplayFromStr>")]
111111
#[cfg_attr(feature = "merge", merge(strategy=merge::vec::overwrite_empty))]
112112
pub keep_tags: Vec<StringList>,
113113

114114
/// Keep snapshots ids that start with ID (can be specified multiple times)
115115
#[cfg_attr(feature = "clap", clap(long = "keep-id", value_name = "ID"))]
116116
#[cfg_attr(feature = "merge", merge(strategy=merge::vec::overwrite_empty))]
117+
#[serde_as(as = "OneOrMany<_>")]
117118
pub keep_ids: Vec<String>,
118119

119120
/// Keep the last N snapshots (N == -1: keep all snapshots)

crates/core/src/error.rs

-4
Original file line numberDiff line numberDiff line change
@@ -290,8 +290,6 @@ pub enum RepositoryErrorKind {
290290
/// error accessing config file
291291
AccessToConfigFileFailed,
292292
/// {0:?}
293-
FromSplitError(#[from] shell_words::ParseError),
294-
/// {0:?}
295293
FromThreadPoolbilderError(rayon::ThreadPoolBuildError),
296294
/// reading Password failed: `{0:?}`
297295
ReadingPasswordFromReaderFailed(std::io::Error),
@@ -437,8 +435,6 @@ pub enum SnapshotFileErrorKind {
437435
UnpackingSnapshotFileResultFailed,
438436
/// collecting IDs failed: {0:?}
439437
FindingIdsFailed(Vec<String>),
440-
/// {0:?}
441-
FromSplitError(#[from] shell_words::ParseError),
442438
/// removing dots from paths failed: `{0:?}`
443439
RemovingDotsFromPathFailed(std::io::Error),
444440
/// canonicalizing path failed: `{0:?}`

crates/core/src/repofile/snapshotfile.rs

+11-33
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ use itertools::Itertools;
1515
use log::info;
1616
use path_dedot::ParseDot;
1717
use serde_derive::{Deserialize, Serialize};
18-
use serde_with::{serde_as, DisplayFromStr};
19-
use shell_words::split;
18+
use serde_with::{serde_as, DisplayFromStr, OneOrMany};
2019

2120
use crate::{
2221
backend::{decrypt::DecryptReadBackend, FileType, FindInBackend},
@@ -52,7 +51,7 @@ pub struct SnapshotOptions {
5251

5352
/// Tags to add to snapshot (can be specified multiple times)
5453
#[cfg_attr(feature = "clap", clap(long, value_name = "TAG[,TAG,..]"))]
55-
#[serde_as(as = "Vec<DisplayFromStr>")]
54+
#[serde_as(as = "OneOrMany<DisplayFromStr>")]
5655
#[cfg_attr(feature = "merge", merge(strategy = merge::vec::overwrite_empty))]
5756
pub tag: Vec<StringList>,
5857

@@ -1106,45 +1105,24 @@ impl Display for PathList {
11061105
}
11071106
}
11081107

1109-
impl FromIterator<PathBuf> for PathList {
1110-
fn from_iter<I: IntoIterator<Item = PathBuf>>(iter: I) -> Self {
1111-
Self(iter.into_iter().collect())
1112-
}
1113-
}
1114-
1115-
impl<'a> FromIterator<&'a String> for PathList {
1116-
fn from_iter<I: IntoIterator<Item = &'a String>>(iter: I) -> Self {
1117-
Self(iter.into_iter().map(PathBuf::from).collect())
1118-
}
1119-
}
1120-
1121-
impl FromIterator<String> for PathList {
1122-
fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> Self {
1123-
Self(iter.into_iter().map(PathBuf::from).collect())
1124-
}
1125-
}
1126-
1127-
impl<'a> FromIterator<&'a str> for PathList {
1128-
fn from_iter<I: IntoIterator<Item = &'a str>>(iter: I) -> Self {
1129-
Self(iter.into_iter().map(PathBuf::from).collect())
1108+
impl<T: Into<PathBuf>> FromIterator<T> for PathList {
1109+
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
1110+
Self(iter.into_iter().map(T::into).collect())
11301111
}
11311112
}
11321113

11331114
impl PathList {
1134-
/// Create a `PathList` by parsing a Strings containing paths separated by whitspaces.
1115+
/// Create a `PathList` from a String containing a single path
1116+
/// Note: for multiple paths, use `PathList::from_iter`.
11351117
///
11361118
/// # Arguments
11371119
///
1138-
/// * `sources` - The String to parse
1120+
/// * `source` - The String to parse
11391121
///
11401122
/// # Errors
1141-
///
1142-
/// * [`SnapshotFileErrorKind::FromSplitError`] - If the parsing failed
1143-
///
1144-
/// [`SnapshotFileErrorKind::FromSplitError`]: crate::error::SnapshotFileErrorKind::FromSplitError
1145-
pub fn from_string(sources: &str) -> RusticResult<Self> {
1146-
let sources = split(sources).map_err(SnapshotFileErrorKind::FromSplitError)?;
1147-
Ok(Self::from_iter(sources))
1123+
/// no errors can occur here
1124+
pub fn from_string(source: &str) -> RusticResult<Self> {
1125+
Ok(Self(vec![source.into()]))
11481126
}
11491127

11501128
/// Number of paths in the `PathList`.

crates/core/src/repository.rs

+15-13
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ use std::{
1212
use bytes::Bytes;
1313
use derive_setters::Setters;
1414
use log::{debug, error, info};
15-
use serde_with::{serde_as, DisplayFromStr};
16-
use shell_words::split;
15+
use serde_with::{serde_as, DisplayFromStr, OneOrMany};
1716

1817
use crate::{
1918
backend::{
@@ -111,7 +110,9 @@ pub struct RepositoryOptions {
111110
env = "RUSTIC_PASSWORD_COMMAND",
112111
conflicts_with_all = &["password", "password_file"],
113112
))]
114-
pub password_command: Option<String>,
113+
#[cfg_attr(feature = "merge", merge(strategy = merge::vec::overwrite_empty))]
114+
#[serde_as(as = "OneOrMany<_>")]
115+
pub password_command: Vec<String>,
115116

116117
/// Don't use a cache.
117118
#[cfg_attr(feature = "clap", clap(long, global = true, env = "RUSTIC_NO_CACHE"))]
@@ -140,7 +141,9 @@ pub struct RepositoryOptions {
140141
feature = "clap",
141142
clap(long, global = true, conflicts_with = "warm_up")
142143
)]
143-
pub warm_up_command: Option<String>,
144+
#[cfg_attr(feature = "merge", merge(strategy = merge::vec::overwrite_empty))]
145+
#[serde_as(as = "OneOrMany<_>")]
146+
pub warm_up_command: Vec<String>,
144147

145148
/// Duration (e.g. 10m) to wait after warm up
146149
#[cfg_attr(feature = "clap", clap(long, global = true, value_name = "DURATION"))]
@@ -177,11 +180,10 @@ impl RepositoryOptions {
177180
);
178181
Ok(Some(read_password_from_reader(&mut file)?))
179182
}
180-
(_, _, Some(command)) => {
181-
let commands = split(command).map_err(RepositoryErrorKind::FromSplitError)?;
182-
debug!("commands: {commands:?}");
183-
let command = Command::new(&commands[0])
184-
.args(&commands[1..])
183+
(_, _, command) if !command.is_empty() => {
184+
debug!("commands: {command:?}");
185+
let command = Command::new(&command[0])
186+
.args(&command[1..])
185187
.stdout(Stdio::piped())
186188
.spawn()?;
187189
let Ok(output) = command.wait_with_output() else {
@@ -205,7 +207,7 @@ impl RepositoryOptions {
205207
}
206208
}))
207209
}
208-
(None, None, None) => Ok(None),
210+
(None, None, _) => Ok(None),
209211
}
210212
}
211213
}
@@ -321,11 +323,11 @@ impl<P> Repository<P, ()> {
321323
let mut be = backends.repository();
322324
let be_hot = backends.repo_hot();
323325

324-
if let Some(command) = &opts.warm_up_command {
325-
if !command.contains("%id") {
326+
if !opts.warm_up_command.is_empty() {
327+
if opts.warm_up_command.iter().all(|c| !c.contains("%id")) {
326328
return Err(RepositoryErrorKind::NoIDSpecified.into());
327329
}
328-
info!("using warm-up command {command}");
330+
info!("using warm-up command {:?}", opts.warm_up_command);
329331
}
330332

331333
if opts.warm_up {

crates/core/src/repository/warm_up.rs

+9-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use std::thread::sleep;
33

44
use log::{debug, error, warn};
55
use rayon::ThreadPoolBuilder;
6-
use shell_words::split;
76

87
use crate::{
98
backend::{FileType, ReadBackend},
@@ -63,8 +62,8 @@ pub(crate) fn warm_up<P: ProgressBars, S>(
6362
repo: &Repository<P, S>,
6463
packs: impl ExactSizeIterator<Item = Id>,
6564
) -> RusticResult<()> {
66-
if let Some(command) = &repo.opts.warm_up_command {
67-
warm_up_command(packs, command, &repo.pb)?;
65+
if !repo.opts.warm_up_command.is_empty() {
66+
warm_up_command(packs, &repo.opts.warm_up_command, &repo.pb)?;
6867
} else if repo.be.needs_warm_up() {
6968
warm_up_repo(repo, packs)?;
7069
}
@@ -86,16 +85,18 @@ pub(crate) fn warm_up<P: ProgressBars, S>(
8685
/// [`RepositoryErrorKind::FromSplitError`]: crate::error::RepositoryErrorKind::FromSplitError
8786
fn warm_up_command<P: ProgressBars>(
8887
packs: impl ExactSizeIterator<Item = Id>,
89-
command: &str,
88+
command: &[String],
9089
pb: &P,
9190
) -> RusticResult<()> {
9291
let p = pb.progress_counter("warming up packs...");
9392
p.set_length(packs.len() as u64);
9493
for pack in packs {
95-
let actual_command = command.replace("%id", &pack.to_hex());
96-
debug!("calling {actual_command}...");
97-
let commands = split(&actual_command).map_err(RepositoryErrorKind::FromSplitError)?;
98-
let status = Command::new(&commands[0]).args(&commands[1..]).status()?;
94+
let command: Vec<_> = command
95+
.iter()
96+
.map(|c| c.replace("%id", &pack.to_hex()))
97+
.collect();
98+
debug!("calling {command:?}...");
99+
let status = Command::new(&command[0]).args(&command[1..]).status()?;
99100
if !status.success() {
100101
warn!("warm-up command was not successful for pack {pack:?}. {status}");
101102
}

0 commit comments

Comments
 (0)