8

How does a cmdlet know when it really should call WriteVerbose(), WriteDebug() and etc.?

Perhaps I miss something simple but I cannot find the answer. All cmdlet implementations I have seen so far just call WriteVerbose() without any hesitation. I know that it is correct to do so, but it is not effective.

Performance suffers when verbose mode is off but a cmdlet still prepares data for WriteVerbose() call, that is, for nothing.

In other words, in a cmdlet I would like to be able to:

if (<VerboseMode>)
{
    .... data preparation, sometimes expensive ...
    WriteVerbose(...);
}

But I don't know how to get this if (<VerboseMode>). Any ideas?


Conclusion: The @stej’s answer shows how get the required information in theory. In practice this is hacky and unlikely suitable. Thus, if a cmdlet produces really expensive verbose or debug output then it looks reasonable to introduce an additional parameter specifying verbosity levels.

Roman Kuzmin
  • 40,627
  • 11
  • 95
  • 117

3 Answers3

7

This is method from System.Management.Automation.MshCommandRuntime.

internal void WriteVerbose(VerboseRecord record)
{
    if ((this.Host == null) || (this.Host.UI == null))
    {
        tracer.TraceError("No host in CommandBase.WriteVerbose()", new object[0]);
        throw tracer.NewInvalidOperationException();
    }
    ActionPreference verbosePreference = this.VerbosePreference;
    if (this.WriteHelper_ShouldWrite(verbosePreference, this.lastVerboseContinueStatus))
    {
        if (record.InvocationInfo == null)
        {
            record.SetInvocationInfo(this.MyInvocation);
        }
        this.CBhost.InternalUI.WriteVerboseRecord(record);
    }
    this.lastVerboseContinueStatus = this.WriteHelper(null, null, verbosePreference, this.lastVerboseContinueStatus, "VerbosePreference");
}

MshCommandRuntime implements interface ICommandRuntime which doesn't know anything about verbosity :| (found through reflector). Instance of MshCommandRuntime should be available in Cmdlet (public ICommandRuntime CommandRuntime { get; set; }).

So it should be possible to cast property CommandRuntime to MshCommandRuntime and check the verbosity. Anyway, this is really ugly.


I totally agree that there should be an easy way how to find it out. And besides that (dreaming) compiler should be clever enough not to evaluate some strings in cases like this:

$DebugPreference = 'SilentlyContinue'
$array = 1..1000
Write-Debug "my array is $array"

Input to Write-Debug will be never used, so $array shouldn't be evaluated in passed string.. (it is possible to test that it is really evaluated like this: Write-Debug "my array is $($array|%{write-host $_; $_})"

stej
  • 28,745
  • 11
  • 71
  • 104
  • 2
    @stej: Thank you for your useful investigation. Theoretically it should be possible to hack in this way using reflection (because most of stuff is internal and not accessible normally). But of course this is not a practical solution. Nevertheless, I will accept the answer in a while if we do not find better alternatives. I also found a related suggestion at Connect: https://connect.microsoft.com/PowerShell/feedback/details/74811/performance-provide-formatting-overloads-for-writeverbose-writedebug-etc (This will not be enough though, I think; we need a flag IsVerbose, IsDebug, etc.) – Roman Kuzmin May 06 '10 at 09:26
  • 1
    @Roman, voted. | What currently came to my mind - when developing cmdlet, you should have access to all variables, shouldn't you. Then it is be possible to get `$DebugPrecedence` and act accordingly. Not ideal, but should work. – stej May 06 '10 at 09:34
  • 2
    `$DebugPreference`, `$VerbosePreference` are better than nothing, indeed. But they are not enough, because they are overridden by cmdlet ubiquitous parameters `-Verbose`, `-Debug` if any specified. But these parameters are not accessible from a cmdlet. Do parameters change the local `$DebugPreference`, `$VerbosePreference` implicitely? I doubt that, but I will try to find out. – Roman Kuzmin May 06 '10 at 10:11
  • 1
    “Do ubiquitous parameters change the local `$DebugPreference`, `$VerbosePreference`?” – No, they do not. Well, basically I am coming to a conclusion that in case of expensive verbose or debug output a cmdlet should introduce additional switches or even parameters, kind of `-VerboseLevel `. Hmm, perhaps this is not a bad idea, actually. – Roman Kuzmin May 06 '10 at 11:22
  • 1
    Yep, in some scripts I used exactly that. More levels of verbosity to get what I really want. And besides that.. I got rid off the 'VERBOSE:' and 'DEBUG:' prefixes. – stej May 06 '10 at 11:45
6

So not only do we have to take the common parameters -Debug and -Verbose of the cmdlet into account, but also the global $DebugPreference and $VerbosePreference flags and the fact that these common parameters are inherited if a cmdlet is called from another cmdlet.

It can be done without hacking around in the internals.

This answer shows you it can be done with little problems in a PowerShell cmdlet.

function f { [cmdletbinding()]Param() 
    $debug = $DebugPreference -ne 'SilentlyContinue'
    $verbose = $VerbosePreference -ne 'SilentlyContinue'
    "f is called"
    "    `$debug = $debug"
    "    `$verbose = $verbose"
}
function g { [cmdletbinding()]Param() 
    "g is called"
    f 
}
f
f -Debug -Verbose
g
g -Debug -Verbose

From C# we have to check both these global flags, but also the common parameters. Be sure to inherit from PSCmdlet instead of Cmdlet to get to the GetVariableValue method.

bool debug = false;
bool containsDebug = MyInvocation.BoundParameters.ContainsKey("Debug");
if (containsDebug)
    debug = ((SwitchParameter)MyInvocation.BoundParameters["Debug"]).ToBool();
else
    debug = (ActionPreference)GetVariableValue("DebugPreference") != ActionPreference.SilentlyContinue;

bool verbose = false;
bool containsVerbose = MyInvocation.BoundParameters.ContainsKey("Verbose");
if (containsVerbose)
    verbose = ((SwitchParameter)MyInvocation.BoundParameters["Verbose"]).ToBool();
else
    verbose = (ActionPreference)GetVariableValue("VerbosePreference") != ActionPreference.SilentlyContinue; 
Community
  • 1
  • 1
Lars Truijens
  • 42,837
  • 6
  • 126
  • 143
5

How about:

BEGIN {
    if ($PSCmdlet.MyInvocation.BoundParameters["Debug"].IsPresent) {
        $HasDebugFlag = $true
    }

    if ($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent) {
        $HasVerboseFlag = $true
    }
}
PROCESS {
    if ($HasVerboseFlag) {
        expensive_processing
    }
}

Caveat: Tested only on PowerShell 3.

serialhobbyist
  • 4,768
  • 5
  • 43
  • 65
  • 3
    This is good, indeed. +1. This should work in some cases, actually, many. It will not work if the modes are set not by parameters but via `$VerbosePreference` or `$DebugPreference`. Thus, the `if`-s should also check for these variables. But V3 introduced custom default values. Bound parameters might not help if the modes are enabled via custom defaults. – Roman Kuzmin Jul 23 '13 at 10:25
  • 1
    And does not work if one cmdlet is called from another. – Lars Truijens Dec 29 '13 at 15:44