Repository: jacobdeichert/mask (language: rust)

Issue:
Windows support?
Currently, this is unknown. I'm pretty sure the executor logic will need to be adjusted for Windows.


Discussion / hints:
FWIW, I tried removing all paths to `sh.exe` from my system (i.e. removing paths such as `C:\Program Files\Git\cmd` which contains the Git for Windows version of sh, as well as renaming other instances of `sh.exe` on my system that were being picked up like the ones included with Cmder), ensured that `sh` was no longer being referenced by running `where sh`, then tried running `mask`. I found that the program seems to exit without doing anything nor does it print any sort of error to the terminal.
Thanks for checking this! So it sounds like `mask` works with the git bash shell and probably also Ubuntu on Windows. 
Oh i misread this clearly. Sounds like mask does not work on windows.
@jakedeichert I suppose if we add PowerShell as a scripting runtime, this could be solved.
Adding PowerShell support will probably be useful to people 👍 

But mask should also work with WSL and bash/sh scripting. I have a feeling it's also not handling Windows paths correctly...

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 413a6e7..cd8540a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,8 +4,12 @@ on: [push, pull_request]
 
 jobs:
   build:
-    name: Build
-    runs-on: ubuntu-latest
+    name: ${{ matrix.platform }}-build
+    runs-on: ${{ matrix.platform }}
+    strategy:
+      fail-fast: false
+      matrix:
+        platform: [ubuntu-latest, windows-latest, macos-latest]
     steps:
       - name: Checkout master
         uses: actions/checkout@master
@@ -15,8 +19,12 @@ jobs:
         run: cargo build --release --frozen
 
   test:
-    name: Test
-    runs-on: ubuntu-latest
+    name: ${{ matrix.platform }}-test
+    runs-on: ${{ matrix.platform }}
+    strategy:
+      fail-fast: false
+      matrix:
+        platform: [ubuntu-latest, windows-latest, macos-latest]
     steps:
       - name: Checkout master
         uses: actions/checkout@master
@@ -28,16 +36,16 @@ jobs:
         run: cargo fetch
       - name: Build in test mode
         run: cargo build --tests --frozen
-      - name: Make mask available globally for certain test suites that depend on it
-        run: cp ./target/debug/mask /usr/share/rust/.cargo/bin
+      - name: Make mask available globally (windows)
+        run: copy ./target/debug/mask.exe ~/.cargo/bin/
+        if: matrix.platform == 'windows-latest'
+      - name: Make mask available globally (linux)
+        run: cp ./target/debug/mask ~/.cargo/bin
+        if: matrix.platform == 'ubuntu-latest'
+      - name: Make mask available globally (mac)
+        run: cp ./target/debug/mask ~/.cargo/bin
+        if: matrix.platform == 'macos-latest'
       - name: Run tests
         run: cargo test --frozen
 
-  format:
-    name: Format
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout master
-        uses: actions/checkout@master
-      - name: Verify formatting is correct
-        run: cargo fmt --all -- --check
+
diff --git a/maskfile.md b/maskfile.md
index 5ba1584..a148486 100644
--- a/maskfile.md
+++ b/maskfile.md
@@ -27,6 +27,23 @@ else
 fi
 ~~~
 
+**Note:** On Windows platforms, `mask` falls back to running `powershell` code blocks.
+
+~~~powershell
+param (
+    $maskfile_command = $env:maskfile_command,
+    $watch = $env:watch
+)
+
+$cargo_cmd = "cargo run -- $maskfile_command"
+$extra_args = "--exts rs --restart $cargo_cmd"
+
+if ($watch) {
+    Start-Process watchexec -ArgumentList $extra_args -NoNewWindow -PassThru
+} else {
+    cargo run -- $maskfile_command
+}
+~~~
 
 
 ## build
@@ -37,7 +54,9 @@ fi
 cargo build --release
 ~~~
 
-
+~~~powershell
+cargo build --release
+~~~
 
 ## link
 
@@ -47,6 +66,9 @@ cargo build --release
 cargo install --force --path .
 ~~~
 
+~~~powershell
+[Diagnostics.Process]::Start("cargo", "install --force --path .").WaitForExit()
+~~~
 
 
 ## test
