Run command on powershell launch using pwd variable - powershell

When you're in file explorer you can click on File > Open Windows Powershell(or its icon in the Quick Access Toolbar) to start an instance of Powershell in the directory that your file explorer is in. I would like to then automatically run a simple command in this directory and close the Powershell window after it is done.
I have tried adding my command to my Powershell Profile but it executes before the path variable has been set and it runs with $pwd being equal to C:\Users\MyUsername (my home directory) or C:\WINDOWS\system32 (seems to be a race condition of some sort, no idea why it does one or the other). To the best of my understanding this is because the file explorer "open in powershell button" opens powershell and THEN cd's to the directory I was in in file explorer. So when the profile.ps1 is ran it is using the only directories it knows if since the cd call hasn't been made yet. This is similar to running the command start powershell.exe in cmd vs start powershell.exe -command "cd 'C:\wherever'". The former correctly runs my profile command while the latter uses the current directory of cmd and not the C:\wherever.
So, obviously the $pwd variable is being assigned at different times in the case of opening it from cmd and opening it from file explorer. Is there some way to delay the execution of a command in the profile until after the shell has fully loaded? Simply sleeping the script doesn't help.
Alternatively, if anyone knows how to edit the registry so that I can change the behavior of clicking File > Open Windows Powershell (since it must have access to some variable storing the current directory and I assume it calls the Powershell executable with this variable as an argument being cd'd to), that would work too.
Then again I could be incredibly naive about how File > Open Windows Powershell and the Powershell instantiation process works.
Any help is greatly appreciated, thank you!

I figured it out in the most hacky, gross way ever, but without easy access to Windows internals this is the only working method I could find. I set up my powershell profile to make my window title my prompt like so:
function Prompt
{
$host.ui.RawUI.WindowTitle = $(get-location)
“PS> “
}
Then I set up a task in the Task Scheduler that was triggered by powershell reaching its prompt (there are 3 possible hooks, when the console is starting up, when it starts an IPC listening thread, and when the console is ready for input). I used the Event Viewer to do this (I was going to post screenshots but I don't have 10 reputation yet).
Then, I set the action on this task to run the script shown below, which reads from the window title of my first instance of powershell
Start-Sleep -s 1
$A = Get-Process -Name powershell | Where-Object -FilterScript {$_.Id -ne $PID}
$B = $A.MainWindowTitle
& C:\Program` Files\MyProgram\MyProgram.exe "$B"
stop-process -Id $A.Id
stop-process -Id $PID
This whole menagerie of events properly runs my program with the current file explorer directory as an argument (and then closes powershell) when I click the little powershell icon on the quick access toolbar in file explorer.

Found a much cleaner and faster way to do this. All I had to do was set up my profile to look like this, no tasks or second instance of powershell required
function Prompt
{
& C:\Program` Files\MyProgram\MyProgram.exe "$pwd"
stop-process -Id $PID
}

Related

Add environment variables with PowerShell and bat files

As part of a project, I need to run two bat files with a PowerShell script. These bat files will perform several operations including the creation of environment variables.
But problem. The first environment variables are created (those created with the first bat file) but not the second ones (which must be created with the execution of the second bat file). The execution of the second one went "well" because the rest of the operations it has to perform are well done.
I use the same function for the execution of these .bat files.
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine")
$argList = "/c '"$batFile'""
$process = Start-Process "cmd.exe" -ArgumentList $argList -Wait -PassThru -Verb runAd
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine")
I use the line
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine")
to reload the environment variables. But that didn't solve the problem. Without this line I have the environment variables corresponding to the second bat file (the one running second) being created. With it, I have only those of the first one.
I also noticed that if I re-run my PowerShell program (so re-run the batch files), I had all the environment variables created.
Well 1st off:
It looks like you have an error in your code for executing your 2nd batch file so I suspect you re-wrote your code to go here and it's not at all in the original form, as how it's written you would never get anything further.
You know what TL;DR: Try this
I've been writing a lot, and its a rabbit hole considering the snippet of code isn't enough of the process, the code is obviously re-written as it introduces a clearly different bug, and your description leaves something to be desired.
i'll leave some of the other points below re-ordered, and you can feel free to read/ignore whatever.
But here, is the long and short of it.
if you need to run this CMD scripts, and get some stuff out of them to ad to path, have them run normally and echo the path they create into stdout, then capture it in a powershell variable, dedupe it in powershell and set the path directly for your existing powershell environment.
Amend both of your CMD Scripts AKA Batch Files to add this to the very top before any existing lines.
#(SETLOCAL
ECHO OFF )
CALL :ORIGINAL_SCRIPT_STARTS_HERE >NUL 2>NUL
ECHO=%PATH%
( ENDLOCAL
EXIT /B )
:ORIGINAL_SCRIPT_STARTS_HERE
REM All your original script lines should be present below this line!!
PowerShell code basically will be
$batfile_1 = "C:\Admin\SE\Batfile_1.cmd"
$batfile_2 = "C:\Admin\SE\Batfile_2.cmd"
$Path_New = $($env:path) -split ";" | select -unique
$Path_New += $(&$batFile_1) -split ";" | ? {$_ -notin $Path_New}
$Path_New += $(&$batFile_2) -split ";" | ? {$_ -notin $Path_New}
$env:path = $($Path_New | select -unique) -join ";"
Although if you don't need the separate testable steps you could make it more concise as:
$batfile_1 = "C:\Admin\SE\Batfile_1.cmd"
$batfile_2 = "C:\Admin\SE\Batfile_2.cmd"
$env:path = $(
$( $env:path
&$batFile_1
&$batFile_2
) -split ";" | select -unique
) -join ";"
Okay leaving the mostly done stuff where I quit-off trying to amend my points as I followed the rabbit hole tracking pieces around, as it will give some light on some aspects here
2nd off: You do not need to start-process to run a CMD script, you can run a cmd script natively it will automatically instantiate a cmd instance.
Start-Process will spawn it as a separate process sure, but you wait for it, and although you use -PassThru and are saving that as a variable, you don't do anything with it to try to check it's status or error code so you may as well just run the CMD script directly and see it's StdOut in your powershell window, or save it in a variable to log it if needed.
3rd off: Why not just set the environment variables directly using Powershell?
I'm guessing these scripts do other stuff but might be that you should just echo what they want to set Path to back to the PowerShell script and then dedupe it and set the path when done.
4th off: $env:Path is your current environment's path, this includes all pathtext that is from the System AND the currentuser profile (HKLM: and HKCU: registry keys for environment), while $( [System.Environment]::GetEnvironmentVariable("Path","Machine") ) is your System (Local machine) pat has derived from your registry.
5th off: The operational Environment is specific to each shell, when you start a new instance of CMD /c it inherits the environment of the previous cmd instance that spawned it.
6th off: Changes made to environmental variables do not 'persist' ie: you can't open a new CMD / Powershell instance and see them, and once you close that original cmd window they're gone, unless you edit the registry values of these items directoy or use SET X in a cmd session (Which is problematic and should be avoided!) and also ONLY affects the USER variables not the system/local machine variables.
Thus, any changes made to the environment in one CMD instance only operate within that existing shell unless they are changes that persist in the registry, in which case they will only affect new shells that are launched.
7th off: When you launch powershell, it is a cmd shell running powershell, and so powershell inherits whatever the local machine and current user's variables are at that moment when the interpreter is started. This will be what is in $env:xxx
8th off: Setting $env:Path = $([System.Environment]::GetEnvironmentVariable("Path","Machine")) will always change the current powershell environment to whatever is stored in the [HKLM:\] registry key for environmental variables.
Now given that we don't have all of your code and only a description of what is happening.
It appears you have one script Lets call it batFile_1.cmd that is setting some variables in your
For each batch file you run whether spawned implicitly or explicitly you will inherit the previous shell's command environment.
However
Each instance of CMD which you spawn with your batch files within them, spawns a separate cmd shell instance and he Instance of CMD that powershell.exe is running inside of, and thus your script was running in.
Now I'm just supposing what is happening since you only give a small snippet, which is not enough to actually reproduce your real issue.
But it seems like you spawn a cmd script,
So it's hard to know exactly is happening without the full context instead of the snippet, although I'll go into one scenario that might be happening below.
A note on Each CMD instance only inherits the values of it's parent.
(I feel this is much clearer to explain in opening an actual CMD window and test how the shell works by spawning another instance of CMD.
When the new instance is exited the variables revert to the previous instance because they only move from parent to child)
eg:
C:\WINDOWS\system32>set "a=hello"
C:\WINDOWS\system32>echo=%a%
hello
C:\WINDOWS\system32>CMD /V
Microsoft Windows [Version 10.0.18362.1082]
(c) 2019 Microsoft Corporation. All rights reserved.
C:\WINDOWS\system32>(SET "a= there!"
More? SET "b=%a%!a!"
More? )
C:\WINDOWS\system32>echo=%b%
hello there!
C:\WINDOWS\system32>echo=%a%
there!
C:\WINDOWS\system32>exit
C:\WINDOWS\system32>echo=%a%
hello
C:\WINDOWS\system32>echo=%b%
%b%
C:\WINDOWS\system32>
But that isn't really what is happening here you seem to be updating the command environment back to the Local machine

Can I remove the location from my terminal in VSC? [duplicate]

So I've just downloaded Visual Studio Code to use as my default IDE for learning Python. I'm running on a 64-bit machine so I made the default terminal windows powershell.
The place where I'll be saving most of my files is about 8 folders deep which all show up in the terminal before any commands can be written. Is there any way to hide or shorten the file path in the terminal?
As #Biclops suggested, there is good info here: configure PowerShell to only show the current folder in the prompt
However, I needed more basic info to get this to work. This is a very good resource to get started: Windows PowerShell Profiles. So I first followed the steps suggested there:
[always using vscode's integrated terminal using PowerShell]
test-path $profile (is there a profile set up?)
new-item -path $profile -itemtype file -force (assuming the answer to the above is false)
notepad $profile (opens notepad)
paste in (from the SuperUser answer above)
function prompt {
$p = Split-Path -leaf -path (Get-Location)
"$p> "
}
save (you shouldn't have to chose a location, it is already done for you)
reload vscode - you will probably get an error message about running scripts (or just do next step before reload)
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser (at your integrated terminal PS prompt, also from the SuperUser answer)
reload vscode
You should be good to go!
Great Question and Great Answers.
But this information is dated, and Windows is currently updating from Windows 10 to Windows 11. In addition, the base Windows PowerShell has been incorporated into the new Windows Terminal Preview app. which is being used here.
The solution provided above by Mark and Tarruda23 (above) almost works. But Windows throws an error - described below.
The steps:
First, it was necessary to determine whether a profile existed. Using the Windows Explorer, the following path was checked. If a profile already exists, this path shows where an existing profile should be found. On this PC, no profile ( .ps1 ) file existed and this folder was empty. Don't close the Explorer.
C:\Users\prior\OneDrive\Documents\WindowsPowerShell
Since no file exists, a new file needed to be created. This new file must be saved with a specific name - shown below.
Navigate to the empty folder and open PowerShell. The .ps1 profile must be created and saved in this folder. Use the Powershell's build-in text editor to create the new file. Type:
ISE
Then type or paste the following into the empty text file:
function prompt {
$p = Split-Path -leaf -path (Get-Location)
"$p> "
}
Save this file with the following name:
Microsoft.PowerShell_profile.ps1
Use the PowerShell to open Notepad and check that .ps1 file. This demonstrates the Windows system has found the new .ps1. Next close the Notepad.
Notepad $profile
Now the PowerShell is probably displaying an error message in red text. This error message reads in part:
\Microsoft.PowerShell _profile.ps1 cannot be loaded because running scripts is disabled on this system.
Run the PowerShell as the Administrator. Type the following.
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned
Windows will prompt with a question:
Do you want to change the execution policy?
Type y for yes. This will change and remove the Windows default settings that prevents running script files. Once done, this will remove that error message.
All should be good now. PowerShell will now start and run with shorter and abbreviated PS> prompt that shows either the User name, or the name of the folder where the PowerShell is running.

How to run powershell script on computer start-up

I am trying to run a PowerShell script Daily.ps1 on start-up, however, due to administrator settings (I cannot run as admin, that is not an option), I cannot run it through the Task Scheduler. For example, this is the contents of Daily.ps1:
if (1 -eq 1) {
"Hello there!"
}
So I tried to have a batch script Daily.cmd run on start up (through the start-up folder), which runs, but I cannot get it run the Daily.ps1, and I get a message saying running scripts is disabled. (Both files are in the same directory)
powershell C:\Users\Simon\Desktop\Daily.ps1
File C:\Users\Simon\Desktop\Daily.ps1 cannot be loaded because running scripts is disabled on this system
I then tried using this line of code from a trick I learned to bypass running scripts directly:
powershell cat Daily.ps1 | powershell invoke-expression
This works but only for one liners. So I added the -raw flag for
cat, which works when in powershell, but not in CMD. For some reason, Daily.ps1's text is still stored as an array of strings. (apologies for formatting)
cmdlet Invoke-Expression at command pipeline position 1
Supply values for the following parameters:
Command: if (1 -eq 1) {
invoke-expression : At line:1 char:14
if (1 -eq 1) {
Missing closing '}' in statement block or type definition.
At line:1 char:1
invoke-expression ~~~~~~~~~~~~~~~~~
So I tried to add this to Daily.cmd:
powershell
cat -raw Daily.ps1 | powershell-invoke-expression
However, the rest of the script doesn't get executed at all once I enter PowerShell.
I don't know to get Daily.ps1 to run through a batch command. Is there a way I missed, or is one of the ways I tried faulty (without admin rights)?
Edit: To clarify, ExecutionPolicy is set to Restricted, and that cannot be changed. Additionally, I can run PowerShell scripts fine through right clicking the file and running with PS.
Create a scheduled task to run at computer startup. Put powershell.exe in the field "program/script" and -File "C:\path\to\your.ps1" in the field "arguments" (you may want to avoid placing the script in a user profile). Set the task to run whether the user is logged on or not.
I found an answer!
After trying many different methods, I came across this line of code that allows you to run PS scripts if ExecutionProperty is set to restricted:
start powershell "cat -raw C:\Users\Simon\Desktop\Daily.ps1 | invoke-expression"
This runs powershell and uses the trick of piping the results of cat -raw [file.ps1] to invoke-expression. This is useful workaround if ExecutionProperty is set to restricted.
Then you can save this line to a .cmd or .bat file and use either Task Scheduler (more customizability) or put it in the startup folder.
P.S. for everyone who kept saying change the ExecutionProperty to something other than restricted. I clearly stated multiple times that I cannot do that(not admin), nor will the Sys Admin do that, nor will it ever happen(must stay restricted) :)

Is there a way to start commandline processes in a new commandline window from a powershell script?

I am using
Start-Process "<PathToFile>.bat"
For .bat files from a lengthy script in Powershell (v3). However, the commandline window pops up for a moment and is immediately closed and the process that normally runs on the commandline, runs in the background with no indication wether it's finished or if any errors occured.
Is there a way to force the command window to stay open until the user exits the window (after the .bat ran)? I suppose even if there is a way that the command window stays open, the PS script will continue to run in the background?
As said by Alex K CMD /K opens a CMD window and then keeps it open.
If you use CMD /C it will call the file, run the commands/process and then exit.
Sadly, it never worked for me with cmd /c. Since I also needed parameters to be handed over to the programm called from the commandline, I opted to write a temporary proxy bat that held the call to the program and the parameters.
Pseudocode here:
"ProgramName /Parameters" | Out-File DirOfTempBatFile -Encoding ascii
$output = Start-Process DirOfTempBatFile -wait
This calls the program (sqlplus with parameters) with keeping the command window open. Additionally, you can access the output of the sqlplus debug messages via $output.

How to get command prompt's output into a variable with PowerShell

I'm using PowerShell v3.0 to start a new cmd.exe process, in which I then load up the Visual Studio Command Prompt to perform a build, like so:
Start-Process cmd.exe -ArgumentList $cmdArgumentsToRunMsBuildInVsCommandPrompt -WindowStyle $windowStyle -Wait
This works, and it opens a new command prompt window and I can see the build happen, and then when the build is finished the command prompt window closes. I would like to be able to get the text that is written to the command prompt window and store it in a variable, in order to inspect if the build passed or not. I tried using this, but it doesn't work; the $buildOutput variable is empty:
Start-Process cmd.exe -ArgumentList $cmdArgumentsToRunMsBuildInVsCommandPrompt -WindowStyle $windowStyle -Wait -OutVariable buildOutput
Write-Host "Build output = $buildOutput"
This makes sense since the cmd.exe process isn't returning any text; it is just writing it to it's own window. Is there a way for me to be able to store that text in a variable for the original powershell script to use? I know that I can provide a parameter to MsBuild to have it write the build log to a file, but I'm looking for a solution that doesn't involve writing to a log file and having to delete it later.
Any suggestions are appreciated. Thanks!
<# EDIT #>
Thanks for all of the responses so far! A popular suggestion has been to just call MsBuild.exe directly without using cmd.exe. The reason I need to go through cmd.exe is some projects don't build successfully if called directly from MsBuild.exe (e.g. XNA projects). Instead I need to call MsBuild.exe from the Visual Studio Command Prompt so that (I assume) all of the required environmental variables are set. I guess I could just call the VS Command Prompt directly, but it will have the same problem as calling cmd.exe too. If I can't find the VS Command Prompt I fallback to calling MsBuild.exe directly, so those answers are still appreciated.
You can always capture the output of console programs this way:
$output = [string](nuget.exe)
Here I used nuget ($output will contain the available commands list), but you can of course use msbuild.exe with the appropriate arguments.
I've solved my problem using a suggestion from the first comment on my question, which was to write the build output to a log file, consume it, then delete it. This allows me to still show the user the cmd window with the build progress if they need, as well inspect the build output once the build completes. It also still allows me to run the build in another process, so we can use PassThru if we don't want our script to wait for the build to complete before continuing execution.
I've created an Invoke-MsBuild powershell module to make building with MsBuild a snap while providing lots of parameters for additional functionality (returns if build succeeded or failed, can show/hide build window, can wait/not wait for build to finish, can automatically show build log on failed builds, etc.). You can view and download the script from my blog.
$process = New-Object System.Diagnostics.Process;
$process.StartInfo.UseShellExecute = $false;
$process.StartInfo.RedirectStandardOutput = $true;
$process.StartInfo.FileName = "cmd.exe";
$process.StartInfo.Arguments = $cmdArgumentsToRunMsBuildInVsCommandPrompt;
$process.Start();
$outputStream = $process.StandardOutput;
$outputStream.ReadToEnd();
You could also redirect StandardError.
edit: I ended up using #David Brabant's answer
I ran into this problem and created an echo function
function echo()
{
$input
}
which let me do this
$output = &"cmd.exe" $args | echo