Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

normalize command line arguments #4724

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
159 changes: 136 additions & 23 deletions src/cascadia/WindowsTerminal/AppHost.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ AppHost::~AppHost()
}

// Method Description:
// - Retrieve any commandline args passed on the commandline, and pass them to
// - Retrieve the normalized command line arguments, and pass them to
// the app logic for processing.
// - If the logic determined there's an error while processing that commandline,
// display a message box to the user with the text of the error, and exit.
Expand All @@ -78,38 +78,151 @@ AppHost::~AppHost()
// - <none>
void AppHost::_HandleCommandlineArgs()
{
if (auto commandline{ GetCommandLineW() })
const auto args{ _NormalizedArgs() };
if (!args.empty())
{
int argc = 0;
const auto result = _logic.SetStartupCommandline({ args });
const auto message = _logic.EarlyExitMessage();
if (!message.empty())
{
const auto displayHelp = result == 0;
const auto messageTitle = displayHelp ? IDS_HELP_DIALOG_TITLE : IDS_ERROR_DIALOG_TITLE;
const auto messageIcon = displayHelp ? MB_ICONWARNING : MB_ICONERROR;
// TODO:GH#4134: polish this dialog more, to make the text more
// like msiexec /?
MessageBoxW(nullptr,
message.data(),
GetStringResource(messageTitle).data(),
MB_OK | messageIcon);

ExitProcess(result);
}
}
}

// Method Description:
// - Retrieve the command line arguments passed,
// prepend the full name of the application in case it was missing (GH#4170),
// and return the arguments as vector of hstrings.
// Arguments:
// - <none>
// Return Value:
// - Program arguments as vector of hstrings. If the function fails it returns an empty vector.
std::vector<winrt::hstring> AppHost::_NormalizedArgs() const noexcept
{
try
{
// get the full name of the terminal app
std::wstring appPath{ wil::GetModuleFileNameW<std::wstring>(nullptr) };

// Get the argv, and turn them into a hstring array to pass to the app.
wil::unique_any<LPWSTR*, decltype(&::LocalFree), ::LocalFree> argv{ CommandLineToArgvW(commandline, &argc) };
if (argv)
// get the program arguments
std::vector<std::wstring> wstrArgs{};
if (!_GetArgs(wstrArgs))
{
std::vector<winrt::hstring> args;
for (auto& elem : wil::make_range(argv.get(), argc))
return {};
}

// check if the first argument is the own call of the terminal app
std::vector<winrt::hstring> hstrArgs{};
hstrArgs.reserve(wstrArgs.size() + 1);
if (wstrArgs.empty())
{
hstrArgs.emplace_back(appPath);
}
else
{
// If the terminal app is in the current directory or in the
// PATH environment then it might be called with its base name only.
// So, the base name is the only part of the path we can compare.
// But it's even worse. The base name could be `WindowsTerminal` if
// called from within the IDE, it could be `wt` if the alias was called,
// or `wtd` for the alias of a developer's build. Thus, we only know
// that the base name has to have a length of at least two characters,
// and it has to begin with 'w' or 'W'.
const std::filesystem::path arg0path{ wstrArgs.front() };
const auto arg0name{ arg0path.stem().wstring() };
if (arg0name.length() < 2U || std::towlower(arg0name.front()) != L'w')
german-one marked this conversation as resolved.
Show resolved Hide resolved
{
args.emplace_back(elem);
hstrArgs.emplace_back(appPath);
}
}

const auto result = _logic.SetStartupCommandline({ args });
const auto message = _logic.EarlyExitMessage();
if (!message.empty())
std::for_each(wstrArgs.cbegin(), wstrArgs.cend(), [&hstrArgs](const auto& elem) { hstrArgs.emplace_back(elem); });
return hstrArgs;
}
catch (...)
{
return {};
}
}

// Method Description:
// - Retrieve the command line.
// - Use our own algorithm to tokenize it because `CommandLineToArgvW` treats \"
// as an escape sequence to preserve the quotation mark (GH#4571)
// - Populate the arguments as vector of wstrings.
// Arguments:
// - args: Reference to a vector of wstrings which receives the arguments.
// Return Value:
// - `true` if the function succeeds, `false` if retrieving the coomand line failed.
// Mothods used may throw.
bool AppHost::_GetArgs(std::vector<std::wstring>& args) const
{
// get the command line
const wchar_t* const cmdLnPtr{ GetCommandLineW() };
if (!cmdLnPtr)
{
return false;
}

std::wstring cmdLn{ cmdLnPtr };
auto cmdLnEnd{ cmdLn.end() }; // end of still valid content in the command line
auto iter{ cmdLn.begin() }; // iterator pointing to the current position in the command line
auto argBegin{ iter }; // iterator pointing to the begin of an argument
auto quoted{ false }; // indicates whether a substring is quoted
auto within{ false }; // indicates whether the current character is inside of an argument

args.reserve(gsl::narrow_cast<size_t>(64U)); // pre-allocate some space for the string handles
while (iter < cmdLnEnd)
{
auto increment{ true }; // used to avoid iterator incrementation if a quotation mark has been removed
switch (*iter)
{
case L' ': // space and tab are the usual separators for arguments
case L'\t':
if (!quoted && within)
{
within = false;
args.emplace_back(argBegin, iter);
}
break;

case L'"': // quotation marks need special handling
std::move(iter + 1, cmdLnEnd, iter); // move the rest of the command line to the left to overwrite the quote
--cmdLnEnd; // the string isn't shorter but its valid content is, we can't just resize without potentially invalidate iterators
quoted = !quoted;
increment = false; // indicate that iter shall not be incremented because it already points to a new character
default: // any other character (including quotation mark)
if (!within)
{
const auto displayHelp = result == 0;
const auto messageTitle = displayHelp ? IDS_HELP_DIALOG_TITLE : IDS_ERROR_DIALOG_TITLE;
const auto messageIcon = displayHelp ? MB_ICONWARNING : MB_ICONERROR;
// TODO:GH#4134: polish this dialog more, to make the text more
// like msiexec /?
MessageBoxW(nullptr,
message.data(),
GetStringResource(messageTitle).data(),
MB_OK | messageIcon);

ExitProcess(result);
within = true;
argBegin = iter;
}
break;
}

if (increment)
{
++iter;
}
}

if (within)
{
args.emplace_back(argBegin, iter);
}

return true;
}

// Method Description:
Expand Down
2 changes: 2 additions & 0 deletions src/cascadia/WindowsTerminal/AppHost.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class AppHost
winrt::TerminalApp::AppLogic _logic;

void _HandleCommandlineArgs();
std::vector<winrt::hstring> _NormalizedArgs() const noexcept;
bool _GetArgs(std::vector<std::wstring>& args) const;

void _HandleCreateWindow(const HWND hwnd, RECT proposedRect, winrt::TerminalApp::LaunchMode& launchMode);
void _UpdateTitleBarContent(const winrt::Windows::Foundation::IInspectable& sender,
Expand Down