27

I have a script that may be run manually or may be run by a scheduled task. I need to programmatically determine if I'm running in -noninteractive mode (which is set when run via scheduled task) or normal mode. I've googled around and the best I can find is to add a command line parameter, but I don't have any feasible way of doing that with the scheduled tasks nor can I reasonably expect the users to add the parameter when they run it manually. Does noninteractive mode set some kind of variable or something I could check for in my script?

Edit: I actually inadvertently answered my own question but I'm leaving it here for posterity.

I stuck a read-host in the script to ask the user for something and when it ran in noninteractive mode, boom, terminating error. Stuck it in a try/catch block and do stuff based on what mode I'm in.

Not the prettiest code structure, but it works. If anyone else has a better way please add it!

matthew
  • 419
  • 1
  • 6
  • 10

13 Answers13

36

I didn't like any of the other answers as a complete solution. [Environment]::UserInteractive reports whether the user is interactive, not specifically if the process is interactive. The api is useful for detecting if you are running inside a service. Here's my solution to handle both cases:

function Assert-IsNonInteractiveShell {
    # Test each Arg for match of abbreviated '-NonInteractive' command.
    $NonInteractive = [Environment]::GetCommandLineArgs() | Where-Object{ $_ -like '-NonI*' }

    if ([Environment]::UserInteractive -and -not $NonInteractive) {
        # We are in an interactive shell.
        return $false
    }

    return $true
}
VertigoRay
  • 5,935
  • 6
  • 39
  • 48
  • 3
    [Environment]::UserInteractive did exactly what I expected and wanted. Under ISE and Powershell console it returns true, as a scheduled task it returns false. Thanks. – Straff Aug 16 '16 at 00:05
  • but this will still fail if there's a file or string containing `-noni` such as `powershell -noni -c "run-noninteractively.ps1 "this-noninterationcheck-will-fail"` – phuclv Jul 01 '23 at 01:01
14

You can check how powershell was called using Get-WmiObject for WMI objects:

(gwmi win32_process | ? { $_.processname -eq "powershell.exe" }) | select commandline

#commandline
#-----------
#"C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe" -noprofile -NonInteractive

UPDATE: 2020-10-08

Starting in PowerShell 3.0, this cmdlet has been superseded by Get-CimInstance

(Get-CimInstance win32_process -Filter "ProcessID=$PID" | ? { $_.processname -eq "pwsh.exe" }) | select commandline

#commandline
#-----------
#"C:\Program Files\PowerShell\6\pwsh.exe"
not2qubit
  • 14,531
  • 8
  • 95
  • 135
CB.
  • 58,865
  • 9
  • 159
  • 159
  • You beat me by a few minutes. – David Brabant Mar 16 '12 at 14:50
  • 12
    To avoid false positives if there's more than one PowerShell.exe instance running: `gwmi -Class Win32_Process -Filter "ProcessID=$PID" | Select -Expand CommandLine` – Andy Arismendi Mar 16 '12 at 15:03
  • As a bool you want `((gwmi win32_process | ? { $_.processname -eq "powershell.exe" }).commandline -match "-NonInteractive")` – Tim Lovell-Smith Apr 18 '12 at 23:44
  • I don't trust these command line parameters. What if I run a PowerShell script through a regular batch file (that can also be run interactively) from a non-interactive Windows session like a service? Who will set the -NonInteractive option then? I have a complete test in C# testing `Environment.UserInteractive`, `GetConsoleWindow` and testing std handles for redirection. But P/Invoke seems unpractical in PS. – ygoe Feb 20 '15 at 21:36
  • @LonelyPixel My answer accounts for that scenario: http://stackoverflow.com/a/34098997/615422 – VertigoRay May 08 '16 at 05:31
  • While we're at it, I eventually went another way using existing and proven C# code directly in PowerShell. I've added it in another answer: http://stackoverflow.com/a/37098722/143684 – ygoe May 08 '16 at 10:24
  • 3
    WMI is a pretty expensive way to accomplish this, and using PowerShell to filter processes rather than WMI increases the cost. Overall, this is a bit like running outside and looking in the window to determine if the lights are on. – brianary Jun 20 '16 at 16:10
  • You'd need to be looking at just "-Noni" because the user doesn't have to specify the full parameter name – Jaykul Oct 14 '18 at 21:17
  • this fails if there's a file or string containing `-noni` such as `powershell -noni -c "run-noninteractively.ps1 "this-noninterationcheck-will-fail"` – phuclv Jul 01 '23 at 01:04
5

I think the question needs a more thorough evaluation.

  • "interactive" means the shell is running as REPL - a continuous read-execute-print loop.

  • "non-interactive" means the shell is executing a script, command, or script block and terminates after execution.

If PowerShell is run with any of the options -Command, -EncodedCommand, or -File, it is non-interactive. Unfortunately, you can also run a script without options (pwsh script.ps1), so there is no bullet-proof way of detecting whether the shell is interactive.

So are we out of luck then? No, fortunately PowerShell does automatically add options that we can test if PowerShell runs a script block or is run via ssh to execute commands (ssh user@host command).

function IsInteractive {
    # not including `-NonInteractive` since it apparently does nothing
    # "Does not present an interactive prompt to the user" - no, it does present!
    $non_interactive = '-command', '-c', '-encodedcommand', '-e', '-ec', '-file', '-f'

    # alternatively `$non_interactive [-contains|-eq] $PSItem`
    -not ([Environment]::GetCommandLineArgs() | Where-Object -FilterScript {$PSItem -in $non_interactive})
}

Now test in your PowerShell profile whether this is in interactive mode, so the profile is not run when you execute a script, command or script block (you still have to remember to run pwsh -f script.ps1 - not pwsh script.ps1)

if (-not (IsInteractive)) {
    exit
}
Thorsten
  • 196
  • 4
  • 8
  • Potentially you could add `[Environment]::CommandLine -match '\.ps1'` as well. Can't think of a scenario where that would return unexpected results. – CrookedJ Dec 27 '21 at 16:25
  • this worked in vscode with a profile containing a keypress prompt where the other answers didn't – Brett Jul 01 '22 at 09:25
  • this fails for case-insensitive or shortened options such as `-Command`, `-Com`, `-encodedC`, `-File`... – phuclv Jul 01 '23 at 01:05
3

Testing for interactivity should probably take both the process and the user into account. Looking for the -NonInteractive (minimally -noni) powershell switch to determine process interactivity (very similar to @VertigoRay's script) can be done using a simple filter with a lightweight -like condition:

function Test-Interactive
{
    <#
    .Synopsis
        Determines whether both the user and process are interactive.
    #>

    [CmdletBinding()] Param()
    [Environment]::UserInteractive -and
        !([Environment]::GetCommandLineArgs() |? {$_ -ilike '-NonI*'})
}

This avoids the overhead of WMI, process exploration, imperative clutter, double negative naming, and even a full regex.

brianary
  • 8,996
  • 2
  • 35
  • 29
  • I like the terseness you provided, but I'm not sure why you added the overhead of `[CmdletBinding()]` and an empty `Param()` block. Used `Measure-Command` and the code you posted takes 6 ms as compared to 5.3 ms that the code I posted runs in. It's a shame that you argued for lightweight, but your code takes consistently longer to run (albeit less than 1 ms). Regardless, I like the terseness of your cat skinning process ... so +1 ... and I adjusted my GitHub to follow suit; however I left my answer more verbose for learning. Now, my GitHub takes 3 ms to run ... Thanks! ;) – VertigoRay Oct 04 '16 at 15:49
  • I also changed my example and GitHub to use `-like` instead of `-ilike` ... as it PowerShell is case insensitive by default. For some reason, using `-ilike` adds a pretty consistent .3 ms to execution times. Just throwing this out there since we're talking about lightweight and speed. – VertigoRay Oct 04 '16 at 16:00
  • @VertigoRay `[CmdletBinding()] Param()` is a habit to get the common parameters, which isn't really needed here I guess. I suppose `-like` works fine, I was probably defending against the default case sensitivity getting changed in a profile script, but that doesn't look likely (or possible? Why even have `-ilike`?). Why optimize for speed so aggressively (I was just going for simplicity)? This should only be run once per script, not in a loop or frequently. A compiled cmdlet would be even faster! – brianary Oct 04 '16 at 16:13
  • I completely agree. With powershell, we're dealing with varying degrees of slowness. I thought you were going for speed, since you mentioned overhead of other options; I enjoyed comparing ways to optimize it. If you were going for simplicity, you have to keep in mind that powershell offers a lower barrier of entry for new programmers. I feel like the way you've laid out your function is less readable to a newbie. Of course, simplicity is relative. – VertigoRay Oct 04 '16 at 19:34
  • @VertigoRay Fair enough. ☺️ – brianary Oct 04 '16 at 19:40
  • 1
    In modern PowerShell, you should skip the Where-Object and just write: `-and !([Environment]::GetCommandLineArgs() -match '-noni')` – Jaykul Oct 14 '18 at 21:25
  • checking the command line like this will fail if the command contains `-noni` non-option such as `powershell -noni -c "run-noninteractively.ps1 "this-noninterationcheck-will-fail"` – phuclv Jul 01 '23 at 01:06
  • @phuclv Not in this case, since `-NonI*` will only match parameters that **start** with "-NonI". It may be possible to construct an ambiguous use case, but it would have to be something pretty contrived. – brianary Jul 02 '23 at 02:24
