// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" #include "AppCommandlineArgs.h" #include "../types/inc/utils.hpp" #include using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace TerminalApp; // Either a ; at the start of a line, or a ; preceded 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(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, so copy out the args here. // * When we're using the vector 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 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(); } // attempt to parse the commandline prefix of the form [options][subcommand] _app.parse(args); auto remainingParams = _app.remaining_size(); // If we parsed the commandline, and _no_ subcommands were provided, try // parse the remaining suffix as a "new-tab" command. if (_noCommandsProvided()) { _newTabCommand.subcommand->parse(args); remainingParams = _newTabCommand.subcommand->remaining_size(); } // if after parsing the prefix and (optionally) the implicit tab subcommand // we still have unparsed parameters we need to fail if (remainingParams > 0) { throw CLI::ExtrasError(args); } } catch (const CLI::ParseError& e) { 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(); } // We're displaying an error message - we should always exit instead of // actually starting the Terminal. _shouldExitEarly = true; return result; } // Method Description: // - Add each subcommand and options to the commandline parser. // Arguments: // - // Return Value: // - void AppCommandlineArgs::_buildParser() { // We define or parser as a prefix command, to support "implicit new tab subcommand" scenario. // In this scenario we will try to parse the prefix that contains parameters like launch mode, // but will not encounter an explicit command. // Instead we will encounter an argument that doesn't belong to the prefix indicating the prefix is over. // Then we will try to parse the remaining arguments as a new tab subcommand. // E.g., for "wt.exe -M -d c:/", we will use -M for the launch mode, but once we will encounter -d // we will know that the prefix is over and try to handle the suffix as a new tab subcommand _app.prefix_command(); // -v,--version: Displays version info auto versionCallback = [this](int64_t /*count*/) { // Set our message to display the application name and the current version. _exitMessage = fmt::format("{0}\n{1}", til::u16u8(CascadiaSettings::ApplicationDisplayName()), til::u16u8(CascadiaSettings::ApplicationVersion())); // Theoretically, we don't need to exit now, since this isn't really // an error case. However, in practice, it feels weird to have `wt // -v` open a new tab, and makes enough sense that `wt -v ; // split-pane` (or whatever) just displays the version and exits. _shouldExitEarly = true; }; _app.add_flag_function("-v,--version", versionCallback, RS_A(L"CmdVersionDesc")); // Launch mode related flags // -M,--maximized: Maximizes the window on launch // -F,--fullscreen: Fullscreens the window on launch // -f,--focus: Sets the terminal into the Focus mode // While fullscreen excludes both maximized and focus mode, the user can combine between the maximized and focused (-fM) auto maximizedCallback = [this](int64_t /*count*/) { _launchMode = (_launchMode.has_value() && _launchMode.value() == LaunchMode::FocusMode) ? LaunchMode::MaximizedFocusMode : LaunchMode::MaximizedMode; }; auto fullscreenCallback = [this](int64_t /*count*/) { _launchMode = LaunchMode::FullscreenMode; }; auto focusCallback = [this](int64_t /*count*/) { _launchMode = (_launchMode.has_value() && _launchMode.value() == LaunchMode::MaximizedMode) ? LaunchMode::MaximizedFocusMode : LaunchMode::FocusMode; }; auto maximized = _app.add_flag_function("-M,--maximized", maximizedCallback, RS_A(L"CmdMaximizedDesc")); auto fullscreen = _app.add_flag_function("-F,--fullscreen", fullscreenCallback, RS_A(L"CmdFullscreenDesc")); auto focus = _app.add_flag_function("-f,--focus", focusCallback, RS_A(L"CmdFocusDesc")); maximized->excludes(fullscreen); focus->excludes(fullscreen); _app.add_option("-w,--window", _windowTarget, RS_A(L"CmdWindowTargetArgDesc")); _app.add_option("-s,--saved", _loadPersistedLayoutIdx, RS_A(L"CmdSavedLayoutArgDesc")); // Subcommands _buildNewTabParser(); _buildSplitPaneParser(); _buildFocusTabParser(); _buildMoveFocusParser(); _buildMovePaneParser(); _buildSwapPaneParser(); _buildFocusPaneParser(); } // Method Description: // - Adds the `new-tab` subcommand and related options to the commandline parser. // - Additionally adds the `nt` subcommand, which is just a shortened version of `new-tab` // Arguments: // - // Return Value: // - void AppCommandlineArgs::_buildNewTabParser() { _newTabCommand.subcommand = _app.add_subcommand("new-tab", RS_A(L"CmdNewTabDesc")); _newTabShort.subcommand = _app.add_subcommand("nt", RS_A(L"CmdNTDesc")); auto setupSubcommand = [this](auto& subcommand) { _addNewTerminalArgs(subcommand); // 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. subcommand.subcommand->callback([&, this]() { // Build the NewTab action from the values we've parsed on the commandline. ActionAndArgs newTabAction{}; newTabAction.Action(ShortcutAction::NewTab); // _getNewTerminalArgs MUST be called before parsing any other options, // as it might clear those options while finding the commandline NewTabArgs args{ _getNewTerminalArgs(subcommand) }; newTabAction.Args(args); _startupActions.push_back(newTabAction); }); }; setupSubcommand(_newTabCommand); setupSubcommand(_newTabShort); } // Method Description: // - Adds the `split-pane` subcommand and related options to the commandline parser. // - Additionally adds the `sp` subcommand, which is just a shortened version of `split-pane` // Arguments: // - // Return Value: // - void AppCommandlineArgs::_buildSplitPaneParser() { _newPaneCommand.subcommand = _app.add_subcommand("split-pane", RS_A(L"CmdSplitPaneDesc")); _newPaneShort.subcommand = _app.add_subcommand("sp", RS_A(L"CmdSPDesc")); auto setupSubcommand = [this](auto& subcommand) { _addNewTerminalArgs(subcommand); subcommand._horizontalOption = subcommand.subcommand->add_flag("-H,--horizontal", _splitHorizontal, RS_A(L"CmdSplitPaneHorizontalArgDesc")); subcommand._verticalOption = subcommand.subcommand->add_flag("-V,--vertical", _splitVertical, RS_A(L"CmdSplitPaneVerticalArgDesc")); subcommand._verticalOption->excludes(subcommand._horizontalOption); auto* sizeOpt = subcommand.subcommand->add_option("-s,--size", _splitPaneSize, RS_A(L"CmdSplitPaneSizeArgDesc")); subcommand._duplicateOption = subcommand.subcommand->add_flag("-D,--duplicate", _splitDuplicate, RS_A(L"CmdSplitPaneDuplicateArgDesc")); sizeOpt->check(CLI::Range(0.01f, 0.99f)); // 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. subcommand.subcommand->callback([&, this]() { // Build the SplitPane action from the values we've parsed on the commandline. ActionAndArgs splitPaneActionAndArgs{}; splitPaneActionAndArgs.Action(ShortcutAction::SplitPane); // _getNewTerminalArgs MUST be called before parsing any other options, // as it might clear those options while finding the commandline auto terminalArgs{ _getNewTerminalArgs(subcommand) }; auto style{ SplitDirection::Automatic }; // Make sure to use the `Option`s here to check if they were set - // _getNewTerminalArgs might reset them while parsing a commandline if ((*subcommand._horizontalOption || *subcommand._verticalOption)) { if (_splitHorizontal) { style = SplitDirection::Down; } else if (_splitVertical) { style = SplitDirection::Right; } } const auto splitMode{ subcommand._duplicateOption && _splitDuplicate ? SplitType::Duplicate : SplitType::Manual }; SplitPaneArgs args{ splitMode, style, _splitPaneSize, terminalArgs }; splitPaneActionAndArgs.Args(args); _startupActions.push_back(splitPaneActionAndArgs); }); }; setupSubcommand(_newPaneCommand); setupSubcommand(_newPaneShort); } // Method Description: // - Adds the `move-pane` subcommand and related options to the commandline parser. // - Additionally adds the `mp` subcommand, which is just a shortened version of `move-pane` // Arguments: // - // Return Value: // - void AppCommandlineArgs::_buildMovePaneParser() { _movePaneCommand = _app.add_subcommand("move-pane", RS_A(L"CmdMovePaneDesc")); _movePaneShort = _app.add_subcommand("mp", RS_A(L"CmdMPDesc")); auto setupSubcommand = [this](auto* subcommand) { subcommand->add_option("-t,--tab", _movePaneTabIndex, RS_A(L"CmdMovePaneTabArgDesc")); // 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. subcommand->callback([&, this]() { // Build the action from the values we've parsed on the commandline. ActionAndArgs movePaneAction{}; if (_movePaneTabIndex >= 0) { movePaneAction.Action(ShortcutAction::MovePane); MovePaneArgs args{ static_cast(_movePaneTabIndex) }; movePaneAction.Args(args); _startupActions.push_back(movePaneAction); } }); }; setupSubcommand(_movePaneCommand); setupSubcommand(_movePaneShort); } // Method Description: // - Adds the `focus-tab` subcommand and related options to the commandline parser. // - Additionally adds the `ft` subcommand, which is just a shortened version of `focus-tab` // Arguments: // - // Return Value: // - void AppCommandlineArgs::_buildFocusTabParser() { _focusTabCommand = _app.add_subcommand("focus-tab", RS_A(L"CmdFocusTabDesc")); _focusTabShort = _app.add_subcommand("ft", RS_A(L"CmdFTDesc")); auto setupSubcommand = [this](auto* subcommand) { auto* indexOpt = subcommand->add_option("-t,--target", _focusTabIndex, RS_A(L"CmdFocusTabTargetArgDesc")); auto* nextOpt = subcommand->add_flag("-n,--next", _focusNextTab, RS_A(L"CmdFocusTabNextArgDesc")); auto* prevOpt = subcommand->add_flag("-p,--previous", _focusPrevTab, RS_A(L"CmdFocusTabPrevArgDesc")); 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. subcommand->callback([&, this]() { // Build the action from the values we've parsed on the commandline. ActionAndArgs focusTabAction{}; if (_focusTabIndex >= 0) { focusTabAction.Action(ShortcutAction::SwitchToTab); SwitchToTabArgs args{ static_cast(_focusTabIndex) }; focusTabAction.Args(args); _startupActions.push_back(focusTabAction); } else if (_focusNextTab || _focusPrevTab) { focusTabAction.Action(_focusNextTab ? ShortcutAction::NextTab : ShortcutAction::PrevTab); // GH#10070 - make sure to not use the MRU order when switching // tabs on the commandline. That wouldn't make any sense! focusTabAction.Args(_focusNextTab ? static_cast(NextTabArgs(TabSwitcherMode::Disabled)) : static_cast(PrevTabArgs(TabSwitcherMode::Disabled))); _startupActions.push_back(std::move(focusTabAction)); } }); }; setupSubcommand(_focusTabCommand); setupSubcommand(_focusTabShort); } static const std::map focusDirectionMap = { { "left", FocusDirection::Left }, { "right", FocusDirection::Right }, { "up", FocusDirection::Up }, { "down", FocusDirection::Down }, { "previous", FocusDirection::Previous }, { "nextInOrder", FocusDirection::NextInOrder }, { "previousInOrder", FocusDirection::PreviousInOrder }, { "first", FocusDirection::First }, }; // Method Description: // - Adds the `move-focus` subcommand and related options to the commandline parser. // - Additionally adds the `mf` subcommand, which is just a shortened version of `move-focus` // Arguments: // - // Return Value: // - void AppCommandlineArgs::_buildMoveFocusParser() { _moveFocusCommand = _app.add_subcommand("move-focus", RS_A(L"CmdMoveFocusDesc")); _moveFocusShort = _app.add_subcommand("mf", RS_A(L"CmdMFDesc")); auto setupSubcommand = [this](auto* subcommand) { auto* directionOpt = subcommand->add_option("direction", _moveFocusDirection, RS_A(L"CmdMoveFocusDirectionArgDesc")); directionOpt->transform(CLI::CheckedTransformer(focusDirectionMap, CLI::ignore_case)); directionOpt->required(); // 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. subcommand->callback([&, this]() { if (_moveFocusDirection != FocusDirection::None) { MoveFocusArgs args{ _moveFocusDirection }; ActionAndArgs actionAndArgs{}; actionAndArgs.Action(ShortcutAction::MoveFocus); actionAndArgs.Args(args); _startupActions.push_back(std::move(actionAndArgs)); } }); }; setupSubcommand(_moveFocusCommand); setupSubcommand(_moveFocusShort); } // Method Description: // - Adds the `swap-pane` subcommand and related options to the commandline parser. // Arguments: // - // Return Value: // - void AppCommandlineArgs::_buildSwapPaneParser() { _swapPaneCommand = _app.add_subcommand("swap-pane", RS_A(L"CmdSwapPaneDesc")); auto setupSubcommand = [this](auto* subcommand) { auto* directionOpt = subcommand->add_option("direction", _swapPaneDirection, RS_A(L"CmdSwapPaneDirectionArgDesc")); directionOpt->transform(CLI::CheckedTransformer(focusDirectionMap, CLI::ignore_case)); directionOpt->required(); // 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. subcommand->callback([&, this]() { if (_swapPaneDirection != FocusDirection::None) { SwapPaneArgs args{ _swapPaneDirection }; ActionAndArgs actionAndArgs{}; actionAndArgs.Action(ShortcutAction::SwapPane); actionAndArgs.Args(args); _startupActions.push_back(std::move(actionAndArgs)); } }); }; setupSubcommand(_swapPaneCommand); } // Method Description: // - Adds the `focus-pane` subcommand and related options to the commandline parser. // - Additionally adds the `fp` subcommand, which is just a shortened version of `focus-pane` // Arguments: // - // Return Value: // - void AppCommandlineArgs::_buildFocusPaneParser() { _focusPaneCommand = _app.add_subcommand("focus-pane", RS_A(L"CmdFocusPaneDesc")); _focusPaneShort = _app.add_subcommand("fp", RS_A(L"CmdFPDesc")); auto setupSubcommand = [this](auto* subcommand) { auto* targetOpt = subcommand->add_option("-t,--target", _focusPaneTarget, RS_A(L"CmdFocusPaneTargetArgDesc")); targetOpt->required(); targetOpt->check(CLI::NonNegativeNumber); // 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. subcommand->callback([&, this]() { // Build the action from the values we've parsed on the commandline. if (_focusPaneTarget >= 0) { ActionAndArgs focusPaneAction{}; focusPaneAction.Action(ShortcutAction::FocusPane); FocusPaneArgs args{ static_cast(_focusPaneTarget) }; focusPaneAction.Args(args); _startupActions.push_back(focusPaneAction); } }); }; setupSubcommand(_focusPaneCommand); setupSubcommand(_focusPaneShort); } // 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: // - void AppCommandlineArgs::_addNewTerminalArgs(AppCommandlineArgs::NewTerminalSubcommand& subcommand) { subcommand.profileNameOption = subcommand.subcommand->add_option("-p,--profile", _profileName, RS_A(L"CmdProfileArgDesc")); subcommand.startingDirectoryOption = subcommand.subcommand->add_option("-d,--startingDirectory", _startingDirectory, RS_A(L"CmdStartingDirArgDesc")); subcommand.titleOption = subcommand.subcommand->add_option("--title", _startingTitle, RS_A(L"CmdTitleArgDesc")); subcommand.tabColorOption = subcommand.subcommand->add_option("--tabColor", _startingTabColor, RS_A(L"CmdTabColorArgDesc")); subcommand.suppressApplicationTitleOption = subcommand.subcommand->add_flag( "--suppressApplicationTitle,!--useApplicationTitle", _suppressApplicationTitle, RS_A(L"CmdSuppressApplicationTitleDesc")); subcommand.colorSchemeOption = subcommand.subcommand->add_option("--colorScheme", _startingColorScheme, RS_A(L"CmdColorSchemeArgDesc")); // 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, RS_A(L"CmdCommandArgDesc")); subcommand.subcommand->positionals_at_end(true); } // Method Description: // - Build a NewTerminalArgs instance from the data we've parsed // Arguments: // - // Return Value: // - A fully initialized NewTerminalArgs corresponding to values we've currently parsed. NewTerminalArgs AppCommandlineArgs::_getNewTerminalArgs(AppCommandlineArgs::NewTerminalSubcommand& subcommand) { NewTerminalArgs args{}; if (!_commandline.empty()) { std::ostringstream cmdlineBuffer; for (const auto& arg : _commandline) { if (cmdlineBuffer.tellp() != 0) { // If there's already something in here, prepend a space cmdlineBuffer << ' '; } if (arg.find(" ") != std::string::npos) { cmdlineBuffer << '"' << arg << '"'; } else { cmdlineBuffer << arg; } } args.Commandline(winrt::to_hstring(cmdlineBuffer.str())); } if (*subcommand.profileNameOption) { args.Profile(winrt::to_hstring(_profileName)); } if (*subcommand.startingDirectoryOption) { args.StartingDirectory(winrt::to_hstring(_startingDirectory)); } if (*subcommand.titleOption) { args.TabTitle(winrt::to_hstring(_startingTitle)); } if (*subcommand.tabColorOption) { try { // This is gonna throw whenever the string that's currently being parsed // isn't a valid hex string. Let's just eat anything this throws because // we should only lock in the TabColor arg when the user gives a valid hex // str, and we shouldn't crash when the user gives us anything else. const auto tabColor = Microsoft::Console::Utils::ColorFromHexString(_startingTabColor); args.TabColor(static_cast(tabColor)); } catch (...) { } } if (*subcommand.suppressApplicationTitleOption) { args.SuppressApplicationTitle(_suppressApplicationTitle); } if (*subcommand.colorSchemeOption) { args.ColorScheme(winrt::to_hstring(_startingColorScheme)); } 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: // - // Return Value: // - true if no sub commands were parsed. bool AppCommandlineArgs::_noCommandsProvided() { return !(*_newTabCommand.subcommand || *_newTabShort.subcommand || *_focusTabCommand || *_focusTabShort || *_moveFocusCommand || *_moveFocusShort || *_movePaneCommand || *_movePaneShort || *_swapPaneCommand || *_focusPaneCommand || *_focusPaneShort || *_newPaneShort.subcommand || *_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: // - // Return Value: // - void AppCommandlineArgs::_resetStateToDefault() { _profileName.clear(); _startingDirectory.clear(); _startingTitle.clear(); _startingTabColor.clear(); _commandline.clear(); _suppressApplicationTitle = false; _splitVertical = false; _splitHorizontal = false; _splitPaneSize = 0.5f; _splitDuplicate = false; _movePaneTabIndex = -1; _focusTabIndex = -1; _focusNextTab = false; _focusPrevTab = false; _moveFocusDirection = FocusDirection::None; _swapPaneDirection = FocusDirection::None; _focusPaneTarget = -1; _loadPersistedLayoutIdx = -1; // DON'T clear _launchMode here! This will get called once for every // subcommand, so we don't want `wt -F new-tab ; split-pane` clearing out // the "global" fullscreen flag (-F). // Same with _windowTarget. } // 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 // separated 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 AppCommandlineArgs::BuildCommands(winrt::array_view& args) { std::vector 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 // separated 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 AppCommandlineArgs::BuildCommands(const std::vector& args) { std::vector 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, and 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 separated 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: // void AppCommandlineArgs::_addCommandsForArg(std::vector& 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: // - // Return Value: // - the deque of actions we've buffered as a result of parsing commands. std::vector& AppCommandlineArgs::GetStartupActions() { return _startupActions; } // Method Description: // - Returns whether we should start listening for inbound PTY connections // coming from the operating system default application feature. // Arguments: // - // Return Value: // - True if the listener should be started. False otherwise. bool AppCommandlineArgs::IsHandoffListener() const noexcept { return _isHandoffListener; } // 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: // - // 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: // - Returns true if we should exit the application before even starting the // window. We might want to do this if we're displaying an error message or // the version string, or if we want to open the settings file. // Arguments: // - // Return Value: // - true iff we should exit the application before even starting the window bool AppCommandlineArgs::ShouldExitEarly() const noexcept { return _shouldExitEarly; } // 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: // - // Return Value: // - void AppCommandlineArgs::ValidateStartupCommands() { // Only check over the actions list for the potential to add a new-tab // command if we are not starting for the purposes of receiving an inbound // handoff connection from the operating system. if (!_isHandoffListener) { // 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. NewTerminalArgs newTerminalArgs{}; NewTabArgs args{ newTerminalArgs }; ActionAndArgs newTabAction{ ShortcutAction::NewTab, args }; // push the arg onto the front _startupActions.insert(_startupActions.begin(), 1, newTabAction); } } } std::optional AppCommandlineArgs::GetPersistedLayoutIdx() const noexcept { return _loadPersistedLayoutIdx >= 0 ? std::optional{ static_cast(_loadPersistedLayoutIdx) } : std::nullopt; } std::optional AppCommandlineArgs::GetLaunchMode() const noexcept { return _launchMode; } // Method Description: // - Attempts to parse an array of commandline args into a list of // commands to execute, and then parses these commands. As commands are // successfully parsed, they will generate ShortcutActions for us to be // able to execute. If we fail to parse any commands, we'll return the // error code from the failure to parse that command, and stop processing // additional commands. // - The first arg in args should be the program name "wt" (or some variant). It // will be ignored during parsing. // Arguments: // - args: an array of strings to process as a commandline. These args can contain spaces // Return Value: // - 0 if the commandline was successfully parsed int AppCommandlineArgs::ParseArgs(winrt::array_view& args) { for (const auto& arg : args) { if (arg == L"-Embedding") { _isHandoffListener = true; return 0; } } auto commands = ::TerminalApp::AppCommandlineArgs::BuildCommands(args); for (auto& cmdBlob : commands) { // On one hand, it seems like we should be able to have one // AppCommandlineArgs for parsing all of them, and collect the // results one at a time. // // On the other hand, re-using a CLI::App seems to leave state from // previous parsings around, so we could get mysterious behavior // where one command affects the values of the next. // // From https://cliutils.github.io/CLI11/book/chapters/options.html: // > If that option is not given, CLI11 will not touch the initial // > value. This allows you to set up defaults by simply setting // > your value beforehand. // // So we pretty much need the to either manually reset the state // each command, or build new ones. const auto result = ParseCommand(cmdBlob); // If this succeeded, result will be 0. Otherwise, the caller should // exit(result), to exit the program. if (result != 0) { return result; } } // If all the args were successfully parsed, we'll have some commands // built in _appArgs, which we'll use when the application starts up. return 0; } // Method Description: // - Attempts to parse an array of commandline args into a list of // commands to execute, and then parses these commands. As commands are // successfully parsed, they will generate ShortcutActions for us to be // able to execute. If we fail to parse any commands, we'll return the // error code from the failure to parse that command, and stop processing // additional commands. // - The first arg in args should be the program name "wt" (or some variant). It // will be ignored during parsing. // Arguments: // - args: ExecuteCommandlineArgs describing the command line to parse // Return Value: // - 0 if the commandline was successfully parsed int AppCommandlineArgs::ParseArgs(const winrt::Microsoft::Terminal::Settings::Model::ExecuteCommandlineArgs& args) { if (!args || args.Commandline().empty()) { return 0; } // Convert the commandline into an array of args with // CommandLineToArgvW, similar to how the app typically does when // called from the commandline. int argc = 0; wil::unique_any argv{ CommandLineToArgvW(args.Commandline().c_str(), &argc) }; if (argv) { std::vector args; // Make sure the first argument is wt.exe, because ParseArgs will // always skip the program name. The particular value of this first // string doesn't terribly matter. args.emplace_back(L"wt.exe"); for (auto& elem : wil::make_range(argv.get(), argc)) { args.emplace_back(elem); } winrt::array_view argsView{ args }; return ParseArgs(argsView); } return 0; } // Method Description: // - Allows disabling addition of help-related info in the exit message // Arguments: // - // Return Value: // - void AppCommandlineArgs::DisableHelpInExitMessage() { _app.set_help_flag(); _app.set_help_all_flag(); } // Method Description: // - Resets the state to allow external consumers to reuse this instance // Arguments: // - // Return Value: // - void AppCommandlineArgs::FullResetState() { _resetStateToDefault(); _currentCommandline = nullptr; _launchMode = std::nullopt; _startupActions.clear(); _exitMessage = ""; _shouldExitEarly = false; _isHandoffListener = false; _windowTarget = {}; } std::string_view AppCommandlineArgs::GetTargetWindow() const noexcept { return _windowTarget; }