Skip to main content

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}