How can i use a cmd variable in a powershell command? - windows

I have created a simple .bat file. In this batch file i have a variable named urlExample which is equal to "example.com".
In the same batch file i want to use this variable urlExample in a powershell command.
Specifically, consider the following code:
#echo off
set urlExample = "example.com"
powershell -ExecutionPolicy Bypass -Command "& {$WebClient = New-Object System.Net.WebClient;$WebClient.DownloadFile($urlExample,"C:\.....somePath")
How could i achieve using the urlExample inside the WebClient command?
P.s. I don't want to simply put the url in the DownloadFile's first argument. I want to pass it with a batch variable.
Thanks in advance

In cmd.exe, all variables are also environment variables, such as %urlExample% in your case, and child processes - such as a call to powershell.exe, the Windows PowerShell CLI, inherit environment variables.
By contrast, PowerShell also has shell(-only) variables (e.g., $urlExample, limited to that session only), whereas environment variables must be accessed via the env namespace (e.g. $env:urlExample - see the conceptual about_Environment_Variables help topic).
While you can use string interpolation on the cmd.exe side to "bake in" the values of cmd.exe-defined environment variables, by embedding %urlExample% in the -Command argument, it is more robust to let PowerShell access the environment variable, by referencing $env:urlExample.
Therefore:
#echo off
:: Note: No spaces around "=", double-quote the name *and* the value.
set "urlExample=example.com"
:: Note the reference to $env:urlExample
:: Embedded " chars. are escaped as \"
powershell -ExecutionPolicy Bypass -Command "$WebClient = New-Object System.Net.WebClient; $WebClient.DownloadFile($env:urlExample, \"C:\.....somePath\")"
Note:
-ExecutionPolicy Bypass isn't strictly needed in this case, given that no execution of a script file is (.ps1) is involved here (whether directly with -File or indirectly, as part of a -Command argument).
However, given that the effective execution policy also applies in less obvious scenarios when you use -Command (the default parameter of powershell.exe)[1] - such as (possibly implicitly) loading a module that is either a script module (*.psm1) and / or contains formatting / type-definition data (*.ps1xml) - using -ExecutionPolicy Bypass is a good habit to form to ensure predictable execution, assuming that you trust the code you're invoking.
As Compo points out, another good habit to form to ensure a predictable execution environment is to use -NoProfile, which bypasses loading of PowerShell's profile files. In addition to preventing potentially unnecessary / unwanted modifications of the execution environment by the profiles, bypassing profile loading also speeds up the command.
[1] Note that pwsh, the PowerShell (Core) CLI, now defaults to -File.

Related

Batch script: Pass returned GUID as URL parameter for desktop shortcut

I'm trying to create desktop shortcuts to a private page we work with that will open in Edge, direct to a specific URL, and pass the GUID as a URL parameter.
I've tried the following but as you can expect, only the string "powershell" is passed on to the URL, not the returned GUID.
SET a=powershell -Command "[guid]::NewGuid().ToString()"
C:\Windows\System32\cmd.exe /c start msedge "https://www.website.com/page?user="%a% --no-first-run
How can I replace the %a% portion of the URL with the returned contents of the system GUID?
powershell -Command "[guid]::NewGuid().ToString()"
Note:
This answer addresses the question as asked: it captures the output from a PowerShell command in a batch-file variable for later use in the same batch file.
Alternatively, the specific task at hand can also be performed in full by a single PowerShell command, as shown in zett42's helpful answer.
Batch files (executed by cmd.exe) have no concept of a what is known as command substitution in POSIX-compatible shells (a feature that PowerShell itself provides too, though it has no official name there): the ability to assign a command's output to a variable.[1]
Instead, you must use a for /f loop to capture command output in a variable (which generally loops over each output line, but in your case there is only one output line):
#echo off
setlocal
:: Capture the output from a PowerShell command in variable %guid%, via
:: a for /f loop:
for /f "usebackq delims=" %%a in (`powershell -Command "[guid]::NewGuid().ToString()"`) do set "guid=%%a"
:: Note: No need for `cmd /c` from a batch file to use `start`
start "" msedge "https://www.website.com/page?user=%guid%" --no-first-run
Note: setlocal, while not strictly necessary, localizes any variable definitions to the batch file at hand.
Run for /? in a cmd.exe session for help.
This answer discusses using for /f to capture command output in more detail; notably:
usebackq isn't strictly necessary here, but is generally advisable to give you the freedom to use both ' and " quoting in the command line being invoked.
Similarly, delims= isn't strictly necessary here, since the output by definition contains no spaces, but it is generally advisable if the intent is to capture an output line in full.
The "" as the first start argument isn't strictly necessary here, but in general it is useful when invoking applications whose paths must be double-quoted. Without "" as the first argument, a double-quoted application path would be interpreted as starts window-title argument (which only meaningfully applies to console applications).
[1] In POSIX-compatible shells, use $(...) (e.g, captured="$(whoami)"). In PowerShell, you can use the command -as-is as the RHS of the assignment (e.g., $captured = whoami)
It is possible to do all of this directly using a PowerShell one-liner:
powershell -noprofile -command start msedge \"https://www.website.com/page?user=$(New-Guid) --no-first-run\"
Passing -noprofile to powershell.exe is most of the time a good idea to reduce startup time and provide a more predictable environment as no user profile will be loaded.
start is an alias for the Start-Process command.
Here start gets passed two positional arguments, the name of the process to start (-FilePath parameter) and the process's arguments as a single string (-ArgumentList parameter). Therefore, the 2nd argument must be quoted. To pass the quotes from the command processor cmd.exe through to PowerShell, they must be backslash-escaped.
Within the process's parameter string, the subexpression operator $(…) is used to call the New-Guid command inline and convert it to a string (by implicitly calling the .ToString() method of the Guid object it returns).
If you actually need to use the GUID as a variable in other parts of your batch script (which is not clear from the question), then this helpful answer provides a solution.

How to start another PowerShell session in a separate window while keeping the environment?

