8

I just decided to try zsh (through oh-my-zsh), and am now playing with precmd to emulate a two-line prompt that has right prompts in more than just the last line.

So I clone the default theme, and inspired by this post (that I'm using to learn a lot too), i do somehting like this (I'll add colors later):

function precmd {
    local cwd="${(%):-[%~]}"
    local who_where="${(%):-%n@%m}"
    local git_info=${(%)$(git_prompt_info)}
    local right_prompt="     $git_info [$who_where]"
    local left_prompt="${(r:(($COLUMNS - ${#${right_prompt}})):: :)cwd}"

    echo "$left_prompt$right_prompt"
}

And it works. But I can't help but wonder: is zsh defining all those variables every time precmd is called?

I've been googling for closures, scope and namespacing in relation to zsh, looking to attach the local vars as data to precmd, so it doesn't need to redefine the variables every time, but I have found nothing. Is there some way to do what I'm trying, or should I just drop it?

As a side note, and only if it is related, what does "to have a function loaded" mean?

11

Zsh doesn't have anything like closures or packages or namespaces. Zsh lacks a bunch of things required to have true closures:

  • Functions aren't first class. You can't pass functions around as arguments to other functions, and functions can't return other functions. (You can pass the name of a function to call, but that's not the same as passing the function itself).

  • You can't have nested functions. All functions in zsh are global. You must prefix your function names to avoid conflicts. Note especially that functions will shadow external programs with the same name. If you have a function called ls, it will be called instead of the program ls. This can be useful, except if you do it by accident.

  • Variables are dynamically scoped, not statically scoped like in most modern languages. Even if you could have nested functions, inner functions wouldn't close over the local variables of outer functions in the way you would normally expect. You couldn't use them to make modules the way people do in, say, Javascript.

  • Zsh does have anonymous functions, but without any of these other things they're not useful for much.

So basically, the best you can do is to prefix all your functions and global variables.

I'll also point out that you should do define your precmd like this:

% autoload -Uz add-zsh-hook
% add-zsh-hook precmd my_precmd_function

add-zsh-hook lets you hook your function into precmd without it overwriting any other functions that might also want to hook precmd.

What it means to have a function loaded is a separate question. Zsh has an autoloading feature that only loads functions from disk when they're actually called. When you do autoload -Uz foobar, that makes the function named foobar available to call. When you actually call foobar, that loads the definition from disk.

7
  • 1
    This is what I was looking for. I can't up-vote yet, but kudos. Oct 8 '13 at 20:04
  • @Matt, extra credit for mentioning the add-zsh-hook.
    – SuperMagic
    Oct 8 '13 at 20:09
  • For point 1, you can pass the definition of a function as argument : myfunc $functions[otherfunction] which myfunc can call for instance as () {eval $1} args (not that I would recommend doing that that). Sep 24 '18 at 15:01
  • For point 3, you can have static scope using private variables (in the zsh/param/private module). Sep 24 '18 at 15:01
  • I had just this crazy idea and it seems not only to work with bash but also zsh: gist.github.com/pinkeen/287ad64d951f7bf138afce975d3bce7b
    – pinkeen
    Jun 6 '19 at 18:26
5

No, closures are too sophisticated for zsh. Zsh is designed to interpret small scripts that are not far removed from direct interaction. It doesn't have fancy language features that are very useful for programming in the large but less so for the kind of small tasks that shells are typically used for.

Note that if there was some form of closure that allowed the value of the variables to be pre-computed once and for all and then stored, the values would not be updated when something changes that causes the information to become invalid.

$git_info and the derived variables can change at any time due to a modification to a file checked into git or to the git repository. So they need to be recomputed each time anyway.

You could cache the values of cwd and who_where in a global variable, since they don't change under normal operation. cwd changes when the currrent directory changes so it would need to be updated from chpwd. However, these variables are extremely quick to compute, so there is no point in bothering. The expensive computation here is running git_prompt_info, and that can change at any time.

When you're displaying information between each command, it may be a better idea to put it as part of the prompt (PS1 or the psvar array). Zsh knows that it must redisplay the prompt in a variety of circumstances, whereas it knows nothing about what you print from precmd.

1
  • +1 for addressing the need to recompute the values each time.
    – chepner
    Oct 17 '13 at 16:49
1

Yes, those variables are (re)defined every time you call the function.

If you want to initialize them only once, you could simply move them to the top-level, out of the function.

4
  • Thanks for the answer. Though I am trying to find alternatives to using the global scope so I can battle it's pollution in the future if the need arises. Maybe there is some alternative to this? Oct 8 '13 at 17:14
  • 1
    Not really. You can use a prefix on your variables, say LRPROMPT_, to limit possible name collisions/pollution.... but it's still possible. Also, it's just your shell and prompt. Collisions can happen, but it's not that big a deal unless you're doing quite a bit more in the shell than is probably reasonable. I use oh-my-zsh AND have more than 2000 lines in custom zsh configuration (beyond omz) and collisions really aren't a problem. I do use prefixes, though.
    – SuperMagic
    Oct 8 '13 at 17:20
  • If your shell startup files are so big that name collisions are really a problem, you are almost certainly going to have other, more serious issues as well :)
    – Ben
    Oct 8 '13 at 17:30
  • @Ben, that number includes functions which have considerable white space and local variables. If I had 2000 global aliases and variables.... that would be a problem.
    – SuperMagic
    Oct 8 '13 at 17:54
0

In order to have closures, the language needs to be able to manipulate functions as elements or objects, which is not possible in zsh, except via strings and the eval builtin (like in the other shells). But that's very limited since you need to handle all the low-level stuff yourself (e.g. quoting). However, when the arguments do not have special characters (thus one does not need to handle quoting), it is easily possible to do some simple things using this idea, and in zsh, anonymous functions can help a bit. For instance, to define functions that compute a*x+b*y, where a and b are constants provided at the definition of the function, x and y being the arguments of the function:

mk_ax_plus_by() { echo "() { echo \$((($1)*(\$1)+($2)*(\$2))) }" }

fct_2x_plus_3y=$(mk_ax_plus_by 2 3)
fct_5x_plus_7y=$(mk_ax_plus_by 5 7)

So, one has a function fct_2x_plus_3y that computes 2*x+3*y and a function fct_5x_plus_7y that computes 5*x+7*y (note that I've chosen the function names just for readability, these can be any names, and you do not even need to store the contents in variables). Note also that these are actually strings (not functions in shell terminology), but they will behave like functions with the eval builtin. An example of use:

% eval $fct_2x_plus_3y 4 9
35
% eval $fct_5x_plus_7y 4 9
83

as 2*4+3*9 gives 35 and 5*4+7*9 gives 83.

Note that unlike in functional languages, one here needs to differentiate parameters that come from the environment (unquoted) and parameters to the defined function (quoted), i.e. that will be evaluated only at the function call. But it is possible to change the implementation to be more like functional languages, and the use will be the same:

mk_ax_plus_by()
{
  local x='$1' y='$2'
  echo "() { echo \$((($1)*($x)+($2)*($y))) }"
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.