Search code examples
c++windowspowershelluser-interfaceconsole

How to prevent Powershell from overwriting the output of a c++ program that uses AttachConsole() after it has finished


I am trying to write a c++ program that can be launched by double clicking the executable from e.g. explorer, or alternatively can be called from a commandline such as powershell or cmd.

I want the application to not open a console when called from a gui environment (clicking in explorer, a desktop shortcut, etc). I do want it to output to a console when called from a commandline to provide useful information to more technically inclined people.I'm pretty new to GUI stuff with c++ so please be gentle.

Details:

  • c++ 23
  • Using cmake and mingw to build,
  • Using Clion to code.
  • Using Windows 11.

I have looked for similar questions online but I have yet to find them.

I have a working example that achieves this, It behaves as expected in cmd, but when running it in Powershell, Powershell will overwrite the output after the program has finished, as if the program never printed lines to the console. (see pictures for examples). The funny thing is that Powershell does work as expected when I reach the bottom of the terminal. Due to the nature of this problem I will include pictures as I do not see a good alternative to show my issue. This code does not launch a GUI, but my final code does and exhibits the same problem.

Unrelated: it appears stdin is still going to powershell while attached.

Cmake config

cmake_minimum_required(VERSION 3.27)
project(consoletest)

set(CMAKE_CXX_STANDARD 23)

add_executable(consoletest main.cpp)
target_link_options(consoletest PRIVATE -mwindows)

Code

#include <iostream>
#define _WIN32_WINNT 0x0501
#include <windows.h>

int main() {
    // Attach output to console if called from console.
    AttachConsole(-1);
    freopen("CONIN$", "r", stdin);
    freopen("CONOUT$", "w", stdout);
    freopen("CONOUT$", "w", stderr);

    std::cout << "\nHello, World!" << std::endl;
    std::cout << "Hello, World!" << std::endl;
    std::cout << "Hello, World!" << std::endl;
    std::cout << "Hello, World!" << std::endl;
    std::cout << "Hello, World!" << std::endl;
    std::cout << "Hello, World!" << std::endl;
    std::cout << "Hello, World!" << std::endl;

    // Send enter key hack for console mode to return to parent console
    INPUT ip;
    ip.type = INPUT_KEYBOARD;
    ip.ki.time = 0;
    ip.ki.dwFlags = KEYEVENTF_UNICODE;
    ip.ki.wScan = VK_RETURN;
    ip.ki.wVk = 0;
    ip.ki.dwExtraInfo = 0;
    SendInput(1, &ip, sizeof(INPUT));

    return 0;
}

Images:

Powershell when there is space left below

Powershell with no space left

CMD works as expected

Edit: The solution from @mklement0 will do for my purposes. I do still wish there is a way to have the program do this automatically without needing additional effort when calling the program. When using the methods from @mklement0, you no longer need the "Enter hack" or the \n in the first cout.


Solution

  • Unlike cmd.exe, PowerShell by default always[1] launches Windows GUI-subsystem applications asynchronously, which results in the symptoms you saw.

    To ensure synchronous execution of an application that explicitly attaches to the caller's console and writes to it, pipe to Write-Output:

    .\consoletest.exe | Write-Output
    

    This approach, as well as all subsequent ones, obviate the need for your attempt to programmatically send a Enter keystroke, because the synchronous execution ensures that the PowerShell doesn't return to and prints its prompt string until after execution has ended.

    If only ever display output is needed, you may also use Write-Host, or using Start-Process:

    # Display output only; output cannot be captured.
    Start-Process -NoNewWindow -Wait .\consoletest.exe
    

    It is the involvement of a pipeline by itself that makes the call synchronous, so you may use this technique for any GUI application, even those that do not attach to the console, in case you can pipe to Out-Null (though piping to Write-Host or Write-Output would work too); e.g.

    # Synchronous invocation of a GUI application that does *not* 
    # attach to and write to the console.
    Notepad | Out-Null
    

    Alternatively, if you (also) want to capture your application's output:

    To capture while also printing to the console, use Tee-Object; adding redirection 2>&1 allows you to capture stderr output too:

    # After this, $output contains contains the collected output.
    .\consoletest.exe 2>&1 | Tee-Object -Variable output
    

    To capture only (without console output):

    $output = .\consoletestexe 2>&1 | Write-Output
    

    In either case, if you later want to separate the stdout from the stderr output lines:

    $stdoutLines, $stderrLines = $output.Where({ $_ -is [string] }, 'Split'})
    

    If you want to write a wrapper command (function) that invokes your application synchronously without extra effort:

    function consoletest {
      # Use the executable's full path:
      # You can determine it programmatically with:
      #     Convert-Path .\consoletest.exe
      # If you want the function to look in the *current* dir. for
      # the executable, use .\consoletest.exe
      # $args passes any arguments through.
      & 'C:\path\to\consoletest.exe' $args 2>&1 | Write-Output
    }
    

    If you place this function in your $PROFILE file, it'll be available in all future sessions (except those started with -NoProfile).


    [1] Interactively, cmd.exe executes GUI-subsystem applications asynchronously, whereas it does so synchronously from batch files and when passing commands to cmd.exe /c.
    As you note, to also make interactive invocation synchronous, you can use start /wait consoletest.exe - unlike PowerShell's Start-Process -Wait analog, this does not preclude output capturing in cmd.exe, via for /f