Repository: cargo-generate/cargo-generate
Pull request #1392 (2025-02-24 13:59:16)

feat: QoL improvments for rhai scripting
## Scope

This PR aims to adress those issues:

- partially issue #1391 
- #1363 
- #1348 
- #1332 
- #616 
- #78 

In detail it focuses on 3 points regarding rhai scripting:

## improving `system::command` 

- now the 2nd argument is optional, but old syntax works as before, hence not a breaking change
  - before
  ```rhai
  system::command("pwd", []);
  ```
  - now
  ```rhai
  system::command("pwd");
  // but this works too
  system::command("pwd", []);
  ```

- the `system::command` now returns the output of the command if there was any, like so:
```rhai
let pwd = system::command("pwd");
print(`PWD: ${pwd}`);
```

- the `system::command` is now executed wrapped in a shell, either `sh` on *nix or `cmd` on windows. This allows for accessing shell built-ins like `pwd`
- stderr is trimmed wrapped into the error bubbled up
- stdout is also trimmed and returned

## adding a `listdir` function

- purpose is to list all entries in a given directory
- the rhai module `file` has now a `listdir` function, similar to [the python function `os.listdir`](https://docs.python.org/3/library/os.html#os.listdir)
```rhai
let files = file::listdir();
for f in files {
    print(`file: ${f}`);
}

// this is actually the same as above, the path must be inside the template directory, cannot be absolute or outside
let files = file::listdir(".");
for f in files {
    print(`file: ${f}`);
}
```

## adding a new `env` rhai module

- purpose of this module is providing more contextual variables like the destination or temp directory
- `env.working_directory` - Returns the current working directory as a string. This is the directory where the `cargo-generate` pre-processes the template, before it is copied over to the users `destination` directory.
- `env.destination_directory` - Returns the destination directory as a string. This is the directory where the template is copied to, and where the user will find the generated project.

## extending the hooks example template

- this new example hooks project can be executed like:
```
RUST_LOG=debug cargo run -- generate --verbose example-templates/many-hooks-in-action/ --name foo1
```
- it shows the features above

## extending the hooks guide

- [the guide book will contain better examples and links to the example template in the repo](https://cargo-generate.github.io/cargo-generate/templates/scripting.rhai-extensions.html)

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/cargo-generate/cargo-generate/pull/1392?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cargo-generate) Report
Attention: Patch coverage is `67.30769%` with `17 lines` in your changes missing coverage. Please review.
> Project coverage is 79.65%. Comparing base [(`104a62c`)](https://app.codecov.io/gh/cargo-generate/cargo-generate/commit/104a62c7a9352d9fefdfdbc54a7fadec21fa458f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cargo-generate) to head [(`ad1ab09`)](https://app.codecov.io/gh/cargo-generate/cargo-generate/commit/ad1ab09be94879efd74d7a0389de2994e3b8313b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cargo-generate).
> Report is 2 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/cargo-generate/cargo-generate/pull/1392?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cargo-generate) | Patch % | Lines |
|---|---|---|
| [src/hooks/file\_mod.rs](https://app.codecov.io/gh/cargo-generate/cargo-generate/pull/1392?src=pr&el=tree&filepath=src%2Fhooks%2Ffile_mod.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cargo-generate#diff-c3JjL2hvb2tzL2ZpbGVfbW9kLnJz) | 41.17% | [8 Missing and 2 partials :warning: ](https://app.codecov.io/gh/cargo-generate/cargo-generate/pull/1392?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cargo-generate) |
| [src/hooks/system\_mod.rs](https://app.codecov.io/gh/cargo-generate/cargo-generate/pull/1392?src=pr&el=tree&filepath=src%2Fhooks%2Fsystem_mod.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cargo-generate#diff-c3JjL2hvb2tzL3N5c3RlbV9tb2QucnM=) | 50.00% | [4 Missing and 2 partials :warning: ](https://app.codecov.io/gh/cargo-generate/cargo-generate/pull/1392?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cargo-generate) |
| [src/hooks/mod.rs](https://app.codecov.io/gh/cargo-generate/cargo-generate/pull/1392?src=pr&el=tree&filepath=src%2Fhooks%2Fmod.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cargo-generate#diff-c3JjL2hvb2tzL21vZC5ycw==) | 88.88% | [0 Missing and 1 partial :warning: ](https://app.codecov.io/gh/cargo-generate/cargo-generate/pull/1392?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cargo-generate) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1392      +/-   ##
==========================================
+ Coverage   79.33%   79.65%   +0.32%     
==========================================
  Files          33       34       +1     
  Lines        4369     4434      +65     
  Branches     4369     4434      +65     
==========================================
+ Hits         3466     3532      +66     
+ Misses        649      644       -5     
- Partials      254      258       +4     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/cargo-generate/cargo-generate/pull/1392?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cargo-generate).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cargo-generate).

sassman: currently [CI is failing](https://github.com/cargo-generate/cargo-generate/actions/runs/13516585711/job/37766423280?pr=1392#step:9:1665) on those tests:

```
failures:
    template_variables::project_dir::tests::test_converted_to_dash_case
    template_variables::project_dir::tests::test_dash_case_is_accepted
    template_variables::project_dir::tests::test_not_converted_to_dash_case_when_with_force
    template_variables::project_dir::tests::test_snake_case_is_accepted
    template_variables::project_name::tests::test_converted_to_dash_case
    template_variables::project_name::tests::test_dash_case_is_accepted
    template_variables::project_name::tests::test_not_converted_to_dash_case_when_with_force
    template_variables::project_name::tests::test_snake_case_is_accepted
```

Locally (also in a fresh docker) this is not unreproducible for me. Also I wonder how the changeset could relate to those tests.
sassman: > currently [CI is failing](https://github.com/cargo-generate/cargo-generate/actions/runs/13516585711/job/37766423280?pr=1392#step:9:1665) on those tests:
> 
> ```
> failures:
>     template_variables::project_dir::tests::test_converted_to_dash_case
>     template_variables::project_dir::tests::test_dash_case_is_accepted
>     template_variables::project_dir::tests::test_not_converted_to_dash_case_when_with_force
>     template_variables::project_dir::tests::test_snake_case_is_accepted
>     template_variables::project_name::tests::test_converted_to_dash_case
>     template_variables::project_name::tests::test_dash_case_is_accepted
>     template_variables::project_name::tests::test_not_converted_to_dash_case_when_with_force
>     template_variables::project_name::tests::test_snake_case_is_accepted
> ```
> 
> Locally (also in a fresh docker) this is not unreproducible for me. Also I wonder how the changeset could relate to those tests.

`Path::canonicalize` was the culprit, seems that the path must exist. Not sure why this wasn't an issue before.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 09b4c73..b650d2c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -75,19 +75,20 @@ dependencies = [
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.6"
+version = "3.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
+checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
 dependencies = [
  "anstyle",
+ "once_cell",
  "windows-sys 0.59.0",
 ]
 
 [[package]]
 name = "anyhow"
-version = "1.0.95"
+version = "1.0.97"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
+checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
 
 [[package]]
 name = "anymap2"
@@ -122,9 +123,9 @@ dependencies = [
 
 [[package]]
 name = "auth-git2"
-version = "0.5.5"
+version = "0.5.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3810b5af212b013fe7302b12d86616c6c39a48e18f2e4b812a5a9e5710213791"
+checksum = "d55eead120c93036f531829cf9b85830a474e75ce71169680879d28078321ddc"
 dependencies = [
  "dirs",
  "git2",
@@ -139,9 +140,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
 
 [[package]]
 name = "bitflags"
-version = "2.6.0"
+version = "2.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
 
 [[package]]
 name = "block-buffer"
@@ -165,9 +166,9 @@ dependencies = [
 
 [[package]]
 name = "bumpalo"
-version = "3.16.0"
+version = "3.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
 
 [[package]]
 name = "byteorder"
@@ -206,7 +207,6 @@ dependencies = [
  "names",
  "openssl",
  "paste",
- "path-absolutize",
  "predicates",
  "regex",
  "remove_dir_all",
@@ -240,9 +240,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.2.6"
+version = "1.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333"
+checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
 dependencies = [
  "jobserver",
  "libc",
@@ -310,9 +310,9 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
 
 [[package]]
 name = "console"
-version = "0.15.10"
+version = "0.15.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b"
+checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
 dependencies = [
  "encode_unicode",
  "libc",
@@ -343,9 +343,9 @@ dependencies = [
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.16"
+version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
 dependencies = [
  "libc",
 ]
@@ -377,9 +377,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
 
 [[package]]
 name = "crunchy"
-version = "0.2.2"
+version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
+checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
 
 [[package]]
 name = "crypto-common"
@@ -440,23 +440,23 @@ dependencies = [
 
 [[package]]
 name = "dirs"
-version = "5.0.1"
+version = "6.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
 dependencies = [
  "dirs-sys",
 ]
 
 [[package]]
 name = "dirs-sys"
-version = "0.4.1"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
 dependencies = [
  "libc",
  "option-ext",
  "redox_users",
- "windows-sys 0.48.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -478,9 +478,9 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
 
 [[package]]
 name = "either"
-version = "1.13.0"
+version = "1.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
+checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d"
 
 [[package]]
 name = "encode_unicode"
@@ -513,15 +513,15 @@ dependencies = [
 
 [[package]]
 name = "equivalent"
-version = "1.0.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
 
 [[package]]
 name = "erased-serde"
-version = "0.4.5"
+version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d"
+checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7"
 dependencies = [
  "serde",
  "typeid",
@@ -635,7 +635,7 @@ dependencies = [
  "cfg-if",
  "libc",
  "wasi 0.13.3+wasi-0.2.2",
- "windows-targets 0.52.6",
+ "windows-targets",
 ]
 
 [[package]]
@@ -664,7 +664,7 @@ dependencies = [
  "gix-utils",
  "itoa",
  "thiserror 1.0.69",
- "winnow",
+ "winnow 0.6.26",
 ]
 
 [[package]]
@@ -685,14 +685,14 @@ dependencies = [
  "smallvec",
  "thiserror 1.0.69",
  "unicode-bom",
- "winnow",
+ "winnow 0.6.26",
 ]
 
 [[package]]
 name = "gix-config-value"
-version = "0.14.10"
+version = "0.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49aaeef5d98390a3bcf9dbc6440b520b793d1bf3ed99317dc407b02be995b28e"
+checksum = "11365144ef93082f3403471dbaa94cfe4b5e72743bdb9560719a251d439f4cee"
 dependencies = [
  "bitflags",
  "bstr",
@@ -788,14 +788,14 @@ dependencies = [
  "itoa",
  "smallvec",
  "thiserror 1.0.69",
- "winnow",
+ "winnow 0.6.26",
 ]
 
 [[package]]
 name = "gix-path"
-version = "0.10.13"
+version = "0.10.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afc292ef1a51e340aeb0e720800338c805975724c1dfbd243185452efd8645b7"
+checksum = "c40f12bb65a8299be0cfb90fe718e3be236b7a94b434877012980863a883a99f"
 dependencies = [
  "bstr",
  "gix-trace",
@@ -822,14 +822,14 @@ dependencies = [
  "gix-validate",
  "memmap2",
  "thiserror 1.0.69",
- "winnow",
+ "winnow 0.6.26",
 ]
 
 [[package]]
 name = "gix-sec"
-version = "0.10.10"
+version = "0.10.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8b876ef997a955397809a2ec398d6a45b7a55b4918f2446344330f778d14fd6"
+checksum = "d84dae13271f4313f8d60a166bf27e54c968c7c33e2ffd31c48cafe5da649875"
 dependencies = [
  "bitflags",
  "gix-path",
@@ -852,15 +852,15 @@ dependencies = [
 
 [[package]]
 name = "gix-trace"
-version = "0.1.11"
+version = "0.1.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04bdde120c29f1fc23a24d3e115aeeea3d60d8e65bab92cc5f9d90d9302eb952"
+checksum = "7c396a2036920c69695f760a65e7f2677267ccf483f25046977d87e4cb2665f7"
 
 [[package]]
 name = "gix-utils"
-version = "0.1.13"
+version = "0.1.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba427e3e9599508ed98a6ddf8ed05493db114564e338e41f6a996d2e4790335f"
+checksum = "ff08f24e03ac8916c478c8419d7d3c33393da9bb41fa4c24455d5406aeefd35f"
 dependencies = [
  "fastrand",
  "unicode-normalization",
@@ -868,9 +868,9 @@ dependencies = [
 
 [[package]]
 name = "gix-validate"
-version = "0.9.2"
+version = "0.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd520d09f9f585b34b32aba1d0b36ada89ab7fefb54a8ca3fe37fc482a750937"
+checksum = "9eaa01c3337d885617c0a42e92823922a2aea71f4caeace6fe87002bdcadbd90"
 dependencies = [
  "bstr",
  "thiserror 2.0.12",
@@ -878,9 +878,9 @@ dependencies = [
 
 [[package]]
 name = "globset"
-version = "0.4.15"
+version = "0.4.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19"
+checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5"
 dependencies = [
  "aho-corasick",
  "bstr",
@@ -1097,9 +1097,9 @@ dependencies = [
 
 [[package]]
 name = "indoc"
-version = "2.0.5"
+version = "2.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
+checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
 
 [[package]]
 name = "instant"
@@ -1118,41 +1118,44 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
 
 [[package]]
 name = "itertools"
-version = "0.13.0"
+version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
 dependencies = [
  "either",
 ]
 
 [[package]]
 name = "itoa"
-version = "1.0.14"
+version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
 
 [[package]]
 name = "jiff"
-version = "0.1.16"
+version = "0.1.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24a46169c7a10358cdccfb179910e8a5a392fc291bdb409da9aeece5b19786d8"
+checksum = "c04ef77ae73f3cf50510712722f0c4e8b46f5aaa1bf5ffad2ae213e6495e78e5"
 dependencies = [
  "jiff-tzdb-platform",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
  "serde",
  "windows-sys 0.59.0",
 ]
 
 [[package]]
 name = "jiff-tzdb"
-version = "0.1.1"
+version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91335e575850c5c4c673b9bd467b0e025f164ca59d0564f69d0c2ee0ffad4653"
+checksum = "cf2cec2f5d266af45a071ece48b1fb89f3b00b2421ac3a5fe10285a6caaa60d3"
 
 [[package]]
 name = "jiff-tzdb-platform"
-version = "0.1.1"
+version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9835f0060a626fe59f160437bc725491a6af23133ea906500027d1bd2f8f4329"
+checksum = "a63c62e404e7b92979d2792352d885a7f8f83fd1d0d31eea582d77b2ceca697e"
 dependencies = [
  "jiff-tzdb",
 ]
@@ -1168,9 +1171,9 @@ dependencies = [
 
 [[package]]
 name = "js-sys"
-version = "0.3.76"
+version = "0.3.77"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
 dependencies = [
  "once_cell",
  "wasm-bindgen",
@@ -1188,9 +1191,9 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.169"
+version = "0.2.170"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
+checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
 
 [[package]]
 name = "libgit2-sys"
@@ -1218,9 +1221,9 @@ dependencies = [
 
 [[package]]
 name = "libssh2-sys"
-version = "0.3.0"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee"
+checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9"
 dependencies = [
  "cc",
  "libc",
@@ -1232,9 +1235,9 @@ dependencies = [
 
 [[package]]
 name = "libz-sys"
-version = "1.1.20"
+version = "1.1.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472"
+checksum = "df9b68e50e6e0b26f672573834882eb57759f6db9b3be2ea3c35c91188bb4eaa"
 dependencies = [
  "cc",
  "libc",
@@ -1244,17 +1247,16 @@ dependencies = [
 
 [[package]]
 name = "linux-raw-sys"
-version = "0.4.14"
+version = "0.4.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
 
 [[package]]
 name = "liquid"
-version = "0.26.9"
+version = "0.26.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cdcc72b82748f47c2933c172313f5a9aea5b2c4eb3fa4c66b4ea55bb60bb4b1"
+checksum = "2a494c3f9dad3cb7ed16f1c51812cbe4b29493d6c2e5cd1e2b87477263d9534d"
 dependencies = [
- "doc-comment",
  "liquid-core",
  "liquid-derive",
  "liquid-lib",
@@ -1263,15 +1265,14 @@ dependencies = [
 
 [[package]]
 name = "liquid-core"
-version = "0.26.9"
+version = "0.26.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2752e978ffc53670f3f2e8b3ef09f348d6f7b5474a3be3f8a5befe5382e4effb"
+checksum = "fc623edee8a618b4543e8e8505584f4847a4e51b805db1af6d9af0a3395d0d57"
 dependencies = [
  "anymap2",
  "itertools",
  "kstring",
  "liquid-derive",
- "num-traits",
  "pest",
  "pest_derive",
  "regex",
@@ -1281,9 +1282,9 @@ dependencies = [
 
 [[package]]
 name = "liquid-derive"
-version = "0.26.8"
+version = "0.26.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b51f1d220e3fa869e24cfd75915efe3164bd09bb11b3165db3f37f57bf673e3"
+checksum = "de66c928222984aea59fcaed8ba627f388aaac3c1f57dcb05cc25495ef8faefe"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1292,13 +1293,12 @@ dependencies = [
 
 [[package]]
 name = "liquid-lib"
-version = "0.26.9"
+version = "0.26.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59b1a298d3d2287ee5b1e43840d885b8fdfc37d3f4e90d82aacfd04d021618da"
+checksum = "9befeedd61f5995bc128c571db65300aeb50d62e4f0542c88282dbcb5f72372a"
 dependencies = [
  "itertools",
  "liquid-core",
- "once_cell",
  "percent-encoding",
  "regex",
  "time",
@@ -1307,9 +1307,9 @@ dependencies = [
 
 [[package]]
 name = "litemap"
-version = "0.7.4"
+version = "0.7.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
+checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
 
 [[package]]
 name = "lock_api"
@@ -1401,9 +1401,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
 
 [[package]]
 name = "once_cell"
-version = "1.20.2"
+version = "1.20.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
 dependencies = [
  "portable-atomic",
 ]
@@ -1436,15 +1436,15 @@ dependencies = [
 
 [[package]]
 name = "openssl-probe"
-version = "0.1.5"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
 
 [[package]]
 name = "openssl-src"
-version = "300.4.1+3.4.0"
+version = "300.4.2+3.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c"
+checksum = "168ce4e058f975fe43e89d9ccf78ca668601887ae736090aacc23ae353c298e2"
 dependencies = [
  "cc",
 ]
@@ -1497,7 +1497,7 @@ dependencies = [
  "libc",
  "redox_syscall",
  "smallvec",
- "windows-targets 0.52.6",
+ "windows-targets",
 ]
 
 [[package]]
@@ -1506,24 +1506,6 @@ version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
 
-[[package]]
-name = "path-absolutize"
-version = "3.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5"
-dependencies = [
- "path-dedot",
-]
-
-[[package]]
-name = "path-dedot"
-version = "3.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397"
-dependencies = [
- "once_cell",
-]
-
 [[package]]
 name = "percent-encoding"
 version = "2.3.1"
@@ -1577,15 +1559,24 @@ dependencies = [
 
 [[package]]
 name = "pkg-config"
-version = "0.3.31"
+version = "0.3.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
 
 [[package]]
 name = "portable-atomic"
-version = "1.10.0"
+version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6"
+checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
+dependencies = [
+ "portable-atomic",
+]
 
 [[package]]
 name = "powerfmt"
@@ -1634,9 +1625,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.92"
+version = "1.0.94"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
+checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
 dependencies = [
  "unicode-ident",
 ]
@@ -1649,9 +1640,9 @@ checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79"
 
 [[package]]
 name = "quote"
-version = "1.0.38"
+version = "1.0.39"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
+checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801"
 dependencies = [
  "proc-macro2",
 ]
@@ -1688,22 +1679,22 @@ dependencies = [
 
 [[package]]
 name = "redox_syscall"
-version = "0.5.8"
+version = "0.5.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
+checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
 dependencies = [
  "bitflags",
 ]
 
 [[package]]
 name = "redox_users"
-version = "0.4.6"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
 dependencies = [
  "getrandom 0.2.15",
  "libredox",
- "thiserror 1.0.69",
+ "thiserror 2.0.12",
 ]
 
 [[package]]
@@ -1779,9 +1770,9 @@ dependencies = [
 
 [[package]]
 name = "rustix"
-version = "0.38.42"
+version = "0.38.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
 dependencies = [
  "bitflags",
  "errno",
@@ -1816,27 +1807,27 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
 [[package]]
 name = "semver"
-version = "1.0.25"
+version = "1.0.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
+checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
 dependencies = [
  "serde",
 ]
 
 [[package]]
 name = "serde"
-version = "1.0.217"
+version = "1.0.218"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
+checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde-untagged"
-version = "0.1.6"
+version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2676ba99bd82f75cae5cbd2c8eda6fa0b8760f18978ea840e980dd5567b5c5b6"
+checksum = "299d9c19d7d466db4ab10addd5703e4c615dec2a5a16dbbafe191045e87ee66e"
 dependencies = [
  "erased-serde",
  "serde",
@@ -1855,9 +1846,9 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.217"
+version = "1.0.218"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
+checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1904,9 +1895,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
 
 [[package]]
 name = "smallvec"
-version = "1.13.2"
+version = "1.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
 
 [[package]]
 name = "smartstring"
@@ -1939,9 +1930,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
 
 [[package]]
 name = "syn"
-version = "2.0.92"
+version = "2.0.99"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70ae51629bf965c5c098cc9e87908a3df5301051a9e087d6f9bef5c9771ed126"
+checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2047,9 +2038,9 @@ dependencies = [
 
 [[package]]
 name = "time"
-version = "0.3.37"
+version = "0.3.38"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
+checksum = "bb041120f25f8fbe8fd2dbe4671c7c2ed74d83be2e7a77529bf7e0790ae3f472"
 dependencies = [
  "deranged",
  "itoa",
@@ -2062,15 +2053,15 @@ dependencies = [
 
 [[package]]
 name = "time-core"
-version = "0.1.2"
+version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
+checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef"
 
 [[package]]
 name = "time-macros"
-version = "0.2.19"
+version = "0.2.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
+checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c"
 dependencies = [
  "num-conv",
  "time-core",
@@ -2097,9 +2088,9 @@ dependencies = [
 
 [[package]]
 name = "tinyvec"
-version = "1.8.1"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8"
+checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
 dependencies = [
  "tinyvec_macros",
 ]
@@ -2112,9 +2103,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
 
 [[package]]
 name = "toml"
-version = "0.8.19"
+version = "0.8.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
+checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
 dependencies = [
  "indexmap",
  "serde",
@@ -2134,28 +2125,28 @@ dependencies = [
 
 [[package]]
 name = "toml_edit"
-version = "0.22.22"
+version = "0.22.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
+checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
 dependencies = [
  "indexmap",
  "serde",
  "serde_spanned",
  "toml_datetime",
- "winnow",
+ "winnow 0.7.3",
 ]
 
 [[package]]
 name = "typeid"
-version = "1.0.2"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e"
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
 
 [[package]]
 name = "typenum"
-version = "1.17.0"
+version = "1.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
 
 [[package]]
 name = "ucd-trie"
@@ -2171,9 +2162,9 @@ checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217"
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.14"
+version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
 
 [[package]]
 name = "unicode-normalization"
@@ -2245,9 +2236,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
 
 [[package]]
 name = "wait-timeout"
-version = "0.2.0"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
+checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
 dependencies = [
  "libc",
 ]
@@ -2279,9 +2270,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.99"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
 dependencies = [
  "cfg-if",
  "once_cell",
@@ -2290,9 +2281,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.99"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
 dependencies = [
  "bumpalo",
  "log",
@@ -2304,9 +2295,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.99"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -2314,9 +2305,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.99"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2327,9 +2318,12 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.99"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
 
 [[package]]
 name = "web-time"
@@ -2372,22 +2366,13 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
-[[package]]
-name = "windows-sys"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
-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.6",
+ "windows-targets",
 ]
 
 [[package]]
@@ -2396,22 +2381,7 @@ version = "0.59.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
 dependencies = [
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
-dependencies = [
- "windows_aarch64_gnullvm 0.48.5",
- "windows_aarch64_msvc 0.48.5",
- "windows_i686_gnu 0.48.5",
- "windows_i686_msvc 0.48.5",
- "windows_x86_64_gnu 0.48.5",
- "windows_x86_64_gnullvm 0.48.5",
- "windows_x86_64_msvc 0.48.5",
+ "windows-targets",
 ]
 
 [[package]]
@@ -2420,46 +2390,28 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
 dependencies = [
- "windows_aarch64_gnullvm 0.52.6",
- "windows_aarch64_msvc 0.52.6",
- "windows_i686_gnu 0.52.6",
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
  "windows_i686_gnullvm",
- "windows_i686_msvc 0.52.6",
- "windows_x86_64_gnu 0.52.6",
- "windows_x86_64_gnullvm 0.52.6",
- "windows_x86_64_msvc 0.52.6",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
 ]
 
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
-
 [[package]]
 name = "windows_aarch64_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
 
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
-
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
 
-[[package]]
-name = "windows_i686_gnu"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
-
 [[package]]
 name = "windows_i686_gnu"
 version = "0.52.6"
@@ -2472,36 +2424,18 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
 
-[[package]]
-name = "windows_i686_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
-
 [[package]]
 name = "windows_i686_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
 
-[[package]]
-name = "windows_x86_64_gnu"
-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.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
 
-[[package]]
-name = "windows_x86_64_gnullvm"
-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.6"
@@ -2510,21 +2444,24 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.48.5"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
 [[package]]
-name = "windows_x86_64_msvc"
-version = "0.52.6"
+name = "winnow"
+version = "0.6.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28"
+dependencies = [
+ "memchr",
+]
 
 [[package]]
 name = "winnow"
-version = "0.6.20"
+version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
+checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
 dependencies = [
  "memchr",
 ]
@@ -2597,18 +2534,18 @@ dependencies = [
 
 [[package]]
 name = "zerofrom"
-version = "0.1.5"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
 dependencies = [
  "zerofrom-derive",
 ]
 
 [[package]]
 name = "zerofrom-derive"
-version = "0.1.5"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
 dependencies = [
  "proc-macro2",
  "quote",
diff --git a/Cargo.toml b/Cargo.toml
index e40000e..253cb34 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -36,7 +36,6 @@ log = "~0.4"
 names = { version = "~0.14", default-features = false }
 openssl = { version = "~0.10", optional = true }
 paste = "~1.0"
-path-absolutize = "~3.1"
 regex = "~1.11"
 remove_dir_all = "~1.0"
 rhai = "~1.20"
diff --git a/example-templates/many-hooks-in-action/LICENSE-APACHE b/example-templates/many-hooks-in-action/LICENSE-APACHE
new file mode 100644
index 0000000..11069ed
--- /dev/null
+++ b/example-templates/many-hooks-in-action/LICENSE-APACHE
@@ -0,0 +1,201 @@
+                              Apache License
+                        Version 2.0, January 2004
+                     http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+   "License" shall mean the terms and conditions for use, reproduction,
+   and distribution as defined by Sections 1 through 9 of this document.
+
+   "Licensor" shall mean the copyright owner or entity authorized by
+   the copyright owner that is granting the License.
+
+   "Legal Entity" shall mean the union of the acting entity and all
+   other entities that control, are controlled by, or are under common
+   control with that entity. For the purposes of this definition,
+   "control" means (i) the power, direct or indirect, to cause the
+   direction or management of such entity, whether by contract or
+   otherwise, or (ii) ownership of fifty percent (50%) or more of the
+   outstanding shares, or (iii) beneficial ownership of such entity.
+
+   "You" (or "Your") shall mean an individual or Legal Entity
+   exercising permissions granted by this License.
+
+   "Source" form shall mean the preferred form for making modifications,
+   including but not limited to software source code, documentation
+   source, and configuration files.
+
+   "Object" form shall mean any form resulting from mechanical
+   transformation or translation of a Source form, including but
+   not limited to compiled object code, generated documentation,
+   and conversions to other media types.
+
+   "Work" shall mean the work of authorship, whether in Source or
+   Object form, made available under the License, as indicated by a
+   copyright notice that is included in or attached to the work
+   (an example is provided in the Appendix below).
+
+   "Derivative Works" shall mean any work, whether in Source or Object
+   form, that is based on (or derived from) the Work and for which the
+   editorial revisions, annotations, elaborations, or other modifications
+   represent, as a whole, an original work of authorship. For the purposes
+   of this License, Derivative Works shall not include works that remain
+   separable from, or merely link (or bind by name) to the interfaces of,
+   the Work and Derivative Works thereof.
+
+   "Contribution" shall mean any work of authorship, including
+   the original version of the Work and any modifications or additions
+   to that Work or Derivative Works thereof, that is intentionally
+   submitted to Licensor for inclusion in the Work by the copyright owner
+   or by an individual or Legal Entity authorized to submit on behalf of
+   the copyright owner. For the purposes of this definition, "submitted"
+   means any form of electronic, verbal, or written communication sent
+   to the Licensor or its representatives, including but not limited to
+   communication on electronic mailing lists, source code control systems,
+   and issue tracking systems that are managed by, or on behalf of, the
+   Licensor for the purpose of discussing and improving the Work, but
+   excluding communication that is conspicuously marked or otherwise
+   designated in writing by the copyright owner as "Not a Contribution."
+
+   "Contributor" shall mean Licensor and any individual or Legal Entity
+   on behalf of whom a Contribution has been received by Licensor and
+   subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   copyright license to reproduce, prepare Derivative Works of,
+   publicly display, publicly perform, sublicense, and distribute the
+   Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   (except as stated in this section) patent license to make, have made,
+   use, offer to sell, sell, import, and otherwise transfer the Work,
+   where such license applies only to those patent claims licensable
+   by such Contributor that are necessarily infringed by their
+   Contribution(s) alone or by combination of their Contribution(s)
+   with the Work to which such Contribution(s) was submitted. If You
+   institute patent litigation against any entity (including a
+   cross-claim or counterclaim in a lawsuit) alleging that the Work
+   or a Contribution incorporated within the Work constitutes direct
+   or contributory patent infringement, then any patent licenses
+   granted to You under this License for that Work shall terminate
+   as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+   Work or Derivative Works thereof in any medium, with or without
+   modifications, and in Source or Object form, provided that You
+   meet the following conditions:
+
+   (a) You must give any other recipients of the Work or
+       Derivative Works a copy of this License; and
+
+   (b) You must cause any modified files to carry prominent notices
+       stating that You changed the files; and
+
+   (c) You must retain, in the Source form of any Derivative Works
+       that You distribute, all copyright, patent, trademark, and
+       attribution notices from the Source form of the Work,
+       excluding those notices that do not pertain to any part of
+       the Derivative Works; and
+
+   (d) If the Work includes a "NOTICE" text file as part of its
+       distribution, then any Derivative Works that You distribute must
+       include a readable copy of the attribution notices contained
+       within such NOTICE file, excluding those notices that do not
+       pertain to any part of the Derivative Works, in at least one
+       of the following places: within a NOTICE text file distributed
+       as part of the Derivative Works; within the Source form or
+       documentation, if provided along with the Derivative Works; or,
+       within a display generated by the Derivative Works, if and
+       wherever such third-party notices normally appear. The contents
+       of the NOTICE file are for informational purposes only and
+       do not modify the License. You may add Your own attribution
+       notices within Derivative Works that You distribute, alongside
+       or as an addendum to the NOTICE text from the Work, provided
+       that such additional attribution notices cannot be construed
+       as modifying the License.
+
+   You may add Your own copyright statement to Your modifications and
+   may provide additional or different license terms and conditions
+   for use, reproduction, or distribution of Your modifications, or
+   for any such Derivative Works as a whole, provided Your use,
+   reproduction, and distribution of the Work otherwise complies with
+   the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+   any Contribution intentionally submitted for inclusion in the Work
+   by You to the Licensor shall be under the terms and conditions of
+   this License, without any additional terms or conditions.
+   Notwithstanding the above, nothing herein shall supersede or modify
+   the terms of any separate license agreement you may have executed
+   with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+   names, trademarks, service marks, or product names of the Licensor,
+   except as required for reasonable and customary use in describing the
+   origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+   agreed to in writing, Licensor provides the Work (and each
+   Contributor provides its Contributions) on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+   implied, including, without limitation, any warranties or conditions
+   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+   PARTICULAR PURPOSE. You are solely responsible for determining the
+   appropriateness of using or redistributing the Work and assume any
+   risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+   whether in tort (including negligence), contract, or otherwise,
+   unless required by applicable law (such as deliberate and grossly
+   negligent acts) or agreed to in writing, shall any Contributor be
+   liable to You for damages, including any direct, indirect, special,
+   incidental, or consequential damages of any character arising as a
+   result of this License or out of the use or inability to use the
+   Work (including but not limited to damages for loss of goodwill,
+   work stoppage, computer failure or malfunction, or any and all
+   other commercial damages or losses), even if such Contributor
+   has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+   the Work or Derivative Works thereof, You may choose to offer,
+   and charge a fee for, acceptance of support, warranty, indemnity,
+   or other liability obligations and/or rights consistent with this
+   License. However, in accepting such obligations, You may act only
+   on Your own behalf and on Your sole responsibility, not on behalf
+   of any other Contributor, and only if You agree to indemnify,
+   defend, and hold each Contributor harmless for any liability
+   incurred by, or claims asserted against, such Contributor by reason
+   of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+   To apply the Apache License to your work, attach the following
+   boilerplate notice, with the fields enclosed by brackets "[]"
+   replaced with your own identifying information. (Don't include
+   the brackets!)  The text should be enclosed in the appropriate
+   comment syntax for the file format. We also recommend that a
+   file or class name and description of purpose be included on the
+   same "printed page" as the copyright notice for easier
+   identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/example-templates/many-hooks-in-action/LICENSE-MIT b/example-templates/many-hooks-in-action/LICENSE-MIT
new file mode 100644
index 0000000..5051252
--- /dev/null
+++ b/example-templates/many-hooks-in-action/LICENSE-MIT
@@ -0,0 +1,25 @@
+Copyright (c) 2018 Ashley Williams
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/example-templates/many-hooks-in-action/README.md b/example-templates/many-hooks-in-action/README.md
new file mode 100644
index 0000000..36016d9
--- /dev/null
+++ b/example-templates/many-hooks-in-action/README.md
@@ -0,0 +1,17 @@
+# Hooks template
+
+This is an example template that uses hook scripts to let the user select a license file.
+
+The selected license file will be renamed into `LICENSE`, and besides this `README.md` be the only files left after expansion.
+
+## Expansion
+
+```sh
+cargo generate --name my-expanded-template gh:cargo-generate/cargo-generate example-templates/many-hooks-in-action
+```
+
+or to select the license directly from commandline:
+
+```sh
+cargo generate --name my-expanded-template gh:cargo-generate/cargo-generate example-templates/many-hooks-in-action -d license=mit
+```
diff --git a/example-templates/many-hooks-in-action/README.md.liquid b/example-templates/many-hooks-in-action/README.md.liquid
new file mode 100644
index 0000000..22236be
--- /dev/null
+++ b/example-templates/many-hooks-in-action/README.md.liquid
@@ -0,0 +1,3 @@
+# Expanded template
+
+This is the `README.md` for the expanded template.
diff --git a/example-templates/many-hooks-in-action/ask-license.rhai b/example-templates/many-hooks-in-action/ask-license.rhai
new file mode 100644
index 0000000..13e52e6
--- /dev/null
+++ b/example-templates/many-hooks-in-action/ask-license.rhai
@@ -0,0 +1,27 @@
+// This example script asks and reacts to the the users selection of license
+
+let license = variable::get("license").to_lower();
+while switch license {
+    "none" => {
+        debug("you selected to not use any license");
+        file::delete("LICENSE-APACHE");
+        file::delete("LICENSE-MIT");
+        false
+    }
+    "mit" => {
+        debug("you selected to use the MIT license");
+        file::delete("LICENSE-APACHE");
+        file::rename("LICENSE-MIT", "LICENSE");
+        false
+    }
+    "apache" => {
+        debug("you selected to use the APACHE license");
+        file::rename("LICENSE-APACHE", "LICENSE");
+        file::delete("LICENSE-MIT");
+        false
+    }
+    _ => true,
+} {
+    license = variable::prompt("Which license do you want to use?", "none", ["none", "apache", "mit"]);
+}
+variable::set("license", license);
diff --git a/example-templates/many-hooks-in-action/cargo-generate.toml b/example-templates/many-hooks-in-action/cargo-generate.toml
new file mode 100644
index 0000000..ae9b1d9
--- /dev/null
+++ b/example-templates/many-hooks-in-action/cargo-generate.toml
@@ -0,0 +1,6 @@
+[template]
+cargo_generate_version = ">=0.9.0"
+
+[hooks]
+pre = ["remove-unwanted.rhai", "ask-license.rhai", "sed-licence.rhai"]
+post = ["post-script.rhai"]
diff --git a/example-templates/many-hooks-in-action/post-script.rhai b/example-templates/many-hooks-in-action/post-script.rhai
new file mode 100644
index 0000000..3f0c1d0
--- /dev/null
+++ b/example-templates/many-hooks-in-action/post-script.rhai
@@ -0,0 +1,10 @@
+print("You have reached a Rhai script, running as POST by cargo-generate");
+
+let pwd = system::command("pwd");
+print(`[post-hook] pwd: ${pwd}`);
+
+let working_directory = env::working_directory;
+print(`[post-hook] working_directory: ${working_directory}`);
+
+// this command will..
+// system::command("foobarbak", ["this one fails.."]);
diff --git a/example-templates/many-hooks-in-action/remove-unwanted.rhai b/example-templates/many-hooks-in-action/remove-unwanted.rhai
new file mode 100644
index 0000000..144bc39
--- /dev/null
+++ b/example-templates/many-hooks-in-action/remove-unwanted.rhai
@@ -0,0 +1,2 @@
+// the delete function will remove the file from the final project
+file::delete("we-dont-keep-this-file.md");
diff --git a/example-templates/many-hooks-in-action/sed-licence.rhai b/example-templates/many-hooks-in-action/sed-licence.rhai
new file mode 100644
index 0000000..38ef2f0
--- /dev/null
+++ b/example-templates/many-hooks-in-action/sed-licence.rhai
@@ -0,0 +1,28 @@
+let pwd = system::command("pwd");
+print(`PWD: ${pwd}`);
+
+let working_directory = env::working_directory;
+print(`working_directory: ${working_directory}`);
+
+let destination_directory = env::destination_directory;
+print(`destination_directory: ${destination_directory}`);
+
+// replace the year in the license file
+system::command("gsed", ["-i", "s/2018/2022/g", "LICENSE"]);
+
+let year = system::date().year;
+// replace the year in the license file, with a variable interpolation
+system::command("gsed", ["-i", `s/2022/${year}/g`, "LICENSE"]);
+
+print("Listing files in the current directory, with 'file::listdir()'");
+let files = file::listdir();
+for f in files {
+    print(`file: ${f}`);
+}
+
+// this is actually the same as above, the path must be inside the template directory, cannot be absolute or ourside
+print("Listing files in the current directory, with 'file::listdir('.')'");
+let files = file::listdir(".");
+for f in files {
+    print(`file: ${f}`);
+}
diff --git a/example-templates/many-hooks-in-action/we-dont-keep-this-file.md b/example-templates/many-hooks-in-action/we-dont-keep-this-file.md
new file mode 100644
index 0000000..4375212
--- /dev/null
+++ b/example-templates/many-hooks-in-action/we-dont-keep-this-file.md
@@ -0,0 +1,3 @@
+# This file should not be kept in the final generated project
+
+The rhai hook `remove-unwanted.rhai` takes care of removing this file from the final generated project.
diff --git a/guide/src/templates/scripting.md b/guide/src/templates/scripting.md
index 2ebc08e..4259427 100644
--- a/guide/src/templates/scripting.md
+++ b/guide/src/templates/scripting.md
@@ -9,9 +9,9 @@ To configure the use of hooks, write a `hooks` section in the `cargo-generate.to
 
 ```toml
 [hooks]
-#init = ["init-script.rhai"]
-#pre = ["pre-script.rhai"]
-#post = ["post-script.rhai"]
+init = ["init-script.rhai"]
+pre = ["pre-script.rhai"]
+post = ["post-script.rhai"]
 ```
 
 [`Rhai`]: https://rhai.rs/book/
diff --git a/guide/src/templates/scripting.rhai-extensions.md b/guide/src/templates/scripting.rhai-extensions.md
index 2fd2b6d..edcdc19 100644
--- a/guide/src/templates/scripting.rhai-extensions.md
+++ b/guide/src/templates/scripting.rhai-extensions.md
@@ -2,7 +2,7 @@
 
 Besides the basic [`Rhai`] features, these are the modules/behaviors defined:
 
-### Variables
+### Variables with the `variable` module
 
 #### get/set
 
@@ -19,7 +19,7 @@ Besides the basic [`Rhai`] features, these are the modules/behaviors defined:
   Set new or overwrite existing variables. Do not allow to change types.
   Note that you can set entire arrays with this (e.g. `variable::set("array",["a","b"])`) but not individual elements (`variable::set("array[1]","a")` will not work).
 
-#### Prompt
+#### Prompt for values with `variable::prompt`
 
 * **`variable::prompt(text: &str, default_value: bool) -> value`**
 
@@ -41,7 +41,7 @@ Besides the basic [`Rhai`] features, these are the modules/behaviors defined:
 
   Prompt the user for a choice value
 
-### Files
+### Files with the `file` module
 
 * **`file::exists(path: &str)`**
 
@@ -62,10 +62,32 @@ Besides the basic [`Rhai`] features, these are the modules/behaviors defined:
 * **`file::write(file: &str, content: Array)`**
 
   Create/overwrite a file inside the template folder, each entry in the array on a new line
+  
+* **`file::listdir(path = ".") -> Array<String>`**
+
+  List the contents of a directory
+  
+  Note: The path is relative to the template folder, and cannot be outside the template folder.
+  
+  Examples:
+  ```rhai
+  let files = file::listdir();
+  for f in files {
+      print(`file: ${f}`);
+  }
+  
+  // this is actually the same as above, the path must be inside the template directory, cannot be absolute or ourside
+  let files = file::listdir(".");
+  for f in files {
+      print(`file: ${f}`);
+  }
+  ```
+  
+  See also: [the many-hooks-in-action example project](https://github.com/cargo-generate/cargo-generate/blob/main/example-templates/many-hooks-in-action/sed-license.rhai#L18)
 
-### System
+### The `system` module
 
-* **`system::command(cmd: &str, args: Array) -> value`**
+* **`system::command(cmd: &str, args: Array = []) -> value`**
 
   Execute a command on the system generating the project from a template.
 
@@ -78,11 +100,51 @@ Besides the basic [`Rhai`] features, these are the modules/behaviors defined:
 
   unless the user uses the flag `--allow-commands`. If the user attempts to use the
   `--silent` flag without the `--allow-commands` flag will fail.
+  
+  Examples:
+  ```rhai
+  // this returns the PWD as a string
+  let pwd = system::command("pwd");
+  
+  // but this works too and does the same
+  system::command("pwd", []);
+  
+  // this will cat a file and returns the content
+  let content = system::command("cat", ["file.txt"]);
+  ```
+  
+  See also: [the many-hooks-in-action example project](https://github.com/cargo-generate/cargo-generate/blob/main/example-templates/many-hooks-in-action/sed-license.rhai#L11)
 
 * **`system::date() -> Date`**
   
   Get the date in UTC from the system as an object with the properties `year`, `month`, and `day`.
+  
+### The `env` module
+
+The `env` module provides access to environment variables.
 
+* **`env::working_directory`**
+  
+  Returns the current working directory as a string. This is the directory where the `cargo-generate` pre-processes the template, before it is copied over to the users `destination` directory.
+  
+  Examples:
+  ```rhai
+  let wd = env::working_directory;
+  print(`Working directory: ${wd}`);
+  ```
+  
+  See also: [the many-hooks-in-action example project](https://github.com/cargo-generate/cargo-generate/blob/main/example-templates/many-hooks-in-action/post-script.rhai#L6)
+  
+* **`env::destination_directory`**
+  
+  Returns the destination directory as a string. This is the directory where the template is copied to, and where the user will find the generated project.
+  
+  Examples:
+  ```rhai
+  let dd = env::destination_directory;
+  print(`Destination directory: ${dd}`);
+  ```
+  
 ### Other
 
 * **`abort(reason: &str)`**: Aborts `cargo-generate` with a script error.
diff --git a/src/absolute_path.rs b/src/absolute_path.rs
new file mode 100644
index 0000000..9898d19
--- /dev/null
+++ b/src/absolute_path.rs
@@ -0,0 +1,329 @@
+use std::{
+    env,
+    path::{Component, Path, PathBuf},
+};
+
+/// This trait extends `Path` and `PathBuf` to provide a method to convert a relative path to an absolute path.
+pub trait AbsolutePathExt {
+    /// Converts a relative path to an absolute path.
+    fn as_absolute(&self) -> std::result::Result<PathBuf, std::io::Error>;
+
+    /// Converts a relative path to an absolute path and checks if it is sandboxed within a given directory.
+    fn as_sandboxed_absolute(&self, sandbox: &Path)
+        -> std::result::Result<PathBuf, std::io::Error>;
+}
+
+impl AbsolutePathExt for PathBuf {
+    fn as_absolute(&self) -> std::result::Result<PathBuf, std::io::Error> {
+        if self.is_absolute() {
+            return Ok(self.clone());
+        }
+
+        let cwd = env::current_dir()?;
+        Ok(canonicalize_path(&cwd.join(self)))
+    }
+
+    fn as_sandboxed_absolute(
+        &self,
+        sandbox: &Path,
+    ) -> std::result::Result<PathBuf, std::io::Error> {
+        let sandbox = canonicalize_path(sandbox);
+
+        let absolute_path = if self.is_relative() {
+            canonicalize_path(&sandbox.join(self))
+        } else {
+            canonicalize_path(self)
+        };
+
+        if !absolute_path.starts_with(sandbox) {
+            return Err(std::io::Error::new(
+                std::io::ErrorKind::Other,
+                "Path cannot escape the sandbox",
+            ));
+        }
+        Ok(absolute_path)
+    }
+}
+
+impl AbsolutePathExt for std::path::Path {
+    fn as_absolute(&self) -> std::result::Result<PathBuf, std::io::Error> {
+        PathBuf::from(self).as_absolute()
+    }
+
+    fn as_sandboxed_absolute(
+        &self,
+        sandbox: &Path,
+    ) -> std::result::Result<PathBuf, std::io::Error> {
+        PathBuf::from(self).as_sandboxed_absolute(sandbox)
+    }
+}
+
+/// Canonicalizes a path without requiring the files and folders to exist.
+fn canonicalize_path(path: &Path) -> PathBuf {
+    let components = path.components().peekable();
+    let mut result = PathBuf::new();
+
+    for component in components {
+        match component {
+            Component::RootDir => {
+                result.push(component);
+            }
+            Component::CurDir => {
+                // Skip current directory components
+            }
+            Component::ParentDir => {
+                // Pop the last component if it's not a root directory
+                if result.components().last() != Some(Component::RootDir) {
+                    result.pop();
+                }
+            }
+            Component::Normal(_) => {
+                result.push(component);
+            }
+            Component::Prefix(prefix) => match prefix.kind() {
+                std::path::Prefix::Disk(_) => {
+                    result.push(prefix.as_os_str());
+                }
+                _ => {
+                    // Skip other types of prefixes
+                }
+            },
+        }
+    }
+
+    result
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::env;
+
+    #[test]
+    fn test_non_existing_path() {
+        #[cfg(target_family = "unix")]
+        let relative_path = PathBuf::from("/non-existing-path");
+        #[cfg(target_family = "windows")]
+        let relative_path = PathBuf::from("D:\\non-existing-path");
+        let absolute_path = relative_path.as_absolute().unwrap();
+        assert!(absolute_path.is_absolute());
+        assert_eq!(absolute_path, relative_path);
+    }
+
+    #[test]
+    fn test_relative_path_to_absolute() {
+        let cwd = env::current_dir().expect("Failed to get current working directory");
+
+        // Test with a relative path
+        let relative_path = PathBuf::from("src/main.rs");
+        let absolute_path = relative_path.as_absolute().unwrap();
+        assert!(absolute_path.is_absolute());
+        assert!(absolute_path.starts_with(&cwd));
+    }
+
+    #[test]
+    fn test_absolute_path_remains_unchanged() {
+        let cwd = env::current_dir().expect("Failed to get current working directory");
+
+        // Test with an absolute path
+        let absolute_path_input = cwd.join("src/main.rs");
+        let absolute_path_output = absolute_path_input.as_absolute().unwrap();
+        assert_eq!(absolute_path_input, absolute_path_output);
+    }
+
+    #[test]
+    fn test_complex_relative_path_to_absolute() {
+        let cwd = env::current_dir().expect("Failed to get current working directory");
+
+        // Test with a path containing `.` and `..`
+        let complex_relative_path = PathBuf::from("src/./../src/main.rs");
+        let complex_absolute_path = complex_relative_path.as_absolute().unwrap();
+        assert!(complex_absolute_path.is_absolute());
+        assert!(complex_absolute_path.starts_with(&cwd));
+        assert!(complex_absolute_path.ends_with("src/main.rs"));
+    }
+
+    #[test]
+    fn test_empty_path() {
+        let cwd = env::current_dir().expect("Failed to get current working directory");
+
+        // Test with an empty path
+        let empty_path = PathBuf::new();
+        let absolute_path = empty_path.as_absolute().unwrap();
+        assert_eq!(absolute_path, cwd);
+    }
+
+    #[test]
+    fn test_root_path() {
+        // Test with the root path
+        #[cfg(target_family = "unix")]
+        let root_path = PathBuf::from("/");
+        #[cfg(target_family = "windows")]
+        let root_path = PathBuf::from("D:\\");
+
+        let absolute_path = root_path.as_absolute().unwrap();
+        assert_eq!(absolute_path, root_path);
+    }
+
+    #[test]
+    fn test_dot_path() {
+        let cwd = env::current_dir().expect("Failed to get current working directory");
+
+        // Test with a single dot path
+        let dot_path = PathBuf::from(".");
+        let absolute_path = dot_path.as_absolute().unwrap();
+        assert_eq!(absolute_path, cwd);
+    }
+
+    #[test]
+    fn test_dot_dot_path() {
+        let cwd = env::current_dir().expect("Failed to get current working directory");
+        let parent_dir = cwd.parent().expect("Failed to get parent directory");
+
+        // Test with a double dot path
+        let dot_dot_path = PathBuf::from("..");
+        let absolute_path = dot_dot_path.as_absolute().unwrap();
+        assert_eq!(absolute_path, parent_dir);
+    }
+
+    #[test]
+    fn test_multiple_consecutive_slashes() {
+        let cwd = env::current_dir().expect("Failed to get current working directory");
+
+        // Test with multiple consecutive slashes
+        let multiple_slashes_path = PathBuf::from("src//main.rs");
+        let absolute_path = multiple_slashes_path.as_absolute().unwrap();
+        assert!(absolute_path.is_absolute());
+        assert!(absolute_path.starts_with(&cwd));
+        assert!(absolute_path.ends_with("src/main.rs"));
+    }
+
+    #[test]
+    fn test_path_within_sandbox() {
+        #[cfg(target_family = "unix")]
+        let sandbox = PathBuf::from("/sandbox");
+        #[cfg(target_family = "windows")]
+        let sandbox = PathBuf::from("D:\\sandbox");
+        let relative_path = PathBuf::from("file.txt");
+        let absolute_path = relative_path.as_sandboxed_absolute(&sandbox).unwrap();
+        assert!(absolute_path.is_absolute());
+        assert!(absolute_path.starts_with(&sandbox));
+    }
+
+    #[test]
+    #[should_panic(expected = "Path cannot escape the sandbox")]
+    fn test_path_outside_sandbox() {
+        #[cfg(target_family = "unix")]
+        let sandbox = PathBuf::from("/sandbox");
+        #[cfg(target_family = "windows")]
+        let sandbox = PathBuf::from("D:\\sandbox");
+        let outside_path = PathBuf::from("/outside/file.txt");
+        outside_path.as_sandboxed_absolute(&sandbox).unwrap();
+    }
+
+    #[test]
+    #[should_panic(expected = "Path cannot escape the sandbox")]
+    fn test_path_with_dot_dot_to_escape_sandbox() {
+        #[cfg(target_family = "unix")]
+        let sandbox = PathBuf::from("/sandbox");
+        #[cfg(target_family = "windows")]
+        let sandbox = PathBuf::from("D:\\sandbox");
+        let relative_path = PathBuf::from("../file.txt");
+        relative_path.as_sandboxed_absolute(&sandbox).unwrap();
+    }
+
+    #[test]
+    fn test_empty_path_in_sandbox() {
+        #[cfg(target_family = "unix")]
+        let sandbox = PathBuf::from("/sandbox");
+        #[cfg(target_family = "windows")]
+        let sandbox = PathBuf::from("D:\\sandbox");
+        let empty_path = PathBuf::new();
+        let absolute_path = empty_path.as_sandboxed_absolute(&sandbox).unwrap();
+        assert_eq!(absolute_path, sandbox);
+    }
+
+    #[test]
+    fn test_root_path_in_sandbox() {
+        let sandbox = PathBuf::from("/sandbox");
+        #[cfg(unix)]
+        let root_path = PathBuf::from("/");
+        #[cfg(windows)]
+        let root_path = PathBuf::from("D:\\");
+
+        let result = root_path.as_sandboxed_absolute(&sandbox);
+        assert!(result.is_err());
+    }
+
+    #[test]
+    fn test_multiple_consecutive_slashes_in_sandbox() {
+        #[cfg(target_family = "unix")]
+        let sandbox = PathBuf::from("/sandbox");
+        #[cfg(target_family = "unix")]
+        let multiple_slashes_path = PathBuf::from("src//main.rs");
+
+        #[cfg(target_family = "windows")]
+        let sandbox = PathBuf::from("D:\\sandbox");
+        #[cfg(target_family = "windows")]
+        let multiple_slashes_path = PathBuf::from("src\\\\main.rs");
+
+        let absolute_path = multiple_slashes_path
+            .as_sandboxed_absolute(&sandbox)
+            .unwrap();
+        assert!(absolute_path.is_absolute());
+        assert!(absolute_path.starts_with(&sandbox));
+        #[cfg(target_family = "unix")]
+        assert!(absolute_path.ends_with("src/main.rs"));
+        #[cfg(target_family = "windows")]
+        assert!(absolute_path.ends_with("src\\main.rs"));
+    }
+
+    #[test]
+    #[should_panic(expected = "Path cannot escape the sandbox")]
+    fn test_path_with_multiple_dot_dot_to_escape_sandbox() {
+        let sandbox = PathBuf::from("/sandbox");
+        let relative_path = PathBuf::from("///../../../....//");
+        relative_path.as_sandboxed_absolute(&sandbox).unwrap();
+    }
+
+    #[test]
+    #[should_panic(expected = "Path cannot escape the sandbox")]
+    fn test_path_with_nested_dot_dot_to_escape_sandbox() {
+        let sandbox = PathBuf::from("/sandbox");
+        let relative_path = PathBuf::from("nested/../../../../file.txt");
+        relative_path.as_sandboxed_absolute(&sandbox).unwrap();
+    }
+
+    #[test]
+    #[should_panic(expected = "Path cannot escape the sandbox")]
+    fn test_path_with_mixed_dots_and_slashes() {
+        let sandbox = PathBuf::from("/sandbox");
+        let relative_path = PathBuf::from("./././.././../file.txt");
+        relative_path.as_sandboxed_absolute(&sandbox).unwrap();
+    }
+
+    #[test]
+    fn test_path_with_trailing_slashes() {
+        #[cfg(target_family = "unix")]
+        let sandbox = PathBuf::from("/sandbox");
+        #[cfg(target_family = "unix")]
+        let relative_path = PathBuf::from("file.txt///");
+
+        #[cfg(target_family = "windows")]
+        let sandbox = PathBuf::from("D:\\sandbox");
+        #[cfg(target_family = "windows")]
+        let relative_path = PathBuf::from("file.txt\\\\\\");
+
+        let absolute_path = relative_path.as_sandboxed_absolute(&sandbox).unwrap();
+        assert!(absolute_path.is_absolute());
+        assert!(absolute_path.starts_with(&sandbox));
+    }
+
+    #[test]
+    #[should_panic(expected = "Path cannot escape the sandbox")]
+    fn test_path_with_leading_slashes() {
+        let sandbox = PathBuf::from("/sandbox");
+        let some_malicious_path = PathBuf::from("///file.txt");
+        some_malicious_path.as_sandboxed_absolute(&sandbox).unwrap();
+    }
+}
diff --git a/src/hooks/context.rs b/src/hooks/context.rs
new file mode 100644
index 0000000..1c8c514
--- /dev/null
+++ b/src/hooks/context.rs
@@ -0,0 +1,12 @@
+use std::path::PathBuf;
+
+use crate::template::LiquidObjectResource;
+
+#[derive(Debug)]
+pub struct RhaiHooksContext {
+    pub liquid_object: LiquidObjectResource,
+    pub allow_commands: bool,
+    pub silent: bool,
+    pub working_directory: PathBuf,
+    pub destination_directory: PathBuf,
+}
diff --git a/src/hooks/env_mod.rs b/src/hooks/env_mod.rs
new file mode 100644
index 0000000..7b3ece8
--- /dev/null
+++ b/src/hooks/env_mod.rs
@@ -0,0 +1,60 @@
+use std::path::PathBuf;
+
+use rhai::Module;
+
+pub struct Environment {
+    /// The temp directory where the template repository is pre-processed
+    pub working_directory: PathBuf,
+    /// The destination directory where the template repository is copied to
+    pub destination_directory: PathBuf,
+}
+
+/// Creates the system module, containing the `command` function,
+/// which allows you to run system command.
+pub fn create_module(env: Environment) -> Module {
+    let mut module = Module::new();
+
+    module.set_var(
+        "working_directory",
+        env.working_directory.to_string_lossy().to_string(),
+    );
+    module.set_var(
+        "destination_directory",
+        env.destination_directory.to_string_lossy().to_string(),
+    );
+
+    module
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::{
+        hooks::{create_rhai_engine, RhaiHooksContext},
+        template::LiquidObjectResource,
+    };
+    use tempfile::TempDir;
+
+    #[test]
+    fn test_env_module() {
+        let tmp_dir = TempDir::new().unwrap();
+        let context = RhaiHooksContext {
+            working_directory: tmp_dir.path().to_path_buf(),
+            destination_directory: tmp_dir.path().join("destination").to_path_buf(),
+            liquid_object: LiquidObjectResource::default(),
+            allow_commands: true,
+            silent: true,
+        };
+        let engine = create_rhai_engine(&context);
+
+        let working_directory = engine.eval::<String>("env::working_directory").unwrap();
+        assert_eq!(
+            working_directory,
+            context.working_directory.to_string_lossy()
+        );
+        let destination_directory = engine.eval::<String>("env::destination_directory").unwrap();
+        assert_eq!(
+            destination_directory,
+            context.destination_directory.to_string_lossy()
+        );
+    }
+}
diff --git a/src/hooks/file_mod.rs b/src/hooks/file_mod.rs
index a19f06d..004a56c 100644
--- a/src/hooks/file_mod.rs
+++ b/src/hooks/file_mod.rs
@@ -1,6 +1,6 @@
+use crate::absolute_path::AbsolutePathExt;
 use console::style;
-use path_absolutize::Absolutize;
-use rhai::{Array, Module};
+use rhai::{Array, Dynamic, Module};
 use std::io::Write;
 use std::path::{Path, PathBuf};
 
@@ -14,7 +14,7 @@ pub fn create_module(dir: &Path) -> Module {
         let dir = dir.clone();
 
         move |path: &str| -> HookResult<bool> {
-            let path = to_absolute_path(&dir, path)?;
+            let path = to_sandboxed_absolute_path(&dir, path)?;
             Ok(path.exists())
         }
     });
@@ -23,8 +23,8 @@ pub fn create_module(dir: &Path) -> Module {
         let dir = dir.clone();
 
         move |from: &str, to: &str| -> HookResult<()> {
-            let from = to_absolute_path(&dir, from)?;
-            let to = to_absolute_path(&dir, to)?;
+            let from = to_sandboxed_absolute_path(&dir, from)?;
+            let to = to_sandboxed_absolute_path(&dir, to)?;
             std::fs::rename(from, to).map_err(|e| e.to_string())?;
             Ok(())
         }
@@ -34,7 +34,7 @@ pub fn create_module(dir: &Path) -> Module {
         let dir = dir.clone();
 
         move |file: &str| -> HookResult<()> {
-            let path = to_absolute_path(&dir, file)?;
+            let path = to_sandboxed_absolute_path(&dir, file)?;
             if path.exists() {
                 if path.is_file() {
                     std::fs::remove_file(path).map_err(|e| e.to_string())?;
@@ -50,15 +50,17 @@ pub fn create_module(dir: &Path) -> Module {
         let dir = dir.clone();
 
         move |file: &str, content: &str| -> HookResult<()> {
-            let file = to_absolute_path(&dir, file)?;
+            let file = to_sandboxed_absolute_path(&dir, file)?;
             std::fs::write(file, content).map_err(|e| e.to_string())?;
             Ok(())
         }
     });
 
     module.set_native_fn("write", {
+        let dir = dir.clone();
+
         move |file: &str, content: Array| -> HookResult<()> {
-            let file = to_absolute_path(&dir, file)?;
+            let file = to_sandboxed_absolute_path(&dir, file)?;
             let mut file = std::fs::File::create(file).map_err(|e| e.to_string())?;
             for v in content.iter() {
                 writeln!(file, "{v}").map_err(|e| e.to_string())?;
@@ -68,15 +70,36 @@ pub fn create_module(dir: &Path) -> Module {
         }
     });
 
+    // listdir(path);
+    module.set_native_fn("listdir", {
+        let dir = dir.clone();
+        move |path: &str| -> HookResult<Array> { listdir(&dir, path) }
+    });
+
+    // listdir(path = ".");
+    module.set_native_fn("listdir", {
+        let dir = dir.clone();
+        move || -> HookResult<Array> { listdir(&dir, ".") }
+    });
+
     module
 }
 
-fn to_absolute_path(base_dir: &Path, relative_path: &str) -> HookResult<PathBuf> {
-    let joined = base_dir.join(relative_path);
-    Ok(joined
-        .absolutize_virtually(base_dir)
-        .map_err(|_| invalid_path(relative_path))?
-        .into_owned())
+fn listdir(base_dir: &Path, path: &str) -> HookResult<Array> {
+    let entries = std::fs::read_dir(to_sandboxed_absolute_path(base_dir, path)?)
+        .map_err(|e| e.to_string())?
+        .filter_map(|e| e.ok())
+        .filter_map(|entry| entry.path().to_str().map(|s| s.to_string()))
+        .map(Dynamic::from)
+        .collect::<Array>();
+
+    Ok(entries)
+}
+
+fn to_sandboxed_absolute_path(sandbox_dir: &Path, any_path: &str) -> HookResult<PathBuf> {
+    Ok(PathBuf::from(any_path)
+        .as_sandboxed_absolute(sandbox_dir)
+        .map_err(|_| invalid_path(any_path))?)
 }
 
 fn invalid_path(path: &str) -> String {
@@ -86,3 +109,124 @@ fn invalid_path(path: &str) -> String {
         style(path).yellow(),
     )
 }
+
+#[cfg(test)]
+mod tests {
+    use std::io::Write;
+
+    use crate::{
+        hooks::{create_rhai_engine, RhaiHooksContext},
+        template::LiquidObjectResource,
+    };
+    use rhai::Array;
+    use tempfile::TempDir;
+
+    #[test]
+    fn test_to_sandboxed_absolute_path() {
+        let tmp_dir = prepare_file_system();
+
+        let absolute_path =
+            super::to_sandboxed_absolute_path(tmp_dir.path(), "Cargo.toml").unwrap();
+        assert_eq!(absolute_path, tmp_dir.path().join("Cargo.toml"));
+
+        let path = std::path::Path::new(".");
+        let absolute_path =
+            super::to_sandboxed_absolute_path(tmp_dir.path(), path.to_str().unwrap()).unwrap();
+        assert_eq!(absolute_path, tmp_dir.path());
+    }
+
+    #[test]
+    fn test_file_module() {
+        let tmp_dir = prepare_file_system();
+        let context = prepare_context(&tmp_dir);
+        let engine = create_rhai_engine(&context);
+        std::env::set_current_dir(tmp_dir.path()).unwrap();
+
+        let files = engine.eval::<Array>("file::listdir()").unwrap();
+        assert_eq!(files.len(), 2);
+        for file in files.iter() {
+            let file = file.clone().into_string().unwrap();
+            assert!(
+                file.ends_with(".dotfile") || file.ends_with("file1"),
+                "file: {file} did not end with .dotfile nor file1"
+            );
+        }
+    }
+
+    #[test]
+    fn test_listdir_with_parameter() {
+        let tmp_dir = prepare_file_system();
+        let context = prepare_context(&tmp_dir);
+        let engine = create_rhai_engine(&context);
+        std::env::set_current_dir(tmp_dir.path()).unwrap();
+
+        // cover the other listdir function with one path argument
+        let files = engine.eval::<Array>(r#"file::listdir(".")"#).unwrap();
+        assert_eq!(files.len(), 2);
+        for file in files.iter() {
+            let file = file.clone().into_string().unwrap();
+            assert!(
+                file.ends_with(".dotfile") || file.ends_with("file1"),
+                "file: {file} did not end with .dotfile nor file1"
+            );
+        }
+    }
+
+    #[test]
+    #[cfg(target_family = "unix")]
+    #[should_panic(expected = "Path must be inside template dir:")]
+    fn test_listdir_does_not_escape_sandboxed_directory() {
+        let tmp_dir = prepare_file_system();
+        let context = prepare_context(&tmp_dir);
+        let engine = create_rhai_engine(&context);
+        std::env::set_current_dir(tmp_dir.path()).unwrap();
+
+        let path_argument = "../../../../etc";
+        engine
+            .eval::<Array>(format!("file::listdir(\"{path_argument}\")").as_str())
+            .unwrap();
+    }
+
+    #[test]
+    #[cfg(target_family = "unix")]
+    #[should_panic(expected = "Path must be inside template dir:")]
+    fn test_listdir_does_not_escape_sandboxed_directory_2() {
+        let tmp_dir = prepare_file_system();
+        let context = prepare_context(&tmp_dir);
+        let engine = create_rhai_engine(&context);
+        std::env::set_current_dir(tmp_dir.path()).unwrap();
+
+        let path_argument = "////../../../../etc";
+        engine
+            .eval::<Array>(format!("file::listdir(\"{path_argument}\")").as_str())
+            .unwrap();
+    }
+
+    #[test]
+    #[should_panic(expected = "Path must be inside template dir:")]
+    fn test_listdir_error_case() {
+        let tmp_dir = TempDir::new().unwrap();
+        let context = prepare_context(&tmp_dir);
+        let engine = create_rhai_engine(&context);
+        engine.eval::<Array>(r#"file::listdir("/tmp")"#).unwrap();
+    }
+
+    fn prepare_file_system() -> TempDir {
+        let tmp_dir = TempDir::new().unwrap();
+        let mut file1 = std::fs::File::create(tmp_dir.path().join("file1")).unwrap();
+        file1.write_all(b"test1").unwrap();
+        let mut file2 = std::fs::File::create(tmp_dir.path().join(".dotfile")).unwrap();
+        file2.write_all(b"test1").unwrap();
+        tmp_dir
+    }
+
+    fn prepare_context(tmp_dir: &TempDir) -> RhaiHooksContext {
+        RhaiHooksContext {
+            working_directory: tmp_dir.path().to_path_buf(),
+            destination_directory: tmp_dir.path().join("destination").to_path_buf(),
+            liquid_object: LiquidObjectResource::default(),
+            allow_commands: true,
+            silent: true,
+        }
+    }
+}
diff --git a/src/hooks/mod.rs b/src/hooks/mod.rs
index 8461bc2..42267a5 100644
--- a/src/hooks/mod.rs
+++ b/src/hooks/mod.rs
@@ -1,16 +1,20 @@
 use anyhow::{Context, Result};
 use console::style;
+use env_mod::Environment;
 use heck::{
     ToKebabCase, ToLowerCamelCase, ToPascalCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase,
     ToTitleCase, ToUpperCamelCase,
 };
 use liquid::ValueView;
+use log::debug;
 use rhai::EvalAltResult;
 use std::{env, path::Path};
 
 use crate::emoji;
 use crate::template::LiquidObjectResource;
 
+mod context;
+mod env_mod;
 mod file_mod;
 mod system_mod;
 mod variable_mod;
@@ -19,6 +23,8 @@ type HookResult<T> = std::result::Result<T, Box<EvalAltResult>>;
 
 struct CleanupJob<F: FnOnce()>(Option<F>);
 
+pub use context::RhaiHooksContext;
+
 impl<F: FnOnce()> CleanupJob<F> {
     pub const fn new(f: F) -> Self {
         Self(Some(f))
@@ -31,15 +37,11 @@ impl<F: FnOnce()> Drop for CleanupJob<F> {
     }
 }
 
-pub fn execute_hooks(
-    dir: &Path,
-    liquid_object: &LiquidObjectResource,
-    scripts: &[String],
-    allow_commands: bool,
-    silent: bool,
-) -> Result<()> {
-    let engine = create_rhai_engine(dir, liquid_object, allow_commands, silent);
-    evaluate_scripts(dir, scripts, engine)?;
+pub fn execute_hooks(context: &RhaiHooksContext, scripts: &[String]) -> Result<()> {
+    debug!("executing rhai with context: {:?}", context);
+
+    let engine = create_rhai_engine(context);
+    evaluate_scripts(&context.working_directory, scripts, engine)?;
     Ok(())
 }
 
@@ -99,24 +101,29 @@ pub fn evaluate_script<T: Clone + 'static>(
     conditional_evaluation_engine.eval_expression::<T>(script)
 }
 
-pub fn create_rhai_engine(
-    dir: &Path,
-    liquid_object: &LiquidObjectResource,
-    allow_commands: bool,
-    silent: bool,
-) -> rhai::Engine {
+pub fn create_rhai_engine(context: &RhaiHooksContext) -> rhai::Engine {
     let mut engine = rhai::Engine::new();
 
     // register modules
-    let module = variable_mod::create_module(liquid_object);
+    let module = variable_mod::create_module(&context.liquid_object);
     engine.register_static_module("variable", module.into());
 
-    let module = file_mod::create_module(dir);
+    let module = file_mod::create_module(&context.working_directory);
     engine.register_static_module("file", module.into());
 
-    let module = system_mod::create_module(allow_commands, silent);
+    let module = system_mod::create_module(
+        context.working_directory.clone(),
+        context.allow_commands,
+        context.silent,
+    );
     engine.register_static_module("system", module.into());
 
+    let module = env_mod::create_module(Environment {
+        working_directory: context.working_directory.clone(),
+        destination_directory: context.destination_directory.clone(),
+    });
+    engine.register_static_module("env", module.into());
+
     // register functions for changing case
     engine.register_fn("to_kebab_case", |str: &str| str.to_kebab_case());
     engine.register_fn("to_lower_camel_case", |str: &str| str.to_lower_camel_case());
diff --git a/src/hooks/system_mod.rs b/src/hooks/system_mod.rs
index 21e5dfc..041f1f5 100644
--- a/src/hooks/system_mod.rs
+++ b/src/hooks/system_mod.rs
@@ -1,5 +1,10 @@
-use rhai::{Dynamic, Module};
-use std::{collections::BTreeMap, process::Command};
+use log::debug;
+use rhai::{Dynamic, FuncRegistration, Module};
+use std::{
+    collections::BTreeMap,
+    path::{Path, PathBuf},
+    process::Command,
+};
 use time::OffsetDateTime;
 
 use crate::{
@@ -11,18 +16,29 @@ use super::HookResult;
 
 /// Creates the system module, containing the `command` function,
 /// which allows you to run system command.
-pub fn create_module(allow_commands: bool, silent: bool) -> Module {
+pub fn create_module(working_directory: PathBuf, allow_commands: bool, silent: bool) -> Module {
     let mut module = Module::new();
 
-    module.set_native_fn("command", move |name: &str, commands_args: rhai::Array| {
-        run_command(name, commands_args, allow_commands, silent)
+    let cwd = working_directory.clone();
+    FuncRegistration::new("command").set_into_module(
+        &mut module,
+        move |name: &str, commands_args: rhai::Array| {
+            run_command(&cwd, name, commands_args, allow_commands, silent)
+        },
+    );
+
+    let cwd = working_directory.clone();
+    FuncRegistration::new("command").set_into_module(&mut module, move |name: &str| {
+        run_command(&cwd, name, rhai::Array::new(), allow_commands, silent)
     });
+
     module.set_native_fn("date", get_utc_date);
 
     module
 }
 
 fn run_command(
+    working_directory: &Path,
     name: &str,
     args: rhai::Array,
     allow_commands: bool,
@@ -73,19 +89,43 @@ fn run_command(
         return Err(format!("User denied execution of system command `{full_command}`.").into());
     }
 
-    let status = Command::new(name).args(args).status();
-
-    match status {
-        Ok(status) => {
-            if status.success() {
-                Ok(Dynamic::UNIT)
-            } else {
-                Err(
-                    format!("System command `{full_command}` returned non-zero status: {status}")
-                        .into(),
-                )
+    debug!(
+        "the command is executed within the working directory: {}",
+        working_directory.display()
+    );
+
+    let output = if cfg!(target_os = "windows") {
+        Command::new("cmd")
+            .arg("/C")
+            .arg(&full_command)
+            .current_dir(working_directory)
+            .output()
+    } else {
+        Command::new("sh")
+            .arg("-c")
+            .arg(&full_command)
+            .current_dir(working_directory)
+            .output()
+    };
+
+    debug!("Command Call: {output:?}");
+    match output {
+        Ok(output) if output.status.success() && output.status.code().unwrap_or(1) == 0 => {
+            match output.stdout.len() {
+                0 => Ok(Dynamic::UNIT),
+                _ => {
+                    let output_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
+                    debug!("Rhai output: {output_str}");
+
+                    Ok(Dynamic::from(output_str))
+                }
             }
         }
+        Ok(output) => Err(format!(
+            "System command `{full_command}` failed to execute: {}",
+            String::from_utf8_lossy(&output.stderr).trim()
+        )
+        .into()),
         Err(e) => Err(format!("System command `{full_command}` failed to execute: {e}").into()),
     }
 }
@@ -110,3 +150,124 @@ fn construct_date_map(dt: OffsetDateTime) -> Dynamic {
     );
     Dynamic::from_map(value)
 }
+
+#[cfg(test)]
+mod tests {
+    use std::io::Write;
+
+    use crate::{
+        hooks::{create_rhai_engine, RhaiHooksContext},
+        template::LiquidObjectResource,
+    };
+    use rhai::Engine;
+    use tempfile::TempDir;
+
+    #[test]
+    fn test_system_module() {
+        let tmp_dir = TempDir::new().unwrap();
+        let mut file1 = std::fs::File::create(tmp_dir.path().join("file1")).unwrap();
+        file1.write_all(b"test1").unwrap();
+        let context = RhaiHooksContext {
+            working_directory: tmp_dir.path().to_path_buf(),
+            destination_directory: tmp_dir.path().join("destination").to_path_buf(),
+            liquid_object: LiquidObjectResource::default(),
+            allow_commands: true,
+            silent: true,
+        };
+        std::env::set_current_dir(&context.working_directory).unwrap();
+        let engine = create_rhai_engine(&context);
+
+        let pwd = engine.eval::<String>(r#"system::command("pwd")"#).unwrap();
+        assert_eq!(
+            pwd,
+            pwd.trim_end(),
+            "there should be no trailing whitespace or newline"
+        );
+        // for reasons on the windows GH runner we get this issue:
+        // assertion `left == right` failed: the system::command should run in the context of the working_directory: C:\Users\RUNNER~1\AppData\Local\Temp\.tmpDXzQr1
+        //   left: "/tmp/.tmpDXzQr1"
+        //   right: "\\\\?\\C:\\Users\\runneradmin\\AppData\\Local\\Temp\\.tmpDXzQr1"
+        #[cfg(target_family = "unix")]
+        assert_eq!(
+            pwd,
+            tmp_dir.path().canonicalize().unwrap().to_str().unwrap(),
+            "the system::command should run in the context of the working_directory: {}",
+            tmp_dir.path().display()
+        );
+
+        #[cfg(target_family = "unix")]
+        let content = engine
+            .eval::<String>(r#"system::command("/bin/cat", ["file1"])"#)
+            .unwrap();
+
+        #[cfg(target_family = "windows")]
+        let content = engine
+            .eval::<String>(r#"system::command("type", ["file1"])"#)
+            .unwrap();
+
+        assert_eq!(content, "test1");
+    }
+
+    // Note, macos and linux have different error message behaviour for this case
+    // macos: System command `nonexistent_command` failed to execute: No such file or directory (os error 2)
+    // linux: System command `nonexistent_command` failed to execute: "
+    //
+    // root cause is that on linux stderr is empty, on macos it contains the error message
+    #[test]
+    #[should_panic(expected = "System command `nonexistent_command` failed to execute: ")]
+    fn test_run_command_failure() {
+        let tmp_dir = TempDir::new().unwrap();
+        let context = RhaiHooksContext {
+            working_directory: tmp_dir.path().to_path_buf(),
+            destination_directory: tmp_dir.path().join("destination").to_path_buf(),
+            liquid_object: LiquidObjectResource::default(),
+            allow_commands: true,
+            silent: true,
+        };
+        let engine = create_rhai_engine(&context);
+        std::env::set_current_dir(tmp_dir.path()).unwrap();
+
+        engine
+            .eval::<String>(r#"system::command("nonexistent_command")"#)
+            .unwrap();
+    }
+
+    #[test]
+    #[should_panic(
+        expected = "Cannot prompt for system command confirmation in silent mode. Use --allow-commands if you want to allow the template to run system commands in silent mode."
+    )]
+    fn test_run_command_silent_mode_denied() {
+        let tmp_dir = TempDir::new().unwrap();
+        let context = RhaiHooksContext {
+            working_directory: tmp_dir.path().to_path_buf(),
+            destination_directory: tmp_dir.path().join("destination").to_path_buf(),
+            liquid_object: LiquidObjectResource::default(),
+            allow_commands: false,
+            silent: true,
+        };
+        let engine = create_rhai_engine(&context);
+        std::env::set_current_dir(tmp_dir.path()).unwrap();
+
+        engine
+            .eval::<String>(r#"system::command("echo", ["hello"])"#)
+            .unwrap();
+    }
+
+    #[test]
+    fn test_get_utc_date() {
+        let tmp_dir = TempDir::new().unwrap();
+        let mut engine = Engine::new();
+        let module = super::create_module(tmp_dir.path().to_path_buf(), true, true);
+        engine.register_static_module("system", module.into());
+
+        let result = engine.eval::<rhai::Map>(r#"system::date()"#).unwrap();
+        let now = time::OffsetDateTime::now_utc();
+        assert!(result.contains_key("year"));
+        assert!(result.contains_key("month"));
+        assert!(result.contains_key("day"));
+
+        assert_eq!(result["year"].as_int().unwrap(), i64::from(now.year()));
+        assert_eq!(result["month"].as_int().unwrap(), now.month() as i64);
+        assert_eq!(result["day"].as_int().unwrap(), i64::from(now.day()));
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index f597625..60a007c 100755
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -22,6 +22,7 @@
     clippy::use_self,
 )]
 
+mod absolute_path;
 mod app_config;
 mod args;
 mod config;
@@ -53,7 +54,7 @@ use console::style;
 use copy::copy_files_recursively;
 use env_logger::fmt::Formatter;
 use fs_err as fs;
-use hooks::execute_hooks;
+use hooks::{execute_hooks, RhaiHooksContext};
 use ignore_me::remove_dir_files;
 use interactive::{prompt_and_check_variable, LIST_SEP};
 use log::Record;
@@ -434,19 +435,20 @@ fn expand_template(
     args: &GenerateArgs,
 ) -> Result<PathBuf> {
     let liquid_object = create_liquid_object(user_parsed_input)?;
+    let context = RhaiHooksContext {
+        liquid_object: liquid_object.clone(),
+        allow_commands: user_parsed_input.allow_commands(),
+        silent: user_parsed_input.silent(),
+        working_directory: template_dir.to_owned(),
+        destination_directory: user_parsed_input.destination().to_owned(),
+    };
 
     // run init hooks - these won't have access to `crate_name`/`within_cargo_project`
     // variables, as these are not set yet. Furthermore, if `project-name` is set, it is the raw
     // user input!
     // The init hooks are free to set `project-name` (but it will be validated before further
     // use).
-    execute_hooks(
-        template_dir,
-        &liquid_object,
-        &config.get_init_hooks(),
-        user_parsed_input.allow_commands(),
-        user_parsed_input.silent(),
-    )?;
+    execute_hooks(&context, &config.get_init_hooks())?;
 
     let project_name_input = ProjectNameInput::try_from((&liquid_object, user_parsed_input))?;
     let project_name = ProjectName::from((&project_name_input, user_parsed_input));
@@ -488,14 +490,14 @@ fn expand_template(
     )?;
     add_missing_provided_values(&liquid_object, user_parsed_input.template_values())?;
 
+    let context = RhaiHooksContext {
+        liquid_object: Arc::clone(&liquid_object),
+        destination_directory: destination.as_ref().to_owned(),
+        ..context
+    };
+
     // run pre-hooks
-    execute_hooks(
-        template_dir,
-        &liquid_object,
-        &config.get_pre_hooks(),
-        user_parsed_input.allow_commands(),
-        user_parsed_input.silent(),
-    )?;
+    execute_hooks(&context, &config.get_pre_hooks())?;
 
     // walk/evaluate the template
     let all_hook_files = config.get_hook_files();
@@ -537,13 +539,7 @@ fn expand_template(
     };
 
     // run post-hooks
-    execute_hooks(
-        template_dir,
-        &liquid_object,
-        &config.get_post_hooks(),
-        user_parsed_input.allow_commands(),
-        user_parsed_input.silent(),
-    )?;
+    execute_hooks(&context, &config.get_post_hooks())?;
 
     // remove all hook and filter files as they are never part of the template output
     let rhai_filter_files = rhai_filter_files
diff --git a/src/template_filters.rs b/src/template_filters.rs
index 1d781a8..ab1813a 100644
--- a/src/template_filters.rs
+++ b/src/template_filters.rs
@@ -17,7 +17,7 @@ use std::{
 };
 
 use crate::{
-    hooks::{create_rhai_engine, PoisonError},
+    hooks::{create_rhai_engine, PoisonError, RhaiHooksContext},
     template::LiquidObjectResource,
 };
 
@@ -144,13 +144,15 @@ impl Filter for RhaiFilter {
     ) -> Result<Value, liquid_core::Error> {
         // Unfortunately, liquid filters can't really cause liquid to fail. It just leaves the
         // substitution as is - thus we resort to displaying warnings to the user.
+        let context = RhaiHooksContext {
+            liquid_object: Arc::clone(&self.liquid_object),
+            allow_commands: self.allow_commands,
+            silent: self.silent,
+            working_directory: self.template_dir.clone(),
+            destination_directory: self.template_dir.clone(),
+        };
 
-        let engine = create_rhai_engine(
-            &self.template_dir,
-            &self.liquid_object,
-            self.allow_commands,
-            self.silent,
-        );
+        let engine = create_rhai_engine(&context);
         let file_path = PathBuf::from(input.to_kstr().to_string());
         if !file_path.exists() {
             warn!(
diff --git a/src/template_variables/project_name.rs b/src/template_variables/project_name.rs
index 6311ea9..57e9d9b 100644
--- a/src/template_variables/project_name.rs
+++ b/src/template_variables/project_name.rs
@@ -45,9 +45,23 @@ pub fn sanitize_project_name(name: &str) -> String {
 
 #[cfg(test)]
 mod tests {
+    use crate::absolute_path::AbsolutePathExt;
+    use std::path::Path;
+
     use super::*;
     use crate::user_parsed_input::UserParsedInputBuilder;
 
+    #[test]
+    fn ensure_that_absolute_path_of_non_existing_path_works() {
+        #[cfg(target_family = "unix")]
+        let path = Path::new("/non-existing-path");
+        #[cfg(target_family = "windows")]
+        let path = Path::new("D:\\non-existing-path");
+
+        let path_absolutize = path.as_absolute().unwrap();
+        assert_eq!(path_absolutize, path);
+    }
+
     #[test]
     fn test_snake_case_is_accepted() {
         let input = ProjectNameInput("lock_firmware".to_string());
diff --git a/src/user_parsed_input.rs b/src/user_parsed_input.rs
index 1a3d768..65b747a 100644
--- a/src/user_parsed_input.rs
+++ b/src/user_parsed_input.rs
@@ -6,8 +6,8 @@ use std::{
     path::{Path, PathBuf},
 };
 
+use crate::absolute_path::AbsolutePathExt;
 use console::style;
-use path_absolutize::Absolutize;
 use regex::Regex;
 
 use crate::{app_config::AppConfig, template_variables::CrateType, GenerateArgs, Vcs};
@@ -94,7 +94,11 @@ impl UserParsedInput {
         let destination = args
             .destination
             .as_ref()
-            .map(|p| p.absolutize().unwrap().to_path_buf())
+            .map(|p| {
+                p.as_absolute()
+                    .expect("cannot get the absolute path of the destination folder")
+                    .to_path_buf()
+            })
             .unwrap_or_else(|| env::current_dir().unwrap_or_else(|_| ".".into()));
 
         let mut default_values = app_config.values.clone().unwrap_or_default();


Test patch:
diff --git a/tests/integration/hooks_and_rhai.rs b/tests/integration/hooks_and_rhai.rs
index bcc02fb..ee9144b 100644
--- a/tests/integration/hooks_and_rhai.rs
+++ b/tests/integration/hooks_and_rhai.rs
@@ -149,15 +149,13 @@ fn it_fails_when_a_system_command_returns_non_zero_exit_code() {
     let template = tempdir()
         .file(
             "system-script.rhai",
-            indoc! {r#"
-                let output = system::command("mkdir", ["invalid_/.dir_name"]);
-            "#},
+            r#"let output = system::command("mkdir", ["invalid_/.dir_name"]);"#,
         )
         .file(
             "cargo-generate.toml",
             indoc! {r#"
-            [hooks]
-            post = ["system-script.rhai"]
+                [hooks]
+                post = ["system-script.rhai"]
             "#},
         )
         .init_git()
@@ -173,10 +171,19 @@ fn it_fails_when_a_system_command_returns_non_zero_exit_code() {
         .assert()
         .failure()
         .stderr(
-            predicates::str::contains(
-                "System command `mkdir invalid_/.dir_name` returned non-zero status",
-            )
-            .from_utf8(),
+            if cfg!(target_os = "linux") {
+                predicates::str::contains(
+                    "System command `mkdir invalid_/.dir_name` failed to execute: mkdir: cannot create directory ‘invalid_/.dir_name’: No such file or directory"
+                ).from_utf8()
+            } else if cfg!(target_os = "macos") {
+                predicates::str::contains(
+                    "System command `mkdir invalid_/.dir_name` failed to execute: mkdir: invalid_: No such file or directory"
+                ).from_utf8()
+            } else { // Windows
+                predicates::str::contains(
+                    "System command `mkdir invalid_/.dir_name` failed to execute: The syntax of the command is incorrect."
+                ).from_utf8()
+            }
         );
 }