3

I wanted to put an updated answer here because it seems that [Environment]::UserInteractive doesn't behave the same between a .NET Core (container running microsoft/nanoserver) and .NET Full (container running microsoft/windowsservercore).

While [Environment]::UserInteractive will return True or False in 'regular' Windows, it will return $null in 'nanoserver'.

If you want a way to check interactive mode regardless of the value, add this check to your script:

($null -eq [Environment]::UserInteractive -or [Environment]::UserInteractive)

EDIT: To answer the comment of why not just check the truthiness, consider the following truth table that assumes such:

left  | right  | result 
=======================
$null | $true  | $false
$null | $false | $true (!) <--- not what you intended
Josh E
  • 7,390
  • 2
  • 32
  • 44
  • Out of curiosity, why can't you just test the truthiness of `[Environment]::UserInteractive`? `$null` should evaluate as `$False` – codewario Aug 31 '18 at 20:55
  • I may have misunderstood some powershell-specific thing, but AIUI, you should always put $null on the left-hand side of a comparison operation. Please see my edited answer for more – Josh E Oct 05 '18 at 15:32
  • 2
    In any case, [Environment]::UserInteractive doesn't tell you that **PowerShell** is running interactively ... – Jaykul Oct 14 '18 at 21:24
  • According to old tests, this [doesn't do anything](https://social.technet.microsoft.com/Forums/lync/en-US/92f23477-d41f-4d93-8c78-ac7de0a2335e/how-can-a-powershell-script-tell-if-it-was-invoked-interactively?forum=ITCG). – not2qubit Oct 08 '20 at 12:09
  • 1
    instead of the `-or` you can just do `if (-not [Environment]::UserInteractive)` to achieve the same, which will work if the envvar is `$null` or `$false` – CrookedJ Dec 24 '21 at 20:07
  • Exactly what does the truth table describe? (In any case: `$null` is falsey. `$null or $true` returns `$true`, and `$null or $false` returns `$false`.) – Mike Rosoft Oct 31 '22 at 11:46
2

I came up with a posh port of existing and proven C# code that uses a fair bit of P/Invoke to determine all the corner cases. This code is used in my PowerShell Build Script that coordinates several build tasks around Visual Studio projects.

# Some code can be better expressed in C#...
#
Add-Type @'
using System;
using System.Runtime.InteropServices;

public class Utils
{
    [DllImport("kernel32.dll")]
    private static extern uint GetFileType(IntPtr hFile);

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetStdHandle(int nStdHandle);

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetConsoleWindow();

    [DllImport("user32.dll")]
    private static extern bool IsWindowVisible(IntPtr hWnd);

    public static bool IsInteractiveAndVisible
    {
        get
        {
            return Environment.UserInteractive &&
                GetConsoleWindow() != IntPtr.Zero &&
                IsWindowVisible(GetConsoleWindow()) &&
                GetFileType(GetStdHandle(-10)) == 2 &&   // STD_INPUT_HANDLE is FILE_TYPE_CHAR
                GetFileType(GetStdHandle(-11)) == 2 &&   // STD_OUTPUT_HANDLE
                GetFileType(GetStdHandle(-12)) == 2;     // STD_ERROR_HANDLE
        }
    }
}
'@

# Use the interactivity check somewhere:
if (![Utils]::IsInteractiveAndVisible)
{
    return
}
ygoe
  • 18,655
  • 23
  • 113
  • 210
  • FYI, This doesn't work in Powershell ISE (at least on PSVersion: 5.1.14409.1012). If you transform all your methods to public you'll see that the part that fails is: `[Utils]::IsWindowVisible([Utils]::GetConsoleWindow())`. This is certainly some ISE bug/feature :). For reference, some differences between ISE and regular Powershell are listed [here](https://blogs.msdn.microsoft.com/powershell/2009/04/17/differences-between-the-ise-and-powershell-console/). Didn't see anything about `IsWindowVisible()` but I suspect this belonged in that list. – Petru Zaharia Aug 27 '17 at 11:58
  • 1
    @PetruZaharia I haven't tried this, and this code doesn't even make sense in ISE, but it probably can't find its console window. – ygoe Aug 27 '17 at 19:25
  • Yeah, the only flaw is that this assumes that all PowerShell is hosted in a console (but then, all the other answers here are assuming PowerShell.exe, so ...). – Jaykul Oct 14 '18 at 21:27
2

This will return a Boolean when the -Noninteractive switch is used to launch the PowerShell prompt.

[Environment]::GetCommandLineArgs().Contains('-NonInteractive')
user2320464
  • 359
  • 1
  • 5
  • 11
  • Best answer here i think. UserInteractive did not work running on nix octopus server in a powershell script step. – Sam Feb 05 '21 at 02:59
  • 1
    This fails if the user has abbreviated the option to, say, `-NonI`. – JdeBP Jun 18 '21 at 09:08
  • @Sam this is the worst one here. For example it fails for simple cases like `-nonI`, `-NonInt`, `-NoNiNt`... – phuclv Jul 01 '23 at 01:03
0
C:\> powershell -NoProfile -NoLogo -NonInteractive -Command "[Environment]::GetCommandLineArgs()"
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
-NoProfile
-NoLogo
-NonInteractive
-Command
[Environment]::GetCommandLineArgs()
FrobberOfBits
  • 17,634
  • 4
  • 52
  • 86
Jonathan
  • 17
  • 1
0

There are a lot of good answers here. This is an old question and mostly the OP seems to have answered it for themselves.

However, in the general case this is a complicated issue especially if you want to put code like this in to your $profile. I had put some unguarded output in a $profile which caused a remote scp command to fail. This created sadness and found me reading this thread.

The real problem with mine and other solutions is it's not possible to know the intent of the person running a script. Someone using a -f or -c option may or may not be expecting an interactive experience. That can be especially problematic if you are trying to check something like this in your $profile.

The code below tries to respect intention if it's put forward. For example, if the -i or -noni option is given, it assumes that's what you want. Everything else is open for interpretation. If you need to support both types of behavior (interactive/non-interactive) you can use options and short cuts differently. For example, let '-c' run the command 'interactive' and -command run 'non-interactive'. Note, you should consider doing such a thing as a nasty hack that will bite you or someone else later, but it can get you out of a jam and 'scripting life' is often filled with compromises. No matter what you choose, document, early, often and everywhere, remember you might have to support the code you write ;-)

function IsShellInteractive {
    if ([Environment]::UserInteractive -eq $false) {
        return $false
        }
    
    # Get the args, minus the first executable (presumable the full path to powershell or pwsh exe)
    $options = [Environment]::GetCommandLineArgs() | Select-Object -Skip 1
    
    # trust any stated intention
    if (($options -contains "-i") -or ($options -contains "-interactive")) {
        return $true
        }

    if (($options -contains "-noni") -or ($options -contains "-noninteractive")) {
        return $false
        }

    # [[-File|-f] <filePath> [args]]
    # [-Command|-c { - | <script-block> [-args <arg-array>]
    #                  | <string> [<CommandParameters>] } ]
    # [-EncodedCommand|-e|-ec <Base64EncodedCommand>]
    # [-NoExit|-noe]
    # [-NoProfile|-nop]
    # [-InputFormat|-ip|-if {Text | XML}]
    # [-OutputFormat|-o|-op {Text | XML}]
    # [-PSConsoleFile <file> ]

    # Who Knows ?
    #  options like 
    #    -noexit, -noe and -psconsolefile" are 'likely' interactive
    #  others like
    #    -noprofile,-nop,-inputformat,-ip,-if,-outputformat,-o,-op are 'likely' non-interactive
    #  still others like 
    #    -file,-f,-command,-c,encodedcommand,-e,-ec could easily go either way
    # remove ones you don't like, or use short cuts one way and long form another
    $nonInteractiveOptions = "-file,-f,-command,-c,encodedcommand,-e,-ec,-noprofile,-nop,-inputformat,-ip,-if,-outputformat,-o,-op" -split ","
    foreach ($opt in $options) {
        if ($opt -in $nonInteractiveOptions) {
            return false;
            }
        }

    return $true
    }
Dweeberly
  • 4,668
  • 2
  • 22
  • 41
0

I am using the following to handle checking for interactivity in my PowerShell profile. I found that I also needed to check [System.Console]::IsOutputRedirected to properly handle cases involving executing over remote SSH:

if ([System.Console]::IsOutputRedirected -or ![Environment]::UserInteractive -or !!([Environment]::GetCommandLineArgs() | Where-Object { $_ -ilike '-noni*' })) {
    // Non-interactive
}
Cory Gross
  • 36,833
  • 17
  • 68
  • 80
