1use crate::common::*;
2
3pub(crate) struct Env {
4 args: Vec<OsString>,
5 dir: PathBuf,
6 input: Box<dyn InputStream>,
7 err: OutputStream,
8 out: OutputStream,
9}
10
11impl Env {
12 pub(crate) fn main() -> Result<Self> {
13 let dir = env::current_dir().context(error::CurrentDirectoryGet)?;
14
15 let style = env::var_os("NO_COLOR").is_none()
16 && env::var_os("TERM").as_deref() != Some(OsStr::new("dumb"));
17
18 let out_stream = OutputStream::stdout(style);
19 let err_stream = OutputStream::stderr(style);
20
21 Ok(Self::new(
22 dir,
23 env::args(),
24 Box::new(io::stdin()),
25 out_stream,
26 err_stream,
27 ))
28 }
29
30 pub(crate) fn run(&mut self) -> Result<()> {
31 #[cfg(windows)]
32 ansi_term::enable_ansi_support().ok();
33
34 Self::initialize_logging();
35
36 let app = {
37 let mut app = Arguments::clap();
38
39 let width = env::var("IMDL_TERM_WIDTH")
40 .ok()
41 .and_then(|width| width.parse::<usize>().ok());
42
43 if let Some(width) = width {
44 app = app.set_term_width(width);
45 }
46
47 app
48 };
49
50 let matches = app.get_matches_from_safe(&self.args)?;
51
52 let args = Arguments::from_clap(&matches);
53
54 let use_color = args.options().use_color;
55 self.err.set_use_color(use_color);
56 self.out.set_use_color(use_color);
57
58 if args.options().terminal {
59 self.err.set_is_term(true);
60 self.out.set_is_term(true);
61 }
62
63 if args.options().quiet {
64 self.err.set_active(false);
65 }
66
67 args.run(self)
68 }
69
70 pub(crate) fn initialize_logging() {
94 static ONCE: Once = Once::new();
95
96 ONCE.call_once(|| {
97 pretty_env_logger::init();
98 });
99 }
100
101 pub(crate) fn new<S, I>(
102 dir: PathBuf,
103 args: I,
104 input: Box<dyn InputStream>,
105 out: OutputStream,
106 err: OutputStream,
107 ) -> Self
108 where
109 S: Into<OsString>,
110 I: IntoIterator<Item = S>,
111 {
112 Self {
113 args: args.into_iter().map(Into::into).collect(),
114 input,
115 dir,
116 out,
117 err,
118 }
119 }
120
121 pub(crate) fn status(&mut self) -> Result<(), i32> {
122 use structopt::clap::ErrorKind;
123
124 if let Err(error) = self.run() {
125 if let Error::Clap { source } = error {
126 if source.use_stderr() {
127 write!(&mut self.err, "{source}").ok();
128 } else {
129 write!(&mut self.out, "{source}").ok();
130 }
131 match source.kind {
132 ErrorKind::VersionDisplayed | ErrorKind::HelpDisplayed => Ok(()),
133 _ => Err(EXIT_FAILURE),
134 }
135 } else {
136 let style = self.err.style();
137 writeln!(
138 &mut self.err,
139 "{}{}: {}{}",
140 style.error().paint("error"),
141 style.message().prefix(),
142 error,
143 style.message().suffix(),
144 )
145 .ok();
146
147 if let Some(lint) = error.lint() {
148 writeln!(
149 &mut self.err,
150 "{}: This check can be disabled with `--allow {}`.",
151 style.message().paint("note"),
152 lint.name()
153 )
154 .ok();
155 }
156
157 Err(EXIT_FAILURE)
158 }
159 } else {
160 Ok(())
161 }
162 }
163
164 pub(crate) fn dir(&self) -> &Path {
165 &self.dir
166 }
167
168 pub(crate) fn err(&self) -> &OutputStream {
169 &self.err
170 }
171
172 pub(crate) fn input<'a>(&'a mut self) -> Box<dyn BufRead + 'a> {
173 self.input.as_mut().buf_read()
174 }
175
176 pub(crate) fn err_mut(&mut self) -> &mut OutputStream {
177 &mut self.err
178 }
179
180 pub(crate) fn out(&self) -> &OutputStream {
181 &self.out
182 }
183
184 pub(crate) fn out_mut(&mut self) -> &mut OutputStream {
185 &mut self.out
186 }
187
188 pub(crate) fn resolve(&self, path: impl AsRef<Path>) -> Result<PathBuf> {
189 let path = path.as_ref();
190
191 if path.components().count() == 0 {
192 return Err(Error::internal("Empty path passed to resolve"));
193 }
194
195 Ok(self.dir().join(path).lexiclean())
196 }
197
198 pub(crate) fn write(&mut self, path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
199 let path = path.as_ref();
200 fs::write(self.resolve(path)?, contents).context(error::Filesystem { path })
201 }
202
203 pub(crate) fn read(&mut self, source: InputTarget) -> Result<Input> {
204 let data = match &source {
205 InputTarget::Path(path) => {
206 let absolute = self.resolve(path)?;
207 fs::read(absolute).context(error::Filesystem { path })?
208 }
209 InputTarget::Stdin => {
210 let mut buffer = Vec::new();
211 self
212 .input
213 .buf_read()
214 .read_to_end(&mut buffer)
215 .context(error::Stdin)?;
216 buffer
217 }
218 };
219
220 Ok(Input { source, data })
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn error_message_on_stdout() {
230 let mut env = test_env! {
231 args: [
232 "torrent",
233 "create",
234 "--input",
235 "foo",
236 "--announce",
237 "udp:bar.com",
238 "--announce-tier",
239 "foo",
240 ],
241 tree: {
242 foo: "",
243 }
244 };
245 env.status().ok();
246 let err = env.err();
247 assert!(
248 err.starts_with("error: Failed to parse announce URL:"),
249 "Unexpected standard error output: {err}",
250 );
251
252 assert_eq!(env.out(), "");
253 }
254
255 #[test]
256 fn quiet() {
257 let mut env = test_env! {
258 args: [
259 "--quiet",
260 "torrent",
261 "create",
262 "--input",
263 "foo",
264 "--announce",
265 "udp:bar.com",
266 "--announce-tier",
267 "foo",
268 ],
269 tree: {
270 foo: "",
271 }
272 };
273 env.status().ok();
274 assert_eq!(env.err(), "");
275 assert_eq!(env.out(), "");
276 }
277
278 #[test]
279 fn terminal() -> Result<()> {
280 let mut create_env = test_env! {
281 args: [
282 "torrent",
283 "create",
284 "--input",
285 "foo",
286 "--announce",
287 "udp:bar.com",
288 ],
289 tree: {
290 foo: "",
291 }
292 };
293
294 create_env.assert_ok();
295
296 let mut env = test_env! {
297 args: [
298 "--terminal",
299 "torrent",
300 "show",
301 "--input",
302 create_env.resolve("foo.torrent")?,
303 ],
304 tree: {
305 }
306 };
307
308 env.assert_ok();
309
310 assert_eq!(env.err(), "");
311 assert!(env.out().starts_with(" Name"));
312
313 Ok(())
314 }
315}