File: commandlineutils.cpp

package info (click to toggle)
martchus-cpp-utilities 5.28.0-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 1,352 kB
  • sloc: cpp: 12,471; awk: 18; ansic: 12; makefile: 10
file content (318 lines) | stat: -rw-r--r-- 11,067 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
#include "./commandlineutils.h"
#include "./argumentparserprivate.h"

#include "../io/ansiescapecodes.h"

#include <iostream>
#include <string>

#include <fcntl.h>
#ifdef PLATFORM_WINDOWS
#include <cstring>
#include <io.h>
#include <tchar.h>
#include <windows.h>
#else
#include <sys/ioctl.h>
#include <unistd.h>
#endif

using namespace std;

namespace CppUtilities {

/*!
 * \brief Prompts for confirmation displaying the specified \a message.
 */
bool confirmPrompt(const char *message, Response defaultResponse)
{
    cout << message;
    cout << ' ' << '[';
    cout << (defaultResponse == Response::Yes ? 'Y' : 'y');
    cout << '/' << (defaultResponse == Response::No ? 'N' : 'n');
    cout << ']' << ' ';
    cout.flush();
    for (string line;;) {
        getline(cin, line);
        if (line == "y" || line == "Y" || (defaultResponse == Response::Yes && line.empty())) {
            return true;
        } else if (line == "n" || line == "N" || (defaultResponse == Response::No && line.empty())) {
            return false;
        } else {
            cout << "Please enter [y] or [n]: ";
            cout.flush();
        }
    }
}

/*!
 * \brief Returns whether the specified env variable is set to a non-zero and non-white-space-only value.
 */
std::optional<bool> isEnvVariableSet(const char *variableName)
{
    const char *envValue = std::getenv(variableName);
    if (!envValue) {
        return std::nullopt;
    }
    for (; *envValue; ++envValue) {
        switch (*envValue) {
        case '0':
        case ' ':
            break;
        default:
            return true;
        }
    }
    return false;
}

/*!
 * \brief Returns the current size of the terminal.
 * \remarks Unknown members of the returned TerminalSize are set to zero.
 */
TerminalSize determineTerminalSize()
{
    TerminalSize size;
#ifndef PLATFORM_WINDOWS
    ioctl(STDOUT_FILENO, TIOCGWINSZ, reinterpret_cast<winsize *>(&size));
#else
    CONSOLE_SCREEN_BUFFER_INFO consoleBufferInfo;
    if (const HANDLE stdHandle = GetStdHandle(STD_OUTPUT_HANDLE)) {
        GetConsoleScreenBufferInfo(stdHandle, &consoleBufferInfo);
        if (consoleBufferInfo.dwSize.X > 0) {
            size.columns = static_cast<unsigned short>(consoleBufferInfo.dwSize.X);
        }
        if (consoleBufferInfo.dwSize.Y > 0) {
            size.rows = static_cast<unsigned short>(consoleBufferInfo.dwSize.Y);
        }
    }
#endif
    return size;
}

#ifdef PLATFORM_WINDOWS
/*!
 * \brief Returns whether Mintty is used.
 */
static bool isMintty()
{
    static const auto mintty = [] {
        const char *const msyscon = std::getenv("MSYSCON");
        const char *const termprog = std::getenv("TERM_PROGRAM");
        return (msyscon && std::strstr(msyscon, "mintty")) || (termprog && std::strstr(termprog, "mintty"));
    }();
    return mintty;
}

/*!
 * \brief Enables virtual terminal processing (and thus processing of ANSI escape codes) of the console
 *        determined by the specified \a nStdHandle.
 * \sa https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
 */
static bool enableVirtualTerminalProcessing(DWORD nStdHandle)
{
    auto stdHandle = GetStdHandle(nStdHandle);
    if (stdHandle == INVALID_HANDLE_VALUE) {
        return false;
    }
    auto dwMode = DWORD();
    if (!GetConsoleMode(stdHandle, &dwMode)) {
        return false;
    }
    dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
    return SetConsoleMode(stdHandle, dwMode);
}

/*!
 * \brief Enables virtual terminal processing (and thus processing of ANSI escape codes) of the console
 *        or disables use of ANSI escape codes if that's not possible.
 */
bool handleVirtualTerminalProcessing()
{
    // try to enable virtual terminal processing
    if (enableVirtualTerminalProcessing(STD_OUTPUT_HANDLE) && enableVirtualTerminalProcessing(STD_ERROR_HANDLE)) {
        return true;
    }
    // disable use of ANSI escape codes otherwise if it makes sense
    if (isMintty()) {
        return false; // no need to disable escape codes if it is just mintty
    }
    if (const char *const term = std::getenv("TERM"); term && std::strstr(term, "xterm")) {
        return false; // no need to disable escape codes if it is some xterm-like terminal
    }
    return EscapeCodes::enabled = false;
}

/*!
 * \brief Closes stdout, stdin and stderr and stops the console.
 * \remarks Internally used by startConsole() to close the console when the application exits.
 */
void stopConsole()
{
    fclose(stdout);
    fclose(stdin);
    fclose(stderr);
    if (GetConsoleWindow()) {
        FreeConsole();
    }
}

/*!
 * \brief Ensure the process has a console attached and properly setup.
 * \remarks
 * - Only available (and required) under Windows where otherwise standard I/O is not possible via the console (unless
 *   when using Mintty).
 * - Attaching a console breaks redirections/pipes so this needs to be opted-in by setting the environment variable
 *   `ENABLE_CONSOLE=1`.
 * - Note that this is only useful to start a console from a GUI application. It is not necassary to call this function
 *   from a console application.
 * - The console is automatically closed when the application exits.
 * - This function alone does not provide good results. It still breaks redirections in PowerShell and other shells and
 *   after the application exists the command prompt is not displayed. A CLI-wrapper is required for proper behavior. The
 *   build system automatically generates one when the CMake variable BUILD_CLI_WRAPPER is set. Note that this CLI-wrapper
 *   still relies on this function (and thus sets `ENABLE_CONSOLE=1`). Without this standard I/O would still not be
 *   possible via the console. The part for skipping in case there's a redirection is still required. Otherwise
 *   redirections/pipes are broken when using the CLI-wrapper as well.
 * \sa
 * - https://docs.microsoft.com/en-us/windows/console/AttachConsole
 * - https://docs.microsoft.com/en-us/windows/console/AllocConsole
 * - https://docs.microsoft.com/en-us/windows/console/SetConsoleCP
 * - https://docs.microsoft.com/en-us/windows/console/SetConsoleOutputCP
 */
void startConsole()
{
    // skip if ENABLE_CONSOLE is set to 0 or not set at all
    if (const auto e = isEnvVariableSet("ENABLE_CONSOLE"); !e.has_value() || !e.value()) {
        return;
    }

    // check whether there's a redirection; skip messing with any streams then to not break redirections/pipes
    auto pos = std::fpos_t();
    std::fgetpos(stdout, &pos);
    const auto skipstdout = pos >= 0;
    std::fgetpos(stderr, &pos);
    const auto skipstderr = pos >= 0;
    std::fgetpos(stdin, &pos);
    const auto skipstdin = pos >= 0;
    const auto skip = skipstdout || skipstderr || skipstdin;

    // attach to the parent process' console or allocate a new console if that's not possible
    if (!skip && (AttachConsole(ATTACH_PARENT_PROCESS) || AllocConsole())) {
        FILE *fp;
#ifdef _MSC_VER
        // take care of normal streams
        if (!skipstdout) {
            freopen_s(&fp, "CONOUT$", "w", stdout);
            std::cout.clear();
            std::clog.clear();
        }
        if (!skipstderr) {
            freopen_s(&fp, "CONOUT$", "w", stderr);
            std::cerr.clear();
        }
        if (!skipstdin) {
            freopen_s(&fp, "CONIN$", "r", stdin);
            std::cin.clear();
        }
        // take care of wide streams
        auto hConOut = CreateFile(
            _T("CONOUT$"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
        auto hConIn = CreateFile(
            _T("CONIN$"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
        if (!skipstdout) {
            SetStdHandle(STD_OUTPUT_HANDLE, hConOut);
            std::wcout.clear();
            std::wclog.clear();
        }
        if (!skipstderr) {
            SetStdHandle(STD_ERROR_HANDLE, hConOut);
            std::wcerr.clear();
        }
        if (!skipstdin) {
            SetStdHandle(STD_INPUT_HANDLE, hConIn);
            std::wcin.clear();
        }
#else
        // redirect stdout
        auto stdHandle = std::intptr_t();
        auto conHandle = int();
        if (!skipstdout) {
            stdHandle = reinterpret_cast<intptr_t>(GetStdHandle(STD_OUTPUT_HANDLE));
            conHandle = _open_osfhandle(stdHandle, _O_TEXT);
            fp = _fdopen(conHandle, "w");
            *stdout = *fp;
            setvbuf(stdout, nullptr, _IONBF, 0);
        }
        // redirect stdin
        if (!skipstdin) {
            stdHandle = reinterpret_cast<intptr_t>(GetStdHandle(STD_INPUT_HANDLE));
            conHandle = _open_osfhandle(stdHandle, _O_TEXT);
            fp = _fdopen(conHandle, "r");
            *stdin = *fp;
            setvbuf(stdin, nullptr, _IONBF, 0);
        }
        // redirect stderr
        if (!skipstderr) {
            stdHandle = reinterpret_cast<intptr_t>(GetStdHandle(STD_ERROR_HANDLE));
            conHandle = _open_osfhandle(stdHandle, _O_TEXT);
            fp = _fdopen(conHandle, "w");
            *stderr = *fp;
            setvbuf(stderr, nullptr, _IONBF, 0);
        }
        // sync
        ios::sync_with_stdio(true);
#endif
        // ensure the console prompt is shown again when app terminates
        std::atexit(stopConsole);
    }

    // set console character set to UTF-8
    if (const auto e = isEnvVariableSet("ENABLE_CP_UTF8"); !e.has_value() || e.value()) {
        SetConsoleCP(CP_UTF8);
        SetConsoleOutputCP(CP_UTF8);
    }

    // enable virtual terminal processing or disable ANSI-escape if that's not possible
    if (const auto e = isEnvVariableSet("ENABLE_HANDLING_VIRTUAL_TERMINAL_PROCESSING"); !e.has_value() || e.value()) {
        handleVirtualTerminalProcessing();
    }
}

/*!
 * \brief Convert command line arguments to UTF-8.
 * \remarks Only available on Windows (on other platforms we can assume passed arguments are already UTF-8 encoded).
 */
pair<vector<unique_ptr<char[]>>, vector<char *>> convertArgsToUtf8()
{
    pair<vector<unique_ptr<char[]>>, vector<char *>> res;
    int argc;

    LPWSTR *argv_w = CommandLineToArgvW(GetCommandLineW(), &argc);
    if (!argv_w || argc <= 0) {
        return res;
    }

    res.first.reserve(static_cast<size_t>(argc));
    res.second.reserve(static_cast<size_t>(argc));
    for (LPWSTR *i = argv_w, *end = argv_w + argc; i != end; ++i) {
        int requiredSize = WideCharToMultiByte(CP_UTF8, 0, *i, -1, nullptr, 0, 0, 0);
        if (requiredSize <= 0) {
            break; // just stop on error
        }

        auto argv = make_unique<char[]>(static_cast<size_t>(requiredSize));
        requiredSize = WideCharToMultiByte(CP_UTF8, 0, *i, -1, argv.get(), requiredSize, 0, 0);
        if (requiredSize <= 0) {
            break;
        }

        res.second.emplace_back(argv.get());
        res.first.emplace_back(std::move(argv));
    }

    LocalFree(argv_w);
    return res;
}
#endif

} // namespace CppUtilities