While working in PowerShell I tend to quickly switch to admin mode by typing
Start-Process wt -Verb runas
When I do so, a new window appears (sadly, no sudo in Windows). In that new session however, the environment is totally fresh. Is it possible to keep variables, aliases, working dir and all other stuff of similar matter while jumping to a new window? If not, then well, it's a valid answer.
To give some example, I am looking for this behavior:
First window
C:\test> $x = 123
C:\test> Start-Process wt
New window
C:\test> $x
123
By (security-minded) design, elevated sessions (-Verb RunAs) do not inherit the caller's environment variables.
Also, wether or not you use -Verb RunAs, the state of a PowerShell session (aliases, functions, current location, ...) is never inherited when you launch another PowerShell process, such as with Start-Process.
You can work around the issue by explicitly and selectively redefining the state of interest via commands executed in the elevated session, based on values from the caller's state, but that is quite cumbersome and has limitations, as the following example shows:
# Define a few things to copy to the elevated session.
$x1 = 123
$x2 = '3" of snow' # !! See the caveat re regular variables below.
$env:foo = 1
$env:foo2 = 2
Set-Alias bar Get-Date
function baz { "hello, world" }
# Note: The following only copies the definitions above.
# You could try to copy ALL such definitions, by omitting a target name / pattern:
# Get-ChildItem env:
# Get-ChildItem function:
# Get-ChildItem alias:
# CAVEAT: This will NOT generally work with *regular variables*.
Start-Process -Verb RunAs powershell #"
-NoExit -Command Set-Location -LiteralPath \"$((Get-Location -PSProvider FileSystem).ProviderPath)\"
$(Get-Variable x? | ForEach-Object { "`${$($_.Name)} = $(if ($_.Value -is [string]) { "'{0}'" -f ($_.Value -replace "'", "''" -replace '"', '\"') } else { $_.Value }); " })
$(Get-ChildItem env:foo* | ForEach-Object { "Set-Item \`"env:$($_.Name)\`" \`"$($_.Value -replace '"', '\"\"')\`"; " })
$(Get-ChildItem function:bar | ForEach-Object { "`$function:$($_.Name) = \`"$($_.Definition -replace '"', '\"\"')\`"; " })
$(Get-ChildItem alias:baz | ForEach-Object { "`$alias:$($_.Name) = \`"$($_.Definition)\`"; " })
"#
Important:
I've omitted the call to Windows Terminal (wt.exe), as that would create another PowerShell session, which means that only the following definitions would be preserved for that session:
Environment variables.
The current location (working directory), IF its default shell is configured to use the parent process' working directory. Alternatively, and more predictably, pass the working dir. explicitly with the -d option: wt.exe -d \"$((Get-Location -PSProvider FileSystem).ProviderPath)\"
If that is enough, you can remove the commands that preserve aliases, functions, and regular variables, add -WindowStyle Hidden to Start-Process, remove -NoExit before -Command in the argument list, and add a wt.exe call at the bottom.
Preserving the other types of definitions requires working directly in the elevated powershell session, which will invariably use a regular (conhost.exe) console window, however.
In general, it's best to place definitions that should be available in both regular and elevated sessions in your $PROFILE file.
Complementarily, see this answer for convenience function Enter-AdminPSSession, which allows you to pass a script block to execute in the elevated session, to which you can pass values from the caller's state as arguments.
Note:
The above uses the Windows PowerShell CLI, powershell.exe. To use PowerShell (Core) 7+ instead, substitute pwsh.exe.
The above covers preserving the current file-system location, environment variables, aliases, and functions in a generic fashion.
Caveat: By contrast, preserving regular variables is limited to strings and numbers - in essence, instances of those data types whose stringified representation is recognized as such when interpreted as a source-code literal.
With nontrivial additional effort, supporting more data types is possible, by using Base64 encoding with the CLI's -EncodedCommand and -EncodedArguments parameters as shown in this answer, but the range of types that can be represented with type fidelity is fundamentally limited by PowerShell's XML-based serialization infrastructure - see this answer.
You can not keep variables, you will lose them immediately after the new window is created, the best you can do instead is to create a script containing all your activities then save it in the same working directory.
When you open a new window just call your script that will be able to give you the same information as in the other window.

Creating a GitHub Action which runs a Powershell script on a Windows host

My GitHub Actions workflow "runs-on" windows-latest. I want a custom Action which executes a PowerShell (core or legacy is fine) script. I have a parallel action that runs on Linux and MacOS. So, my .github/actions/install-tf-win directory contains this action.yml
name: install_tf_win
description: installs Terraform/TerraGrunt for Windows
runs:
using: "composite"
steps:
- run: install-tf-win\install-tf-win.ps1
shell: pwsh
The directory also contains install-tf-win.ps1. I have tried all sorts of variations on that run statement. Starting with "&" and without, variations in paths used with forwards and backwards slashes. I originally started with $GITHUB_ACTION_PATH/install-tf-win.ps1 (works for Linux/MacOS), however it seemed that GITHUB_ACTION_PATH was getting evaluated to be an empty string and then there were complaints about /install-tf-win.ps1 not being found. I tried both pwsh and powershell for the shell key value.
The form shown above results in this error:
Run ./.github/actions/install-tf-win
install-tf-win\install-tf-win.ps1: D:\a\_temp\c1d3d7fa-074b-4f90-ade0-799dcebd84ec.ps1:2
Line |
2 | install-tf-win\install-tf-win.ps1
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| The module 'install-tf-win' could not be loaded. For more information, run 'Import-Module
| install-tf-win'.
I can obviously code my way around this by just putting the PowerShell statements in a step. But, the documentation suggests this should work. https://docs.github.com/en/free-pro-team#latest/actions/creating-actions/metadata-syntax-for-github-actions#runsstepsshell .
So, how do I get this to work, please?
The error message implies that you cannot refer to your script with a relative path:
The command is passed to pwsh, PowerShell [Core]'s CLI, via its -Command (-c) parameter, which interprets the install-tf-win part of
install-tf-win\install-tf-win.ps1 as a module name rather than as a subdirectory in the absence of an actual subdirectory by that name on Windows[1], so the implication is that such a path doesn't exist.
The linked documentation, suggests that you need an absolute path, based on the GITHUB_ACTION_PATH environment variable, which in PowerShell must be referenced as $env:GITHUB_ACTION_PATH (untested):
# ...
- run: '& $env:GITHUB_ACTION_PATH/install-tf-win/install-tf-win.ps1'
shell: pwsh
Note:
The need to use &, the call operator, which is a syntactic necessity due to the script path containing a(n environment-)variable reference; the same would apply if the path were quoted - see this answer for background information.
Since & is a metacharacter in YAML when used unquoted at the start of a value, the entire string is quoted. Single-quoting ('...') is employed in this case, so that YAML doesn't interpret the contents of the string up front.
As an aside: The implication of '...'-quoting working (as confirmed by Kevin, the OP) is that when pwsh -c is ultimately called on Windows, the string's content is (properly) double-quoted ("..."), because PowerShell's CLI only recognizes " as having syntactic function for command-line argument parsing. By contrast, a '...'-quoted -c argument would be interpreted as a verbatim string rather than as a command, causing its content to be simply echoed.
[1] How PowerShell interprets a path such as foo\bar.ps1 when executed as a command, as of PowerShell 7.1
Interpreted as a command - both inside a PowerShell session and via the -Command / -c parameter of the PowerShell CLI, as used by GitHub Actions - the form foo\bar.ps1 is ambiguous:
It could refer to a module named foo, and its bar.ps1 command (even though module commands don't have .ps1 name extensions).
It could refer to a subdirectory foo and file bar.ps1 in it.
This applies to Windows only:
PowerShell first interprets the path as a file-system path.
If the path doesn't exist, it falls back to interpreting it as a module-command reference.
On Unix-like platforms - even though PowerShell generally allows you to use \ and / interchangeably as the path separator on all supported platforms - this particular path form is always interpreted as a module reference; in other words: you cannot use this syntax form to invoke a script this way (see below).
This surprising inconsistency is discussed in GitHub issue #14307.
There are two ways to avoid this ambiguity, which work on all supported platforms; the following cause PowerShell to unequivocally treat the path as file-system script path:
Use / instead of \: foo/bar.ps1
Prefix the path with .\ (or ./): .\foo\bar.ps1
(Of course, you could also use a full path.)
Note that when the -File parameter of PowerShell's is used (rather than -Command / -c), this ambiguity doesn't arise, as the argument is then always considered a (potentially relative) file-system path, even on Unix-like platforms, irrespective of whether you use \ or /.

Open a command file with Windows PowerShell running it directly

I want to make a file having Windows Powershell commands. Then I want to open it with windows powershell directly and without pressing any key I want windows powershell start running those commands directly same as command prompy I can make .cmd or .bat file.
For example:
These are two commands or Powershell, I want to save this file. Then I want directly execute this file by powershell. I have tried to save it as ps1 and ps2 extension as well but not working. Many methods online are not working. Any solution?
PowerShell script files, across all versions, use the .ps1 filename extension.
From within PowerShell, you can invoke them directly, e.g., .\script.ps1
Note that, unlike in cmd.exe, you must use .\ (or a full path) in order to execute a file located in the current directory - just script.ps1 won't work - see this answer for background information.
From cmd.exe, you must use PowerShell's CLI (powershell.exe in Windows PowerShell / pwsh in PowerShell [Core] v6+) in order to execute a script file:
powershell.exe -File script.ps1
pwsh -File script.ps1 (-File may be omitted)
Note that with -File the .\-prefix is not required.
However, if you use -Command (-c) instead (which is the default with powershell.exe, whereas pwsh now defaults to -File), you do need the .\, because the -Command argument(s) are interpreted as a piece of PowerShell code, i.e. as if you had submitted it inside a PowerShell session.
You've discovered this in your own answer, where you pass a PowerShell command directly to the (implied) -Command parameter.
Note, however, that it's better to double-quote such commands, so as to prevent cmd.exe from interpreting certain characters itself, which breaks the call.
For instance, the following call would break, if you didn't enclose the -Command (-c) argument in "...":
# From cmd.exe; "..." required.
C:\>powershell.exe -c "Write-Output 'a & b'"
a & b
Another important consideration is that you need to escape embedded " chars. as \" for the CLI (even though PowerShell-internally you would use `" or ""):
# From cmd.exe; note the inner " escaped as \"
C:\>powershell.exe -c "Write-Output \"hi there\""
hi there
I have found the solution. I use command powershell.exe and can directly execute powershell commands within cmd.
powershell.exe $MyVariable=Get-Content .\Path.txt
is working fine for me

How to escape schtasks /tr arguments

I need to schedule a PowerShell task like following:
powershell.exe -file ..\Execute\execute.ps1
And I have tried assigning it to an argument $Argument then pass it to schtasks like following:
$Argument = "powershell.exe -file '..\Execute\execute.ps1'"
schtasks /create /tn "SOE_Checks" /tr $Argument /sc DAILY /st 05:00 /ru "System" /rl HIGHEST /f
but after running above code, nothing happened - while the task is created successfully, it appears not to run.
I have also tried assigning it to $Argument without the quotes, it worked but I got the following warnings:
ERROR: Invalid syntax. Value expected for '/tr'.
Type "SCHTASKS /CREATE /?" for usage.
Can anyone please let me know what I have done wrong here? (I am aware that I can accomplish this using PowerShell's New-ScheduledTaskAction but I want it to work this way)
Just want to add that if I change the file path to a specific location in $Argument like this, $Argument = "powershell.exe -file 'C:\SOE\Execute\execute.ps1'", it works fine without any warnings but this is not ideal.
I have read this but it does not work for me
Scheduled tasks created with schtasks.exe execute with the working directory set to $env:windir\system32[1], so unless your script happens to be located in ..\Execute\execute.ps1 relative to there, your command won't work as intended.
If you don't want to hard-code the script path directly into the command, construct the command dynamically, by resolving the relative path to an absolute one when you assign to $Argument:
$Argument = 'powershell.exe -file \"{0}\"' -f (Convert-Path ..\Execute\execute.ps1)
Note the - unfortunate - need to escape the embedded " as \", which is longstanding bug that hasn't been fixed for the sake of backward compatibility - see this GitHub docs issue for background.
Convert-Path resolves a relative path to an absolute one.
Note that the relative path must refer to an existing file (or directory).
Similarly, relative paths inside your script will be relative to $env:windir\system32 too; to make them relative to the script's directory, change to your script's directory first by executing Set-Location $PSScriptRoot at the start of your script.
Optional reading: How to quote commands that run from a scheduled task:
Note: Virtually the same rules apply as when running a command from the Windows Run dialog (press WinKey+R), which you can use to test-drive a command (the command to pass to schtasks /tr, without outer quoting, not the whole schtasks command line) - though note that the working directory will be the user's home directory, and that you won't be able to use '...'-quoting around the PowerShell CLI's -File argument - see below):
cmd.exe is NOT involved during execution, which means:
You needn't worry about non-double-quoted use of cmd.exe metacharacters such as &, for instance, so you can use these characters even in single-quoted strings passed to the PowerShell CLI powershell.exe as (part of) the -Command argument(s).
Conversely, output redirections (e.g., > c:\path\to\log.txt) are not directly supported.
In the context of invoking the PowerShell CLI, this means:
With -File, you cannot use them on the command line and must instead perform them from within your script.
With -Command, however, you can use them, because it is then PowerShell that applies them (but note that Windows PowerShell's > operator creates UTF-16LE files).
(Even though cmd.exe isn't involved) references to environment variables using the same syntax form as in cmd.exe are expanded (e.g., %USERNAME%)
Caveat: You cannot escape such references:
%% doesn't work - the additional % is simply treated as a literal, and expansion still occurs; e.g., %%OS%% results in %Windows_NT%.
^% (accidentally) prevents expansion, but retains the ^ - the ^ doesn't escape; rather, it "disrupts" the variable name, in which case the token is left as-is; e.g., ^%OS^% results in ^%OS^%, i.e., is retained as-is.
The above applies to the commands as they must end up defined inside a scheduled task, as you would see or define them interactively in Task Scheduler (taskschd.msc).
Additionally, for creating a scheduled task from the command line / a PowerShell script / a batch file:
you must quote the command as a whole and
comply with the syntax rules of the calling shell regarding escaping and up-front string interpolation.
(You can only get away without quoting if the command consists of only a single word that needs no escaping, such the path to an executable that contains no spaces or special characters and to which no arguments are passed.)
When calling schtasks.exe[2], quote the /tr argument as a whole as follows:
from PowerShell, use "...", if you need to expand (string-interpolate) the command string up front; otherwise, use '...'.
Important: The need to escape nested " as \" applies in both cases, which in the case of outer "..." quoting means that nested " must be escaped as \`" (sic).
Surprisingly, schtasks.exe recognizes embedded '...' quoting and automatically translates it to "..." quoting - that is why your original command, "powershell.exe -file '..\Execute\execute.ps1'", worked, even though in direct invocation the PowerShell CLI does not support the use of '...' in combination with -File.
from cmd.exe (whether directly or from a batch file), you must use "...".
PowerShell examples:
The following PowerShell commands create and execute two run-once
scheduled tasks, named test1 and test2, that run when the next calendar minute starts, in the context of the calling user, visibly. (You'll have to remove these tasks manually afterwards.)
You may have to wait for up to 1 minute to see the invocation kick in, at which point a new console window pops up for each task.
# Create sample script test.ps1 in the current dir. that
# echoes its arguments and then waits for a keypress.
'"Hi, $Args."; Read-Host "Press ENTER to exit"' > test.ps1
# Find the start of the next calendar minute.
$nextFullMinute = ([datetime]::Now.AddMinutes(1).TimeOfDay.ToString('hh\:mm'))
# -File example:
# Invoke test.ps1 and pass it 'foo' as an argument.
# Note the escaped embedded "..." quoting around the script path
# and that with -File you can only pass literal arguments at
# invocation time).
schtasks.exe /create /f /tn test1 /sc once /st $nextFullMinute `
/tr "powershell -File \`"$PWD/test.ps1\`" foo" #`# (dummy comment to fix broken syntax highlighting)
# -Command example:
# Invoke test.ps1 and pass it $env:USERNAME as an argument.
# Note the '...' around the script path and the need to invoke it with
# &, as well as the ` before $env:USERNAME to prevent its premature expansion.
schtasks.exe /create /f /tn test2 /sc once /st $nextFullMinute `
/tr "powershell -Command & '$PWD/test.ps1' `$env:USERNAME"
"Tasks will execute at ${nextFullMinute}:00"
[1] Note that the Task Scheduler GUI allows you to configure a working directory, but this feature isn't available via the schtasks.exe utility.
[2] The same applies to values passed to the -Argument parameter of the New-ScheduledTaskAction PowerShell cmdlet, though note that the executable name/path is specified separately there, via the -Execute parameter.
By contrast, the Register-ScheduledJob cmdlet for creating scheduled PowerShell jobs accepts a script block as the command to run, which eliminates the quoting headaches.

Resources