imdl/
env.rs

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  /// Initialize `pretty-env-logger` as the global logging backend.
71  ///
72  /// This function is called in `Env::run`, so the logger will always be
73  /// initialized when the program runs via main, and in tests which construct
74  /// and `Env` and run them.
75  ///
76  /// The logger will not be initialized in tests which don't construct an
77  /// `Env`, for example in unit tests that test functionality below the level
78  /// of a full program invocation.
79  ///
80  /// To enable logging in those tests, call `Env::initialize_logging()` like
81  /// so:
82  ///
83  /// ```no_run
84  /// #[test]
85  /// fn foo() {
86  ///   Env::initialize_logging();
87  ///   // Rest of the test...
88  /// }
89  /// ```
90  ///
91  /// If the logger has already been initialized, `Env::initialize_logging()` is
92  /// a no-op, so it's safe to call more than once.
93  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}