13

I've found nothing about this in all my searching.

In the code below, Bar1 acts like I'd expect a "regular" variable to act (since help declare says that -g is only "seen" in function calls): it changes value depending on the scope (outer or inner).

Bar2, being "plainly" declared, also acts like I expect (being gfhjnfd in the outer scope, and blat in the inner scope.

But Foo is a globally unchanging variable. help declare only says "to make NAMEs readonly". Where is this extended (and unwanted) feature documented?

$ cat test.sh
#!/bin/bash

declare -r Foo=bar
declare -g Bar1=fdhtbn
declare Bar2=gfhjnfd

X()
{
    local Foo=$1
    local Bar1=snorkel
    local Bar2=$2
    echo Foo in X = $Foo
    echo Bar1 in X = $Bar1
    echo Bar2 in X = $Bar2
}

echo Foo, above X, = $Foo
echo Bar1, above X = $Bar1
echo Bar2, above X = $Bar2
echo
X baz blat
echo
echo Foo, below X, = $Foo
echo Bar1, below X = $Bar1
echo Bar2, below X = $Bar2

$ ./test.sh
Foo, above X, = bar
Bar1, above X = fdhtbn
Bar2, above X = gfhjnfd

./test.sh: line 9: local: Foo: readonly variable
Foo in X = bar
Bar1 in X = snorkel
Bar2 in X = blat

Foo, below X, = bar
Bar1, below X = fdhtbn
Bar2, below X = gfhjnfd
3
  • Are you expecting to use local Foo to remove the read-only-ness from the outer Foo variable? You can't do that.
    – Kusalananda
    Commented Jun 14 at 7:55
  • @Kusalananda yes, that's what I'm expecting, since I can't find anything that says "-r implies globalness". Where's that documented?
    – RonJohn
    Commented Jun 14 at 8:13
  • declare -g at the global scope doesn't do anything interesting. The -g option is for declaring a named inside a function as global, as declare foo inside a function implicitly makes the variable local, just as if you had used local.
    – chepner
    Commented Jun 17 at 17:48

3 Answers 3

13

Whether readonly in one scope will prevent a variable with the same name to be declared in child scopes (in those shells that have dynamic scoping such as bash) varies between shells¹.

That came up during Austin group discussions about specifying local scope for variables in POSIX sh (an effort now abandoned as getting a consensus (on that an many other aspects) prove impossible).

In bash, that's by design and the maintainer said they wouldn't budge on that (at least not in the default environment), as it's considered there a security measure.

readonly can be used for two not fully-compatible purposes:

  1. as a development tool to define global constants expected not to change, so you're warned when your code tries to and you can then fix your code (a bit like the const of C though here it happens at runtime).
  2. as a security measure in restricted shells (a very poor sandboxing security feature) where some special variables such as $PATH, $HISTFILE (though an administrator wanting to provide a restricted shell environment would likely want to set more variables read-only such as LD_PRELOAD, PERL5LIB...) would need to remain read-only to prevent users bypassing the restrictions.

In bash, modifications of $PATH (which has the only allowed commands) in restricted shells relies on the readonly attribute, so it's important those variables cannot be modified (including their list of attributes or scopes) even in child scopes.

In zsh, $PATH and a few other variables (including LD_PRELOAD, but not PERL5LIB, PYTHONPATH...) are restricted, but not via the read-only attribute, so readonly can be used reliably for the first usage mentioned above, but that means that if your restricted-environment has some commands written in perl or python, users may be able to bypass restrictions via the PERL5LIB/PYTHONPATH environment variables for instance and making those readonly won't help as the users can do typeset +r PERL5LIB to remove that readonly attribute or shadow it in a child scope with a non-readonly one.


¹ See List of shells that support `local` keyword for defining local variables for more details about local scope in various POSIX-like shells.

8

The underlying problem here is that the local keyword doesn't really create a local variable, or at least, not in the sense that you're expecting.

Per the Bash Reference Manual:

Variables local to the function may be declared with the local builtin (local variables). Ordinarily, variables and their values are shared between a function and its caller. These variables are visible only to the function and the commands it invokes. This is particularly important when a shell function calls other functions.

and

local can only be used within a function; it makes the variable name have a visible scope restricted to that function and its children.

(where boldface represents my emphasis).

In computer science terms, Bash uses dynamic scope instead of lexical scope; but from the standpoint of a typical programmer living in the year 2024, what this really means is that local doesn't create a local variable, it just temporarily modifies the global variable. Its special magic is that it automatically restores the variable's original value (and other properties) when the function returns, which is useful, but is not a full implementation of "local variables" in the sense that you expect.

So if local could override readonly, then something like this:

readonly three=3

print_three() {
  echo "$three"
}

print_four() {
  local three=4

  print_three
}

print_four

would print 4 instead of 3 — the variable three wouldn't really be readonly.


As Stéphane Chazelas points out in his answer, some shells do let local override readonly. But I would argue that this is a mistake in any shell where local has the same basic behavior as in Bash (namely, temporarily modifying a global variable instead of creating a separate local variable), and that behavior genuinely conflicts with the intention of readonly. I can only assume that their motivation was to have local variables be slightly more like true local variables, but that's not helpful so long as they're not truly local.

4
  • In your example, print_four() does not pass it's local param three to print_three(). There's no echo $1. Thus, I'd expect it to print 3.`
    – RonJohn
    Commented Jun 15 at 17:54
  • Here's where I'd have expected print_four() to print 3: the code readonly three=3; print_three() { echo "three in print_three: $three"; echo "param 1 in print_three: $1"; }; print_four() { local three=4; print_three $three; }; print_four. But that's my lexical thinking...
    – RonJohn
    Commented Jun 15 at 17:56
  • I don't agree with that characterisation. In bash, the manual clearly describes it as local variables being created separately and shadowing the one(s) from parent scope(s). Even in the Almquist shell (where local comes from and where local variables inherit value and attributes of the parent variable), the local variables can be seen as being copies of the parent variable, discarded upon return of the function, same as in the local variables of the Bourne shell that you get in subshells where the copy is done by fork() and discarded upon exit. Commented Jun 17 at 5:59
  • @StéphaneChazelas: Thanks for your comment! I think that both ways of looking at it are valid and useful. The way that I characterized it in this answer is more useful for intuiting its behavior, whereas the "creates a local variable" framing is more useful for understanding its overall purpose. In this case Bash's handling of readonly is being driven by behavioral considerations, so that framing seems more helpful in answering this question. But I'm not offended if you disagree. :-)
    – ruakh
    Commented Jun 17 at 6:14
6

This is documented under local in

www.gnu.org/software/bash/manual/bash.html#Bash-Builtins

The return status is zero unless local is used outside a function, an invalid name is supplied, or name is a readonly variable.

I do find this surprising, as the first sentence there says:

For each argument, a local variable named name is created, and assigned value.

That implies a new variable is created with scope restricted to that function. There is an argument that a global constant should be reliably preserved.

15
  • And in the -r subsection of the declare section on the same page: "These names cannot then be assigned values by subsequent assignment statements or unset." Presumably those "subsequent assignment statements" means even those in inner scopes?
    – RonJohn
    Commented Jun 14 at 8:34
  • You also can't unset a readonly variable, even though that might be seen as conceptually different from modifying the value (compare with e.g. removing a file vs. writing to it). In a way, preventing the variable from being shadowed by another one with the same name seems to fit the same idea. Especially given how variable scoping works. A function F expecting to be called from the main level and using a readonly variable defined there could be made to see another variable instead if called through G where the same name was redefined to shadow the main-level readonly one.
    – ilkkachu
    Commented Jun 14 at 10:18
  • @ilkkachu "A function F expecting to be called from the main level and using a readonly variable defined there". To clarify: where is "there"? Do you mean "defined in 'main'" or "defined in F"?
    – RonJohn
    Commented Jun 14 at 11:02
  • 1
    @ruakh, ksh is an outlier here, though; it actually has lexical scoping. Somewhat significantly, considering that otherwise Bash has borrowed nearly everything from ksh.
    – ilkkachu
    Commented Jun 14 at 22:52
  • 1
    @ilkkachu: Yes, exactly. :-)
    – ruakh
    Commented Jun 15 at 16:17

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .