1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
//! It's well known that a program cannot modify the environment of its parent shell.
//! But this is useful to do, and we can use some tricks to do this.  Almost all shells support
//! some way to evaluate the output of programs (even Windows), so by returning the right commands
//! to be eval'd by the parent shell, we can apply these changes.
//!
//! # Install
//!
//! Add the following to your `Cargo.toml` file:
//!
//! ```toml
//! [dependencies]
//! setenv = "0.1"
//! ```
//!
//! # Usage
//!
//! This library provides two things:
//!
//! 1. Some code to try to detect what shell is in use
//! 2. What syntax is needed to do certain actions.
//!
//! At the moment, the only two commands supported are [`cd`] for changing directories, and
//! [`setenv`] for setting environment variables.
//!
//! Two other functions are also provided as a convienence: `split_env` which is just a wrapper
//! around std::env::split_paths, and `set_env_list` which is a wrapper around
//! std::env::join_paths.
//!
//! # Examples
//!
//! To make use of all this, each executable using `setenv` should be wrapped in an
//! alias/function/bat file.  Here are some examples:
//!
//! ## Windows:
//!
//! ```text
//! for /f "tokens=*" %%I in ('d:\target\debug\myapp.exe  %*') do (
//!   %%I
//! )
//! ```
//!
//! ### Bash:
//!
//! ```c
//! function dothing() {
//!   eval `/target/debug/myapp "$@"`
//! }
//! ```
//!
//! ## Ksh:
//!
//! ```text
//! dothing() {
//!   eval `/target/debug/myapp "$@"`
//! }
//! ```
//!
//! ## Zsh:
//!
//! ```text
//! function dothing() {
//!   eval `/target/debug/myapp $*`
//! }
//! ```
//!
//! ## Tcsh:
//!
//! ```text
//! alias dothing 'eval `/target/debug/myapp \!*`'
//! ```
//!
//! # Notes
//!
//! Since all text send to stdout is eval'd by the shell, great care must be taken to control what
//! is printed to stdout.  All user-facing messages should go to stderr instead.
//!
//! [`cd`]: enum.Shell.html#method.cd
//! [`setenv`]: enum.Shell.html#method.setenv

use std::ffi::{OsString, OsStr};
use std::convert::AsRef;
use std::env::var_os;

/// THe types of shells we know about
pub enum Shell {
    Windows,
    /// The default if we can't figure out the shell
    Bash,
    Tcsh,
    Zsh,
    Ksh,
}

/// Figure out what shell we are using.  If we can't figure it out, fallback to `Bash`, since many
/// shells support the same `export foo=bar` syntax from bash.
pub fn get_shell() -> Shell {
    if cfg!(windows) {
        Shell::Windows
    } else {

        if let Some(shell) = var_os("BASH") {
            if shell.to_string_lossy().ends_with("/bash") {
                return Shell::Bash;
            }
        }
        if let Some(zsh) = var_os("ZSH_NAME") {
            if zsh.to_string_lossy() == "zsh" {
                return Shell::Zsh;
            }
        }
        if let Some(shell) = var_os("shell") {
            if shell.to_string_lossy().ends_with("/tcsh") {
                return Shell::Tcsh;
            }
        }
        return match var_os("SHELL") {
            None => Shell::Bash,
            Some(oss) => {
                if oss.to_string_lossy().ends_with("/bash") {
                    Shell::Bash
                } else if oss.to_string_lossy().ends_with("/ksh") {
                    Shell::Ksh
                } else if oss.to_string_lossy().ends_with("/zsh") {
                    Shell::Zsh
                } else if oss.to_string_lossy().ends_with("/tcsh") {
                    Shell::Tcsh
                } else {
                    Shell::Bash
                } // many shells support export foo=bar
            }
        };
    }
}

impl Shell {
    /// Prints to stdout the necessary command to change directory.
    pub fn cd<P: AsRef<OsStr>>(&self, p: P) {
        match *self {
            Shell::Windows => {
                println!("cd /d {}", p.as_ref().to_string_lossy());
            }
            _ => {
                println!("cd '{}';", p.as_ref().to_string_lossy());
            }
        }
    }

    /// Prints to stdout the necessary command to set an envionrment variable
    pub fn setenv<K: AsRef<OsStr>, V: AsRef<OsStr>>(&self, k: K, v: V) {
        match *self {
            Shell::Windows => {
                println!("set {}={}",
                         k.as_ref().to_string_lossy(),
                         v.as_ref().to_string_lossy())
            }
            Shell::Tcsh => {
                println!("setenv {} '{}';",
                         k.as_ref().to_string_lossy(),
                         v.as_ref().to_string_lossy())
            }
            _ => {
                println!("export {}='{}';",
                         k.as_ref().to_string_lossy(),
                         v.as_ref().to_string_lossy())
            }
        }
    }

    /// A simple wrapper around `std::env::split_paths`
    pub fn split_env<K: AsRef<OsStr>>(&self, k: K) -> Vec<OsString> {
        match std::env::var(k) {
            Err(..) => Vec::new(),
            Ok(ref v) => std::env::split_paths(v).map(|p| p.into_os_string()).collect(),
        }
    }

    /// A simple wrapper around `std::env::join_paths` and `setenv`
    pub fn setenv_list<K, I, T>(&self, k: K, v: I)
        where K: AsRef<OsStr>,
              I: IntoIterator<Item = T>,
              T: AsRef<OsStr>
    {
        let paths = std::env::join_paths(v).unwrap();
        self.setenv(k, paths);
    }
}