0
powerShell -NonInteractive { Get-WmiObject Win32_Process -Filter "Name like '%powershell%'" | select-Object CommandLine }

powershell -Command { Get-WmiObject Win32_Process -Filter "Name like '%powershell%'" | select-Object CommandLine }

In the first case, you'll get the "-NonInteractive" param. In the latter you won't.

David Brabant
  • 41,623
  • 16
  • 83
  • 111
-1

Script: IsNonInteractive.ps1

function Test-IsNonInteractive()
{
    #ref: http://www.powershellmagazine.com/2013/05/13/pstip-detecting-if-the-console-is-in-interactive-mode/
    #powershell -NoProfile -NoLogo -NonInteractive -File .\IsNonInteractive.ps1
    return [bool]([Environment]::GetCommandLineArgs() -Contains '-NonInteractive')
}

Test-IsNonInteractive

Example Usage (from command prompt)

pushd c:\My\Powershell\Scripts\Directory
::run in non-interactive mode
powershell -NoProfile -NoLogo -NonInteractive -File .\IsNonInteractive.ps1
::run in interactive mode
powershell -File .\IsNonInteractive.ps1
popd

More Involved Example Powershell Script

#script options
$promptForCredentialsInInteractive = $true

#script starts here

function Test-IsNonInteractive()
{
    #ref: http://www.powershellmagazine.com/2013/05/13/pstip-detecting-if-the-console-is-in-interactive-mode/
    #powershell -NoProfile -NoLogo -NonInteractive -File .\IsNonInteractive.ps1
    return [bool]([Environment]::GetCommandLineArgs() -Contains '-NonInteractive')
}

function Get-CurrentUserCredentials()
{
    return [System.Net.CredentialCache]::DefaultCredentials
}
function Get-CurrentUserName()
{
    return ("{0}\{1}" -f $env:USERDOMAIN,$env:USERNAME)
}

$cred = $null
$user = Get-CurrentUserName

if (Test-IsNonInteractive) 
{
    $msg = 'non interactive'
    $cred = Get-CurrentUserCredentials
} 
else 
{
    $msg = 'interactive'
    if ($promptForCredentialsInInteractive) 
    {
        $cred = (get-credential -UserName $user -Message "Please enter the credentials you wish this script to use when accessing network resources")
        $user = $cred.UserName
    } 
    else 
    {
        $cred = Get-CurrentUserCredentials
    }
}

$msg = ("Running as user '{0}' in '{1}' mode" -f $user,$msg)
write-output $msg
JohnLBevan
  • 22,735
  • 13
  • 96
  • 178
  • 1
    It would be nice if `[Environment]::UserInteractive` worked. I submitted a bug because it makes me sad: https://connect.microsoft.com/PowerShell/feedbackdetail/view/1588843 – VertigoRay Jul 25 '15 at 10:41
  • 1
    I'm using your solution in my code for now even though it's not fully valid. For example, `powershell.exe -NonInt -c "blah"` will run as -NonInteractive, but your check will fail. – VertigoRay Jul 25 '15 at 10:49
  • @VertigoRay It does look like `[Environment]::UserInteractive` works as it should, maybe not as you expect it to. I am using this in a scheduled task, and it properly shows "false". If I run it manually, and the window opens, it reports "true". Thx for the comment. – B_Dubb42 Oct 31 '15 at 13:13
  • @B_Dubb86 Are you running it as the System Account? If you look at the only comment on the https://connect.microsoft.com/PowerShell/feedbackdetail/view/1588843 that I linked previously, you'll likely see why yours works. To save you the click, here's the quote: "Environment.UserInteractive reports whether the user is interactive, not specifically if the process is interactive. The api is useful for detecting if you are running inside a service." – VertigoRay Nov 13 '15 at 14:39
-1

Implement two scripts, one core.ps1 to be manually launched, and one scheduled.ps1 that launches core.ps1 with a parameter.

Dan
  • 1,927
  • 2
  • 24
  • 35
  • Thanks Dan. That's a good idea but the way we've got the whole scheduled task thing set up that makes it a bit complicated and confusing for anyone but me (and even me after I forget about this for a week and come back to it). – matthew Mar 16 '12 at 14:26
  • Will this work, then? powershell.exe -noexit script.ps1 -Argument1 {arg1}-Argument2 {arg2} – Dan Mar 16 '12 at 14:28
  • Figured out a way I could do it but giving you the answer check-mark since yours was technically an answer, just not the one I used :) – matthew Mar 16 '12 at 14:32