2

I was wondering if there's any simple way to make aliases for powershell like cmd. For example: In cmd, doskey art=php artisan $* where $* is optional. Currently, I'm using the following alias in powershell.

function runArtisanCommand
{
    param(
        [Parameter(Mandatory=$false, Position = 0, ValueFromRemainingArguments = $true)]
        $command
    )
    php artisan $command
}

Set-Alias art runArtisanCommand

This works somewhat but don't take flags. For example: I can't write art -h or art route:list -c. In art -h command, it prints the output of php artisan and don't read flag at all but in art route:list -c command, it errors out with.

runArtisanCommand : Missing an argument for parameter 'command'. Specify a parameter of type 'System.Object' and try again.
At line:1 char:16
+ art route:list -c
+                ~~
    + CategoryInfo          : InvalidArgument: (:) [runArtisanCommand], ParameterBindingException
    + FullyQualifiedErrorId : MissingArgument,runArtisanCommand

I would love a simpler solution than this. Thanks in advance.

JawadR1
  • 379
  • 3
  • 16
  • 6
    `function runArtisanCommand {& php artisan $args}` – Mathias R. Jessen Jan 28 '21 at 11:23
  • 1
    As a follow on to what Mathias gives you. You are trying to set an alias to your function name, that's not a thing. You can set alias directly in your function. Of course and alias is a short name for your full function name. function Verb-Noun { [CmdletBinding()] [Alias('vn')] Param ( )}. You don't need an alias if you are just going to call/run your function by your function name as Mathias is showing you. For running executables, see options here:: [• PowerShell: Running Executables](https://social.technet.microsoft.com/wiki/contents/articles/7703.powershell-running-executables.aspx) – postanote Jan 28 '21 at 15:31
  • @postanote: Good point re verb-noun naming convention if you want your function to have a longer name, separate from a short alias. But there's nothing wrong per se with using `Set-Alias` to define the alias - in fact, if you're not the author of the target function, you have no other choice. Also, when you author a module, you may want to keep your alias definitions separate from the function definitions for maintainability. – mklement0 Jan 29 '21 at 20:50
  • 1
    @postanote While I appreciate you pointing out an issue in my code and adding info that I didn't knew before i:e "[Alias('xx')]", I think phrases like "that's not a thing" shouldn't be used unless the code causes either an error or notable performance difference. Phrases like this could be discouraging for beginners. I believe in "make it work first, follow best practices later". P.S: This is a suggestion and not a criticism. :) – JawadR1 Feb 01 '21 at 06:42

1 Answers1

4

The simplest and most convenient way to pass unknown arguments through is by spatting the automatic $args array - as @args - in a simple function or script (one that neither uses a [CmdletBinding()] nor [Parameter()] attributes):

# Note: @args rather than $args makes the function work with named 
#       arguments for PowerShell commands too - see explanation below.
function runArtisanCommand { php artisan @args } 

# As in your question: Define alias 'art' for the function
# Note: Of course, you could directly name your *function* 'art'.
#       If you do want the function to have a longer name, consider one
#       that adheres to PowerShell's Verb-Noun naming convention, such as
#       'Invoke-ArtisanCommand'.
Set-Alias art runArtisanCommand

As an aside: Since the target executable, php, is neither quoted nor specified based on a variable or expression, it can be invoked as-is; otherwise, you would need &, the call operator - see this answer for background information.


As for what you tried:

The problem was that use of -c as a pass-through argument only works if you precede it with --:

# OK, thanks to '--'
art -- route:list -c

-- tells PowerShell to treat all remaining arguments as unnamed (positional) arguments, instead of trying to interpret tokens such as -c as parameter names.

Without --, -c is interpreted as referring to your -command parameter (the parameter you declared as $command with ValueFromRemainingArguments = $true), given that PowerShell allows you to specify name prefixes in lieu of full parameter names, as long as the given prefix is unambiguous.

Because a parameter of any type other than [switch] requires an associated argument, -c (aka -command) failed with an error message to that effect.

You could have avoided the collision by naming your parameter so that it doesn't collide with any pass-through parameters, such as by naming it -_args (with parameter variable $_args):

function runArtisanCommand
{
    param(
        # Note: `Mandatory = $false` and `Position = 0` are *implied*.
        [Parameter(ValueFromRemainingArguments)]
        $_args
    )
    php artisan @_args
}

However, given that use of a [Parameter()] attribute implicitly makes your function an advanced function, it invariably also accepts common parameters, such as -ErrorAction, -OutVariable, -Verbose... - all of which can be passed by unambiguous prefix / short alias too; e.g., -outv for -OutVariable, or alias -ea for ErrorAction; collisions with them cannot be avoided.

Therefore, intended pass-through arguments such as -e still wouldn't work:

# FAILS, because -e ambiguously matches common parameters -ErrorAction 
# and -ErrorVariable.
PS> art router:list -e
Parameter cannot be processed because the parameter name 'e' is ambiguous.
Possible matches include: -ErrorAction -ErrorVariable.

Again, -- is needed:

# OK, thanks to '--'
art -- router:list -e

Summary:

  • Especially for functions wrapping calls to external programs, such as php.exe, using a simple function with @args, as shown at the top, is not only simpler, but also more robust.

  • For functions wrapping PowerShell commands (with explicitly declared parameters):

    • a simple function with @args works too,
    • but if you also want support for tab-completion and showing a syntax diagram with the supported parameters, by passing -?, or via Get-Help, consider defining an (invariably advanced) proxy (wrapper) function via the PowerShell SDK - see below.

Optional background information: Pass-through arguments in PowerShell

As Mathias R. Jessen points out, the simplest way to pass (undeclared) arguments passed to a function or script through to another command is to use the automatic $args variable, which is an automatically populated array of all the arguments passed to a simple function or script (one that isn't advanced, through use of the [CmdletBinding()] and/or [Parameter()] attributes).

As for why @args (splatting) rather than $args should be used:

  • Using $args as-is in your wrapper function only works for passing positional arguments through (those not prefixed by the parameter name; e.g., *.txt), as opposed to named arguments (e.g., -Path *.txt).

  • If the ultimate target command is an external program (such as php.exe in this case), this isn't a problem, because PowerShell of necessity then treats all arguments as positional arguments (it cannot know the target program's syntax).

  • However, if a PowerShell command (with formally declared parameters) is ultimately called, only splatting the $args array - which syntactically means us of @args instead - supports passing named arguments through.[1]

Therefore, as a matter of habit, I suggest always using @args in simple wrapper functions, which equally works with external programs.[2]

To give an example with a simple wrapper function for Get-ChildItem:

# Simple wrapper function for Get-ChildItem that lists recursively
# and by relative path only.
function dirTree {
  # Use @args to make sure that named arguments are properly passed through.
  Get-ChildItem -Recurse -Name @args 
}

# Invoke it with a *named* argument passed through to Get-ChildItem
# If $args rather than @args were used inside the function, this call would fail.
dirTree -Filter *.txt

Using a proxy function for more sophisticated pass-through processing:

The use of @args is convenient, but comes at the expense of not supporting the following:

  • tab-completion, given that tab-completion only works with formally declared parameters (typically with a param(...) block).

  • showing a syntax diagram with the supported parameters, by passing -?, or via Get-Help

To overcome these limitations, the parameter declarations of the ultimate target command must be duplicated in the (then advanced) wrapper function; while that is cumbersome, PowerShell can automate the process by scaffolding a so-called proxy (wrapper) function via the PowerShell SDK - see this answer.

Note:

  • With respect to common parameters such as -ErrorAction, it is the proxy function itself that (automatically) processes them, but that shouldn't make a difference to the caller.

  • Scaffolding a proxy function only works with PowerShell commands, given that PowerShell has no knowledge of the syntax of external programs.

    • However, you can manually duplicate the parameter declarations of the external target program.

[1] Note that the automatic $args array has built-in magic to support this; passing named arguments through with splatting is not supported with a custom array and requires use of a hash table instead, as discussed in the help topic about splatting linked to above.

[2] In fact, only @args also supports the correct interpretation of --%, the stop-parsing symbol.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thank you for the detailed information. @Mathias comment solved my problem but then I switched to your solution and don't see an immediate difference but all the explaination helps. Marking your answer as accepted because can't mark Mathias comment as best and your's will solve the problem too. Thank you again. – JawadR1 Feb 01 '21 at 06:29
  • 1
    Thanks, @Jerry555555. When wrapping external programs, `$args` instead of `@args` works fine, except if you need to use `--%`. When wrapping PowerShell commands, only `@args` works properly. Therefore, the answer recommends `@args` as the best overall solution. – mklement0 Feb 01 '21 at 09:03