PowerShell flexibility in scope of environment variables via scripts - bash

In bash, if I define a variable inside a script (say, set_var.sh), I can choose whether those definitions persist after "running" the script.
This will depend on how I "run" the script, the options being:
sh set_var.sh (variables do not persist)
./set_var.sh (variables do not persist, same as point 1)
source set_var.sh (variables persist)
This is irrespective from the variable being exported to the environment or not in set_var.sh.
Can the same as items 1 and 3 above be achieved with PowerShell 5.1 scripts, for both PS and $env variables?
For an illustration see (1) below.
EDIT:
As per answer by Mathias R. Jessen, the equivalent to item #3 above is "dot sourcing".
There is also an "intermediate" case (not identified above, perhaps there is also a way to get this in bash), where environment variables persist but PS variables don't.
My script:
# set_var.ps1
$env:TEST_VAR = 'test_var'
$TEST_VAR2 = 'test_var2'
What I checked:
> $env:TEST_VAR ; $TEST_VAR2 ;
> . .\set_var.ps1
> $env:TEST_VAR ; $TEST_VAR2 ;
test_var
test_var2
> Remove-Variable TEST_VAR2 ; Remove-Item env:TEST_VAR ;
> $env:TEST_VAR ; $TEST_VAR2 ;
> .\set_var.ps1
> $env:TEST_VAR ; $TEST_VAR2 ;
test_var
> Remove-Item env:TEST_VAR ;
> $env:TEST_VAR ; $TEST_VAR2 ;
> & .\set_var.ps1
> $env:TEST_VAR ; $TEST_VAR2 ;
test_var
(1) Example of persistent / non-persistent variables
I have script set_var.sh with the following contents:
#!/bin/bash
export TEST_VAR=test_var
TEST_VAR2=test_var2
Then the following commands prove my point:
$ echo $TEST_VAR ; echo $TEST_VAR2
$ sh set_var.sh ; echo $TEST_VAR ; echo $TEST_VAR2
$ source set_var.sh ; echo $TEST_VAR ; echo $TEST_VAR2
test_var
test_var2
$ echo $TEST_VAR ; echo $TEST_VAR2
test_var
test_var2
$ env | grep TEST_VAR
TEST_VAR=test_var
$ unset TEST_VAR ; unset TEST_VAR2
$ echo $TEST_VAR ; echo $TEST_VAR2
$ ./set_var.sh ; echo $TEST_VAR ; echo $TEST_VAR2
$ echo $TEST_VAR ; echo $TEST_VAR2

To complement Mathias' helpful answer:
Regarding your edit:
It is a script's direct invocation / invocation via & - which, unlike in sh / bash, runs in-process - preserves environment-variable changes, but not regular variables in the script.[1]
Using & with a script block ({ ... }) in lieu of a script file for a succinct demonstration:
PS> & { $foo = 'bar'; $env:foo = 'bar-env' }; "[$foo]", "[$env:foo]"
[] # regular variable in the script [block] scope went out of scope
[bar-env] # environment variable is visible to caller (the whole *process*)
As for the PowerShell equivalent of 1. (sh set_var.sh)
and 2. (./set_var.sh):
An explicit call to the PowerShell CLI (powershell.exe in Windows PowerShell, pwsh in PowerShell [Core], v6+): is needed to run a script in a child process.
Note: The following examples use pwsh, but the same applies to powershell.exe (whose CLI is a mostly identical to pwsh's, with only a few parameters having been added to the latter).
In the simplest case, use -File (which is the implied parameter in PowerShell [Core], but is required in Windows PowerShell):
pwsh -File set_var.ps1 # PowerShell v6+: same as: pwsh set_var.ps1
Note, however, that you'll only get textual output (strings) from the script this way rather than the rich objects that PowerShell normally supports.
The simplest way to receive objects instead - available from inside PowerShell only[2] - is to pass a script block in which the script is called instead, which automatically makes the CLI output XML-serialized representations of the script's output objects, and causes these representations to be automatically deserialized by the calling session:
pwsh { .\set_var.ps1 }
Note:
The .\ prefix is necessary, because PowerShell - by design, as a security feature - does not permit running scripts by mere file name from the current directory.
The XML-based serialization using the CLIXML format - which is also used for PowerShell's remoting and background jobs - has limits with respect to type fidelity; except for .NET primitive types and a handful well-known types, you get [pscustomobject]-based approximations of the original objects - see this answer.
[1] However, PowerShell does offer the ability to set variables across in-session scope boundaries, e.g. $global:foo = 'bar' to set a session-global variable or Set-Variable foo bar -Scope 1 to set a variable in the caller's (parent) scope (which when called from a script's top-level scope would also be the global scope).
[2] From outside of PowerShell, you can explicitly request output in CLIXML format (the XML-based object-serialization format), namely with the -of Xml CLI parameter, but you'll have to parse the XML yourself and reconstruct objects from it.

You want . - the dot-sourcing operator - which executes your script or command in the caller's scope, thereby persisting all variables after execution:
# set_vars.ps1
$TEST_VAR = 123
# script.ps1
. $PSScriptRoot/set_vars.ps1
echo $TEST_VAR # outputs 123
To avoid locally defined variables persisting after execution, either execute the script as-is, or by explicitly using the & invocation operator:
# script.ps1
& $PSScriptRoot/set_vars.ps1
echo $TEST_VAR
# outputs an empty string, assuming $TEST_VAR was not already defined in the callers scope

Related

csh scripting - passing variables and calling csh script from another csh script

I am trying to understand passing two variables from one csh script to another csh script. Perform simple arithmatic function then passing the sum variable to the orginal csh script. Then outputing the sum variable to standard output and a file.
script1.sh
#!/bin/csh
setenv num1 3
setenv num2 2
source script2.sh
set total=$sum
echo $total > total.txt
echo $total
script2.sh
#!/bin/csh
set int1=$num1
set int2=$num2
set sum=$int1 + $int2
With 'csh', the 'set' command perform simple (string) assignment, and takes a single word. The assignment 'set sum=$int +$int2' has 2 issues
It does not use a single word
Even the expression was combined into a single word (set sum="$int1 + $int2"), the set does not evaluate expression.
Consider instead using the '#' command which will take an expression
# sum2 = $int1 + $int2
Sde note: For 'csh' script, the common convention is to use a '.csh' suffix. The '.sh' suffix is commonly used to files associated with sh-like shells (sh, bash, ash, ...)

Is there file scope in bash programming?

I want to make this variable
local to="$HOME/root_install/grunt"
be available to the entire file
makeGrunt(){
# set paths
local to="$HOME/root_install/grunt"
cd $to
sudo npm install -g grunt-init
sudo git clone https://github.com/gruntjs/grunt-init-gruntfile.git ~/.grunt-init/gruntfile
sudo grunt-init gruntfile
}
In POSIX-like shells - unless you use nonstandard constructs such as local, typeset, or declare - variables created implicitly through
assignment have global scope in the shell at hand.
Thus, to="$HOME/root_install/grunt" will make variable $to available anywhere in the current shell - unless you're inside a function and that variable was explicitly marked as local.
andlrc's helpful answer demonstrates the pitfalls associated with subshells - subshells are child processes that are clones of the original shell - they see the same state, but cannot modify the original shell's environment.
Bash shells use dynamic
scopes
which means that all variables are available for all called functions, commands,
etc. Consider this:
var=1
a() {
local var=2
b
}
b() {
echo "$var"
}
a # 2
b # 1
a # 2
When using the local keyword a variable will be available for the function, in
where it's defined, but also within all functions called from that function.
The same applies when a variable is created without the local keyword. With
that exception that it will also be available outside the function.
One more thing to be aware of is that whenever a subshell is created a variable
will not be able to "leave" it, i.e. when a pipe is involved. Consider this:
sum=0
seq 3 | while read -r num; do
sum=$((sum + num))
echo "$sum" # will print 1, 3 and 6
done
echo "$sum" # 0 huh? 1 + 2 + 3 = 0?

Set case sensitive Windows environment variables in Perl

I am trying to set some Windows environment variables in Perl with case sensitivity in order to use them in a shell script.
However, I noticed that the environment variables are all in uppercase when I try to use them in lowercase in a shell script called from Perl with system command and run with Msys.
For example, the following script:
#!perl
system "echo echo TOTO=\$TOTO > toto.sh";
system "echo echo Titi=\$Titi >> toto.sh";
system "echo echo TITI=\$TITI >> toto.sh";
$ENV{'TOTO'}="0+0";
$ENV{'Titi'}="Not toto!";
system("sh toto.sh");
returns (while run in Msys) the following output:
TOTO=0+0
Titi=
TITI=Not toto!
Does anybody know whether this is possible in Windows (i.e. how to set the environment variable Titi and not TITI)?
Thanks
Windows environment variable identifiers are case-independent in the same way as Windows files.
Titi and TITI are both names for the same variable, and its value can be acccessed through either $ENV{TITI} or $ENV{Titi} in Perl. Likewise echo %TITI% and echo %Titi% on the command line will give the same result.
If you explain why it is that you need case-sensitive environment variable names then we may be able to help you.
The following should apply independently of the casing problem.
Let's take the first line:
system "echo echo TOTO=\$TOTO > toto.sh";
If you call
echo echo TOTO=\$TOTO
on your command line, the output will be:
echo TOTO=\$TOTO
which seems to be, what you want.
However, the line you are calling with system is first interpreted by perl, so your escaped \$ becomes a $.
The output then depends on the current value of $TOTO (before you set the environment variable in the script), which is probably empty.
You should change all those system calls to be single quoted:
system 'echo echo TOTO=\$TOTO > toto.sh';
If you do this, you will get the following output:
TOTO=0+0
Titi=
TITI=not TOTO
Titi is empty, because you didn't assign a value to it.
It appeared to be that it is not possible to do: Windows will only use the uppercase variables.
The way I did was to temporarily transform the script by replacing variables with their upper-case equivalent and switch them back to the previous state after using them.
In my case, I had to deal with the variables exported from the shell script Run_Session.sh. I have created a Perl script to perform the uppercase replacement and this is the result.
###############################
## SUBROUTINE uc_variables ##
#-----------------------------#
# This subroutine convert the environment variables exported
# by the top level script to upper case, and can revert the operation.
#
# Input:
# -----
# $mode: "ON" (default) or "OFF"
#
# Output:
# ------
# N/A
#-----------------------#
#########################
sub uc_variables {
my $mode=shift;
chdir $Bin;
if ($mode eq "OFF") {
foreach my $bak (glob "{*,*/*}.sh.bak") {
(my $orig = $bak)=~s/\.bak//;
move($bak,$orig);
}
} else {
#Check if back-up already exists
uc_variables("OFF") if (-e "Run_Session.sh.bak");
#List variables to update
my %uc_var;
open RUN_SESSION,"<Run_Session.sh" or die "Error while reading Run_Session.sh ($!)\n";
map { /export\s+(\w*[a-z]\w*)/ and ($uc_var{$1}=uc($1))=~s/Directory/DIR/i } <RUN_SESSION>;
close RUN_SESSION;
#Replace variables
foreach my $shell (glob "{*,*/*}.sh") {
#Back-up
copy($shell,"$shell.bak");
#Read data
open SHELL,"<$shell" or die "Error while reading $shell ($!)\n";
my $SHELL = join("", <SHELL>);
close SHELL;
#Replace
map { $SHELL=~s/$_/$uc_var{$_}/g } keys %uc_var;
#Print
open SHELL,">$shell" or die "Error while writing in $shell ($!)\n";
print SHELL $SHELL;
close SHELL;
}
}
} #end of uc_variables

how to list the variables defined by a Bash function, including those pre-existing variables that are unchanged in value by the function

I want to run a Bash function that defines a few variables and then, after the function has run, list all of the variables that it has 'attempted' to define. Such a list would include the names of those variables that were pre-existing but were unchanged in value by the function. Could you point me in the right direction on this?
EDIT: I have added some code below both to explain further what it is that I want to accomplish and to offer inspiration.
#!/bin/bash
variable1="zappo"
#listOfVariables1="$(printenv)"
listOfVariables1="$(set)"
function1(){
variable1="zappo"
}
function1
#listOfVariables2="$(printenv)"
listOfVariables2="$(set)"
echo -e "\ncomparing initial list of variables with final list of variables...\n"
diff <(echo "${listOfVariables1}") <(echo "${listOfVariables2}")
The set command will display all shell environmental variables, including those user defined.
To easily the variable list (which can be quite long) before and after a function, you could use diff
e.g.
set > set_before.tmp
my_func
set > set_after.tmp
diff set_before.tmp set_after.tmp
try to edit you bash function it to print environment in the end of execution. Then you can compare (diff list1 list2) list you got with printenv results without your script.
#!/bin/bash
printenv > /tmp/list1
{
cat $(which your_function)
echo 'printenv > /tmp/list2'
} >/tmp/fake.sh
chmod +x /tmp/fake.sh
/tmp/fake.sh
diff /tmp/list1 /tmp/list2
also, if you need just to set this variables, you need to run . $(which your_function)
variables will not be changed in parent scope if you simple run your_function.

How Can I access bash variables in tcl(expect) script

