Repository: mitsuhiko/minijinja (language: rust)

Issue:
Feature request: Inline replacement
## Description

It would be nice if minijinja could make inline replacements. For example, specifying `--inline` would modify the template file with the final output.


Discussion / hints:
I would need more information here. I'm not sure I understand the feature request here.
@mitsuhiko If you have
```
$ cat input.txt
Hello {{ name }}
```

Running something like `minijinja --inline input.txt -D name="Test" output.txt` would result in
```
$ cat output.txt
Test
```
How is this different from what you can already do?

```
minijinja-cli input.txt -D name="Test" > output.txt
```
@mitsuhiko Correction - if you have
```
$ cat input.txt
Hello {{ name }}
```

Running something like `minijinja --inline input.txt -D name="Test"` would result in
```
$ cat input.txt
Test
```

Doing that with v1.0.10 results in `input.txt` being empty, similar to when you do `cat some-file.txt | [ do something here ] > some-file.txt`
Sorry i absolutely do not comprehend. How does

```
Hello {{ name }}
```

Turn into

```
Test
```

?

What it sounds in a way to me is that you want to replace the template file in-place? I'm struggling to understand why you would want that. But even if you do, you can do this:

```
minijinja-cli input.txt -D name="Test" > input.txt
```
@mitsuhiko When I run `minijinja-cli input.txt -D name="Test" > input.txt`, `input.txt` is empty.

Can you post the results of `cat input.txt`? I believe you'll find it is empty.
Yes you are right. If the filenames match then the shell will truncate the file before running. So the ask here is basically to let MiniJinja write to a file other than stdout itself so that the output could be placed to the same file as the input file?

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9870e3de..0cb2c7a5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
 
 All notable changes to MiniJinja are documented here.
 
+## 1.0.13
+
+- `minijinja-cli` now supports an `-o` or `--output` parameter to write
+  into a target file.  #405
+
 ## 1.0.12
 
 - The `urlencode` filter now correctly skips over none and undefined.  #394
diff --git a/Cargo.lock b/Cargo.lock
index d8d7c4ee..bd5b9773 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -961,12 +961,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
 
 [[package]]
 name = "errno"
-version = "0.3.6"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e"
+checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
 dependencies = [
  "libc",
- "windows-sys 0.48.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -1001,6 +1001,12 @@ dependencies = [
  "serde_json",
 ]
 
+[[package]]
+name = "fastrand"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
+
 [[package]]
 name = "fd-lock"
 version = "3.0.13"
@@ -1703,9 +1709,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "libc"
-version = "0.2.150"
+version = "0.2.152"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
+checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
 
 [[package]]
 name = "libm"
@@ -1721,9 +1727,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
 
 [[package]]
 name = "linux-raw-sys"
-version = "0.4.11"
+version = "0.4.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
+checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
 
 [[package]]
 name = "liquid"
@@ -1939,6 +1945,7 @@ dependencies = [
  "serde_json5",
  "serde_qs",
  "serde_yaml",
+ "tempfile",
  "toml 0.7.8",
 ]
 
@@ -2625,15 +2632,15 @@ dependencies = [
 
 [[package]]
 name = "rustix"
-version = "0.38.21"
+version = "0.38.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
+checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca"
 dependencies = [
  "bitflags 2.4.1",
  "errno",
  "libc",
  "linux-raw-sys",
- "windows-sys 0.48.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -2968,6 +2975,19 @@ version = "0.12.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a"
 
+[[package]]
+name = "tempfile"
+version = "3.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "redox_syscall 0.4.1",
+ "rustix",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "tera"
 version = "1.19.1"
@@ -3518,6 +3538,15 @@ dependencies = [
  "windows-targets 0.48.5",
 ]
 
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.0",
+]
+
 [[package]]
 name = "windows-targets"
 version = "0.42.2"
@@ -3548,6 +3577,21 @@ dependencies = [
  "windows_x86_64_msvc 0.48.5",
 ]
 
+[[package]]
+name = "windows-targets"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.0",
+ "windows_aarch64_msvc 0.52.0",
+ "windows_i686_gnu 0.52.0",
+ "windows_i686_msvc 0.52.0",
+ "windows_x86_64_gnu 0.52.0",
+ "windows_x86_64_gnullvm 0.52.0",
+ "windows_x86_64_msvc 0.52.0",
+]
+
 [[package]]
 name = "windows_aarch64_gnullvm"
 version = "0.42.2"
@@ -3560,6 +3604,12 @@ version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
 
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
+
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.42.2"
@@ -3572,6 +3622,12 @@ version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
 
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
+
 [[package]]
 name = "windows_i686_gnu"
 version = "0.42.2"
@@ -3584,6 +3640,12 @@ version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
 
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
+
 [[package]]
 name = "windows_i686_msvc"
 version = "0.42.2"
@@ -3596,6 +3658,12 @@ version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
 
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
+
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.42.2"
@@ -3608,6 +3676,12 @@ version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
 
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
+
 [[package]]
 name = "windows_x86_64_gnullvm"
 version = "0.42.2"
@@ -3620,6 +3694,12 @@ version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
 
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
+
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.42.2"
@@ -3632,6 +3712,12 @@ version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
 
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
+
 [[package]]
 name = "winnow"
 version = "0.5.19"
diff --git a/minijinja-cli/Cargo.toml b/minijinja-cli/Cargo.toml
index 870ec970..564bb807 100644
--- a/minijinja-cli/Cargo.toml
+++ b/minijinja-cli/Cargo.toml
@@ -45,6 +45,7 @@ serde_json = "1.0.105"
 serde_json5 = { version = "0.1.0", optional = true }
 serde_qs = { version = "0.12.0", optional = true }
 serde_yaml = { version = "0.9.25", optional = true }
+tempfile = "3.9.0"
 toml = { version = "0.7.6", optional = true }
 
 [build-dependencies]
diff --git a/minijinja-cli/src/cli.rs b/minijinja-cli/src/cli.rs
index b22fc389..b7ff7cd4 100644
--- a/minijinja-cli/src/cli.rs
+++ b/minijinja-cli/src/cli.rs
@@ -38,6 +38,9 @@ pub(super) fn make_command() -> Command {
             #[cfg(feature = "repl")]
             arg!(--repl "starts the repl with the given data")
                 .conflicts_with_all(["expr", "template"]),
+            arg!(-o --output <FILENAME> "path tot he output file")
+                .default_value("-")
+                .value_parser(value_parser!(PathBuf)),
             arg!(template: [TEMPLATE] "path to the input template").default_value("-"),
             arg!(data: [DATA] "path to the data file").value_parser(value_parser!(PathBuf)),
         ])
diff --git a/minijinja-cli/src/main.rs b/minijinja-cli/src/main.rs
index 109297a3..07124836 100644
--- a/minijinja-cli/src/main.rs
+++ b/minijinja-cli/src/main.rs
@@ -1,10 +1,11 @@
 use std::borrow::Cow;
 use std::collections::BTreeMap;
+use std::io::Write;
 use std::path::{Path, PathBuf};
 use std::sync::Mutex;
 use std::{fs, io};
 
