Dot Slash and Backslashes in PowerShell Cross-Platform - powershell

I'm writing PowerShell scripts which call other PowerShell scripts with & .\other\script.ps1 but the backslash \ is, I assume, a Windows thing and I want my scripts to work cross-platform.
I've noticed that PowerShell in Windows does accept & ./other/script.ps1 but that may be accidental. Is PowerShell ALWAYS gonna translate my backslashes to the host platform?
What is the best approach to handling path seperators cross-platform in PowerShell?

In my experience Windows PowerShell is happy to accept either \ or / as a path separator so one way to be cross-platform is to always use /.
Alternatively you can use the Path cmdlets to handle building paths for you, for example:
Join-Path -Path $Pwd -ChildPath (Join-Path -Path 'Other' -ChildPath 'script.ps1')
Or to get the path for a file in the current directory:
Resolve-Path test.txt
Path cmdlets:
~> get-command *-path* | Select Name
Name
----
Convert-Path
Join-Path
Resolve-Path
Split-Path
Test-Path

I don't have enough credit to add a comment to the accepted answer by #mark-wragg but I just want to point out I started using forward slashes only in cross-platform scripts etc and hit a problem with symbolic links.
As an example, run
new-item -itemtype SymbolicLink -path TestScript8.ps1 -target ./TestScript.ps1
and you will not be able to open TestScript8.ps1 in VS Code on Windows.

Related

Join-Path AdditionalChildPath parameter deprecated or am I misusing? [duplicate]

I want to generate a string for a file path inside a powershell script. I want this to work in both in windows and mac.
At the moment the code is hardcoded to windows like paths ("\" -> windows, "/" -> unix):
$templatep="$CoreRoot\templates\$serviceName"
I changed this to:
$templatep= Join-Path $CoreRoot "templates" $serviceName
And it works in mac with Powershell 6.0. BUT it doesn't work in my windows server with Powershell 4. I have to do something like this:
$templatep= Join-Path $CoreRoot -ChildPath "templates" | Join-Path -ChildPath $serviceName
Any idea why this is just working in my mac? Is this a new feature in powershell 5 or 6?
I dont't like the having to pipe multiple Join-Paths. Is there a better way to do this?
Thanks!
First, a workaround using the .NET framework:
[IO.Path]::Combine('a', 'b', 'c')
This yields a/b/c on Unix, and a\b\c on Windows, and conveniently supports any number of path components.
Note:
This workaround is only for filesystem paths, whereas Join-Path is designed to work with any PowerShell drive provider's paths.
Make sure that no component other than the first starts with \ (Windows) or / (Unix), because any preceding component is then ignored; e.g., on Windows:
[IO.Path]::Combine('\a', '\b', 'c') # -> '\b\c' - '\a' is ignored(!)
Note that Join-Path does not exhibit this behavior; see this answer for details.
As an alternative to sequencing Join-Path calls with a pipeline you can simply use (...) (a subexpression):
Join-Path a (Join-Path b c) # -> 'a\b\c' (on Windows)
The syntax displayed by Join-Path -? as of Windows PowerShell v5.1.14393.693 (incidental parameters omitted):
Join-Path [-Path] <String[]> [-ChildPath] <String> ...
This syntax implies that invocation Join-Path a b c results in a syntax error in Windows PowerShell, because there is no parameter to bind the c argument to.
By contrast, the syntax displayed in PowerShell (Core) v6+ reveals an additional parameter:
Join-Path [-Path] <String[]> [-ChildPath] <String> [[-AdditionalChildPath] <String[]>]
It is the additional -AdditionalChildPath parameter, which is declared in a manner that collects all remaining positional arguments that (ValueFromRemainingArguments), that makes specifying an arbitrary number of child components work, so that Join-Path a b c indeed works, for instance.
Unfortunately, this enhancement won't be back-ported to Windows PowerShell.
Note that even though [-Path] <String[]> is an array parameter, its purpose is not to accept multiple child path components of a single output path, but to allow joining of multiple parent-child path pairs; e.g.:
$ Join-Path a,b c # same as: Join-Path -Path a,b -ChildPath c
a\c
b\c
Finally, even you can typically get away with hard-coding / as the path separator on both platforms, because many Windows API functions as well as PowerShell's own cmdlets accept \ and / interchangeably.
However, not all utilities may behave this way, so it's generally safer to use the platform-appropriate separator.
For instance, the following works just fine on Windows:
Get-Item c:/windows/system32 # same as: Get-Item c:\windows\system32

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.

Open all files with a certain extension in Powershell

On Linux if I am in a directory and I want to open all .py files with an application (such as atom), I would simply type atom *.py and then all .py files will open with atom. If I type the same thing in Powershell, I receive an error, so I assume the syntax is different on Powershell. How would I accomplish this with Powershell?
Sorry if this is a very beginner question, its my first time using Powershell.
Collect the files you want to open first, then pipe them to the call of your external program:
Get-ChildItem -Path .\* -Include *.py | ForEach-Object {Start-Process -FilePath atom.exe -ArgumentList "`"$($_.FullName)`""}
Adjust the path to your external program if needed and also the the argument list (named arguments instead positional for example). Through the special quoting, this statement is prepared to even handle filenames with spaces.
If this line is to long for your *nix background, you can shrink it to:
gci *.py|%{start atom "`"$($_.FullName)`""}
Try this:
& "Full path of atom.exe" #("*.py")
If atom is set as the default for items with the .py extension you could run gi *.py. Assuming this is not the case i'd right some function as part of a profile script to do this:
$AtomPath = "Path\To\Atom.exe"
function openWithAtom{
Get-ChildItem -Path .\* -Include $args[0] | ForEach-Object {Start-Process -FilePath $AtomPath -ArgumentList "`"$($_.FullName)`""}
}
new-item alias:atom -value openWithAtom
then you would run it with this: atom *.py, or atom filename.py
This tutorial should help with seting up a profile https://www.howtogeek.com/126469/how-to-create-a-powershell-profile/
Also if you haven't already id style your window so it looks more like a terminal.

Change directory in PowerShell

My PowerShell prompt's currently pointed to my C drive (PS C:\>). How do I change directory to a folder on my Q (PS Q:\>) drive?
The folder name on my Q drive is "My Test Folder".
Unlike the CMD.EXE CHDIR or CD command, the PowerShell Set-Location cmdlet will change drive and directory, both. Get-Help Set-Location -Full will get you more detailed information on Set-Location, but the basic usage would be
PS C:\> Set-Location -Path Q:\MyDir
PS Q:\MyDir>
By default in PowerShell, CD and CHDIR are alias for Set-Location.
(Asad reminded me in the comments that if the path contains spaces, it must be enclosed in quotes.)
To go directly to that folder, you can use the Set-Location cmdlet or cd alias:
Set-Location "Q:\My Test Folder"
Multiple posted answer here, but probably this can help who is newly using PowerShell
SO if any space is there in your directory path do not forgot to add double inverted commas "".
You can simply type Q: and that should solve your problem.
Set-Location -Path 'Q:\MyDir'
In PowerShell cd = Set-Location
You can also use the sl command to be able to change directories. It is Set-Location but it is much shorter.
Example:
# Too verbose
Set-Location -Path C:\
# Just the right amount of characters to type
sl C:\
If your Folder inside a Drive contains spaces In Power Shell you can Simply Type the command then drive name and folder name within Single Quotes(''):
Set-Location -Path 'E:\FOLDER NAME'
The Screenshot is attached here
On Powershell use Set-Location instead of cd.
Put path in quotes. Single quotes works for me.
Set-Location 'C:\Program Files\MongoDB\Server\6.0'

Power shell script for copying files with a specific name and a specific extension to a folder from where the script is executing

All,
My intention is to copy all the files with starting with the name 'US.Services' and with the extension .dll from a directory and its sub directories to the place where the script is being executed, i have the following but nothing gets copied. Any help would be appreciated.
Get-Childitem -Path ".\.\" -Filter *US.Services*.dll -Recurse |
Copy-Item -Destination "."
Thanks -Nen
Since PowerShell v3 can use the $PSScriptRoot automatic variable to refer to the location where the script is saved (in PowerShell v2 that would be $here = $MyInvocation.MyCommand.Path | Split-Path.
Be aware the both those approaches work only when the script is executed, if you just paste them to PowerShell console they won't return any value.
If I understand your question correctly you look for files that start with the given string and end with the extension, so you need to use the * wildcard here: US.Services*.dll.
Get-Childitem -Path $PSScriptRoot -Recurse -Filter "US.Services*.dll" |
Copy-Item -Destination $PSScriptRoot
This will likely produce exceptions if there are files with the same name copied to the single directory, as two files cannot be named the same within single directory.