0

I'm setting up my shell environments and I want to be able to use some of the same functions/aliases in zsh as in bash. One of these functions opens either .bashrc or .zshrc in an editor (whichever file is relevant), waits for the editor to close, then reloads the rc file.

# a very simplified version of this function
editrc() {
  local rcfile=".$(basename $SHELL)rc"
  code -w ~/$rcfile
  . ~/$rcfile
}

I use the value of rcfile in a few other functions, so I've pulled it out of the function declaration.

_rc=".$(basename $SHELL)rc"
editrc() {
  code -w ~/$_rc
  . ~/$_rc
}

# ... other functions that use it ...
unset _rc

However, because I'm a neat freak, I want to unset _rc at the end of my script, but I still want my functions to run correctly. Is there a clever way to evaluate $_rc at the time the function is declared?

I know I could use eval and place everything except $_rc instances within single quotes, but that seems like a pain, since the full version of my function uses both single-quotes and double-quotes.

_rc=".$(basename $SHELL)rc"
eval 'editrc() {
  echo Here'"'"'s a thing that uses single quotes.  As you can see it'"'"'s a pain.
  code -w ~/'$_rc'
  . ~/'$_rc'
}'
# ... other functions using `_rc`
unset _rc

I'm guessing I could declare my functions, then do some magic with eval "$(declare -f editrc | awk)". It very well be more pain than it's worth, but I'm always interested in learning new things.

Note: I'd love to generalize this into a utility function that does this.

_myvar=foo
anothervar=bar
myfunc() {
  echo $_myvar $anothervar
}

# redeclares myfunc with `$_myvar` expanded, but leaves `$anothervar` as-is
expandfunctionvars myfunc '$_myvar' 
dx_over_dt
  • 13,240
  • 17
  • 54
  • 102
  • 1
    Your function won't work reliably, because if you run a **bash**, and from within you open a **zsh** as a subshell, `SHELL` will be still set to bash. A workaround would be to put a `export SHELL=/bin/zsh` into `~/.zshenv`. – user1934428 Aug 04 '20 at 08:31
  • @user1934428 I assume I have to do likewise for bash within zsh? I'm still new to both shells. What's the `.zshenv` bash equivalent? – dx_over_dt Aug 04 '20 at 15:31
  • I am not aware that there is an exact equivalent. However, `bash` sets the `SHELL` variable automatically if it is not set already and you don't nee to care about bash login shells. For an interactive bash subshell started from within a zsh, you could place the definition into `.bashrc`. For a non-interactive bash, you are out of luck, but if you write a script, you **know** what language is used, and can set the variable explicitly in the script. – user1934428 Aug 05 '20 at 06:35

2 Answers2

3

Is there a clever way to evaluate $_rc at the time the function is declared?

_rc=".$(basename "$SHELL")rc"
# while you could eval here, source lets you work with a stream
source <(
  cat <<EOF
  editrc() {
       local _rc
       # first safely trasfer context
       $(declare -p _rc)
EOF
  # use quoted here string to do anything inside without caring. 
  cat <<'EOF' 
     # do anything else
     echo "Here's a thing that uses single quotes.  As you can see it's not a pain, just choose proper quoting."
      code -w "~/$_rc"
      . "~/$_rc"
  }
EOF
)
unset _rc

Generally first use declare -p to transfer variables as strings to be evaluated. Then after you "import" variables, use a quoted here document to do anything as in a normal script.

References to read:

The source command reads a pipe created by process substitution. Inside the process subtitution I output the function to be sourced. With the first here document I output the function name definition, with a local of the variable so that it doesn't pollute global namespace. Then with declare -p I output the variable definition as a properly quoted string later to be sourced by source. Then with a quoted here document I output the rest of the function, so that I do not need to care about quoting.

The code is bash specific, I know nothing about zsh and don't use it.

You could do it with eval too:

eval '
editrc() {
       local _rc
       # first safely trasfer context
       '"$(declare -p _rc)"'
       # use quoted here string to do anything inside without caring. 
       # do anything else
       echo "Here'\''s a thing that uses single quotes.  As you can see it'\''s not a pain, just choose proper quoting."
      code -w "~/$_rc"
      . "~/$_rc"
}'

But for me using a quoted here document delimiter allows for easier writing.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • Could you explain this? It looks like you create a list and put that into the input of source (though I don't know the difference between `<` and `<<`. I don't know what `cat < – dx_over_dt Aug 02 '20 at 21:24
  • With more variable, just do `local var1 var2 var3 ...` and do `declare -p var1 var2 var3...`. I have a script I use that method in wild [here](https://gitlab.com/Kamcuk/kamilscripts/-/blob/master/bin/alias_complete.sh#L311). – KamilCuk Aug 02 '20 at 21:33
  • I just posted what I meant by a generalized solution. – dx_over_dt Aug 02 '20 at 21:55
0

While KamilCuck was working on their answer, I devised a function that will take in any function name and a set of variable names, expand just those variables, and redeclare the function.

expandFnVars() {
  if [[ $# -lt 2 ]]; then
    >&2 echo 'expandFnVars requires at least two arguments: the function name and the variable(s) to be expanded'
    return 1
  fi

  local fn="$1"
  shift

  local vars=("$@")

  if [[ -z "$(declare -F $fn 2> /dev/null)" ]]; then
    >&2 echo $fn is not a function.
    return 1
  fi

  foundAllVars=true
  for v in $vars; do
    if [[ -z "$(declare -p $v 2> /dev/null)" ]]; then
      >&2 echo $v is not a declared value.
      foundAllVars=false
    fi
  done

  [[ $foundAllVars != true ]] && return 1

  fn="$(declare -f $fn)"

  for v in $vars; do
    local val="$(eval 'echo $'$v)" # get the value of the varable represented by $v
    val="${val//\"/\\\"}" # escape any double-quotes
    val="${val//\\/\\\\\\}" # escape any backslashes
    fn="$(echo "$fn" | sed -r 's/"?\$'$v'"?/"'"$val"'"/g')" # replace instances of "$$v" and $$v with $val
  done

  eval "$fn"
}

Usage:

foo="foo bar"
bar='$foo'
baz=baz

fn() {
  echo $bar $baz
}

expandFnVars fn bar

declare -f fn
# prints:
#  fn () 
#  {
#     echo "$foo" $baz
#  }

expandFnVars fn foo

declare -f fn
# prints:
#  fn () 
#  {
#     echo "foo bar" $baz
#  }

Looking at it now, I see one flaw. Suppose $bar in the original function was in single-quotes. We probably would not want its value to be replaced. This could be fixed by some clever regex lookbehinds to count the number of unescaped 's, but I'm happy with it as-is.

dx_over_dt
  • 13,240
  • 17
  • 54
  • 102
  • 1
    There are other problems with it too: for example, if you use it to replace `$foo` (which contains `bar`) and the function contains `echo $food`, it will change that to `echo bard`. If one variable's value has another variable's name in, that will get substituted with its value (which wouldn't happen otherwise). If the function contains `echo "value: $foo"`, the second `"` will be removed, causing a syntax error. It won't replace `${foo}`, which the shell would. (Some of these problems can be removed easily; others less so.) – psmears Aug 03 '20 at 06:48
  • Good points. Want to take a stab at a better solution? – dx_over_dt Aug 03 '20 at 07:19