matthew
matthew

Reputation: 449

Powershell test for noninteractive mode

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!

Upvotes: 30

Views: 37538

Answers (15)

ZnqbuZ
ZnqbuZ

Reputation: 1

I wrote a PSGallery module for this:

PS> Install-Module TestInteractive
PS> Import-Module TestInteractive
PS> Test-Interactive
True

because somehow I need to detect whether PowerShell is a 'normal' shell, i.e. an interactive REPL (not only -Interactive), and I need a cross-platform solution. It supports shortened options and is case-insensitive, and it does not match parameters in case like pwsh -c "pwsh -Interactive".

Upvotes: 0

Cory Gross
Cory Gross

Reputation: 37206

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
}

Upvotes: 3

Shenk
Shenk

Reputation: 411

There are many good answers here, and there are also many relevant concerns in the comments.

However, no one has yet supplied an answer to adequately account for abbreviated command-line arguments and false-positive command-line arguments being provided. Here is my solution to that issue, beginning with the knowledge from the prior answers.

General-use function

function Assert-IsNonInteractiveShell {
    # Determines whether the user is interactive.
    -not [Environment]::UserInteractive -or
        # Test each command-line arg for a match of an abbreviated '-NonInteractive' command.
        # Capture groups are necessary to ensure that no optional letters are skipped.
        [bool]([Environment]::GetCommandLineArgs() | Where-Object {
            $_ -imatch '^-NonI(?:n(?:t(?:e(?:r(?:a(?:c(?:t(?:i(?:ve?)?)?)?)?)?)?)?)?)?$'
        } | Select-Object -First 1)
}

I put the second part on separate lines so that it fits better in a stackoverflow code block but that can be made single-line if preferred.

Also, I added Select-Object -First 1 to halt the pipeline after a single match is found. This is useful, for example, if the commandline has a large number of arguments.

Examples:

Assert-IsNonInteractiveShell.ps1 resides in the current directory and contains the above function and a call to it on the following line. All of these work the same whether called from cmd or within powershell.

# 1) Succeeds because -noni is an accepted abbreviation for -NonInteractive
powershell.exe -noni -c ".\Assert-IsNonInteractiveShell.ps1"
True

# 2) Succeeds because -nonint is an accepted abbreviation for -NonInteractive
powershell.exe -nonint -file "Assert-IsNonInteractiveShell.ps1"
True

# 3) Succeeds because -NonInteractive was provided
# In this example, -NonInteractive is treated as a separate arg in the command to execute
powershell.exe -c ".\Assert-IsNonInteractiveShell.ps1" -NonInteractive
True

# 4) Succeeds because powershell executes a new subshell of powershell with these arguments
powershell.exe -c "powershell.exe .\Assert-IsNonInteractiveShell.ps1 -noninteractive"
True

# 5) Fails because the new subshell of powershell did not receive the -NonInteractive arg
powershell.exe -NonInteractive -c "powershell.exe .\Assert-IsNonInteractiveShell.ps1"
False

# 6) Fails because powershell treats the whole string a single argument
# Therefore, -noninteractive is treated as an argument to the ps1 file itself
powershell.exe -c ".\Assert-IsNonInteractiveShell.ps1 -noninteractive"
False

# 7) Fails because -noninteractive-fake-arg is an invalid abbreviation for -NonInteractive
powershell.exe -c ".\Assert-IsNonInteractiveShell.ps1" -noninteractive-fake-arg
False

# 8) Fails because -noninteractivefakearg is an invalid abbreviation for -NonInteractive
powershell.exe -file "Assert-IsNonInteractiveShell.ps1" -noninteractivefakearg
False

Edge case catch-all function

If you really want example #6 above to succeed, then you could, alternatively or in addition to [Environment]::GetCommandLineArgs(), check either of the following depending on whether it's being called inside of a function or directly, respectively:

To get the parent invocation's arguments:

(Get-PSCallStack | Select-Object -Skip 1 -First 1).InvocationInfo.UnboundArguments

To get the current invocation's arguments:

$MyInvocation.UnboundArguments

Thus, if you wanted to check the environment command line, the parent invocation's unbound arguments, and the local invocation's arguments you could do the following:

function Assert-IsNonInteractiveShell {
    $arguments = (@([Environment]::GetCommandLineArgs()) +
        @((Get-PSCallStack|Select-Object -Skip 1 -First 1).InvocationInfo.UnboundArguments) +
        @($MyInvocation.UnboundArguments)
    )
    # Determines whether the user is interactive.
    -not [Environment]::UserInteractive -or
        # Test each command-line arg for a match of an abbreviated '-NonInteractive' command.
        # Capture groups are necessary to ensure that no optional letters are skipped.
        [bool]($arguments | Where-Object {
            $_ -imatch '^-NonI(?:n(?:t(?:e(?:r(?:a(?:c(?:t(?:i(?:ve?)?)?)?)?)?)?)?)?)?$'
        } | Select-Object -First 1)
}

And adjust as needed.

But it's important to remember that in this case you're really only checking for the existence of that argument on the command line. It's up to you to determine whether that is relevant to your use case.

Upvotes: 1

Dweeberly
Dweeberly

Reputation: 4777

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
    }

Upvotes: 1

VertigoRay
VertigoRay

Reputation: 6273

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
}

Upvotes: 41

CB.
CB.

Reputation: 60910

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"

Upvotes: 14

Thorsten
Thorsten

Reputation: 316

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
}

Upvotes: 5

user2320464
user2320464

Reputation: 409

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

[Environment]::GetCommandLineArgs().Contains('-NonInteractive')

Upvotes: 3

Josh E
Josh E

Reputation: 7434

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

Upvotes: 4

brianary
brianary

Reputation: 9322

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.

Upvotes: 3

ygoe
ygoe

Reputation: 20364

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
}

Upvotes: 4

JohnLBevan
JohnLBevan

Reputation: 24410

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

Upvotes: -1

Jonathan
Jonathan

Reputation: 27

C:\> powershell -NoProfile -NoLogo -NonInteractive -Command "[Environment]::GetCommandLineArgs()"
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
-NoProfile
-NoLogo
-NonInteractive
-Command
[Environment]::GetCommandLineArgs()

Upvotes: 1

David Brabant
David Brabant

Reputation: 43489

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.

Upvotes: 0

Dan
Dan

Reputation: 1959

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

Upvotes: -1

Related Questions