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");
    }
}