Shell – Why can’t I define a readonly variable named path in zsh

shellvariablezsh

In zsh, path is a special array variable, the contents of which is linked to the well-known PATH variable.

So special, in fact, that defining and calling the function

f() { local -r path=42 }

causes the error f: read-only variable: path. If the local variable is declared as mutable (i.e. without -r), everything works as expected. I haven't been able to reproduce this error with other variable names.

Why does this error occur and is it intentional? Do similar rules exist for other names?

I'm using zsh 5.2 (x86_64-apple-darwin16.0) on macOS 10.12.6.

Best Answer

TL;DR don't reuse "special builtin parameter" such as path because uh they're special. Or according to The Mailing List one can use the -h flag:

% () { local -hr path=42; echo $path }
42
% 

(However changing path to an integer might mess up subsequent code that forgets this override and assumes path is instead path...)

Longer digging around follows (but I totally missed the -h hide thing...)

% print ${(t)path}
array-special

This is a property (feature? bug?) of special variables, but not similar variables linked by the user:

% () { typeset -r PATH=/blah; echo $PATH }
(anon): read-only variable: PATH
% typeset -Tar FOO=bar foo
% print $foo
bar
% print ${(t)foo}
array-readonly-tag_local
% () { local -r foo=blah; echo $foo }
blah

There are various other parameters that exhibit this behavior:

% for p in $parameters[(I)*]; do print $p $parameters[$p]; done | grep array-
cdpath array-special
...
% () { local -r cdpath=42 }
(anon): read-only variable: cdpath

So some variables are like in Animal Farm more special than others. This error message comes from various places in Src/params.c which if that is modified to print which message is the specific message we on compiling that zsh find:

% () { local -r path }
% () { local -r path=foo }
(anon): read-only variable (setarrvalue): path

Is the rather generic code

/**/
mod_export void
setarrvalue(Value v, char **val)
{
    if (unset(EXECOPT))
        return;
    if (v->pm->node.flags & PM_READONLY) {
        zerr("read-only variable (setarrvalue): %s", v->pm->node.nam);
        freearray(val);
        return;
    }

This shows that the problem happens elsewhere; non-special variables doubtless do not have PM_READONLY set while the special variables that fail do. The next obvious place to look is the code for local which goes by a variety of names (typeset export ...). These are all builtins so can be found lurking in the depths of Src/builtin.c

% grep BUILTIN Src/builtin.c | grep local
    BUILTIN("local", BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN, (HandlerFunc)bin_typeset, 0, -1, 0, "AE:%F:%HL:%R:%TUZ:%ahi:%lp:%rtux", NULL),

These all call bin_typeset with various flags set so let's study the source for that function...swearing in the comments, check. Notes that things are complicated, check. Nothing really jumps out, though the rabbit hole (for when the "treat arguments as patterns" -m option is not set, which is the case here) appears to lead to the typeset_single function...

There is some code for POSIXBUILTINS related to readonly, but that's turned off in my test shells

% print $options[POSIXBUILTINS]
off

so I'm going to ignore that code (I hope. Could this be a shoggoth lair and no mere rabbit hole?). Meanwhile! Some debugging points to the PM_READONLY flag being toggled on for path by the following line

    /*
     * The remaining on/off flags should be harmless to use,
     * because we've checked for unpleasant surprises above.
     */
    pm->node.flags = (PM_TYPE(pm->node.flags) | on | PM_SPECIAL) & ~off;

Which in turn comes from the on variable which in turn is already on when the typeset_single function is entered, sigh, so back to bin_typeset we go... okay, basically there's a TYPESET_OPTSTR that somehow via some macros enables PM_READONLY by default; when instead a user-supplied variable runs through this code path the PM_READONLY gets turned off and all is well.

Whether this can be changed so that special variables such as path can be made readonly is a question for a ZSH developer (try the zsh-workers mailing list?) otherwise meanwhile don't mess around with the special variables.

Related Question