Convert PowerShell script importing within other PowerShell script to EXE [duplicate] - powershell

$password = ConvertTo-SecureString “Password+++” -AsPlainText -Force
$Cred = New-Object System.Management.Automation.PSCredential ("Admin", $password)
$FileLocale = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
Write-Output $FileLocale
$AntFile = "$FileLocale\StartApps.ps1"
Write-Output $AntFile
Start-Process PowerShell.exe -ArgumentList "-command &$AntFile -UserCredential $Cred"
Hi, that code works in .ps1, I call the other script, and he makes his job. But when I transform it in .exe with the help of ps2exe, he doesn't do his job anymore. As admin or not. It's not the first time I use that start-process, but it's the first time I use a variable as a target for the command. Do anyone know what go wrong between the ps1 and exe ?
Thanks

While an executable compiled with ps2exe uses a .ps1 file as input, at runtime no actual .ps1 file is involved, which is why PowerShell's command-reflection variables cannot tell you anything about a running script file.
When running an actual .ps1 file, you'd use the following about_Automatic_Variables:
$PSCommandPath contains the the executing script file's full file path.
$PSScriptRoot contains the script file's full directory path (i.e. the full path of the directory in which the script file is located).
In a ps2exe-compiled executable (.exe), where these variables have no values, you can use the following instead:
[Environment]::GetCommandLineArgs()[0] contains the executable file's full file path.
Split-Path -LiteralPath ([Environment]::GetCommandLineArgs()[0]) contains the executable file's full directory path.
Applied to your code - assuming that a separate StartApp.ps1 file is present alongside your .exe file:[1]
$FileLocale = Split-Path -LiteralPath ([Environment]::GetCommandLineArgs()[0])
$AntFile = Join-Path $FileLocale StartApps.ps1
If you want to make your code work in both invocation scenarios - the original .ps1 file directly as well as the compiled .exe file - use the following:
$FileLocale =
if ($PSScriptRoot) { $PSScriptRoot }
else { Split-Path -LiteralPath ([Environment]::GetCommandLineArgs()[0]) }
$AntFile = Join-Path $FileLocale StartApps.ps1
[1] Note that at runtime no information is available about where the original .ps1 file that served as compile-time input was originally located - only that file's content becomes part of the .exe file.

Related

Powershell script with embedded exe

Is there a way to embed an exe to an existing powershell script? My boss wants me to come up with a way to install software on our employees computers that work from home and aren't tech savvy. Essentially, I need to copy a file locally to their computer (which is an exe) and run it from within powershell (or command line) terminal with some arguments (i.e., /norestart /quiet, etc).
You can use Base64 encoding to embed an exe in a PowerShell script. Run this script to encode the exe. It produces 'base64Decoder.ps1' in the Downloads folder.
# Requires PowerShell 5.1
# Run this script to encode the exe.
# It produces 'base64Decoder.ps1' in the Downloads folder.
$folder = "$env:UserProfile\Downloads\Demo\"
$file = "PowerShell-7.0.0-win-x64.msi"
$option = [System.Base64FormattingOptions]::InsertLineBreaks
$path = Join-Path -Path $folder -ChildPath $file
$bytes = Get-Content $path -Encoding Byte -ReadCount 0
$outputProgram = [System.Text.StringBuilder]::new()
[void]$outputProgram.AppendLine( '$encodedText = #"' )
[void]$outputProgram.AppendLine( ([Convert]::ToBase64String($bytes, $option)) )
[void]$outputProgram.AppendLine( '"#' )
[void]$outputProgram.Append(
#"
`$downloads = Join-Path -Path `$Env:USERPROFILE -ChildPath "Downloads"
`$file = "$file"
`$path = Join-Path -Path `$downloads -ChildPath `$file
`$value = [System.Convert]::FromBase64String(`$encodedText)
Set-Content -Path `$path -Value `$value -Encoding Byte
"#
)
$downloads = Join-Path -Path $Env:USERPROFILE -ChildPath "Downloads"
$outFile = "base64Decoder.ps1"
$outPath = Join-Path -Path $downloads -ChildPath $outFile
Set-Content -Path $outPath -Value ($outputProgram.ToString())
You can copy and paste the contents of base64Decoder.ps1 into an existing PowerShell script to embed the exe. Or, if too large, include base64Decoder.ps1 with the original script and invoke it when necessary.
Run the script on the target computer to reproduce the original file in the Downloads folder. This is valid PowerShell syntax and can be included in a script.
& "$env:UserProfile\Downloads\base64Decoder.ps1"
You might have to set the execution policy before the script will run.
Set-ExecutionPolicy RemoteSigned
Invoke the exe with Start-Process. This can be saved in a script.
Start-Process -FilePath "$env:UserProfile\Downloads\PowerShell-7.0.0-win-x64.msi" -ArgumentList '/? '
If you want to send a PowerShell script via E-mail, attach it as .txt and have them rename it. I'm sure you know that file attachments are generally limited to 10MB.
If the exe is available online, you can use Invoke-WebRequest which is much easier.
Invoke-WebRequest "https://github.com/PowerShell/PowerShell/releases/download/v7.0.0/PowerShell-7.0.0-win-x64.msi" -outfile "$env:UserProfile\Downloads\PowerShell-7.0.0-win-x64.msi"
You can test these steps in Windows Sandbox.
While this is the technically correct answer to your question, I don't recommend it.
First, it is more complicated than simply downloading an installer from the Internet and using (the MSI) switches on it.
Second, the performance of my script is poor for nontrivial exe's. And it will create more problems than it solves.
I'm not sure what the assumption is here. But if these computers are not managed, I imagine there will be a support request for each install. What you won't be able to do is just E-mail this script to 100 people or put it in a logon script and walk away. That would be very bad. Even if this were in the office, I would not deploy an unattended install without thorough testing. And that's assuming local storage and a logon script or equivalent: not people working from home as a one-off.

Install an .MSI file from the same directory as the powershell script without setting a static file path

I have a script that we use that is in Powershell however I need the script to be able to find the files that it needs to install an application dynamically as users can copy the folder to any folder on their computer. Currently I have the below code set, I guess my question is, if the script is in the same folder as the install files. How do I tell powershell to just look in the directory that its being ran from for the install files?
$Install = "C:\Other Folder\File.msi"
$InstallArg = "/quite /norestart"
Start-Process '
-FilePath $Install '
-ArgumentList $InstallArg '
-PassThru | Wait-Process
Any help would be appreciated. Thank you.
Update, I found that I have to be in the directory the script is in. However since we have to run ISE with admin credentials it automatically defaults to C:\Windows\System32 as the directory powershell is looking in regardless if I tell it to open the script. If that is the case how can I tell it to look where the script is located so that it can find the files that it needs?
I have found my answer below is how I got it to work with our current situation. Thank you Thomas for the help!
$ScriptLocation = Get-ChildItem -Path C:\Users -Filter Untitled2.ps1 -Recurse | Select Directory
cd $ScriptLocation.Directory
$Install = ".\Install.msi"
$InstallArg = "/quite /norestart"
Start-Process '
-FilePath $Install '
-ArgumentList $InstallArg '
-PassThru | Wait-Process
Define a relative path:
$Install = ".\File.msi"
If you are not running the script inside the directory, where the script itself and the executable are stored, you will have to determine the absolute path to it. With PowerShell 3+ you can easily determine the directory, where your script is stored, using the $PSScriptRoot variable. You could define your $Install variable like this:
$Install = Join-Path -Path $PSScriptRoot -ChildPath "File.msi"
Thank you so much for your help everyone! I accidentally stumbled upon the perfect solution.
# Set active path to script-location:
$path = $MyInvocation.MyCommand.Path
if (!$path) {
$path = $psISE.CurrentFile.Fullpath
}
if ($path) {
$path = Split-Path $path -Parent
}
Set-Location $path

Powershell Script "$dirs"

So I do not have a degree or any formal training in any programming language but my job has had me slowly learn the basics of SQL and I have now been given a new task at work. The previous person in charge of this task ran powershell scripts to combine and rename PDFs. I get the macro level of how this all works. The script sets a loop through the parent directory into all the children directory concatenates the PDFs using PDFtk Server then renames the combined PDFs to child directory they are named in. However, I cannot figure out how to specify the dirs (I think thats the term). As it stands now I can only successfully run the powershell script in the folder in which Console2 is located.
$path = Split-Path -parent $MyInvocation.MyCommand.Definition
Function mergeFiles
{
# Loop through all directories
$dirs = dir $path -Recurse | Where { $_.psIsContainer -eq $true }
$cmd = 'C:\Program Files (x86)\PDFtk Server\bin\pdftk.exe'
$In1 = 'A.pdf'
$In2 = 'B.pdf'
$Out1 = 'C.pdf'
Foreach ($dir In $dirs)
This is the first part of the merge files function. Can someone help me figure out how to identify a specfic "dirs"? (Like if I had the PDF in a folder on my desktop)
The location that is populating the directory it will search based on the location of your script, based on this line:
$path = Split-Path -parent $MyInvocation.MyCommand.Definition
$MyInvocation.MyCommand.Definition is the full path to the running script and the command Split-Path -parent will return the parent directory. You could just change that line to be the the location you want i.e.:
$path = 'C:\Users\JC\Desktop\PDF'
but you probably don't want to hardcode that path. What you want to do is add the path as an input argument to the script. To do that, add the following to the top of your script:
PARAM($path)
Then when you invoke your script, you just pass the path you are interested in:
.\theScript.ps1 C:\Users\JC\Desktop\PDF
You can then get more advanced and specify a default value, for example, if you want the default to be the location the script is run:
PARAM($path = '.')

Using PSScriptRoot with Import-CSV

As detailed in:
PowerShell-Import-Local-CSV
$empCsv = Import-Csv "$PSScriptRoot\addEmp.csv"
Is classed as an answer. This doesn't work for me when i run this code inside a function. How can i test this?
You can test your Powershell version with $PSVersionTable.PSVersion.
If it is below version 3.0 you can use:
Split-Path $script:MyInvocation.MyCommand.Path -Parent
to find the directory the current script is running from instead of $PSScriptRoot.
$PSScriptRoot will provide the path for the currently running script/function, so if a script runs a function from another location, $PSScriptRoot will contain that other location.
If you want to get the path of the script that's calling the function, leverage $MyInvocation:
$empCsv = Import-Csv "$(Split-Path $MyInvocation.ScriptName)\addEmp.csv"
Demo
Create the Test.psm1 file.
Create the Caller.ps1 file in a subfolder.
Run Caller.ps1.
$PSScriptRoot will refer to Test.psm1 location, $MyInvocation will refer to Caller.ps1 location.
Test.psm1:
Function Test{
Write-host "PSScriptRoot: $PSScriptRoot"
Write-host "MyInvocation: $(Split-Path $MyInvocation.ScriptName)"
}
Caller.ps1:
Import-Module ..\Test.psm1 -Force
Test

How to get the current directory of the cmdlet being executed

This should be a simple task, but I have seen several attempts on how to get the path to the directory where the executed cmdlet is located with mixed success. For instance, when I execute C:\temp\myscripts\mycmdlet.ps1 which has a settings file at C:\temp\myscripts\settings.xml I would like to be able to store C:\temp\myscripts in a variable within mycmdlet.ps1.
This is one solution which works (although a bit cumbersome):
$invocation = (Get-Variable MyInvocation).Value
$directorypath = Split-Path $invocation.MyCommand.Path
$settingspath = $directorypath + '\settings.xml'
Another one suggested this solution which only works on our test environment:
$settingspath = '.\settings.xml'
I like the latter approach a lot and prefer it to having to parse the filepath as a parameter each time, but I can't get it to work on my development environment. What should I do? Does it have something to do with how PowerShell is configured?
Yes, that should work. But if you need to see the absolute path, this is all you need:
(Get-Item .).FullName
The reliable way to do this is just like you showed $MyInvocation.MyCommand.Path.
Using relative paths will be based on $pwd, in PowerShell, the current directory for an application, or the current working directory for a .NET API.
PowerShell v3+:
Use the automatic variable $PSScriptRoot.
The easiest method seems to be to use the following predefined variable:
$PSScriptRoot
about_Automatic_Variables and about_Scripts both state:
In PowerShell 2.0, this variable is valid only in script modules (.psm1). Beginning in PowerShell 3.0, it is valid in all scripts.
I use it like this:
$MyFileName = "data.txt"
$filebase = Join-Path $PSScriptRoot $MyFileName
You can also use:
(Resolve-Path .\).Path
The part in brackets returns a PathInfo object.
(Available since PowerShell 2.0.)
Try :
(Get-Location).path
or:
($pwd).path
Path is often null. This function is safer.
function Get-ScriptDirectory
{
$Invocation = (Get-Variable MyInvocation -Scope 1).Value;
if($Invocation.PSScriptRoot)
{
$Invocation.PSScriptRoot;
}
Elseif($Invocation.MyCommand.Path)
{
Split-Path $Invocation.MyCommand.Path
}
else
{
$Invocation.InvocationName.Substring(0,$Invocation.InvocationName.LastIndexOf("\"));
}
}
Get-Location will return the current location:
$Currentlocation = Get-Location
I like the one-line solution :)
$scriptDir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
Try this:
$WorkingDir = Convert-Path .
In Powershell 3 and above you can simply use
$PSScriptRoot
If you just need the name of the current directory, you could do something like this:
((Get-Location) | Get-Item).Name
Assuming you are working from C:\Temp\Location\MyWorkingDirectory>
Output
MyWorkingDirectory
Most answers don't work when debugging in the following IDEs:
PS-ISE (PowerShell ISE)
VS Code (Visual Studio Code)
Because in those the $PSScriptRoot is empty and Resolve-Path .\ (and similars) will result in incorrect paths.
Freakydinde's answer is the only one that resolves those situations, so I up-voted that, but I don't think the Set-Location in that answer is really what is desired. So I fixed that and made the code a little clearer:
$directorypath = if ($PSScriptRoot) { $PSScriptRoot } `
elseif ($psise) { split-path $psise.CurrentFile.FullPath } `
elseif ($psEditor) { split-path $psEditor.GetEditorContext().CurrentFile.Path }
For what it's worth, to be a single-line solution, the below is a working solution for me.
$currFolderName = (Get-Location).Path.Substring((Get-Location).Path.LastIndexOf("\")+1)
The 1 at the end is to ignore the /.
Thanks to the posts above using the Get-Location cmdlet.
this function will set the prompt location to script path, dealing with the differents way to get scriptpath between vscode, psise and pwd :
function Set-CurrentLocation
{
$currentPath = $PSScriptRoot # AzureDevOps, Powershell
if (!$currentPath) { $currentPath = Split-Path $pseditor.GetEditorContext().CurrentFile.Path -ErrorAction SilentlyContinue } # VSCode
if (!$currentPath) { $currentPath = Split-Path $psISE.CurrentFile.FullPath -ErrorAction SilentlyContinue } # PsISE
if ($currentPath) { Set-Location $currentPath }
}
You would think that using '.\' as the path means that it's the invocation path. But not all the time. Example, if you use it inside a job ScriptBlock. In which case, it might point to %profile%\Documents.
This is what I came up with. It's an array including multiple methods of finding a path, uses the current location, filters out null\empty results, and returns the first not-null value.
#((
($MyInvocation.MyCommand.Module.ModuleBase),
($PSScriptRoot),
(Split-Path -Parent -Path $MyInvocation.MyCommand.Definition -ErrorAction SilentlyContinue),
(Get-Location | Select-Object -ExpandProperty Path)
) | Where-Object { $_ })[0]
To only get the current folder name, you can also use:
(Split-Path -Path (Get-Location) -Leaf)
To expand on #Cradle 's answer: you could also write a multi-purpose function that will get you the same result per the OP's question:
Function Get-AbsolutePath {
[CmdletBinding()]
Param(
[parameter(
Mandatory=$false,
ValueFromPipeline=$true
)]
[String]$relativePath=".\"
)
if (Test-Path -Path $relativePath) {
return (Get-Item -Path $relativePath).FullName -replace "\\$", ""
} else {
Write-Error -Message "'$relativePath' is not a valid path" -ErrorId 1 -ErrorAction Stop
}
}
I had similar problems and it made me a lot of trouble since I am making programs written in PowerShell (full end user GUI applications) and I have a lot of files and resources I need to load from disk.
From my experience, using . to represent current directory is unreliable. It should represent current working directory, but it often does not.
It appears that PowerShell saves location from which PowerShell has been invoked inside ..
To be more precise, when PowerShell is first started, it starts, by default, inside your home user directory. That is usually directory of your user account, something like C:\USERS\YOUR USER NAME.
After that, PowerShell changes directory to either directory from which you invoked it, or to directory where script you are executing is located before either presenting you with PowerShell prompt or running the script. But that happens after PowerShell app itself originally starts inside your home user directory.
And . represents that initial directory inside which PowerShell started. So . only represents current directory in case if you invoked PowerShell from the wanted directory. If you later change directory in PowerShell code, change appears not to be reflected inside . in every case.
In some cases . represents current working directory, and in others directory from which PowerShell (itself, not the script) has been invoked, what can lead to inconsistent results.
For this reason I use invoker script. PowerShell script with single command inside:
POWERSHELL.
That will ensure that PowerShell is invoked from the wanted directory and thus make . represent current directory. But it only works if you do not change directory later in PowerShell code.
In case of a script, I use invoker script which is similar to last one I mentioned, except it contains a file option:
POWERSHELL -FILE DRIVE:\PATH\SCRIPT NAME.PS1.
That ensures that PowerShell is started inside current working directory.
Simply clicking on script invokes PowerShell from your home user directory no matter where script is located.
It results with current working directory being directory where script is located, but PowerShell invocation directory being C:\USERS\YOUR USER NAME, and with . returning one of these two directories depending on the situation, what is ridiculous.
But to avoid all this fuss and using invoker script, you can simply use either $PWD or $PSSCRIPTROOT instead of . to represent current directory depending on weather you wish to represent current working directory or directory from which script has been invoked.
And if you, for some reason, want to retrieve other of two directories which . returns, you can use $HOME.
I personally just have invoker script inside root directory of my apps I develop with PowerShell which invokes my main app script, and simply remember to never ever change current working directory inside my source code of my app, so I never have to worry about this, and I can use . to represent current directory and to support relative file addressing in my applications without any problems.
This should work in newer versions of PowerShell (newer than version 2).
Mine was a short, so unplug everything but USB from it and recompile