Watch for environment variable change - ZSH - bash

Is there a way to watch for changes to an environment variable in zsh/bash? When switching my kubernetes environment, for example, I would like to be able to read the variable that's set and make changes to my terminal window if I'm in production vs. development etc.
The way we switch environments is part of our tooling. I'd like to be able to extend that on my own machine without having to update any tooling. If watching for an environment variable change isn't possible, I'm also looking for a way to use something similar to builtin.
Example: create a function of the same name as an alias, call that alias from within the function, then do some other action afterward.

Both shells provide a way to execute arbitrary code prior to displaying a prompt; you can use this to check the value of a specific variable and take an appropriate action.
In .bashrc:
# The function name doesn't matter; it's just easier
# to set PROMPT_COMMAND to the name of a function than
# to arbitrary code.
pre_prompt () {
if [[ $SOME_VAR == "prod" ]]; then
doSomething
else [[
doSomethingElse
fi
}
PROMPT_COMMAND=pre_prompt
In .zshrc:
precmd () {
if [[ $SOME_VAR == "prod" ]]; then
doSomething
else [[
doSomethingElse
fi
}

Related

Does `bash` have an environment variable stack?

I'm curious about whether bash has a stack for environment variables.
As an example of a usecase, it is apparently good practice to unset CDPATH in a script. Of course, you'd want to restore the original value of CDPATH afterwards so that the script doesn't break the user's environment.
It'd be nice if we could do
#!/usr/bin/env bash
pushenv CDPATH
<script here>
popenv CDPATH
and call it day. Unfortunately, one has to mess around with these kinds of shenanigans:
#!/usr/bin/env bash
old_CDPATH="$CDPATH"
unset CDPATH
<script here>
CDPATH="$old_CDPATH"
unset old_CDPATH
Besides being messy, this poses a genuine issue. If the variable old_CDPATH may be in use already; if $CDPATH is used in the <script here> itself, it will break.
This is what stacks are used for, and is why the pushd and popd bash builtins exist. But do we have similar builtin functionality for arbitrary variables?
You may say "Just do things in a subshell, and don't export old_CDPATH" to which I say, no, sometimes you have to run code (e.g. source scripts) in the same shell (e.g. if the very purpose of those scripts is to modify environment variables.
So, how do we get stack-like behaviour? We could use this idiom:
#!/usr/bin/env bash
declare -a arr_old_CDPATH
function do_thing() {
arr_old_CDPATH+="$CDPATH"
unset CDPATH
<script here>
CDPATH="${arr_old_CDPATH[-1]}"
unset arr_old_CDPATH[-1]
}
do_thing
We could even package it up into pop and push functions:
#!/usr/bin/env bash
function push() { # $1 = stack (array) name, $2 = var name
local -n arr=$1
arr+=("${!2}")
unset "$2"
}
function pop() { # $1 = stack (array) name
local -n arr=$1
echo "${arr[-1]}"
unset arr[-1]
}
push cdstack CDPATH
<script here>
CDPATH="$(pop CDPATH)"
We could also get more sophisticated, automatically creating a stack for each variable. Then we could just do "pop CDPATH":
declare _env_stack_name
env_stack_set_name() { # $1 = var name
local stack_name=_stack_env_$1
[ -v $stack_name ] || declare -ag $stack_name
_env_stack_name=$stack_name
}
pushenv() { # $1 = var name
env_stack_set_name $1
local -n arr=$_env_stack_name
arr+=("${!1}")
unset "$1"
}
popenv() { # $1 = var name
env_stack_set_name $1
local -n arr=$_env_stack_name
local -n var=$1
var=${arr[-1]}
unset arr[-1]
}
pushenv CDPATH
<script here>
popenv CDPATH
This all seems quite excessive for basic stack functionality, which is required for complex scripts that need to run in a single shell.
Is there a better idiom for this or a more generic bash builtin than pushd/popd which can allow the pushing/popping of environment variables?
Does bash have an environment variable stack?
No.
As an example of a usecase, it is apparently good practice to unset CDPATH in a script. Of course, you'd want to restore the original value of CDPATH afterwards so that the script doesn't break the user's environment.
No, it is never necessary to restore the original value of any environment variable for the benefit of the parent process (including in the common case in which the parent is a shell). No process can change another running process's environment, so a called (as opposed to sourced) script's changes to its own environment have no effect on the environment of its parent process.
sometimes you have to run code (e.g. source scripts) in the same shell (e.g. if the very purpose of those scripts is to modify environment variables.
Yes, but this is under the control of the author of the script doing the sourcing. That's not to say that there is no use case for an environment variable stack, but in practice, it's not an especially important one.

Set an environment variable and retrieve it later

I would like to make an executable shell-script that once launched execute a certain action, and if launched again in a second time undo the action. I've tried to define an environment variable to determinate if the action has been already executed, but I can't make it work.
#!/bin/bash
if [ -z ${CUSTOM_VARIABLE} ]
then
CUSTOM_VARIABLE=true
export CUSTOM_VARIABLE
# do stuff here
else
# undo stuff here
unset CUSTOM_VARIABLE
fi
Is this a correct approach? How can I fix the code
Thanks in advance
Note jdv's comment.
You cannot run your script as a standalone execution and have it alter the environment of the parent. Calling it x.sh,
x.sh
will never change your env. On the other hand, you can source it.
. x.sh
which reads it and executes its content in the current scope, so that would work. You aren't really creating a separate program doing that - just storing a scripted list of commands and using the source shorthand to execute them in bulk, so to speak.
You could also define it as a function for similar result -
$: x() {
if [[ -z "${CUSTOM_VARIABLE}" ]]
then export CUSTOM_VARIABLE=true
else unset CUSTOM_VARIABLE
fi
}
YMMV.
Good luck.

Bash: per-login environment variables

Is there a way to have an environment variable, or some similar construct, persist across all bash terminal instances in any given login session?
What I'm trying to do is have a variable prompt, which I can switch (for all terminal windows, future, and maybe also current) at a whim. The process of switching is not too important, but I'd like for it to be a set-and-forget thing.
So far, the best option seems to be to have a set of hardcoded values in .bashrc, which my 'switcher' script would edit, but that seems a bit hacky, and prone to destroying everything if there are any minor errors.
I would go for the easiest solution: Write the data to a file instead of a variable.
You can also create some helpers in your .bashrc to automatically retrieve/set the value. A file seems to fulfil all your requirements.
EDIT: You can also have the helpers set the variable on each login from the file.
There's no way to change the prompt in every active shell at once, but you can define functions to your .bashrc which you can call to change the prompt at will.
use_prompt_1 () {
PS1="\w \$"
}
use_prompt_2 () {
PS1="\u#\h \$"
}
You can also use PROMPT_COMMAND to examine your environment to choose a prompt. Add something like the following to your .bashrc as well:
dynamic_prompt () {
if [[ $PWD =~ some_regex ]]; then
use_prompt_1
else
use_prompt_2
fi
}
PROMPT_COMMAND=dynamic_prompt

How do I stop variables in .bashrc being visible in the shell after sourcing?

As an example, my .bashrc sources other scripts using a relative path in the following way, although my question is about any temporary variable used in .bashrc.
DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
source $DIR/.somefile
Now I have DIR cluttering every shell. Short of explicitly unset DIR at the end of .bashrc, is there any way to limit the variable's scope?
[EDIT] To see a list: ( set -o posix ; set ) | less
I appreciate very much that you ask that question. I hate variables that are no longer used but remain cluttering the shell. Some people build large bash scripts that do complicated things, and they don't care about such variables, and at the end there are hundreds of unused variables cluttering the namespace of the shell, and it's completely unclear which of them is still needed and which is not.
The general solution is to use local variables inside a function:
function source_it()
{
local DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
source $DIR/.somefile
}
source_it
The variable DIR will be visible within the function and in all functions called by the function. It will cease to exist upon return from the function.
That will work even if you already have a global variable called DIR. The local variable will shadow the global variable DIR, thus the function uses the local version only, and once the function returns, the global version of DIR will be visible again, completely untouched.
Nested function calls can create arbitrary many versions of a local variable:
#!/bin/bash
factorial()
{
local n="$1"
if test $n -eq 0
then
echo 1
else
local m=$((n-1))
echo $((n*$(factorial $m)))
fi
}
factorial 5 # result: 120
Answering to your 1st comment: You can even write a self-destructing function. Try this:
#!/bin/bash
fun()
{
echo this is fun
# unset -f fun # upon your 2nd comment, changed that to:
unset -f $FUNCNAME
}
fun
fun
result:
this is fun
(script name): line 10: fun: command not found
However, that looks a bit strange - I am not sure if every future implementation of bash will allow functions to destroy themself as their last act.

Is there any mechanism in Shell script alike "include guard" in C++?

let's see an example: in my main.sh, I'd like to source a.sh and b.sh. a.sh, however, might have already sourced b.sh. Thus it will cause the codes in b.sh executed twice. Is there any mechanism alike "include guard" in C++?
If you're sourcing scripts, you are usually using them to define functions and/or variables.
That means you can test whether the script has been sourced before by testing for (one of) the functions or variables it defines.
For example (in b.sh):
if [ -z "$B_SH_INCLUDED" ]
then
B_SH_INCLUDED=yes
...rest of original contents of b.sh
fi
There is no other way to do it that I know of. In particular, you can't do early exits or returns because that will affect the shell sourcing the file. You don't have to use a name that is solely for the file; you could use a name that the file always has defined.
In bash, an early return does not affect the sourcing file, it returns to it as if the current file were a function. I prefer this method because it avoids wrapping the entire content in if...fi.
if [ -n "$_for_example" ]; then return; fi
_for_example=`date`
TL;DR:
Bash has a source guard mechanism which lets you decide what to do if executed or sourced.
Longer version:
Over the years working with Bash sourcing I found that a different approach worked excellently which I will discuss below.
The problem for me was similar to the one of the original poster:
sourcing other scripts led to double script execution
additionally, scripts are less testable with unit test frameworks like BATS
The main idea of my solution is to write scripts in a way that can safely sourced multiple times. A major part plays the extraction of functionality (compared to have a large script which would not render very testable).
So, only functions and global variables are defined, other scripts can be sourced at will.
As an example, consider the following three bash scripts:
main.sh
#!/bin/env bash
source script2.sh
source script3.sh
GLOBAL_VAR=value
function_1() {
echo "do something"
function_2 "completely different"
}
run_main() {
echo "starting..."
function_1
}
# Enter: the source guard
# make the script only run when executed, not when sourced)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
run_main "$#"
fi
script2.sh
#!/bin/env bash
source script3.sh
ALSO_A_GLOBAL_VAR=value2
function_2() {
echo "do something ${1}"
}
# this file can be sourced or be executed if called directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
function_2 "$#"
fi
script3.sh
#!/bin/env bash
export SUPER_USEFUL_VAR=/tmp/path
function_3() {
echo "hello again"
}
# no source guard here: this script defines only the variable and function if called but does not executes them because no code inside refers to the function.
Note that script3.sh is sourced twice. But since only functions and variables are (re-)defined, no functional code is executed during the sourcing.
The execution starts with with running main.sh as one would expect.
There might be a drawback when it comes to dependency cycles (in general a bad idea): I have no idea how Bash reacts if files source (directly or indirectly) each other.
Personally I usually use
set +o nounset # same as set -u
on most of my scripts, therefore I always turn it off and back on.
#!/usr/bin/env bash
set +u
if [ -n "$PRINTF_SCRIPT_USAGE_SH" ] ; then
set -u
return
else
set -u
readonly PRINTF_SCRIPT_USAGE_SH=1
fi
If you do not prefer nounset, you can do this
[[ -n "$PRINTF_SCRIPT_USAGE_SH" ]] && return || readonly PRINTF_SCRIPT_USAGE_SH=1

Resources