Skip to content

User-defined functions #1059

Closed
Closed

Description

@rileyshahar

Awesome tool, I'm mostly very happy with it, and this is a relatively minor QOL issue issue with two workarounds I mention below.

My use-case is something like:

build_dir := "build"
file_name := "out"

default arg="": (html arg) (pdf arg)

html arg="":
    pandoc input.md -o "{{build_dir}}/{{arg}}/{{file_name}}.html"

pdf arg="":
    pandoc input.md -o "{{build_dir}}/{{arg}}/{{file_name}}.pdf"

I would like a way to deduplicate the specific string formatting {{build_dir}}/{{arg}}/{{file_name}} so that my uses of it don't become out of sync. My current solution is to use arg as a just variable instead of an argument, i.e. arg := "" and file_path := build_dir + "/" + arg + "/" + file_name at the top of the Justfile. This is imperfect because it requires passing the argument to the CLI as just pdf arg=foo instead of just pdf foo, and also it puts into global scope something that shouldn't necessarily be there.

Another workaround is something like this:

[...]

default arg="": (_default (build_dir + "/" + arg + "/" + file_name))
_default arg: (html arg) (pdf arg)

html arg="":
    pandoc input.md -o "{{arg}}.html"

pdf arg="":
    pandoc input.md -o "{{arg}}.pdf"

But this is inelegant and doesn't allow the html and pdf targets to be run directly.

Is there a better way to accomplish this currently? If not, is it possible to add some syntax for declaring functions on strings? I'm imagining something like (this is obviously just a first draft at a syntax):

buildpath(s) := build_dir + "/" + s + "/" + file_name

pdf arg="":
    pandoc input.md -o "{{buildpath(arg)}}.pdf"

I haven't looked super closely into the code base so I'm not sure how easy this would be. I'd be happy to look into writing a PR for this if it's desirable and feasible. Regardless, thanks for a great tool.

Activity

  1. casey commented on Jan 3, 2022

    @casey
    Owner

    This is definitely desirable!

    I like the syntax you proposed, i.e.:

    buildpath(s) := build_dir + "/" + s + "/" + file_name
    

    One thing that strikes me is that recipes are already similar to functions, so another option would be to allow calling recipes in expressions, which would return their standard output:

    build_dir := "build"
    file_name := "out"
    
    default arg="": (html arg) (pdf arg)
    
    path arg:
      echo {{build_dir}}/{{arg}}/{{file_name}}
    
    html arg="":
        pandoc input.md -o "{{(path arg)}}.html"
    
    pdf arg="":
        pandoc input.md -o "{{(path arg)}}.pdf"
    

    This might be easier, since a lot of the infrastructure (error messages, etc) is already there.

  2. rileyshahar commented on Jan 4, 2022

    @rileyshahar
    Author

    Cool! Here are some initial thoughts on steps for implementation:

    1. Add a new item, closure, to the grammar:
    item          : recipe
                     | [...]
                     | closure
                    
    closure     : NAME '(' sequence? ')' ':=' expression eol
    
    1. Implement an AST item closure, defined as
    pub struct Closure<'src> {
        parameter: Name<'src>, // something a little more complex is actually needed to model multivariate functions
        rule: Expression<'src>,
    }
    
    pub type NamedClosure<'src> = Binding<'src, Closure>

    You could reuse Assignment here, but that requires Closure to be added to the Expression enum, which I think is not desirable, since you don't want a closure to be a subterm of an expression.

    1. Parse closures, presumably by adding another case to the conditional here:

      just/src/parser.rs

      Lines 340 to 345 in ef3629f

      if self.next_are(&[Identifier, ColonEquals]) {
      items.push(Item::Assignment(self.parse_assignment(false)?));
      } else {
      let doc = pop_doc_comment(&mut items, eol_since_last_comment);
      items.push(Item::Recipe(self.parse_recipe(doc, false)?));
      }

    2. Evaluate closures to Functions via capturing the context. Maybe we only need a reference here or maybe we need to clone, I need to look more into it to know for sure - what do you think?

    3. Add a variant of Thunk which doesn't need the function until evaluation time.

    Obviously this is a pretty big change so I expect to run into other issues during implementation, but how do you feel about this general outline? I'm definitely open to other strategies.

    (Of course error handling, tests, etc. are also necessary, this just describes the happy path.)

  3. casey commented on Jan 5, 2022

    @casey
    Owner

    Obviously this is a pretty big change so I expect to run into other issues during implementation, but how do you feel about this general outline? I'm definitely open to other strategies.

    The outline sounds great!

    One general suggestion I'd make, since this is a big feature, is that anything that can reasonably be left for a follow-up PR should be. Better to get something bare-bones in than to make the PR harder to land. So tests and documentation should definitely be done as part of the PR, but if there are additional nice-to-have features that can be left out, they should be if they make the PR more complicated.

    As for tests, I have a lot of unit tests, but I don't think it's important to have full unit test coverage. I like to have full test coverage via integration tests, and then I mostly use unit tests when something is tricky to write or debug, in which case I write unit tests expressing the correct behavior. So don't worry about unit tests unless you find them useful while developing.

    If they're recursive, custom functions will probably push just into Turing-completeness, see #792.

  4. yuri-potatoq commented on Nov 5, 2023

    @yuri-potatoq

    Hi everybody, I'm really excited about working on this issue.
    I'm not a pro with parser stuff, but i'm studying this code to figure out how hard it can be.

    If there is someone who wants to pair with me, i would really appreciate it.
    I would really love to want these custom functions feature 🚀

  5. Splines commented on Jul 3, 2024

    @Splines

    I came here because I misunderstood this sentence:

    The [private] attribute may also be used to hide recipes or aliases without needing to change the name.
    [...] This is useful for helper recipes which are only meant to be used as dependencies of other recipes.

    I thought "dependency" meant that I could call another recipe and capture its output as described in this issue 😅
    But unfortunately that's not possible (yet).
    "Dependency" is meant in the sense of this phrase: "Dependencies run before recipes that depend on them".

  6. changed the title [-]Allow custom functions for string manipulation[/-] [+]User-defined functions[/+] on Jul 7, 2024
  7. changed the title [-]User-defined functions[/-] [+][feature] User-defined functions[/+] on Jul 7, 2024
  8. rougsig commented on Aug 2, 2024

    @rougsig

    As a workaround:

    // justfile.shell.zsh
    get_version () {
      echo $(cat $1/package.json | jq -r '.version')
    }
    
    eval $1
    // justfile
    set shell := ["zsh", "./justfile.shell.zsh"]
    
    sample:
      echo $(get_version app)
  9. changed the title [-][feature] User-defined functions[/-] [+]User-defined functions[/+] on Mar 23, 2026
  10. CaptaiNiveau commented on Mar 26, 2026

    @CaptaiNiveau

    This would be really nice to prevent logic duplication while keeping the code clean (no external shell invocations or similar workarounds). Is there already existing work on this?

  11. casey commented on Apr 5, 2026

    @casey
    Owner

    Added in #3247! They use the following syntax:

    foo(a, b) := a + b

    I made them unstable, so you'll need set unstable, --unstable, or JUST_UNSTABLE=1 to use them.

    The only reason I made them unstable is because I'm not totally sure about the syntax. The alternative that I'm considering is to use the fn keyword to introduce them:

    fn foo(a, b) := a + b

    I personally like the syntax without fn better, but the syntax with fn would allow for easily grepping function definitions with ^fn, which is kind of nice.

    I created issue #3248 to track stabilization. Please leave a comment about the syntax either way!

  12. CaptaiNiveau commented on Apr 7, 2026

    @CaptaiNiveau

    Awesome! I'll check them out tomorrow :D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions