From 61345cbdc97d4820dd564bcb136f1526c4b54e16 Mon Sep 17 00:00:00 2001
From: Jeffrey Finkelstein <jeffrey.finkelstein@protonmail.com>
Date: Sat, 21 May 2022 23:31:12 -0400
Subject: [PATCH] mktemp: respect TMPDIR environment variable

Change `mktemp` so that it respects the value of the `TMPDIR`
environment variable if no directory is otherwise specified in its
arguments. For example, before this commit

    $ TMPDIR=. mktemp
    /tmp/tmp.WDJ66MaS1T

After this commit,

    $ TMPDIR=. mktemp
    ./tmp.h96VZBhv8P

This matches the behavior of GNU `mktemp`.
---
 src/uu/mktemp/src/mktemp.rs  | 21 ++++++++---
 tests/by-util/test_mktemp.rs | 72 ++++++++++++++++++++++++++++++++++++
 2 files changed, 87 insertions(+), 6 deletions(-)

diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs
index 1131e0f01c0..dfa7fd10303 100644
--- a/src/uu/mktemp/src/mktemp.rs
+++ b/src/uu/mktemp/src/mktemp.rs
@@ -170,12 +170,21 @@ impl Options {
             let template = matches.value_of(OPT_TMPDIR).unwrap().to_string();
             (tmpdir, template)
         } else {
-            let tmpdir = matches.value_of(OPT_TMPDIR).map(String::from);
-            let template = matches
-                .value_of(ARG_TEMPLATE)
-                .unwrap_or(DEFAULT_TEMPLATE)
-                .to_string();
-            (tmpdir, template)
+            // If no template argument is given, `--tmpdir` is implied.
+            match matches.value_of(ARG_TEMPLATE) {
+                None => {
+                    let tmpdir = match matches.value_of(OPT_TMPDIR) {
+                        None => Some(env::temp_dir().display().to_string()),
+                        Some(tmpdir) => Some(tmpdir.to_string()),
+                    };
+                    let template = DEFAULT_TEMPLATE;
+                    (tmpdir, template.to_string())
+                }
+                Some(template) => {
+                    let tmpdir = matches.value_of(OPT_TMPDIR).map(String::from);
+                    (tmpdir, template.to_string())
+                }
+            }
         };
         Self {
             directory: matches.contains_id(OPT_DIRECTORY),
diff --git a/tests/by-util/test_mktemp.rs b/tests/by-util/test_mktemp.rs
index 9e4a2742cd8..8b58672a2d6 100644
--- a/tests/by-util/test_mktemp.rs
+++ b/tests/by-util/test_mktemp.rs
@@ -5,6 +5,8 @@ use crate::common::util::*;
 use uucore::display::Quotable;
 
 use std::path::PathBuf;
+#[cfg(not(windows))]
+use std::path::MAIN_SEPARATOR;
 use tempfile::tempdir;
 
 #[cfg(unix)]
@@ -39,6 +41,22 @@ macro_rules! assert_matches_template {
     }};
 }
 
+/// Like [`assert_matches_template`] but for the suffix of a string.
+#[cfg(windows)]
+macro_rules! assert_suffix_matches_template {
+    ($template:expr, $s:expr) => {{
+        let n = ($s).len();
+        let m = ($template).len();
+        let suffix = &$s[n - m..n];
+        assert!(
+            matches_template($template, suffix),
+            "\"{}\" does not end with \"{}\"",
+            $template,
+            suffix
+        );
+    }};
+}
+
 #[test]
 fn test_mktemp_mktemp() {
     let scene = TestScenario::new(util_name!());
@@ -663,3 +681,57 @@ fn test_mktemp_with_posixly_correct() {
         .args(&["--suffix=b", "aXXXX"])
         .succeeds();
 }
+
+/// Test that files are created relative to `TMPDIR` environment variable.
+#[test]
+fn test_tmpdir_env_var() {
+    // `TMPDIR=. mktemp`
+    let (at, mut ucmd) = at_and_ucmd!();
+    let result = ucmd.env(TMPDIR, ".").succeeds();
+    let filename = result.no_stderr().stdout_str().trim_end();
+    #[cfg(not(windows))]
+    {
+        let template = format!(".{}tmp.XXXXXXXXXX", MAIN_SEPARATOR);
+        assert_matches_template!(&template, filename);
+    }
+    // On Windows, `env::temp_dir()` seems to give an absolute path
+    // regardless of the value of `TMPDIR`; see
+    // * https://github.com/uutils/coreutils/pull/3552#issuecomment-1211804981
+    // * https://doc.rust-lang.org/std/env/fn.temp_dir.html
+    // * https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppath2w
+    #[cfg(windows)]
+    assert_suffix_matches_template!("tmp.XXXXXXXXXX", filename);
+    assert!(at.file_exists(filename));
+
+    // FIXME This is not working because --tmpdir is configured to
+    // require a value.
+    //
+    // // `TMPDIR=. mktemp --tmpdir`
+    // let (at, mut ucmd) = at_and_ucmd!();
+    // let result = ucmd.env(TMPDIR, ".").arg("--tmpdir").succeeds();
+    // let filename = result.no_stderr().stdout_str().trim_end();
+    // let template = format!(".{}tmp.XXXXXXXXXX", MAIN_SEPARATOR);
+    // assert_matches_template!(&template, filename);
+    // assert!(at.file_exists(filename));
+
+    // `TMPDIR=. mktemp --tmpdir XXX`
+    let (at, mut ucmd) = at_and_ucmd!();
+    let result = ucmd.env(TMPDIR, ".").args(&["--tmpdir", "XXX"]).succeeds();
+    let filename = result.no_stderr().stdout_str().trim_end();
+    #[cfg(not(windows))]
+    {
+        let template = format!(".{}XXX", MAIN_SEPARATOR);
+        assert_matches_template!(&template, filename);
+    }
+    #[cfg(windows)]
+    assert_suffix_matches_template!("XXX", filename);
+    assert!(at.file_exists(filename));
+
+    // `TMPDIR=. mktemp XXX` - in this case `TMPDIR` is ignored.
+    let (at, mut ucmd) = at_and_ucmd!();
+    let result = ucmd.env(TMPDIR, ".").arg("XXX").succeeds();
+    let filename = result.no_stderr().stdout_str().trim_end();
+    let template = "XXX";
+    assert_matches_template!(template, filename);
+    assert!(at.file_exists(filename));
+}