lib.rs 9.3 KB
Newer Older
Cyril Plisko's avatar
Cyril Plisko committed
1
2
3
//! Does all the magic to have you potentially long output piped through the
//! external pager. Similar to what git does for its output.
//!
Cyril Plisko's avatar
Cyril Plisko committed
4
//! # Quick Start
Cyril Plisko's avatar
Cyril Plisko committed
5
//!
Cyril Plisko's avatar
Cyril Plisko committed
6
//! ```rust
Cyril Plisko's avatar
Cyril Plisko committed
7
//! extern crate pager;
Cyril Plisko's avatar
Cyril Plisko committed
8
//! use pager::Pager;
Cyril Plisko's avatar
Cyril Plisko committed
9
10
11
12
13
14
15
16
//! fn main() {
//!     Pager::new().setup();
//!     // The rest of your program goes here
//! }
//! ```
//!
//! Under the hood this forks the current process, connects child' stdout
//! to parent's stdin, and then replaces the parent with the pager of choice
Cyril Plisko's avatar
Cyril Plisko committed
17
18
19
//! (environment variable PAGER). The child just continues as normal. If PAGER
//! environment variable is not present `Pager` probes current PATH for `more`.
//! If found it is used as a default pager.
Cyril Plisko's avatar
Cyril Plisko committed
20
21
22
23
//!
//! You can control pager to a limited degree. For example you can change the
//! environment variable used for finding pager executable.
//!
Cyril Plisko's avatar
Cyril Plisko committed
24
//! ```rust
Cyril Plisko's avatar
Cyril Plisko committed
25
//! extern crate pager;
Cyril Plisko's avatar
Cyril Plisko committed
26
//! use pager::Pager;
Cyril Plisko's avatar
Cyril Plisko committed
27
//! fn main() {
28
//!     Pager::with_env("MY_PAGER").setup();
Cyril Plisko's avatar
Cyril Plisko committed
29
30
31
32
//!     // The rest of your program goes here
//! }
//! ```
//!
33
34
35
36
37
38
39
40
41
42
43
//! Also you can set alternative default (fallback) pager to be used instead of
//! `more`. PAGER environment variable (if set) will still have precedence.
//!
//! ```rust
//! extern crate pager;
//! use pager::Pager;
//! fn main() {
//!     Pager::with_default_pager("pager").setup();
//!     // The rest of your program goes here
//! }
//! ```
Cyril Plisko's avatar
Cyril Plisko committed
44
45
//! Alternatively you can specify directly the desired pager command, exactly
//! 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
Cyril Plisko's avatar
Cyril Plisko committed
47
48
49
50
51
52
53
//! avoid forcing your consumers into modifying their existing PAGER
//! configuration just for your application.
//!
//! ```rust
//! extern crate pager;
//! use pager::Pager;
//! fn main() {
54
//!     Pager::with_pager("pager -r").setup();
Cyril Plisko's avatar
Cyril Plisko committed
55
56
57
58
//!     // The rest of your program goes here
//! }
//! ```
//!
Cyril Plisko's avatar
Cyril Plisko committed
59
//! If no suitable pager found `setup()` does nothing and your executable keeps
Cyril Plisko's avatar
Cyril Plisko committed
60
61
62
//! running as usual. `Pager` cleans after itself and doesn't leak resources in
//! case of setup failure.
//!
Cyril Plisko's avatar
Cyril Plisko committed
63
64
65
66
67
68
69
70
71
72
73
74
//! Sometimes you may want to bypass pager if the output of you executable is not a `tty`.
//! If this case you may use `.skip_on_notty()` to get the desirable effect.
//!
//! ```rust
//! extern crate pager;
//! use pager::Pager;
//! fn main() {
//!     Pager::new().skip_on_notty().setup();
//!     // The rest of your program goes here
//! }
//! ```
//!
Cyril Plisko's avatar
Cyril Plisko committed
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
Cyril Plisko's avatar
Cyril Plisko committed
77
//! reflect the fact that no Pager is active.
Cyril Plisko's avatar
Cyril Plisko committed
78

Cyril Plisko's avatar
Cyril Plisko committed
79
#![doc(html_root_url = "https://docs.rs/pager/0.16.0")]
Cyril Plisko's avatar
Cyril Plisko committed
80
81
82
83
84
85
86
87
88
89
#![cfg_attr(feature = "pedantic", warn(clippy::pedantic))]
#![warn(clippy::use_self)]
#![warn(deprecated_in_future)]
#![warn(future_incompatible)]
#![warn(unreachable_pub)]
#![warn(missing_debug_implementations)]
#![warn(rust_2018_compatibility)]
#![warn(rust_2018_idioms)]
#![warn(unused)]
#![deny(warnings)]
Cyril Plisko's avatar
Cyril Plisko committed
90

Cyril Plisko's avatar
Cyril Plisko committed
91
mod utils;
92

93
94
use std::env;
use std::ffi::{OsStr, OsString};
Cyril Plisko's avatar
Cyril Plisko committed
95

Cyril Plisko's avatar
Cyril Plisko committed
96
/// Default pager environment variable
97
const DEFAULT_PAGER_ENV: &str = "PAGER";
Cyril Plisko's avatar
Cyril Plisko committed
98

99
100
101
102
103
104
/// Environment variable to disable pager altogether
const NOPAGER_ENV: &str = "NOPAGER";

/// Last resort pager. Should work everywhere.
const DEFAULT_PAGER: &str = "more";

105
/// Keeps track of the current pager state
106
#[derive(Debug)]
Cyril Plisko's avatar
Cyril Plisko committed
107
pub struct Pager {
108
    default_pager: Option<OsString>,
Cyril Plisko's avatar
Cyril Plisko committed
109
    pager: Option<OsString>,
dalance's avatar
dalance committed
110
    envs: Vec<OsString>,
111
    on: bool,
Cyril Plisko's avatar
Cyril Plisko committed
112
    skip_on_notty: bool,
Cyril Plisko's avatar
Cyril Plisko committed
113
114
}

115
116
117
impl Default for Pager {
    fn default() -> Self {
        Self {
118
119
            default_pager: None,
            pager: env::var_os(DEFAULT_PAGER_ENV),
dalance's avatar
dalance committed
120
            envs: Vec::new(),
121
            on: true,
122
            skip_on_notty: true,
123
124
125
126
        }
    }
}

Cyril Plisko's avatar
Cyril Plisko committed
127
impl Pager {
Cyril Plisko's avatar
Cyril Plisko committed
128
    /// Creates new instance of `Pager` with default settings
Cyril Plisko's avatar
Cyril Plisko committed
129
    pub fn new() -> Self {
Cyril Plisko's avatar
Cyril Plisko committed
130
        Self::default()
Cyril Plisko's avatar
Cyril Plisko committed
131
132
    }

Cyril Plisko's avatar
Cyril Plisko committed
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),
Cyril Plisko's avatar
Cyril Plisko committed
137
            ..Self::default()
138
        }
Cyril Plisko's avatar
Cyril Plisko committed
139
140
    }

141
142
    #[deprecated(since = "0.12.0", note = "use with_env() instead")]
    pub fn env(env: &str) -> Self {
143
        Self::with_env(env)
144
145
    }

146
147
148
149
150
151
152
153
    /// Creates a new `Pager` instance with the specified default fallback
    pub fn with_default_pager<S>(pager: S) -> Self
    where
        S: Into<OsString>,
    {
        let default_pager = Some(pager.into());
        Self {
            default_pager,
Cyril Plisko's avatar
Cyril Plisko committed
154
            ..Self::default()
155
156
157
        }
    }

Cyril Plisko's avatar
Cyril Plisko committed
158
    /// Creates a new `Pager` instance directly specifying the desired pager
Cyril Plisko's avatar
Cyril Plisko committed
159
    pub fn with_pager(pager: &str) -> Self {
160
        Self {
161
            pager: Some(pager.into()),
Cyril Plisko's avatar
Cyril Plisko committed
162
            ..Self::default()
Cyril Plisko's avatar
Cyril Plisko committed
163
164
165
        }
    }

dalance's avatar
dalance committed
166
    /// Launch pager with the specified environment variables
167
168
169
    pub fn pager_envs(self, envs: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
        let envs = envs.into_iter().map(|s| s.into()).collect();
        Self { envs, ..self }
dalance's avatar
dalance committed
170
171
    }

Cyril Plisko's avatar
Cyril Plisko committed
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")]
Cyril Plisko's avatar
Cyril Plisko committed
174
    pub fn skip_on_notty(self) -> Self {
175
        Self {
Cyril Plisko's avatar
Cyril Plisko committed
176
177
            skip_on_notty: true,
            ..self
Cyril Plisko's avatar
Cyril Plisko committed
178
179
180
        }
    }

Cyril Plisko's avatar
Cyril Plisko committed
181
    /// Gives quick assessment of successful `Pager` setup
182
183
    pub fn is_on(&self) -> bool {
        self.on
Cyril Plisko's avatar
Cyril Plisko committed
184
185
    }

186
187
188
189
190
191
192
193
194
195
196
197
198
    fn pager(&self) -> Option<OsString> {
        let fallback_pager = || Some(OsStr::new(DEFAULT_PAGER).into());

        if env::var_os(NOPAGER_ENV).is_some() {
            None
        } else {
            self.pager
                .clone()
                .or_else(|| self.default_pager.clone())
                .or_else(fallback_pager)
        }
    }

Cyril Plisko's avatar
Cyril Plisko committed
199
200
    /// Initiates Pager framework and sets up all the necessary environment for sending standard
    /// output to the activated pager.
