Repository: supercollider/supercollider
Pull request #6640: Use boost program options for sclang CLI options
Author: login: capital-G
Labels: name: comp: sclang
description: sclang C++ implementation (primitives, etc.). for changes to class lib use "comp: class library"
name: has merge note
description: The Conversation has a note concerning how/when this PR is merged
Created: 2025-02-24T16:50:44Z | Merged: 2025-04-22T18:45:13Z

<!-- For information about contributing see: https://github.com/supercollider/supercollider/wiki/Contributing-directory -->

## Purpose and Motivation

This replaces the current CLI options parsing in sclang with boost program options.

This allows to
* use multi-character options (e.g. `--standalone` is now as valid as `-a` but is probably easier to remember?)
* automatic generation of help command
* overwrite language configuration settings configured via YAML. Allows to set e.g. included directories or else w/o the need to create a YAML file to set such parameters. This probably makes it easier to provide a "run sclang testsuite" and "build docs" command within cmake or via a shell script.
* replaces custom written code with boost library code which hopefully reduces maintenance

While at it, I also transformed some `char*` to `std::string` and `char**` to `std::vector<std::string>>` where they are not within the realm of the sclang managed memory.

`--help` now returns this

```shell
❯ ./sclang --help
Start a REPL interpreter:           sclang [ -options ]
Execute a .scd file:                sclang [ -options ] path_to_sc_file [ args passed to sclang... ]

Generic options:
  -h [ --help ]                     Print this message and exit
  -v [ --version ]                  Print sclang version and exit

sclang options:
  -d [ --runtime-directory ] arg    Set runtime directory
  -D [ --daemon ]                   Enter daemon mode
  -g [ --heap-growth ] arg (=256k)  Set heap growth size (allows k, m and g as 
                                    suffix multipliers)
  -l [ --yaml-config ] arg          Set yaml config
  -m [ --heap-size ] arg (=2m)      Set initial heap size (allows k, m and g as
                                    suffix multipliers)
  -r [ --call-run ]                 Call Main.run on startup
  -s [ --call-stop ]                Call Main.stop on shutdown
  -u [ --port ] arg (=57120)        Set UDP listening port
  -i [ --ide ] arg                  Set IDE name
  -a [ --standalone ]               Standalone mode (exclude SCClassLibrary and
                                    user and system Extensions folders from 
                                    search path)

Language config options (overwrites provided yaml entries):
  --include-path arg                class library path to be included for 
                                    searching (allows multiple mentions)
  --exclude-path arg                class library path to be to excluded from 
                                    searching (overrides includePaths, allows 
                                    multiple mentions)
  --post-inline-warnings            Boolean flag to turn on warnings about 
                                    missing inline opportunities.
  
```

This is more an exercise to get familiar with the sclang code than a necessary feature, yet I think it is still an improvement over the current code basis.
Maybe this could be a start to transform some "older" sclang parts into more modern C++ where there is no need for manual memory management so they become more robust and easier to maintain?

I think the `notifier` syntax of program options allows for really compact code.

<!-- Please provide a description, even if you are linking to a related Issue or PR. -->
<!-- If this fixes an open issue, link to it by writing "Fixes #555." -->

## Types of changes

<!-- Delete lines that don't apply -->

- Documentation
- New feature

## To-do list

<!-- Complete an item by checking it: [x]. Add new entries to track your progress -->

- [x] Code is tested
- [x] All tests are passing
- [x] Updated documentation
- [x] This PR is ready for review <!-- If not ready for review, consider making a Draft PR instead, or describe what kind of specific feedback you want. -->

Although I tested it on my machine, it wouldn't hurt if someone else give this some testing.


Linked issue #555: find next / previous key binding
It would be very good to have a key binding for find next and find previous. On OS X the standard ones are cmd-g [ctrl-g] and cmd-shift-g [ctrl-shift-g], used throughout the system. I'd suggest to replace cmd-g [ctrl-g] which currently means "go to line number X" by cmd-# [ctrl-#].

Linked issue comments: user: login: timblechmann
created_at: 2012-10-01T12:55:04Z
body: `ctrl+g` is a standard for 'goto line' on other editors. `ctrl+#` does not sound like a nice shortcut to me. we will have to find a compromise between platform-specific conventions and a cross-platform user experience

author_association: CONTRIBUTOR
user: login: danstowell
created_at: 2012-10-01T13:00:40Z
body: OK, but ctrl-g is standard for "find next" on a lot of things, not just on
mac. Here on my ubuntu machine it's used for that purpose in firefox,
gedit, evince, thunderbird. (but not in libreoffice)

2012/10/1 Tim Blechmann notifications@github.com

> ctrl+g is a standard for 'goto line' on other editors. ctrl+# does not
> sound like a nice shortcut to me. we will have to find a compromise between
> platform-specific conventions and a cross-platform user experience
> 
> —
> Reply to this email directly or view it on GitHubhttps://github.com/supercollider/supercollider/issues/555#issuecomment-9030604.

## 

http://www.mcld.co.uk

author_association: MEMBER
user: login: jleben
created_at: 2012-11-15T10:34:15Z
body: Done via bbb8da32fabb8719d8847448fba7767385d06af7.

author_association: MEMBER

Diff:
diff --git a/QtCollider/LanguageClient.cpp b/QtCollider/LanguageClient.cpp
index 83c73a5bf83..a243a812bca 100644
--- a/QtCollider/LanguageClient.cpp
+++ b/QtCollider/LanguageClient.cpp
@@ -34,7 +34,7 @@ extern double elapsedTime();
 
 using namespace QtCollider;
 
-LangClient::LangClient(const char* name): SC_TerminalClient(name) {}
+LangClient::LangClient(const std::string& name): SC_TerminalClient(name) {}
 
 void LangClient::commandLoop() {
     int exit_code = QcApplication::instance()->exec();
diff --git a/QtCollider/LanguageClient.h b/QtCollider/LanguageClient.h
index 23143f4df18..98707532925 100644
--- a/QtCollider/LanguageClient.h
+++ b/QtCollider/LanguageClient.h
@@ -39,7 +39,7 @@ struct SCRequestEvent : public QEvent {
 class LangClient : public QObject, public SC_TerminalClient {
     Q_OBJECT
 public:
-    LangClient(const char* name);
+    LangClient(const std::string& name);
     virtual ~LangClient() {}
     virtual void sendSignal(Signal);
 
diff --git a/include/lang/SC_LanguageClient.h b/include/lang/SC_LanguageClient.h
index 0b441551622..6fe785312b8 100644
--- a/include/lang/SC_LanguageClient.h
+++ b/include/lang/SC_LanguageClient.h
@@ -29,6 +29,7 @@
 #include "SC_Export.h"
 #include <cstdio>
 #include <cstdarg>
+#include <string>
 
 // =====================================================================
 // SC_LanguageClient - abstract sclang client.
@@ -40,17 +41,19 @@ SCLANG_DLLEXPORT void destroyLanguageClient(class SC_LanguageClient*);
 class SCLANG_DLLEXPORT SC_LanguageClient {
 public:
     struct Options {
-        Options(): mMemSpace(2 * 1024 * 1024), mMemGrow(256 * 1024), mPort(57120), mRuntimeDir(0) {}
-
-        int mMemSpace; // memory space in bytes
-        int mMemGrow; // memory growth in bytes
-        int mPort; // network port number
-        char* mRuntimeDir; // runtime directory
+        static constexpr int defaultMemSpace = 2 * 1024 * 1024;
+        static constexpr int defaultMemGrow = 256 * 1024;
+        static constexpr int defaultPort = 57120;
+
+        int mMemSpace = defaultMemSpace; // memory space in bytes
+        int mMemGrow = defaultMemGrow; // memory growth in bytes
+        int mPort = defaultPort; // network port number
+        std::string mRuntimeDir; // runtime directory
     };
 
 protected:
     // create singleton instance
-    SC_LanguageClient(const char* name);
+    SC_LanguageClient(const std::string& name);
     virtual ~SC_LanguageClient();
     friend void destroyLanguageClient(class SC_LanguageClient*);
 
@@ -67,7 +70,7 @@ class SCLANG_DLLEXPORT SC_LanguageClient {
     }
 
     // initialize language runtime
-    void initRuntime(const Options& opt = Options());
+    void initRuntime(const Options& opt);
     void shutdownRuntime();
 
     // return application name
@@ -92,7 +95,7 @@ class SCLANG_DLLEXPORT SC_LanguageClient {
     void runLibrary(const char* methodName);
     void interpretCmdLine();
     void interpretPrintCmdLine();
-    void executeFile(const char* fileName);
+    void executeFile(const std::string& fileName);
     void runMain();
     void stopMain();
 
@@ -112,11 +115,6 @@ class SCLANG_DLLEXPORT SC_LanguageClient {
     //     only called from the main language thread.
     virtual void flush() = 0;
 
-    // command line argument handling utilities
-    static void snprintMemArg(char* dst, size_t size, int arg);
-    static bool parseMemArg(const char* arg, int* res);
-    static bool parsePortArg(const char* arg, int* res);
-
     // AppClock driver
     //    to be called from client mainloop.
     void tick();
diff --git a/lang/CMakeLists.txt b/lang/CMakeLists.txt
index 6cd06359e6c..561023da45f 100644
--- a/lang/CMakeLists.txt
+++ b/lang/CMakeLists.txt
@@ -65,6 +65,7 @@ set(sclang_sources
 	LangSource/PyrParseNode.cpp
 	LangSource/PyrSignal.cpp
 	LangSource/PyrSymbolTable.cpp
+    LangSource/SC_CLIOptions.cpp
 	LangSource/SC_LanguageClient.cpp
 	LangSource/SC_LanguageConfig.cpp
 	LangSource/SC_TerminalClient.cpp
@@ -243,7 +244,7 @@ if(SC_HIDAPI)
   endif()
 endif()
 
-target_link_libraries(libsclang boost_thread_lib boost_system_lib boost_regex_lib)
+target_link_libraries(libsclang boost_thread_lib boost_system_lib boost_regex_lib boost_program_options_lib)
 target_include_directories(libsclang PUBLIC ${boost_include_dirs})
 
 if (SCLANG_SERVER)
diff --git a/lang/LangSource/SC_CLIOptions.cpp b/lang/LangSource/SC_CLIOptions.cpp
new file mode 100644
index 00000000000..72f67316e67
--- /dev/null
+++ b/lang/LangSource/SC_CLIOptions.cpp
@@ -0,0 +1,285 @@
+#include "boost/program_options.hpp"
+#include <iostream>
+#include <string>
+#include "SC_TerminalClient.h"
+#include "SC_CLIOptions.hpp"
+
+#include <SC_Filesystem.hpp>
+#include <SC_LanguageConfig.hpp>
+#include <SC_Version.hpp>
+
+namespace po = boost::program_options;
+
+namespace SC_CLI {
+
+/// parses the CLI - if the program should terminate after parsing it will return an exit code.
+/// If parsing was successful and the program should continue it will return a std::nullopt .
+std::optional<int> CLIOptions::parse(int argc, char* argv[], SC_TerminalClient::Options& terminalOptions) {
+    po::options_description description("Interpreter for sclang (SuperCollider language)");
+
+    auto genericDescription = buildGenericDescription();
+    auto terminalDescription = buildTerminalDescription(terminalOptions);
+    auto languageConfigDescription = buildLanguageConfigDescription();
+
+    // Hidden options, will be allowed both on command line and
+    // in config file, but will not be shown to the user.
+    po::options_description hiddenDescription("Hidden options");
+    // clang-format off
+    hiddenDescription.add_options()
+        ("input-file", po::value<std::string>(&mInputFile), "input file")
+        ("sc-args", po::value<std::vector<std::string>>(&mScArgs), "sc args")
+    ;
+    // clang-format on
+
+    // positional options are declared here but captured via hidden description
+    po::positional_options_description positionalOptions;
+    positionalOptions.add("input-file", 1);
+    positionalOptions.add("sc-args", -1);
+
+    po::options_description cmdLineOptions;
+    cmdLineOptions.add(genericDescription)
+        .add(terminalDescription)
+        .add(hiddenDescription)
+        .add(languageConfigDescription);
+
+    po::variables_map vm;
+    try {
+        store(po::command_line_parser(argc, argv).options(cmdLineOptions).positional(positionalOptions).run(), vm);
+        notify(vm);
+    } catch (po::unknown_option& e) {
+        std::cerr << e.what() << std::endl;
+        return 1;
+    } catch (po::invalid_option_value& e) {
+        std::cerr << e.what() << std::endl;
+        return 1;
+    }
+    if (mParsingFailed) {
+        return 1;
+    }
+
+    if (vm.count("sc-args")) {
+        terminalOptions.mArgs = vm["sc-args"].as<std::vector<std::string>>();
+    }
+
+    if (mPrintVersion) {
+        std::cout << "sclang " << SC_VersionString() << " (" << SC_BuildString() << ")" << std::endl;
+        return 0;
+    }
+
+    // this is handled here and not in the description declaration b/c we need access to the declarations
+    if (mPrintHelp) {
+        // construct a options_description w/o hidden positional options
+        po::options_description visibleDescription;
+        visibleDescription.add(genericDescription).add(terminalDescription).add(languageConfigDescription);
+
+        std::cout
+            << "Start a REPL interpreter:           sclang [ -options ]\n"
+               "Execute a .scd file:                sclang [ -options ] path_to_sc_file [ args passed to sclang... ]\n"
+            << visibleDescription << std::endl;
+        return 0;
+    }
+
+    if (!mInputFile.empty()) {
+        terminalOptions.mDaemon = true;
+    }
+
+    SC_LanguageConfig::readLibraryConfig(terminalOptions.mStandalone);
+
+    // for the language config we don't use the notifier of program options because
+    // we want to fill the config with a provided yaml file or their default values.
+    // as po does not allow to specify the "notify order", we parse the config manually
+    parseLanguageConfig(vm);
+
+    return std::nullopt;
+}
+
+po::options_description CLIOptions::buildGenericDescription() {
+    po::options_description options("Generic options");
+    // clang-format off
+    options.add_options()
+        ("help,h", po::bool_switch(&mPrintHelp), "Print this message and exit")
+        ("version,v", po::bool_switch(&mPrintVersion), "Print sclang version and exit")
+    ;
+    // clang-format on
+    return options;
+}
+
+po::options_description CLIOptions::buildTerminalDescription(SC_TerminalClient::Options& terminalOptions) {
+    po::options_description description("sclang options");
+
+    // clang-format off
+    description.add_options()
+        (
+            "runtime-directory,d",
+            po::value<std::string>(&terminalOptions.mRuntimeDir),
+            "Set runtime directory"
+        )
+        (
+            "daemon,D",
+            po::bool_switch(&terminalOptions.mDaemon)->default_value(terminalOptions.mDaemon),
+            "Enter daemon mode"
+        )
+        (
+            "heap-growth,g",
+            po::value<std::string>()->default_value(convertMemoryInteger(terminalOptions.mMemGrow))->notifier([&](const std::string &heapGrowth) {
+                terminalOptions.mMemGrow = parseMemoryString(heapGrowth);
+            }),
+            "Set heap growth size (allows k, m and g as suffix multipliers)"
+        )
+        (
+            "yaml-config,l",
+            po::value<std::string>(&terminalOptions.mLibraryConfigFile)->notifier([&](const std::string &yaml_config) {
+                terminalOptions.mLibraryConfigFile = yaml_config;
+                SC_LanguageConfig::setConfigPath(yaml_config);
+            }),
+            "Set yaml config"
+        )
+        (
+            "heap-size,m",
+            po::value<std::string>()->default_value(convertMemoryInteger(terminalOptions.mMemSpace))->notifier([&](const std::string &heapSize) {
+                terminalOptions.mMemSpace = parseMemoryString(heapSize);
+            }),
+            "Set initial heap size (allows k, m and g as suffix multipliers)"
+        )
+        (
+            "call-run,r",
+            po::bool_switch(&terminalOptions.mCallRun)->default_value(terminalOptions.mCallRun),
+            "Call Main.run on startup"
+        )
+        (
+            "call-stop,s",
+            po::bool_switch(&terminalOptions.mCallStop)->default_value(terminalOptions.mCallStop),
+            "Call Main.stop on shutdown"
+        )
+        (
+            "port,u",
+            po::value<int>(&terminalOptions.mPort)->default_value(terminalOptions.mPort)->notifier([&](int port) {
+                if (port < 0 || port > 65535) {
+                    std::cerr << "UDP port must be in range between 0-65535, but was " << port << std::endl;
+                    mParsingFailed = true;
+                }
+            }),
+            "Set UDP listening port"
+        )
+        (
+            "ide,i",
+            po::value<std::string>()->notifier([&](std::string ide_name) {
+                SC_Filesystem::instance().setIdeName(ide_name);
+            }),
+            "Set IDE name"
+        )
+        (
+            "exclude-default-paths,a",
+            po::bool_switch(&terminalOptions.mStandalone)->default_value(terminalOptions.mStandalone),
+            "Exclude searching in default SCClassLibrary and user and system extensions paths (aka 'standalone' mode)."
+        )
+    ;
+    // clang-format on
+
+    return description;
+}
+
+po::options_description CLIOptions::buildLanguageConfigDescription() {
+    po::options_description description("Language config options (overwrites provided yaml entries)");
+
+    // clang-format off
+    description.add_options()
+        (
+            "include-path",
+            po::value<std::vector<std::string>>(),
+            "Class library path to be included for searching (allows multiple mentions)"
+        )
+        (
+            "exclude-path",
+            po::value<std::vector<std::string>>(),
+            "Class library path to be excluded from searching (overrides includePaths, allows multiple mentions)"
+        )
+        (
+            "post-inline-warnings",
+            po::bool_switch()->default_value(false),
+            "Boolean flag to turn on warnings about missing inline opportunities."
+        )
+        // exclude-default-paths is not included as this behavior is set via the `--standalone` flag
+    ;
+    // clang-format on
+    return description;
+}
+
+void CLIOptions::parseLanguageConfig(po::variables_map& vm) {
+    // see HelpSource/Classes/LanguageConfig.schelp
+    if (vm.at("post-inline-warnings").as<bool>()) {
+        gLanguageConfig->setPostInlineWarnings(true);
+    };
+
+    if (vm.count("include-path")) {
+        for (auto includePath : vm["include-path"].as<std::vector<std::string>>()) {
+            gLanguageConfig->addIncludedDirectory(SC_Codecvt::utf8_str_to_path(includePath));
+        }
+    }
+    if (vm.count("exclude-path")) {
+        for (auto excludePath : vm["exclude-path"].as<std::vector<std::string>>()) {
+            gLanguageConfig->addExcludedDirectory(SC_Codecvt::utf8_str_to_path(excludePath));
+        }
+    }
+}
+
+// converts e.g. 1k to 1024
+int CLIOptions::parseMemoryString(const std::string& input) {
+    if (input.empty()) {
+        return 0;
+    }
+
+    char suffix = input.back();
+    int multiplier = 1;
+
+    if (suffix == 'k' || suffix == 'K') {
+        multiplier = 1024;
+    } else if (suffix == 'm' || suffix == 'M') {
+        multiplier = 1024 * 1024;
+    } else if (suffix == 'g' || suffix == 'G') {
+        multiplier = 1024 * 1024 * 1024;
+    } else if (!isdigit(suffix)) {
+        std::cerr << "Couldn't parse memory value '" << input << "': '" << suffix << "' is an unknown modifier"
+                  << std::endl;
+        exit(1);
+    }
+
+    std::string numberPart = std::isdigit(suffix) ? input : input.substr(0, input.size() - 1);
+    try {
+        int value = boost::lexical_cast<int>(numberPart);
+        return value * multiplier;
+    } catch (const boost::bad_lexical_cast&) {
+        std::cerr << "Couldn't parse memory value '" << input << "'" << std::endl;
+        exit(1);
+    }
+}
+
+// converts e.g. 1024 to 1k
+std::string CLIOptions::convertMemoryInteger(int memSize) {
+    int rem = memSize;
+    int mod = 0;
+    std::string modChar;
+
+    while ((rem % 1024) == 0 && (mod < 4)) {
+        rem /= 1024;
+        mod++;
+    }
+
+    switch (mod) {
+    case 1:
+        modChar = 'k';
+        break;
+    case 2:
+        modChar = 'm';
+        break;
+    case 3:
+        modChar = 'g';
+        break;
+    default:
+        rem = memSize;
+        modChar = "";
+    }
+
+    return std::to_string(rem) + modChar;
+}
+}
diff --git a/lang/LangSource/SC_CLIOptions.hpp b/lang/LangSource/SC_CLIOptions.hpp
new file mode 100644
index 00000000000..8b7c8e6ba4b
--- /dev/null
+++ b/lang/LangSource/SC_CLIOptions.hpp
@@ -0,0 +1,30 @@
+#include "boost/program_options.hpp"
+#include <string>
+#include <optional>
+#include "SC_TerminalClient.h"
+
+namespace po = boost::program_options;
+
+namespace SC_CLI {
+class CLIOptions {
+public:
+    std::optional<int> parse(int argc, char* argv[], SC_TerminalClient::Options& terminalOptions);
+
+    std::string mInputFile;
+    std::vector<std::string> mScArgs;
+
+private:
+    bool mParsingFailed = false;
+    bool mPrintHelp = false;
+    bool mPrintVersion = false;
+
+    po::options_description buildGenericDescription();
+    po::options_description buildTerminalDescription(SC_TerminalClient::Options& terminalOptions);
+    static po::options_description buildLanguageConfigDescription();
+
+    static void parseLanguageConfig(po::variables_map& vm);
+
+    static int parseMemoryString(const std::string& input);
+    static std::string convertMemoryInteger(int memSize);
+};
+}
diff --git a/lang/LangSource/SC_LanguageClient.cpp b/lang/LangSource/SC_LanguageClient.cpp
index af7c15b8757..542d64f25f3 100644
--- a/lang/LangSource/SC_LanguageClient.cpp
+++ b/lang/LangSource/SC_LanguageClient.cpp
@@ -71,7 +71,7 @@ class HiddenLanguageClient {
     bool mRunning;
 };
 
-SC_LanguageClient::SC_LanguageClient(const char* name) {
+SC_LanguageClient::SC_LanguageClient(const std::string& name) {
     mHiddenClient = new HiddenLanguageClient;
 
     lockInstance();
@@ -99,9 +99,8 @@ void SC_LanguageClient::initRuntime(const Options& opt) {
     // start virtual machine
     if (!mHiddenClient->mRunning) {
         mHiddenClient->mRunning = true;
-        if (opt.mRuntimeDir) {
-            int err = chdir(opt.mRuntimeDir);
-            if (err)
+        if (!opt.mRuntimeDir.empty()) {
+            if (chdir(opt.mRuntimeDir.c_str()) != 0)
                 error("Cannot change to runtime directory: %s", strerror(errno));
         }
         pyr_init_mem_pools(opt.mMemSpace, opt.mMemGrow);
@@ -167,7 +166,7 @@ void SC_LanguageClient::runLibrary(const char* methodName) {
     unlock();
 }
 
-void SC_LanguageClient::executeFile(const char* fileName) {
+void SC_LanguageClient::executeFile(const std::string& fileName) {
     std::string escaped_file_name(fileName);
     int i = 0;
     while (i < escaped_file_name.size()) {
@@ -180,88 +179,6 @@ void SC_LanguageClient::executeFile(const char* fileName) {
     runLibrary(s_interpretCmdLine);
 }
 
-void SC_LanguageClient::snprintMemArg(char* dst, size_t size, int arg) {
-    int rem = arg;
-    int mod = 0;
-    const char* modstr = "";
-
-    while (((rem % 1024) == 0) && (mod < 4)) {
-        rem /= 1024;
-        mod++;
-    }
-
-    switch (mod) {
-    case 0:
-        modstr = "";
-        break;
-    case 1:
-        modstr = "k";
-        break;
-    case 2:
-        modstr = "m";
-        break;
-    case 3:
-        modstr = "g";
-        break;
-    default:
-        rem = arg;
-        modstr = "";
-        break;
-    }
-
-    snprintf(dst, size, "%d%s", rem, modstr);
-}
-
-bool SC_LanguageClient::parseMemArg(const char* arg, int* res) {
-    long value, factor = 1;
-    char* endPtr = nullptr;
-
-    if (*arg == '\0')
-        return false;
-
-    value = strtol(arg, &endPtr, 0);
-
-    char spec = *endPtr++;
-    if (spec != '\0') {
-        if (*endPtr != '\0')
-            // trailing characters
-            return false;
-
-        switch (spec) {
-        case 'k':
-            factor = 1024;
-            break;
-        case 'm':
-            factor = 1024 * 1024;
-            break;
-        default:
-            // invalid mem spec
-            return false;
-        }
-    }
-
-    *res = value * factor;
-
-    return true;
-}
-
-bool SC_LanguageClient::parsePortArg(const char* arg, int* res) {
-    long value;
-    char* endPtr;
-
-    if (*arg == '\0')
-        return false;
-
-    value = strtol(arg, &endPtr, 0);
-
-    if ((*endPtr != '\0') || (value < 0) || (value > 65535))
-        return false;
-
-    *res = value;
-
-    return true;
-}
-
 void SC_LanguageClient::tick() {
     if (trylock()) {
         if (isLibraryCompiled()) {
diff --git a/lang/LangSource/SC_LanguageConfig.cpp b/lang/LangSource/SC_LanguageConfig.cpp
index 8d1f7a8ae24..1fd0388fe1f 100644
--- a/lang/LangSource/SC_LanguageConfig.cpp
+++ b/lang/LangSource/SC_LanguageConfig.cpp
@@ -38,12 +38,12 @@ bool SC_LanguageConfig::gPostInlineWarnings = false;
 
 SC_LanguageConfig* gLanguageConfig;
 
-static const char* INCLUDE_PATHS = "includePaths";
-static const char* EXCLUDE_PATHS = "excludePaths";
-static const char* POST_INLINE_WARNINGS = "postInlineWarnings";
-static const char* CLASS_LIB_DIR_NAME = "SCClassLibrary";
-const char* SCLANG_YAML_CONFIG_FILENAME = "sclang_conf.yaml";
-static const char* EXCLUDE_DEFAULT_PATHS = "excludeDefaultPaths";
+static const std::string INCLUDE_PATHS = "includePaths";
+static const std::string EXCLUDE_PATHS = "excludePaths";
+static const std::string POST_INLINE_WARNINGS = "postInlineWarnings";
+static const std::string CLASS_LIB_DIR_NAME = "SCClassLibrary";
+const std::string SCLANG_YAML_CONFIG_FILENAME = "sclang_conf.yaml";
+static std::string EXCLUDE_DEFAULT_PATHS = "excludeDefaultPaths";
 
 using DirName = SC_Filesystem::DirName;
 namespace fs = std::filesystem;
@@ -103,7 +103,7 @@ bool SC_LanguageConfig::removeIncludedDirectory(const Path& path) { return remov
 
 bool SC_LanguageConfig::removeExcludedDirectory(const Path& path) { return removePath(mExcludedDirectories, path); }
 
-static void processPathList(const char* nodeName, YAML::Node& doc,
+static void processPathList(const std::string& nodeName, YAML::Node& doc,
                             const std::function<void(const std::filesystem::path&)>& func) {
     const YAML::Node& items = doc[nodeName];
     if (items && items.IsSequence()) {
@@ -117,14 +117,14 @@ static void processPathList(const char* nodeName, YAML::Node& doc,
     }
 }
 
-static void processBool(const char* nodeName, YAML::Node& doc, const std::function<void(bool)>& successFunc,
+static void processBool(const std::string& nodeName, YAML::Node& doc, const std::function<void(bool)>& successFunc,
                         const std::function<void()>& failFunc) {
     const YAML::Node& item = doc[nodeName];
     if (item) {
         try {
             successFunc(item.as<bool>());
         } catch (...) {
-            postfl("WARNING: Cannot parse config file entry \"%s\"\n", nodeName);
+            postfl("WARNING: Cannot parse config file entry \"%s\"\n", nodeName.c_str());
             failFunc();
         }
     } else {
diff --git a/lang/LangSource/SC_LanguageConfig.hpp b/lang/LangSource/SC_LanguageConfig.hpp
index 0642eabf815..3a391b71cee 100644
--- a/lang/LangSource/SC_LanguageConfig.hpp
+++ b/lang/LangSource/SC_LanguageConfig.hpp
@@ -29,7 +29,7 @@
 
 class SC_LanguageConfig;
 extern SC_LanguageConfig* gLanguageConfig;
-extern const char* SCLANG_YAML_CONFIG_FILENAME;
+extern const std::string SCLANG_YAML_CONFIG_FILENAME;
 
 /**
  * \brief Language configuration settings.
diff --git a/lang/LangSource/SC_TerminalClient.cpp b/lang/LangSource/SC_TerminalClient.cpp
index 3d80ea90711..6049774ff3a 100644
--- a/lang/LangSource/SC_TerminalClient.cpp
+++ b/lang/LangSource/SC_TerminalClient.cpp
@@ -25,7 +25,10 @@
 */
 
 #include "SC_TerminalClient.h"
+
+#include "SC_CLIOptions.hpp"
 #include <cstdlib>
+
 #ifdef SC_QT
 #    include "../../QtCollider/LanguageClient.h"
 #endif
@@ -72,7 +75,7 @@ static FILE* gPostDest = stdout;
 static UINT gOldCodePage; // for remembering the old codepage when we switch to UTF-8
 #endif
 
-SC_TerminalClient::SC_TerminalClient(const char* name):
+SC_TerminalClient::SC_TerminalClient(const std::string& name):
     SC_LanguageClient(name),
     mReturnCode(0),
     mUseReadline(false),
@@ -102,160 +105,20 @@ void SC_TerminalClient::postError(const char* str, size_t len) {
 
 void SC_TerminalClient::flush() { fflush(gPostDest); }
 
-void SC_TerminalClient::printUsage() {
-    Options opt;
-
-    const size_t bufSize = 128;
-    char memGrowBuf[bufSize];
-    char memSpaceBuf[bufSize];
-
-    snprintMemArg(memGrowBuf, bufSize, opt.mMemGrow);
-    snprintMemArg(memSpaceBuf, bufSize, opt.mMemSpace);
-
-    fprintf(stdout, "Usage:\n   %s [options] [file..] [-]\n\n", getName());
-    fprintf(
-        stdout,
-        "Options:\n"
-        "   -v                             Print supercollider version and exit\n"
-        "   -d <path>                      Set runtime directory\n"
-        "   -D                             Enter daemon mode (no input)\n"
-        "   -g <memory-growth>[km]         Set heap growth (default %s)\n"
-        "   -h                             Display this message and exit\n"
-        "   -l <path>                      Set library configuration file\n"
-        "   -m <memory-space>[km]          Set initial heap size (default %s)\n"
-        "   -r                             Call Main.run on startup\n"
-        "   -s                             Call Main.stop on shutdown\n"
-        "   -u <network-port-number>       Set UDP listening port (default %d)\n"
-        "   -i <ide-name>                  Specify IDE name (for enabling IDE-specific class code, default \"%s\")\n"
-        "   -a                             Standalone mode (exclude SCClassLibrary and user and system Extensions "
-        "folders from search path)\n",
-        memGrowBuf, memSpaceBuf, opt.mPort, SC_Filesystem::instance().getIdeName().c_str());
-}
-
-bool SC_TerminalClient::parseOptions(int& argc, char**& argv, Options& opt) {
-    const char* optstr = ":d:Dg:hl:m:rsu:i:av";
-    int c;
-
-    // inhibit error reporting
-    opterr = 0;
-
-    while ((c = getopt(argc, argv, optstr)) != -1) {
-        switch (c) {
-        case 'd':
-            opt.mRuntimeDir = optarg;
-            break;
-        case 'D':
-            opt.mDaemon = true;
-            break;
-        case 'g':
-            if (!parseMemArg(optarg, &opt.mMemGrow)) {
-                optopt = c;
-                goto optArgInvalid;
-            }
-            break;
-        case 'h':
-            goto help;
-        case 'l':
-            opt.mLibraryConfigFile = optarg;
-            break;
-        case 'm':
-            if (!parseMemArg(optarg, &opt.mMemSpace)) {
-                optopt = c;
-                goto optArgInvalid;
-            }
-            break;
-        case 'r':
-            opt.mCallRun = true;
-            break;
-        case 'v':
-            fprintf(stdout, "sclang %s (%s)\n", SC_VersionString().c_str(), SC_BuildString().c_str());
-            quit(0);
-            return false;
-            break;
-        case 's':
-            opt.mCallStop = true;
-            break;
-        case 'u':
-            if (!parsePortArg(optarg, &opt.mPort)) {
-                optopt = c;
-                goto optArgInvalid;
-            }
-            break;
-        case '?':
-            goto optInvalid;
-            break;
-        case ':':
-            goto optArgExpected;
-            break;
-        case 'i':
-            SC_Filesystem::instance().setIdeName(optarg);
-            break;
-        case 'a':
-            opt.mStandalone = true;
-            break;
-        default:
-            ::post("%s: unknown error (getopt)\n", getName());
-            quit(255);
-            return false;
-        }
-    }
-
-    argv += optind;
-    argc -= optind;
-
-    return true;
-
-help:
-    printUsage();
-    quit(0);
-    return false;
-
-optInvalid:
-    ::post("%s: invalid option -%c\n", getName(), optopt);
-    quit(1);
-    return false;
-
-optArgExpected:
-    ::post("%s: missing argument for option -%c\n", getName(), optopt);
-    quit(1);
-    return false;
-
-optArgInvalid:
-    ::post("%s: invalid argument for option -%c -- %s\n", getName(), optopt, optarg);
-    quit(1);
-    return false;
-}
-
 int SC_TerminalClient::run(int argc, char** argv) {
     Options& opt = mOptions;
 
-    if (!parseOptions(argc, argv, opt)) {
-        return mReturnCode;
-    }
-
-    // finish argv processing
-    const char* codeFile = nullptr;
+    SC_CLI::CLIOptions cliOptions;
 
-    if (argc > 0) {
-        codeFile = argv[0];
-        opt.mDaemon = true;
-        argv++;
-        argc--;
+    if (const auto errorCode = cliOptions.parse(argc, argv, mOptions)) {
+        return *errorCode;
     }
 
-    opt.mArgc = argc;
-    opt.mArgv = argv;
-
-    // read library configuration file
-    if (opt.mLibraryConfigFile)
-        SC_LanguageConfig::setConfigPath(opt.mLibraryConfigFile);
-    SC_LanguageConfig::readLibraryConfig(opt.mStandalone);
-
     // initialize runtime
     initRuntime(opt);
 
     // Create config directory so that it can be used by Quarks, etc. See #2919.
-    if (!opt.mStandalone && !opt.mLibraryConfigFile)
+    if (!opt.mStandalone && opt.mLibraryConfigFile.empty())
         std::filesystem::create_directories(SC_Filesystem::instance().getDirectory(SC_Filesystem::DirName::UserConfig));
 
     // startup library
@@ -270,8 +133,8 @@ int SC_TerminalClient::run(int argc, char** argv) {
     }
 
     // enter main loop
-    if (codeFile)
-        executeFile(codeFile);
+    if (!cliOptions.mInputFile.empty())
+        executeFile(cliOptions.mInputFile);
     if (opt.mCallRun)
         runMain();
 
@@ -691,8 +554,8 @@ void SC_TerminalClient::cleanupInput() {
 }
 
 int SC_TerminalClient::prArgv(struct VMGlobals* g, int) {
-    int argc = ((SC_TerminalClient*)SC_TerminalClient::instance())->options().mArgc;
-    char** argv = ((SC_TerminalClient*)SC_TerminalClient::instance())->options().mArgv;
+    auto& argv = static_cast<SC_TerminalClient*>(SC_TerminalClient::instance())->options().mArgs;
+    size_t argc = argv.size();
 
     PyrSlot* argvSlot = g->sp;
 
@@ -700,7 +563,7 @@ int SC_TerminalClient::prArgv(struct VMGlobals* g, int) {
     SetObject(argvSlot, argvObj); // this is okay here as we don't use the receiver
 
     for (int i = 0; i < argc; i++) {
-        PyrString* str = newPyrString(g->gc, argv[i], 0, true);
+        PyrString* str = newPyrString(g->gc, argv[i].c_str(), 0, true);
         SetObject(argvObj->slots + i, str);
         argvObj->size++;
         g->gc->GCWriteNew(argvObj, (PyrObject*)str); // we know str is white so we can use GCWriteNew
diff --git a/lang/LangSource/SC_TerminalClient.h b/lang/LangSource/SC_TerminalClient.h
index b4cf0e96482..38696ae5f87 100644
--- a/lang/LangSource/SC_TerminalClient.h
+++ b/lang/LangSource/SC_TerminalClient.h
@@ -55,25 +55,15 @@ class SCLANG_DLLEXPORT SC_TerminalClient : public SC_LanguageClient {
     };
 
     struct Options : public SC_LanguageClient::Options {
-        Options():
-            mLibraryConfigFile(0),
-            mDaemon(false),
-            mCallRun(false),
-            mCallStop(false),
-            mStandalone(false),
-            mArgc(0),
-            mArgv(0) {}
-
-        const char* mLibraryConfigFile;
+        std::string mLibraryConfigFile;
         bool mDaemon;
         bool mCallRun;
         bool mCallStop;
         bool mStandalone;
-        int mArgc;
-        char** mArgv;
+        std::vector<std::string> mArgs;
     };
 
-    SC_TerminalClient(const char* name);
+    SC_TerminalClient(const std::string& name);
     virtual ~SC_TerminalClient();
 
     const Options& options() const { return mOptions; }
@@ -94,9 +84,6 @@ class SCLANG_DLLEXPORT SC_TerminalClient : public SC_LanguageClient {
     void stop() { mIoContext.stop(); }
 
 protected:
-    bool parseOptions(int& argc, char**& argv, Options& opt);
-    void printUsage();
-
     void interpretCmdLine(const char* cmdLine, bool silent);
     void interpretCmdLine(const char* buf, size_t size, bool silent);
 


Reviews:
user: login: Spacechild1
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T14:17:00Z
user: login: capital-G
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T15:24:45Z
user: login: capital-G
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T15:55:02Z
user: login: capital-G
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T16:20:46Z
user: login: Spacechild1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T16:38:22Z
user: login: Spacechild1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T19:03:15Z
user: login: Spacechild1
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T19:29:54Z
user: login: Spacechild1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T09:50:31Z
user: login: Spacechild1
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-29T21:38:10Z
user: login: Spacechild1
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T17:49:36Z
user: login: JordanHendersonMusic
body: I think this is a good idea, I've just got one suggestion where I think the code could be easier to understand.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T20:27:04Z
user: login: Spacechild1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T20:53:10Z
user: login: capital-G
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T15:31:19Z
user: login: JordanHendersonMusic
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T15:38:26Z
user: login: JordanHendersonMusic
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T14:36:01Z

Review comments:
diff_hunk: @@ -0,0 +1,31 @@
+#include "boost/program_options.hpp"
+#include <string>
+#include "SC_TerminalClient.h"
+
+namespace po = boost::program_options;
+using namespace std;
+
+namespace SC_CLI {
+class CLIOptions {
+public:
+    CLIOptions() {};
+    bool parse(int argc, char* argv[], SC_TerminalClient::Options& terminal_options, int& exit_code);
+
+    string mInputFile = "";
+    vector<string> mScArgs = {};
+
+private:
+    bool mParsingFailed = false;
+    bool mPrintHelp = false;
+    bool mPrintVersion = false;
+
+    po::options_description build_generic_description();
path: lang/LangSource/SC_CLIOptions.hpp
user: login: Spacechild1
body: The sclang/scsynth codebase generally uses camelCase. This goes both for function names as well as variables.
created_at: 2025-03-26T13:58:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,277 @@
+#include "boost/program_options.hpp"
+#include <iostream>
+#include <string>
+#include "SC_TerminalClient.h"
+#include "SC_CLIOptions.hpp"
+
+#include <SC_Filesystem.hpp>
+#include <SC_LanguageConfig.hpp>
+#include <SC_Version.hpp>
+
+namespace po = boost::program_options;
+using namespace std;
+
+namespace SC_CLI {
+
+/// parses the CLI - if the program should terminate after parsing it will return true
+/// an exit code is passed by reference
+bool CLIOptions::parse(int argc, char* argv[], SC_TerminalClient::Options& terminal_options, int& exit_code) {
+    exit_code = 0;
+    po::options_description description("Interpreter for sclang (SuperCollider language)");
+
+    auto generic_description = build_generic_description();
+    auto terminal_description = build_terminal_description(terminal_options);
+    auto language_config_description = build_language_config_description();
+
+    // Hidden options, will be allowed both on command line and
+    // in config file, but will not be shown to the user.
+    po::options_description hidden_description("Hidden options");
+    hidden_description.add_options()("input-file", po::value<string>(&mInputFile),
+                                     "input file")("sc-args", po::value<vector<string>>(&mScArgs), "sc args");
+
+    // positional options are declared here but captured via hidden description
+    po::positional_options_description pos_options;
+    pos_options.add("input-file", 1);
+    pos_options.add("sc-args", -1);
+
+    po::options_description cmdline_options;
+    cmdline_options.add(generic_description)
+        .add(terminal_description)
+        .add(hidden_description)
+        .add(language_config_description);
+
+    po::variables_map vm;
+    try {
+        store(po::command_line_parser(argc, argv).options(cmdline_options).positional(pos_options).run(), vm);
+        notify(vm);
+    } catch (po::unknown_option& e) {
+        cerr << e.what() << endl;
+        exit_code = 1;
+        return true;
+    } catch (po::invalid_option_value& e) {
+        cerr << e.what() << endl;
+        exit_code = 1;
+        return true;
+    }
+    if (mParsingFailed) {
+        exit_code = 1;
+        return true;
+    }
+
+    if (vm.count("sc-args")) {
+        terminal_options.mArgs = vm["sc-args"].as<vector<string>>();
+    }
+
+    if (mPrintVersion) {
+        cout << "sclang " << SC_VersionString() << " (" << SC_BuildString() << ")" << endl;
+        return true;
+    }
+
+    // this is handled here and not in the description declaration b/c we need access to the declarations
+    if (mPrintHelp) {
+        // construct a options_description w/o hidden positional options
+        po::options_description visible_description("");
+        visible_description.add(generic_description).add(terminal_description).add(language_config_description);
+
+        cout << "Start a REPL interpreter:           sclang [ -options ]\n"
+                "Execute a .scd file:                sclang [ -options ] path_to_sc_file [ args passed to sclang... ]\n"
+             << visible_description << endl;
+        return true;
+    }
+
+    if (!mInputFile.empty()) {
+        terminal_options.mDaemon = true;
+    }
+
+    SC_LanguageConfig::readLibraryConfig(terminal_options.mStandalone);
+
+    // for the language config we don't use the notifier of program options because
+    // we want to fill the config with a provided yaml file or their default values.
+    // as po does not allow to specify the "notify order", we parse the config manually
+    parse_language_config(vm);
+
+
+    return false;
+}
+
+po::options_description CLIOptions::build_generic_description() {
path: lang/LangSource/SC_CLIOptions.cpp
user: login: Spacechild1
body: `build_generic_description` and `build_terminal_description` seem to be pure functions and as such they should be either `static` methods or free functions! That was probably just an oversight since the other helper methods are already `static`.
created_at: 2025-03-26T14:02:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,277 @@
+#include "boost/program_options.hpp"
+#include <iostream>
+#include <string>
+#include "SC_TerminalClient.h"
+#include "SC_CLIOptions.hpp"
+
+#include <SC_Filesystem.hpp>
+#include <SC_LanguageConfig.hpp>
+#include <SC_Version.hpp>
+
+namespace po = boost::program_options;
+using namespace std;
+
+namespace SC_CLI {
+
+/// parses the CLI - if the program should terminate after parsing it will return true
+/// an exit code is passed by reference
+bool CLIOptions::parse(int argc, char* argv[], SC_TerminalClient::Options& terminal_options, int& exit_code) {
+    exit_code = 0;
+    po::options_description description("Interpreter for sclang (SuperCollider language)");
+
+    auto generic_description = build_generic_description();
+    auto terminal_description = build_terminal_description(terminal_options);
+    auto language_config_description = build_language_config_description();
+
+    // Hidden options, will be allowed both on command line and
+    // in config file, but will not be shown to the user.
+    po::options_description hidden_description("Hidden options");
+    hidden_description.add_options()("input-file", po::value<string>(&mInputFile),
+                                     "input file")("sc-args", po::value<vector<string>>(&mScArgs), "sc args");
+
+    // positional options are declared here but captured via hidden description
+    po::positional_options_description pos_options;
+    pos_options.add("input-file", 1);
+    pos_options.add("sc-args", -1);
+
+    po::options_description cmdline_options;
+    cmdline_options.add(generic_description)
+        .add(terminal_description)
+        .add(hidden_description)
+        .add(language_config_description);
+
+    po::variables_map vm;
+    try {
+        store(po::command_line_parser(argc, argv).options(cmdline_options).positional(pos_options).run(), vm);
+        notify(vm);
+    } catch (po::unknown_option& e) {
+        cerr << e.what() << endl;
+        exit_code = 1;
+        return true;
+    } catch (po::invalid_option_value& e) {
+        cerr << e.what() << endl;
+        exit_code = 1;
+        return true;
+    }
+    if (mParsingFailed) {
+        exit_code = 1;
+        return true;
+    }
+
+    if (vm.count("sc-args")) {
+        terminal_options.mArgs = vm["sc-args"].as<vector<string>>();
+    }
+
+    if (mPrintVersion) {
+        cout << "sclang " << SC_VersionString() << " (" << SC_BuildString() << ")" << endl;
+        return true;
+    }
+
+    // this is handled here and not in the description declaration b/c we need access to the declarations
+    if (mPrintHelp) {
+        // construct a options_description w/o hidden positional options
+        po::options_description visible_description("");
+        visible_description.add(generic_description).add(terminal_description).add(language_config_description);
+
+        cout << "Start a REPL interpreter:           sclang [ -options ]\n"
+                "Execute a .scd file:                sclang [ -options ] path_to_sc_file [ args passed to sclang... ]\n"
+             << visible_description << endl;
+        return true;
+    }
+
+    if (!mInputFile.empty()) {
+        terminal_options.mDaemon = true;
+    }
+
+    SC_LanguageConfig::readLibraryConfig(terminal_options.mStandalone);
+
+    // for the language config we don't use the notifier of program options because
+    // we want to fill the config with a provided yaml file or their default values.
+    // as po does not allow to specify the "notify order", we parse the config manually
+    parse_language_config(vm);
+
+
+    return false;
+}
+
+po::options_description CLIOptions::build_generic_description() {
+    po::options_description options("Generic options");
+    options.add_options()("help,h", po::bool_switch(&mPrintHelp), "Print this message and exit")(
+        "version,v", po::bool_switch(&mPrintVersion), "Print sclang version and exit");
+    return options;
+}
+
+po::options_description CLIOptions::build_terminal_description(SC_TerminalClient::Options& terminal_options) {
+    po::options_description description("sclang options");
+
+    // clang-format off
+    description.add_options()
+        (
+            "runtime-directory,d",
+            po::value<string>(&terminal_options.mRuntimeDir),
+            "Set runtime directory"
+        )
+        (
+            "daemon,D",
+            po::bool_switch(&terminal_options.mDaemon)->default_value(terminal_options.mDaemon),
+            "Enter daemon mode"
+        )
+        (
+            "heap-growth,g",
+            po::value<string>()->default_value(convert_memory_integer(terminal_options.mMemGrow))->notifier([&](const string &heapGrowth) {
+                terminal_options.mMemGrow = parse_memory_string(heapGrowth);
+            }),
+            "Set heap growth size (allows k, m and g as suffix multipliers)"
+        )
+        (
+            "yaml-config,l",
+            po::value<string>(&terminal_options.mLibraryConfigFile)->notifier([&](const string &yaml_config) {
+                terminal_options.mLibraryConfigFile = yaml_config;
+                SC_LanguageConfig::setConfigPath(yaml_config);
+            }),
+            "Set yaml config"
+        )
+        (
+            "heap-size,m",
+            po::value<string>()->default_value(convert_memory_integer(terminal_options.mMemSpace))->notifier([&](const string &heapSize) {
+                terminal_options.mMemSpace = parse_memory_string(heapSize);
+            }),
+            "Set initial heap size (allows k, m and g as suffix multipliers)"
+        )
+        (
+            "call-run,r",
+            po::bool_switch(&terminal_options.mCallRun)->default_value(terminal_options.mCallRun),
+            "Call Main.run on startup"
+        )
+        (
+            "call-stop,s",
+            po::bool_switch(&terminal_options.mCallStop)->default_value(terminal_options.mCallStop),
+            "Call Main.stop on shutdown"
+        )
+        (
+            "port,u",
+            po::value<int>(&terminal_options.mPort)->default_value(terminal_options.mPort)->notifier([&](int port) {
+                if (port < 0 || port > 65535) {
+                    cerr << "UDP port must be in range between 0-65535, but was " << port << endl;
+                    mParsingFailed = true;
+                }
+            }),
+            "Set UDP listening port"
+        )
+        (
+            "ide,i",
+            po::value<string>()->notifier([&](string ide_name) {
+                SC_Filesystem::instance().setIdeName(ide_name);
+            }),
+            "Set IDE name"
+        )
+        (
+            "standalone,a",
+            po::bool_switch(&terminal_options.mStandalone)->default_value(terminal_options.mStandalone),
+            "Standalone mode (exclude SCClassLibrary and user and system Extensions folders from search path)"
+        )
+    ;
+    // clang-format on
+
+    return description;
+}
+
+po::options_description CLIOptions::build_language_config_description() {
+    po::options_description description("Language config options (overwrites provided yaml entries)");
+
+    // clang-format off
+    description.add_options()
+        (
+            "include-path",
+            po::value<vector<string>>(),
+            "class library path to be included for searching (allows multiple mentions)"
path: lang/LangSource/SC_CLIOptions.cpp
user: login: Spacechild1
body: "class library path" should start with a capital letter for consistency :monocle_face:
created_at: 2025-03-26T14:04:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,277 @@
+#include "boost/program_options.hpp"
+#include <iostream>
+#include <string>
+#include "SC_TerminalClient.h"
+#include "SC_CLIOptions.hpp"
+
+#include <SC_Filesystem.hpp>
+#include <SC_LanguageConfig.hpp>
+#include <SC_Version.hpp>
+
+namespace po = boost::program_options;
+using namespace std;
+
+namespace SC_CLI {
+
+/// parses the CLI - if the program should terminate after parsing it will return true
+/// an exit code is passed by reference
+bool CLIOptions::parse(int argc, char* argv[], SC_TerminalClient::Options& terminal_options, int& exit_code) {
+    exit_code = 0;
+    po::options_description description("Interpreter for sclang (SuperCollider language)");
+
+    auto generic_description = build_generic_description();
+    auto terminal_description = build_terminal_description(terminal_options);
+    auto language_config_description = build_language_config_description();
+
+    // Hidden options, will be allowed both on command line and
+    // in config file, but will not be shown to the user.
+    po::options_description hidden_description("Hidden options");
+    hidden_description.add_options()("input-file", po::value<string>(&mInputFile),
+                                     "input file")("sc-args", po::value<vector<string>>(&mScArgs), "sc args");
+
+    // positional options are declared here but captured via hidden description
+    po::positional_options_description pos_options;
+    pos_options.add("input-file", 1);
+    pos_options.add("sc-args", -1);
+
+    po::options_description cmdline_options;
+    cmdline_options.add(generic_description)
+        .add(terminal_description)
+        .add(hidden_description)
+        .add(language_config_description);
+
+    po::variables_map vm;
+    try {
+        store(po::command_line_parser(argc, argv).options(cmdline_options).positional(pos_options).run(), vm);
+        notify(vm);
+    } catch (po::unknown_option& e) {
+        cerr << e.what() << endl;
+        exit_code = 1;
+        return true;
+    } catch (po::invalid_option_value& e) {
+        cerr << e.what() << endl;
+        exit_code = 1;
+        return true;
+    }
+    if (mParsingFailed) {
+        exit_code = 1;
+        return true;
+    }
+
+    if (vm.count("sc-args")) {
+        terminal_options.mArgs = vm["sc-args"].as<vector<string>>();
+    }
+
+    if (mPrintVersion) {
+        cout << "sclang " << SC_VersionString() << " (" << SC_BuildString() << ")" << endl;
+        return true;
+    }
+
+    // this is handled here and not in the description declaration b/c we need access to the declarations
+    if (mPrintHelp) {
+        // construct a options_description w/o hidden positional options
+        po::options_description visible_description("");
+        visible_description.add(generic_description).add(terminal_description).add(language_config_description);
+
+        cout << "Start a REPL interpreter:           sclang [ -options ]\n"
+                "Execute a .scd file:                sclang [ -options ] path_to_sc_file [ args passed to sclang... ]\n"
+             << visible_description << endl;
+        return true;
+    }
+
+    if (!mInputFile.empty()) {
+        terminal_options.mDaemon = true;
+    }
+
+    SC_LanguageConfig::readLibraryConfig(terminal_options.mStandalone);
+
+    // for the language config we don't use the notifier of program options because
+    // we want to fill the config with a provided yaml file or their default values.
+    // as po does not allow to specify the "notify order", we parse the config manually
+    parse_language_config(vm);
+
+
+    return false;
+}
+
+po::options_description CLIOptions::build_generic_description() {
+    po::options_description options("Generic options");
+    options.add_options()("help,h", po::bool_switch(&mPrintHelp), "Print this message and exit")(
+        "version,v", po::bool_switch(&mPrintVersion), "Print sclang version and exit");
+    return options;
+}
+
+po::options_description CLIOptions::build_terminal_description(SC_TerminalClient::Options& terminal_options) {
+    po::options_description description("sclang options");
+
+    // clang-format off
+    description.add_options()
+        (
+            "runtime-directory,d",
+            po::value<string>(&terminal_options.mRuntimeDir),
+            "Set runtime directory"
+        )
+        (
+            "daemon,D",
+            po::bool_switch(&terminal_options.mDaemon)->default_value(terminal_options.mDaemon),
+            "Enter daemon mode"
+        )
+        (
+            "heap-growth,g",
+            po::value<string>()->default_value(convert_memory_integer(terminal_options.mMemGrow))->notifier([&](const string &heapGrowth) {
+                terminal_options.mMemGrow = parse_memory_string(heapGrowth);
+            }),
+            "Set heap growth size (allows k, m and g as suffix multipliers)"
+        )
+        (
+            "yaml-config,l",
+            po::value<string>(&terminal_options.mLibraryConfigFile)->notifier([&](const string &yaml_config) {
+                terminal_options.mLibraryConfigFile = yaml_config;
+                SC_LanguageConfig::setConfigPath(yaml_config);
+            }),
+            "Set yaml config"
+        )
+        (
+            "heap-size,m",
+            po::value<string>()->default_value(convert_memory_integer(terminal_options.mMemSpace))->notifier([&](const string &heapSize) {
+                terminal_options.mMemSpace = parse_memory_string(heapSize);
+            }),
+            "Set initial heap size (allows k, m and g as suffix multipliers)"
+        )
+        (
+            "call-run,r",
+            po::bool_switch(&terminal_options.mCallRun)->default_value(terminal_options.mCallRun),
+            "Call Main.run on startup"
+        )
+        (
+            "call-stop,s",
+            po::bool_switch(&terminal_options.mCallStop)->default_value(terminal_options.mCallStop),
+            "Call Main.stop on shutdown"
+        )
+        (
+            "port,u",
+            po::value<int>(&terminal_options.mPort)->default_value(terminal_options.mPort)->notifier([&](int port) {
+                if (port < 0 || port > 65535) {
+                    cerr << "UDP port must be in range between 0-65535, but was " << port << endl;
+                    mParsingFailed = true;
+                }
+            }),
+            "Set UDP listening port"
+        )
+        (
+            "ide,i",
+            po::value<string>()->notifier([&](string ide_name) {
+                SC_Filesystem::instance().setIdeName(ide_name);
+            }),
+            "Set IDE name"
+        )
+        (
+            "standalone,a",
+            po::bool_switch(&terminal_options.mStandalone)->default_value(terminal_options.mStandalone),
+            "Standalone mode (exclude SCClassLibrary and user and system Extensions folders from search path)"
+        )
+    ;
+    // clang-format on
+
+    return description;
+}
+
+po::options_description CLIOptions::build_language_config_description() {
+    po::options_description description("Language config options (overwrites provided yaml entries)");
+
+    // clang-format off
+    description.add_options()
+        (
+            "include-path",
+            po::value<vector<string>>(),
+            "class library path to be included for searching (allows multiple mentions)"
+        )
+        (
+            "exclude-path",
+            po::value<vector<string>>(),
+            "class library path to be to excluded from searching (overrides includePaths, allows multiple mentions)"
path: lang/LangSource/SC_CLIOptions.cpp
user: login: Spacechild1
body: Small typo:

> class library path to be ~~to~~ excluded from searching

Also start with capital letter.
created_at: 2025-03-26T14:05:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,31 @@
+#include "boost/program_options.hpp"
+#include <string>
+#include "SC_TerminalClient.h"
+
+namespace po = boost::program_options;
+using namespace std;
+
+namespace SC_CLI {
+class CLIOptions {
+public:
+    CLIOptions() {};
+    bool parse(int argc, char* argv[], SC_TerminalClient::Options& terminal_options, int& exit_code);
+
+    string mInputFile = "";
+    vector<string> mScArgs = {};
path: lang/LangSource/SC_CLIOptions.hpp
user: login: Spacechild1
body: Redundant initializers! Should be:
```
string mInputFile;
vector<string> mScArgs;
```
created_at: 2025-03-26T14:07:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -99,8 +99,9 @@ void SC_LanguageClient::initRuntime(const Options& opt) {
     // start virtual machine
     if (!mHiddenClient->mRunning) {
         mHiddenClient->mRunning = true;
-        if (opt.mRuntimeDir) {
-            int err = chdir(opt.mRuntimeDir);
+        if (opt.mRuntimeDir.data() != nullptr) {
path: lang/LangSource/SC_LanguageClient.cpp
user: login: Spacechild1
body: If the string is empty, the return value of `data()` is undefined! I guess you meant to write
`if (!opt.mRuntimeDir.empty()) {`
created_at: 2025-03-26T14:09:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -99,8 +99,9 @@ void SC_LanguageClient::initRuntime(const Options& opt) {
     // start virtual machine
     if (!mHiddenClient->mRunning) {
         mHiddenClient->mRunning = true;
-        if (opt.mRuntimeDir) {
-            int err = chdir(opt.mRuntimeDir);
+        if (opt.mRuntimeDir.data() != nullptr) {
+            // int err = chdir(opt.mRuntimeDir);
+            int err = 0;
path: lang/LangSource/SC_LanguageClient.cpp
user: login: Spacechild1
body: This needs a comment. Why did you remove the `chdir` call? If you remove it, then you should also remove the subsequent error check.

If there is a good reason not to switch the current working directory, then you should put the whole `if` clause in an `#if 0 ` block (+ add a comment why the code is disabled)
created_at: 2025-03-26T14:12:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,12 +38,12 @@ bool SC_LanguageConfig::gPostInlineWarnings = false;
 
 SC_LanguageConfig* gLanguageConfig;
 
-static const char* INCLUDE_PATHS = "includePaths";
-static const char* EXCLUDE_PATHS = "excludePaths";
-static const char* POST_INLINE_WARNINGS = "postInlineWarnings";
-static const char* CLASS_LIB_DIR_NAME = "SCClassLibrary";
-const char* SCLANG_YAML_CONFIG_FILENAME = "sclang_conf.yaml";
-static const char* EXCLUDE_DEFAULT_PATHS = "excludeDefaultPaths";
+static const std::string INCLUDE_PATHS = "includePaths";
+static const std::string EXCLUDE_PATHS = "excludePaths";
+static const std::string POST_INLINE_WARNINGS = "postInlineWarnings";
+static const std::string CLASS_LIB_DIR_NAME = "SCClassLibrary";
+const std::string SCLANG_YAML_CONFIG_FILENAME = "sclang_conf.yaml";
path: lang/LangSource/SC_LanguageConfig.cpp
user: login: Spacechild1
body: missed a `static`.

Actually, you can just put the whole block in an anonymous namespace instead.
created_at: 2025-03-26T14:14:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,17 +41,17 @@ SCLANG_DLLEXPORT void destroyLanguageClient(class SC_LanguageClient*);
 class SCLANG_DLLEXPORT SC_LanguageClient {
 public:
     struct Options {
-        Options(): mMemSpace(2 * 1024 * 1024), mMemGrow(256 * 1024), mPort(57120), mRuntimeDir(0) {}
+        Options(): mMemSpace(2 * 1024 * 1024), mMemGrow(256 * 1024), mPort(57120), mRuntimeDir("") {}
 
         int mMemSpace; // memory space in bytes
         int mMemGrow; // memory growth in bytes
         int mPort; // network port number
-        char* mRuntimeDir; // runtime directory
+        std::string mRuntimeDir; // runtime directory
     };
 
 protected:
     // create singleton instance
-    SC_LanguageClient(const char* name);
+    SC_LanguageClient(const std::string name);
path: include/lang/SC_LanguageClient.h
user: login: Spacechild1
body: Should be `const std::string&`
created_at: 2025-03-26T14:15:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -92,7 +93,7 @@ class SCLANG_DLLEXPORT SC_LanguageClient {
     void runLibrary(const char* methodName);
     void interpretCmdLine();
     void interpretPrintCmdLine();
-    void executeFile(const char* fileName);
+    void executeFile(const std::string fileName);
path: include/lang/SC_LanguageClient.h
user: login: Spacechild1
body: should be `const std::string&`
created_at: 2025-03-26T14:15:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -167,7 +168,7 @@ void SC_LanguageClient::runLibrary(const char* methodName) {
     unlock();
 }
 
-void SC_LanguageClient::executeFile(const char* fileName) {
+void SC_LanguageClient::executeFile(const std::string fileName) {
path: lang/LangSource/SC_LanguageClient.cpp
user: login: Spacechild1
body: `const std::string&`
created_at: 2025-03-26T14:15:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,7 +103,7 @@ bool SC_LanguageConfig::removeIncludedDirectory(const Path& path) { return remov
 
 bool SC_LanguageConfig::removeExcludedDirectory(const Path& path) { return removePath(mExcludedDirectories, path); }
 
-static void processPathList(const char* nodeName, YAML::Node& doc,
+static void processPathList(const std::string nodeName, YAML::Node& doc,
path: lang/LangSource/SC_LanguageConfig.cpp
user: login: Spacechild1
body: `const std::string&`
created_at: 2025-03-26T14:15:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -117,14 +117,14 @@ static void processPathList(const char* nodeName, YAML::Node& doc,
     }
 }
 
-static void processBool(const char* nodeName, YAML::Node& doc, const std::function<void(bool)>& successFunc,
+static void processBool(const std::string nodeName, YAML::Node& doc, const std::function<void(bool)>& successFunc,
path: lang/LangSource/SC_LanguageConfig.cpp
user: login: Spacechild1
body: `const std::string&`
created_at: 2025-03-26T14:16:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,7 +74,7 @@ static FILE* gPostDest = stdout;
 static UINT gOldCodePage; // for remembering the old codepage when we switch to UTF-8
 #endif
 
-SC_TerminalClient::SC_TerminalClient(const char* name):
+SC_TerminalClient::SC_TerminalClient(const std::string name):
path: lang/LangSource/SC_TerminalClient.cpp
user: login: Spacechild1
body: `const std::string&`
created_at: 2025-03-26T14:16:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,24 +56,22 @@ class SCLANG_DLLEXPORT SC_TerminalClient : public SC_LanguageClient {
 
     struct Options : public SC_LanguageClient::Options {
         Options():
-            mLibraryConfigFile(0),
+            mLibraryConfigFile(""),
             mDaemon(false),
             mCallRun(false),
             mCallStop(false),
             mStandalone(false),
-            mArgc(0),
-            mArgv(0) {}
+            mArgs({}) {}
 
-        const char* mLibraryConfigFile;
+        std::string mLibraryConfigFile;
         bool mDaemon;
         bool mCallRun;
         bool mCallStop;
         bool mStandalone;
-        int mArgc;
-        char** mArgv;
+        std::vector<std::string> mArgs;
     };
 
-    SC_TerminalClient(const char* name);
+    SC_TerminalClient(const std::string name);
path: lang/LangSource/SC_TerminalClient.h
user: login: Spacechild1
body: `const std::string&`
created_at: 2025-03-26T14:16:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,277 @@
+#include "boost/program_options.hpp"
+#include <iostream>
+#include <string>
+#include "SC_TerminalClient.h"
+#include "SC_CLIOptions.hpp"
+
+#include <SC_Filesystem.hpp>
+#include <SC_LanguageConfig.hpp>
+#include <SC_Version.hpp>
+
+namespace po = boost::program_options;
+using namespace std;
+
+namespace SC_CLI {
+
+/// parses the CLI - if the program should terminate after parsing it will return true
+/// an exit code is passed by reference
+bool CLIOptions::parse(int argc, char* argv[], SC_TerminalClient::Options& terminal_options, int& exit_code) {
+    exit_code = 0;
+    po::options_description description("Interpreter for sclang (SuperCollider language)");
+
+    auto generic_description = build_generic_description();
+    auto terminal_description = build_terminal_description(terminal_options);
+    auto language_config_description = build_language_config_description();
+
+    // Hidden options, will be allowed both on command line and
+    // in config file, but will not be shown to the user.
+    po::options_description hidden_description("Hidden options");
+    hidden_description.add_options()("input-file", po::value<string>(&mInputFile),
+                                     "input file")("sc-args", po::value<vector<string>>(&mScArgs), "sc args");
+
+    // positional options are declared here but captured via hidden description
+    po::positional_options_description pos_options;
+    pos_options.add("input-file", 1);
+    pos_options.add("sc-args", -1);
+
+    po::options_description cmdline_options;
+    cmdline_options.add(generic_description)
+        .add(terminal_description)
+        .add(hidden_description)
+        .add(language_config_description);
+
+    po::variables_map vm;
+    try {
+        store(po::command_line_parser(argc, argv).options(cmdline_options).positional(pos_options).run(), vm);
+        notify(vm);
+    } catch (po::unknown_option& e) {
+        cerr << e.what() << endl;
+        exit_code = 1;
+        return true;
+    } catch (po::invalid_option_value& e) {
+        cerr << e.what() << endl;
+        exit_code = 1;
+        return true;
+    }
+    if (mParsingFailed) {
+        exit_code = 1;
+        return true;
+    }
+
+    if (vm.count("sc-args")) {
+        terminal_options.mArgs = vm["sc-args"].as<vector<string>>();
+    }
+
+    if (mPrintVersion) {
+        cout << "sclang " << SC_VersionString() << " (" << SC_BuildString() << ")" << endl;
+        return true;
+    }
+
+    // this is handled here and not in the description declaration b/c we need access to the declarations
+    if (mPrintHelp) {
+        // construct a options_description w/o hidden positional options
+        po::options_description visible_description("");
+        visible_description.add(generic_description).add(terminal_description).add(language_config_description);
+
+        cout << "Start a REPL interpreter:           sclang [ -options ]\n"
+                "Execute a .scd file:                sclang [ -options ] path_to_sc_file [ args passed to sclang... ]\n"
+             << visible_description << endl;
+        return true;
+    }
+
+    if (!mInputFile.empty()) {
+        terminal_options.mDaemon = true;
+    }
+
+    SC_LanguageConfig::readLibraryConfig(terminal_options.mStandalone);
+
+    // for the language config we don't use the notifier of program options because
+    // we want to fill the config with a provided yaml file or their default values.
+    // as po does not allow to specify the "notify order", we parse the config manually
+    parse_language_config(vm);
+
+
+    return false;
+}
+
+po::options_description CLIOptions::build_generic_description() {
path: lang/LangSource/SC_CLIOptions.cpp
user: login: capital-G
body: Thanks for the comprehensive review, helps me a lot to learn these things (somehow forgot to pass the strings by reference...).

Concerning this comment tho: these methods need access to the CLIOptions, see e.g.
https://github.com/supercollider/supercollider/blob/6585c12efd2750853f91e709aaf472df743e8ca9/lang/LangSource/SC_CLIOptions.cpp#L97-L102
where `mPrintVersion` and `mPrintHelp` are member variables.

Where it was possible to keep them pure I used static, but I failed to come up with a way to make them static here. So, is it fine to use non-static methods here?
created_at: 2025-03-26T15:24:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,12 +38,12 @@ bool SC_LanguageConfig::gPostInlineWarnings = false;
 
 SC_LanguageConfig* gLanguageConfig;
 
-static const char* INCLUDE_PATHS = "includePaths";
-static const char* EXCLUDE_PATHS = "excludePaths";
-static const char* POST_INLINE_WARNINGS = "postInlineWarnings";
-static const char* CLASS_LIB_DIR_NAME = "SCClassLibrary";
-const char* SCLANG_YAML_CONFIG_FILENAME = "sclang_conf.yaml";
-static const char* EXCLUDE_DEFAULT_PATHS = "excludeDefaultPaths";
+static const std::string INCLUDE_PATHS = "includePaths";
+static const std::string EXCLUDE_PATHS = "excludePaths";
+static const std::string POST_INLINE_WARNINGS = "postInlineWarnings";
+static const std::string CLASS_LIB_DIR_NAME = "SCClassLibrary";
+const std::string SCLANG_YAML_CONFIG_FILENAME = "sclang_conf.yaml";
path: lang/LangSource/SC_LanguageConfig.cpp
user: login: capital-G
body: Should this be static?
The header exports this, see from develop branch

https://github.com/supercollider/supercollider/blob/a28099a199d22eea4fb63d792d83c2c86f97d690/lang/LangSource/SC_LanguageConfig.hpp#L32
created_at: 2025-03-26T15:55:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -99,8 +99,9 @@ void SC_LanguageClient::initRuntime(const Options& opt) {
     // start virtual machine
     if (!mHiddenClient->mRunning) {
         mHiddenClient->mRunning = true;
-        if (opt.mRuntimeDir) {
-            int err = chdir(opt.mRuntimeDir);
+        if (opt.mRuntimeDir.data() != nullptr) {
+            // int err = chdir(opt.mRuntimeDir);
+            int err = 0;
path: lang/LangSource/SC_LanguageClient.cpp
user: login: capital-G
body: This was actually a comment during development - reintroduced the functionality, thx for spotting it!
created_at: 2025-03-26T16:20:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,277 @@
+#include "boost/program_options.hpp"
+#include <iostream>
+#include <string>
+#include "SC_TerminalClient.h"
+#include "SC_CLIOptions.hpp"
+
+#include <SC_Filesystem.hpp>
+#include <SC_LanguageConfig.hpp>
+#include <SC_Version.hpp>
+
+namespace po = boost::program_options;
+using namespace std;
+
+namespace SC_CLI {
+
+/// parses the CLI - if the program should terminate after parsing it will return true
+/// an exit code is passed by reference
+bool CLIOptions::parse(int argc, char* argv[], SC_TerminalClient::Options& terminal_options, int& exit_code) {
+    exit_code = 0;
+    po::options_description description("Interpreter for sclang (SuperCollider language)");
+
+    auto generic_description = build_generic_description();
+    auto terminal_description = build_terminal_description(terminal_options);
+    auto language_config_description = build_language_config_description();
+
+    // Hidden options, will be allowed both on command line and
+    // in config file, but will not be shown to the user.
+    po::options_description hidden_description("Hidden options");
+    hidden_description.add_options()("input-file", po::value<string>(&mInputFile),
+                                     "input file")("sc-args", po::value<vector<string>>(&mScArgs), "sc args");
+
+    // positional options are declared here but captured via hidden description
+    po::positional_options_description pos_options;
+    pos_options.add("input-file", 1);
+    pos_options.add("sc-args", -1);
+
+    po::options_description cmdline_options;
+    cmdline_options.add(generic_description)
+        .add(terminal_description)
+        .add(hidden_description)
+        .add(language_config_description);
+
+    po::variables_map vm;
+    try {
+        store(po::command_line_parser(argc, argv).options(cmdline_options).positional(pos_options).run(), vm);
+        notify(vm);
+    } catch (po::unknown_option& e) {
+        cerr << e.what() << endl;
+        exit_code = 1;
+        return true;
+    } catch (po::invalid_option_value& e) {
+        cerr << e.what() << endl;
+        exit_code = 1;
+        return true;
+    }
+    if (mParsingFailed) {
+        exit_code = 1;
+        return true;
+    }
+
+    if (vm.count("sc-args")) {
+        terminal_options.mArgs = vm["sc-args"].as<vector<string>>();
+    }
+
+    if (mPrintVersion) {
+        cout << "sclang " << SC_VersionString() << " (" << SC_BuildString() << ")" << endl;
+        return true;
+    }
+
+    // this is handled here and not in the description declaration b/c we need access to the declarations
+    if (mPrintHelp) {
+        // construct a options_description w/o hidden positional options
+        po::options_description visible_description("");
+        visible_description.add(generic_description).add(terminal_description).add(language_config_description);
+
+        cout << "Start a REPL interpreter:           sclang [ -options ]\n"
+                "Execute a .scd file:                sclang [ -options ] path_to_sc_file [ args passed to sclang... ]\n"
+             << visible_description << endl;
+        return true;
+    }
+
+    if (!mInputFile.empty()) {
+        terminal_options.mDaemon = true;
+    }
+
+    SC_LanguageConfig::readLibraryConfig(terminal_options.mStandalone);
+
+    // for the language config we don't use the notifier of program options because
+    // we want to fill the config with a provided yaml file or their default values.
+    // as po does not allow to specify the "notify order", we parse the config manually
+    parse_language_config(vm);
+
+
+    return false;
+}
+
+po::options_description CLIOptions::build_generic_description() {
path: lang/LangSource/SC_CLIOptions.cpp
user: login: Spacechild1
body: > where mPrintVersion and mPrintHelp are member variables.

A sorry, totally missed that! Nevermind then.
created_at: 2025-03-26T16:38:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,12 +38,12 @@ bool SC_LanguageConfig::gPostInlineWarnings = false;
 
 SC_LanguageConfig* gLanguageConfig;
 
-static const char* INCLUDE_PATHS = "includePaths";
-static const char* EXCLUDE_PATHS = "excludePaths";
-static const char* POST_INLINE_WARNINGS = "postInlineWarnings";
-static const char* CLASS_LIB_DIR_NAME = "SCClassLibrary";
-const char* SCLANG_YAML_CONFIG_FILENAME = "sclang_conf.yaml";
-static const char* EXCLUDE_DEFAULT_PATHS = "excludeDefaultPaths";
+static const std::string INCLUDE_PATHS = "includePaths";
+static const std::string EXCLUDE_PATHS = "excludePaths";
+static const std::string POST_INLINE_WARNINGS = "postInlineWarnings";
+static const std::string CLASS_LIB_DIR_NAME = "SCClassLibrary";
+const std::string SCLANG_YAML_CONFIG_FILENAME = "sclang_conf.yaml";
path: lang/LangSource/SC_LanguageConfig.cpp
user: login: Spacechild1
body: Ah, I see!
created_at: 2025-03-26T19:03:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,17 +41,17 @@ SCLANG_DLLEXPORT void destroyLanguageClient(class SC_LanguageClient*);
 class SCLANG_DLLEXPORT SC_LanguageClient {
 public:
     struct Options {
-        Options(): mMemSpace(2 * 1024 * 1024), mMemGrow(256 * 1024), mPort(57120), mRuntimeDir(0) {}
+        Options(): mMemSpace(2 * 1024 * 1024), mMemGrow(256 * 1024), mPort(57120), mRuntimeDir("") {}
path: include/lang/SC_LanguageClient.h
user: login: Spacechild1
body: No need to explicitly initialize `mRuntimeDir` because `std::string` has a default constructor.
created_at: 2025-03-26T19:06:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,284 @@
+#include "boost/program_options.hpp"
+#include <iostream>
+#include <string>
+#include "SC_TerminalClient.h"
+#include "SC_CLIOptions.hpp"
+
+#include <SC_Filesystem.hpp>
+#include <SC_LanguageConfig.hpp>
+#include <SC_Version.hpp>
+
+namespace po = boost::program_options;
+using namespace std;
+
+namespace SC_CLI {
+
+/// parses the CLI - if the program should terminate after parsing it will return true
+/// an exit code is passed by reference
+bool CLIOptions::parse(int argc, char* argv[], SC_TerminalClient::Options& terminalOptions, int& exit_code) {
+    exit_code = 0;
path: lang/LangSource/SC_CLIOptions.cpp
user: login: Spacechild1
body: Nitpick: camelCase 🤓 
created_at: 2025-03-26T19:18:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,284 @@
+#include "boost/program_options.hpp"
+#include <iostream>
+#include <string>
+#include "SC_TerminalClient.h"
+#include "SC_CLIOptions.hpp"
+
+#include <SC_Filesystem.hpp>
+#include <SC_LanguageConfig.hpp>
+#include <SC_Version.hpp>
+
+namespace po = boost::program_options;
+using namespace std;
+
+namespace SC_CLI {
+
+/// parses the CLI - if the program should terminate after parsing it will return true
+/// an exit code is passed by reference
+bool CLIOptions::parse(int argc, char* argv[], SC_TerminalClient::Options& terminalOptions, int& exit_code) {
+    exit_code = 0;
+    po::options_description description("Interpreter for sclang (SuperCollider language)");
+
+    auto genericDescription = buildGenericDescription();
+    auto terminalDescription = buildTerminalDescription(terminalOptions);
+    auto languageConfigDescription = buildLanguageConfigDescription();
+
+    // Hidden options, will be allowed both on command line and
+    // in config file, but will not be shown to the user.
+    po::options_description hiddenDescription("Hidden options");
+    // clang-format off
+    hiddenDescription.add_options()
+        ("input-file", po::value<string>(&mInputFile), "input file")
+        ("sc-args", po::value<vector<string>>(&mScArgs), "sc args")
+    ;
+    // clang-format on
+
+    // positional options are declared here but captured via hidden description
+    po::positional_options_description positionalOptions;
+    positionalOptions.add("input-file", 1);
+    positionalOptions.add("sc-args", -1);
+
+    po::options_description cmdLineOptions;
+    cmdLineOptions.add(genericDescription)
+        .add(terminalDescription)
+        .add(hiddenDescription)
+        .add(languageConfigDescription);
+
+    po::variables_map vm;
+    try {
+        store(po::command_line_parser(argc, argv).options(cmdLineOptions).positional(positionalOptions).run(), vm);
+        notify(vm);
+    } catch (po::unknown_option& e) {
+        cerr << e.what() << endl;
+        exit_code = 1;
+        return true;
+    } catch (po::invalid_option_value& e) {
+        cerr << e.what() << endl;
+        exit_code = 1;
+        return true;
+    }
+    if (mParsingFailed) {
+        exit_code = 1;
+        return true;
+    }
+
+    if (vm.count("sc-args")) {
+        terminalOptions.mArgs = vm["sc-args"].as<vector<string>>();
+    }
+
+    if (mPrintVersion) {
+        cout << "sclang " << SC_VersionString() << " (" << SC_BuildString() << ")" << endl;
+        return true;
+    }
+
+    // this is handled here and not in the description declaration b/c we need access to the declarations
+    if (mPrintHelp) {
+        // construct a options_description w/o hidden positional options
+        po::options_description visibleDescription("");
+        visibleDescription.add(genericDescription).add(terminalDescription).add(languageConfigDescription);
+
+        cout << "Start a REPL interpreter:           sclang [ -options ]\n"
+                "Execute a .scd file:                sclang [ -options ] path_to_sc_file [ args passed to sclang... ]\n"
+             << visibleDescription << endl;
+        return true;
+    }
+
+    if (!mInputFile.empty()) {
+        terminalOptions.mDaemon = true;
+    }
+
+    SC_LanguageConfig::readLibraryConfig(terminalOptions.mStandalone);
+
+    // for the language config we don't use the notifier of program options because
+    // we want to fill the config with a provided yaml file or their default values.
+    // as po does not allow to specify the "notify order", we parse the config manually
+    parseLanguageConfig(vm);
+
+    return false;
+}
+
+po::options_description CLIOptions::buildGenericDescription() {
+    po::options_description options("Generic options");
+    // clang-format off
+    options.add_options()
+        ("help,h", po::bool_switch(&mPrintHelp), "Print this message and exit")
+        ("version,v", po::bool_switch(&mPrintVersion), "Print sclang version and exit")
+    ;
+    // clang-format on
+    return options;
+}
+
+po::options_description CLIOptions::buildTerminalDescription(SC_TerminalClient::Options& terminalOptions) {
+    po::options_description description("sclang options");
+
+    // clang-format off
+    description.add_options()
+        (
+            "runtime-directory,d",
+            po::value<string>(&terminalOptions.mRuntimeDir),
+            "Set runtime directory"
+        )
+        (
+            "daemon,D",
+            po::bool_switch(&terminalOptions.mDaemon)->default_value(terminalOptions.mDaemon),
+            "Enter daemon mode"
+        )
+        (
+            "heap-growth,g",
+            po::value<string>()->default_value(convertMemoryInteger(terminalOptions.mMemGrow))->notifier([&](const string &heapGrowth) {
+                terminalOptions.mMemGrow = parseMemoryString(heapGrowth);
+            }),
+            "Set heap growth size (allows k, m and g as suffix multipliers)"
+        )
+        (
+            "yaml-config,l",
+            po::value<string>(&terminalOptions.mLibraryConfigFile)->notifier([&](const string &yaml_config) {
+                terminalOptions.mLibraryConfigFile = yaml_config;
+                SC_LanguageConfig::setConfigPath(yaml_config);
+            }),
+            "Set yaml config"
+        )
+        (
+            "heap-size,m",
+            po::value<string>()->default_value(convertMemoryInteger(terminalOptions.mMemSpace))->notifier([&](const string &heapSize) {
+                terminalOptions.mMemSpace = parseMemoryString(heapSize);
+            }),
+            "Set initial heap size (allows k, m and g as suffix multipliers)"
+        )
+        (
+            "call-run,r",
+            po::bool_switch(&terminalOptions.mCallRun)->default_value(terminalOptions.mCallRun),
+            "Call Main.run on startup"
+        )
+        (
+            "call-stop,s",
+            po::bool_switch(&terminalOptions.mCallStop)->default_value(terminalOptions.mCallStop),
+            "Call Main.stop on shutdown"
+        )
+        (
+            "port,u",
+            po::value<int>(&terminalOptions.mPort)->default_value(terminalOptions.mPort)->notifier([&](int port) {
+                if (port < 0 || port > 65535) {
+                    cerr << "UDP port must be in range between 0-65535, but was " << port << endl;
+                    mParsingFailed = true;
+                }
+            }),
+            "Set UDP listening port"
+        )
+        (
+            "ide,i",
+            po::value<string>()->notifier([&](string ide_name) {
+                SC_Filesystem::instance().setIdeName(ide_name);
+            }),
+            "Set IDE name"
+        )
+        (
+            "standalone,a",
+            po::bool_switch(&terminalOptions.mStandalone)->default_value(terminalOptions.mStandalone),
+            "Standalone mode (exclude SCClassLibrary and user and system Extensions folders from search path)"
+        )
+    ;
+    // clang-format on
+
+    return description;
+}
+
+po::options_description CLIOptions::buildLanguageConfigDescription() {
+    po::options_description description("Language config options (overwrites provided yaml entries)");
+
+    // clang-format off
+    description.add_options()
+        (
+            "include-path",
+            po::value<vector<string>>(),
+            "Class library path to be included for searching (allows multiple mentions)"
+        )
+        (
+            "exclude-path",
+            po::value<vector<string>>(),
+            "Class library path to be to excluded from searching (overrides includePaths, allows multiple mentions)"
path: lang/LangSource/SC_CLIOptions.cpp
user: login: Spacechild1
body: -> "Class library path to be excluded ..."
created_at: 2025-03-26T19:20:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,31 @@
+#include "boost/program_options.hpp"
+#include <string>
+#include "SC_TerminalClient.h"
+
+namespace po = boost::program_options;
+using namespace std;
+
+namespace SC_CLI {
+class CLIOptions {
+public:
+    CLIOptions() {};
+    bool parse(int argc, char* argv[], SC_TerminalClient::Options& terminalOptions, int& exit_code);
+
+    string mInputFile;
+    vector<string> mScArgs;
+
+private:
+    bool mParsingFailed = false;
+    bool mPrintHelp = false;
+    bool mPrintVersion = false;
+
+    po::options_description buildGenericDescription();
+    po::options_description buildTerminalDescription(SC_TerminalClient::Options& terminalOptions);
+    static po::options_description buildLanguageConfigDescription();
+
+    static void parseLanguageConfig(po::variables_map& vm);
+
+    static int parseMemoryString(std::string input);
path: lang/LangSource/SC_CLIOptions.hpp
user: login: Spacechild1
body: Should be `const std::string&`?
created_at: 2025-03-26T19:21:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -99,9 +99,8 @@ void SC_LanguageClient::initRuntime(const Options& opt) {
     // start virtual machine
     if (!mHiddenClient->mRunning) {
         mHiddenClient->mRunning = true;
-        if (opt.mRuntimeDir) {
-            int err = chdir(opt.mRuntimeDir);
-            if (err)
+        if (!opt.mRuntimeDir.empty()) {
+            if (int err = chdir(opt.mRuntimeDir.c_str()))
path: lang/LangSource/SC_LanguageClient.cpp
user: login: Spacechild1
body: Unused variable `err`. Better:
```
if (chdir(opt.mRuntimeDir.c_str()) != 0)
```
created_at: 2025-03-26T19:22:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,284 @@
+#include "boost/program_options.hpp"
+#include <iostream>
+#include <string>
+#include "SC_TerminalClient.h"
+#include "SC_CLIOptions.hpp"
+
+#include <SC_Filesystem.hpp>
+#include <SC_LanguageConfig.hpp>
+#include <SC_Version.hpp>
+
+namespace po = boost::program_options;
+using namespace std;
+
+namespace SC_CLI {
+
+/// parses the CLI - if the program should terminate after parsing it will return true
+/// an exit code is passed by reference
+bool CLIOptions::parse(int argc, char* argv[], SC_TerminalClient::Options& terminalOptions, int& exit_code) {
+    exit_code = 0;
+    po::options_description description("Interpreter for sclang (SuperCollider language)");
+
+    auto genericDescription = buildGenericDescription();
+    auto terminalDescription = buildTerminalDescription(terminalOptions);
+    auto languageConfigDescription = buildLanguageConfigDescription();
+
+    // Hidden options, will be allowed both on command line and
+    // in config file, but will not be shown to the user.
+    po::options_description hiddenDescription("Hidden options");
+    // clang-format off
+    hiddenDescription.add_options()
+        ("input-file", po::value<string>(&mInputFile), "input file")
+        ("sc-args", po::value<vector<string>>(&mScArgs), "sc args")
+    ;
+    // clang-format on
+
+    // positional options are declared here but captured via hidden description
+    po::positional_options_description positionalOptions;
+    positionalOptions.add("input-file", 1);
+    positionalOptions.add("sc-args", -1);
+
+    po::options_description cmdLineOptions;
+    cmdLineOptions.add(genericDescription)
+        .add(terminalDescription)
+        .add(hiddenDescription)
+        .add(languageConfigDescription);
+
+    po::variables_map vm;
+    try {
+        store(po::command_line_parser(argc, argv).options(cmdLineOptions).positional(positionalOptions).run(), vm);
+        notify(vm);
+    } catch (po::unknown_option& e) {
+        cerr << e.what() << endl;
+        exit_code = 1;
+        return true;
+    } catch (po::invalid_option_value& e) {
+        cerr << e.what() << endl;
+        exit_code = 1;
+        return true;
+    }
+    if (mParsingFailed) {
+        exit_code = 1;
+        return true;
+    }
+
+    if (vm.count("sc-args")) {
+        terminalOptions.mArgs = vm["sc-args"].as<vector<string>>();
+    }
+
+    if (mPrintVersion) {
+        cout << "sclang " << SC_VersionString() << " (" << SC_BuildString() << ")" << endl;
+        return true;
+    }
+
+    // this is handled here and not in the description declaration b/c we need access to the declarations
+    if (mPrintHelp) {
+        // construct a options_description w/o hidden positional options
+        po::options_description visibleDescription("");
+        visibleDescription.add(genericDescription).add(terminalDescription).add(languageConfigDescription);
+
+        cout << "Start a REPL interpreter:           sclang [ -options ]\n"
+                "Execute a .scd file:                sclang [ -options ] path_to_sc_file [ args passed to sclang... ]\n"
+             << visibleDescription << endl;
+        return true;
+    }
+
+    if (!mInputFile.empty()) {
+        terminalOptions.mDaemon = true;
+    }
+
+    SC_LanguageConfig::readLibraryConfig(terminalOptions.mStandalone);
+
+    // for the language config we don't use the notifier of program options because
+    // we want to fill the config with a provided yaml file or their default values.
+    // as po does not allow to specify the "notify order", we parse the config manually
+    parseLanguageConfig(vm);
+
+    return false;
+}
+
+po::options_description CLIOptions::buildGenericDescription() {
+    po::options_description options("Generic options");
+    // clang-format off
+    options.add_options()
+        ("help,h", po::bool_switch(&mPrintHelp), "Print this message and exit")
+        ("version,v", po::bool_switch(&mPrintVersion), "Print sclang version and exit")
+    ;
+    // clang-format on
+    return options;
+}
+
+po::options_description CLIOptions::buildTerminalDescription(SC_TerminalClient::Options& terminalOptions) {
+    po::options_description description("sclang options");
+
+    // clang-format off
+    description.add_options()
+        (
+            "runtime-directory,d",
+            po::value<string>(&terminalOptions.mRuntimeDir),
+            "Set runtime directory"
+        )
+        (
+            "daemon,D",
+            po::bool_switch(&terminalOptions.mDaemon)->default_value(terminalOptions.mDaemon),
+            "Enter daemon mode"
+        )
+        (
+            "heap-growth,g",
+            po::value<string>()->default_value(convertMemoryInteger(terminalOptions.mMemGrow))->notifier([&](const string &heapGrowth) {
+                terminalOptions.mMemGrow = parseMemoryString(heapGrowth);
+            }),
+            "Set heap growth size (allows k, m and g as suffix multipliers)"
+        )
+        (
+            "yaml-config,l",
+            po::value<string>(&terminalOptions.mLibraryConfigFile)->notifier([&](const string &yaml_config) {
+                terminalOptions.mLibraryConfigFile = yaml_config;
+                SC_LanguageConfig::setConfigPath(yaml_config);
+            }),
+            "Set yaml config"
+        )
+        (
+            "heap-size,m",
+            po::value<string>()->default_value(convertMemoryInteger(terminalOptions.mMemSpace))->notifier([&](const string &heapSize) {
+                terminalOptions.mMemSpace = parseMemoryString(heapSize);
+            }),
+            "Set initial heap size (allows k, m and g as suffix multipliers)"
+        )
+        (
+            "call-run,r",
+            po::bool_switch(&terminalOptions.mCallRun)->default_value(terminalOptions.mCallRun),
+            "Call Main.run on startup"
+        )
+        (
+            "call-stop,s",
+            po::bool_switch(&terminalOptions.mCallStop)->default_value(terminalOptions.mCallStop),
+            "Call Main.stop on shutdown"
+        )
+        (
+            "port,u",
+            po::value<int>(&terminalOptions.mPort)->default_value(terminalOptions.mPort)->notifier([&](int port) {
+                if (port < 0 || port > 65535) {
+                    cerr << "UDP port must be in range between 0-65535, but was " << port << endl;
+                    mParsingFailed = true;
+                }
+            }),
+            "Set UDP listening port"
+        )
+        (
+            "ide,i",
+            po::value<string>()->notifier([&](string ide_name) {
+                SC_Filesystem::instance().setIdeName(ide_name);
+            }),
+            "Set IDE name"
+        )
+        (
+            "standalone,a",
+            po::bool_switch(&terminalOptions.mStandalone)->default_value(terminalOptions.mStandalone),
+            "Standalone mode (exclude SCClassLibrary and user and system Extensions folders from search path)"
+        )
+    ;
+    // clang-format on
+
+    return description;
+}
+
+po::options_description CLIOptions::buildLanguageConfigDescription() {
+    po::options_description description("Language config options (overwrites provided yaml entries)");
+
+    // clang-format off
+    description.add_options()
+        (
+            "include-path",
+            po::value<vector<string>>(),
+            "Class library path to be included for searching (allows multiple mentions)"
+        )
+        (
+            "exclude-path",
+            po::value<vector<string>>(),
+            "Class library path to be to excluded from searching (overrides includePaths, allows multiple mentions)"
+        )
+        (
+            "post-inline-warnings",
+            po::bool_switch()->default_value(false),
+            "Boolean flag to turn on warnings about missing inline opportunities."
+        )
+        // exclude-default-paths is not included as this behavior is set via the `--standalone` flag
+    ;
+    // clang-format on
+    return description;
+}
+
+void CLIOptions::parseLanguageConfig(po::variables_map& vm) {
+    // see HelpSource/Classes/LanguageConfig.schelp
+    if (vm.at("post-inline-warnings").as<bool>()) {
+        gLanguageConfig->setPostInlineWarnings(true);
+    };
+
+    if (vm.count("include-path")) {
+        for (auto includePath : vm["include-path"].as<vector<string>>()) {
+            gLanguageConfig->addIncludedDirectory(SC_Codecvt::utf8_str_to_path(includePath));
+        }
+    }
+    if (vm.count("exclude-path")) {
+        for (auto excludePath : vm["exclude-path"].as<vector<string>>()) {
+            gLanguageConfig->addExcludedDirectory(SC_Codecvt::utf8_str_to_path(excludePath));
+        }
+    }
+}
+
+// converts e.g. 1k to 1024
+int CLIOptions::parseMemoryString(std::string input) {
+    if (input.empty()) {
+        return 0;
+    }
+
+    char suffix = input.back();
+    int multiplier = 1;
+
+    if (suffix == 'k' || suffix == 'K') {
+        multiplier = 1024;
+    } else if (suffix == 'm' || suffix == 'M') {
+        multiplier = 1024 * 1024;
+    } else if (suffix == 'g' || suffix == 'G') {
+        multiplier = 1024 * 1024 * 1024;
+    } else if (!isdigit(suffix)) {
+        cerr << "Unknown modifier '" << suffix << "'" << endl;
+        return 0;
+    }
+
+    std::string numberPart = std::isdigit(suffix) ? input : input.substr(0, input.size() - 1);
+    int value = boost::lexical_cast<int>(numberPart);
path: lang/LangSource/SC_CLIOptions.cpp
user: login: Spacechild1
body: Note: `boost::lexical_cast` throws an `boost::bad_lexical_cast` exception if the conversion is not successfully. I guess we should catch it here, print an error and return 0?
created_at: 2025-03-26T19:27:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -101,168 +104,29 @@ void SC_TerminalClient::postError(const char* str, size_t len) {
 
 void SC_TerminalClient::flush() { fflush(gPostDest); }
 
-void SC_TerminalClient::printUsage() {
-    Options opt;
-
-    const size_t bufSize = 128;
-    char memGrowBuf[bufSize];
-    char memSpaceBuf[bufSize];
-
-    snprintMemArg(memGrowBuf, bufSize, opt.mMemGrow);
-    snprintMemArg(memSpaceBuf, bufSize, opt.mMemSpace);
-
-    fprintf(stdout, "Usage:\n   %s [options] [file..] [-]\n\n", getName());
-    fprintf(
-        stdout,
-        "Options:\n"
-        "   -v                             Print supercollider version and exit\n"
-        "   -d <path>                      Set runtime directory\n"
-        "   -D                             Enter daemon mode (no input)\n"
-        "   -g <memory-growth>[km]         Set heap growth (default %s)\n"
-        "   -h                             Display this message and exit\n"
-        "   -l <path>                      Set library configuration file\n"
-        "   -m <memory-space>[km]          Set initial heap size (default %s)\n"
-        "   -r                             Call Main.run on startup\n"
-        "   -s                             Call Main.stop on shutdown\n"
-        "   -u <network-port-number>       Set UDP listening port (default %d)\n"
-        "   -i <ide-name>                  Specify IDE name (for enabling IDE-specific class code, default \"%s\")\n"
-        "   -a                             Standalone mode (exclude SCClassLibrary and user and system Extensions "
-        "folders from search path)\n",
-        memGrowBuf, memSpaceBuf, opt.mPort, SC_Filesystem::instance().getIdeName().c_str());
-}
-
-bool SC_TerminalClient::parseOptions(int& argc, char**& argv, Options& opt) {
-    const char* optstr = ":d:Dg:hl:m:rsu:i:av";
-    int c;
-
-    // inhibit error reporting
-    opterr = 0;
-
-    while ((c = getopt(argc, argv, optstr)) != -1) {
-        switch (c) {
-        case 'd':
-            opt.mRuntimeDir = optarg;
-            break;
-        case 'D':
-            opt.mDaemon = true;
-            break;
-        case 'g':
-            if (!parseMemArg(optarg, &opt.mMemGrow)) {
-                optopt = c;
-                goto optArgInvalid;
-            }
-            break;
-        case 'h':
-            goto help;
-        case 'l':
-            opt.mLibraryConfigFile = optarg;
-            break;
-        case 'm':
-            if (!parseMemArg(optarg, &opt.mMemSpace)) {
-                optopt = c;
-                goto optArgInvalid;
-            }
-            break;
-        case 'r':
-            opt.mCallRun = true;
-            break;
-        case 'v':
-            fprintf(stdout, "sclang %s (%s)\n", SC_VersionString().c_str(), SC_BuildString().c_str());
-            quit(0);
-            return false;
-            break;
-        case 's':
-            opt.mCallStop = true;
-            break;
-        case 'u':
-            if (!parsePortArg(optarg, &opt.mPort)) {
-                optopt = c;
-                goto optArgInvalid;
-            }
-            break;
-        case '?':
-            goto optInvalid;
-            break;
-        case ':':
-            goto optArgExpected;
-            break;
-        case 'i':
-            SC_Filesystem::instance().setIdeName(optarg);
-            break;
-        case 'a':
-            opt.mStandalone = true;
-            break;
-        default:
-            ::post("%s: unknown error (getopt)\n", getName());
-            quit(255);
-            return false;
-        }
-    }
-
-    argv += optind;
-    argc -= optind;
-
-    return true;
-
-help:
-    printUsage();
-    quit(0);
-    return false;
-
-optInvalid:
-    ::post("%s: invalid option -%c\n", getName(), optopt);
-    quit(1);
-    return false;
-
-optArgExpected:
-    ::post("%s: missing argument for option -%c\n", getName(), optopt);
-    quit(1);
-    return false;
-
-optArgInvalid:
-    ::post("%s: invalid argument for option -%c -- %s\n", getName(), optopt, optarg);
-    quit(1);
-    return false;
-}
-
 int SC_TerminalClient::run(int argc, char** argv) {
     Options& opt = mOptions;
+    int error_code;
path: lang/LangSource/SC_TerminalClient.cpp
user: login: Spacechild1
body: camelCase 🤓 
created_at: 2025-03-26T19:28:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -101,168 +104,29 @@ void SC_TerminalClient::postError(const char* str, size_t len) {
 
 void SC_TerminalClient::flush() { fflush(gPostDest); }
 
-void SC_TerminalClient::printUsage() {
-    Options opt;
-
-    const size_t bufSize = 128;
-    char memGrowBuf[bufSize];
-    char memSpaceBuf[bufSize];
-
-    snprintMemArg(memGrowBuf, bufSize, opt.mMemGrow);
-    snprintMemArg(memSpaceBuf, bufSize, opt.mMemSpace);
-
-    fprintf(stdout, "Usage:\n   %s [options] [file..] [-]\n\n", getName());
-    fprintf(
-        stdout,
-        "Options:\n"
-        "   -v                             Print supercollider version and exit\n"
-        "   -d <path>                      Set runtime directory\n"
-        "   -D                             Enter daemon mode (no input)\n"
-        "   -g <memory-growth>[km]         Set heap growth (default %s)\n"
-        "   -h                             Display this message and exit\n"
-        "   -l <path>                      Set library configuration file\n"
-        "   -m <memory-space>[km]          Set initial heap size (default %s)\n"
-        "   -r                             Call Main.run on startup\n"
-        "   -s                             Call Main.stop on shutdown\n"
-        "   -u <network-port-number>       Set UDP listening port (default %d)\n"
-        "   -i <ide-name>                  Specify IDE name (for enabling IDE-specific class code, default \"%s\")\n"
-        "   -a                             Standalone mode (exclude SCClassLibrary and user and system Extensions "
-        "folders from search path)\n",
-        memGrowBuf, memSpaceBuf, opt.mPort, SC_Filesystem::instance().getIdeName().c_str());
-}
-
-bool SC_TerminalClient::parseOptions(int& argc, char**& argv, Options& opt) {
-    const char* optstr = ":d:Dg:hl:m:rsu:i:av";
-    int c;
-
-    // inhibit error reporting
-    opterr = 0;
-
-    while ((c = getopt(argc, argv, optstr)) != -1) {
-        switch (c) {
-        case 'd':
-            opt.mRuntimeDir = optarg;
-            break;
-        case 'D':
-            opt.mDaemon = true;
-            break;
-        case 'g':
-            if (!parseMemArg(optarg, &opt.mMemGrow)) {
-                optopt = c;
-                goto optArgInvalid;
-            }
-            break;
-        case 'h':
-            goto help;
-        case 'l':
-            opt.mLibraryConfigFile = optarg;
-            break;
-        case 'm':
-            if (!parseMemArg(optarg, &opt.mMemSpace)) {
-                optopt = c;
-                goto optArgInvalid;
-            }
-            break;
-        case 'r':
-            opt.mCallRun = true;
-            break;
-        case 'v':
-            fprintf(stdout, "sclang %s (%s)\n", SC_VersionString().c_str(), SC_BuildString().c_str());
-            quit(0);
-            return false;
-            break;
-        case 's':
-            opt.mCallStop = true;
-            break;
-        case 'u':
-            if (!parsePortArg(optarg, &opt.mPort)) {
-                optopt = c;
-                goto optArgInvalid;
-            }
-            break;
-        case '?':
-            goto optInvalid;
-            break;
-        case ':':
-            goto optArgExpected;
-            break;
-        case 'i':
-            SC_Filesystem::instance().setIdeName(optarg);
-            break;
-        case 'a':
-            opt.mStandalone = true;
-            break;
-        default:
-            ::post("%s: unknown error (getopt)\n", getName());
-            quit(255);
-            return false;
-        }
-    }
-
-    argv += optind;
-    argc -= optind;
-
-    return true;
-
-help:
-    printUsage();
-    quit(0);
-    return false;
-
-optInvalid:
-    ::post("%s: invalid option -%c\n", getName(), optopt);
-    quit(1);
-    return false;
-
-optArgExpected:
-    ::post("%s: missing argument for option -%c\n", getName(), optopt);
-    quit(1);
-    return false;
-
-optArgInvalid:
-    ::post("%s: invalid argument for option -%c -- %s\n", getName(), optopt, optarg);
-    quit(1);
-    return false;
-}
-
 int SC_TerminalClient::run(int argc, char** argv) {
     Options& opt = mOptions;
+    int error_code;
 
-    if (!parseOptions(argc, argv, opt)) {
-        return mReturnCode;
-    }
-
-    // finish argv processing
-    const char* codeFile = nullptr;
+    SC_CLI::CLIOptions cli_options;
path: lang/LangSource/SC_TerminalClient.cpp
user: login: Spacechild1
body: camelCase 🤓 
created_at: 2025-03-26T19:28:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,24 +56,22 @@ class SCLANG_DLLEXPORT SC_TerminalClient : public SC_LanguageClient {
 
     struct Options : public SC_LanguageClient::Options {
         Options():
-            mLibraryConfigFile(0),
+            mLibraryConfigFile(""),
path: lang/LangSource/SC_TerminalClient.h
user: login: Spacechild1
body: Redundant because `std::string` has a default constructor
created_at: 2025-03-26T19:29:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,24 +56,22 @@ class SCLANG_DLLEXPORT SC_TerminalClient : public SC_LanguageClient {
 
     struct Options : public SC_LanguageClient::Options {
         Options():
-            mLibraryConfigFile(0),
+            mLibraryConfigFile(""),
             mDaemon(false),
             mCallRun(false),
             mCallStop(false),
             mStandalone(false),
-            mArgc(0),
-            mArgv(0) {}
+            mArgs({}) {}
path: lang/LangSource/SC_TerminalClient.h
user: login: Spacechild1
body: Redundant because `std::vector` has a default constructor
created_at: 2025-03-26T19:29:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,31 @@
+#include "boost/program_options.hpp"
+#include <string>
+#include "SC_TerminalClient.h"
+
+namespace po = boost::program_options;
+using namespace std;
+
+namespace SC_CLI {
+class CLIOptions {
+public:
+    CLIOptions() {};
path: lang/LangSource/SC_CLIOptions.hpp
user: login: Spacechild1
body: Please no semi-colon after inline function definitions :) Also, this constructor does nothing, so you can just omit it altogether.
created_at: 2025-03-28T09:27:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,25 +55,17 @@ class SCLANG_DLLEXPORT SC_TerminalClient : public SC_LanguageClient {
     };
 
     struct Options : public SC_LanguageClient::Options {
-        Options():
-            mLibraryConfigFile(0),
-            mDaemon(false),
-            mCallRun(false),
-            mCallStop(false),
-            mStandalone(false),
-            mArgc(0),
-            mArgv(0) {}
-
-        const char* mLibraryConfigFile;
+        Options(): mDaemon(false), mCallRun(false), mCallStop(false), mStandalone(false) {}
path: lang/LangSource/SC_TerminalClient.h
user: login: Spacechild1
body: Just a comment: since C++11 you can initialize member variables inline. In this case, `Options` wouldn't even need a user-defined constructor. In fact, that's what you are already doing in `CLIOptions`. Generally, in modern C++ we only tend to write constructors when they are really necessary.
created_at: 2025-03-28T09:34:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,17 +41,17 @@ SCLANG_DLLEXPORT void destroyLanguageClient(class SC_LanguageClient*);
 class SCLANG_DLLEXPORT SC_LanguageClient {
 public:
     struct Options {
-        Options(): mMemSpace(2 * 1024 * 1024), mMemGrow(256 * 1024), mPort(57120), mRuntimeDir(0) {}
+        Options(): mMemSpace(2 * 1024 * 1024), mMemGrow(256 * 1024), mPort(57120) {}
path: include/lang/SC_LanguageClient.h
user: login: Spacechild1
body: Just as a side note, this constructor can be omitted as well:
```
struct Options {
    int mMemSpace = 2 * 1024 * 1024; // memory space in bytes
    int mMemGrow = 256 * 1024; // memory growth in bytes     
    int mPort = 57120; // network port number
    std::string mRuntimeDir; // runtime directory
};
```
A little bit more verbose, but maybe clearer:
```
struct Options {
    static constexpr int defaultMemSpace = 2 * 1024 * 1024;
    static constexpr int defaultMemGrow = 256 * 1024;
    static constexpr int defaultPort = 57120;

    int mMemSpace = defaultMemSpace; // memory space in bytes
    int mMemGrow = defaultMemGrow; // memory growth in bytes     
    int mPort = defaultPort; // network port number
    std::string mRuntimeDir; // runtime directory
};
```


created_at: 2025-03-28T09:44:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,288 @@
+#include "boost/program_options.hpp"
+#include <iostream>
+#include <string>
+#include "SC_TerminalClient.h"
+#include "SC_CLIOptions.hpp"
+
+#include <SC_Filesystem.hpp>
+#include <SC_LanguageConfig.hpp>
+#include <SC_Version.hpp>
+
+namespace po = boost::program_options;
+using namespace std;
+
+namespace SC_CLI {
+
+/// parses the CLI - if the program should terminate after parsing it will return true
+/// an exit code is passed by reference
+bool CLIOptions::parse(int argc, char* argv[], SC_TerminalClient::Options& terminalOptions, int& exitCode) {
+    exitCode = 0;
+    po::options_description description("Interpreter for sclang (SuperCollider language)");
+
+    auto genericDescription = buildGenericDescription();
+    auto terminalDescription = buildTerminalDescription(terminalOptions);
+    auto languageConfigDescription = buildLanguageConfigDescription();
+
+    // Hidden options, will be allowed both on command line and
+    // in config file, but will not be shown to the user.
+    po::options_description hiddenDescription("Hidden options");
+    // clang-format off
+    hiddenDescription.add_options()
+        ("input-file", po::value<string>(&mInputFile), "input file")
+        ("sc-args", po::value<vector<string>>(&mScArgs), "sc args")
+    ;
+    // clang-format on
+
+    // positional options are declared here but captured via hidden description
+    po::positional_options_description positionalOptions;
+    positionalOptions.add("input-file", 1);
+    positionalOptions.add("sc-args", -1);
+
+    po::options_description cmdLineOptions;
+    cmdLineOptions.add(genericDescription)
+        .add(terminalDescription)
+        .add(hiddenDescription)
+        .add(languageConfigDescription);
+
+    po::variables_map vm;
+    try {
+        store(po::command_line_parser(argc, argv).options(cmdLineOptions).positional(positionalOptions).run(), vm);
+        notify(vm);
+    } catch (po::unknown_option& e) {
+        cerr << e.what() << endl;
+        exitCode = 1;
+        return true;
+    } catch (po::invalid_option_value& e) {
+        cerr << e.what() << endl;
+        exitCode = 1;
+        return true;
+    }
+    if (mParsingFailed) {
+        exitCode = 1;
+        return true;
+    }
+
+    if (vm.count("sc-args")) {
+        terminalOptions.mArgs = vm["sc-args"].as<vector<string>>();
+    }
+
+    if (mPrintVersion) {
+        cout << "sclang " << SC_VersionString() << " (" << SC_BuildString() << ")" << endl;
+        return true;
+    }
+
+    // this is handled here and not in the description declaration b/c we need access to the declarations
+    if (mPrintHelp) {
+        // construct a options_description w/o hidden positional options
+        po::options_description visibleDescription("");
+        visibleDescription.add(genericDescription).add(terminalDescription).add(languageConfigDescription);
+
+        cout << "Start a REPL interpreter:           sclang [ -options ]\n"
+                "Execute a .scd file:                sclang [ -options ] path_to_sc_file [ args passed to sclang... ]\n"
+             << visibleDescription << endl;
+        return true;
+    }
+
+    if (!mInputFile.empty()) {
+        terminalOptions.mDaemon = true;
+    }
+
+    SC_LanguageConfig::readLibraryConfig(terminalOptions.mStandalone);
+
+    // for the language config we don't use the notifier of program options because
+    // we want to fill the config with a provided yaml file or their default values.
+    // as po does not allow to specify the "notify order", we parse the config manually
+    parseLanguageConfig(vm);
+
+    return false;
+}
+
+po::options_description CLIOptions::buildGenericDescription() {
+    po::options_description options("Generic options");
+    // clang-format off
+    options.add_options()
+        ("help,h", po::bool_switch(&mPrintHelp), "Print this message and exit")
+        ("version,v", po::bool_switch(&mPrintVersion), "Print sclang version and exit")
+    ;
+    // clang-format on
+    return options;
+}
+
+po::options_description CLIOptions::buildTerminalDescription(SC_TerminalClient::Options& terminalOptions) {
+    po::options_description description("sclang options");
+
+    // clang-format off
+    description.add_options()
+        (
+            "runtime-directory,d",
+            po::value<string>(&terminalOptions.mRuntimeDir),
+            "Set runtime directory"
+        )
+        (
+            "daemon,D",
+            po::bool_switch(&terminalOptions.mDaemon)->default_value(terminalOptions.mDaemon),
+            "Enter daemon mode"
+        )
+        (
+            "heap-growth,g",
+            po::value<string>()->default_value(convertMemoryInteger(terminalOptions.mMemGrow))->notifier([&](const string &heapGrowth) {
+                terminalOptions.mMemGrow = parseMemoryString(heapGrowth);
+            }),
+            "Set heap growth size (allows k, m and g as suffix multipliers)"
+        )
+        (
+            "yaml-config,l",
+            po::value<string>(&terminalOptions.mLibraryConfigFile)->notifier([&](const string &yaml_config) {
+                terminalOptions.mLibraryConfigFile = yaml_config;
+                SC_LanguageConfig::setConfigPath(yaml_config);
+            }),
+            "Set yaml config"
+        )
+        (
+            "heap-size,m",
+            po::value<string>()->default_value(convertMemoryInteger(terminalOptions.mMemSpace))->notifier([&](const string &heapSize) {
+                terminalOptions.mMemSpace = parseMemoryString(heapSize);
+            }),
+            "Set initial heap size (allows k, m and g as suffix multipliers)"
+        )
+        (
+            "call-run,r",
+            po::bool_switch(&terminalOptions.mCallRun)->default_value(terminalOptions.mCallRun),
+            "Call Main.run on startup"
+        )
+        (
+            "call-stop,s",
+            po::bool_switch(&terminalOptions.mCallStop)->default_value(terminalOptions.mCallStop),
+            "Call Main.stop on shutdown"
+        )
+        (
+            "port,u",
+            po::value<int>(&terminalOptions.mPort)->default_value(terminalOptions.mPort)->notifier([&](int port) {
+                if (port < 0 || port > 65535) {
+                    cerr << "UDP port must be in range between 0-65535, but was " << port << endl;
+                    mParsingFailed = true;
+                }
+            }),
+            "Set UDP listening port"
+        )
+        (
+            "ide,i",
+            po::value<string>()->notifier([&](string ide_name) {
+                SC_Filesystem::instance().setIdeName(ide_name);
+            }),
+            "Set IDE name"
+        )
+        (
+            "standalone,a",
+            po::bool_switch(&terminalOptions.mStandalone)->default_value(terminalOptions.mStandalone),
+            "Standalone mode (exclude SCClassLibrary and user and system Extensions folders from search path)"
+        )
+    ;
+    // clang-format on
+
+    return description;
+}
+
+po::options_description CLIOptions::buildLanguageConfigDescription() {
+    po::options_description description("Language config options (overwrites provided yaml entries)");
+
+    // clang-format off
+    description.add_options()
+        (
+            "include-path",
+            po::value<vector<string>>(),
+            "Class library path to be included for searching (allows multiple mentions)"
+        )
+        (
+            "exclude-path",
+            po::value<vector<string>>(),
+            "Class library path to be excluded from searching (overrides includePaths, allows multiple mentions)"
+        )
+        (
+            "post-inline-warnings",
+            po::bool_switch()->default_value(false),
+            "Boolean flag to turn on warnings about missing inline opportunities."
+        )
+        // exclude-default-paths is not included as this behavior is set via the `--standalone` flag
+    ;
+    // clang-format on
+    return description;
+}
+
+void CLIOptions::parseLanguageConfig(po::variables_map& vm) {
+    // see HelpSource/Classes/LanguageConfig.schelp
+    if (vm.at("post-inline-warnings").as<bool>()) {
+        gLanguageConfig->setPostInlineWarnings(true);
+    };
+
+    if (vm.count("include-path")) {
+        for (auto includePath : vm["include-path"].as<vector<string>>()) {
+            gLanguageConfig->addIncludedDirectory(SC_Codecvt::utf8_str_to_path(includePath));
+        }
+    }
+    if (vm.count("exclude-path")) {
+        for (auto excludePath : vm["exclude-path"].as<vector<string>>()) {
+            gLanguageConfig->addExcludedDirectory(SC_Codecvt::utf8_str_to_path(excludePath));
+        }
+    }
+}
+
+// converts e.g. 1k to 1024
+int CLIOptions::parseMemoryString(const std::string& input) {
+    if (input.empty()) {
+        return 0;
+    }
+
+    char suffix = input.back();
+    int multiplier = 1;
+
+    if (suffix == 'k' || suffix == 'K') {
+        multiplier = 1024;
+    } else if (suffix == 'm' || suffix == 'M') {
+        multiplier = 1024 * 1024;
+    } else if (suffix == 'g' || suffix == 'G') {
+        multiplier = 1024 * 1024 * 1024;
+    } else if (!isdigit(suffix)) {
+        cerr << "Couldn't parse memory value '" << input << "': '" << suffix << "' is an unknown modifier" << endl;
+        exit(1);
+    }
+
+    std::string numberPart = std::isdigit(suffix) ? input : input.substr(0, input.size() - 1);
+    try {
+        int value = boost::lexical_cast<int>(numberPart);
+        return value * multiplier;
+    } catch (boost::bad_lexical_cast& e) {
path: lang/LangSource/SC_CLIOptions.cpp
user: login: Spacechild1
body: Nitpick: should be `const boost::bad_lexical_cast&`. You can also omit the name of the variable if you don't actually use it.
created_at: 2025-03-28T09:47:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,288 @@
+#include "boost/program_options.hpp"
+#include <iostream>
+#include <string>
+#include "SC_TerminalClient.h"
+#include "SC_CLIOptions.hpp"
+
+#include <SC_Filesystem.hpp>
+#include <SC_LanguageConfig.hpp>
+#include <SC_Version.hpp>
+
+namespace po = boost::program_options;
+using namespace std;
+
+namespace SC_CLI {
+
+/// parses the CLI - if the program should terminate after parsing it will return true
+/// an exit code is passed by reference
+bool CLIOptions::parse(int argc, char* argv[], SC_TerminalClient::Options& terminalOptions, int& exitCode) {
+    exitCode = 0;
+    po::options_description description("Interpreter for sclang (SuperCollider language)");
+
+    auto genericDescription = buildGenericDescription();
+    auto terminalDescription = buildTerminalDescription(terminalOptions);
+    auto languageConfigDescription = buildLanguageConfigDescription();
+
+    // Hidden options, will be allowed both on command line and
+    // in config file, but will not be shown to the user.
+    po::options_description hiddenDescription("Hidden options");
+    // clang-format off
+    hiddenDescription.add_options()
+        ("input-file", po::value<string>(&mInputFile), "input file")
+        ("sc-args", po::value<vector<string>>(&mScArgs), "sc args")
+    ;
+    // clang-format on
+
+    // positional options are declared here but captured via hidden description
+    po::positional_options_description positionalOptions;
+    positionalOptions.add("input-file", 1);
+    positionalOptions.add("sc-args", -1);
+
+    po::options_description cmdLineOptions;
+    cmdLineOptions.add(genericDescription)
+        .add(terminalDescription)
+        .add(hiddenDescription)
+        .add(languageConfigDescription);
+
+    po::variables_map vm;
+    try {
+        store(po::command_line_parser(argc, argv).options(cmdLineOptions).positional(positionalOptions).run(), vm);
+        notify(vm);
+    } catch (po::unknown_option& e) {
+        cerr << e.what() << endl;
+        exitCode = 1;
+        return true;
+    } catch (po::invalid_option_value& e) {
+        cerr << e.what() << endl;
+        exitCode = 1;
+        return true;
+    }
+    if (mParsingFailed) {
+        exitCode = 1;
+        return true;
+    }
+
+    if (vm.count("sc-args")) {
+        terminalOptions.mArgs = vm["sc-args"].as<vector<string>>();
+    }
+
+    if (mPrintVersion) {
+        cout << "sclang " << SC_VersionString() << " (" << SC_BuildString() << ")" << endl;
+        return true;
+    }
+
+    // this is handled here and not in the description declaration b/c we need access to the declarations
+    if (mPrintHelp) {
+        // construct a options_description w/o hidden positional options
+        po::options_description visibleDescription("");
path: lang/LangSource/SC_CLIOptions.cpp
user: login: Spacechild1
body: Since you said you are ok with nitpicks, here's another one: no need to explicitly pass an empty string, just use the default constructor, as you do with the other instances of `po::options_description`.
created_at: 2025-03-29T21:14:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,30 @@
+#include "boost/program_options.hpp"
+#include <string>
+#include "SC_TerminalClient.h"
+
+namespace po = boost::program_options;
+using namespace std;
path: lang/LangSource/SC_CLIOptions.hpp
user: login: Spacechild1
body: Never put a `using namespace std;` in global scope in a header! I don't actually see why you need this. Just so you can type `string` and `vector` instead of `std::string` and `std::vector`? Funnily enough, you do write `std::string` twice at the end of the class declaration 😉 

Personally, I never use `using namespace std` and prefer standard library types to stick out.
created_at: 2025-03-29T21:21:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,288 @@
+#include "boost/program_options.hpp"
+#include <iostream>
+#include <string>
+#include "SC_TerminalClient.h"
+#include "SC_CLIOptions.hpp"
+
+#include <SC_Filesystem.hpp>
+#include <SC_LanguageConfig.hpp>
+#include <SC_Version.hpp>
+
+namespace po = boost::program_options;
+using namespace std;
path: lang/LangSource/SC_CLIOptions.cpp
user: login: Spacechild1
body: `using namespace std;` is less dangerous in a source file than in a header, but again, is it really necessay? Not to mention that you are actually using the `std::` prefix anyway in most places!

I would suggest to just get rid of the `using namespace std;`. It's something you often find in example code for the sake of brevity, but it's really an anti-pattern, similar to wildcard imports in Python (`from module_name import *`)


created_at: 2025-03-29T21:27:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -682,16 +546,16 @@ void SC_TerminalClient::cleanupInput() {
 }
 
 int SC_TerminalClient::prArgv(struct VMGlobals* g, int) {
-    int argc = ((SC_TerminalClient*)SC_TerminalClient::instance())->options().mArgc;
-    char** argv = ((SC_TerminalClient*)SC_TerminalClient::instance())->options().mArgv;
+    int argc = ((SC_TerminalClient*)SC_TerminalClient::instance())->options().mArgs.size();
+    std::vector<std::string> argv = ((SC_TerminalClient*)SC_TerminalClient::instance())->options().mArgs;
path: lang/LangSource/SC_TerminalClient.cpp
user: login: Spacechild1
body: This actually makes a copy of the argument vector, but we only need a reference. Does it matter? Not really, but I just thought I should point it out. Also, that's a good place for using auto. Then you might also realize that the first line is really redundant. Finally, avoid C-style casts for up- and downcasting. The modern C++ equivalent would be `static_cast`.

```
auto& argv = static_cast<SC_TerminalClient*>(SC_TerminalClient::instance())->options().mArgs;
size_t argc = argv.size();
```
created_at: 2025-03-29T21:36:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,29 @@
+#include "boost/program_options.hpp"
+#include <string>
+#include "SC_TerminalClient.h"
+
+namespace po = boost::program_options;
+
+namespace SC_CLI {
+class CLIOptions {
+public:
+    bool parse(int argc, char* argv[], SC_TerminalClient::Options& terminalOptions, int& exitCode);
path: lang/LangSource/SC_CLIOptions.hpp
user: login: JordanHendersonMusic
body: I think this signature is a bit confusing with the bool and the exit code. IIUC, this function returns three possible states? Perhaps just use an enum? Or maybe an enum and an optional?
created_at: 2025-04-01T20:26:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,29 @@
+#include "boost/program_options.hpp"
+#include <string>
+#include "SC_TerminalClient.h"
+
+namespace po = boost::program_options;
+
+namespace SC_CLI {
+class CLIOptions {
+public:
+    bool parse(int argc, char* argv[], SC_TerminalClient::Options& terminalOptions, int& exitCode);
path: lang/LangSource/SC_CLIOptions.hpp
user: login: Spacechild1
body: I think you're right. There are basically three possible states:

1. return `true` + exit code 0 (e.g. `--version`)
2. return `true` + exit code 1 (parse error)
3. return `false`: ok and continue

> Perhaps just use an enum?

Yes, I think that would be a good solution.

> Or maybe an enum and an optional?

A simpler solution would be to just return the error code as an `std::optional`:
```
// If the optional is empty, the program should continue; otherwise the optional will hold the exit code
std::optional<int> parse(int argc, char* argv[], SC_TerminalClient::Options& terminalOptions);
```

I guess it's a matter of taste.
created_at: 2025-04-01T20:53:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,29 @@
+#include "boost/program_options.hpp"
+#include <string>
+#include "SC_TerminalClient.h"
+
+namespace po = boost::program_options;
+
+namespace SC_CLI {
+class CLIOptions {
+public:
+    bool parse(int argc, char* argv[], SC_TerminalClient::Options& terminalOptions, int& exitCode);
path: lang/LangSource/SC_CLIOptions.hpp
user: login: capital-G
body: Went for the `std::optional` approach which seems like a good way to do things.

IIUC there is no way to make this actually type safe? So something like
```cpp
    if (const auto errorCode = cliOptions.parse(argc, argv, mOptions)) {
        return *errorCode;
    }
```
could introduce runtime problems b/c the type system doesn't guarantee that error code is actually non-null - only the runtime will assure me this, right?

E.g. in Rust you would wrap the return value into an [`Option`](https://doc.rust-lang.org/std/option/) and pattern match it, so e.g.

```rust
    let parse_error: Option<int> = Some(42);

    match parse_error {
        Some(error_code) => {
            // error_code is guaranteed to exist as int by the type checker
            !exit(error_code)
        },
        None => {},
    }
```
created_at: 2025-04-03T15:31:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,29 @@
+#include "boost/program_options.hpp"
+#include <string>
+#include "SC_TerminalClient.h"
+
path: lang/LangSource/SC_CLIOptions.hpp
user: login: JordanHendersonMusic
body: `#include <optional>`
created_at: 2025-04-09T14:36:00Z
author_association: CONTRIBUTOR

PR comments:
user: login: capital-G
created_at: 2025-03-26T11:21:00Z
body: I introduced and fixed a bug regarding the config file creation, but from my perspective everything is now working fine. Though a test on e.g. windows would be highly appreciated!

This PR would allow us to make testing and doc creation easier as it introduces a way to explicitly set language config parameters instead of relying on setting them through a yaml file.

edit: rebased against develop so macOS run works
author_association: CONTRIBUTOR
user: login: capital-G
created_at: 2025-03-26T16:20:18Z
body: Hope I fixed most things except the two open questions?

I had to change the signature of

https://github.com/capital-G/supercollider/blame/4c11b5e76d00f76f97c72cf97d566922a78b5fab/include/lang/SC_LanguageClient.h#L54

Although this is in the include folder (and therefore exposed to other libs?) this shouldn't be a big problem b/c we don't have a stability contract regarding the LanguageClient interface, right?

If everything is done properly I'll try to give this a rebase so we have a proper git history for this.

edit: and once again, thanks for the review :) learned a lot

edit2: fully applied the naming conventions - would be great if `clang-format` would spot these errors b/c my IDE (CLion) suggested to use `snake_case` for the variables. Additionally, would it make sense to introduce a `.clang-tidy`? CLion already spots mistakes such as passing a string by value using clang-tidy.
author_association: CONTRIBUTOR
user: login: Spacechild1
created_at: 2025-03-26T19:09:10Z
body: > this shouldn't be a big problem b/c we don't have a stability contract regarding the LanguageClient interface, right?

Yes, the interface is only used by internal clients (the terminal client and QtCollider) so we are free to make changes. AFAICT there is no official way to build sclang as a library in the first place.
author_association: CONTRIBUTOR
user: login: Spacechild1
created_at: 2025-03-26T19:30:10Z
body: Thanks! Caught a few more things.
author_association: CONTRIBUTOR
user: login: Spacechild1
created_at: 2025-03-28T09:51:29Z
body: A few more little things. Sorry I didn't catch these immediately...
author_association: CONTRIBUTOR
user: login: capital-G
created_at: 2025-03-29T10:03:31Z
body: > A few more little things. Sorry I didn't catch these immediately...

No problem at all, I really enjoy learning some things here, hope you don't get annoyed by it :)

author_association: CONTRIBUTOR
user: login: Spacechild1
created_at: 2025-04-01T17:53:17Z
body: I think this is a great feature! I have do admit that I didn't actually test the command line options myself, but I trust you when you say that they all work as expected. I don't think that any of your changes could possibly behave differently on Windows.
author_association: CONTRIBUTOR
user: login: JordanHendersonMusic
created_at: 2025-04-03T15:38:18Z
body: Looks good! I also appreciate the three '\\\' so it appears in the editor 😃
author_association: CONTRIBUTOR
user: login: dyfer
created_at: 2025-04-11T17:20:34Z
body: I ran the Windows build from this PR in a VM, tested a few of the long args and they seem to work.

Adjacent to this PR:
In the past, when the "standalone"/"portable" build of SC was discussed, there were 2 different concepts of the "standalone" parameter proposed:
- excluding all search paths (and needing to include the scclasslibrary path in order to run SC) - useful e.g. for running SC with a different class library for testing; current `-a` option; running a "portable" build of SC requires manually including the path to the bundled class library
- excluding all search paths _external to the app bundle/folder_ (i.e. user/system extensions) - this is actually desirable for a "portable"/"standalone" app; not currently implemented IIUC

While changing/extending this is probably beyond the scope of this PR, I also wonder if the new `--standalone` flag could actually mean the _latter_ (new) behavior; (the long option for `-a` could be `--exclude-all-search-paths` or something like that).

Even if we merge this as is, we can still change the behavior of the flag before the 3.14 release, if we think it's a desirable change. Please let me know what you think!
author_association: MEMBER
user: login: capital-G
created_at: 2025-04-12T10:31:22Z
body: > While changing/extending this is probably beyond the scope of this PR, I also wonder if the new `--standalone` flag could actually mean the _latter_ (new) behavior; (the long option for `-a` could be `--exclude-all-search-paths` or something like that).

Yeah, I also found this a bit misleading and I also find your flag more sensible in this case.
Maybe we should use `--exclude-default-search-paths` instead of `--standalone` and also explain this a bit better it in the help of the command - should I adapt this @dyfer ?

This PR is actually one step towards making isolated sclang environments easier :)

> I ran the Windows build from this PR in a VM, tested a few of the long args and they seem to work.

Thanks for reporting :)
author_association: CONTRIBUTOR
user: login: capital-G
created_at: 2025-04-14T09:03:14Z
body: @dyfer I now added [switch from --standalone to --exclude-default-paths CLI flag](https://github.com/supercollider/supercollider/pull/6640/commits/d692730e6def44e726025934d2a70c26fedae4c1)

Help looks now like

```
❯ ./sclang --help
Start a REPL interpreter:           sclang [ -options ]
Execute a .scd file:                sclang [ -options ] path_to_sc_file [ args passed to sclang... ]

Generic options:
  -h [ --help ]                     Print this message and exit
  -v [ --version ]                  Print sclang version and exit

sclang options:
  -d [ --runtime-directory ] arg    Set runtime directory
  -D [ --daemon ]                   Enter daemon mode
  -g [ --heap-growth ] arg (=256k)  Set heap growth size (allows k, m and g as 
                                    suffix multipliers)
  -l [ --yaml-config ] arg          Set yaml config
  -m [ --heap-size ] arg (=2m)      Set initial heap size (allows k, m and g as
                                    suffix multipliers)
  -r [ --call-run ]                 Call Main.run on startup
  -s [ --call-stop ]                Call Main.stop on shutdown
  -u [ --port ] arg (=57120)        Set UDP listening port
  -i [ --ide ] arg                  Set IDE name
  -a [ --exclude-default-paths ]    Exclude searching in default SCClassLibrary
                                    and user and system extensions paths (aka 
                                    'standalone' mode).

Language config options (overwrites provided yaml entries):
  --include-path arg                Class library path to be included for 
                                    searching (allows multiple mentions)
  --exclude-path arg                Class library path to be excluded from 
                                    searching (overrides includePaths, allows 
                                    multiple mentions)
  --post-inline-warnings            Boolean flag to turn on warnings about 
                                    missing inline opportunities.

```


author_association: CONTRIBUTOR
user: login: dyfer
created_at: 2025-04-14T15:44:05Z
body: Looks great! I like the changed flag.
author_association: MEMBER
user: login: capital-G
created_at: 2025-04-14T19:00:43Z
body: @JordanHendersonMusic is there anything missing for a merge?
author_association: CONTRIBUTOR
user: login: dyfer
created_at: 2025-04-15T01:42:40Z
body: Sorry @capital-G, #6727 created a conflict here... needs to be resolved.
author_association: MEMBER
user: login: dyfer
created_at: 2025-04-25T06:13:44Z
body: I see the individual commits from this PR on the develop branch... I think this PR was supposed to be squashed on merge, but I don't think it was. 

I certainly made that mistake in the past! Just a note for the future.
author_association: MEMBER
user: login: JordanHendersonMusic
created_at: 2025-04-25T06:15:06Z
body: Ah my fault! Can it be undone easily? I might have done this elsewhere too!
author_association: CONTRIBUTOR
user: login: dyfer
created_at: 2025-04-25T06:16:25Z
body: I don't think so. We shouldn't force push to `develop` as that will mess up all the active forks.
It's not the end of the world, just unnecessarily messy.
author_association: MEMBER