Use of Read-Only Variables in Shell Scripts

Use of read-only variables in shell scripts

It sounds like you might think that readonly does more than it really does. For one thing, readonly status is not exported into the environment or inherited by child processes:

$ declare -rx LOGS=hello
$ LOGS=goodbye
bash: LOGS: readonly variable
$ bash -c 'echo "$LOGS"'
hello
$ bash -c 'LOGS=goodbye; echo "$LOGS"'
goodbye
$

shell script set -e with readonly variable

This is because the line readonly V="$(cat non-existant-file)" is not a simple assignment: it is the composition of an assignment that fails, followed by the instruction readonly V, which succeeds.

That explains the behavior you observed, and this Bash pitfall is mentioned for a similar construct (local) in the documentation BashFAQ/105 indicated by @codeforester.

So, if you try instead the following code, you should observe the behavior you expect:

#!/bin/bash
set -ex
V=$(cat non-existant-file)
readonly V
echo "var V: $V"

Minor remarks:

  • I corrected the shebang that should be #!/usr/bin/env bash or #!/bin/bash, not !#/bin/bash

  • I replaced V="$(cat non-existant-file)" with V=$(cat non-existant-file) because the quotes are unnecessary here.

How can I make a read-only variable?

You can make use of readonly:

$ var="hello"
$ readonly var
$ echo $var
hello
$ var="bye"
sh: var: readonly variable

readonly' exit status in bash

You can do it in two steps without needing a temporary variable:

foo="$(false)"
echo $?
readonly foo

Alternatively you can do

readonly foo="$(false)" status="$?"

to capture both the output and the exit status simultaneously.

How can I limit readonly variable scope to a function?

Replace:

readonly local foo="bar"

with:

local -r foo="bar"

The issue is that readonly local foo="bar" defines two readonly variables: one named local and one namedfoo. It does not create any local variables.

By contrast, local -r foo="bar" creates a variable named foo which is both local and readonly.

As David C Rankin points out, once you have created a global read-only variable, you cannot unset it. You need to close your existing shell and start a new one.

Unset readonly variable in bash

Actually, you can unset a readonly variable. but I must warn that this is a hacky method. Adding this answer, only as information, not as a recommendation. Use it at your own risk. Tested on ubuntu 13.04, bash 4.2.45.

This method involves knowing a bit of bash source code & it's inherited from this answer.

$ readonly PI=3.14
$ unset PI
-bash: unset: PI: cannot unset: readonly variable
$ cat << EOF| sudo gdb
attach $$
call unbind_variable("PI")
detach
EOF
$ echo $PI

$

A oneliner answer is to use the batch mode and other commandline flags, as provided in F. Hauri's answer:

$ sudo gdb -ex 'call unbind_variable("PI")' --pid=$$ --batch

sudo may or may not be needed based on your kernel's ptrace_scope settings. Check the comments on vip9937's answer for more details.

Bash local AND readonly variable

First attempt: local readonly var1

That is the way I used to define it. It is wrong. I will define my variable var1 as local, but it will not be readonly, as you can see on example below, I can change the value of var1, and I don't want that!

:~$ (
> myfunction()
> {
> # Define variable
> local readonly var1="val1"
>
> echo "Readonly output:"
> readonly | grep -E 'readonly|local|var1'
> echo ""
>
> echo "Local output:"
> local | grep -E 'readonly|local|var1'
> echo ""
>
> var1="val2"
> echo "VAR1 INSIDE: ${var1}"
> }
> myfunction
> echo "VAR1 OUTSIDE: ${var1}"
> )
Readonly output:

Local output:
var1=val1

VAR1 INSIDE: val2
VAR1 OUTSIDE:

Second attempt: readonly local var1

This time it will define var1 as readonly, but it will also define a variable called local, so using this way it will not handle local as keyword, it will be a variable name.

Check also that the scope of var1 is not local, in fact it is global, we can see the value of var1 outside the function.

:~$ (
> myfunction()
> {
> # Define variable
> readonly local var1="val1"
>
> echo "Readonly output:"
> readonly | grep -E 'readonly|local|var1'
> echo ""
>
> echo "Local output:"
> local | grep -E 'readonly|local|var1'
> echo ""
>
> echo "VAR1 INSIDE: ${var1}"
> }
> myfunction
> echo "VAR1 OUTSIDE: ${var1}"
> )
Readonly output:
declare -r local
declare -r var1="val1"

Local output:

VAR1 INSIDE: val1
VAR1 OUTSIDE: val1

As it should be: local -r var1

This way it will do exactly what I want, it will define var1 as scope local AND readonly.

:~$ (
> myfunction()
> {
> # Define variable
> local -r var1="val1"
>
> echo "Readonly output:"
> readonly | grep -E 'readonly|local|var1'
> echo ""
>
> echo "Local output:"
> local | grep -E 'readonly|local|var1'
> echo ""
>
> #var1="val2"
> echo "VAR1 INSIDE: ${var1}"
> }
> myfunction
> echo "VAR1 OUTSIDE: ${var1}"
> )
Readonly output:
declare -r var1="val1"

Local output:
var1=val1

VAR1 INSIDE: val1
VAR1 OUTSIDE:

We can define it as below also, but one line is better than two!

local var1="val1"
readonly var1


Related Topics



Leave a reply



Submit