From ec276dee89024de42ab1acd48f55b36e5f1ff7f4 Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Mon, 23 Dec 2024 15:21:02 +0000 Subject: [PATCH] feat: Add script hooks that use configured interpreters --- .../reference/configuration-file/hooks.md | 8 +- .../configuration-file/interpreters.md | 76 ++++++++++++++++++ .../configuration-file/variables.md.yaml | 4 +- .../chezmoi.io/docs/reference/target-types.md | 77 +------------------ assets/chezmoi.io/mkdocs.yml | 1 + internal/cmd/config.go | 53 +++++++++---- .../cmd/testdata/scripts/hooks_windows.txtar | 14 ++++ 7 files changed, 140 insertions(+), 93 deletions(-) create mode 100644 assets/chezmoi.io/docs/reference/configuration-file/interpreters.md create mode 100644 internal/cmd/testdata/scripts/hooks_windows.txtar diff --git a/assets/chezmoi.io/docs/reference/configuration-file/hooks.md b/assets/chezmoi.io/docs/reference/configuration-file/hooks.md index cc03d6112b9..6fa7ea03cab 100644 --- a/assets/chezmoi.io/docs/reference/configuration-file/hooks.md +++ b/assets/chezmoi.io/docs/reference/configuration-file/hooks.md @@ -15,7 +15,10 @@ Each event can have a `.pre` and/or a `.post` command. The *event*.`pre` command is executed before *event* occurs and the *event*`.post` command is executed after *event* has occurred. - A command contains a `command` and an optional array of strings `args`. + A command contains a `command` or `script` and an optional array of strings + `args`. `command`s are executed directly. `script`s are executed with +configured interpreter for the script's extension, see the [section on +interpreters](interpreters.md). !!! example @@ -27,6 +30,9 @@ after *event* has occurred. [hooks.apply.post] command = "echo" args = ["post-apply-hook"] + + [hooks.add.post] + script = "post-add-hook.ps1' ``` When running hooks, the `CHEZMOI=1` and `CHEZMOI_*` environment variables will diff --git a/assets/chezmoi.io/docs/reference/configuration-file/interpreters.md b/assets/chezmoi.io/docs/reference/configuration-file/interpreters.md new file mode 100644 index 00000000000..32272f8594f --- /dev/null +++ b/assets/chezmoi.io/docs/reference/configuration-file/interpreters.md @@ -0,0 +1,76 @@ +# Interpreters + + + +The execution of scripts and hooks on Windows depends on the file extension. +Windows will natively execute scripts with a `.bat`, `.cmd`, `.com`, and `.exe` +extensions. Other extensions require an interpreter, which must be in your +`%PATH%`. + +The default script interpreters are: + +| Extension | Command | Arguments | +| --------- | ------------ | --------- | +| `.nu` | `nu` | *none* | +| `.pl` | `perl` | *none* | +| `.py` | `python3` | *none* | +| `.ps1` | `powershell` | `-NoLogo` | +| `.rb` | `ruby` | *none* | + +Script interpreters can be added or overridden by adding the corresponding +extension (without the leading dot) as a key under the `interpreters` +section of the configuration file. + +!!! note + + The leading `.` is dropped from *extension*, for example to specify the + interpreter for `.pl` files you configure `interpreters.pl` (where `.` + in this case just means "a child of" in the configuration file, however + that is specified in your preferred format). + +!!! example + + To change the Python interpreter to `C:\Python39\python3.exe` and add a + Tcl/Tk interpreter, include the following in your config file: + + ```toml title="~/.config/chezmoi/chezmoi.toml" + [interpreters.py] + command = 'C:\Python39\python3.exe' + [interpreters.tcl] + command = "tclsh" + ``` + + Or if using YAML: + + ```yaml title="~/.config/chezmoi/chezmoi.yaml" + interpreters: + py: + command: "C:\Python39\python3.exe" + tcl: + command: "tclsh" + ``` + + Note that the TOML version can also be written like this, which + resembles the YAML version more and makes it clear that the key + for each file extension should not have a leading `.`: + + ```toml title="~/.config/chezmoi/chezmoi.toml" + [interpreters] + py = { command = 'C:\Python39\python3.exe' } + tcl = { command = "tclsh" } + ``` + +!!! note + + If you intend to use PowerShell Core (`pwsh.exe`) as the `.ps1` + interpreter, include the following in your config file: + + ```toml title="~/.config/chezmoi/chezmoi.toml" + [interpreters.ps1] + command = "pwsh" + args = ["-NoLogo"] + ``` + +If the script in the source state is a template (with a `.tmpl` extension), then +chezmoi will strip the `.tmpl` extension and use the next remaining extension to +determine the interpreter to use. diff --git a/assets/chezmoi.io/docs/reference/configuration-file/variables.md.yaml b/assets/chezmoi.io/docs/reference/configuration-file/variables.md.yaml index f8d8e4c2f4f..1e68751bc2a 100644 --- a/assets/chezmoi.io/docs/reference/configuration-file/variables.md.yaml +++ b/assets/chezmoi.io/docs/reference/configuration-file/variables.md.yaml @@ -285,10 +285,10 @@ sections: interpreters: '*extension*.`args`': type: '[]string' - description: See [Scripts on Windows](../target-types.md#scripts-on-windows) + description: See [Interpreters](interpreters.md) '*extension*.`command`': default: '*special*' - description: See [Scripts on Windows](../target-types.md#scripts-on-windows) + description: See [Interpreters](interpreters.md) keepassxc: args: type: '[]string' diff --git a/assets/chezmoi.io/docs/reference/target-types.md b/assets/chezmoi.io/docs/reference/target-types.md index 91c22df2d51..42743161990 100644 --- a/assets/chezmoi.io/docs/reference/target-types.md +++ b/assets/chezmoi.io/docs/reference/target-types.md @@ -96,82 +96,7 @@ chezmoi sets a number of `CHEZMOI*` environment variables when running scripts, corresponding to commonly-used template data variables. Extra environment variables can be set in the `env` or `scriptEnv` configuration variables. -### Scripts on Windows - - - -The execution of scripts on Windows depends on the script's file extension. -Windows will natively execute scripts with a `.bat`, `.cmd`, `.com`, and `.exe` -extensions. Other extensions require an interpreter, which must be in your -`%PATH%`. - -The default script interpreters are: - -| Extension | Command | Arguments | -| --------- | ------------ | --------- | -| `.nu` | `nu` | *none* | -| `.pl` | `perl` | *none* | -| `.py` | `python3` | *none* | -| `.ps1` | `powershell` | `-NoLogo` | -| `.rb` | `ruby` | *none* | - -Script interpreters can be added or overridden by adding the corresponding -extension (without the leading dot) as a key under the `interpreters` -section of the configuration file. - -!!! note - - The leading `.` is dropped from *extension*, for example to specify the - interpreter for `.pl` files you configure `interpreters.pl` (where `.` - in this case just means "a child of" in the configuration file, however - that is specified in your preferred format). - -!!! example - - To change the Python interpreter to `C:\Python39\python3.exe` and add a - Tcl/Tk interpreter, include the following in your config file: - - ```toml title="~/.config/chezmoi/chezmoi.toml" - [interpreters.py] - command = 'C:\Python39\python3.exe' - [interpreters.tcl] - command = "tclsh" - ``` - - Or if using YAML: - - ```yaml title="~/.config/chezmoi/chezmoi.yaml" - interpreters: - py: - command: "C:\Python39\python3.exe" - tcl: - command: "tclsh" - ``` - - Note that the TOML version can also be written like this, which - resembles the YAML version more and makes it clear that the key - for each file extension should not have a leading `.`: - - ```toml title="~/.config/chezmoi/chezmoi.toml" - [interpreters] - py = { command = 'C:\Python39\python3.exe' } - tcl = { command = "tclsh" } - ``` - -!!! note - - If you intend to use PowerShell Core (`pwsh.exe`) as the `.ps1` - interpreter, include the following in your config file: - - ```toml title="~/.config/chezmoi/chezmoi.toml" - [interpreters.ps1] - command = "pwsh" - args = ["-NoLogo"] - ``` - -If the script in the source state is a template (with a `.tmpl` extension), then -chezmoi will strip the `.tmpl` extension and use the next remaining extension to -determine the interpreter to use. +Scripts are executed using an interpreter, if configured. See the [section on interpreters](configuration-file/interpreters.md). ## `symlink` mode diff --git a/assets/chezmoi.io/mkdocs.yml b/assets/chezmoi.io/mkdocs.yml index 447c911a91b..6a8e2197aac 100644 --- a/assets/chezmoi.io/mkdocs.yml +++ b/assets/chezmoi.io/mkdocs.yml @@ -117,6 +117,7 @@ nav: - Variables: reference/configuration-file/variables.md - Editor: reference/configuration-file/editor.md - Hooks: reference/configuration-file/hooks.md + - Interpreters: reference/configuration-file/interpreters.md - pinentry: reference/configuration-file/pinentry.md - textconv: reference/configuration-file/textconv.md - umask: reference/configuration-file/umask.md diff --git a/internal/cmd/config.go b/internal/cmd/config.go index 7cb3c70e644..9c3a6a0744d 100644 --- a/internal/cmd/config.go +++ b/internal/cmd/config.go @@ -17,6 +17,7 @@ import ( "os" "os/exec" "os/user" + "path" "path/filepath" "reflect" "regexp" @@ -79,6 +80,7 @@ type doPurgeOptions struct { type commandConfig struct { Command string `json:"command" mapstructure:"command" yaml:"command"` + Script string `json:"script" mapstructure:"script" yaml:"script"` Args []string `json:"args" mapstructure:"args" yaml:"args"` } @@ -1213,7 +1215,7 @@ func (c *Config) destAbsPathInfos( // diffFile outputs the diff between fromData and fromMode and toData and toMode // at path. func (c *Config) diffFile( - path chezmoi.RelPath, + relPath chezmoi.RelPath, fromData []byte, fromMode fs.FileMode, toData []byte, @@ -1227,19 +1229,19 @@ func (c *Config) diffFile( } if fromMode.IsRegular() { var err error - fromData, _, err = c.TextConv.convert(path.String(), fromData) + fromData, _, err = c.TextConv.convert(relPath.String(), fromData) if err != nil { return err } } if toMode.IsRegular() { var err error - toData, _, err = c.TextConv.convert(path.String(), toData) + toData, _, err = c.TextConv.convert(relPath.String(), toData) if err != nil { return err } } - diffPatch, err := chezmoi.DiffPatch(path, fromData, fromMode, toData, toMode) + diffPatch, err := chezmoi.DiffPatch(relPath, fromData, fromMode, toData, toMode) if err != nil { return err } @@ -2515,22 +2517,45 @@ func (c *Config) runEditor(args []string) error { return err } +// runHook runs a command or script hook. +func (c *Config) runHook(command commandConfig) error { + var name string + var args []string + switch { + case command.Command != "" && command.Script != "": + return errors.New("cannot specify both command and script") + case command.Command != "": + name = command.Command + args = command.Args + case command.Script != "": + extension := strings.TrimPrefix(strings.ToLower(path.Ext(command.Script)), ".") + if interpreter, ok := c.Interpreters[extension]; ok { + name = interpreter.Command + args = slices.Concat(interpreter.Args, []string{command.Script}, command.Args) + } else { + name = command.Script + args = command.Args + } + default: + return nil + } + return c.run(c.homeDirAbsPath, name, args) +} + // runHookPost runs the hook's post command, if it is set. func (c *Config) runHookPost(hook string) error { - command := c.Hooks[hook].Post - if command.Command == "" { - return nil + if err := c.runHook(c.Hooks[hook].Post); err != nil { + return fmt.Errorf("%s: post: %w", hook, err) } - return c.run(c.homeDirAbsPath, command.Command, command.Args) + return nil } // runHookPre runs the hook's pre command, if it is set. func (c *Config) runHookPre(hook string) error { - command := c.Hooks[hook].Pre - if command.Command == "" { - return nil + if err := c.runHook(c.Hooks[hook].Pre); err != nil { + return fmt.Errorf("%s: pre: %w", hook, err) } - return c.run(c.homeDirAbsPath, command.Command, command.Args) + return nil } // setEncryption configures c's encryption. @@ -2991,8 +3016,8 @@ func (f *ConfigFile) toMap() map[string]any { func parseCommand(command string, args []string) (string, []string, error) { // If command is found, then return it. - if path, err := chezmoi.LookPath(command); err == nil { - return path, args, nil + if commandPath, err := chezmoi.LookPath(command); err == nil { + return commandPath, args, nil } // Otherwise, if the command contains spaces, parse it as a shell command. diff --git a/internal/cmd/testdata/scripts/hooks_windows.txtar b/internal/cmd/testdata/scripts/hooks_windows.txtar new file mode 100644 index 00000000000..f8a1a386bdc --- /dev/null +++ b/internal/cmd/testdata/scripts/hooks_windows.txtar @@ -0,0 +1,14 @@ +[!windows] skip 'Windows only' + +# test that chezmoi status runs hooks with an interpreter +exec chezmoi status +stdout pre-status-hook + +-- bin/pre-status-hook.ps1 -- +"pre-status-hook" +-- home/user/.config/chezmoi/chezmoi.yaml -- +hooks: + status: + pre: + script: 'pre-status-hook.ps1' +-- home/user/.local/share/chezmoi/.keep --