pager/
lib.rs

1//! Does all the magic to have you potentially long output piped through the
2//! external pager. Similar to what git does for its output.
3//!
4//! # Quick Start
5//!
6//! ```rust
7//! extern crate pager;
8//! use pager::Pager;
9//! fn main() {
10//!     Pager::new().setup();
11//!     // The rest of your program goes here
12//! }
13//! ```
14//!
15//! Under the hood this forks the current process, connects child' stdout
16//! to parent's stdin, and then replaces the parent with the pager of choice
17//! (environment variable PAGER). The child just continues as normal. If PAGER
18//! environment variable is not present `Pager` probes current PATH for `more`.
19//! If found it is used as a default pager.
20//!
21//! You can control pager to a limited degree. For example you can change the
22//! environment variable used for finding pager executable.
23//!
24//! ```rust
25//! extern crate pager;
26//! use pager::Pager;
27//! fn main() {
28//!     Pager::with_env("MY_PAGER").setup();
29//!     // The rest of your program goes here
30//! }
31//! ```
32//!
33//! Also you can set alternative default (fallback) pager to be used instead of
34//! `more`. PAGER environment variable (if set) will still have precedence.
35//!
36//! ```rust
37//! extern crate pager;
38//! use pager::Pager;
39//! fn main() {
40//!     Pager::with_default_pager("pager").setup();
41//!     // The rest of your program goes here
42//! }
43//! ```
44//! Alternatively you can specify directly the desired pager command, exactly
45//! as it would appear in PAGER environment variable. This is useful if you
46//! need some specific pager and/or flags (like "less -r") and would like to
47//! avoid forcing your consumers into modifying their existing PAGER
48//! configuration just for your application.
49//!
50//! ```rust
51//! extern crate pager;
52//! use pager::Pager;
53//! fn main() {
54//!     Pager::with_pager("pager -r").setup();
55//!     // The rest of your program goes here
56//! }
57//! ```
58//!
59//! If no suitable pager found `setup()` does nothing and your executable keeps
60//! running as usual. `Pager` cleans after itself and doesn't leak resources in
61//! case of setup failure.
62//!
63//! Sometimes you may want to bypass pager if the output of you executable is not a `tty`.
64//! If this case you may use `.skip_on_notty()` to get the desirable effect.
65//!
66//! ```rust
67//! extern crate pager;
68//! use pager::Pager;
69//! fn main() {
70//!     Pager::new().skip_on_notty().setup();
71//!     // The rest of your program goes here
72//! }
73//! ```
74//!
75//! If you need to disable pager altogether set environment variable `NOPAGER` and `Pager::setup()`
76//! will skip initialization. The host application will continue as normal. `Pager::is_on()` will
77//! reflect the fact that no Pager is active.
78
79#![doc(html_root_url = "https://docs.rs/pager/0.16.1")]
80#![cfg_attr(feature = "pedantic", warn(clippy::pedantic))]
81#![warn(clippy::use_self)]
82#![warn(deprecated_in_future)]
83#![warn(future_incompatible)]
84#![warn(unreachable_pub)]
85#![warn(missing_debug_implementations)]
86#![warn(rust_2018_compatibility)]
87#![warn(rust_2018_idioms)]
88#![warn(unused)]
89#![deny(warnings)]
90
91mod utils;
92
93use std::env;
94use std::ffi::{OsStr, OsString};
95
96/// Default pager environment variable
97const DEFAULT_PAGER_ENV: &str = "PAGER";
98
99/// Environment variable to disable pager altogether
100const NOPAGER_ENV: &str = "NOPAGER";
101
102/// Last resort pager. Should work everywhere.
103const DEFAULT_PAGER: &str = "more";
104
105/// Keeps track of the current pager state
106#[derive(Debug)]
107pub struct Pager {
108    default_pager: Option<OsString>,
109    pager: Option<OsString>,
110    envs: Vec<OsString>,
111    on: bool,
112    skip_on_notty: bool,
113}
114
115impl Default for Pager {
116    fn default() -> Self {
117        Self {
118            default_pager: None,
119            pager: env::var_os(DEFAULT_PAGER_ENV),
120            envs: Vec::new(),
121            on: true,
122            skip_on_notty: true,
123        }
124    }
125}
126
127impl Pager {
128    /// Creates new instance of `Pager` with default settings
129    pub fn new() -> Self {
130        Self::default()
131    }
132
133    /// Creates new instance of pager using `env` environment variable instead of PAGER
134    pub fn with_env(env: &str) -> Self {
135        Self {
136            pager: env::var_os(env),
137            ..Self::default()
138        }
139    }
140
141    #[deprecated(since = "0.12.0", note = "use with_env() instead")]
142    pub fn env(env: &str) -> Self {
143        Self::with_env(env)
144    }
145
146    /// Creates a new `Pager` instance with the specified default fallback
147    pub fn with_default_pager<S>(pager: S) -> Self
148    where
149        S: Into<OsString>,
150    {
151        let default_pager = Some(pager.into());
152        Self {
153            default_pager,
154            ..Self::default()
155        }
156    }
157
158    /// Creates a new `Pager` instance directly specifying the desired pager
159    pub fn with_pager(pager: &str) -> Self {
160        Self {
161            pager: Some(pager.into()),
162            ..Self::default()
163        }
164    }
165
166    /// Launch pager with the specified environment variables
167    pub fn pager_envs(self, envs: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
168        let envs = envs.into_iter().map(|s| s.into()).collect();
169        Self { envs, ..self }
170    }
171
172    /// Instructs `Pager` to bypass invoking pager if output is not a `tty`
173    #[deprecated(since = "0.14.0", note = "'skip_on_notty' is default now")]
174    pub fn skip_on_notty(self) -> Self {
175        Self {
176            skip_on_notty: true,
177            ..self
178        }
179    }
180
181    /// Gives quick assessment of successful `Pager` setup
182    pub fn is_on(&self) -> bool {
183        self.on
184    }
185
186    fn pager(&self) -> Option<OsString> {
187        let fallback_pager = || Some(OsStr::new(DEFAULT_PAGER).into());
188
189        if env::var_os(NOPAGER_ENV).is_some() {
190            None
191        } else {
192            self.pager
193                .clone()
194                .or_else(|| self.default_pager.clone())
195                .or_else(fallback_pager)
196        }
197    }
198
199    /// Initiates Pager framework and sets up all the necessary environment for sending standard
200    /// output to the activated pager.
201    pub fn setup(&mut self) {
202        if self.skip_on_notty && !utils::isatty(libc::STDOUT_FILENO) {
203            self.on = false;
204            return;
205        }
206        if let Some(ref pager) = self.pager() {
207            let (pager_stdin, main_stdout) = utils::pipe();
208            let pid = utils::fork();
209            match pid {
210                -1 => {
211                    // Fork failed
212                    utils::close(pager_stdin);
213                    utils::close(main_stdout);
214                    self.on = false
215                }
216                0 => {
217                    // I am child
218                    utils::dup2(main_stdout, libc::STDOUT_FILENO);
219                    utils::close(pager_stdin);
220                }
221                _ => {
222                    // I am parent
223                    utils::dup2(pager_stdin, libc::STDIN_FILENO);
224                    utils::close(main_stdout);
225                    utils::execvpe(pager, &self.envs);
226                }
227            }
228        } else {
229            self.on = false;
230        }
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use std::ops::Drop;
238
239    enum PagerEnv {
240        Reinstate(OsString, OsString),
241        Remove(OsString),
242    }
243
244    impl PagerEnv {
245        fn new<S: AsRef<OsStr>>(env: S) -> Self {
246            let env = env.as_ref().into();
247            if let Some(value) = env::var_os(&env) {
248                Self::Reinstate(env, value)
249            } else {
250                Self::Remove(env)
251            }
252        }
253
254        fn set<S: AsRef<OsStr>>(&self, value: S) {
255            match self {
256                Self::Reinstate(env, _) | Self::Remove(env) => env::set_var(env, value),
257            }
258        }
259
260        fn remove(&self) {
261            match self {
262                Self::Reinstate(env, _) | Self::Remove(env) => env::remove_var(env),
263            }
264        }
265    }
266
267    impl Drop for PagerEnv {
268        fn drop(&mut self) {
269            match self {
270                Self::Reinstate(env, value) => env::set_var(env, value),
271                Self::Remove(env) => env::remove_var(env),
272            }
273        }
274    }
275
276    fn assert_pager(pager: &Pager, result: &str) {
277        assert_eq!(pager.pager(), Some(OsStr::new(result).into()));
278    }
279
280    #[test]
281    fn nopager() {
282        let nopager = PagerEnv::new(NOPAGER_ENV);
283        nopager.set("");
284
285        let pager = Pager::new();
286        assert!(pager.pager().is_none());
287    }
288
289    #[test]
290    fn fallback_uses_more() {
291        let pager = Pager::new();
292        assert_pager(&pager, DEFAULT_PAGER);
293    }
294
295    #[test]
296    fn with_default_pager_without_env() {
297        let pagerenv = PagerEnv::new(DEFAULT_PAGER_ENV);
298        pagerenv.remove();
299
300        let pager = Pager::with_default_pager("more_or_less");
301        assert_pager(&pager, "more_or_less");
302    }
303
304    #[test]
305    fn with_default_pager_with_env() {
306        let pagerenv = PagerEnv::new(DEFAULT_PAGER_ENV);
307        pagerenv.set("something_else");
308
309        let pager = Pager::with_default_pager("more_or_less");
310        assert_pager(&pager, "something_else");
311    }
312
313    #[test]
314    fn with_default_pager() {
315        let pager = Pager::with_default_pager("more_or_less");
316        assert_pager(&pager, "more_or_less");
317    }
318
319    #[test]
320    fn with_pager() {
321        let pager = Pager::with_pager("now_or_never");
322        assert_pager(&pager, "now_or_never");
323    }
324}