Repository: twpayne/chezmoi (2026-04-14T18:06:15Z)

Issue:
Allow to invoke `stdinIsATTY` in regular templates
<!--

chezmoi has a strict zero-tolerance policy on LLM contributions. If you use an
LLM (Large Language Model, like ChatGPT, Claude, Gemini, GitHub Copilot, or
Llama) to make any kind of contribution then you will immediately be banned
without recourse.

-->

## Is your feature request related to a problem? Please describe.

I am writing a chezmoi script that will prompt the user whether they want to restore or create a GPG key.

That script, however, should not execute when the stdin is not interactive (like in scripts or docker containers).

The `stdinIsATTY` function serves that purpose well, but I can only use it during init.

## Describe the solution you'd like

It would be nice if `stdinIsATTY` was available to all templates, beyond just init.

## Describe alternatives you've considered

I can check that within the script itself, like `test -t 0` in Bash.

## Additional context

N/A


Discussion:
> I can check that within the script itself, like `test -t 0` in Bash.

What is the downside of using the `test -t 0` approach? In my mind it makes more sense to keep all the logic in a single language if possible, i.e. if you can do it in the shell script, then it's better to do that rather than mixing templates and shell scripts.
The complete logic is actually:

```
{{- $chezmoiForce := or (has "--force" .chezmoi.args) (has "--force=true" .chezmoi.args) -}}
{{- $interactive := and stdinIsATTY (not $chezmoiForce) -}}
```

Meaning I already require templating to access `.chezmoi.args` (i.e. all logic in bash is not achievable anyway).

Also, my intention is to filter the script through `.chezmoiignore` rather than filtering it in the script itself. There, I can only use templates.
Makes sense, thank you for the context.

#5016 should fix this. Would you be able to test this?

Note that

> {{- $chezmoiForce := or (has "--force" .chezmoi.args) (has "--force=true" .chezmoi.args) -}}

does not always detect whether `--force` is set. For example `--force=yes` is another way of setting `--force`. Feel free to open another issue for this. I'm wondering if a template variable like `.chezmoi.flags.force`, populated from chezmoi's flag parsing, might be a more robust solution here.

Awesome. It works great!

```console
> ./chezmoi execute-template '{{stdinIsATTY}}'
true

> docker run --rm -v $PWD:$PWD -w $PWD ubuntu ./chezmoi execute-template '{{stdinIsATTY}}'
false
```

---

> does not always detect whether `--force` is set. For example `--force=yes` is another way of setting `--force`. Feel free to open another issue for this. I'm wondering if a template variable like `.chezmoi.flags.force`, populated from chezmoi's flag parsing, might be a more robust solution here.

Yes, that'd be nice. https://github.com/twpayne/chezmoi/issues/5020
Thanks a lot, @twpayne!



Patch:
diff --git a/assets/chezmoi.io/docs/reference/templates/init-functions/stdinIsATTY.md b/assets/chezmoi.io/docs/reference/templates/functions/stdinIsATTY.md
similarity index 79%
rename from assets/chezmoi.io/docs/reference/templates/init-functions/stdinIsATTY.md
rename to assets/chezmoi.io/docs/reference/templates/functions/stdinIsATTY.md
index 763e6dc27f6..812e0a52f61 100644
--- a/assets/chezmoi.io/docs/reference/templates/init-functions/stdinIsATTY.md
+++ b/assets/chezmoi.io/docs/reference/templates/functions/stdinIsATTY.md
@@ -1,8 +1,8 @@
 # `stdinIsATTY`
 
 `stdinIsATTY` returns `true` if chezmoi's standard input is a TTY. It is
-primarily useful for determining whether `prompt*` functions should be called
-or default values be used.
+primarily useful for determining whether `prompt*` functions should be called in
+a config file template or if a default value should be used instead.
 
 !!! example
 
diff --git a/assets/chezmoi.io/mkdocs.yml b/assets/chezmoi.io/mkdocs.yml
index 4dd6a9f0304..74ab13dfe4f 100644
--- a/assets/chezmoi.io/mkdocs.yml
+++ b/assets/chezmoi.io/mkdocs.yml
@@ -242,6 +242,7 @@ nav:
       - replaceAllRegex: reference/templates/functions/replaceAllRegex.md
       - setValueAtPath: reference/templates/functions/setValueAtPath.md
       - stat: reference/templates/functions/stat.md
+      - stdinIsATTY: reference/templates/functions/stdinIsATTY.md
       - toIni: reference/templates/functions/toIni.md
       - toPrettyJson: reference/templates/functions/toPrettyJson.md
       - toString: reference/templates/functions/toString.md
@@ -272,7 +273,6 @@ nav:
       - promptMultichoiceOnce: reference/templates/init-functions/promptMultichoiceOnce.md
       - promptString: reference/templates/init-functions/promptString.md
       - promptStringOnce: reference/templates/init-functions/promptStringOnce.md
-      - stdinIsATTY: reference/templates/init-functions/stdinIsATTY.md
       - writeToStdout: reference/templates/init-functions/writeToStdout.md
     - 1Password functions:
       - reference/templates/1password-functions/index.md
