Repository: microsoft/terminal (language: cpp)

Issue:
[wt.exe] Add support for appending a commandline to a profile's commandline 
<!-- 
🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨

I ACKNOWLEDGE THE FOLLOWING BEFORE PROCEEDING:
1. If I delete this entire template and go my own path, the core team may close my issue without further explanation or engagement.
2. If I list multiple bugs/concerns in this one issue, the core team may close my issue without further explanation or engagement.
3. If I write an issue that has many duplicates, the core team may close my issue without further explanation or engagement (and without necessarily spending time to find the exact duplicate ID number).
4. If I leave the title incomplete when filing the issue, the core team may close my issue without further explanation or engagement.
5. If I file something completely blank in the body, the core team may close my issue without further explanation or engagement.

All good? Then proceed!
-->

# Description of the new feature/enhancement
After the #5090 update it would be great if we could do something like this:
```
wt "yarn some-command"; split-pane -V -d ./some-dir "yarn some-other-command"
```

It would enable us to execute commands in the Windows Terminal sessions and it can be used as any normal console terminal right now.
<!-- 
A clear and concise description of what the problem is that the new feature would solve.
Describe why and how a user would use this new functionality (if applicable).
-->




Discussion / hints:
Uh, that's literally how it's supposed to work now... [This doc](https://github.com/microsoft/terminal/blob/master/doc/user-docs/UsingCommandlineArguments.md#opening-with-multiple-panes) has some examples.

Are you seeing some sort of error when you try that? Are you running the commandline from `cmd`, `powershell`, `pwsh`, `bash`, something else?
@zadjii-msft The examples you gave are for different terminal programs to be assigned to the different views.

My example with `yarn` was that `yarn` manager had to be executed with the default terminal profile, not that `yarn` itself is a terminal.

`wt cmd "dir"` -> WT window running a cmd instance and no `dir` command execution.
`wt "dir"` -> ``[error 0x80070002 when launching `dir']``

My default terminal profile is using `git bash`, but I've tested it out also with `cmd`.
oh oh oh I think I see what's happening. `yarn` isn't a Windows executable, it's a WSL one, right? The `commandline` argument is just a _Windows_ commandline to start, not "a commandline to run in the profile that's spawned".

You can also see this in the `dir` example you gave. `dir` is a "cmd intrinsic", which means it's a feature of `cmd.exe` itself, there's not a `dir.exe` that cmd spawns. So you can't just `CreateProcess("dir",...)`, because there _isn't a dir.exe to spawn_. You need to launch `cmd` to be able to run dir. As a `wt` commandline, that'd be something like `wt cmd /k dir`.

So, for the `yarn` case, you'd probably want `wt wsl yarn some-command`

**EDIT**: Okay I'm seeing now the update to your post to include notes about "git bash". I'm not super familiar with how git bash works unfortunately, but I'd presume that you'd need to use the following commandline:

```sh
wt "C:/Program Files/Git/bin/bash.exe" yarn some-command
```

Here, I'm assuming that `"C:/Program Files/Git/bin/bash.exe" yarn some-command` will run `yarn some-command` in git bash. I don't know if that path is totally appropriate, or if `bash.exe` requires extra arguments to launch a child process appropriately. 
I've tested it. It works, but every time I have to enter the full path of the command line terminal, before passing commands to it. It would be great the fire up the default if none are present. :)
So that gets complicated. 
* We can't really know if the thing that the user asked us to spawn was a shell or something else, so it doesn't really make sense for us to blindly send the commandline as input to executable spawned by the user's profile and assume that the process on the other end will appropriately treat it as a commandline and execute it. 
* It's also impossible for us to just append the provided commandline to the commandline of the user's profile. What if the user's profile is `wsl ping` (contrived example but gets the point across)? Then, a commandline of `wsl ping yarn some-command` would certainly not work.
* It's also probably impossible for us to know if the commandline they've provided is a real executable or not. If we could, then we could _maybe_ test the commandline, find that it doesn't work, then fall back to `{profile.commandline} {commandline from args}`. I'm sure there's another thread about this somewhere, but IIRC we determined that it's generally impossible to tell if a particular commandline will resolve to a process without just trying to _start the process_. 

There's maybe room for us to add something like `--appendCommandline [arguments...]` that behaves the way you're expecting. For that reason, I'm gonna leave this issue open and tack it onto the commandline args megathread. I hate that name for it, so it'd probably need a proper spec review, but I don't hate the _idea_ of it.


Just an idea:
What if the unified way of starting a specific command line terminal is `wt -p "Windows PowerShell" | cmd | <any-exe-in-PATH> | <path-to-exe>`, if `-p` is not specified wt usese the default profile and all added arugments after that are run in the profile.

Your idea with `--appendCommandline` also sounds okay. Maybe you and your team would have to decide which scenario is more common/sensible to implement.
I'd personally like to see support for adding command line arguments for the profile after launch. This way once a tab is launched we can have it so that it automatically executes a command in that terminal window.
@Ruegen `cmd /k command1 && command`; `powershell -noexit -c command1 && command2`; `bash -c "command1; exec bash -l"`
This seams to work for running command in a new open window.
```
wt ubuntu run "ls / && bash"
```
This will run Ubuntu WSL and then execute `ls /` and then leave bash interactive shell open. Without bash at the end it just exits.
> I've tested it. It works, but every time I have to enter the full path of the command line terminal, before passing commands to it. It would be great the fire up the default if none are present. :)

I believe the reason for this might be that "C:/Program Files/Git/bin/bash.exe" is used, but the user to use it has not been "logged in". What I mean by that is, take this for example. This is my GitBash profile:

```json
      {
        "guid": "{00000000-0000-0000-0000-000000000001}",
        "acrylicOpacity": 0.9,
        "useAcrylic": true,
        "closeOnExit": true,
        "colorScheme": "Solarized Dark",
        "commandline": "\"%PROGRAMFILES%\\Git\\usr\\bin\\bash.exe\" --login -i -l",
        "cursorColor": "#FFFFFF",
        "cursorShape": "bar",
        "fontFace": "Consolas",
        "fontSize": 10,
        "historySize": 9001,
        "icon": "%PROGRAMFILES%\\Git\\mingw64\\share\\git\\git-for-windows.ico",
        "name": "GitBash",
        "padding": "15, 5, 10, 55",
        "snapOnInput": true,
        "startingDirectory": "%USERPROFILE%\\Desktop"
      },
```

Notice how it has `--login -i -l`. Without that, my bash profile is not loaded and as a result I don't have my aliases for example. And perhaps the same reason is causing something as simple as `ls` to fail.

![image](https://user-images.githubusercontent.com/535842/86516407-3c658480-be18-11ea-8ec6-505cf06493b0.png)

Although, probably I'm totally off the ball on this one. 
It would be incredibly useful to open a Windows Terminal (wsl) profile from the commandline and run a command inside afterwards. Something like

`wt -p Debian "cd ~/my-project & npm start"` 

I could create shortcuts to projects and stuff, start environments with a single click. I tried many different combinations of the `wt` command, but nothing worked. Or am I missing something?

Thanks for the help!
Definitely would be helpful to have this. I have several things that need to be started for my local development, so ideally I want to have a one-liner that starts it all in separate tabs/panes.

I know I could theoretically solve this on the OS level with eg. Docker or some process manager, but that's a pretty advanced stuff for a long term Windows user :)
At the moment I'm using the cmd.exe to start a node.js programm. I would also like to see the possibillity to start this with wt instead of the old cmd directly. Currently I have a desktop shortcut to start that.
@DennisBecker Can you not use `wt.exe -- node.exe <whatever the args to node are>`? That should spawn a new WT window, running `node.exe <whatever the args to node are>`
@tihomir-kit I was attempting the **exact** same thing. I'll elaborate on a fix since I landed here searching for a solution, too. Basically the goal was:

- Use Windows Terminal to launch my default profile (e.g. Git's `bash`), which in my case already included the `-l` to login and run my `.bash_profile` with the aliases and stuff. Then...
- Try to execute some arbitrary command now that my aliases and SSH agent are now available, etc.

The **problem** with this was that I was incorrectly _assuming_ that the `commandline` in my profile would be executed (the path to `bash.exe` in your example above). In reality, you actually have to provide that command all over again and include the `-c` parameter to bash so it knows to also run that command. 

The key is just repeating the path to `bash.exe` and then also ensuring you add bash's `-c` parameter. For example:

```bash 
wt -p GitBash -- "%PROGRAMFILES%\Git\usr\bin\bash.exe" -i -l -c "COMMAND"
```

Breaking it down:

1. `wt -p GitBash -- `
    - Launches Windows Terminal with the "GitBash" profile. Everything after this is the command line to execute. Just set all your `wt` flags at/before this point.
2. `"%PROGRAMFILES%\Git\usr\bin\bash.exe" -i -l`
    - Launch `bash`, with an interactive shell (`-i`) and telling it to login (`-l`), which runs your bash profile script, loads aliases, etc. Somewhat redundant since that's already defined in your `commandline` profile setting.
4. `-c "COMMAND"`
    - Now that it's logged in, tells bash to run some arbitrary command within the scope of that shell.


Following what @patricknelson wrote, if you have `C:\Program Files\Git\bin` on your PATH, you can simplify this further to:
`wt bash -i -l -c "COMMAND"`

eg.
`wt bash -i -l -c "yarn start"`

And you don't even need a custom profile.
@patricknelson Thanks a lot, that did the trick!

@akaltar For some reason that doesn't work for me. I checked the `C:\Program Files\Git\bin` and bash indeed is in there. I also tried adding `%PROGRAMFILES%\Git\usr\bin` to my PATH but that didn't help either. 

What I get is: 
```
[process exited with code 1]
```
Hmm, now I ran into a different issue, is there a way to end the double dash? With that, I can run something, but I would like to chain a `; split-pane -V` with another bash command to run after that.
Well this worked finally:

```batch
set bsh="%PROGRAMFILES%\Git\usr\bin\bash.exe"

wt ^
  -p GitBash -- "%bsh%" -i -l -c "alias1"; ^
  split-pane -V -- "%bsh%" -i -l -c "alias2"
```

I did it with a batch script instead of bash because bash complains about insufficient permissions when I try to run WT from it..

The last thing that would be nice to have is to be able to actually stop the command that was running and keep the pane. For example if an `npm run start` was running and you stop it with CTRL+C, the process will exit, but the pane will remain unusable.

You can set `closeOnExit` to `never` to make your panes stay open but remain unusable. :smile:
@tihomir-kit 
You can use this:
```bash -i -l -c "yarn start:local\;bash -i -l";```
Eg just make sure that after whatever command you execute, execute bash again.
This needs a `;` which needs to be escaped with `/` since it's in a bash script in a batch file

Also when adding the git bash path to your PATH, make sure it comes before any other bash executables. I had to move it up in the list quite a bit.
(removed comment, sorry I didn't read the question properly)
I'm gonna make probably a 🌶️ take, but yank "spec needed" off this. A spec seems too heavy. I think we should just do this as 
```
--appendCommandline: If specified, the [command...] parameter 
                     is appended to the profile's current commandline, 
                     rather than replacing it. 
```

so

`wt new-tab -p Ubuntu --appendCommandline ping 8.8.8.8`

would do what you'd expect. We can come up with a short form in the review, if we really need. 

Should be trivial to add to `AppCommandlineArgs::_addNewTerminalArgs` and then evaluate in `AppCommandlineArgs::_getNewTerminalArgs`.


It's up to the user to make sure that their combination of chosen profile & `--appendCommandline` actually makes sense. 

> **Note**
> ## Walkthrough

* [ ] Add flag to `AppCommandlineArgs::_addNewTerminalArgs`
* [ ] Evaluate the full commandline in `AppCommandlineArgs::_getNewTerminalArgs.`
@zadjii-msft I like your suggestion. It perfectly fits with my original intent.
When evaluating the full commandline in AppCommandlineArgs::_getNewTerminalArgs, should we fetch the chosen profile
's commandline and concatonate it with the argument, storing it in the NewTerminalArgs's commandline field? Do we need to first get the global shared settings, then the profile from the NewTerminalArg to get the commandline from the profile or is there a more optimal way to do this? Thanks!
Oh, you bring up a very good point. Nowhere in `AppCommandlineArgs` does it know anything about the `Profile` itself. It's just stashing the commandline it found into the `NewTerminalArgs`, then letting `TerminalSettings::CreateWithNewTerminalArgs` actually reason about how to layer that with the profile. 

Maybe it makes more sense then to add a boolean to NewTerminalArgs, that'll change how `CreateWithNewTerminalArgs` behaves.

In that case:
* add a `Boolean AppendCommandline` to `NewTerminalArgs` in `ActionArgs.idl` (and in `ActionArgs.h`)
* Use `--appendCommandline` to set that boolean
* in `CreateWithNewTerminalArgs`, use that boolean to append the Commandline, instead of replacing it.



Fix patch:
diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp
index e9b6368ff0b..2fa36a9711f 100644
--- a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp
+++ b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp
@@ -568,6 +568,9 @@ void AppCommandlineArgs::_addNewTerminalArgs(AppCommandlineArgs::NewTerminalSubc
     subcommand.colorSchemeOption = subcommand.subcommand->add_option("--colorScheme",
                                                                      _startingColorScheme,
                                                                      RS_A(L"CmdColorSchemeArgDesc"));
+
+    subcommand.appendCommandLineOption = subcommand.subcommand->add_flag("--appendCommandLine", _appendCommandLineOption, RS_A(L"CmdAppendCommandLineDesc"));
+
     // 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",
@@ -654,6 +657,10 @@ NewTerminalArgs AppCommandlineArgs::_getNewTerminalArgs(AppCommandlineArgs::NewT
     {
         args.ColorScheme(winrt::to_hstring(_startingColorScheme));
     }
+    if (*subcommand.appendCommandLineOption)
+    {
+        args.AppendCommandLine(_appendCommandLineOption);
+    }
 
     return args;
 }
@@ -699,6 +706,7 @@ void AppCommandlineArgs::_resetStateToDefault()
     _startingTabColor.clear();
     _commandline.clear();
     _suppressApplicationTitle = false;
+    _appendCommandLineOption = false;
 
     _splitVertical = false;
     _splitHorizontal = false;
diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.h b/src/cascadia/TerminalApp/AppCommandlineArgs.h
index 31eac78168b..292a3500c42 100644
--- a/src/cascadia/TerminalApp/AppCommandlineArgs.h
+++ b/src/cascadia/TerminalApp/AppCommandlineArgs.h
@@ -67,6 +67,7 @@ class TerminalApp::AppCommandlineArgs final
         CLI::Option* tabColorOption;
         CLI::Option* suppressApplicationTitleOption;
         CLI::Option* colorSchemeOption;
+        CLI::Option* appendCommandLineOption;
     };
 
     struct NewPaneSubcommand : public NewTerminalSubcommand
@@ -105,6 +106,7 @@ class TerminalApp::AppCommandlineArgs final
 
     // _commandline will contain the command line with which we'll be spawning a new terminal
     std::vector<std::string> _commandline;
+    bool _appendCommandLineOption{ false };
 
     bool _splitVertical{ false };
     bool _splitHorizontal{ false };
diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw
index 80f0a9df574..6834eff5847 100644
--- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw
@@ -840,4 +840,7 @@
     <value>Run as Administrator</value>
     <comment>This text is displayed on context menu for profile entries in add new tab button.</comment>
   </data>
-</root>
\ No newline at end of file
+  <data name="CmdAppendCommandLineDesc" xml:space="preserve">
+    <value>If set, the command will be appended to the profile's default command instead of replacing it.</value>
+  </data>
+</root>
diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h
index 87e1ac4fcfe..5633c039469 100644
--- a/src/cascadia/TerminalSettingsModel/ActionArgs.h
+++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h
@@ -309,6 +309,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
         ACTION_ARG(Windows::Foundation::IReference<Windows::UI::Color>, TabColor, nullptr);
         ACTION_ARG(Windows::Foundation::IReference<int32_t>, ProfileIndex, nullptr);
         ACTION_ARG(winrt::hstring, Profile, L"");
+        ACTION_ARG(bool, AppendCommandLine, false);
         ACTION_ARG(Windows::Foundation::IReference<bool>, SuppressApplicationTitle, nullptr);
         ACTION_ARG(winrt::hstring, ColorScheme);
         ACTION_ARG(Windows::Foundation::IReference<bool>, Elevate, nullptr);
@@ -320,6 +321,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
         static constexpr std::string_view TabColorKey{ "tabColor" };
         static constexpr std::string_view ProfileIndexKey{ "index" };
         static constexpr std::string_view ProfileKey{ "profile" };
+        static constexpr std::string_view AppendCommandLineKey{ "appendCommandLine" };
         static constexpr std::string_view SuppressApplicationTitleKey{ "suppressApplicationTitle" };
         static constexpr std::string_view ColorSchemeKey{ "colorScheme" };
         static constexpr std::string_view ElevateKey{ "elevate" };
@@ -340,6 +342,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
                        otherAsUs->_TabColor == _TabColor &&
                        otherAsUs->_ProfileIndex == _ProfileIndex &&
                        otherAsUs->_Profile == _Profile &&
+                       otherAsUs->_AppendCommandLine == _AppendCommandLine &&
                        otherAsUs->_SuppressApplicationTitle == _SuppressApplicationTitle &&
                        otherAsUs->_ColorScheme == _ColorScheme &&
                        otherAsUs->_Elevate == _Elevate &&
diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl
index 936165b8b28..011ad7beac5 100644
--- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl
+++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl
@@ -129,6 +129,7 @@ namespace Microsoft.Terminal.Settings.Model
         String TabTitle;
         Windows.Foundation.IReference<Windows.UI.Color> TabColor;
         String Profile; // Either a GUID or a profile's name if the GUID isn't a match
+        Boolean AppendCommandLine;
 
         // We use IReference<> to treat some args as nullable where null means
         // "use the inherited value". See ProfileIndex,
diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp
index aaa151d9cf2..c594d42fb3b 100644
--- a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp
+++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp
@@ -129,7 +129,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
             // Override commandline, starting directory if they exist in newTerminalArgs
             if (!newTerminalArgs.Commandline().empty())
             {
-                defaultSettings.Commandline(newTerminalArgs.Commandline());
+                if (!newTerminalArgs.AppendCommandLine())
+                {
+                    defaultSettings.Commandline(newTerminalArgs.Commandline());
+                }
+                else
+                {
+                    defaultSettings.Commandline(defaultSettings.Commandline() + L" " + newTerminalArgs.Commandline());
+                }
             }
             if (!newTerminalArgs.StartingDirectory().empty())
             {


Test patch: