4

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

#!/bin/bash
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
Exited

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
  fi
}

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

4

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"
  done
}

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.

2

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}

demonstrating:

$ OS=macosx
$ echo "$NOTIFY"

$ : ${NOTIFY:=$(case "$OS" in (macosx) echo macos_notify ;; (linux) echo linux_notify ;; (*) echo : ;; esac)}
$ echo "$NOTIFY"
macos_notify
$ NOTIFY=no
$ : ${NOTIFY:=$(case "$OS" in (macosx) echo macos_notify ;; (linux) echo linux_notify ;; (*) echo : ;; esac)}
$ echo "$NOTIFY"
no
$ NOTIFY=""
$ : ${NOTIFY:=$(case "$OS" in (macosx) echo macos_notify ;; (linux) echo linux_notify ;; (*) echo : ;; esac)}
$ echo "$NOTIFY"
macos_notify
5
  • 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
1

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="$1"
    [[ "" == "${_assignmentVariableName}" ]] && AbortScript "${FUNCNAME}: assignmentVariableName is blank."
    shift

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

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

    _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.

2
  • 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 .