Cyril Plisko's avatar
Cyril Plisko committed
201
    pub fn setup(&mut self) {
Cyril Plisko's avatar
Cyril Plisko committed
202
203
204
205
        if self.skip_on_notty && !utils::isatty(libc::STDOUT_FILENO) {
            self.on = false;
            return;
        }
206
        if let Some(ref pager) = self.pager() {
Cyril Plisko's avatar
Cyril Plisko committed
207
208
            let (pager_stdin, main_stdout) = utils::pipe();
            let pid = utils::fork();
Cyril Plisko's avatar
Cyril Plisko committed
209
210
211
            match pid {
                -1 => {
                    // Fork failed
Cyril Plisko's avatar
Cyril Plisko committed
212
213
                    utils::close(pager_stdin);
                    utils::close(main_stdout);
214
                    self.on = false
Cyril Plisko's avatar
Cyril Plisko committed
215
216
217
                }
                0 => {
                    // I am child
Cyril Plisko's avatar
Cyril Plisko committed
218
219
                    utils::dup2(main_stdout, libc::STDOUT_FILENO);
                    utils::close(pager_stdin);
Cyril Plisko's avatar
Cyril Plisko committed
220
221
222
                }
                _ => {
                    // I am parent
Cyril Plisko's avatar
Cyril Plisko committed
223
224
                    utils::dup2(pager_stdin, libc::STDIN_FILENO);
                    utils::close(main_stdout);
dalance's avatar
dalance committed
225
226
227
228
229
                    if self.envs.is_empty() {
                        utils::execvp(pager);
                    } else {
                        utils::execvpe(pager, &self.envs);
                    }
Cyril Plisko's avatar
Cyril Plisko committed
230
231
                }
            }
232
233
        } else {
            self.on = false;
Cyril Plisko's avatar
Cyril Plisko committed
234
235
236
        }
    }
}
237
238
239
240
241
242
243
244

#[cfg(test)]
mod tests {
    use super::*;
    use std::ops::Drop;

    enum PagerEnv {
        Reinstate(OsString, OsString),
Cyril Plisko's avatar
rustfmt    
Cyril Plisko committed
245
        Remove(OsString),
246
247
248
249
250
251
    }

    impl PagerEnv {
        fn new<S: AsRef<OsStr>>(env: S) -> Self {
            let env = env.as_ref().into();
            if let Some(value) = env::var_os(&env) {
Cyril Plisko's avatar
Cyril Plisko committed
252
                Self::Reinstate(env, value)
253
            } else {
Cyril Plisko's avatar
Cyril Plisko committed
254
                Self::Remove(env)
255
256
257
258
259
            }
        }

        fn set<S: AsRef<OsStr>>(&self, value: S) {
            match self {
Cyril Plisko's avatar
Cyril Plisko committed
260
                Self::Reinstate(env, _) | Self::Remove(env) => env::set_var(env, value),
261
262
263
264
265
            }
        }

        fn remove(&self) {
            match self {
Cyril Plisko's avatar
Cyril Plisko committed
266
                Self::Reinstate(env, _) | Self::Remove(env) => env::remove_var(env),
267
268
269
270
271
272
273
            }
        }
    }

    impl Drop for PagerEnv {
        fn drop(&mut self) {
            match self {
Cyril Plisko's avatar
Cyril Plisko committed
274
275
                Self::Reinstate(env, value) => env::set_var(env, value),
                Self::Remove(env) => env::remove_var(env),
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
            }
        }
    }

    fn assert_pager(pager: &Pager, result: &str) {
        assert_eq!(pager.pager(), Some(OsStr::new(result).into()));
    }

    #[test]
    fn nopager() {
        let nopager = PagerEnv::new(NOPAGER_ENV);
        nopager.set("");

        let pager = Pager::new();
        assert!(pager.pager().is_none());
    }

    #[test]
    fn fallback_uses_more() {
        let pager = Pager::new();
        assert_pager(&pager, DEFAULT_PAGER);
    }

    #[test]
    fn with_default_pager_without_env() {
        let pagerenv = PagerEnv::new(DEFAULT_PAGER_ENV);
        pagerenv.remove();

        let pager = Pager::with_default_pager("more_or_less");
        assert_pager(&pager, "more_or_less");
    }

    #[test]
    fn with_default_pager_with_env() {
        let pagerenv = PagerEnv::new(DEFAULT_PAGER_ENV);
        pagerenv.set("something_else");

        let pager = Pager::with_default_pager("more_or_less");
        assert_pager(&pager, "something_else");
    }

    #[test]
    fn with_default_pager() {
        let pager = Pager::with_default_pager("more_or_less");
        assert_pager(&pager, "more_or_less");
    }

    #[test]
    fn with_pager() {
        let pager = Pager::with_pager("now_or_never");
        assert_pager(&pager, "now_or_never");
    }
}