3

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.

2

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.

1

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
  • The -v test may have appeared in a recent bash version, I don't know. – glenn jackman Oct 29 '18 at 21:32
  • I considered the $(param:=value} thing but discarded it lacking support for declare -r / readonly – cat Oct 29 '18 at 23:25
  • Well do you want read-only or not? Are you really asking us how to assign to a read-only variable? – glenn jackman Oct 30 '18 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 '18 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. – glenn jackman Oct 31 '18 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.

  • 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 '18 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 '18 at 13:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.