Is there any way I can use rm -rf in PowerShell?

153 Views Asked by At

I know that I can use rm -r -fo to remove a folder and all its content in PowerShell. But is there any way I can use rm -rf in PowerShell?

I tried to add the below scripts into my $Profile, but both did not work.

  1. First try:

    function rm -rf() {
        rm -r -fo
    }
    
  2. Second try:

    Set-Alias rm -rf "rm -r -fo"
    
2

There are 2 best solutions below

2
stackprotector On BEST ANSWER

As suggested in the comments, you can write a proxy function, that adds the -rf parameter to Remove-Item. If you then set the alias rm to point to your proxy function instead of Remove-Item, you will be able to use rm -rf like in Unix, while still having the opportunity to use all other parameters of Remove-Item. You will basically have Remove-Item with the additional parameter -rf.

As documented in iRon's excellent Q&A, you can generate the proxy function body with the following commands:

$MetaData = [System.Management.Automation.CommandMetaData](Get-Command Remove-Item)
[System.Management.Automation.ProxyCommand]::Create($MetaData)

Which will give you:

[CmdletBinding(DefaultParameterSetName='Path', SupportsShouldProcess=$true, ConfirmImpact='Medium', SupportsTransactions=$true, HelpUri='https://go.microsoft.com/fwlink/?LinkID=113373')]
param(
    [Parameter(ParameterSetName='Path', Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
    [string[]]
    ${Path},

    [Parameter(ParameterSetName='LiteralPath', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
    [Alias('PSPath')]
    [string[]]
    ${LiteralPath},

    [string]
    ${Filter},

    [string[]]
    ${Include},

    [string[]]
    ${Exclude},

    [switch]
    ${Recurse},

    [switch]
    ${Force},

    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [pscredential]
    [System.Management.Automation.CredentialAttribute()]
    ${Credential})


dynamicparam
{
    try {
        $targetCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Remove-Item', [System.Management.Automation.CommandTypes]::Cmdlet, $PSBoundParameters)
        $dynamicParams = @($targetCmd.Parameters.GetEnumerator() | Microsoft.PowerShell.Core\Where-Object { $_.Value.IsDynamic })
        if ($dynamicParams.Length -gt 0)
        {
            $paramDictionary = [Management.Automation.RuntimeDefinedParameterDictionary]::new()
            foreach ($param in $dynamicParams)
            {
                $param = $param.Value

                if(-not $MyInvocation.MyCommand.Parameters.ContainsKey($param.Name))
                {
                    $dynParam = [Management.Automation.RuntimeDefinedParameter]::new($param.Name, $param.ParameterType, $param.Attributes)
                    $paramDictionary.Add($param.Name, $dynParam)
                }
            }
            return $paramDictionary
        }
    } catch {
        throw
    }
}

begin
{
    try {
        $outBuffer = $null
        if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
        {
            $PSBoundParameters['OutBuffer'] = 1
        }
        $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Remove-Item', [System.Management.Automation.CommandTypes]::Cmdlet)
        $scriptCmd = {& $wrappedCmd @PSBoundParameters }
        $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
        $steppablePipeline.Begin($PSCmdlet)
    } catch {
        throw
    }
}

process
{
    try {
        $steppablePipeline.Process($_)
    } catch {
        throw
    }
}

end
{
    try {
        $steppablePipeline.End()
    } catch {
        throw
    }
}
<#

.ForwardHelpTargetName Microsoft.PowerShell.Management\Remove-Item
.ForwardHelpCategory Cmdlet

#>

You can paste this – as is – directly into your own function definition and you will already have a function that does the same as Remove-Item.

To add the parameter -rf, add the the following parameter definition:

[Alias('RF')]
[switch]
$RecurseForce

To not pass it to Remove-Item directly, add the following line to the dynamicparam block:

$PSBoundParameters.Remove('RecurseForce') | Out-Null

Then you just need to handle the call of Remove-Item, when the -rf parameter is provided. To do so, change the line $scriptCmd = {& $wrappedCmd @PSBoundParameters } to:

if ($RecurseForce) {
    $scriptCmd = {& $wrappedCmd @PSBoundParameters -Recurse -Force }
} else {
    $scriptCmd = {& $wrappedCmd @PSBoundParameters }
}

Furthermore, it is recommended to remove the OutBuffer-related code and to replace all throw statements by $PSCmdlet.ThrowTerminatingError($_).

In the following full example, I named the proxy function Remove-ItemProxy and made all the changes I already described. In the end I call Set-Alias to make it available through rm. No worries, Set-Alias will only affect your current session and won't alter your system permanently. You can copy and paste the following block into your PowerShell profile and whenever you open a PowerShell session, you will be able to use rm -rf:

function Remove-ItemProxy {
    [CmdletBinding(DefaultParameterSetName='Path', SupportsShouldProcess=$true, ConfirmImpact='Medium', SupportsTransactions=$true, HelpUri='https://go.microsoft.com/fwlink/?LinkID=113373')]
    param(
        [Parameter(ParameterSetName='Path', Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
        [string[]]
        ${Path},

        [Parameter(ParameterSetName='LiteralPath', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [Alias('PSPath')]
        [string[]]
        ${LiteralPath},

        [string]
        ${Filter},

        [string[]]
        ${Include},

        [string[]]
        ${Exclude},

        [switch]
        ${Recurse},

        [switch]
        ${Force},

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [pscredential]
        [System.Management.Automation.CredentialAttribute()]
        ${Credential},
        
        [Alias('RF')]
        [switch]
        $RecurseForce)


    dynamicparam
    {
        try {
            $PSBoundParameters.Remove('RecurseForce') | Out-Null
            $targetCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Remove-Item', [System.Management.Automation.CommandTypes]::Cmdlet, $PSBoundParameters)
            $dynamicParams = @($targetCmd.Parameters.GetEnumerator() | Microsoft.PowerShell.Core\Where-Object { $_.Value.IsDynamic })
            if ($dynamicParams.Length -gt 0)
            {
                $paramDictionary = [Management.Automation.RuntimeDefinedParameterDictionary]::new()
                foreach ($param in $dynamicParams)
                {
                    $param = $param.Value

                    if(-not $MyInvocation.MyCommand.Parameters.ContainsKey($param.Name))
                    {
                        $dynParam = [Management.Automation.RuntimeDefinedParameter]::new($param.Name, $param.ParameterType, $param.Attributes)
                        $paramDictionary.Add($param.Name, $dynParam)
                    }
                }
                return $paramDictionary
            }
        } catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }

    begin
    {
        try {
            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Remove-Item', [System.Management.Automation.CommandTypes]::Cmdlet)
            if ($RecurseForce) {
                $scriptCmd = {& $wrappedCmd @PSBoundParameters -Recurse -Force }
            } else {
                $scriptCmd = {& $wrappedCmd @PSBoundParameters }
            }
            $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
            $steppablePipeline.Begin($PSCmdlet)
        } catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }

    process
    {
        try {
            $steppablePipeline.Process($_)
        } catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }

    end
    {
        try {
            $steppablePipeline.End()
        } catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
    <#

    .ForwardHelpTargetName Microsoft.PowerShell.Management\Remove-Item
    .ForwardHelpCategory Cmdlet

    #>
}

Set-Alias -Name rm -Value Remove-ItemProxy -Option AllScope
0
mklement0 On

stackprotector's helpful answer is definitely the best solution: it wraps Remove-Item in all aspects, including tab-completion, streaming pipeline-input support, and integration with Get-Help:

To offer a simpler - but limited - alternative, which is easier to author, however:

Remove-Item -ErrorAction Ignore Alias:rm # remove the built-in `rm` alias

# Simple (non-advanced) wrapper function
function rm {
<#
.SYNOPSIS
  Wrapper for Remove-Item with support for -rf to mean -Recurse -Force
#>
  param([switch] $rf)  # Custom -rf switch
  # If -rf was specified, translate it into the -Recurse -Force switches
  # via a hashtable to be used for splatting.
  $extraArgs = if ($rf) { @{ Recurse=$true; Force=$true } } else { @{} }
  # Invoke Remove-Item with any extra arguments and all pass-through arguments.
  if ($MyInvocation.ExpectingInput) {  # pipeline input present
    $input | Remove-Item @args @extraArgs
  } else { 
    Remove-Item @args @extraArgs 
  }
}

Limitations:

  • No tab-completion support for Remove-Item's parameters (you do get it for -rf, and by default the files and directories in the current directory also tab-complete)

  • Only simple Get-Help / -? output, focused on -rf only (conversely, however, forwarding the help to Remove-Item, as in the proxy-function approach, doesn't describe the custom -rf parameter at all).

  • While pipeline input is supported, it is collected in full up front, unlike with the proxy-function approach.

Note:

  • The solution relies on splatting to pass arguments (passing arguments stored in a hashtable or array variable by prefixing that variable with @).

    • In non-advanced functions, such as in this case, the automatic $args variable contains all arguments that weren't bound to declared parameters, and @args passes them through via splatting.[1]

    • Splatting an empty hashtable (@{}) results in no arguments being passed, as desired (ditto for an empty array, @()).

  • $MyInvocation.ExpectingInput indicates whether pipeline input is present, which can then be enumerated via the automatic $input variable; as noted, doing so requires collecting all input objects in memory first, given that functions without a process block are only executed after all pipeline input has been received (i.e., they execute as if their body were in an end block).


[1] Note that the automatic $args variable contains an array, and while splatting with an array - as opposed to with a hashtable - normally doesn't support passing named arguments through (those preceded with their target parameter name, e.g. -Path *.txt), $args has magic built into it that supports that too. For the details of this magic, see the footnote of this answer.