Repository: casey/just
Pull request #1333 (2022-09-07 07:54:11)

Add echo-comments setting
Add a new setting "ignore-recipe-comments". If set, this causes lines internal to a non-shebang recipe beginning with the character '#' (other than '#!' itself) to be treated as comments of the justfile itself. They will not be echoed to stderr when the recipe executes.

Can potentially close https://github.com/casey/just/issues/1274

neunenak: I'm not 100% sure if "ignore-recipe-comments" is the best possible name for what this setting does. It's more like, don't *echo* comments within a recipe, but that's a bit unwieldy to make for a setting name.
casey: What about `set echo-comments := false`?
casey: Wondering if maybe `set comment-regex := "#.*"` is potentially a better solution, since otherwise what is a comment is hard coded.
neunenak: > Wondering if maybe `set comment-regex := "#.*"` is potentially a better solution, since otherwise what is a comment is hard coded.

I thought about that, but I think that hard-coding the notion of a Justfile comment does make sense.

There's basically two types of recipes, shebang and linewise. A linewise recipe is lines of shell code, and `just` does some additional interpretation of those lines as it executes - printing them before execution, paying attention to the `@` sigil, etc. It makes sense that comments here should also be interpreted by `just`, and the most natural thing to do with comments interpreted by just within the body of a recipe is treat them exactly the same as comments outside the body of a recipe, i.e. ignore them because they are intended for the human reader of a justfile. And it makes sense that the syntax for a comment - namely, a line beginning with `#` - should be the same everywhere in a justfile that `just` directly interprets (ignoring for the time being the question of possibly having separate syntax for doc comments). So there's no need for a special command to specify what a comment looks like, it's just always `#`.

In a shebang recipe, the entire recipe block is treated as source code in some other language, and `just` doesn't try to interpret anything about the text. It's already the case that in a recipe like:

```
some-recipe:
    #!/usr/bin/node

    // Comments
    console.log("hello")
```
`just` doesn't try to interpret the `//` as a comment - it doesn't know or care that Javascript uses `//` for comments. This is exactly equivalent to:

some-recipe:
    #!/usr/bin/python

    # Comment
    print("hello")
```

where `just` doesn't know or care that Python happens to use the same `#`-convention for comments as `just` itself. This PR as written only applies to linewise recipes and not shebang ones (maybe it would make sense to clarify that in the definition of the setting).
neunenak: > What about `set echo-comments := false`?

The only issue there is that it means that the option would have to default to true in order to retain the current behavior in existing justfiles. All the other boolean options are default false. Is that a principled decision or just a coincidence?
casey: Sorry for letting this sit!

My concern is that someone might use `set shell` to change their linewise interpreter to something that doesn't use `#` as a comment, and then it would look wierd to have `#` comments in a language that didn't support it.

But thinking about it, it seems like having a simple setting that does the right thing 99.9% of the time is probably a good idea, and if someone complains that their justfile looks weird, we can cross that bridge when we come to it.

> Is that a principled decision or just a coincidence?

It's kind of a principled decision, since I think it's nice when settings are default off, but if we don't have a better name, I'm open to breaking it.

I think `echo-comments := false` is probably better than `ignore-recipe-comments := true`, because it's not entirely clear what ignoring a comment means. I.e. what would it do with a comment if it's not ignoring it?
neunenak: > It's kind of a principled decision, since I think it's nice when settings are default off, but if we don't have a better name, I'm open to breaking it.
> 
> I think `echo-comments := false` is probably better than `ignore-recipe-comments := true`, because it's not entirely clear what ignoring a comment means. I.e. what would it do with a comment if it's not ignoring it?

How about `suppress-comment-echo`, which can default to false?
neunenak: Btw @casey do you know why https://github.com/casey/just/actions/runs/3120617565/jobs/5061331667 is failing? It looks like this is because of clippy errors, but when I run stable clippy locally it reports nothing wrong.
casey: @neunenak Sounds good, let's go with `echo-comments`.

