Repository: casey/just
Pull request #2342 (2024-09-03 22:27:07)

Display recipe aliases inline with the recipe doc
This updates the `--list` subcommand to display aliases inline with the recipe doc.

Before:
```
Available recipes:
    choose               # Open an interactive chooser of available commands
    c                    # alias for `choose`
```

After:
```
Available recipes:
    choose               # Open an interactive chooser of available commands [aliases: c]
```

casey: I personally think this is an improvement, but I'd like to get some feedback on it first.

Paging some regulars:

@laniakea64 @neunenak @runeimp

What do you think?
marcaddeo: > I agree that displaying aliases inline would be nicer, and I like the `[aliases: alias1, alias2]` notation. But placing this in the recipe documentation is confusing. Maybe it would be better placed with the recipe name/signature, perhaps in a distinctive color?

I think that may make it look somewhat odd to have aliases next to the recipe signature. Putting the aliases there would mean increasing the width of the recipe signature, causing all recipe docs to have to have a lot of padding between them. I think that suggested notation works best for flags/short flags. Whereas there's no limitation on how short or long, or how many, your aliases are.

The way I see it is, an alias is not pertinent information to be prominently displayed in the listing. The list is still useful, even if you have to search slightly for your commands alias.

But maybe my thinking is wrong here :)

I had originally colored the `[alias: alias1, alias2]` portion of the recipe doc in the annotation color (purple) so it stood out a bit. I wasn't sure if that was a desirable trait, however, and changed it back to the default. Maybe changing the color to purple here, or a different color, would be a good middle ground so that they're more noticeable?
casey: I think changing the color would be nice, so that it doesn't look like it's part of the doc.
casey: > ```
> Available recipes:
>     choose               # [aliases: c] Open an interactive chooser of available commands
> ```

I think the downside is that it pushes the doc to the right, and the doc is probably more interesting to the user than the list of aliases.
runeimp: I love the concept of inlining aliases. I may have suggested it at one point but don't remember. In any case there is on glaring problem (that I hate) which is that a recipe could have infinite aliases. The current method is the only way that handles the situation consistently. But if we know (do we know?) that if a recipe has an alias at all that say a single alias is used 97% of the time and two aliases 2% of the time and three or more aliases only 1% of the time then I believe there is wiggle room to inline by default.

We could also do the inlining by default and have a `set inline-flags=false` available for those few that like to have 3 or more aliases all the time.
marcaddeo: > I think changing the color would be nice, so that it doesn't look like it's part of the doc.

Are there any opinions on what color it should be, or a scheme the project is adhering to?

> I love the concept of inlining aliases. I may have suggested it at one point but don't remember. In any case there is on glaring problem (that I hate) which is that a recipe could have infinite aliases. The current method is the only way that handles the situation consistently. But if we know (do we know?) that if a recipe has an alias at all that say a single alias is used 97% of the time and two aliases 2% of the time and three or more aliases only 1% of the time then I believe there is wiggle room to inline by default.

Is this something really to be concerned with, though? You also can have as long as a recipe dog as you want, spanning multiple lines. Wouldn't it be up to the end user to decide how many aliases is too many, like it's their decision on how long the recipe doc is?

> We could also do the inlining by default and have a `set inline-flags=false` available for those few that like to have 3 or more aliases all the time.

So with `set inline-flags=false`, the aliases would revert to the original behavior of being displayed on their own lines? Would this be better suited as a `--no-inline-aliases` flag on the `--list` subcommand, if we go that direction?
casey: One concern: Are there possibly cases where the alias is actually the descriptive, user-discoverable name, and it would hurt discoverability if it isn't present in the list output on its own line?
marcaddeo: I've updated the code so that the `[aliases: a, b, c]` notation is now colored Purple. I don't have a strong opinion on what the color should be, happy to change to something else.

Here's how it looks in my terminal:

![image](https://github.com/user-attachments/assets/d5e20800-f897-405e-966c-55352897cdb4)

For recipe's that have aliases but do not have a document, should the aliases display as `# [aliases: a, b,c ]` (as seen in the screenshot) or should the `# ` be omitted?

As an aside, the inspiration for this notation (aliases on the right of the command help text) came from `clap` (at least for me, can't speak for the other people who requested this) so that's what I was trying to emulate.

This is how clap handles many and long aliases for comparison:

```
± cargo run --features derive --features cargo --example 03_04_subcommands --
   Compiling clap v4.5.17 (/Users/marc/dev/clap)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
     Running `target/debug/examples/03_04_subcommands`
A simple to use, efficient, and full-featured Command Line Argument Parser

Usage: 03_04_subcommands <COMMAND>

Commands:
  add   Adds files to myapp [aliases: a, b, c, d, e, f, g, h, a-really-long-alias-goes-here-and-stuff, this-is-just-going-to-be-the-longest-alias-you-have-ever-seen-its-really-incredible-you-wont-believe-your-eyes]
  help  Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version
```

And with Just using these changes (other recipes removed for brevity):

```
± cargo run --bin just -- --list
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/just --list`
Available recipes:
    sloc                                          # count non-empty lines of code [aliases: a, a-really-long-alias-goes-here-and-stuff, b, c, d, e, f, g, h, this-is-just-going-to-be-the-longest-alias-you-have-ever-seen-its-really-incredible-you-wont-believe-your-eyes]
    test                                          # [aliases: t]
```

So this follows the pattern clap is using pretty closely, but obviously Just is not clap so it doesn't necessarily hold much weight on the decisions made here. But I Just wanted to show that this is an established practice in other tools.


> > One concern: Are there possibly cases where the alias is actually the descriptive, user-discoverable name,
> 
> #1725
> 
> > and it would hurt discoverability if it isn't present in the list output on its own line?
> 
> Trying this out on that justfile: yes, moving the aliases to the right of the documentation comment hurts discoverability.
> 
> If the aliases were inlined to the left of the documentation comment, it would actually **improve** discoverability compared to having the aliases on their own lines.

I'm not sure I'm understanding the concern here. I looked through #1725 and the referenced discussion, and it seems like aliases are being used as a workaround to what looks to be a bug in argument expansion when passing to dependencies?
runeimp: @marcaddeo

> > I love the concept of inlining aliases. I may have suggested it at one point but don't remember. In any case there is on glaring problem (that I hate) which is that a recipe could have infinite aliases. The current method is the only way that handles the situation consistently. But if we know (do we know?) that if a recipe has an alias at all that say a single alias is used 97% of the time and two aliases 2% of the time and three or more aliases only 1% of the time then I believe there is wiggle room to inline by default.
> 
> Is this something really to be concerned with, though? You also can have as long as a recipe doc as you want, spanning multiple lines. Wouldn't it be up to the end user to decide how many aliases is too many, like it's their decision on how long the recipe doc is?

Yes, knowing what percentage of the user base this would directly affect lets us know if we can make changes with abandon or how much consideration we should consider for potential disruption, if that information is available. It also can inform the context of reasonable defaults to avoid unnecessary configuration options which potentially adds complexity with no benefit to the end user. I generally prefer control. But I use Mac because I like the defaults of the OS. I could get more control with POSIX systems like GNU/Linux. But I'm not in love with most of the defaults.

> > We could also do the inlining by default and have a `set inline-flags=false` available for those few that like to have 3 or more aliases all the time.
> 
> So with `set inline-flags=false`, the aliases would revert to the original behavior of being displayed on their own lines? Would this be better suited as a `--no-inline-aliases` flag on the `--list` subcommand, if we go that direction?

Right, `inline-flags=false` defaults to the current setup. I believe the command line option should exist _along with_ the configuration setting. Cover all the bases. 🙂
marcaddeo: Just pushed a commit that adds a `--no-inline-aliases` command line flag to opt-out of the inline aliases.

I'm not sure if it makes sense for this to be a Justfile setting, however, as the documentation reads:

> Settings control interpretation and execution. Each setting may be specified at most once, anywhere in the justfile.

And this setting wouldn't control interpretation or execution, just the display of the `--list` subcommand.
runeimp: What is the big hangup over having a setting? My default recipe is `just --list` 99.99% of the time. I use Just specifically to document what command line tools and preferred options I have with any given project.

Also when I do run `just --list` bare in an existing project I don't want to have to remember the special option that gives me the output I want over 99% of the time in any project. Are settings difficult to create and maintain in Rust?
marcaddeo: > What is the big hangup over having a setting? My default recipe is `just --list` 99.99% of the time. I use Just specifically to document what command line tools and preferred options I have with any given project.
> 
> Also when I do run `just --list` bare in an existing project I don't want to have to remember the special option that gives me the output I want over 99% of the time in any project. Are settings difficult to create and maintain in Rust?

I was just going off of what the documentation states, so as not to conflict with the design decisions of Just.

However, Just options can also be set via environment variables. So you could add `export JUST_NO_INLINE_ALIASES=true` to your `.bashrc`/etc to opt-out of the new list formatting.

runeimp: > I was just going off of what the documentation states, so as not to conflict with the design decisions of Just.
> 
> However, Just options can also be set via environment variables. So you could add `export JUST_NO_INLINE_ALIASES=true` to your `.bashrc`/etc to opt-out of the new list formatting.

@marcaddeo Sorry, if I was a bit aggressive. It's just been a very rough few months for me and I was feeling a bit attacked with that prior statement. The environment variable is not a 100% solution for me. My problem is easily fixed by simply adding a setting I can set differently in every other project as I need instead of an all or nothing 100% of the time. I can do a hack like adding `--no-inline-aliases` to the default recipe. But that is just another hack I'd like to avoid. The _real_ solution is simply using a setting.

@casey could you weigh in? I don't want to go against your general plans for Just but this exception seems reasonable to me.
runeimp: > > What is the big hangup over having a setting?
> 
> Because this option doesn't decide how the justfile will behave. It only decides how `just` presents information.
> 
I disagree. I do consider the display of information a behavior of the tool.

> `just --list` is not only for justfile authors, it's for anyone who might run the justfile recipes. If there are options in how to present the recipes included in `just --list` output, it makes most sense to leave it to whoever is going to read the `just --list` output to decide what's clearer for them.
> 
> And the global environment variable suggestion seems much better anyway, as it lets you have your preference on this regardless of whether you authored the justfile.

@laniakea64 I appreciate that your considering this is an XY-problem. And this could be, but in the sense that this feature should not have been an option only to begin with. I think that it should have instead been a setting first with the option added in support of the possible transitive nature of the setting in any given situation.

The issue is that I will want it one way for over 90% of my projects. And I will 100% want it the other way for a small percentages of projects. A setting via an environment variable only allows for one way for all local projects. And some special consideration on how to manage that setting in the other projects to overcome the environment variable. The only way to handle that which is consistent with anything is a setting I can manage per project. I can set the environment variable if needed for the majority of projects and then consistently set the local setting in the Justfile for unambiguous handling in the other projects. Or I end up special case handling for every project I realize has to be handled differently because a simple setting is not desired.
runeimp: I doubt a universal rational for when to and when not to is achievable. More than one alias? That doesn't mean much if all the aliases are one or two letters. Any alias more than 8 characters? Lots of people will think that is too many or not enough of a limit. Only when the terminal has over 80 characters wide? Basically the same problem as max length of a single alias. What font is being used in the terminal? etc.

All of that and more is likely way too fiddly. I suspect the only way to make such a formula work at all would require several more settings. I think the binary `true|false` is sufficient.
runeimp: Good catch! I haven't played with documentation blocks yet so completely forgot about them in the discussion.
casey: I'm still quite conflicted about this. On the one hand, I think in the common case it looks better. On the other hand, when there are many aliases, it's not great. I don't like the idea of a setting, since this feels like a user preference thing, and not necessarily a justfile author thing, and the justfile author can include a default recipe with `just --list --no-inline-aliases` if they want a way to control list display on a per-justfile basis.

I also think I like it on the right, not a super strong preference, but it mirrors `clap`'s behavior with aliases and environment variables on the right after help text.

Does that all sound reasonable? 
runeimp: > I'm still quite conflicted about this. On the one hand, I think in the common case it looks better. On the other hand, when there are many aliases, it's not great. I don't like the idea of a setting, since this feels like a user preference thing, and not necessarily a justfile author thing, and the justfile author can include a default recipe with `just --list --no-inline-aliases` if they want a way to control list display on a per-justfile basis.
> 
> I also think I like it on the right, not a super strong preference, but it mirrors `clap`'s behavior with aliases and environment variables on the right after help text.
> 
> Does that all sound reasonable?

I agree my general use case can be easily fixed manually by just adjusting the recipe where needed. But a default setting in a tool documents the preference for the style of the `--list` option itself. Whether it be in a recipe or command line. The environment variable solves for much of this. But it locks you into a single preference for all Justfiles and does not allow for the portability of those preferences.

As this is really a personal preference thing I think a setting makes sense. Having the aliases on the left of the description is a UX thing. Objectively I think we can agree that on the left makes logical sense in that, how you use the option, is effectively in one place at a glance. But people who don't care if aliases exist or not, because they have amazing memories or simply don't care about such things, won't care much if they are listed at all. Both options can be accomplished with a setting.

None of this is a deal breaker for me. I'm just voicing for my preference and the flexibility other users may also find very satisfying. As a front-end developer for most of my early dev career, I've always put a premium on UX. 😇 
marcaddeo: @casey all feedback should be addressed, let me know if you'd like to see any other changes. I also added a test for the new `--no-inline-aliases` flag.
casey: I think that since there are a bunch of different ways to display aliases, the best way is with an option which takes a value, instead of multiple flags. The option should be something like `--list-aliases`, with values like `left | right | off | separate`. This prevents creating multiple different flags that all control the same thing.

Is `--list-aliases` a good name? I'm not wedded to it. `--alias-style` is another option. 

I'm also not sure about the option names. `inline-left`, `inline-right`, `hidden`, are ideas for the option names.
marcaddeo: Alrighty, I switched it up to an `--alias-style` option. Since `--no-aliases` already exists, I didn't add an "omit" or "none" option to `AliasStyle`.
marcaddeo: Hey @casey, is there anything else you'd like changed in this PR? Would love to get it merged soon. If we think the changes are stable, I can update the relevant documentation to match.
casey: @marcaddeo Yooo, sorry for not looking at this, I've been letting my notifications pile up. Can you fix the merge conflicts and I'll take another look?
marcaddeo: > @marcaddeo Yooo, sorry for not looking at this, I've been letting my notifications pile up. Can you fix the merge conflicts and I'll take another look?

😅 happens! PR has been rebased, let me know what you think.
casey: This looks good! Did some compulsive tweaking of the code to avoid the need to do the `left` / `right` stuff.

What do you think about changing the name of the values to `left`, `right`, and `separate`? I think `recipe` is a little unclear, and having `inline-left` but just `inline` is a little inconsistent.
marcaddeo: > This looks good! Did some compulsive tweaking of the code to avoid the need to do the `left` / `right` stuff.
> 
> What do you think about changing the name of the values to `left`, `right`, and `separate`? I think `recipe` is a little unclear, and having `inline-left` but just `inline` is a little inconsistent.

`left, `right`, and `separate` seems good to me, should be pretty clear what they do

Patch:
diff --git a/src/alias_style.rs b/src/alias_style.rs
new file mode 100644
index 00000000..2f380d7c
--- /dev/null
+++ b/src/alias_style.rs
@@ -0,0 +1,8 @@
+use super::*;
+
+#[derive(Debug, PartialEq, Clone, ValueEnum)]
+pub(crate) enum AliasStyle {
+  Left,
+  Right,
+  Separate,
+}
diff --git a/src/analyzer.rs b/src/analyzer.rs
index 1087bdee..e160d8bc 100644
--- a/src/analyzer.rs
+++ b/src/analyzer.rs
@@ -201,7 +201,7 @@ impl<'run, 'src> Analyzer<'run, 'src> {
             Rc::clone(next)
           }),
         }),