-use anyhow::{bail, Context, Error};
+use anyhow::{anyhow, bail, Context, Error};
 use clap::ArgMatches;
 use minijinja::machinery::{get_compiled_template, parse, tokenize, Instructions};
 use minijinja::{
@@ -14,17 +15,61 @@ use minijinja::{
 #[cfg(feature = "repl")]
 mod repl;
 
+const STDIN_STDOUT: &str = "-";
 mod cli;
 
-const STDIN: &str = "-";
-
 #[cfg(not(feature = "json5"))]
 use serde_json as preferred_json;
 #[cfg(feature = "json5")]
 use serde_json5 as preferred_json;
 
+struct Output {
+    temp: Option<(PathBuf, tempfile::NamedTempFile)>,
+}
+
+impl Output {
+    pub fn new(filename: &Path) -> Result<Output, Error> {
+        Ok(Output {
+            temp: if filename == Path::new(STDIN_STDOUT) {
+                None
+            } else {
+                let filename = std::env::current_dir()?.join(filename);
+                let ntf = tempfile::NamedTempFile::new_in(
+                    filename
+                        .parent()
+                        .ok_or_else(|| anyhow!("cannot write to root"))?,
+                )?;
+                Some((filename.to_path_buf(), ntf))
+            },
+        })
+    }
+
+    pub fn commit(&mut self) -> Result<(), Error> {
+        if let Some((filename, temp)) = self.temp.take() {
+            temp.persist(filename)?;
+        }
+        Ok(())
+    }
+}
+
+impl Write for Output {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        match self.temp {
+            Some((_, ref mut out)) => out.write(buf),
+            None => std::io::stdout().write(buf),
+        }
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        match self.temp {
+            Some((_, ref mut out)) => out.flush(),
+            None => std::io::stdout().flush(),
+        }
+    }
+}
+
 fn load_data(format: &str, path: &Path) -> Result<(BTreeMap<String, Value>, bool), Error> {
-    let (contents, stdin_used) = if path == Path::new(STDIN) {
+    let (contents, stdin_used) = if path == Path::new(STDIN_STDOUT) {
         (
             io::read_to_string(io::stdin()).context("unable to read data from stdin")?,
             true,
@@ -127,7 +172,7 @@ fn create_env(
         UndefinedBehavior::Lenient
     });
     env.set_path_join_callback(move |name, parent| {
-        let p = if parent == STDIN {
+        let p = if parent == STDIN_STDOUT {
             cwd.join(name)
         } else {
             Path::new(parent)
@@ -149,7 +194,7 @@ fn create_env(
             }
         }
 
-        if name == STDIN {
+        if name == STDIN_STDOUT {
             if stdin_used_for_data {
                 return Err(MError::new(
                     ErrorKind::InvalidOperation,
@@ -207,7 +252,7 @@ fn execute() -> Result<i32, Error> {
     let cwd = std::env::current_dir()?;
     let ctx = context!(..defines, ..base);
     let template = match matches.get_one::<String>("template").unwrap().as_str() {
-        STDIN => Cow::Borrowed(STDIN),
+        STDIN_STDOUT => Cow::Borrowed(STDIN_STDOUT),
         rel_name => Cow::Owned(cwd.join(rel_name).to_string_lossy().to_string()),
     };
     let allowed_template = if matches.get_flag("no-include") {
@@ -215,6 +260,7 @@ fn execute() -> Result<i32, Error> {
     } else {
         None
     };
+    let mut output = Output::new(matches.get_one::<PathBuf>("output").unwrap())?;
 
     let no_newline = matches.get_flag("no-newline");
 
@@ -223,9 +269,9 @@ fn execute() -> Result<i32, Error> {
     if let Some(expr) = matches.get_one::<String>("expr") {
         let rv = env.compile_expression(expr)?.eval(ctx)?;
         match matches.get_one::<String>("expr-out").unwrap().as_str() {
-            "print" => println!("{}", rv),
-            "json" => println!("{}", serde_json::to_string(&rv)?),
-            "json-pretty" => println!("{}", serde_json::to_string_pretty(&rv)?),
+            "print" => writeln!(&mut output, "{}", rv)?,
+            "json" => writeln!(&mut output, "{}", serde_json::to_string(&rv)?)?,
+            "json-pretty" => writeln!(&mut output, "{}", serde_json::to_string_pretty(&rv)?)?,
             "status" => {
                 return Ok(if let Ok(n) = i32::try_from(rv.clone()) {
                     n
@@ -241,23 +287,23 @@ fn execute() -> Result<i32, Error> {
         match dump.as_str() {
             "ast" => {
                 let tmpl = env.get_template(&template)?;
-                println!("{:#?}", parse(tmpl.source(), tmpl.name())?);
+                writeln!(&mut output, "{:#?}", parse(tmpl.source(), tmpl.name())?)?;
             }
             "tokens" => {
                 let tmpl = env.get_template(&template)?;
                 let tokens: Result<Vec<_>, _> =
                     tokenize(tmpl.source(), false, Default::default()).collect();
                 for (token, _) in tokens? {
-                    println!("{:?}", token);
+                    writeln!(&mut output, "{:?}", token)?;
                 }
             }
             "instructions" => {
                 let tmpl = env.get_template(&template)?;
                 let ctmpl = get_compiled_template(&tmpl);
                 for (block_name, instructions) in ctmpl.blocks.iter() {
-                    print_instructions(instructions, block_name);
+                    print_instructions(&mut output, instructions, block_name)?;
                 }
-                print_instructions(&ctmpl.instructions, "<root>");
+                print_instructions(&mut output, &ctmpl.instructions, "<root>")?;
             }
             _ => unreachable!(),
         }
@@ -269,24 +315,30 @@ fn execute() -> Result<i32, Error> {
     } else {
         let result = env.get_template(&template)?.render(ctx)?;
         if no_newline {
-            print!("{result}");
+            write!(&mut output, "{result}")?;
         } else {
-            println!("{result}");
+            writeln!(&mut output, "{result}")?;
         }
     }
 
+    output.commit()?;
     Ok(0)
 }
 
-fn print_instructions(instructions: &Instructions, block_name: &str) {
-    println!("Block: {block_name:?}");
+fn print_instructions(
+    output: &mut Output,
+    instructions: &Instructions,
+    block_name: &str,
+) -> Result<(), Error> {
+    writeln!(output, "Block: {block_name:?}")?;
     for idx in 0.. {
         if let Some(instruction) = instructions.get(idx) {
-            println!("  {idx:4}: {instruction:?}");
+            writeln!(output, "  {idx:4}: {instruction:?}")?;
         } else {
             break;
         }
     }
+    Ok(())
 }
 
 pub fn print_error(err: &Error) {


Test patch: