If I had a script which sets variables read-only to some odd values, and sets errexit because of other unsafe operations:

set -e 
declare -r NOTIFY=$(case "$OS" in (macosx) echo macos_notify ;; (linux) echo linux_notify ;; (*) echo : ;; esac)
declare -r SAY=_say # _say is a function
declare -r VERSION=0.99
set +e 

And I source it to get the definitions, the second time because it's in development:

$ . s.bash 

$ . s.bash 
bash: declare: NOTIFY: readonly variable

Normally declare -r EXISTING_VAR would neither stop the script nor remove the old, working definition of EXISTING_VAR.

But with errexit, assigning to an existing variable is understandably a failure. The easy options are to remove -r or use set +e for that part of the script.

Barring those, is it possible to write a Bash function to take the place of declare -r but not re-assign if the name already exists?

I tried:

# arg #1: var name, #2: value
set_var_once () {
  # test whether the variable with the 
  # name stored in $1 exists 
  if [[ -z "${!1}" ]] 
  then # if it doesn't, set it
    declare -r $1=$2

I also tried things along the lines of eval "declare -r $1=$(eval $2)", it feels like eval is required somewhere here but I'm not sure where.

All of the versions of set_var_once result in not setting the variable they should.

3 Answers 3


declare -r make a variable readonly but also declares it in the current scope and so makes it local to the current function. You'd want readonly instead that only does the former:

readonly_once() {
  local __assign
  for __assign do
    [[ -v ${__assign%%=*} ]] || readonly "$__assign"

To be used as:

readonly_once VAR1=foo VAR2="$(cmd)" PATH ...

Note that since contrary to readonly, that readonly_once is not a keyword (yes, readonly is also a keyword even though bash keeps that fact hidden), that $(cmd) needs to be quoted to prevent split+glob, it's not an assignment at that point.

$(cmd) will be expanded (and so cmd run) even if the value will end-up not being assigned to VAR2 if it was already defined.

That function only works for scalar variables, not arrays nor associative arrays.


If your shell is bash, you can use the -v test:

[[ -v NOTIFY ]]  || NOTIFY=$(case "$OS" in (macosx) echo macos_notify ;; (linux) echo linux_notify ;; (*) echo : ;; esac)
[[ -v SAY ]]     || SAY=_say # _say is a function
[[ -v VERSION ]] || VERSION=0.99

For example

$ unset myvar
$ [[ -v myvar ]] && echo "already set to $myvar" || myvar=10
$ [[ -v myvar ]] && echo "already set to $myvar" || myvar=10
already set to 10
$ myvar=5
$ [[ -v myvar ]] && echo "already set to $myvar" || myvar=10
already set to 5
$ myvar=""
$ [[ -v myvar ]] && echo "already set to $myvar" || myvar=10
already set to 

Or, use the ${param:=value} expansion and the : command

: ${NOTIFY:=$(case "$OS" in (macosx) echo macos_notify ;; (linux) echo linux_notify ;; (*) echo : ;; esac)}
: ${SAY:=_say}
: ${VERSION:=0.99}


$ OS=macosx
$ echo "$NOTIFY"

$ : ${NOTIFY:=$(case "$OS" in (macosx) echo macos_notify ;; (linux) echo linux_notify ;; (*) echo : ;; esac)}
$ echo "$NOTIFY"
$ : ${NOTIFY:=$(case "$OS" in (macosx) echo macos_notify ;; (linux) echo linux_notify ;; (*) echo : ;; esac)}
$ echo "$NOTIFY"
$ : ${NOTIFY:=$(case "$OS" in (macosx) echo macos_notify ;; (linux) echo linux_notify ;; (*) echo : ;; esac)}
$ echo "$NOTIFY"
  • The -v test may have appeared in a recent bash version, I don't know. Oct 29, 2018 at 21:32
  • I considered the $(param:=value} thing but discarded it lacking support for declare -r / readonly
    – cat
    Oct 29, 2018 at 23:25
  • Well do you want read-only or not? Are you really asking us how to assign to a read-only variable? Oct 30, 2018 at 3:04
  • 1
    I'm asking how not to assign to a readonly variable if it's set. I ended up using the [[ -v VAR ]] || declare -r VAR= thing anyway :)
    – cat
    Oct 30, 2018 at 16:07
  • Ah, I see. Well we can ask the shell if the variable is readonly: [[ -v VAR ]] && [[ "$(declare -p VAR)" == "declare -r"* ]] && echo "can't set readonly variable" -- now, if you declare the variable with other attributes (e.g. declare -ri answer=42) then you may need to adjust that pattern. Oct 31, 2018 at 0:05

I have found the following to be useful

# Function SetCommand
# Function to find a command and assign the absolute path of that command to
# a variable.  The intent is to only invoke known good commands.
# If a command is not found, abort.  Assume the script needed this command.
# Function is called as follows:
#     SetCommand assignmentVariableName queryString
# where
#     assignmentVariableName is the name of the variable to which the path
# is assigned,
#     queryString is the name of the command
# Example: SetCommand CMD_FOO foo
SetCommand() {
    local _assignmentVariableName
    local _fullPath
    local _queryString

    [ $# -ne 2 ] && AbortScript "${FUNCNAME}: Invalid number of arguments."

    [[ "" == "${_assignmentVariableName}" ]] && AbortScript "${FUNCNAME}: assignmentVariableName is blank."

    [[ "" == "${_queryString}" ]] && AbortScript "${FUNCNAME}: queryString is blank."

    if [[ ! -z ${!_assignmentVariableName+x} ]]; then
        Print2Stderr "${FUNCNAME}: ${_assignmentVariableName} already defined."
        return ${constErrorExitCode}

    _fullPath=$(${CMD_WHICH} ${_queryString} 2>/dev/null)
    [[ "" == "${_fullPath}" ]] && AbortScript "${FUNCNAME}: Could not find command, ${_queryString}."

    eval readonly ${_assignmentVariableName}=${_fullPath}

    return ${constSuccessExitCode}
} # End SetCommand

The AbortScript is another function I use. It just prints out the error message and then exits the script.

  • This doesn't appear to work, I still get bash: NOTIFY: readonly variable but this time from readonly rather than from declare. Remember, it needs to survive being re-sourced with the same readonly names pre-declared
    – cat
    Oct 29, 2018 at 20:22
  • 1
    Sorry about that. I copied my original snippet from an older script. I have updated the above. Please try now.
    – Lewis M
    Oct 30, 2018 at 13:26

You must log in to answer this question.

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