-      doc,
+      doc: doc.filter(|doc| !doc.is_empty()),
       groups: groups.into(),
       loaded: loaded.into(),
       modules: self.modules,
diff --git a/src/color.rs b/src/color.rs
index 7742597b..723dcf61 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -12,27 +12,16 @@ pub(crate) struct Color {
 }
 
 impl Color {
-  fn restyle(self, style: Style) -> Self {
-    Self { style, ..self }
-  }
-
-  fn redirect(self, stream: impl IsTerminal) -> Self {
-    Self {
-      is_terminal: stream.is_terminal(),
-      ..self
-    }
-  }
-
-  fn effective_style(&self) -> Style {
-    if self.active() {
-      self.style
-    } else {
-      Style::new()
+  pub(crate) fn active(&self) -> bool {
+    match self.use_color {
+      UseColor::Always => true,
+      UseColor::Never => false,
+      UseColor::Auto => self.is_terminal,
     }
   }
 
-  pub(crate) fn auto() -> Self {
-    Self::default()
+  pub(crate) fn alias(self) -> Self {
+    self.restyle(Style::new().fg(Purple))
   }
 
   pub(crate) fn always() -> Self {
@@ -42,25 +31,38 @@ impl Color {
     }
   }
 
-  pub(crate) fn never() -> Self {
-    Self {
-      use_color: UseColor::Never,
-      ..Self::default()
-    }
+  pub(crate) fn annotation(self) -> Self {
+    self.restyle(Style::new().fg(Purple))
   }
 
-  pub(crate) fn stderr(self) -> Self {
-    self.redirect(io::stderr())
+  pub(crate) fn auto() -> Self {
+    Self::default()
   }
 
-  pub(crate) fn stdout(self) -> Self {
-    self.redirect(io::stdout())
+  pub(crate) fn banner(self) -> Self {
+    self.restyle(Style::new().fg(Cyan).bold())
+  }
+
+  pub(crate) fn command(self, foreground: Option<ansi_term::Color>) -> Self {
+    self.restyle(Style {
+      foreground,
+      is_bold: true,
+      ..Style::default()
+    })
   }
 
   pub(crate) fn context(self) -> Self {
     self.restyle(Style::new().fg(Blue).bold())
   }
 
+  pub(crate) fn diff_added(self) -> Self {
+    self.restyle(Style::new().fg(Green))
+  }
+
+  pub(crate) fn diff_deleted(self) -> Self {
+    self.restyle(Style::new().fg(Red))
+  }
+
   pub(crate) fn doc(self) -> Self {
     self.restyle(Style::new().fg(Blue))
   }
@@ -69,6 +71,14 @@ impl Color {
     self.restyle(Style::new().fg(Cyan))
   }
 
+  fn effective_style(&self) -> Style {
+    if self.active() {
+      self.style
+    } else {
+      Style::new()
+    }
+  }
+
   pub(crate) fn error(self) -> Self {
     self.restyle(Style::new().fg(Red).bold())
   }
@@ -77,65 +87,59 @@ impl Color {
     self.restyle(Style::new().fg(Yellow).bold())
   }
 
-  pub(crate) fn warning(self) -> Self {
-    self.restyle(Style::new().fg(Yellow).bold())
+  pub(crate) fn message(self) -> Self {
+    self.restyle(Style::new().bold())
   }
 
-  pub(crate) fn banner(self) -> Self {
-    self.restyle(Style::new().fg(Cyan).bold())
+  pub(crate) fn never() -> Self {
+    Self {
+      use_color: UseColor::Never,
+      ..Self::default()
+    }
   }
 
-  pub(crate) fn command(self, foreground: Option<ansi_term::Color>) -> Self {
-    self.restyle(Style {
-      foreground,
-      is_bold: true,
-      ..Style::default()
-    })
+  pub(crate) fn paint<'a>(&self, text: &'a str) -> ANSIGenericString<'a, str> {
+    self.effective_style().paint(text)
   }
 
   pub(crate) fn parameter(self) -> Self {
     self.restyle(Style::new().fg(Cyan))
   }
 
-  pub(crate) fn message(self) -> Self {
-    self.restyle(Style::new().bold())
-  }
-
-  pub(crate) fn annotation(self) -> Self {
-    self.restyle(Style::new().fg(Purple))
-  }
-
-  pub(crate) fn string(self) -> Self {
-    self.restyle(Style::new().fg(Green))
+  pub(crate) fn prefix(&self) -> Prefix {
+    self.effective_style().prefix()
   }
 
-  pub(crate) fn diff_added(self) -> Self {
-    self.restyle(Style::new().fg(Green))
+  fn redirect(self, stream: impl IsTerminal) -> Self {
+    Self {
+      is_terminal: stream.is_terminal(),
+      ..self
+    }
   }
 
-  pub(crate) fn diff_deleted(self) -> Self {
-    self.restyle(Style::new().fg(Red))
+  fn restyle(self, style: Style) -> Self {
+    Self { style, ..self }
   }
 
-  pub(crate) fn active(&self) -> bool {
-    match self.use_color {
-      UseColor::Always => true,
-      UseColor::Never => false,
-      UseColor::Auto => self.is_terminal,
-    }
+  pub(crate) fn stderr(self) -> Self {
+    self.redirect(io::stderr())
   }
 
-  pub(crate) fn paint<'a>(&self, text: &'a str) -> ANSIGenericString<'a, str> {
-    self.effective_style().paint(text)
+  pub(crate) fn stdout(self) -> Self {
+    self.redirect(io::stdout())
   }
 
-  pub(crate) fn prefix(&self) -> Prefix {
-    self.effective_style().prefix()
+  pub(crate) fn string(self) -> Self {
+    self.restyle(Style::new().fg(Green))
   }
 
   pub(crate) fn suffix(&self) -> Suffix {
     self.effective_style().suffix()
   }
+
+  pub(crate) fn warning(self) -> Self {
+    self.restyle(Style::new().fg(Yellow).bold())
+  }
 }
 
 impl From<UseColor> for Color {
diff --git a/src/config.rs b/src/config.rs
index df7e91ef..575e73cd 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -12,6 +12,7 @@ use {
 
 #[derive(Debug, PartialEq)]
 pub(crate) struct Config {
+  pub(crate) alias_style: AliasStyle,
   pub(crate) allow_missing: bool,
   pub(crate) check: bool,
   pub(crate) color: Color,
@@ -86,6 +87,7 @@ mod cmd {
 }
 
 mod arg {
+  pub(crate) const ALIAS_STYLE: &str = "ALIAS_STYLE";
   pub(crate) const ALLOW_MISSING: &str = "ALLOW-MISSING";
   pub(crate) const ARGUMENTS: &str = "ARGUMENTS";
   pub(crate) const CHECK: &str = "CHECK";
@@ -145,6 +147,16 @@ impl Config {
           .usage(AnsiColor::Yellow.on_default() | Effects::BOLD)
           .valid(AnsiColor::Green.on_default()),
       )
+      .arg(
+        Arg::new(arg::ALIAS_STYLE)
+          .long("alias-style")
+          .env("JUST_ALIAS_STYLE")
+          .action(ArgAction::Set)
+          .value_parser(clap::value_parser!(AliasStyle))
+          .default_value("right")
+          .help("Set list command alias display style")
+          .conflicts_with(arg::NO_ALIASES),
+      )
       .arg(
         Arg::new(arg::CHECK)
           .long("check")
@@ -739,6 +751,10 @@ impl Config {
     let explain = matches.get_flag(arg::EXPLAIN);
 
     Ok(Self {
+      alias_style: matches
+        .get_one::<AliasStyle>(arg::ALIAS_STYLE)
+        .unwrap()
+        .clone(),
       allow_missing: matches.get_flag(arg::ALLOW_MISSING),
       check: matches.get_flag(arg::CHECK),
       color: (*matches.get_one::<UseColor>(arg::COLOR).unwrap()).into(),
diff --git a/src/lib.rs b/src/lib.rs
index 7ce669ea..30777e51 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -7,6 +7,7 @@
 pub(crate) use {
   crate::{
     alias::Alias,
+    alias_style::AliasStyle,
     analyzer::Analyzer,
     argument_parser::ArgumentParser,
     assignment::Assignment,
@@ -179,6 +180,7 @@ pub mod summary;
 pub mod request;
 
 mod alias;
+mod alias_style;
 mod analyzer;
 mod argument_parser;
 mod assignment;
diff --git a/src/parser.rs b/src/parser.rs
index 5d7821a3..2258c13b 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -952,7 +952,7 @@ impl<'run, 'src> Parser<'run, 'src> {
       attributes,
       body,
       dependencies,
-      doc,
+      doc: doc.filter(|doc| !doc.is_empty()),
       file_depth: self.file_depth,
       import_offsets: self.import_offsets.clone(),
       name,
diff --git a/src/subcommand.rs b/src/subcommand.rs
index 8d345788..fbb5cb0b 100644
--- a/src/subcommand.rs
+++ b/src/subcommand.rs
@@ -432,40 +432,64 @@ impl Subcommand {
   }
 
   fn list_module(config: &Config, module: &Justfile, depth: usize) {
-    fn format_doc(
+    fn print_doc_and_aliases(
       config: &Config,
       name: &str,
       doc: Option<&str>,
+      aliases: &[&str],
       max_signature_width: usize,
       signature_widths: &BTreeMap<&str, usize>,
     ) {
-      if let Some(doc) = doc {
-        if !doc.is_empty() && doc.lines().count() <= 1 {
-          let color = config.color.stdout();
-          print!(
-            "{:padding$}{} ",
-            "",
-            color.doc().paint("#"),
-            padding = max_signature_width.saturating_sub(signature_widths[name]) + 1,
-          );
+      let color = config.color.stdout();
 
-          let mut end = 0;
-          for backtick in backtick_re().find_iter(doc) {
-            let prefix = &doc[end..backtick.start()];
-            if !prefix.is_empty() {
-              print!("{}", color.doc().paint(prefix));
-            }
-            print!("{}", color.doc_backtick().paint(backtick.as_str()));
-            end = backtick.end();
-          }
+      let inline_aliases = config.alias_style != AliasStyle::Separate && !aliases.is_empty();
+
+      if inline_aliases || doc.is_some() {
+        print!(
+          "{:padding$}{}",
+          "",
+          color.doc().paint("#"),
+          padding = max_signature_width.saturating_sub(signature_widths[name]) + 1,
+        );
+      }
 
-          let suffix = &doc[end..];
-          if !suffix.is_empty() {
-            print!("{}", color.doc().paint(suffix));
+      let print_aliases = || {
+        print!(
+          " {}",
+          color.alias().paint(&format!(
+            "[alias{}: {}]",
+            if aliases.len() == 1 { "" } else { "es" },
+            aliases.join(", ")
+          ))
+        );
+      };
+
+      if inline_aliases && config.alias_style == AliasStyle::Left {
+        print_aliases();
+      }
+
+      if let Some(doc) = doc {
+        print!(" ");
+        let mut end = 0;
+        for backtick in backtick_re().find_iter(doc) {
+          let prefix = &doc[end..backtick.start()];
+          if !prefix.is_empty() {
+            print!("{}", color.doc().paint(prefix));
           }
+          print!("{}", color.doc_backtick().paint(backtick.as_str()));
+          end = backtick.end();
+        }
+
+        let suffix = &doc[end..];
+        if !suffix.is_empty() {
+          print!("{}", color.doc().paint(suffix));
         }
       }
 
+      if inline_aliases && config.alias_style == AliasStyle::Right {
+        print_aliases();
+      }
+
       println!();
     }
 
@@ -589,8 +613,14 @@ impl Subcommand {
 
       if let Some(recipes) = recipe_groups.get(&group) {
         for recipe in recipes {
+          let recipe_alias_entries = if config.alias_style == AliasStyle::Separate {
+            aliases.get(recipe.name())
+          } else {
+            None
+          };
+
           for (i, name) in iter::once(&recipe.name())
-            .chain(aliases.get(recipe.name()).unwrap_or(&Vec::new()))
+            .chain(recipe_alias_entries.unwrap_or(&Vec::new()))
             .enumerate()
           {
             let doc = if i == 0 {
@@ -616,10 +646,14 @@ impl Subcommand {
               RecipeSignature { name, recipe }.color_display(config.color.stdout())
             );
 
-            format_doc(
+            print_doc_and_aliases(
               config,
               name,
-              doc.as_deref(),
+              doc.filter(|doc| doc.lines().count() <= 1).as_deref(),
+              aliases
+                .get(recipe.name())
+                .map(Vec::as_slice)
+                .unwrap_or_default(),
               max_signature_width,
               &signature_widths,
             );
@@ -638,10 +672,11 @@ impl Subcommand {
             Self::list_module(config, submodule, depth + 1);
           } else {
             print!("{list_prefix}{} ...", submodule.name());
-            format_doc(
+            print_doc_and_aliases(
               config,
               submodule.name(),
               submodule.doc.as_deref(),
+              &[],
               max_signature_width,
               &signature_widths,
             );


Test patch:
diff --git a/tests/alias_style.rs b/tests/alias_style.rs
new file mode 100644
index 00000000..dddea0d8
--- /dev/null
+++ b/tests/alias_style.rs
@@ -0,0 +1,123 @@
+use super::*;
+
+#[test]
+fn default() {
+  Test::new()
+    .justfile(
+      "
+        alias f := foo
+
+        # comment
+        foo:
+
+        bar:
+      ",
+    )
+    .args(["--list"])
+    .stdout(
+      "
+        Available recipes:
+            bar
+            foo # comment [alias: f]
+      ",
+    )
+    .run();
+}
+
+#[test]
+fn multiple() {
+  Test::new()
+    .justfile(
+      "
+        alias a := foo
+        alias b := foo
+
+        # comment
+        foo:
+
+        bar:
+      ",
+    )
+    .args(["--list"])
+    .stdout(
+      "
+        Available recipes:
+            bar
+            foo # comment [aliases: a, b]
+      ",
+    )
+    .run();
+}
+
+#[test]
+fn right() {
+  Test::new()
+    .justfile(
+      "
+        alias f := foo
+
+        # comment
+        foo:
+
+        bar:
+      ",
+    )
+    .args(["--alias-style=right", "--list"])
+    .stdout(
+      "
+        Available recipes:
+            bar
+            foo # comment [alias: f]
+      ",
+    )
+    .run();
+}
+
+#[test]
+fn left() {
+  Test::new()
+    .justfile(
+      "
+        alias f := foo
+
+        # comment
+        foo:
+
+        bar:
+      ",
+    )
+    .args(["--alias-style=left", "--list"])
+    .stdout(
+      "
+        Available recipes:
+            bar
+            foo # [alias: f] comment
+      ",
+    )
+    .run();
+}
+
+#[test]
+fn separate() {
+  Test::new()
+    .justfile(
+      "
+        alias f := foo
+
+        # comment
+        foo:
+
+        bar:
+      ",
+    )
+    .args(["--alias-style=separate", "--list"])
+    .stdout(
+      "
+        Available recipes:
+            bar
+            foo # comment
+            f   # alias for `foo`
+      ",
+    )
+    .run();
+}
diff --git a/tests/lib.rs b/tests/lib.rs
index 3ee9c380..a86a3da7 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -36,6 +36,7 @@ fn default<T: Default>() -> T {
 #[macro_use]
 mod test;
 
+mod alias_style;
 mod allow_duplicate_recipes;
 mod allow_duplicate_variables;
 mod allow_missing;
diff --git a/tests/misc.rs b/tests/misc.rs
index ab16d5e2..79b609f3 100644
--- a/tests/misc.rs
+++ b/tests/misc.rs
@@ -11,9 +11,30 @@ test! {
   args: ("--list"),
   stdout: "
     Available recipes:
-        foo
-        f   # alias for `foo`
-  ",
+        foo # [alias: f]
+  ",
+}
+
+#[test]
+fn alias_listing_with_doc() {
+  Test::new()
+    .justfile(
+      "
+        # foo command
+        foo:
+          echo foo
+
+        alias f := foo
+      ",
+    )
+    .arg("--list")
+    .stdout(
+      "
+      Available recipes:
+          foo # foo command [alias: f]
+    ",
+    )
+    .run();
 }
 
 test! {
@@ -22,9 +43,7 @@ test! {
   args: ("--list"),
   stdout: "
     Available recipes:
-        foo
-        f   # alias for `foo`
-        fo  # alias for `foo`
+        foo # [aliases: f, fo]
   ",
 }
 
@@ -34,8 +53,7 @@ test! {
   args: ("--list"),
   stdout: "
     Available recipes:
-        foo PARAM='foo'
-        f PARAM='foo'   # alias for `foo`
+        foo PARAM='foo' # [alias: f]
   ",
 }
 
@@ -927,8 +945,7 @@ a:
   stdout:   r"
     Available recipes:
         a
-        b
-        c # alias for `b`
+        b # [alias: c]
   ",
 }
 
@@ -942,8 +959,7 @@ a:
   args:     ("--list", "--unsorted"),
   stdout:   r"
     Available recipes:
-        b
-        c # alias for `b`
+        b # [alias: c]
         a
   ",
 }