Skip to content

feature request - anonymous functions #986

Open

Description

@tungstengmd

in zsh, if you initialise a function with no name, it's treated as an anonymous function, more here
i think it'd be really cool to have that as a feature here

Activity

  1. McDutchie commented on May 21, 2026

    @McDutchie

    I think I found a reasonably easy way to add this to the existing code. Please test the patch below, and try to break it.

    Only the syntax function { commandlist; } is supported by this patch, so an anonymous function should have all the features of a ksh-style function. The zsh variant () { commandlist; } doesn't make as much sense for ksh because POSIX-style functions don't keep local state (except for positional parameters).

    • parse.c: Accept the anonymous function syntax; substitute "(anon)" for the missing function name.
    • xec.c: If the name of a function definition starts with (, assume it's anonymous; allocate a temporary function node on the AST stack, process the function definition as normal, but then immediately execute and unset it.
    Patch version 1
    diff --git a/src/cmd/ksh93/sh/parse.c b/src/cmd/ksh93/sh/parse.c
    index 1dc6d85cd..b3d414669 100644
    --- a/src/cmd/ksh93/sh/parse.c
    +++ b/src/cmd/ksh93/sh/parse.c
    @@ -814,6 +814,7 @@ static Shnode_t *funct(Lex_t *lexp)
     {
     	Shnode_t *t;
     	int flag;
    +	int anon = 0;
     	struct slnod *volatile slp=0;
     	Stk_t *volatile savstak=0;
     	struct functnod *volatile fp;
    @@ -835,7 +836,7 @@ static Shnode_t *funct(Lex_t *lexp)
     	t->funct.functargs = 0;
     	if(!(flag = (lexp->token==FUNCTSYM)))
     		t->funct.functtyp |= FPOSIX;
    -	else if(sh_lex(lexp))
    +	else if((anon = sh_lex(lexp)) && anon!=LBRACE)
     		sh_syntax(lexp,0);
     	if(!(iop=fcfile()))
     	{
    @@ -843,12 +844,12 @@ static Shnode_t *funct(Lex_t *lexp)
     		fcclose();
     		fcfopen(iop);
     	}
    -	t->funct.functnam= (char*)lexp->arg->argval;
    +	t->funct.functnam = anon ? "(anon)" : (char*)lexp->arg->argval;
     #if SHOPT_KIA
     	if(kia.file)
     		kia.current = kiaentity(lexp,t->funct.functnam,-1,'p',-1,-1,kia.script,'p',0,"");
     #endif /* SHOPT_KIA */
    -	if(flag)
    +	if(flag && !anon)
     		lexp->token = sh_lex(lexp);
     	if(t->funct.functtyp&FPOSIX)
     		skipnl(lexp,0);
    diff --git a/src/cmd/ksh93/sh/xec.c b/src/cmd/ksh93/sh/xec.c
    index f32c85123..599331662 100644
    --- a/src/cmd/ksh93/sh/xec.c
    +++ b/src/cmd/ksh93/sh/xec.c
    @@ -2293,6 +2293,7 @@ int sh_exec(const Shnode_t *t, int flags)
     			char *fname = ((struct functnod*)t)->functnam;
     			char *cp = strrchr(fname,'.');
     			Namval_t *npv=0,*mp;
    +			int anon = *fname == '(';
     #if SHOPT_NAMESPACE
     			/* Namespace definition: a modified function definition */
     			if(t->tre.tretyp==TNSPACE)
    @@ -2339,6 +2340,12 @@ int sh_exec(const Shnode_t *t, int flags)
     				break;
     			}
     #endif /* SHOPT_NAMESPACE */
    +			if(anon)
    +			{
    +				np = stkalloc(sh.stk,sizeof(Namval_t));
    +				memset(np,0,sizeof(Namval_t));
    +				np->nvname = fname;
    +			}
     			/* look for discipline functions */
     			error_info.line = t->funct.functline-sh.st.firstline;
     			if(cp || sh.prefix)
    @@ -2473,6 +2480,14 @@ int sh_exec(const Shnode_t *t, int flags)
     				nv_onattr(np,NV_FUNCTION);
     			if(type&FOPTGET)
     				nv_onattr(np,NV_OPTGET);
    +			if(anon)
    +			{
    +				char *argv[2];
    +				argv[0] = np->nvname;
    +				argv[1] = NULL;
    +				sh_funct(np,1,argv,NULL,sh_isstate(SH_ERREXIT));
    +				nv_unset(np,NV_RDONLY);
    +			}
     			break;
     		    }
     
  2. McDutchie commented on May 22, 2026

    @McDutchie

    Found a couple problems. This patch improves namespace handling in xec.c, and disallows anonymous functions within compound assignments (i.e., typeset -T definitions) where they make no sense.

    Patch version 2
    diff --git a/src/cmd/ksh93/sh/parse.c b/src/cmd/ksh93/sh/parse.c
    index 1dc6d85cd..fbdb36f7c 100644
    --- a/src/cmd/ksh93/sh/parse.c
    +++ b/src/cmd/ksh93/sh/parse.c
    @@ -814,6 +814,7 @@ static Shnode_t *funct(Lex_t *lexp)
     {
     	Shnode_t *t;
     	int flag;
    +	int anon = 0;
     	struct slnod *volatile slp=0;
     	Stk_t *volatile savstak=0;
     	struct functnod *volatile fp;
    @@ -835,7 +836,7 @@ static Shnode_t *funct(Lex_t *lexp)
     	t->funct.functargs = 0;
     	if(!(flag = (lexp->token==FUNCTSYM)))
     		t->funct.functtyp |= FPOSIX;
    -	else if(sh_lex(lexp))
    +	else if((anon = sh_lex(lexp)) && (anon!=LBRACE || lexp->assignlevel))
     		sh_syntax(lexp,0);
     	if(!(iop=fcfile()))
     	{
    @@ -843,12 +844,12 @@ static Shnode_t *funct(Lex_t *lexp)
     		fcclose();
     		fcfopen(iop);
     	}
    -	t->funct.functnam= (char*)lexp->arg->argval;
    +	t->funct.functnam = anon ? "(anon)" : (char*)lexp->arg->argval;
     #if SHOPT_KIA
     	if(kia.file)
     		kia.current = kiaentity(lexp,t->funct.functnam,-1,'p',-1,-1,kia.script,'p',0,"");
     #endif /* SHOPT_KIA */
    -	if(flag)
    +	if(flag && !anon)
     		lexp->token = sh_lex(lexp);
     	if(t->funct.functtyp&FPOSIX)
     		skipnl(lexp,0);
    diff --git a/src/cmd/ksh93/sh/xec.c b/src/cmd/ksh93/sh/xec.c
    index f32c85123..91223a2f4 100644
    --- a/src/cmd/ksh93/sh/xec.c
    +++ b/src/cmd/ksh93/sh/xec.c
    @@ -2293,6 +2293,7 @@ int sh_exec(const Shnode_t *t, int flags)
     			char *fname = ((struct functnod*)t)->functnam;
     			char *cp = strrchr(fname,'.');
     			Namval_t *npv=0,*mp;
    +			int anon = *fname == '(';
     #if SHOPT_NAMESPACE
     			/* Namespace definition: a modified function definition */
     			if(t->tre.tretyp==TNSPACE)
    @@ -2372,12 +2373,19 @@ int sh_exec(const Shnode_t *t, int flags)
     				if(funptr(mp)==b_true || funptr(mp)==b_false || funptr(mp)==b_break)
     					sh.dont_optimize_builtins = 1;
     			}
    +			if(anon)
    +			{
    +				/* Create temporary node for anonymous function */
    +				np = stkalloc(sh.stk,sizeof(Namval_t));
    +				memset(np,0,sizeof(Namval_t));
    +				np->nvname = fname;
    +			}
     #if SHOPT_NAMESPACE
    -			if(sh.namespace && !sh.prefix && *fname!='.')
    +			else if(sh.namespace && !sh.prefix && *fname!='.')
     				np = sh_fsearch(fname,NV_ADD|NV_NOSCOPE);
    -			if(!np)
     #endif /* SHOPT_NAMESPACE */
    -			np = nv_open(fname,sh_subfuntree(1),NV_NOARRAY|NV_VARNAME|NV_NOSCOPE);
    +			if(!np)
    +				np = nv_open(fname,sh_subfuntree(1),NV_NOARRAY|NV_VARNAME|NV_NOSCOPE);
     			if(npv)
     			{
     				if(!sh.mktype)
    @@ -2473,6 +2481,15 @@ int sh_exec(const Shnode_t *t, int flags)
     				nv_onattr(np,NV_FUNCTION);
     			if(type&FOPTGET)
     				nv_onattr(np,NV_OPTGET);
    +			if(anon)
    +			{
    +				char *argv[2];
    +				/* Execute and unset anonymous function */
    +				argv[0] = np->nvname;
    +				argv[1] = NULL;
    +				sh_funct(np,1,argv,NULL,sh_isstate(SH_ERREXIT));
    +				nv_unset(np,NV_RDONLY);
    +			}
     			break;
     		    }
     
  3. tungstengmd commented on May 22, 2026

    @tungstengmd
    Author

    i'll add the patch by the time i get to my computer hopefully!

  4. tungstengmd commented on May 22, 2026

    @tungstengmd
    Author

    this implementation seems not to include the ability for arguments to actually be passed to the anonymous functions, which zsh allows [and which is a feature i really like about it]
    coude you maybe patch that in too?

  5. tungstengmd commented on May 22, 2026

    @tungstengmd
    Author

    testing the example script from the zsh manual [with one change to make it ksh compatible]:

    variable=outside
    function {
      typeset variable=inside
      print "I am $variable with arguments $*"
    } this and that
    print "I am $variable"

    and it doesn't work, due to the inability to pass args :<

  6. McDutchie commented on May 23, 2026

    @McDutchie

    Yes, that's not implemented in the patch. I've tried to look into how to do it, and it's definitely non-trivial, because it requires changing ksh's whole idea of what a function definition is. So that is going to take me much longer to figure out, if ever.

    Plus, that syntax of adding arguments after the } seems really ugly to me. The zsh manual also flags up that it's inconsistent with established shell grammar: “Arguments to the function may be specified as words following the closing brace defining the function, hence if there are none no arguments (other than $0) are set. This is a difference from the way other functions are parsed: normal function definitions may be followed by certain keywords such as else or fi, which will be treated as arguments to anonymous functions, so that a newline or semicolon is needed to force keyword interpretation.”

    So, it would require modifying the shell grammar in a more fundamental way and I'm not sure I'm prepared to do that. It's a general rule that keywords after } don't require semicolons to separate them. For example, if true; then { { echo one; echo two; } } fi, on a single line, is valid grammar on all the POSIX shells. Introducing an exception to that carries a high risk of introducing regressions along with it.

    I also don't really see the point of arguments to anonymous functions in the first place, because anonymous functions are one-time functions, so any arguments can simply be moved into the function body, either by hardcoding them or by using the set command. For example, the following is exactly equivalent to your example above:

    variable=outside
    function {
      typeset variable=inside
      set -- this and that
      print "I am $variable with arguments $*"
    }
    print "I am $variable"

    Anonymous functions are good for executing a locally scoped block once, for example, if you want to do initialisation without unwanted global side effects. But if an anonymous function is executed multiple times with different arguments (in a loop for example), then it's better off being a normally named function anyway – it's a clearer coding style, and it's more efficient as you avoid redefining the function multiple times.

    So, I don't really see why the lack of arguments to anonymous functions is actually a problem.

  7. tungstengmd commented on May 23, 2026

    @tungstengmd
    Author

    ig it was because i saw someone using them in alias definitions and using $1 and such, but i suppose a neater thing to do would probably be putting it in a regular named function
    these functions do have a use to them outside of that occasionally, though i have more experience in using them in perl, c# and elixir; seeing as this shell was considered on par with perl at some point though, there may be a use for it if you code ksh93 like it was in fact perl?

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

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions