Sunday, May 28, 2017

Access Windows Environment Variables from within Bash in WSL

I have been using my MacBook a lot now that it is my main computer at work. So much so that I found it necessary to invert my scroll wheel on my mouse on my windows desktop to behave like my MacBook. I've also been using bash a lot more since I can have similar experience between the 2 machines.

While in the process of writing the scripts to configure my bash environment on my Windows machine, I found the need to be able to access environment variables that are set in Windows. With WSL, the only environment variables that really come over to bash is PATH.

I googled around for a bit, but didn't find any way to actually do this. Then I remembered that WSL has interop between Windows and WSL. This means that I can execute a Windows executable and redirect the output back to bash. Which means I should be able to execute powershell.exe to get the information I need.

I first started with a test of just doing:

$ echo $(powershell.exe -Command "gci ENV:")

And that gave me what I wanted back. Now there are some differences in the paths between WSL and Windows, so I knew I would also have to adjust for that.

What I did was put a file called ~/.env.ps1 in my home path.

#!~/bin/powershell
# Will return all the environment variables in KEY=VALUE format
function Get-EnvironmentVariables {
	return (Get-ChildItem ENV: | foreach { "WIN_$(Get-LinuxSafeValue -Value ($_.Name -replace '\(|\)','').ToUpper())=$(Convert-ToWSLPath -Path $_.Value)" })
}

# converts the C:\foo\bar path to the WSL counter part of /mnt/c/foo/bar
function Convert-ToWSLPath {
	param (
		[Parameter(Mandatory=$true)]
		$Path
	)
	(Get-LinuxSafeValue -Value (($Path -split ';' | foreach {
		if ($_ -ne $null -and $_ -ne '' -and $_.Length -gt 0) {
			(( (Fix-Path -Path $_) -replace '(^[A-Za-z])\:(.*)', '/mnt/$1$2') -replace '\\','/')
		}
	} ) -join ':'));
}

function Fix-Path {
	param (
		[Parameter(Mandatory=$true)]
		$Path
	)
	if ( $Path -match '^[A-Z]\:' ) {
		return $Path.Substring(0,1).ToLower()+$Path.Substring(1);
	} else {
		return $Path
	}
}

# Ouputs a string of exports that can be evaluated
function Import-EnvironmentVariables {
	return (Get-EnvironmentVariables | foreach { "export $_;" }) | Out-String
}

# Just escapes special characters
function Get-LinuxSafeValue {
	param (
		[Parameter(Mandatory=$true)]
		$Value
	)
	process {
		return $Value -replace "(\s|'|`"|\$|\#|&|!|~|``|\*|\?|\(|\)|\|)",'\$1';
	}
}

Now in my `.bashrc` I have the following:

#!/usr/bin/env bash

source ~/.wsl_helper.bash
eval $(winenv)

If I run env now, I get output like the following:

WIN_ONEDRIVE=/mnt/d/users/rconr/onedrive
PATH=~/bin:/foo:/usr/bin
WIN_PATH=/mnt/c/windows:/mnt/c/windows/system32

Notice the environment variables that are prefixed with WIN_? These are environment variables directly from Windows. I can now add additional steps to my .bashrc using these variables.

ln -s "$WIN_ONEDRIVE" ~/OneDrive

Additionally, I added a script to my ~/bin folder that is in my path called powershell. This will allow me to make "native" style calls to powershell from within bash scripts.
#!/usr/bin/env bash

# rename to `powershell`
# chmod +x powershell

. ~/.wsl_helper.bash

PS_WORKING_DIR=$(lxssdir)
if [ -f "$1" ] && "$1" ~= ".ps1$"; then
	powershell.exe  -NoLogo -ExecutionPolicy ByPass -Command "Set-Location '${PS_WORKING_DIR}'; Invoke-Command -ScriptBlock ([ScriptBlock]::Create((Get-Content $1))) ${*:2}"
elif [ -f "$1" ] && "$1" ~!= "\.ps1$"; then
	powershell.exe -NoLogo -ExecutionPolicy ByPass -Command "Set-Location '${PS_WORKING_DIR}'; Invoke-Command -ScriptBlock ([ScriptBlock]::Create((Get-Content $1))) ${*:2}"
else
	powershell.exe -NoLogo -ExecutionPolicy ByPass ${*:1}
fi
unset PS_WORKING_DIR

In the powershell file, you will see a call to source a file called .wsl_helper.bash. This script has some helper functions that will do things like transform a path from a Windows style path to a linux WSL path, and do the opposite as well.

#!/usr/bin/env bash
# This is the translated path to where the LXSS root directory is
export LXSS_ROOT=/mnt/c/Users/$USER/AppData/Local/lxss

# translate to linux path from windows path
function windir() {
	echo "$1" | sed -e 's|^\([a-z]\):\(.*\)|/mnt/\L\1\E\2|' -e 's|\\|/|g'
}

# translate the path back to windows path
function wsldir() {
	echo "$1" | sed -e 's|^/mnt/\([a-z]\)/\(.*\)|\U\1\:\\\E\2|' -e 's|/|\\|g'
}

# gets the lxss path from windows
function lxssdir() {
	if [ $# -eq 0 ]; then
		if echo "$PWD" | grep "^/mnt/[a-zA-Z]/" > /dev/null 2>&1; then
			echo "$PWD";
		else
			echo "$LXSS_ROOT$PWD";
		fi
	else
		echo "$LXSS_ROOT$1";
	fi
}

function printwinenv() {
	_winenv --get
}

# this will load the output exports of the windows envrionment variables
function winenv() {
	_winenv --import
}

function _winenv() {
	if [ $# -eq 0 ]; then
		CMD_VERB="Get"
	else
		while test $# -gt 0; do
		  case "$1" in
				-g|--get)
				CMD_VERB="Get"
				shift
				;;
				-i|--import)
				CMD_VERB="Import"
				shift
				;;
				*)
				CMD_VERB="Get"
				break
				;;
			esac
		done
	fi
	CMD_DIR=$(wsldir "$LXSS_ROOT$HOME/\.env.ps1")
	echo $(powershell.exe -Command "Import-Module -Name $CMD_DIR; $CMD_VERB-EnvironmentVariables") | sed -e 's|\r|\n|g' -e 's|^[\s\t]*||g';
}

No comments:

Post a Comment

Access Windows Environment Variables from within Bash in WSL

I have been using my MacBook a lot now that it is my main computer at work. So much so that I found it necessary to invert my scroll wheel on my mouse on my windows desktop to behave like my MacBook. I've also been using bash a lot more since I can have similar experience between the 2 machines.

While in the process of writing the scripts to configure my bash environment on my Windows machine, I found the need to be able to access environment variables that are set in Windows. With WSL, the only environment variables that really come over to bash is PATH.

I googled around for a bit, but didn't find any way to actually do this. Then I remembered that WSL has interop between Windows and WSL. This means that I can execute a Windows executable and redirect the output back to bash. Which means I should be able to execute powershell.exe to get the information I need.

I first started with a test of just doing:

$ echo $(powershell.exe -Command "gci ENV:")

And that gave me what I wanted back. Now there are some differences in the paths between WSL and Windows, so I knew I would also have to adjust for that.

What I did was put a file called ~/.env.ps1 in my home path.

#!~/bin/powershell
# Will return all the environment variables in KEY=VALUE format
function Get-EnvironmentVariables {
	return (Get-ChildItem ENV: | foreach { "WIN_$(Get-LinuxSafeValue -Value ($_.Name -replace '\(|\)','').ToUpper())=$(Convert-ToWSLPath -Path $_.Value)" })
}

# converts the C:\foo\bar path to the WSL counter part of /mnt/c/foo/bar
function Convert-ToWSLPath {
	param (
		[Parameter(Mandatory=$true)]
		$Path
	)
	(Get-LinuxSafeValue -Value (($Path -split ';' | foreach {
		if ($_ -ne $null -and $_ -ne '' -and $_.Length -gt 0) {
			(( (Fix-Path -Path $_) -replace '(^[A-Za-z])\:(.*)', '/mnt/$1$2') -replace '\\','/')
		}
	} ) -join ':'));
}

function Fix-Path {
	param (
		[Parameter(Mandatory=$true)]
		$Path
	)
	if ( $Path -match '^[A-Z]\:' ) {
		return $Path.Substring(0,1).ToLower()+$Path.Substring(1);
	} else {
		return $Path
	}
}

# Ouputs a string of exports that can be evaluated
function Import-EnvironmentVariables {
	return (Get-EnvironmentVariables | foreach { "export $_;" }) | Out-String
}

# Just escapes special characters
function Get-LinuxSafeValue {
	param (
		[Parameter(Mandatory=$true)]
		$Value
	)
	process {
		return $Value -replace "(\s|'|`"|\$|\#|&|!|~|``|\*|\?|\(|\)|\|)",'\$1';
	}
}

Now in my `.bashrc` I have the following:

#!/usr/bin/env bash

source ~/.wsl_helper.bash
eval $(winenv)

If I run env now, I get output like the following:

WIN_ONEDRIVE=/mnt/d/users/rconr/onedrive
PATH=~/bin:/foo:/usr/bin
WIN_PATH=/mnt/c/windows:/mnt/c/windows/system32

Notice the environment variables that are prefixed with WIN_? These are environment variables directly from Windows. I can now add additional steps to my .bashrc using these variables.

ln -s "$WIN_ONEDRIVE" ~/OneDrive

Additionally, I added a script to my ~/bin folder that is in my path called powershell. This will allow me to make "native" style calls to powershell from within bash scripts.
#!/usr/bin/env bash

# rename to `powershell`
# chmod +x powershell

. ~/.wsl_helper.bash

PS_WORKING_DIR=$(lxssdir)
if [ -f "$1" ] && "$1" ~= ".ps1$"; then
	powershell.exe  -NoLogo -ExecutionPolicy ByPass -Command "Set-Location '${PS_WORKING_DIR}'; Invoke-Command -ScriptBlock ([ScriptBlock]::Create((Get-Content $1))) ${*:2}"
elif [ -f "$1" ] && "$1" ~!= "\.ps1$"; then
	powershell.exe -NoLogo -ExecutionPolicy ByPass -Command "Set-Location '${PS_WORKING_DIR}'; Invoke-Command -ScriptBlock ([ScriptBlock]::Create((Get-Content $1))) ${*:2}"
else
	powershell.exe -NoLogo -ExecutionPolicy ByPass ${*:1}
fi
unset PS_WORKING_DIR

In the powershell file, you will see a call to source a file called .wsl_helper.bash. This script has some helper functions that will do things like transform a path from a Windows style path to a linux WSL path, and do the opposite as well.

#!/usr/bin/env bash
# This is the translated path to where the LXSS root directory is
export LXSS_ROOT=/mnt/c/Users/$USER/AppData/Local/lxss

# translate to linux path from windows path
function windir() {
	echo "$1" | sed -e 's|^\([a-z]\):\(.*\)|/mnt/\L\1\E\2|' -e 's|\\|/|g'
}

# translate the path back to windows path
function wsldir() {
	echo "$1" | sed -e 's|^/mnt/\([a-z]\)/\(.*\)|\U\1\:\\\E\2|' -e 's|/|\\|g'
}

# gets the lxss path from windows
function lxssdir() {
	if [ $# -eq 0 ]; then
		if echo "$PWD" | grep "^/mnt/[a-zA-Z]/" > /dev/null 2>&1; then
			echo "$PWD";
		else
			echo "$LXSS_ROOT$PWD";
		fi
	else
		echo "$LXSS_ROOT$1";
	fi
}

function printwinenv() {
	_winenv --get
}

# this will load the output exports of the windows envrionment variables
function winenv() {
	_winenv --import
}

function _winenv() {
	if [ $# -eq 0 ]; then
		CMD_VERB="Get"
	else
		while test $# -gt 0; do
		  case "$1" in
				-g|--get)
				CMD_VERB="Get"
				shift
				;;
				-i|--import)
				CMD_VERB="Import"
				shift
				;;
				*)
				CMD_VERB="Get"
				break
				;;
			esac
		done
	fi
	CMD_DIR=$(wsldir "$LXSS_ROOT$HOME/\.env.ps1")
	echo $(powershell.exe -Command "Import-Module -Name $CMD_DIR; $CMD_VERB-EnvironmentVariables") | sed -e 's|\r|\n|g' -e 's|^[\s\t]*||g';
}