cnf_lib/util/output_matcher.rs
1// SPDX-License-Identifier: GPL-3.0-or-later
2// SPDX-FileCopyrightText: (C) 2023 Andreas Hartmann <hartan@7x.de>
3// This file is part of cnf-lib, available at <https://gitlab.com/hartang/rust/cnf>
4
5//! # Helper for matching [command output](std::process::Output)
6use std::borrow::Cow;
7
8/// Match substrings in [command output](std::process::Output).
9///
10/// Due to the problems described in [`crate::env::ExecutionError::NonZero`], the output of
11/// [`crate::env::Environment::output_of()`] doesn't reliably catch the contents of *stderr*
12/// in the actual *stderr* variable. For [`Provider`s][crate::provider::Provider], this means
13/// that when searching for error strings in command output, **both stdout and stderr** must be
14/// checked for occurences of the pattern. This struct removes a lof of the resulting boilerplate
15/// code for first converting a `Vec<u8>` into something string-like, and then matching *stdout*
16/// and *stderr* against a pattern.
17#[derive(Debug)]
18pub struct OutputMatcher<'a> {
19 stdout: Cow<'a, str>,
20 stderr: Cow<'a, str>,
21 status: std::process::ExitStatus,
22}
23
24impl<'a> OutputMatcher<'a> {
25 /// Create a new instance of [`OutputMatcher`] for the given command output.
26 pub fn new(output: &'a std::process::Output) -> Self {
27 Self {
28 stdout: String::from_utf8_lossy(&output.stdout),
29 stderr: String::from_utf8_lossy(&output.stderr),
30 status: output.status,
31 }
32 }
33
34 /// Access stdout with all leading/trailing whitespaces trimmed.
35 fn stdout(&self) -> &str {
36 self.stdout.trim()
37 }
38
39 /// Access stderr with all leading/trailing whitespaces trimmed.
40 fn stderr(&self) -> &str {
41 self.stderr.trim()
42 }
43
44 /// Returns true if either stdout or stderr starts with `pat`.
45 pub fn starts_with(&self, pat: &str) -> bool {
46 self.stdout().starts_with(pat) || self.stderr().starts_with(pat)
47 }
48
49 /// Returns true if either stdout or stderr contains `pat`.
50 pub fn contains(&self, pat: &str) -> bool {
51 self.stdout().contains(pat) || self.stderr().contains(pat)
52 }
53
54 /// Returns true if either stdout or stderr ends with `pat`.
55 pub fn ends_with(&self, pat: &str) -> bool {
56 self.stdout().ends_with(pat) || self.stderr().ends_with(pat)
57 }
58
59 /// Returns the raw error code of the command output.
60 pub fn exit_code(&self) -> Option<i32> {
61 self.status.code()
62 }
63
64 /// Returns true if **both** stdout **and** stderr are empty (except for whitespace)
65 pub fn is_empty(&self) -> bool {
66 self.stdout().is_empty() && self.stderr().is_empty()
67 }
68}
69
70impl<'x> From<&'x std::process::Output> for OutputMatcher<'x> {
71 fn from(value: &'x std::process::Output) -> Self {
72 Self::new(value)
73 }
74}