1use clap::Parser;
2use std::path::PathBuf;
3
4#[derive(Parser, Debug, Default)]
5#[command(
6 name = "sed",
7 version,
8 about = "A GNU-compatible stream editor implemented in Rust",
9 long_about = "A GNU-compatible stream editor implemented in Rust.\n\n\
10 Uses Rust regex syntax (similar to PCRE/ERE). The -E/-r flags are \
11 accepted for compatibility but are no-ops since extended regex \
12 syntax is always used.",
13 max_term_width = 100
14)]
15pub struct Options {
16 #[arg(short = 'n', long = "quiet", alias = "silent")]
18 pub quiet: bool,
19
20 #[arg(short = 'e', long = "expression", value_name = "SCRIPT")]
22 pub expressions: Vec<String>,
23
24 #[arg(short = 'f', long = "file", value_name = "SCRIPT-FILE")]
26 pub script_files: Vec<PathBuf>,
27
28 #[arg(
31 long = "in-place",
32 value_name = "SUFFIX",
33 num_args = 0..=1,
34 default_missing_value = "",
35 require_equals = true
36 )]
37 pub in_place: Option<String>,
38
39 #[arg(short = 'E', short_alias = 'r', long = "regexp-extended")]
41 pub extended_regexp: bool,
42
43 #[arg(short = 's', long = "separate")]
45 pub separate: bool,
46
47 #[arg(short = 'z', long = "null-data")]
49 pub null_data: bool,
50
51 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
53 pub args: Vec<String>,
54}
55
56impl Options {
57 pub fn script_and_files(&self) -> crate::Result<(String, Vec<PathBuf>)> {
62 let mut scripts = self.expressions.clone();
63
64 for path in &self.script_files {
65 let content = std::fs::read_to_string(path).map_err(|e| {
66 crate::Error::Parse(format!(
67 "couldn't read script file '{}': {}",
68 path.display(),
69 e
70 ))
71 })?;
72 scripts.push(content);
73 }
74
75 if scripts.is_empty() {
76 if let Some((first, rest)) = self.args.split_first() {
78 Ok((
79 first.clone(),
80 rest.iter().map(PathBuf::from).collect(),
81 ))
82 } else {
83 Err(crate::Error::Parse(
84 "no script provided. Usage: sed [OPTIONS] SCRIPT [FILE...]"
85 .into(),
86 ))
87 }
88 } else {
89 Ok((
90 scripts.join("\n"),
91 self.args.iter().map(PathBuf::from).collect(),
92 ))
93 }
94 }
95}
96
97pub fn preprocess_args(raw: impl Iterator<Item = String>) -> Vec<String> {
107 let mut result = Vec::new();
108 let mut saw_double_dash = false;
109
110 for arg in raw {
111 if saw_double_dash {
112 result.push(arg);
113 continue;
114 }
115
116 if arg == "--" {
117 saw_double_dash = true;
118 result.push(arg);
119 continue;
120 }
121
122 if arg == "-i" {
123 result.push("--in-place".to_string());
124 } else if arg.starts_with("-i") && !arg.starts_with("--") {
125 let suffix = &arg[2..];
126 result.push(format!("--in-place={suffix}"));
127 } else {
128 result.push(arg);
129 }
130 }
131
132 result
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use clap::CommandFactory;
139
140 #[test]
141 fn debug_assert() {
142 Options::command().debug_assert();
143 }
144
145 #[test]
146 fn preprocess_i_flag() {
147 let args = vec![
148 "sed".into(),
149 "-i".into(),
150 "s/foo/bar/".into(),
151 "file".into(),
152 ];
153 let processed = preprocess_args(args.into_iter());
154 assert_eq!(
155 processed,
156 vec!["sed", "--in-place", "s/foo/bar/", "file"]
157 );
158 }
159
160 #[test]
161 fn preprocess_i_suffix() {
162 let args =
163 vec!["sed".into(), "-i.bak".into(), "s/foo/bar/".into()];
164 let processed = preprocess_args(args.into_iter());
165 assert_eq!(
166 processed,
167 vec!["sed", "--in-place=.bak", "s/foo/bar/"]
168 );
169 }
170}