Repository: casey/just
Pull request #413 (2019-04-18 02:18:15)

Deprecate `=` in assignments, aliases, and exports in favor of `:=`
This PR adds `:=` as a synonym for `=` in assignments, aliases and exports, and adds a deprecation warning `=` in those same contexts.

The deprecation warning looks like this:

```
warning: `=` in assignments, exports, and aliases is being phased out on favor of `:=`
Please see this issue for more details: https://github.com/casey/just/issues/379
```

Since this is a disruptive change, I'd like to go over the rationale, just in case anyone is curious.

Currently, `=` is used in assignments, aliases, exports, and default arguments:

```
alias b = build
foo = "bar"
export var = "val"

build module="rob":
  ./build --module {{module}} 
```

The just language doesn't have keywords in the conventional sense, so the parser uses lookahead to disambiguate conflicting constructs.

However, lookahead is limited and messy, and some conflicts cannot be resolved. So for example, these constructs fail to parse:

```
# the parser thinks that this is an export
export foo = "bar":
  echo {{foo}}

# the parser thinks that this is an alias:
bar = "baz"
alias foo = bar:
  echo {{foo}}
```

Also, there are additional constructs that I'd like to add that will produce further conflicts, like settings.

In order to fix this, there are a few options:

A. Use more lookahead to disambiguate expressions
B. Use a token other than `=` to introduce default arguments to recipes
C. Use a token other than `=` in assignments, exports and aliases

Using more lookahead, as in A, gets messy very quickly, especially as it's extended to longer and more complex constructs, which would limit extension of the grammar in the future, so I think ruling that out is reasonable.

B is reasonable, but I don't think that there is a good alternative to `=` for default argument values. `:` is already used to end recipe argument lists.

On the other hand, `:=` is a good alternative for assignments, exports, aliases, and a future `set setting := value` notation. `:=` is used in `make`, which `just` is most similar to, and is also used for assignment in Go and Algol. So, I think C is the best choice.

My current plan is to release this as `v0.4.3`, and follow up after some time has passed with a version, probably `v0.5.0` that removes the old meaning of `=`. I don't know what the most popular ways of getting just are, but I'd like to give v0.4.3 a chance to appear in package managers so that nobody is in the unfortunate situation of needing to run the same justfile on `< 0.4.3` and `>= 0.5.0`, and not having a construct available that will work on both.

I was thinking of supplying a `--fix` flag, as I mentioned in #379, but just's current parse-and-dump functionality re-orders constructs and strips comments, so using it to implement fixup functionality would be harder than I thought.


Patch:
diff --git a/GRAMMAR.md b/GRAMMAR.md
index 03efab3b..1e212958 100644
--- a/GRAMMAR.md
+++ b/GRAMMAR.md
@@ -49,23 +49,23 @@ item          : recipe
 eol           : NEWLINE
               | COMMENT NEWLINE
 
-assignment    : NAME '=' expression eol
+alias         : 'alias' NAME ':=' NAME
 
-alias         : 'alias' NAME '=' NAME
+assignment    : NAME ':=' expression eol
 
 export        : 'export' assignment
 
 expression    : value '+' expression
               | value
 
-value         : NAME '(' arguments? ')'
+value         : NAME '(' sequence? ')'
               | STRING
               | RAW_STRING
               | BACKTICK
               | NAME
               | '(' expression ')'
 
-arguments     : expression ',' arguments
+sequence      : expression ',' sequence
               | expression ','?
 
 recipe        : '@'? NAME parameter* ('+' parameter)? ':' dependencies? body?
diff --git a/README.adoc b/README.adoc
index 372d63b2..9ba698b9 100644
--- a/README.adoc
+++ b/README.adoc
@@ -210,7 +210,7 @@ build test deploy lint
 Aliases allow recipes to be invoked with alternative names:
 
 ```make
-alias b = build
+alias b := build
 
 build:
   echo 'Building!'
@@ -249,9 +249,9 @@ Available recipes:
 Variables, strings, concatenation, and substitution using `{{...}}` are supported:
 
 ```make
-version = "0.2.7"
-tardir  = "awesomesauce-" + version
-tarball = tardir + ".tar.gz"
+version := "0.2.7"
+tardir  := "awesomesauce-" + version
+tarball := tardir + ".tar.gz"
 
 publish:
     rm -f {{tarball}}
@@ -285,29 +285,29 @@ braces:
 Double-quoted strings support escape sequences:
 
 ```make
-string-with-tab             = "\t"
-string-with-newline         = "\n"
-string-with-carriage-return = "\r"
-string-with-double-quote    = "\""
-string-with-slash           = "\\"
+string-with-tab             := "\t"
+string-with-newline         := "\n"
+string-with-carriage-return := "\r"
+string-with-double-quote    := "\""
+string-with-slash           := "\\"
 ```
 
 ```sh
 $ just --evaluate
-"tring-with-carriage-return = "
-string-with-double-quote    = """
-string-with-newline         = "
+"tring-with-carriage-return := "
+string-with-double-quote    := """
+string-with-newline         := "
 "
-string-with-slash           = "\"
-string-with-tab             = "     "
+string-with-slash           := "\"
+string-with-tab             := "     "
 ```
 
 Single-quoted strings do not recognize escape sequences and may contain line breaks:
 
 ```make
-escapes = '\t\n\r\"\\'
+escapes := '\t\n\r\"\\'
 
-line-breaks = 'hello
+line-breaks := 'hello
 this
 is
   a
@@ -318,9 +318,9 @@ string!
 
 ```sh
 $ just --evaluate
-escapes = "\t\n\r\"\\"
+escapes := "\t\n\r\"\\"
 
-line-breaks = "hello
+line-breaks := "hello
 this
 is
   a
@@ -410,7 +410,7 @@ Starting server with database localhost:6379 on port 1337...
 Backticks can be used to store the result of commands:
 
 ```make
-localhost = `dumpinterfaces | cut -d: -f2 | sed 's/\/.*//' | sed 's/ //g'`
+localhost := `dumpinterfaces | cut -d: -f2 | sed 's/\/.*//' | sed 's/ //g'`
 
 serve:
     ./serve {{localhost}} 8080
@@ -421,7 +421,7 @@ serve:
 Variables can be overridden from the command line.
 
 ```make
-os = "linux"
+os := "linux"
 
 test: build
     ./test --test {{os}}
@@ -457,7 +457,7 @@ $ just --set os bsd
 Assignments prefixed with the `export` keyword will be exported to recipes as environment variables:
 
 ```make
-export RUST_BACKTRACE = "1"
+export RUST_BACKTRACE := "1"
 
 test:
     # will print a stack trace if it crashes
@@ -487,7 +487,7 @@ cd my-awesome-project && make
 Parameters may have default values:
 
 ```make
-default = 'all'
+default := 'all'
 
 test target tests=default:
     @echo 'Testing {{target}}:{{tests}}...'
@@ -513,7 +513,7 @@ Testing server:unit...
 Default values may be arbitrary expressions, but concatenations must be parenthesized:
 
 ```make
-arch = "wasm"
+arch := "wasm"
 
 test triple=(arch + "-unknown-unknown"):
   ./test {{triple}}
diff --git a/justfile b/justfile
index 27300064..82c9c2a9 100755
--- a/justfile
+++ b/justfile
@@ -2,9 +2,9 @@
 # ^ A shebang isn't required, but allows a justfile to be executed
 #   like a script, with `./justfile test`, for example.
 
-bt='0'
+bt = '0'
 
-export RUST_BACKTRACE=bt
+export RUST_BACKTRACE = bt
 
 alias t = test
 
diff --git a/src/alias.rs b/src/alias.rs
index ad4e8262..b1ae10e3 100644
--- a/src/alias.rs
+++ b/src/alias.rs
@@ -10,6 +10,6 @@ pub struct Alias<'a> {
 
 impl<'a> Display for Alias<'a> {
   fn fmt(&self, f: &mut Formatter) -> fmt::Result {
-    write!(f, "alias {} = {}", self.name, self.target)
+    write!(f, "alias {} := {}", self.name, self.target)
   }
 }
diff --git a/src/color.rs b/src/color.rs
index c9bb990f..196c483a 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -76,6 +76,10 @@ impl Color {
     self.restyle(Style::new().fg(Red).bold())
   }
 
+  pub fn warning(self) -> Color {
+    self.restyle(Style::new().fg(Yellow).bold())
+  }
+
   pub fn banner(self) -> Color {
     self.restyle(Style::new().fg(Cyan).bold())
   }
diff --git a/src/justfile.rs b/src/justfile.rs
index 8905e9c8..f86bde36 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -6,6 +6,7 @@ pub struct Justfile<'a> {
   pub assignments: BTreeMap<&'a str, Expression<'a>>,
   pub exports: BTreeSet<&'a str>,
   pub aliases: BTreeMap<&'a str, Alias<'a>>,
+  pub deprecated_equals: bool,
 }
 
 impl<'a> Justfile<'a> where {
@@ -80,7 +81,7 @@ impl<'a> Justfile<'a> where {
       }
 
       for (name, value) in scope {
-        println!("{0:1$} = \"{2}\"", name, width, value);
+        println!("{0:1$} := \"{2}\"", name, width, value);
       }
       return Ok(());
     }
@@ -175,7 +176,7 @@ impl<'a> Display for Justfile<'a> {
       if self.exports.contains(name) {
         write!(f, "export ")?;
       }
-      write!(f, "{} = {}", name, expression)?;
+      write!(f, "{} := {}", name, expression)?;
       items -= 1;
       if items != 0 {
         write!(f, "\n\n")?;
diff --git a/src/lexer.rs b/src/lexer.rs
index 63a11b00..2927a22f 100644
--- a/src/lexer.rs
+++ b/src/lexer.rs
@@ -323,7 +323,7 @@ impl<'a> Lexer<'a> {
       '@' => self.lex_single(At),
       '=' => self.lex_single(Equals),
       ',' => self.lex_single(Comma),
-      ':' => self.lex_single(Colon),
+      ':' => self.lex_colon(),
       '(' => self.lex_single(ParenL),
       ')' => self.lex_single(ParenR),
       '{' => self.lex_brace_l(),
@@ -442,6 +442,20 @@ impl<'a> Lexer<'a> {
     Ok(())
   }
 
+  /// Lex a token starting with ':'
+  fn lex_colon(&mut self) -> CompilationResult<'a, ()> {
+    self.advance()?;
+
+    if let Some('=') = self.next {
+      self.advance()?;
+      self.token(ColonEquals);
+    } else {
+      self.token(Colon);
+    }
+
+    Ok(())
+  }
+
   /// Lex a token starting with '{'
   fn lex_brace_l(&mut self) -> CompilationResult<'a, ()> {
     if !self.rest_starts_with("{{") {
diff --git a/src/parser.rs b/src/parser.rs
index 939b12cc..0b6e7b68 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -12,6 +12,7 @@ pub struct Parser<'a> {
   exports: BTreeSet<&'a str>,
   aliases: BTreeMap<&'a str, Alias<'a>>,
   alias_tokens: BTreeMap<&'a str, Token<'a>>,
+  deprecated_equals: bool,
 }
 
 impl<'a> Parser<'a> {
@@ -31,6 +32,7 @@ impl<'a> Parser<'a> {
       exports: empty(),
       aliases: empty(),
       alias_tokens: empty(),
+      deprecated_equals: false,
       text,
     }
   }
@@ -152,10 +154,10 @@ impl<'a> Parser<'a> {
     }
 
     if let Some(token) = self.expect(Colon) {
-      // if we haven't accepted any parameters, an equals
+      // if we haven't accepted any parameters, a :=
       // would have been fine as part of an assignment
       if parameters.is_empty() {
-        return Err(self.unexpected_token(&token, &[Name, Plus, Colon, Equals]));
+        return Err(self.unexpected_token(&token, &[Name, Plus, Colon, ColonEquals]));
       } else {
         return Err(self.unexpected_token(&token, &[Name, Plus, Colon]));
       }
@@ -420,6 +422,10 @@ impl<'a> Parser<'a> {
             if token.lexeme() == "export" {
               let next = self.tokens.next().unwrap();
               if next.kind == Name && self.accepted(Equals) {
+                self.deprecated_equals = true;
+                self.assignment(next, true)?;
+                doc = None;
+              } else if next.kind == Name && self.accepted(ColonEquals) {
                 self.assignment(next, true)?;
                 doc = None;
               } else {
@@ -430,6 +436,10 @@ impl<'a> Parser<'a> {
             } else if token.lexeme() == "alias" {
               let next = self.tokens.next().unwrap();
               if next.kind == Name && self.accepted(Equals) {
+                self.deprecated_equals = true;
+                self.alias(next)?;
+                doc = None;
+              } else if next.kind == Name && self.accepted(ColonEquals) {
                 self.alias(next)?;
                 doc = None;
               } else {
@@ -438,6 +448,10 @@ impl<'a> Parser<'a> {
                 doc = None;
               }
             } else if self.accepted(Equals) {
+              self.deprecated_equals = true;
+              self.assignment(token, false)?;
+              doc = None;
+            } else if self.accepted(ColonEquals) {
               self.assignment(token, false)?;
               doc = None;
             } else {
@@ -501,6 +515,7 @@ impl<'a> Parser<'a> {
       assignments: self.assignments,
       exports: self.exports,
       aliases: self.aliases,
+      deprecated_equals: self.deprecated_equals,
     })
   }
 }
@@ -604,10 +619,10 @@ foo a='b\t':
   summary_test! {
     parse_export,
     r#"
-export a = "hello"
+export a := "hello"
 
   "#,
-    r#"export a = "hello""#,
+    r#"export a := "hello""#,
   }
 
   summary_test! {
@@ -615,9 +630,9 @@ export a = "hello"
     r#"
 foo:
   echo a
-alias f = foo
+alias f := foo
 "#,
-r#"alias f = foo
+r#"alias f := foo
 
 foo:
     echo a"#
@@ -626,11 +641,11 @@ foo:
   summary_test! {
   parse_alias_before_target,
     r#"
-alias f = foo
+alias f := foo
 foo:
   echo a
 "#,
-r#"alias f = foo
+r#"alias f := foo
 
 foo:
     echo a"#
@@ -639,11 +654,11 @@ foo:
   summary_test! {
   parse_alias_with_comment,
     r#"
-alias f = foo #comment
+alias f := foo #comment
 foo:
   echo a
 "#,
-r#"alias f = foo
+r#"alias f := foo
 
 foo:
     echo a"#
@@ -655,9 +670,9 @@ foo:
 x:
 y:
 z:
-foo = \"xx\"
-bar = foo
-goodbye = \"y\"
+foo := \"xx\"
+bar := foo
+goodbye := \"y\"
 hello a b    c   : x y    z #hello
   #! blah
   #blarg
@@ -666,11 +681,11 @@ hello a b    c   : x y    z #hello
   2
   3
 ",
-    "bar = foo
+    "bar := foo
 
-foo = \"xx\"
+foo := \"xx\"
 
-goodbye = \"y\"
+goodbye := \"y\"
 
 hello a b c: x y z
     #! blah
@@ -690,14 +705,14 @@ z:"
   summary_test! {
   parse_shebang,
     "
-practicum = 'hello'
+practicum := 'hello'
 install:
 \t#!/bin/sh
 \tif [[ -f {{practicum}} ]]; then
 \t\treturn
 \tfi
 ",
-    "practicum = 'hello'
+    "practicum := 'hello'
 
 install:
     #!/bin/sh
@@ -714,27 +729,27 @@ install:
 
   summary_test! {
   parse_assignments,
-    r#"a = "0"
-c = a + b + a + b
-b = "1"
+    r#"a := "0"
+c := a + b + a + b
+b := "1"
 "#,
-    r#"a = "0"
+    r#"a := "0"
 
-b = "1"
+b := "1"
 
-c = a + b + a + b"#,
+c := a + b + a + b"#,
   }
 
   summary_test! {
   parse_assignment_backticks,
-    "a = `echo hello`
-c = a + b + a + b
-b = `echo goodbye`",
-    "a = `echo hello`
+    "a := `echo hello`
+c := a + b + a + b
+b := `echo goodbye`",
+    "a := `echo hello`
 
-b = `echo goodbye`
+b := `echo goodbye`
 
-c = a + b + a + b",
+c := a + b + a + b",
   }
 
   summary_test! {
@@ -753,14 +768,14 @@ c = a + b + a + b",
 
   summary_test! {
     string_quote_escape,
-    r#"a = "hello\"""#,
-    r#"a = "hello\"""#,
+    r#"a := "hello\"""#,
+    r#"a := "hello\"""#,
   }
 
   summary_test! {
     string_escapes,
-    r#"a = "\n\t\r\"\\""#,
-    r#"a = "\n\t\r\"\\""#,
+    r#"a := "\n\t\r\"\\""#,
+    r#"a := "\n\t\r\"\\""#,
   }
 
   summary_test! {
@@ -774,11 +789,11 @@ c = a + b + a + b",
   summary_test! {
   unary_functions,
     "
-x = arch()
+x := arch()
 
 a:
   {{os()}} {{os_family()}}",
-    "x = arch()
+    "x := arch()
 
 a:
     {{os()}} {{os_family()}}",
@@ -787,11 +802,11 @@ a:
   summary_test! {
   env_functions,
     r#"
-x = env_var('foo',)
+x := env_var('foo',)
 
 a:
   {{env_var_or_default('foo' + 'bar', 'baz',)}} {{env_var(env_var("baz"))}}"#,
-    r#"x = env_var('foo')
+    r#"x := env_var('foo')
 
 a:
     {{env_var_or_default('foo' + 'bar', 'baz')}} {{env_var(env_var("baz"))}}"#,
@@ -832,10 +847,10 @@ f x=(`echo hello` + "foo"):
   summary_test! {
     parameter_default_concatination_variable,
     r#"
-x = "10"
+x := "10"
 f y=(`echo hello` + x) +z="foo":
 "#,
-    r#"x = "10"
+    r#"x := "10"
 
 f y=(`echo hello` + x) +z="foo":"#,
   }
@@ -843,24 +858,24 @@ f y=(`echo hello` + x) +z="foo":"#,
   summary_test! {
     parameter_default_multiple,
     r#"
-x = "10"
+x := "10"
 f y=(`echo hello` + x) +z=("foo" + "bar"):
 "#,
-    r#"x = "10"
+    r#"x := "10"
 
 f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
   }
 
   summary_test! {
     concatination_in_group,
-    "x = ('0' + '1')",
-    "x = ('0' + '1')",
+    "x := ('0' + '1')",
+    "x := ('0' + '1')",
   }
 
   summary_test! {
     string_in_group,
-    "x = ('0'   )",
-    "x = ('0')",
+    "x := ('0'   )",
+    "x := ('0')",
   }
 
   #[rustfmt::skip]
diff --git a/src/run.rs b/src/run.rs
index 132e7720..14e899c2 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -321,6 +321,24 @@ pub fn run() {
     }
   });
 
+  if justfile.deprecated_equals {
+    let warning = color.warning().stderr();
+    let message = color.message().stderr();
+
+    eprintln!(
+      "{}",
+      warning.paint(
+        "warning: `=` in assignments, exports, and aliases is being phased out on favor of `:=`"
+      )
+    );
+
+    eprintln!(
+      "{}",
+      message
+        .paint("Please see this issue for more details: https://github.com/casey/just/issues/379")
+    );
+  }
+
   if matches.is_present("SUMMARY") {
     if justfile.count() == 0 {
       eprintln!("Justfile contains no recipes.");
diff --git a/src/token_kind.rs b/src/token_kind.rs
index 23cda3d6..08e4f226 100644
--- a/src/token_kind.rs
+++ b/src/token_kind.rs
@@ -5,6 +5,7 @@ pub enum TokenKind {
   At,
   Backtick,
   Colon,
+  ColonEquals,
   Comma,
   Comment,
   Dedent,
@@ -35,6 +36,7 @@ impl Display for TokenKind {
         At => "'@'",
         Backtick => "backtick",
         Colon => "':'",
+        ColonEquals => "':='",
         Comma => "','",
         Comment => "comment",
         Dedent => "dedent",


Test patch:
diff --git a/src/testing.rs b/src/testing.rs
index 03c5a464..773e2ee2 100644
--- a/src/testing.rs
+++ b/src/testing.rs
@@ -16,6 +16,7 @@ pub fn token_summary(tokens: &[Token]) -> String {
       At => "@",
       Backtick => "`",
       Colon => ":",