@@ -78,7 +100,26 @@ fi
 echo "Tests passed!"
 ~~~
 
+~~~powershell
+param (
+    $file = $env:file
+)
 
+$extra_args = ""
+$verbose = $env:verbose 
+
+if ($verbose) {
+    $extra_args = "-- --nocapture --test-threads=1"
+}
+
+Write-Output "Running tests..."
+if (!$file) {
+    cargo test $extra_args
+} else {
+    cargo test --test $file $extra_args
+}
+Write-Output "Tests passed!"
+~~~
 
 ## deps
 
@@ -92,6 +133,9 @@ echo "Tests passed!"
 cargo update
 ~~~
 
+~~~powershell
+cargo update
+~~~
 
 
 ## format
@@ -111,6 +155,17 @@ else
 fi
 ~~~
 
+~~~powershell
+param (
+    $check = $env:check
+)
+
+if ($check) {
+    cargo fmt --all -- --check
+} else {
+    cargo fmt
+}
+~~~
 
 
 ## lint
@@ -120,3 +175,7 @@ fi
 ~~~bash
 cargo clippy
 ~~~
+
+~~~powershell
+cargo clippy
+~~~
diff --git a/src/executor.rs b/src/executor.rs
index 1efeb3e..f41440a 100644
--- a/src/executor.rs
+++ b/src/executor.rs
@@ -53,6 +53,18 @@ fn prepare_command(cmd: &Command) -> process::Command {
             child.arg("-r").arg(source);
             child
         }
+        #[cfg(windows)]
+        "cmd" | "batch" => {
+            let mut child = process::Command::new("cmd.exe");
+            child.arg("/c").arg(source);
+            child
+        }
+        #[cfg(windows)]
+        "powershell" => {
+            let mut child = process::Command::new("powershell.exe");
+            child.arg("-c").arg(source);
+            child
+        }
         // Any other executor that supports -c (sh, bash, zsh, fish, dash, etc...)
         _ => {
             let mut child = process::Command::new(executor);
diff --git a/src/parser.rs b/src/parser.rs
index 673cd27..f81aa9b 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -27,6 +27,16 @@ pub fn build_command_structure(maskfile_contents: String) -> Command {
                         }
                         current_command = Command::new(heading_level as u8);
                     }
+                    #[cfg(not(windows))]
+                    Tag::CodeBlock(lang_code) => {
+                        if lang_code.to_string() != "powershell"
+                            && lang_code.to_string() != "batch"
+                            && lang_code.to_string() != "cmd"
+                        {
+                            current_command.script.executor = lang_code.to_string();
+                        }
+                    }
+                    #[cfg(windows)]
                     Tag::CodeBlock(lang_code) => {
                         current_command.script.executor = lang_code.to_string();
                     }
