2020-01-27 16:34:12 +01:00
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
# include "pch.h"
# include "AppCommandlineArgs.h"
# include "ActionArgs.h"
using namespace winrt : : TerminalApp ;
using namespace TerminalApp ;
// Either a ; at the start of a line, or a ; preceeded by any non-\ char.
const std : : wregex AppCommandlineArgs : : _commandDelimiterRegex { LR " (^;|[^ \\ ];) " } ;
AppCommandlineArgs : : AppCommandlineArgs ( )
{
_buildParser ( ) ;
_resetStateToDefault ( ) ;
}
// Method Description:
// - Attempt to parse a given command as a single commandline. If the command
// doesn't have a subcommand, we'll try parsing the commandline again, as a
// new-tab command.
// - Actions generated by this command are added to our _startupActions list.
// Arguments:
// - command: The individual commandline to parse as a command.
// Return Value:
// - 0 if the commandline was successfully parsed
// - nonzero return values are defined in CLI::ExitCodes
int AppCommandlineArgs : : ParseCommand ( const Commandline & command )
{
const int argc = static_cast < int > ( command . Argc ( ) ) ;
// Stash a pointer to the current Commandline instance we're parsing.
// When we're trying to parse the commandline for a new-tab/split-pane
// subcommand, we'll need to inspect the original Args from this
// Commandline to find the entirety of the commandline args for the new
// terminal instance. Discard the pointer when we leave this method. The
// pointer will be safe for usage, since the parse callback will be
// executed on the same thread, higher on the stack.
_currentCommandline = & command ;
auto clearPointer = wil : : scope_exit ( [ this ] ( ) { _currentCommandline = nullptr ; } ) ;
try
{
// CLI11 needs a mutable vector<string>, so copy out the args here.
// * When we're using the vector<string> parse(), it also expects that
// there isn't a leading executable name in the args, so slice that
// out.
// - In AppCommandlineArgs::BuildCommands, we'll make sure each
// subsequent command in a single commandline starts with a wt.exe.
// Our very first argument might not be "wt.exe", it could be `wt`,
// or `wtd.exe`, etc. Regardless, we want to ignore the first arg of
// every Commandline
// * Not only that, but this particular overload of parse() wants the
// args _reversed_ here.
std : : vector < std : : string > args { command . Args ( ) . begin ( ) + 1 , command . Args ( ) . end ( ) } ;
std : : reverse ( args . begin ( ) , args . end ( ) ) ;
// Revert our state to the initial state. As this function can be called
// multiple times during the parsing of a single commandline (once for each
// sub-command), we don't want the leftover state from previous calls to
// pollute this run's state.
_resetStateToDefault ( ) ;
// Manually check for the "/?" or "-?" flags, to manually trigger the help text.
if ( argc = = 2 & & ( NixHelpFlag = = til : : at ( command . Args ( ) , 1 ) | | WindowsHelpFlag = = til : : at ( command . Args ( ) , 1 ) ) )
{
throw CLI : : CallForHelp ( ) ;
}
// Clear the parser's internal state
_app . clear ( ) ;
// attempt to parse the commandline
_app . parse ( args ) ;
// If we parsed the commandline, and _no_ subcommands were provided, try
// parsing again as a "new-tab" command.
if ( _noCommandsProvided ( ) )
{
_newTabCommand . subcommand - > clear ( ) ;
_newTabCommand . subcommand - > parse ( args ) ;
}
}
catch ( const CLI : : CallForHelp & e )
{
return _handleExit ( _app , e ) ;
}
catch ( const CLI : : ParseError & e )
{
// If we parsed the commandline, and _no_ subcommands were provided, try
// parsing again as a "new-tab" command.
if ( _noCommandsProvided ( ) )
{
try
{
// CLI11 mutated the original vector the first time it tried to
// parse the args. Reconstruct it the way CLI11 wants here.
// "See above for why it's begin() + 1"
std : : vector < std : : string > args { command . Args ( ) . begin ( ) + 1 , command . Args ( ) . end ( ) } ;
std : : reverse ( args . begin ( ) , args . end ( ) ) ;
_newTabCommand . subcommand - > clear ( ) ;
_newTabCommand . subcommand - > parse ( args ) ;
}
catch ( const CLI : : ParseError & e )
{
return _handleExit ( * _newTabCommand . subcommand , e ) ;
}
}
else
{
return _handleExit ( _app , e ) ;
}
}
return 0 ;
}
// Method Description:
// - Calls App::exit() for the provided command, and collects it's output into
// our _exitMessage buffer.
// Arguments:
// - command: Either the root App object, or a subcommand for which to call exit() on.
// - e: the CLI::Error to process as the exit reason for parsing.
// Return Value:
// - 0 if the command exited successfully
// - nonzero return values are defined in CLI::ExitCodes
int AppCommandlineArgs : : _handleExit ( const CLI : : App & command , const CLI : : Error & e )
{
// Create some streams to collect the output that would otherwise go to stdout.
std : : ostringstream out ;
std : : ostringstream err ;
const auto result = command . exit ( e , out , err ) ;
// I believe only CallForHelp will return 0
if ( result = = 0 )
{
_exitMessage = out . str ( ) ;
}
else
{
_exitMessage = err . str ( ) ;
}
return result ;
}
// Method Description:
// - Add each subcommand and options to the commandline parser.
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs : : _buildParser ( )
{
_buildNewTabParser ( ) ;
_buildSplitPaneParser ( ) ;
_buildFocusTabParser ( ) ;
}
// Method Description:
// - Adds the `new-tab` subcommand and related options to the commandline parser.
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs : : _buildNewTabParser ( )
{
_newTabCommand . subcommand = _app . add_subcommand ( " new-tab " , NEEDS_LOC ( " Create a new tab " ) ) ;
_addNewTerminalArgs ( _newTabCommand ) ;
// When ParseCommand is called, if this subcommand was provided, this
// callback function will be triggered on the same thread. We can be sure
// that `this` will still be safe - this function just lets us know this
// command was parsed.
_newTabCommand . subcommand - > callback ( [ & , this ] ( ) {
// Buld the NewTab action from the values we've parsed on the commandline.
auto newTabAction = winrt : : make_self < implementation : : ActionAndArgs > ( ) ;
newTabAction - > Action ( ShortcutAction : : NewTab ) ;
auto args = winrt : : make_self < implementation : : NewTabArgs > ( ) ;
// _getNewTerminalArgs MUST be called before parsing any other options,
// as it might clear those options while finding the commandline
args - > TerminalArgs ( _getNewTerminalArgs ( _newTabCommand ) ) ;
newTabAction - > Args ( * args ) ;
_startupActions . push_back ( * newTabAction ) ;
} ) ;
}
// Method Description:
// - Adds the `split-pane` subcommand and related options to the commandline parser.
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs : : _buildSplitPaneParser ( )
{
_newPaneCommand . subcommand = _app . add_subcommand ( " split-pane " , NEEDS_LOC ( " Create a new pane " ) ) ;
_addNewTerminalArgs ( _newPaneCommand ) ;
_horizontalOption = _newPaneCommand . subcommand - > add_flag ( " -H,--horizontal " ,
_splitHorizontal ,
NEEDS_LOC ( " Create the new pane as a horizontal split (think [-]) " ) ) ;
_verticalOption = _newPaneCommand . subcommand - > add_flag ( " -V,--vertical " ,
_splitVertical ,
NEEDS_LOC ( " Create the new pane as a vertical split (think [|]) " ) ) ;
_verticalOption - > excludes ( _horizontalOption ) ;
// When ParseCommand is called, if this subcommand was provided, this
// callback function will be triggered on the same thread. We can be sure
// that `this` will still be safe - this function just lets us know this
// command was parsed.
_newPaneCommand . subcommand - > callback ( [ & , this ] ( ) {
// Buld the SplitPane action from the values we've parsed on the commandline.
auto splitPaneActionAndArgs = winrt : : make_self < implementation : : ActionAndArgs > ( ) ;
splitPaneActionAndArgs - > Action ( ShortcutAction : : SplitPane ) ;
auto args = winrt : : make_self < implementation : : SplitPaneArgs > ( ) ;
// _getNewTerminalArgs MUST be called before parsing any other options,
// as it might clear those options while finding the commandline
args - > TerminalArgs ( _getNewTerminalArgs ( _newPaneCommand ) ) ;
args - > SplitStyle ( SplitState : : Automatic ) ;
// Make sure to use the `Option`s here to check if they were set -
// _getNewTerminalArgs might reset them while parsing a commandline
if ( ( * _horizontalOption | | * _verticalOption ) & & ( _splitHorizontal ) )
{
if ( _splitHorizontal )
{
args - > SplitStyle ( SplitState : : Horizontal ) ;
}
else if ( _splitVertical )
{
args - > SplitStyle ( SplitState : : Horizontal ) ;
}
}
splitPaneActionAndArgs - > Args ( * args ) ;
_startupActions . push_back ( * splitPaneActionAndArgs ) ;
} ) ;
}
// Method Description:
// - Adds the `new-tab` subcommand and related options to the commandline parser.
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs : : _buildFocusTabParser ( )
{
_focusTabCommand = _app . add_subcommand ( " focus-tab " , NEEDS_LOC ( " Move focus to another tab " ) ) ;
auto * indexOpt = _focusTabCommand - > add_option ( " -t,--target " , _focusTabIndex , NEEDS_LOC ( " Move focus the tab at the given index " ) ) ;
auto * nextOpt = _focusTabCommand - > add_flag ( " -n,--next " ,
_focusNextTab ,
NEEDS_LOC ( " Move focus to the next tab " ) ) ;
auto * prevOpt = _focusTabCommand - > add_flag ( " -p,--previous " ,
_focusPrevTab ,
NEEDS_LOC ( " Move focus to the previous tab " ) ) ;
nextOpt - > excludes ( prevOpt ) ;
indexOpt - > excludes ( prevOpt ) ;
indexOpt - > excludes ( nextOpt ) ;
// When ParseCommand is called, if this subcommand was provided, this
// callback function will be triggered on the same thread. We can be sure
// that `this` will still be safe - this function just lets us know this
// command was parsed.
_focusTabCommand - > callback ( [ & , this ] ( ) {
// Buld the action from the values we've parsed on the commandline.
auto focusTabAction = winrt : : make_self < implementation : : ActionAndArgs > ( ) ;
if ( _focusTabIndex > = 0 )
{
focusTabAction - > Action ( ShortcutAction : : SwitchToTab ) ;
auto args = winrt : : make_self < implementation : : SwitchToTabArgs > ( ) ;
args - > TabIndex ( _focusTabIndex ) ;
focusTabAction - > Args ( * args ) ;
_startupActions . push_back ( * focusTabAction ) ;
}
else if ( _focusNextTab | | _focusPrevTab )
{
focusTabAction - > Action ( _focusNextTab ? ShortcutAction : : NextTab : ShortcutAction : : PrevTab ) ;
_startupActions . push_back ( * focusTabAction ) ;
}
} ) ;
}
// Method Description:
// - Add the `NewTerminalArgs` parameters to the given subcommand. This enables
// that subcommand to support all the properties in a NewTerminalArgs.
// Arguments:
// - subcommand: the command to add the args to.
// Return Value:
// - <none>
void AppCommandlineArgs : : _addNewTerminalArgs ( AppCommandlineArgs : : NewTerminalSubcommand & subcommand )
{
subcommand . profileNameOption = subcommand . subcommand - > add_option ( " -p,--profile " ,
_profileName ,
NEEDS_LOC ( " Open with the given profile. Accepts either the name or guid of a profile " ) ) ;
subcommand . startingDirectoryOption = subcommand . subcommand - > add_option ( " -d,--startingDirectory " ,
_startingDirectory ,
NEEDS_LOC ( " Open in the given directory instead of the profile's set startingDirectory " ) ) ;
2020-01-29 22:01:05 +01:00
// Using positionals_at_end allows us to support "wt new-tab -d wsl -d Ubuntu"
// without CLI11 thinking that we've specified -d twice.
// There's an alternate construction where we make all subcommands "prefix commands",
// which lets us get all remaining non-option args provided at the end, but that
// doesn't support "wt new-tab -- wsl -d Ubuntu -- sleep 10" because the first
// -- breaks out of the subcommand (instead of the subcommand options).
// See https://github.com/CLIUtils/CLI11/issues/417 for more info.
subcommand . commandlineOption = subcommand . subcommand - > add_option ( " command " , _commandline , NEEDS_LOC ( " An optional command, with arguments, to be spawned in the new tab or pane " ) ) ;
subcommand . subcommand - > positionals_at_end ( true ) ;
2020-01-27 16:34:12 +01:00
}
// Method Description:
// - Build a NewTerminalArgs instance from the data we've parsed
// Arguments:
// - <none>
// Return Value:
// - A fully initialized NewTerminalArgs corresponding to values we've currently parsed.
NewTerminalArgs AppCommandlineArgs : : _getNewTerminalArgs ( AppCommandlineArgs : : NewTerminalSubcommand & subcommand )
{
auto args = winrt : : make_self < implementation : : NewTerminalArgs > ( ) ;
2020-01-29 22:01:05 +01:00
if ( ! _commandline . empty ( ) )
2020-01-27 16:34:12 +01:00
{
2020-01-29 22:01:05 +01:00
std : : ostringstream cmdlineBuffer ;
for ( const auto & arg : _commandline )
2020-01-27 16:34:12 +01:00
{
2020-01-29 22:01:05 +01:00
if ( cmdlineBuffer . tellp ( ) ! = 0 )
2020-01-27 16:34:12 +01:00
{
2020-01-29 22:01:05 +01:00
// If there's already something in here, prepend a space
cmdlineBuffer < < ' ' ;
2020-01-27 16:34:12 +01:00
}
2020-01-29 22:01:05 +01:00
if ( arg . find ( " " ) ! = std : : string : : npos )
2020-01-27 16:34:12 +01:00
{
2020-01-29 22:01:05 +01:00
cmdlineBuffer < < ' " ' < < arg < < ' " ' ;
2020-01-27 16:34:12 +01:00
}
2020-01-29 22:01:05 +01:00
else
2020-01-27 16:34:12 +01:00
{
2020-01-29 22:01:05 +01:00
cmdlineBuffer < < arg ;
2020-01-27 16:34:12 +01:00
}
}
2020-01-29 22:01:05 +01:00
args - > Commandline ( winrt : : to_hstring ( cmdlineBuffer . str ( ) ) ) ;
2020-01-27 16:34:12 +01:00
}
if ( * subcommand . profileNameOption )
{
args - > Profile ( winrt : : to_hstring ( _profileName ) ) ;
}
if ( * subcommand . startingDirectoryOption )
{
args - > StartingDirectory ( winrt : : to_hstring ( _startingDirectory ) ) ;
}
return * args ;
}
// Method Description:
// - This function should return true if _no_ subcommands were parsed from the
// given commandline. In that case, we'll fall back to trying the commandline
// as a new tab command.
// Arguments:
// - <none>
// Return Value:
// - true if no sub commands were parsed.
bool AppCommandlineArgs : : _noCommandsProvided ( )
{
return ! ( * _newTabCommand . subcommand | |
* _focusTabCommand | |
* _newPaneCommand . subcommand ) ;
}
// Method Description:
// - Reset any state we might have accumulated back to its default values. Since
// we'll be re-using these members across the parsing of many commandlines, we
// need to make sure the state from one run doesn't pollute the following one.
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs : : _resetStateToDefault ( )
{
_profileName . clear ( ) ;
_startingDirectory . clear ( ) ;
_commandline . clear ( ) ;
_splitVertical = false ;
_splitHorizontal = false ;
_focusTabIndex = - 1 ;
_focusNextTab = false ;
_focusPrevTab = false ;
}
// Function Description:
// - Builds a list of Commandline objects for the given argc,argv. Each
// Commandline represents a single command to parse. These commands can be
// seperated by ";", which indicates the start of the next commandline. If the
// user would like to provide ';' in the text of the commandline, they can
// escape it as "\;".
// Arguments:
// - args: an array of arguments to parse into Commandlines
// Return Value:
// - a list of Commandline objects, where each one represents a single
// commandline to parse.
std : : vector < Commandline > AppCommandlineArgs : : BuildCommands ( winrt : : array_view < const winrt : : hstring > & args )
{
std : : vector < Commandline > commands ;
commands . emplace_back ( Commandline { } ) ;
// For each arg in argv:
// Check the string for a delimiter.
// * If there isn't a delimiter, add the arg to the current commandline.
// * If there is a delimiter, split the string at that delimiter. Add the
// first part of the string to the current command, and start a new
// command with the second bit.
for ( const auto & arg : args )
{
_addCommandsForArg ( commands , { arg } ) ;
}
return commands ;
}
// Function Description:
// - Builds a list of Commandline objects for the given argc,argv. Each
// Commandline represents a single command to parse. These commands can be
// seperated by ";", which indicates the start of the next commandline. If the
// user would like to provide ';' in the text of the commandline, they can
// escape it as "\;".
// Arguments:
// - argc: the number of arguments provided in argv
// - argv: a c-style array of wchar_t strings. These strings can include spaces in them.
// Return Value:
// - a list of Commandline objects, where each one represents a single
// commandline to parse.
std : : vector < Commandline > AppCommandlineArgs : : BuildCommands ( const std : : vector < const wchar_t * > & args )
{
std : : vector < Commandline > commands ;
// Initialize a first Commandline without a leading `wt.exe` argument. When
// we're run from the commandline, `wt.exe` (or whatever the exe's name is)
// will be the first argument passed to us
commands . resize ( 1 ) ;
// For each arg in argv:
// Check the string for a delimiter.
// * If there isn't a delimiter, add the arg to the current commandline.
// * If there is a delimiter, split the string at that delimiter. Add the
// first part of the string to the current command, ansd start a new
// command with the second bit.
for ( const auto & arg : args )
{
_addCommandsForArg ( commands , { arg } ) ;
}
return commands ;
}
// Function Description:
// - Update and append Commandline objects for the given arg to the given list
// of commands. Each Commandline represents a single command to parse. These
// commands can be seperated by ";", which indicates the start of the next
// commandline. If the user would like to provide ';' in the text of the
// commandline, they can escape it as "\;".
// - As we parse arg, if it doesn't contain a delimiter in it, we'll add it to
// the last command in commands. Otherwise, we'll generate a new Commandline
// object for each command in arg.
// Arguments:
// - commands: a list of Commandline objects to modify and append to
// - arg: a single argument that should be parsed into args to append to the
// current command, or create more Commandlines
// Return Value:
// <none>
void AppCommandlineArgs : : _addCommandsForArg ( std : : vector < Commandline > & commands , std : : wstring_view arg )
{
std : : wstring remaining { arg } ;
std : : wsmatch match ;
// Keep looking for matches until we've found no unescaped delimiters,
// or we've hit the end of the string.
std : : regex_search ( remaining , match , AppCommandlineArgs : : _commandDelimiterRegex ) ;
do
{
if ( match . empty ( ) )
{
// Easy case: no delimiter. Add it to the current command.
commands . back ( ) . AddArg ( remaining ) ;
break ;
}
else
{
// Harder case: There was a match.
const bool matchedFirstChar = match . position ( 0 ) = = 0 ;
// If the match was at the beginning of the string, then the
// next arg should be "", since there was no content before the
// delimiter. Otherwise, add one, since the regex will include
// the last character of the string before the delimiter.
const auto delimiterPosition = matchedFirstChar ? match . position ( 0 ) : match . position ( 0 ) + 1 ;
const auto nextArg = remaining . substr ( 0 , delimiterPosition ) ;
if ( ! nextArg . empty ( ) )
{
commands . back ( ) . AddArg ( nextArg ) ;
}
// Create a new commandline
commands . emplace_back ( Commandline { } ) ;
// Initialize it with "wt.exe" as the first arg, as if that command
// was passed individually by the user on the commandline.
commands . back ( ) . AddArg ( std : : wstring { AppCommandlineArgs : : PlaceholderExeName } ) ;
// Look for the next match in the string, but updating our
// remaining to be the text after the match.
remaining = match . suffix ( ) . str ( ) ;
std : : regex_search ( remaining , match , AppCommandlineArgs : : _commandDelimiterRegex ) ;
}
} while ( ! remaining . empty ( ) ) ;
}
// Method Description:
// - Returns the deque of actions we've buffered as a result of parsing commands.
// Arguments:
// - <none>
// Return Value:
// - the deque of actions we've buffered as a result of parsing commands.
std : : deque < winrt : : TerminalApp : : ActionAndArgs > & AppCommandlineArgs : : GetStartupActions ( )
{
return _startupActions ;
}
// Method Description:
// - Get the string of text that should be displayed to the user on exit. This
// is usually helpful for cases where the user entered some sort of invalid
// commandline. It's additionally also used when the user has requested the
// help text.
// Arguments:
// - <none>
// Return Value:
// - The help text, or an error message, generated from parsing the input
// provided by the user.
const std : : string & AppCommandlineArgs : : GetExitMessage ( )
{
return _exitMessage ;
}
// Method Description:
// - Ensure that the first command in our list of actions is a NewTab action.
// This makes sure that if the user passes a commandline like "wt split-pane
// -H", we _first_ create a new tab, so there's always at least one tab.
// - If the first command in our queue of actions is a NewTab action, this does
// nothing.
// - This should only be called once - if the first NewTab action is popped from
// our _startupActions, calling this again will add another.
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs : : ValidateStartupCommands ( )
{
// If we parsed no commands, or the first command we've parsed is not a new
// tab action, prepend a new-tab command to the front of the list.
if ( _startupActions . empty ( ) | |
_startupActions . front ( ) . Action ( ) ! = ShortcutAction : : NewTab )
{
// Build the NewTab action from the values we've parsed on the commandline.
auto newTabAction = winrt : : make_self < implementation : : ActionAndArgs > ( ) ;
newTabAction - > Action ( ShortcutAction : : NewTab ) ;
auto args = winrt : : make_self < implementation : : NewTabArgs > ( ) ;
auto newTerminalArgs = winrt : : make_self < implementation : : NewTerminalArgs > ( ) ;
args - > TerminalArgs ( * newTerminalArgs ) ;
newTabAction - > Args ( * args ) ;
_startupActions . push_front ( * newTabAction ) ;
}
}