+      ColonEquals => ":=",
       Comma => ",",
       Comment => "#",
       Dedent => "<",
diff --git a/tests/integration.rs b/tests/integration.rs
index 33b8db35..8d152682 100644
--- a/tests/integration.rs
+++ b/tests/integration.rs
@@ -157,7 +157,7 @@ fn integration_test(
 
 integration_test! {
   name: alias_listing,
-  justfile: "foo:\n  echo foo\nalias f = foo",
+  justfile: "foo:\n  echo foo\nalias f := foo",
   args: ("--list"),
   stdin:  "",
   stdout: "Available recipes:
@@ -170,7 +170,7 @@ integration_test! {
 
 integration_test! {
   name: alias_listing_multiple_aliases,
-  justfile: "foo:\n  echo foo\nalias f = foo\nalias fo = foo",
+  justfile: "foo:\n  echo foo\nalias f := foo\nalias fo := foo",
   args: ("--list"),
   stdin:  "",
   stdout: "Available recipes:
@@ -184,7 +184,7 @@ integration_test! {
 
 integration_test! {
   name: alias_listing_parameters,
-  justfile: "foo PARAM='foo':\n  echo {{PARAM}}\nalias f = foo",
+  justfile: "foo PARAM='foo':\n  echo {{PARAM}}\nalias f := foo",
   args: ("--list"),
   stdin:  "",
   stdout: "Available recipes:
@@ -197,7 +197,7 @@ integration_test! {
 
 integration_test! {
   name: alias_listing_private,
-  justfile: "foo PARAM='foo':\n  echo {{PARAM}}\nalias _f = foo",
+  justfile: "foo PARAM='foo':\n  echo {{PARAM}}\nalias _f := foo",
   args: ("--list"),
   stdin:  "",
   stdout: "Available recipes:
@@ -209,7 +209,7 @@ integration_test! {
 
 integration_test! {
   name: alias,
-  justfile: "foo:\n  echo foo\nalias f = foo",
+  justfile: "foo:\n  echo foo\nalias f := foo",
   args: ("f"),
   stdin:  "",
   stdout: "foo\n",
@@ -219,7 +219,7 @@ integration_test! {
 
 integration_test! {
   name: alias_with_parameters,
-  justfile: "foo value='foo':\n  echo {{value}}\nalias f = foo",
+  justfile: "foo value='foo':\n  echo {{value}}\nalias f := foo",
   args: ("f", "bar"),
   stdin:  "",
   stdout: "bar\n",
@@ -229,7 +229,7 @@ integration_test! {
 
 integration_test! {
   name: alias_with_dependencies,
-  justfile: "foo:\n  echo foo\nbar: foo\nalias b = bar",
+  justfile: "foo:\n  echo foo\nbar: foo\nalias b := bar",
   args: ("b"),
   stdin:  "",
   stdout: "foo\n",
@@ -239,13 +239,13 @@ integration_test! {
 
 integration_test! {
   name: duplicate_alias,
-  justfile: "alias foo = bar\nalias foo = baz\n",
+  justfile: "alias foo := bar\nalias foo := baz\n",
   args: (),
   stdin:  "",
   stdout: "" ,
   stderr: "error: Alias `foo` first defined on line `1` is redefined on line `2`
   |
-2 | alias foo = baz
+2 | alias foo := baz
   |       ^^^
 ",
   status: EXIT_FAILURE,
@@ -253,13 +253,13 @@ integration_test! {
 
 integration_test! {
   name: unknown_alias_target,
-  justfile: "alias foo = bar\n",
+  justfile: "alias foo := bar\n",
   args: (),
   stdin:  "",
   stdout: "",
   stderr: "error: Alias `foo` has an unknown target `bar`
   |
-1 | alias foo = bar
+1 | alias foo := bar
   |       ^^^
 ",
   status: EXIT_FAILURE,
@@ -267,13 +267,13 @@ integration_test! {
 
 integration_test! {
   name: alias_shadows_recipe,
-  justfile: "bar:\n  echo bar\nalias foo = bar\nfoo:\n  echo foo",
+  justfile: "bar:\n  echo bar\nalias foo := bar\nfoo:\n  echo foo",
   args: (),
   stdin:  "",
   stdout: "",
   stderr: "error: Alias `foo` defined on `3` shadows recipe defined on `4`
   |
-3 | alias foo = bar
+3 | alias foo := bar
   |       ^^^
 ",
   status: EXIT_FAILURE,
@@ -387,8 +387,8 @@ c:
 
 integration_test! {
   name:     show,
-  justfile: r#"hello = "foo"
-bar = hello + hello
+  justfile: r#"hello := "foo"
+bar := hello + hello
 recipe:
  echo {{hello + "bar" + bar}}"#,
   args:     ("--show", "recipe"),
@@ -431,7 +431,7 @@ integration_test! {
 
 integration_test! {
   name:     backtick_success,
-  justfile: "a = `printf Hello,`\nbar:\n printf '{{a + `printf ' world.'`}}'",
+  justfile: "a := `printf Hello,`\nbar:\n printf '{{a + `printf ' world.'`}}'",
   args:     (),
   stdin:    "",
   stdout:   "Hello, world.",
@@ -441,7 +441,7 @@ integration_test! {
 
 integration_test! {
   name:     backtick_trimming,
-  justfile: "a = `echo Hello,`\nbar:\n echo '{{a + `echo ' world.'`}}'",
+  justfile: "a := `echo Hello,`\nbar:\n echo '{{a + `echo ' world.'`}}'",
   args:     (),
   stdin:    "",
   stdout:   "Hello, world.\n",
@@ -451,21 +451,21 @@ integration_test! {
 
 integration_test! {
   name:     backtick_code_assignment,
-  justfile: "b = a\na = `exit 100`\nbar:\n echo '{{`exit 200`}}'",
+  justfile: "b := a\na := `exit 100`\nbar:\n echo '{{`exit 200`}}'",
   args:     (),
   stdin:    "",
   stdout:   "",
   stderr:   "error: Backtick failed with exit code 100
   |
-2 | a = `exit 100`
-  |     ^^^^^^^^^^
+2 | a := `exit 100`
+  |      ^^^^^^^^^^
 ",
   status:   100,
 }
 
 integration_test! {
   name:     backtick_code_interpolation,
-  justfile: "b = a\na = `echo hello`\nbar:\n echo '{{`exit 200`}}'",
+  justfile: "b := a\na := `echo hello`\nbar:\n echo '{{`exit 200`}}'",
   args:     (),
   stdin:    "",
   stdout:   "",
@@ -477,7 +477,6 @@ integration_test! {
   status:   200,
 }
 
-// 😬鎌
 integration_test! {
   name:     backtick_code_interpolation_mod,
   justfile: "f:\n 無{{`exit 200`}}",
@@ -579,7 +578,7 @@ backtick-fail:
 
 integration_test! {
   name:     backtick_code_long,
-  justfile: "\n\n\n\n\n\nb = a\na = `echo hello`\nbar:\n echo '{{`exit 200`}}'",
+  justfile: "\n\n\n\n\n\nb := a\na := `echo hello`\nbar:\n echo '{{`exit 200`}}'",
   args:     (),
   stdin:    "",
   stdout:   "",
@@ -629,14 +628,14 @@ integration_test! {
   justfile: "foo:
  echo hello
  echo {{`exit 111`}}
-a = `exit 222`",
+a := `exit 222`",
   args:     (),
   stdin:    "",
   stdout:   "",
   stderr:   "error: Backtick failed with exit code 222
   |
-4 | a = `exit 222`
-  |     ^^^^^^^^^^
+4 | a := `exit 222`
+  |      ^^^^^^^^^^
 ",
   status:   222,
 }
@@ -646,7 +645,7 @@ integration_test! {
   justfile: "foo:
  echo hello
  echo {{`exit 111`}}
-a = `exit 222`",
+a := `exit 222`",
   args:     ("--set", "foo", "bar", "--set", "baz", "bob", "--set", "a", "b", "a", "b"),
   stdin:    "",
   stdout:   "",
@@ -660,7 +659,7 @@ integration_test! {
   justfile: "foo:
  echo hello
  echo {{`exit 111`}}
-a = `exit 222`",
+a := `exit 222`",
   args:     ("foo=bar", "baz=bob", "a=b", "a", "b"),
   stdin:    "",
   stdout:   "",
@@ -674,7 +673,7 @@ integration_test! {
   justfile: "foo:
  echo hello
  echo {{`exit 111`}}
-a = `exit 222`",
+a := `exit 222`",
   args:     ("foo=bar", "a=b", "a", "b"),
   stdin:    "",
   stdout:   "",
@@ -685,9 +684,9 @@ a = `exit 222`",
 integration_test! {
   name:     overrides_first,
   justfile: r#"
-foo = "foo"
-a = "a"
-baz = "baz"
+foo := "foo"
+a := "a"
+baz := "baz"
 
 recipe arg:
  echo arg={{arg}}
@@ -702,9 +701,9 @@ recipe arg:
 integration_test! {
   name:     overrides_not_evaluated,
   justfile: r#"
-foo = `exit 1`
-a = "a"
-baz = "baz"
+foo := `exit 1`
+a := "a"
+baz := "baz"
 
 recipe arg:
  echo arg={{arg}}
@@ -719,7 +718,7 @@ recipe arg:
 integration_test! {
   name:     dry_run,
   justfile: r#"
-var = `echo stderr 1>&2; echo backtick`
+var := `echo stderr 1>&2; echo backtick`
 
 command:
   @touch /this/is/not/a/file
@@ -748,20 +747,20 @@ echo `echo command interpolation`
 integration_test! {
   name:     evaluate,
   justfile: r#"
-foo = "a\t"
-hello = "c"
-bar = "b\t"
-ab = foo + bar + hello
+foo := "a\t"
+hello := "c"
+bar := "b\t"
+ab := foo + bar + hello
 
 wut:
   touch /this/is/not/a/file
 "#,
   args:     ("--evaluate"),
   stdin:    "",
-  stdout:   r#"ab    = "a	b	c"
-bar   = "b	"
-foo   = "a	"
-hello = "c"
+  stdout:   r#"ab    := "a	b	c"
+bar   := "b	"
+foo   := "a	"
+hello := "c"
 "#,
   stderr:   "",
   status:   EXIT_SUCCESS,
@@ -770,10 +769,10 @@ hello = "c"
 integration_test! {
   name:     export_success,
   justfile: r#"
-export FOO = "a"
-baz = "c"
-export BAR = "b"
-export ABC = FOO + BAR + baz
+export FOO := "a"
+baz := "c"
+export BAR := "b"
+export ABC := FOO + BAR + baz
 
 wut:
   echo $FOO $BAR $ABC
@@ -788,10 +787,10 @@ wut:
 integration_test! {
   name:     export_override,
   justfile: r#"
-export FOO = "a"
-baz = "c"
-export BAR = "b"
-export ABC = FOO + "-" + BAR + "-" + baz
+export FOO := "a"
+baz := "c"
+export BAR := "b"
+export ABC := FOO + "-" + BAR + "-" + baz
 
 wut:
   echo $FOO $BAR $ABC
@@ -806,10 +805,10 @@ wut:
 integration_test! {
   name:     export_shebang,
   justfile: r#"
-export FOO = "a"
-baz = "c"
-export BAR = "b"
-export ABC = FOO + BAR + baz
+export FOO := "a"
+baz := "c"
+export BAR := "b"
+export ABC := FOO + BAR + baz
 
 wut:
   #!/bin/sh
@@ -825,7 +824,7 @@ wut:
 integration_test! {
   name:     export_recipe_backtick,
   justfile: r#"
-export EXPORTED_VARIABLE = "A-IS-A"
+export EXPORTED_VARIABLE := "A-IS-A"
 
 recipe:
   echo {{`echo recipe $EXPORTED_VARIABLE`}}
@@ -840,7 +839,7 @@ recipe:
 integration_test! {
   name:     raw_string,
   justfile: r#"
-export EXPORTED_VARIABLE = '\z'
+export EXPORTED_VARIABLE := '\z'
 
 recipe:
   printf "$EXPORTED_VARIABLE"
@@ -931,7 +930,7 @@ default:
 integration_test! {
   name:     quiet_flag_no_assignment_backtick_stderr,
   justfile: r#"
-a = `echo hello 1>&2`
+a := `echo hello 1>&2`
 default:
   exit 100
 "#,
@@ -1056,39 +1055,39 @@ integration_test! {
 
 integration_test! {
   name:     color_always,
-  justfile: "b = a\na = `exit 100`\nbar:\n echo '{{`exit 200`}}'",
+  justfile: "b := a\na := `exit 100`\nbar:\n echo '{{`exit 200`}}'",
   args:     ("--color", "always"),
   stdin:    "",
   stdout:   "",
   stderr:   "\u{1b}[1;31merror:\u{1b}[0m \u{1b}[1mBacktick failed with exit code 100
-\u{1b}[0m  |\n2 | a = `exit 100`\n  |     \u{1b}[1;31m^^^^^^^^^^\u{1b}[0m\n",
+\u{1b}[0m  |\n2 | a := `exit 100`\n  |      \u{1b}[1;31m^^^^^^^^^^\u{1b}[0m\n",
   status:   100,
 }
 
 integration_test! {
   name:     color_never,
-  justfile: "b = a\na = `exit 100`\nbar:\n echo '{{`exit 200`}}'",
+  justfile: "b := a\na := `exit 100`\nbar:\n echo '{{`exit 200`}}'",
   args:     ("--color", "never"),
   stdin:    "",
   stdout:   "",
   stderr:   "error: Backtick failed with exit code 100
   |
-2 | a = `exit 100`
-  |     ^^^^^^^^^^
+2 | a := `exit 100`
+  |      ^^^^^^^^^^
 ",
   status:   100,
 }
 
 integration_test! {
   name:     color_auto,
-  justfile: "b = a\na = `exit 100`\nbar:\n echo '{{`exit 200`}}'",
+  justfile: "b := a\na := `exit 100`\nbar:\n echo '{{`exit 200`}}'",
   args:     ("--color", "auto"),
   stdin:    "",
   stdout:   "",
   stderr:   "error: Backtick failed with exit code 100
   |
-2 | a = `exit 100`
-  |     ^^^^^^^^^^
+2 | a := `exit 100`
+  |      ^^^^^^^^^^
 ",
   status:   100,
 }
@@ -1432,9 +1431,9 @@ foo:
 integration_test! {
   name:     test_os_arch_functions_in_expression,
   justfile: r#"
-a = arch()
-o = os()
-f = os_family()
+a := arch()
+o := os()
+f := os_family()
 
 foo:
   echo {{a}} {{o}} {{f}}
@@ -1450,9 +1449,9 @@ foo:
 integration_test! {
   name:     env_var_functions,
   justfile: r#"
-p = env_var('USER')
-b = env_var_or_default('ZADDY', 'HTAP')
-x = env_var_or_default('XYZ', 'ABC')
+p := env_var('USER')
+b := env_var_or_default('ZADDY', 'HTAP')
+x := env_var_or_default('XYZ', 'ABC')
 
 foo:
   /bin/echo '{{p}}' '{{b}}' '{{x}}'
@@ -1468,9 +1467,9 @@ foo:
 integration_test! {
   name:     env_var_functions,
   justfile: r#"
-p = env_var('USERNAME')
-b = env_var_or_default('ZADDY', 'HTAP')
-x = env_var_or_default('XYZ', 'ABC')
+p := env_var('USERNAME')
+b := env_var_or_default('ZADDY', 'HTAP')
+x := env_var_or_default('XYZ', 'ABC')
 
 foo:
   /bin/echo '{{p}}' '{{b}}' '{{x}}'
@@ -1573,7 +1572,7 @@ c: b a
 
 integration_test! {
   name:     parameter_shadows_variable,
-  justfile: "FOO = 'hello'\na FOO:",
+  justfile: "FOO := 'hello'\na FOO:",
   args:     ("a"),
   stdin:    "",
   stdout:   "",
@@ -1587,15 +1586,15 @@ integration_test! {
 
 integration_test! {
   name:     unknown_function_in_assignment,
-  justfile: r#"foo = foo() + "hello"
+  justfile: r#"foo := foo() + "hello"
 bar:"#,
   args:     ("bar"),
   stdin:    "",
   stdout:   "",
   stderr:   r#"error: Call to unknown function `foo`
   |
-1 | foo = foo() + "hello"
-  |       ^^^
+1 | foo := foo() + "hello"
+  |        ^^^
 "#,
   status:   EXIT_FAILURE,
 }
@@ -1659,13 +1658,13 @@ integration_test! {
 
 integration_test! {
   name:     duplicate_variable,
-  justfile: "a = 'hello'\na = 'hello'\nfoo:",
+  justfile: "a := 'hello'\na := 'hello'\nfoo:",
   args:     ("foo"),
   stdin:    "",
   stdout:   "",
   stderr:   "error: Variable `a` has multiple definitions
   |
-2 | a = 'hello'
+2 | a := 'hello'
   | ^
 ",
   status:   EXIT_FAILURE,
@@ -1691,7 +1690,7 @@ integration_test! {
   args:     ("foo"),
   stdin:    "",
   stdout:   "",
-  stderr:   "error: Expected name, '+', ':', or '=', but found raw string
+  stderr:   "error: Expected name, '+', ':', or ':=', but found raw string
   |
 1 | foo 'bar'
   |     ^^^^^
@@ -1729,13 +1728,13 @@ integration_test! {
 
 integration_test! {
   name:     variable_self_dependency,
-  justfile: "z = z\na:",
+  justfile: "z := z\na:",
   args:     ("a"),
   stdin:    "",
   stdout:   "",
   stderr:   "error: Variable `z` is defined in terms of itself
   |
-1 | z = z
+1 | z := z
   | ^
 ",
   status:   EXIT_FAILURE,
@@ -1743,13 +1742,13 @@ integration_test! {
 
 integration_test! {
   name:     variable_circular_dependency,
-  justfile: "x = y\ny = z\nz = x\na:",
+  justfile: "x := y\ny := z\nz := x\na:",
   args:     ("a"),
   stdin:    "",
   stdout:   "",
   stderr:   "error: Variable `x` depends on its own value: `x -> y -> z -> x`
   |
-1 | x = y
+1 | x := y
   | ^
 ",
   status:   EXIT_FAILURE,
@@ -1757,15 +1756,15 @@ integration_test! {
 
 integration_test! {
   name:     invalid_escape_sequence,
-  justfile: r#"x = "\q"
+  justfile: r#"x := "\q"
 a:"#,
   args:     ("a"),
   stdin:    "",
   stdout:   "",
   stderr:   "error: `\\q` is not a valid escape sequence
   |
-1 | x = \"\\q\"
-  |     ^^^^
+1 | x := \"\\q\"
+  |      ^^^^
 ",
   status:   EXIT_FAILURE,
 }
@@ -1773,7 +1772,7 @@ a:"#,
 integration_test! {
   name:     multiline_raw_string,
   justfile: "
-string = 'hello
+string := 'hello
 whatever'
 
 a:
@@ -1793,7 +1792,7 @@ whatever'
 integration_test! {
   name:     error_line_after_multiline_raw_string,
   justfile: "
-string = 'hello
+string := 'hello
 
 whatever' + 'yo'
 
@@ -1814,7 +1813,7 @@ a:
 integration_test! {
   name:     error_column_after_multiline_raw_string,
   justfile: "
-string = 'hello
+string := 'hello
 
 whatever' + bar
 
@@ -2039,7 +2038,7 @@ integration_test! {
    name:     comment_before_variable,
    justfile: "
 #
-A='1'
+A:='1'
 echo:
   echo {{A}}
  ",
@@ -2068,7 +2067,7 @@ integration_test! {
    name:     dotenv_variable_in_backtick,
    justfile: "
 #
-X=`echo $DOTENV_KEY`
+X:=`echo $DOTENV_KEY`
 echo:
   echo {{X}}
  ",
@@ -2097,8 +2096,8 @@ integration_test! {
    name:     dotenv_variable_in_function_in_backtick,
    justfile: "
 #
-X=env_var_or_default('DOTENV_KEY', 'foo')
-Y=env_var('DOTENV_KEY')
+X:=env_var_or_default('DOTENV_KEY', 'foo')
+Y:=env_var('DOTENV_KEY')
 echo:
   echo {{X}}
   echo {{Y}}
@@ -2113,15 +2112,15 @@ echo:
 integration_test! {
    name:     invalid_escape_sequence_message,
    justfile: r#"
-X = "\'"
+X := "\'"
 "#,
    args:     (),
    stdin:    "",
    stdout:   "",
    stderr:   r#"error: `\'` is not a valid escape sequence
   |
-2 | X = "\'"
-  |     ^^^^
+2 | X := "\'"
+  |      ^^^^
 "#,
    status:   EXIT_FAILURE,
 }
@@ -2200,7 +2199,7 @@ foo x=`echo foo`:
 integration_test! {
    name:     default_variable,
    justfile: "
-y = 'foo'
+y := 'foo'
 foo x=y:
   echo {{x}}
 ",
@@ -2292,7 +2291,7 @@ assembly_source_files = $(wildcard src/arch/$(arch)/*.s)
 integration_test! {
    name:     backtick_variable_cat,
    justfile: "
-stdin = `cat`
+stdin := `cat`
 
 default:
   echo {{stdin}}
@@ -2338,7 +2337,7 @@ default stdin = `cat justfile`:
 integration_test! {
    name:     backtick_variable_read_single,
    justfile: "
-password = `read PW && echo $PW`
+password := `read PW && echo $PW`
 
 default:
   echo {{password}}
@@ -2353,8 +2352,8 @@ default:
 integration_test! {
    name:     backtick_variable_read_multiple,
    justfile: "
-a = `read A && echo $A`
-b = `read B && echo $B`
+a := `read A && echo $A`
+b := `read B && echo $B`
 
 default:
   echo {{a}}
@@ -2381,3 +2380,63 @@ default a=`read A && echo $A` b=`read B && echo $B`:
    stderr:   "echo foo\necho bar\n",
    status:   EXIT_SUCCESS,
 }
+
+integration_test! {
+   name:     equals_deprecated_assignment,
+   justfile: "
+
+foo = 'bar'
+
+default:
+  echo {{foo}}
+
+",
+   args:     (),
+   stdin:    "",
+   stdout:   "bar\n",
+   stderr:   "warning: `=` in assignments, exports, and aliases is being phased out on favor of `:=`
+Please see this issue for more details: https://github.com/casey/just/issues/379
+echo bar
+",
+   status:   EXIT_SUCCESS,
+}
+
+integration_test! {
+   name:     equals_deprecated_export,
+   justfile: "
+
+export FOO = 'bar'
+
+default:
+  echo $FOO
+
+",
+   args:     (),
+   stdin:    "",
+   stdout:   "bar\n",
+   stderr:   "warning: `=` in assignments, exports, and aliases is being phased out on favor of `:=`
+Please see this issue for more details: https://github.com/casey/just/issues/379
+echo $FOO
+",
+   status:   EXIT_SUCCESS,
+}
+
+integration_test! {
+   name:     equals_deprecated_alias,
+   justfile: "
+
+alias foo = default
+
+default:
+  echo default
+
+",
+   args:     ("foo"),
+   stdin:    "",
+   stdout:   "default\n",
+   stderr:   "warning: `=` in assignments, exports, and aliases is being phased out on favor of `:=`
+Please see this issue for more details: https://github.com/casey/just/issues/379
+echo default
+",
+   status:   EXIT_SUCCESS,
+}