How Can I access bash variables in tcl(expect) script.
I have bash file say f1.sh which set some variables like
export var1=a1
export var2=a2
These variable I need to use in my expect script .
I tried using this in my script which does not work
system "./f1.sh"
puts "var1 is $::env(var1)"
puts "var2 is $::env(var2)"
But this does not seems to work.
I see that non of the variable from f1.sh are getting set as environment variable.
system "./f1.sh" << # Is this command in my script right ?
How I need to access these bash variables from tcl file.
I would say that this problem is rather general. First I met this problem, when I wanted to initialize Microsoft Visual Studio environment (which is done using .cmd script) in PoserShell. Later I've faced this problem with other scripting languages in any combinations (Bash, Tcl, Python etc.).
Solution provided by Hai Vu is good. It works well, if you know from the beginning, which variables you need. However, if you are going to use script for initialization of some environment it my contains dozens of variables (which you don't even need to know about, but which are needed for normal operation of the environment).
In general, the solution for the problem is following:
Execute script and at the end print ALL environment variables and capture the output.
Match lines of output for the pattern like "variable=value", where is what you want to get.
Set environment variables using facilities of your language.
I do not have ready made solution, but I guess, that something similar to this should work (note, that snippets below was not tested - they are aimed only to give an idea of the solution):
Execute script; print vars and capture the output (argument expanding - {*} - requires Tcl 8.5, here we can go without it, but I prefer to use it):
set bashCommand {bash -c 'myScriptName arg1 arg2 2>&1 >/dev/null && export -p'}
if [catch {*}${bashCommand} output] {
set errMsg "ERROR: Failed to run script."
append errMsg "\n" $output
error $errMsg
}
;# If we get here, output contains the output of "export -p" command
Parse the output of the command:
set vars [dict create]
foreach line [split $output "\n"] {
regex -- {^declare -x ([[:alpha:]_]*)=\"(.*)\"$} $line dummy var val
;# 3. Store var-val pair of set env var.
}
Store var-val pair or set env var. Here several approaches can be used:
3.1. Set Tcl variables and use them like this (depending on context):
set $var $val
or
variable $var $val
3.2. Set environment variable (actually, sub-case of 3.1):
global ::env
set ::env($var) $val
3.3 Set dict or array and use it within your application (or script) without modification of global environment:
set myEnv($var) val ;# set array
dict set myEnvDict $var $val ;# set dict
I'd like to repeat, that this is only the idea of the receipt. And more important, that as most of the modern scripting languages support regexes, this receipt can provide bridge between almost arbitrary pair of languages, but not only Bash<->Tcl
You can use a here-document, like this:
#!/bin/bash
process=ssh
expect <<EOF
spawn $process
...
EOF
Exported variables are only passed from a parent process to it's children, not the other way around. The script f1.sh (actually the bash instance that's running the script) gets it's own copies of var1 and var2 and it doesn't matter if it changes them, the changes are lost when it exits. For variable exporting to work, you would need to start the expect script from the bash script.
In f1.sh, printf what you want to return...
printf '%s\n%s\n' "$var1" "$var2"
...and read it with exec in Tcl:
lassign [split [exec ./f1.sh] \n] var1 var2
Perhaps I did not look hard enough, but I don't see any way to do this. When you execute the bash script, you create a different process. What happens in that process does not propagate back to the current process.
We can work-around this issue by doing the following (thanks to potrzebie for the idea):
Duplicate the bash script to a temp script
Append to the temp script some commands at the end to echo a marker, and a list of variables and their values
Execute the temp script and parse the output
The result is a list of alternating names and values. We use this list to set the environment variables for our process.
#!/usr/bin/env tclsh
package require fileutil
# Execute a bash script and extract some environment variables
proc getBashVar {bashScript varsList} {
# Duplicate the bash script to a temp script
set tempScriptName [fileutil::tempfile getBashVar]
file copy -force $bashScript $tempScriptName
# Append a marker to the end of the script. We need this marker to
# identify where in the output to begin extracting the variables.
# After that append the list of specified varibles and their values.
set f [open $tempScriptName a]
set marker "#XXX-MARKER"
puts $f "\necho \\$marker"
foreach var $varsList {
puts $f "echo $var \\\"$$var\\\" "
}
close $f
# Execute the temp script and parse the output
set scriptOutput [exec bash $tempScriptName]
append pattern $marker {\s*(.*)}
regexp $pattern $scriptOutput all vars
# Set the environment
array set ::env $vars
# Finally, delete the temp script to clean up
file delete $tempScriptName
}
# Test
getBashVar f1.sh {var1 var2}
puts "var1 = $::env(var1)"
puts "var2 = $::env(var2)"

Resources