edit/
lib.rs

1//! `edit` lets you open and edit something in a text editor, regardless of platform.
2//! (Think `git commit`.)
3//!
4//! It works on Windows, Mac, and Linux, and [knows about] lots of different text editors to fall
5//! back upon in case standard environment variables such as `VISUAL` and `EDITOR` aren't set.
6//!
7//! ```rust,ignore
8//! let template = "Fill in the blank: Hello, _____!";
9//! let edited = edit::edit(template)?;
10//! println!("after editing: '{}'", edited);
11//! // after editing: 'Fill in the blank: Hello, world!'
12//! ```
13//!
14//! [knows about]: ../src/edit/lib.rs.html#31-61
15//!
16//! Features
17//! ========
18//!
19//! The `edit` crate has the following optional features:
20//!
21//! - `better-path` *(enabled by default)* — Use
22//!   [`which`](https://docs.rs/which) to locate executable programs in `PATH`.
23//!   If this is disabled, programs are still looked up in `PATH`, but a basic
24//!   search is used that does not check for executability.
25//!
26//! - `quoted-env` — Use [`shell-words`](https://docs.rs/shell-words) to split
27//!   apart the values of the `VISUAL` and `EDITOR` environment variables.  If
28//!   this is disabled, the envvars are split up on whitespace.
29
30use std::{
31    env,
32    ffi::OsStr,
33    fs,
34    io::{Error, ErrorKind, Result, Write},
35    path::{Path, PathBuf},
36    process::{Command, Stdio},
37};
38pub use tempfile::Builder;
39#[cfg(feature = "which")]
40use which::which;
41
42static ENV_VARS: &[&str] = &["VISUAL", "EDITOR"];
43
44// TODO: should we hardcode full paths as well in case $PATH is borked?
45#[cfg(not(any(target_os = "windows", target_os = "macos")))]
46#[rustfmt::skip]
47static HARDCODED_NAMES: &[&str] = &[
48    // CLI editors
49    "sensible-editor", "nano", "pico", "vim", "nvim", "vi", "emacs",
50    // GUI editors
51    "code", "atom", "subl", "gedit", "gvim",
52    // Generic "file openers"
53    "xdg-open", "gnome-open", "kde-open",
54];
55
56#[cfg(target_os = "macos")]
57#[rustfmt::skip]
58static HARDCODED_NAMES: &[&str] = &[
59    // CLI editors
60    "nano", "pico", "vim", "nvim", "vi", "emacs",
61    // open has a special flag to open in the default text editor
62    // (this really should come before the CLI editors, but in order
63    // not to break compatibility, we still prefer CLI over GUI)
64    "open -Wt",
65    // GUI editors
66    "code -w", "atom -w", "subl -w", "gvim", "mate",
67    // Generic "file openers"
68    "open -a TextEdit",
69    "open -a TextMate",
70    // TODO: "open -f" reads input from standard input and opens with
71    // TextEdit. if this flag were used we could skip the tempfile
72    "open",
73];
74
75#[cfg(target_os = "windows")]
76#[rustfmt::skip]
77static HARDCODED_NAMES: &[&str] = &[
78    // GUI editors
79    "code.cmd -n -w", "atom.exe -w", "subl.exe -w",
80    // notepad++ does not block for input
81    // Installed by default
82    "notepad.exe",
83    // Generic "file openers"
84    "cmd.exe /C start",
85];
86
87#[cfg(feature = "better-path")]
88fn get_full_editor_path<T: AsRef<OsStr>>(binary_name: T) -> which::Result<PathBuf> {
89    which(binary_name)
90}
91
92#[cfg(not(feature = "better-path"))]
93fn get_full_editor_path<T: AsRef<OsStr> + AsRef<Path>>(binary_name: T) -> Result<PathBuf> {
94    if let Some(paths) = env::var_os("PATH") {
95        for dir in env::split_paths(&paths) {
96            if dir.join(&binary_name).is_file() {
97                return Ok(dir.join(&binary_name));
98            }
99        }
100    }
101
102    Err(Error::from(ErrorKind::NotFound))
103}
104
105#[cfg(not(feature = "quoted-env"))]
106fn string_to_cmd(s: String) -> (PathBuf, Vec<String>) {
107    let mut args = s.split_ascii_whitespace();
108    (
109        args.next().unwrap().into(),
110        args.map(String::from).collect(),
111    )
112}
113
114#[cfg(feature = "quoted-env")]
115fn string_to_cmd(s: String) -> (PathBuf, Vec<String>) {
116    match shell_words::split(&s) {
117        Ok(mut v) if !v.is_empty() => (v.remove(0).into(), v),
118        _ => {
119            let mut args = s.split_ascii_whitespace();
120            (
121                args.next().unwrap().into(),
122                args.map(String::from).collect(),
123            )
124        }
125    }
126}
127
128fn get_full_editor_cmd(s: String) -> Result<(PathBuf, Vec<String>)> {
129    let (path, args) = string_to_cmd(s);
130    match get_full_editor_path(&path) {
131        Ok(result) => Ok((result, args)),
132        Err(_) if path.exists() => Ok((path, args)),
133        Err(_) => Err(Error::from(ErrorKind::NotFound)),
134    }
135}
136
137fn get_editor_args() -> Result<(PathBuf, Vec<String>)> {
138    ENV_VARS
139        .iter()
140        .filter_map(env::var_os)
141        .filter(|v| !v.is_empty())
142        .filter_map(|v| v.into_string().ok())
143        .filter_map(|s| get_full_editor_cmd(s).ok())
144        .next()
145        .or_else(|| {
146            HARDCODED_NAMES
147                .iter()
148                .map(|s| s.to_string())
149                .filter_map(|s| get_full_editor_cmd(s).ok())
150                .next()
151        })
152        .ok_or_else(|| Error::from(ErrorKind::NotFound))
153}
154
155/// Find the system default editor, if there is one.
156///
157/// This function checks several sources to find an editor binary (in order of precedence):
158///
159/// - the `VISUAL` environment variable
160/// - the `EDITOR` environment variable
161/// - hardcoded lists of common CLI editors on MacOS/Unix
162/// - hardcoded lists of GUI editors on Windows/MacOS/Unix
163/// - platform-specific generic "file openers" (e.g. `xdg-open` on Linux and `open` on MacOS)
164///
165/// Also, it doesn't blindly return whatever is in an environment variable. If a specified editor
166/// can't be found or isn't marked as executable (the executable bit is checked when the default
167/// feature `better-path` is enabled), this function will fall back to the next one that is.
168///
169/// # Returns
170///
171/// If successful, returns the name of the system default editor.
172/// Note that in most cases the full path of the editor isn't returned; what is guaranteed is the
173/// return value being suitable as the program name for e.g. [`Command::new`].
174///
175/// On some platforms, a text editor is installed by default, so the chances of a failure are low
176/// save for `PATH` being unset or something weird like that. However, it is possible for one not
177/// to be located, and in that case `get_editor` will return [`ErrorKind::NotFound`].
178///
179/// # Example
180///
181/// ```rust,ignore
182/// use edit::get_editor;
183///
184/// // will print e.g. "default editor: nano"
185/// println!("default editor:", get_editor().expect("can't find an editor").to_str());
186/// ```
187///
188/// [`Command::new`]: https://doc.rust-lang.org/std/process/struct.Command.html#method.new
189/// [`ErrorKind::NotFound`]: https://doc.rust-lang.org/std/io/enum.ErrorKind.html#variant.NotFound
190pub fn get_editor() -> Result<PathBuf> {
191    get_editor_args().map(|(x, _)| x)
192}
193
194/// Open the contents of a string or buffer in the [default editor].
195///
196/// This function saves its input to a temporary file and then opens the default editor to it.
197/// It waits for the editor to return, re-reads the (possibly changed/edited) temporary file, and
198/// then deletes it.
199///
200/// # Arguments
201///
202/// `text` is written to the temporary file before invoking the editor. (The editor opens with
203/// the contents of `text` already in the file).
204///
205/// # Returns
206///
207/// If successful, returns the edited string.
208/// If the edited version of the file can't be decoded as UTF-8, returns [`ErrorKind::InvalidData`].
209/// If no text editor could be found, returns [`ErrorKind::NotFound`].
210/// Any errors related to spawning the editor process will also be passed through.
211///
212/// [default editor]: fn.get_editor.html
213/// [`ErrorKind::InvalidData`]: https://doc.rust-lang.org/std/io/enum.ErrorKind.html#variant.InvalidData
214/// [`ErrorKind::NotFound`]: https://doc.rust-lang.org/std/io/enum.ErrorKind.html#variant.NotFound
215pub fn edit<S: AsRef<[u8]>>(text: S) -> Result<String> {
216    let builder = Builder::new();
217    edit_with_builder(text, &builder)
218}
219
220/// Open the contents of a string or buffer in the [default editor] using a temporary file with a
221/// custom path or filename.
222///
223/// This function saves its input to a temporary file created using `builder`, then opens the
224/// default editor to it. It waits for the editor to return, re-reads the (possibly changed/edited)
225/// temporary file, and then deletes it.
226///
227/// Other than the custom [`Builder`], this function is identical to [`edit`].
228///
229/// # Arguments
230///
231/// `builder` is used to create a temporary file, potentially with a custom name, path, or prefix.
232///
233/// `text` is written to the temporary file before invoking the editor. (The editor opens with
234/// the contents of `text` already in the file).
235///
236/// # Returns
237///
238/// If successful, returns the edited string.
239/// If the temporary file can't be created with the provided builder, may return any error returned
240/// by [`OpenOptions::open`].
241/// If the edited version of the file can't be decoded as UTF-8, returns [`ErrorKind::InvalidData`].
242/// If no text editor could be found, returns [`ErrorKind::NotFound`].
243/// Any errors related to spawning the editor process will also be passed through.
244///
245/// [default editor]: fn.get_editor.html
246/// [`edit`]: fn.edit.html
247/// [`Builder`]: struct.Builder.html
248/// [`OpenOptions::open`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#errors
249/// [`ErrorKind::InvalidData`]: https://doc.rust-lang.org/std/io/enum.ErrorKind.html#variant.InvalidData
250/// [`ErrorKind::NotFound`]: https://doc.rust-lang.org/std/io/enum.ErrorKind.html#variant.NotFound
251pub fn edit_with_builder<S: AsRef<[u8]>>(text: S, builder: &Builder) -> Result<String> {
252    String::from_utf8(edit_bytes_with_builder(text, builder)?)
253        .map_err(|_| Error::from(ErrorKind::InvalidData))
254}
255
256/// Open the contents of a string or buffer in the [default editor] and return them as raw bytes.
257///
258/// See [`edit`], the version of this function that takes and returns [`String`].
259///
260/// # Arguments
261///
262/// `buf` is written to the temporary file before invoking the editor.
263///
264/// # Returns
265///
266/// If successful, returns the contents of the temporary file in raw (`Vec<u8>`) form.
267///
268/// [default editor]: fn.get_editor.html
269/// [`edit`]: fn.edit.html
270/// [`String`]: https://doc.rust-lang.org/std/string/struct.String.html
271pub fn edit_bytes<B: AsRef<[u8]>>(buf: B) -> Result<Vec<u8>> {
272    let builder = Builder::new();
273    edit_bytes_with_builder(buf, &builder)
274}
275
276/// Open the contents of a string or buffer in the [default editor] using a temporary file with a
277/// custom path or filename and return them as raw bytes.
278///
279/// See [`edit_with_builder`], the version of this function that takes and returns [`String`].
280///
281/// Other than the custom [`Builder`], this function is identical to [`edit_bytes`].
282///
283/// # Arguments
284///
285/// `builder` is used to create a temporary file, potentially with a custom name, path, or prefix.
286///
287/// `buf` is written to the temporary file before invoking the editor.
288///
289/// # Returns
290///
291/// If successful, returns the contents of the temporary file in raw (`Vec<u8>`) form.
292///
293/// [default editor]: fn.get_editor.html
294/// [`edit_with_builder`]: fn.edit_with_builder.html
295/// [`String`]: https://doc.rust-lang.org/std/string/struct.String.html
296/// [`Builder`]: struct.Builder.html
297/// [`edit_bytes`]: fn.edit_bytes.html
298pub fn edit_bytes_with_builder<B: AsRef<[u8]>>(buf: B, builder: &Builder) -> Result<Vec<u8>> {
299    let mut file = builder.tempfile()?;
300    file.write_all(buf.as_ref())?;
301
302    let path = file.into_temp_path();
303    edit_file(&path)?;
304
305    let edited = fs::read(&path)?;
306
307    path.close()?;
308    Ok(edited)
309}
310
311/// Open an existing file (or create a new one, depending on the editor's behavior) in the
312/// [default editor] and wait for the editor to exit.
313///
314/// # Arguments
315///
316/// A [`Path`] to a file, new or existing, to open in the default editor.
317///
318/// # Returns
319///
320/// A Result is returned in case of errors finding or spawning the editor, but the contents of the
321/// file are not read and returned as in [`edit`] and [`edit_bytes`].
322///
323/// [default editor]: fn.get_editor.html
324/// [`Path`]: https://doc.rust-lang.org/std/path/struct.Path.html
325/// [`edit`]: fn.edit.html
326/// [`edit_bytes`]: fn.edit_bytes.html
327pub fn edit_file<P: AsRef<Path>>(file: P) -> Result<()> {
328    let (editor, args) = get_editor_args()?;
329    let status = Command::new(&editor)
330        .args(&args)
331        .arg(file.as_ref())
332        .stdin(Stdio::inherit())
333        .stdout(Stdio::inherit())
334        .stderr(Stdio::inherit())
335        .output()?
336        .status;
337
338    if status.success() {
339        Ok(())
340    } else {
341        let full_command = if args.is_empty() {
342            format!(
343                "{} {}",
344                editor.to_string_lossy(),
345                file.as_ref().to_string_lossy()
346            )
347        } else {
348            format!(
349                "{} {} {}",
350                editor.to_string_lossy(),
351                args.join(" "),
352                file.as_ref().to_string_lossy()
353            )
354        };
355
356        Err(Error::new(
357            ErrorKind::Other,
358            format!("editor '{}' exited with error: {}", full_command, status),
359        ))
360    }
361}
362
363/// Open an existing file (or create a new one, depending on the editor's behavior) in the
364/// [default editor] and *do not* wait for the editor to exit.
365///
366/// # Arguments
367///
368/// A [`Path`] to a file, new or existing, to open in the default editor.
369///
370/// # Returns
371///
372/// A Result is returned in case of errors finding or spawning the editor, but the contents of the
373/// file are not read and returned as in [`edit`] and [`edit_bytes`].
374///
375/// [default editor]: fn.get_editor.html
376/// [`Path`]: https://doc.rust-lang.org/std/path/struct.Path.html
377/// [`edit`]: fn.edit.html
378/// [`edit_bytes`]: fn.edit_bytes.html
379pub fn edit_file_without_waiting<P: AsRef<Path>>(file: P) -> Result<()> {
380    let (editor, args) = get_editor_args()?;
381    Command::new(&editor)
382        .args(&args)
383        .arg(file.as_ref())
384        .stdin(Stdio::null())
385        .stdout(Stdio::null())
386        .stderr(Stdio::null())
387        .spawn()?;
388    Ok(())
389}