Try with rust 1.56, which is what CI uses: `rustup install 1.56 && cargo +1.56.0 clippy --all --all-targets`
casey: Nice, looks good to me!
casey: Whoops! I actually found something weird. Line continuations in comments cause the next line to be included in the comment, so this test fails:

```
#[test]
fn continuations() {
  Test::new()
    .justfile(
      "
      set echo-comments := false

      some_recipe:
        # Alternate shells still ignore comments \\
        echo something-useful
    ",
    )
    .stdout("something-useful\n")
    .stderr("echo something-useful\n")
    .run();
}
```

This is a bit odd, since normally the contents of a comment, including backslashes, are ignored. What do you think?

This also made me think of potentially better name for this setting `skip-comments`, since not only does it not echo them, it also doesn't invoke the shell with them. The setting could also default to false. Not sure though, since not echoing comments is the main effect, and maybe `echo-comments := false` is clearer than `skip-comments := true`.
casey: And sorry for letting this sit! Next time hit the request review button or add a comment letting me know it's ready to take a look at.
neunenak: > And sorry for letting this sit! Next time hit the request review button or add a comment letting me know it's ready to take a look at.

No worries. I'm actually not sure where in the Github UI the request review button is (I've been using Gitlab at work so I'm much more used to their UI these days). As I recall, Github had a notion of Draft vs Ready open PRs, but I'm not sure where a "ready to review" button would live, unless that's a project-specific thing.
neunenak: > Whoops! I actually found something weird. Line continuations in comments cause the next line to be included in the comment, so this test fails:
> 
> ```
> #[test]
> fn continuations() {
>   Test::new()
>     .justfile(
>       "
>       set echo-comments := false
> 
>       some_recipe:
>         # Alternate shells still ignore comments \\
>         echo something-useful
>     ",
>     )
>     .stdout("something-useful\n")
>     .stderr("echo something-useful\n")
>     .run();
> }
> ```
> 
> This is a bit odd, since normally the contents of a comment, including backslashes, are ignored. What do you think?
> 
> This also made me think of potentially better name for this setting `skip-comments`, since not only does it not echo them, it also doesn't invoke the shell with them. The setting could also default to false. Not sure though, since not echoing comments is the main effect, and maybe `echo-comments := false` is clearer than `skip-comments := true`.

Huh, this is weird, and not something I expected - probably has something to do with how the tokenizer works? I would definitely expect that test case to succeed, I'll try to fix it.
neunenak: Actually, thinking about this a bit, I think this is a bug (or at least unintuitive behavior) with how just comments work in general. Ignoring this change, on the current published version of just, a justfile:
```
some-recipe:
    # a comment \\
    echo "HELLO"
```

will print the following:
```
➤ just some-recipe
# a comment \echo "HELLO"
```

and not print "HELLO". 

This is definitely unintuitive, but it's also how just comments currently work, and it's the sort of thing that would definitely break existing justfiles if it changed suddenly. Deciding what to do about this deserves it's own issue (I'd suggest adding this to the list of things to fix in just edition 2). I don't think it needs to block this PR though, since echoing or not echoing what `just` treats as a comment is orthogonal to changing what `just` treats as a comment in the presence of line continuations.

casey: So I think the issue is that, in recipe bodies, just doesn't have comments at all. It doesn't do anything differently if a line starts with `#`.

This change makes just treat those lines as comments, so any aspect of how they behave can be changed, and it wouldn't be a breaking change, since it's opt-in with the setting. I think it's reasonable to have this setting just kind of turn on comments in general, i.e. doesn't echo them, and also considers them to be a single line, and ignores line continuations at the end of them.
neunenak: > So I think the issue is that, in recipe bodies, just doesn't have comments at all. It doesn't do anything differently if a line starts with `#`.
> 
> This change makes just treat those lines as comments, so any aspect of how they behave can be changed, and it wouldn't be a breaking change, since it's opt-in with the setting. I think it's reasonable to have this setting just kind of turn on comments in general, i.e. doesn't echo them, and also considers them to be a single line, and ignores line continuations at the end of them.