@@ -52,6 +62,16 @@ pub fn build_command_structure(maskfile_contents: String) -> Command {
                 Tag::BlockQuote => {
                     current_command.desc = text.clone();
                 }
+                #[cfg(not(windows))]
+                Tag::CodeBlock(lang_code) => {
+                    if lang_code.to_string() != "powershell"
+                        && lang_code.to_string() != "batch"
+                        && lang_code.to_string() != "cmd"
+                    {
+                        current_command.script.source = text.to_string();
+                    }
+                }
+                #[cfg(windows)]
                 Tag::CodeBlock(_) => {
                     current_command.script.source = text.to_string();
                 }


Test patch:
diff --git a/tests/arguments_and_flags_test.rs b/tests/arguments_and_flags_test.rs
index 8f85b48..0b66c8f 100644
--- a/tests/arguments_and_flags_test.rs
+++ b/tests/arguments_and_flags_test.rs
@@ -17,6 +17,15 @@ fn positional_arguments() {
 ~~~bash
 echo "Testing $test_case in $file"
 ~~~
+
+~~~powershell
+param (
+    $test_case = $env:test_case,
+    $file = $env:file
+)
+
+Write-Output "Testing $test_case in $file"
+~~~
 "#,
     );
 
@@ -64,6 +73,20 @@ else
     echo $PORT
 fi
 ```
+
+```powershell
+param (
+    [string]
+    [Parameter(Mandatory = $false)]
+    $port = $env:port
+)
+
+if ($env:verbose) {
+    Write-Output "Starting an http server on PORT: $port"
+} else {
+    Write-Output $port
+}
+```
 "#,
     );
 
@@ -97,6 +120,16 @@ mod when_entering_negative_numbers {
 ~~~bash
 echo $(($a + $b))
 ~~~
+
+~~~powershell
+param (
+    [int]$a = $env:a,
+    [int]$b = $env:b
+)
+$sum = "$($a + $b)"
+
+Write-Output $sum
+~~~
 "#,
         );
 
@@ -124,6 +157,16 @@ echo $(($a + $b))
 ~~~bash
 echo $(($a + $b))
 ~~~
+
+~~~powershell
+param (
+    [int]$a = $env:a,
+    [int]$b = $env:b
+)
+$sum = "$($a + $b)"
+
+Write-Output $sum
+~~~
 "#,
         );
 
@@ -152,6 +195,13 @@ mod numerical_option_flag {
 ~~~bash
 echo "Value: $val"
 ~~~
+
+~~~powershell
+param (
+    $in = $env:val
+)
+Write-Output "Value: $in"
+~~~
 "#,
         );
 
@@ -176,6 +226,14 @@ echo "Value: $val"
 ~~~bash
 echo "Value: $val"
 ~~~
+
+~~~powershell
+param (
+    [int]$in = $env:val
+)
+
+Write-Output "Value: $in"
+~~~
 "#,
         );
 
@@ -200,6 +258,13 @@ echo "Value: $val"
 ~~~bash
 echo "Value: $val"
 ~~~
+
+~~~powershell
+param (
+    [Double]$in = $env:val
+)
+Write-Output "Value: $in"
+~~~
 "#,
         );
 
@@ -224,6 +289,10 @@ echo "Value: $val"
 ~~~bash
 echo "This shouldn't render"
 ~~~
+
+~~~powershell
+Write-Output "This shouldn't render"
+~~~
 "#,
         );
 
@@ -251,6 +320,10 @@ echo "This shouldn't render"
 ~~~bash
 echo "No arg this time"
 ~~~
+
+~~~powershell
+Write-Output "No arg this time"
+~~~
 "#,
         );
 
diff --git a/tests/env_vars_test.rs b/tests/env_vars_test.rs
index 48ed484..c8281f0 100644
--- a/tests/env_vars_test.rs
+++ b/tests/env_vars_test.rs
@@ -20,11 +20,23 @@ mod env_var_mask {
 $MASK test
 ~~~
 
+~~~powershell
+$path = $env:MASK.replace("\\?\", "")
+$pos = $path.IndexOf(" ");
+$arglist = $path.Substring($pos + 1);
+
+Start-Process mask.exe -ArgumentList "$arglist test" -wait -NoNewWindow -PassThru
+~~~
+
 ## test
 
 ~~~bash
 echo "tests passed"
 ~~~
+
+~~~powershell
+Write-Output "tests passed"
+~~~
 "#,
         );
 
@@ -45,15 +57,29 @@ echo "tests passed"
 ~~~bash
 echo "mask = $MASK"
 ~~~
+
+~~~powershell
+param (
+    $var = "$env:mask /"
+)
+
+Write-Output "mask = $var"
+~~~
+
 "#,
         );
 
+        #[cfg(windows)]
+        let predicate = contains("mask = mask --maskfile \\");
+        #[cfg(not(windows))]
+        let predicate = contains("mask = mask --maskfile /");
+
         common::run_mask(&maskfile_path)
             .current_dir(".github")
             .command("run")
             .assert()
             // Absolute maskfile path starts with /
-            .stdout(contains("mask = mask --maskfile /"))
+            .stdout(predicate)
             // And ends with maskfile.md
             .stdout(contains("maskfile.md"))
             .success();
@@ -73,6 +99,14 @@ mod env_var_maskfile_dir {
 ~~~bash
 echo "maskfile_dir = $MASKFILE_DIR"
 ~~~
+
+~~~powershell
+param (
+    $var = $env:maskfile_dir
+)
+
+Write-Output "maskfile_dir = /$var"
+~~~
 "#,
         );
 
diff --git a/tests/integration_test.rs b/tests/integration_test.rs
index 148b93a..f3011c9 100644
--- a/tests/integration_test.rs
+++ b/tests/integration_test.rs
@@ -128,6 +128,10 @@ mod exits_with_the_child_process_status_code {
 ~~~sh
 exit 0
 ~~~
+
+~~~powershell
+Exit 0
+~~~
 "#,
         );
 
@@ -147,6 +151,10 @@ exit 0
 ~~~sh
 exit 1
 ~~~
+
+~~~powershell
+Exit 1
+~~~
 "#,
         );
 
@@ -166,6 +174,10 @@ exit 1
 ~~~sh
 exit 2
 ~~~
+
+~~~powershell
+Exit 2
+~~~
 "#,
         );
 
diff --git a/tests/subcommands_test.rs b/tests/subcommands_test.rs
index 5cadcea..921b706 100644
--- a/tests/subcommands_test.rs
+++ b/tests/subcommands_test.rs
@@ -22,6 +22,14 @@ fn positional_arguments() {
 echo "Starting service $service_name"
 ~~~
 
+~~~powershell
+param(
+    $service_name = $env:service_name
+)
+
+Write-Output "Starting service $service_name"
+~~~
+
 ### services stop (service_name)
 
 > Stop a service.
@@ -29,6 +37,14 @@ echo "Starting service $service_name"
 ~~~bash
 echo "Stopping service $service_name"
 ~~~
+
+~~~powershell
+param(
+    $service_name = $service_name
+)
+
+Write-Output "Stopping service $service_name"
+~~~
 "#,
     );
 
@@ -48,13 +64,17 @@ fn exits_with_error_when_missing_subcommand() {
 "#,
     );
 
+    #[cfg(not(windows))]
+    let predicate =
+        contains("error: 'mask service' requires a subcommand, but one was not provided");
+    #[cfg(windows)]
+    let predicate =
+        contains("error: 'mask.exe service' requires a subcommand, but one was not provided");
     common::run_mask(&maskfile_path)
         .command("service")
         .assert()
         .code(1)
-        .stderr(contains(
-            "error: 'mask service' requires a subcommand, but one was not provided",
-        ))
+        .stderr(predicate)
         .failure();
 }
 
diff --git a/tests/supported_runtimes_test.rs b/tests/supported_runtimes_test.rs
index 79fbd22..c9ad3af 100644
--- a/tests/supported_runtimes_test.rs
+++ b/tests/supported_runtimes_test.rs
@@ -27,6 +27,71 @@ echo "this won't do anything..."
         .failure();
 }
 
+#[cfg(windows)]
+#[test]
+fn powershell() {
+    let (_temp, maskfile_path) = common::maskfile(
+        r#"
+## powershell
+~~~powershell
+param (
+    $name = $env:name
+)
+
+Write-Output "Hello, $name!"
+~~~
+"#,
+    );
+
+    common::run_mask(&maskfile_path)
+        .command("powershell")
+        .env("name", "World")
+        .assert()
+        .stdout(contains("Hello, World!"))
+        .success();
+}
+
+#[cfg(windows)]
+#[test]
+fn batch() {
+    let (_temp, maskfile_path) = common::maskfile(
+        r#"
+## batch
+~~~batch
+echo "Hello, %name%!"
+~~~
+"#,
+    );
+
+    common::run_mask(&maskfile_path)
+        .command("batch")
+        .env("name", "World")
+        .assert()
+        .stdout(contains("Hello, World!"))
+        .success();
+}
+
+#[cfg(windows)]
+#[test]
+fn cmd() {
+    let (_temp, maskfile_path) = common::maskfile(
+        r#"
+## cmd
+~~~cmd
+echo "Hello, %name%!"
+~~~
+"#,
+    );
+
+    common::run_mask(&maskfile_path)
+        .command("cmd")
+        .env("name", "World")
+        .assert()
+        .stdout(contains("Hello, World!"))
+        .success();
+}
+
+#[cfg(not(windows))]
 #[test]
 fn sh() {
     let (_temp, maskfile_path) = common::maskfile(
@@ -46,6 +111,7 @@ echo Hello, $name!
         .success();
 }
 
+#[cfg(not(windows))]
 #[test]
 fn bash() {
     let (_temp, maskfile_path) = common::maskfile(