diff --git a/internal/cmd/config.go b/internal/cmd/config.go
index 0e4b21681a9..aebfa20ed7e 100644
--- a/internal/cmd/config.go
+++ b/internal/cmd/config.go
@@ -569,6 +569,7 @@ func newConfig(options ...configOption) (*Config, error) {
 		"splitList":                   c.splitListTemplateFunc,
 		"squote":                      c.squoteTemplateFunc,
 		"stat":                        c.statTemplateFunc,
+		"stdinIsATTY":                 c.stdinIsATTYTemplateFunc,
 		"toIni":                       c.toIniTemplateFunc,
 		"toPrettyJson":                c.toPrettyJsonTemplateFunc,
 		"toString":                    c.toStringTemplateFunc,
@@ -949,7 +950,6 @@ func (c *Config) createConfigFileContents(filename chezmoi.RelPath, data []byte,
 		"promptMultichoiceOnce": c.promptMultichoiceOnceInteractiveTemplateFunc,
 		"promptString":          c.promptStringInteractiveTemplateFunc,
 		"promptStringOnce":      c.promptStringOnceInteractiveTemplateFunc,
-		"stdinIsATTY":           c.stdinIsATTYInitTemplateFunc,
 		"writeToStdout":         c.writeToStdout,
 	}
 	chezmoi.RecursiveMerge(c.templateFuncs, initTemplateFuncs)
diff --git a/internal/cmd/executetemplatecmd.go b/internal/cmd/executetemplatecmd.go
index 22a47546acb..b5e443f59a8 100644
--- a/internal/cmd/executetemplatecmd.go
+++ b/internal/cmd/executetemplatecmd.go
@@ -237,10 +237,6 @@ func (c *Config) runExecuteTemplateCmd(cmd *cobra.Command, args []string) error
 			return promptStringInitTemplateFunc(prompt, args...)
 		}
 
-		stdinIsATTYInitTemplateFunc := func() bool {
-			return c.executeTemplate.stdinIsATTY
-		}
-
 		initTemplateFuncs := map[string]any{
 			"exit":                  c.exitInitTemplateFunc,
 			"promptBool":            promptBoolInitTemplateFunc,
@@ -253,7 +249,6 @@ func (c *Config) runExecuteTemplateCmd(cmd *cobra.Command, args []string) error
 			"promptMultichoiceOnce": promptMultichoiceOnceInitTemplateFunc,
 			"promptString":          promptStringInitTemplateFunc,
 			"promptStringOnce":      promptStringOnceInitTemplateFunc,
-			"stdinIsATTY":           stdinIsATTYInitTemplateFunc,
 			"writeToStdout":         c.writeToStdout,
 		}
 
diff --git a/internal/cmd/inittemplatefuncs.go b/internal/cmd/inittemplatefuncs.go
index 23b4bd59587..a2dfc8a2240 100644
--- a/internal/cmd/inittemplatefuncs.go
+++ b/internal/cmd/inittemplatefuncs.go
@@ -1,10 +1,6 @@
 package cmd
 
 import (
-	"os"
-
-	"golang.org/x/term"
-
 	"chezmoi.io/chezmoi/internal/chezmoi"
 )
 
@@ -12,17 +8,6 @@ func (c *Config) exitInitTemplateFunc(code int) string {
 	panic(chezmoi.ExitCodeError(code))
 }
 
-func (c *Config) stdinIsATTYInitTemplateFunc() bool {
-	if c.noTTY {
-		return false
-	}
-	file, ok := c.stdin.(*os.File)
-	if !ok {
-		return false
-	}
-	return term.IsTerminal(int(file.Fd()))
-}
-
 func (c *Config) writeToStdout(args ...string) string {
 	for _, arg := range args {
 		_ = mustValue(c.stdout.Write([]byte(arg)))
diff --git a/internal/cmd/templatefuncs.go b/internal/cmd/templatefuncs.go
index b97deb74d7e..78147dd3371 100644
--- a/internal/cmd/templatefuncs.go
+++ b/internal/cmd/templatefuncs.go
@@ -22,6 +22,7 @@ import (
 	"github.com/bmatcuk/doublestar/v4"
 	"github.com/bradenhilton/mozillainstallhash"
 	"github.com/itchyny/gojq"
+	"golang.org/x/term"
 	"gopkg.in/ini.v1"
 	"howett.net/plist"
 
@@ -498,6 +499,17 @@ func (c *Config) statTemplateFunc(name string) any {
 	}
 }
 
+func (c *Config) stdinIsATTYTemplateFunc() bool {
+	if c.noTTY {
+		return false
+	}
+	file, ok := c.stdin.(*os.File)
+	if !ok {
+		return false
+	}
+	return term.IsTerminal(int(file.Fd()))
+}
+
 func (c *Config) toIniTemplateFunc(data map[string]any) string {
 	var builder strings.Builder
 	must(writeIniMap(&builder, data, ""))


Test patch:
diff --git a/internal/cmd/testdata/scripts/templatefuncs.txtar b/internal/cmd/testdata/scripts/templatefuncs.txtar
index 71d36bb4b34..19cb96f5eac 100644
--- a/internal/cmd/testdata/scripts/templatefuncs.txtar
+++ b/internal/cmd/testdata/scripts/templatefuncs.txtar
@@ -178,6 +178,10 @@ exec chezmoi execute-template '{{ dict | setValueAtPath "key1.key2" "value2" | t
 rmfinalnewline golden/setValueAtPath
 cmp stdout golden/setValueAtPath
 
+# test stdinIsATTY template function
+exec chezmoi execute-template '{{ stdinIsATTY }}'
+stdout false
+
 # test toIni template function
 exec chezmoi execute-template '{{ dict "key" "value" "section" (dict "subkey" "subvalue") | toIni }}'
 cmp stdout golden/toIni