Makes sense. This actually turned out to be pretty easy to fix to make that test case pass - just add `if line.is_continuation() && !comment_line { ... }` to the appropriate spot in the loop in `run_linewise` in `recipe.rs`. 


Patch:
diff --git a/GRAMMAR.md b/GRAMMAR.md
index 6e5f818a..6bb4afc1 100644
--- a/GRAMMAR.md
+++ b/GRAMMAR.md
@@ -59,6 +59,7 @@ assignment    : NAME ':=' expression eol
 export        : 'export' assignment
 
 setting       : 'set' 'dotenv-load' boolean?
+              | 'set' 'ignore-comments' boolean?
               | 'set' 'export' boolean?
               | 'set' 'positional-arguments' boolean?
               | 'set' 'allow-duplicate-recipes' boolean?
diff --git a/README.md b/README.md
index 61ff9211..7da202f3 100644
--- a/README.md
+++ b/README.md
@@ -641,15 +641,16 @@ foo:
 
 #### Table of Settings
 
-| Name                      | Value              | Description                                                                                   |
-| ------------------------- | ------------------ | --------------------------------------------------------------------------------------------- |
-| `allow-duplicate-recipes` | boolean            | Allow recipes appearing later in a `justfile` to override earlier recipes with the same name. |
-| `dotenv-load`             | boolean            | Load a `.env` file, if present.                                                               |
-| `export`                  | boolean            | Export all variables as environment variables.                                                |
-| `positional-arguments`    | boolean            | Pass positional arguments.                                                                    |
-| `shell`                   | `[COMMAND, ARGS…]` | Set the command used to invoke recipes and evaluate backticks.                                |
-| `windows-shell`           | `[COMMAND, ARGS…]` | Set the command used to invoke recipes and evaluate backticks.                                |
-| `windows-powershell`      | boolean            | Use PowerShell on Windows as default shell. (Deprecated. Use `windows-shell` instead.         |
+| Name                      | Value              | Default | Description                                                                                   |
+| ------------------------- | ------------------ | --------|---------------------------------------------------------------------------------------------- |
+| `allow-duplicate-recipes` | boolean            | False   | Allow recipes appearing later in a `justfile` to override earlier recipes with the same name. |
+| `dotenv-load`             | boolean            | False   | Load a `.env` file, if present.                                                               |
+| `export`                  | boolean            | False   | Export all variables as environment variables.                                                |
+| `ignore-comments`         | boolean            | False   | Ignore recipe lines beginning with `#`.                                                       |
+| `positional-arguments`    | boolean            | False   | Pass positional arguments.                                                                    |
+| `shell`                   | `[COMMAND, ARGS…]` | -       | Set the command used to invoke recipes and evaluate backticks.                                |
+| `windows-powershell`      | boolean            | False   | Use PowerShell on Windows as default shell. (Deprecated. Use `windows-shell` instead.         |
+| `windows-shell`           | `[COMMAND, ARGS…]` | -       | Set the command used to invoke recipes and evaluate backticks.                                |
 
 Boolean settings can be written as:
 
diff --git a/src/analyzer.rs b/src/analyzer.rs
index 39e03a5e..bbd4434f 100644
--- a/src/analyzer.rs
+++ b/src/analyzer.rs
@@ -40,7 +40,7 @@ impl<'src> Analyzer<'src> {
       }
     }
 
-    let mut settings = Settings::new();
+    let mut settings = Settings::default();
 
     for (_, set) in self.sets {
       match set.value {
@@ -53,6 +53,9 @@ impl<'src> Analyzer<'src> {
         Setting::Export(export) => {
           settings.export = export;
         }
+        Setting::IgnoreComments(ignore_comments) => {
+          settings.ignore_comments = ignore_comments;
+        }
         Setting::PositionalArguments(positional_arguments) => {
           settings.positional_arguments = positional_arguments;
         }
diff --git a/src/justfile.rs b/src/justfile.rs
index 26f06ea2..ae90df8e 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -257,7 +257,15 @@ impl<'src> Justfile<'src> {
 
     let mut ran = BTreeSet::new();
     for (recipe, arguments) in grouped {
-      Self::run_recipe(&context, recipe, arguments, &dotenv, search, &mut ran)?;
+      Self::run_recipe(
+        &context,
+        recipe,
+        arguments,
+        &dotenv,
+        search,
+        &self.settings,
+        &mut ran,
+      )?;
     }
 
     Ok(())
@@ -281,6 +289,7 @@ impl<'src> Justfile<'src> {
     arguments: &[&str],
     dotenv: &BTreeMap<String, String>,
     search: &Search,
+    settings: &Settings,
     ran: &mut BTreeSet<Vec<String>>,
   ) -> RunResult<'src, ()> {
     let mut invocation = vec![recipe.name().to_owned()];
@@ -319,6 +328,7 @@ impl<'src> Justfile<'src> {
         &arguments.iter().map(String::as_ref).collect::<Vec<&str>>(),
         dotenv,
         search,
+        settings,
         ran,
       )?;
     }
@@ -341,6 +351,7 @@ impl<'src> Justfile<'src> {
           &evaluated.iter().map(String::as_ref).collect::<Vec<&str>>(),
           dotenv,
           search,
+          settings,
           &mut ran,
         )?;
       }
diff --git a/src/keyword.rs b/src/keyword.rs
index 2913e711..439a1540 100644
--- a/src/keyword.rs
+++ b/src/keyword.rs
@@ -10,6 +10,7 @@ pub(crate) enum Keyword {
   Export,
   False,
   If,
+  IgnoreComments,
   PositionalArguments,
   Set,
   Shell,
diff --git a/src/lib.rs b/src/lib.rs
index 4aa7ead7..41ab113e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,5 +1,6 @@
 #![deny(clippy::all, clippy::pedantic)]
 #![allow(
+  clippy::default_trait_access,
   clippy::doc_markdown,
   clippy::enum_glob_use,
   clippy::missing_errors_doc,
diff --git a/src/line.rs b/src/line.rs
index 6f26506d..f0bc6dc9 100644
--- a/src/line.rs
+++ b/src/line.rs
@@ -12,6 +12,13 @@ impl<'src> Line<'src> {
     self.fragments.is_empty()
   }
 
+  pub(crate) fn is_comment(&self) -> bool {
+    match self.fragments.first() {
+      Some(Fragment::Text { token }) => token.lexeme().starts_with('#'),
+      _ => false,
+    }
+  }
+
   pub(crate) fn is_continuation(&self) -> bool {
     match self.fragments.last() {
       Some(Fragment::Text { token }) => token.lexeme().ends_with('\\'),
diff --git a/src/node.rs b/src/node.rs
index 60eb0422..65c1c63c 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -228,7 +228,8 @@ impl<'src> Node<'src> for Set<'src> {
       | Setting::DotenvLoad(value)
       | Setting::Export(value)
       | Setting::PositionalArguments(value)
-      | Setting::WindowsPowerShell(value) => {
+      | Setting::WindowsPowerShell(value)
+      | Setting::IgnoreComments(value) => {
         set.push_mut(value.to_string());
       }
       Setting::Shell(Shell { command, arguments })
diff --git a/src/parser.rs b/src/parser.rs
index ac6324e5..4e76082c 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -755,36 +755,23 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
     let name = Name::from_identifier(self.presume(Identifier)?);
     let lexeme = name.lexeme();
 
-    if Keyword::AllowDuplicateRecipes == lexeme {
-      let value = self.parse_set_bool()?;
-      return Ok(Set {
-        value: Setting::AllowDuplicateRecipes(value),
-        name,
-      });
-    } else if Keyword::DotenvLoad == lexeme {
-      let value = self.parse_set_bool()?;
-      return Ok(Set {
-        value: Setting::DotenvLoad(value),
-        name,
-      });
-    } else if Keyword::Export == lexeme {
-      let value = self.parse_set_bool()?;
-      return Ok(Set {
-        value: Setting::Export(value),
-        name,
-      });
-    } else if Keyword::PositionalArguments == lexeme {
-      let value = self.parse_set_bool()?;
-      return Ok(Set {
-        value: Setting::PositionalArguments(value),
-        name,
-      });
-    } else if Keyword::WindowsPowershell == lexeme {
-      let value = self.parse_set_bool()?;
-      return Ok(Set {
-        value: Setting::WindowsPowerShell(value),
-        name,
-      });
+    let set_bool: Option<Setting> = match Keyword::from_lexeme(lexeme) {
+      Some(kw) => match kw {
+        Keyword::AllowDuplicateRecipes => {
+          Some(Setting::AllowDuplicateRecipes(self.parse_set_bool()?))
+        }
+        Keyword::DotenvLoad => Some(Setting::DotenvLoad(self.parse_set_bool()?)),
+        Keyword::IgnoreComments => Some(Setting::IgnoreComments(self.parse_set_bool()?)),
+        Keyword::Export => Some(Setting::Export(self.parse_set_bool()?)),
+        Keyword::PositionalArguments => Some(Setting::PositionalArguments(self.parse_set_bool()?)),
+        Keyword::WindowsPowershell => Some(Setting::WindowsPowerShell(self.parse_set_bool()?)),
+        _ => None,
+      },
+      None => None,
+    };
+
+    if let Some(value) = set_bool {
+      return Ok(Set { name, value });
     }
 
     self.expect(ColonEquals)?;
diff --git a/src/recipe.rs b/src/recipe.rs
index 63787a3f..80156f22 100644
--- a/src/recipe.rs
+++ b/src/recipe.rs
@@ -95,7 +95,7 @@ impl<'src, D> Recipe<'src, D> {
     }
   }
 
-  pub(crate) fn run_linewise<'run>(
+  fn run_linewise<'run>(
     &self,
     context: &RecipeContext<'src, 'run>,
     dotenv: &BTreeMap<String, String>,
@@ -114,6 +114,10 @@ impl<'src, D> Recipe<'src, D> {
       let mut continued = false;
       let quiet_command = lines.peek().map_or(false, |line| line.is_quiet());
       let infallible_command = lines.peek().map_or(false, |line| line.is_infallible());
+
+      let comment_line =
+        context.settings.ignore_comments && lines.peek().map_or(false, |line| line.is_comment());
+
       loop {
         if lines.peek().is_none() {
           break;
@@ -121,7 +125,7 @@ impl<'src, D> Recipe<'src, D> {
         let line = lines.next().unwrap();
         line_number += 1;
         evaluated += &evaluator.evaluate_line(line, continued)?;
-        if line.is_continuation() {
+        if line.is_continuation() && !comment_line {
           continued = true;
           evaluated.pop();
         } else {
@@ -138,6 +142,10 @@ impl<'src, D> Recipe<'src, D> {
         command = &command[1..];
       }
 
+      if comment_line {
+        continue;
+      }
+
       if command.is_empty() {
         continue;
       }
diff --git a/src/setting.rs b/src/setting.rs
index 6349cabd..a30abdc2 100644
--- a/src/setting.rs
+++ b/src/setting.rs
@@ -4,6 +4,7 @@ use super::*;
 pub(crate) enum Setting<'src> {
   AllowDuplicateRecipes(bool),
   DotenvLoad(bool),
+  IgnoreComments(bool),
   Export(bool),
   PositionalArguments(bool),
   Shell(Shell<'src>),
@@ -16,6 +17,7 @@ impl<'src> Display for Setting<'src> {
     match self {
       Setting::AllowDuplicateRecipes(value)
       | Setting::DotenvLoad(value)
+      | Setting::IgnoreComments(value)
       | Setting::Export(value)
       | Setting::PositionalArguments(value)
       | Setting::WindowsPowerShell(value) => write!(f, "{}", value),
diff --git a/src/settings.rs b/src/settings.rs
index c755a604..6e27305f 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -5,11 +5,12 @@ pub(crate) const DEFAULT_SHELL_ARGS: &[&str] = &["-cu"];
 pub(crate) const WINDOWS_POWERSHELL_SHELL: &str = "powershell.exe";
 pub(crate) const WINDOWS_POWERSHELL_ARGS: &[&str] = &["-NoLogo", "-Command"];
 
-#[derive(Debug, PartialEq, Serialize)]
+#[derive(Debug, PartialEq, Serialize, Default)]
 pub(crate) struct Settings<'src> {
   pub(crate) allow_duplicate_recipes: bool,
   pub(crate) dotenv_load: Option<bool>,
   pub(crate) export: bool,
+  pub(crate) ignore_comments: bool,
   pub(crate) positional_arguments: bool,
   pub(crate) shell: Option<Shell<'src>>,
   pub(crate) windows_powershell: bool,
@@ -17,18 +18,6 @@ pub(crate) struct Settings<'src> {
 }
 
 impl<'src> Settings<'src> {
-  pub(crate) fn new() -> Settings<'src> {
-    Settings {
-      allow_duplicate_recipes: false,
-      dotenv_load: None,
-      export: false,
-      positional_arguments: false,
-      shell: None,
-      windows_powershell: false,
-      windows_shell: None,
-    }
-  }
-
   pub(crate) fn shell_command(&self, config: &Config) -> Command {
     let (command, args) = self.shell(config);
 
@@ -82,7 +71,7 @@ mod tests {
 
   #[test]
   fn default_shell() {
-    let settings = Settings::new();
+    let settings = Settings::default();
 
     let config = Config {
       shell_command: false,
@@ -94,8 +83,10 @@ mod tests {
 
   #[test]
   fn default_shell_powershell() {
-    let mut settings = Settings::new();
-    settings.windows_powershell = true;
+    let settings = Settings {
+      windows_powershell: true,
+      ..Default::default()
+    };
 
     let config = Config {
       shell_command: false,
@@ -114,7 +105,7 @@ mod tests {
 
   #[test]
   fn overwrite_shell() {
-    let settings = Settings::new();
+    let settings = Settings::default();
 
     let config = Config {
       shell_command: true,
@@ -128,8 +119,10 @@ mod tests {
 
   #[test]
   fn overwrite_shell_powershell() {
-    let mut settings = Settings::new();
-    settings.windows_powershell = true;
+    let settings = Settings {
+      windows_powershell: true,
+      ..Default::default()
+    };
 
     let config = Config {
       shell_command: true,
@@ -143,20 +136,21 @@ mod tests {
 
   #[test]
   fn shell_cooked() {
-    let mut settings = Settings::new();
-
-    settings.shell = Some(Shell {
-      command: StringLiteral {
-        kind: StringKind::from_token_start("\"").unwrap(),
-        raw: "asdf.exe",
-        cooked: "asdf.exe".to_string(),
-      },
-      arguments: vec![StringLiteral {
-        kind: StringKind::from_token_start("\"").unwrap(),
-        raw: "-nope",
-        cooked: "-nope".to_string(),
-      }],
-    });
+    let settings = Settings {
+      shell: Some(Shell {
+        command: StringLiteral {
+          kind: StringKind::from_token_start("\"").unwrap(),
+          raw: "asdf.exe",
+          cooked: "asdf.exe".to_string(),
+        },
+        arguments: vec![StringLiteral {
+          kind: StringKind::from_token_start("\"").unwrap(),
+          raw: "-nope",
+          cooked: "-nope".to_string(),
+        }],
+      }),
+      ..Default::default()
+    };
 
     let config = Config {
       shell_command: false,
@@ -168,8 +162,10 @@ mod tests {
 
   #[test]
   fn shell_present_but_not_shell_args() {
-    let mut settings = Settings::new();
-    settings.windows_powershell = true;
+    let settings = Settings {
+      windows_powershell: true,
+      ..Default::default()
+    };
 
     let config = Config {
       shell: Some("lol".to_string()),
@@ -181,8 +177,10 @@ mod tests {
 
   #[test]
   fn shell_args_present_but_not_shell() {
-    let mut settings = Settings::new();
-    settings.windows_powershell = true;
+    let settings = Settings {
+      windows_powershell: true,
+      ..Default::default()
+    };
 
     let config = Config {
       shell_command: false,


Test patch:
diff --git a/tests/ignore_comments.rs b/tests/ignore_comments.rs
new file mode 100644
index 00000000..1dc04b19
--- /dev/null
+++ b/tests/ignore_comments.rs
@@ -0,0 +1,85 @@
+use super::*;
+
+#[test]
+fn ignore_comments_in_recipe() {
+  Test::new()
+    .justfile(
+      "
+      set ignore-comments
+
+      some_recipe:
+        # A recipe-internal comment
+        echo something-useful
+    ",
+    )
+    .stdout("something-useful\n")
+    .stderr("echo something-useful\n")
+    .run();
+}
+
+#[test]
+fn dont_ignore_comments_in_recipe_by_default() {
+  Test::new()
+    .justfile(
+      "
+      some_recipe:
+        # A recipe-internal comment
+        echo something-useful
+    ",
+    )
+    .stdout("something-useful\n")
+    .stderr("# A recipe-internal comment\necho something-useful\n")
+    .run();
+}
+
+#[test]
+fn ignore_recipe_comments_with_shell_setting() {
+  Test::new()
+    .justfile(
+      "
+      set shell := ['echo', '-n']
+      set ignore-comments
+
+      some_recipe:
+        # Alternate shells still ignore comments
+        echo something-useful
+    ",
+    )
+    .stdout("something-useful\n")
+    .stderr("echo something-useful\n")
+    .run();
+}
+
+#[test]
+fn continuations_iwth_echo_comments_false() {
+  Test::new()
+    .justfile(
+      "
+      set ignore-comments
+
+      some_recipe:
+        # Comment lines ignore line continuations \\
+        echo something-useful
+    ",
+    )
+    .stdout("something-useful\n")
+    .stderr("echo something-useful\n")
+    .run();
+}
+
+#[test]
+fn continuations_with_echo_comments_true() {
+  Test::new()
+    .justfile(
+      "
+      set ignore-comments := false
+
+      some_recipe:
+        # comment lines can be continued \\
+        echo something-useful
+    ",
+    )
+    .stdout("")
+    .stderr("# comment lines can be continued echo something-useful\n")
+    .run();
+}
diff --git a/tests/json.rs b/tests/json.rs
index e312111d..8742b0b3 100644
--- a/tests/json.rs
+++ b/tests/json.rs
@@ -44,6 +44,7 @@ fn alias() {
         "export": false,
         "positional_arguments": false,
         "shell": null,
+        "ignore_comments": false,
         "windows_powershell": false,
         "windows_shell": null,
       },
@@ -71,6 +72,7 @@ fn assignment() {
         "allow_duplicate_recipes": false,
         "dotenv_load": null,
         "export": false,
+        "ignore_comments": false,
         "positional_arguments": false,
         "shell": null,
         "windows_powershell": false,
@@ -113,6 +115,7 @@ fn body() {
         "allow_duplicate_recipes": false,
         "dotenv_load": null,
         "export": false,
+        "ignore_comments": false,
         "positional_arguments": false,
         "shell": null,
         "windows_powershell": false,
@@ -165,6 +168,7 @@ fn dependencies() {
         "allow_duplicate_recipes": false,
         "dotenv_load": null,
         "export": false,
+        "ignore_comments": false,
         "positional_arguments": false,
         "shell": null,
         "windows_powershell": false,
@@ -254,6 +258,7 @@ fn dependency_argument() {
         "allow_duplicate_recipes": false,
         "dotenv_load": null,
         "export": false,
+        "ignore_comments": false,
         "positional_arguments": false,
         "shell": null,
         "windows_powershell": false,
@@ -307,6 +312,7 @@ fn duplicate_recipes() {
         "allow_duplicate_recipes": true,
         "dotenv_load": null,
         "export": false,
+        "ignore_comments": false,
         "positional_arguments": false,
         "shell": null,
         "windows_powershell": false,
@@ -342,6 +348,7 @@ fn doc_comment() {
         "allow_duplicate_recipes": false,
         "dotenv_load": null,
         "export": false,
+        "ignore_comments": false,
         "positional_arguments": false,
         "shell": null,
         "windows_powershell": false,
@@ -365,6 +372,7 @@ fn empty_justfile() {
         "allow_duplicate_recipes": false,
         "dotenv_load": null,
         "export": false,
+        "ignore_comments": false,
         "positional_arguments": false,
         "shell": null,
         "windows_powershell": false,
@@ -497,6 +505,7 @@ fn parameters() {
         "allow_duplicate_recipes": false,
         "dotenv_load": null,
         "export": false,
+        "ignore_comments": false,
         "positional_arguments": false,
         "shell": null,
         "windows_powershell": false,
@@ -568,6 +577,7 @@ fn priors() {
         "allow_duplicate_recipes": false,
         "dotenv_load": null,
         "export": false,
+        "ignore_comments": false,
         "positional_arguments": false,
         "shell": null,
         "windows_powershell": false,
@@ -603,6 +613,7 @@ fn private() {
         "allow_duplicate_recipes": false,
         "dotenv_load": null,
         "export": false,
+        "ignore_comments": false,
         "positional_arguments": false,
         "shell": null,
         "windows_powershell": false,
@@ -638,6 +649,7 @@ fn quiet() {
         "allow_duplicate_recipes": false,
         "dotenv_load": null,
         "export": false,
+        "ignore_comments": false,
         "positional_arguments": false,
         "shell": null,
         "windows_powershell": false,
@@ -665,6 +677,7 @@ fn settings() {
       set dotenv-load
       set export
       set positional-arguments
+      set ignore-comments
       set shell := ['a', 'b', 'c']
 
       foo:
@@ -691,6 +704,7 @@ fn settings() {
         "allow_duplicate_recipes": false,
         "dotenv_load": true,
         "export": true,
+        "ignore_comments": true,
         "positional_arguments": true,
         "shell": {
           "arguments": ["b", "c"],
@@ -732,6 +746,7 @@ fn shebang() {
         "allow_duplicate_recipes": false,
         "dotenv_load": null,
         "export": false,
+        "ignore_comments": false,
         "positional_arguments": false,
         "shell": null,
         "windows_powershell": false,
@@ -769,6 +784,7 @@ fn simple() {
         "export": false,
         "positional_arguments": false,
         "shell": null,
+        "ignore_comments": false,
         "windows_powershell": false,
         "windows_shell": null,
       },
diff --git a/tests/lib.rs b/tests/lib.rs
index 9867e3da..3e6f9d0a 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -50,6 +50,7 @@ mod export;
 mod fall_back_to_parent;
 mod fmt;
 mod functions;
+mod ignore_comments;
 mod init;
 #[cfg(unix)]
 mod interrupts;