Repository: compiler-explorer/compiler-explorer (ts)

Issue:
[BUG]: Enabling a tool returns error 400
### Describe the bug

see title

json contains `"tools":[{"id":"PVS-Studio","args":[],"stdin":""}]`

### Steps to reproduce

see title v2.0

hitting the recompile button, or making any other change, makes it send `"tools":[{"id":"PVS-Studio","args":"","stdin":""}]` which works

### Expected behavior

200 ok

### Reproduction link

Not applicable

### Screenshots

![image](https://github.com/user-attachments/assets/fc996094-64d5-4f4b-afed-101939f5dcc3)


### Operating System

_No response_

### Browser version

_No response_

Pull request description:
Unify types for tool args
- Uses strings across the board in the UI part (no functional change here from before; all state is the same as it was before).
- Sends _arrays_ in the POST, using the same `splitArguments` code as the backend.
- Backend _still supports_ strings (though doesn't advertise), also using same `splitArguments`.
- Moves `splitArguments` into common utils, and rephrases to avoid unnecessary use of underscore and ES2021+ code.

Tested locally:
- with both old and new client code (ran new backend and old webcode to show sending strings still works)
- with creating and removing tool windows (checked with `ldd` locally)
- with various strings on the client `moo foo "this is bad" #moo` and even "error" things like `this is "badger` (with a missing close quote). All works as you'd expect

Happy to break the "move the splitArguments" code into a separate PR if that'd be easier to review separately.

Fixes #7195 

Patch:
diff --git a/lib/base-compiler.ts b/lib/base-compiler.ts
index 7c9623169..4381c6f77 100644
--- a/lib/base-compiler.ts
+++ b/lib/base-compiler.ts
@@ -30,7 +30,7 @@ import * as PromClient from 'prom-client';
 import temp from 'temp';
 import _ from 'underscore';
 
-import {unique} from '../shared/common-utils.js';
+import {splitArguments, unique} from '../shared/common-utils.js';
 import {OptRemark} from '../static/panes/opt-view.interfaces.js';
 import {PPOptions} from '../static/panes/pp-view.interfaces.js';
 import {ParsedAsmResultLine} from '../types/asmresult/asmresult.interfaces.js';
@@ -1173,7 +1173,7 @@ export class BaseCompiler {
         options = options.concat(this.optionsForBackend(backendOptions, outputFilename));
 
         if (this.compiler.options) {
-            options = options.concat(utils.splitArguments(this.compiler.options));
+            options = options.concat(splitArguments(this.compiler.options));
         }
 
         if (this.compiler.supportsOptOutput && backendOptions.produceOptInfo) {
@@ -2555,7 +2555,7 @@ export class BaseCompiler {
 
         const options: string[] = [];
         if (this.compiler.options) {
-            const compilerOptions = utils.splitArguments(this.compiler.options);
+            const compilerOptions = splitArguments(this.compiler.options);
             options.push(...removeToolchainArg(compilerOptions));
         }
         options.push(...libsAndOptions.options, ...libIncludes);
@@ -2599,13 +2599,13 @@ export class BaseCompiler {
 
     getUsedEnvironmentVariableFlags(makeExecParams: ExecutionOptionsWithEnv) {
         if (this.lang.id === 'c++') {
-            return utils.splitArguments(makeExecParams.env.CXXFLAGS);
+            return splitArguments(makeExecParams.env.CXXFLAGS);
         } else if (this.lang.id === 'fortran') {
-            return utils.splitArguments(makeExecParams.env.FFLAGS);
+            return splitArguments(makeExecParams.env.FFLAGS);
         } else if (this.lang.id === 'cuda') {
-            return utils.splitArguments(makeExecParams.env.CUDAFLAGS);
+            return splitArguments(makeExecParams.env.CUDAFLAGS);
         } else {
-            return utils.splitArguments(makeExecParams.env.CFLAGS);
+            return splitArguments(makeExecParams.env.CFLAGS);
         }
     }
 
@@ -2694,7 +2694,7 @@ export class BaseCompiler {
 
             const toolchainparam = this.getCMakeExtToolchainParam(parsedRequest.backendOptions.overrides || []);
 
-            const cmakeArgs = utils.splitArguments(parsedRequest.backendOptions.cmakeArgs);
+            const cmakeArgs = splitArguments(parsedRequest.backendOptions.cmakeArgs);
             const partArgs: string[] = [
                 toolchainparam,
                 ...this.getExtraCMakeArgs(parsedRequest),
@@ -3546,7 +3546,7 @@ but nothing was dumped. Possible causes are:
             });
         }
 
-        const compilerOptions = utils.splitArguments(this.compiler.options);
+        const compilerOptions = splitArguments(this.compiler.options);
         if (hasToolchainArg(compilerOptions)) {
             const possibleToolchains: CompilerOverrideOptions = await this.getPossibleToolchains();
 
diff --git a/lib/buildenvsetup/base.ts b/lib/buildenvsetup/base.ts
index 0667ea272..06e9676ab 100644
--- a/lib/buildenvsetup/base.ts
+++ b/lib/buildenvsetup/base.ts
@@ -26,6 +26,7 @@ import path from 'path';
 
 import _ from 'underscore';
 
+import {splitArguments} from '../../shared/common-utils.js';
 import {Arch, CacheKey, ExecutionOptionsWithEnv} from '../../types/compilation/compilation.interfaces.js';
 import {CompilerInfo} from '../../types/compiler.interfaces.js';
 import {UnprocessedExecResult} from '../../types/execution/execution.interfaces.js';
@@ -55,7 +56,7 @@ export class BuildEnvSetupBase {
         this.compiler = compilerInfo;
         this.env = env;
 
-        this.compilerOptionsArr = utils.splitArguments(this.compiler.options);
+        this.compilerOptionsArr = splitArguments(this.compiler.options);
         this.compilerArch = this.getCompilerArch();
         this.compilerTypeOrGCC = compilerInfo.compilerType || 'gcc';
         if (this.compilerTypeOrGCC === 'clang-intel') this.compilerTypeOrGCC = 'gcc';
diff --git a/lib/compilers/ada.ts b/lib/compilers/ada.ts
index 5b3c2229f..8a45bd7e6 100644
--- a/lib/compilers/ada.ts
+++ b/lib/compilers/ada.ts
@@ -25,6 +25,7 @@
 
 import path from 'path';
 
+import {splitArguments} from '../../shared/common-utils.js';
 import type {ConfiguredOverrides} from '../../types/compilation/compiler-overrides.interfaces.js';
 import type {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
 import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
@@ -32,7 +33,6 @@ import {SelectedLibraryVersion} from '../../types/libraries/libraries.interfaces
 import {unwrap} from '../assert.js';
 import {BaseCompiler} from '../base-compiler.js';
 import {CompilationEnvironment} from '../compilation-env.js';
-import * as utils from '../utils.js';
 
 export class AdaCompiler extends BaseCompiler {
     static get key() {
@@ -170,7 +170,7 @@ export class AdaCompiler extends BaseCompiler {
         // in the correct list.
 
         let part = 0; // 0: gnatmake, 1: compiler, 2: linker, 3: binder
-        for (const a of backend_opts.concat(utils.splitArguments(this.compiler.options), userOptions)) {
+        for (const a of backend_opts.concat(splitArguments(this.compiler.options), userOptions)) {
             if (a === '-cargs') {
                 part = 1;
                 continue;
diff --git a/lib/compilers/argument-parsers.ts b/lib/compilers/argument-parsers.ts
index e6b55297b..016a8ce38 100644
--- a/lib/compilers/argument-parsers.ts
+++ b/lib/compilers/argument-parsers.ts
@@ -29,6 +29,7 @@ import * as Sentry from '@sentry/node';
 import fs from 'fs-extra';
 import _ from 'underscore';
 
+import {splitArguments} from '../../shared/common-utils.js';
 import {CompilerOverrideOptions} from '../../types/compilation/compiler-overrides.interfaces.js';
 import {Argument} from '../../types/compiler-arguments.interfaces.js';
 import {BaseCompiler} from '../base-compiler.js';
@@ -1195,7 +1196,7 @@ export class GolangParser extends GCCParser {
     static override async getOptions(compiler: BaseCompiler, helpArg: string) {
         const optionFinder1 = /^\s*(--?[\d#+,<=>[\]a-z|-]* ?[\d+,<=>[\]a-z|-]*)\s+(.*)/i;
         const optionFinder2 = /^\s*(--?[\d#+,<=>[\]a-z|-]* ?[\d+,<=>[\]a-z|-]*)/i;
-        const result = await compiler.execCompilerCached(compiler.compiler.exe, utils.splitArguments(helpArg), {
+        const result = await compiler.execCompilerCached(compiler.compiler.exe, splitArguments(helpArg), {
             ...compiler.getDefaultExecOptions(),
             createAndUseTempDir: true,
         });
diff --git a/lib/compilers/dart.ts b/lib/compilers/dart.ts
index 73075bc57..8224f5f58 100644
--- a/lib/compilers/dart.ts
+++ b/lib/compilers/dart.ts
@@ -26,6 +26,7 @@ import path from 'path';
 
 import Semver from 'semver';
 
+import {splitArguments} from '../../shared/common-utils.js';
 import type {ConfiguredOverrides} from '../../types/compilation/compiler-overrides.interfaces.js';
 import type {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
 import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
@@ -59,7 +60,7 @@ export class DartCompiler extends BaseCompiler {
         let options = this.optionsForFilter(filters, outputFilename, userOptions);
 
         if (this.compiler.options) {
-            options = options.concat(utils.splitArguments(this.compiler.options));
+            options = options.concat(splitArguments(this.compiler.options));
         }
 
         const libIncludes = this.getIncludeArguments(libraries, path.dirname(inputFilename));
diff --git a/lib/compilers/spirv.ts b/lib/compilers/spirv.ts
index d7d8006ce..6085b3059 100644
--- a/lib/compilers/spirv.ts
+++ b/lib/compilers/spirv.ts
@@ -24,6 +24,7 @@
 
 import path from 'path';
 
+import {splitArguments} from '../../shared/common-utils.js';
 import type {ExecutionOptionsWithEnv} from '../../types/compilation/compilation.interfaces.js';
 import type {ConfiguredOverrides} from '../../types/compilation/compiler-overrides.interfaces.js';
 import {LLVMIrBackendOptions} from '../../types/compilation/ir.interfaces.js';
@@ -69,9 +70,9 @@ export class SPIRVCompiler extends BaseCompiler {
         backendOptions = backendOptions || {};
 
         if (this.compiler.options) {
-            const compilerOptions = utils
-                .splitArguments(this.compiler.options)
-                .filter(option => option !== '-fno-crash-diagnostics');
+            const compilerOptions = splitArguments(this.compiler.options).filter(
+                option => option !== '-fno-crash-diagnostics',
+            );
 
             options = options.concat(compilerOptions);
         }
diff --git a/lib/compilers/win32.ts b/lib/compilers/win32.ts
index 4e371de0a..14966ac63 100644
--- a/lib/compilers/win32.ts
+++ b/lib/compilers/win32.ts
@@ -26,6 +26,7 @@ import path from 'path';
 
 import _ from 'underscore';
 
+import {splitArguments} from '../../shared/common-utils.js';
 import type {CacheKey, ExecutionOptions} from '../../types/compilation/compilation.interfaces.js';
 import type {ConfiguredOverrides} from '../../types/compilation/compiler-overrides.interfaces.js';
 import type {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
@@ -37,7 +38,6 @@ import {CompilationEnvironment} from '../compilation-env.js';
 import {MapFileReaderVS} from '../mapfiles/map-file-vs.js';
 import {AsmParser} from '../parsers/asm-parser.js';
 import {PELabelReconstructor} from '../pe32-support.js';
-import * as utils from '../utils.js';
 
 export class Win32Compiler extends BaseCompiler {
     static get key() {
@@ -103,7 +103,7 @@ export class Win32Compiler extends BaseCompiler {
         backendOptions = backendOptions || {};
 
         if (this.compiler.options) {
-            options = options.concat(utils.splitArguments(this.compiler.options));
+            options = options.concat(splitArguments(this.compiler.options));
         }
 
         if (this.compiler.supportsOptOutput && backendOptions.produceOptInfo) {
diff --git a/lib/compilers/ylc.ts b/lib/compilers/ylc.ts
index d1e9c386b..0ec31e783 100644
--- a/lib/compilers/ylc.ts
+++ b/lib/compilers/ylc.ts
@@ -1,11 +1,11 @@
 import path from 'path';
 
+import {splitArguments} from '../../shared/common-utils.js';
 import {ConfiguredOverrides} from '../../types/compilation/compiler-overrides.interfaces.js';
 import {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
 import {SelectedLibraryVersion} from '../../types/libraries/libraries.interfaces.js';
 import {unwrap} from '../assert.js';
 import {BaseCompiler} from '../base-compiler.js';
-import * as utils from '../utils.js';
 
 import {BaseParser} from './argument-parsers.js';
 
@@ -37,7 +37,7 @@ export class YLCCompiler extends BaseCompiler {
         options = options.concat(this.optionsForBackend(backendOptions, outputFilename));
 
         if (this.compiler.options) {
-            options = options.concat(utils.splitArguments(this.compiler.options));
+            options = options.concat(splitArguments(this.compiler.options));
         }
 
         if (this.compiler.supportsOptOutput && backendOptions.produceOptInfo) {
diff --git a/lib/execution/base-execution-env.ts b/lib/execution/base-execution-env.ts
index 663695a7b..199756f78 100644
--- a/lib/execution/base-execution-env.ts
+++ b/lib/execution/base-execution-env.ts
@@ -28,6 +28,7 @@ import path from 'path';
 import fs from 'fs-extra';
 import temp from 'temp';
 
+import {splitArguments} from '../../shared/common-utils.js';
 import {
     BuildResult,
     ExecutionOptions,
@@ -182,7 +183,7 @@ export class LocalExecutionEnvironment implements IExecutionEnvironment {
         assert(this.dirPath !== 'not initialized');
 
         const execExecutableOptions: ExecutableExecutionOptions = {
-            args: typeof params.args === 'string' ? utils.splitArguments(params.args) : params.args || [],
+            args: typeof params.args === 'string' ? splitArguments(params.args) : params.args || [],
             stdin: params.stdin || '',
             ldPath: this.buildResult.preparedLdPaths || [],
             env: {},
diff --git a/lib/handlers/compile.ts b/lib/handlers/compile.ts
index 7eb77135c..b8342e772 100644
--- a/lib/handlers/compile.ts
+++ b/lib/handlers/compile.ts
@@ -35,12 +35,13 @@ import _ from 'underscore';
 // @ts-ignore
 import which from 'which';
 
-import {remove} from '../../shared/common-utils.js';
+import {remove, splitArguments} from '../../shared/common-utils.js';
 import {
     ActiveTool,
     BypassCache,
     ExecutionParams,
     FiledataPair,
+    LegacyCompatibleActiveTool,
     UnparsedExecutionParams,
 } from '../../types/compilation/compilation.interfaces.js';
 import {CompilerOverrideOptions} from '../../types/compilation/compiler-overrides.interfaces.js';
@@ -381,7 +382,7 @@ export class CompileHandler implements ICompileHandler {
             backendOptions: Record<string, any> = {},
             filters: ParseFiltersAndOutputOptions,
             bypassCache = BypassCache.None,
-            tools;
+            inputTools: LegacyCompatibleActiveTool[] = [];
         const execReqParams: UnparsedExecutionParams = {};
         let libraries: any[] = [];
         // IF YOU MODIFY ANYTHING HERE PLEASE UPDATE THE DOCUMENTATION!
@@ -398,7 +399,7 @@ export class CompileHandler implements ICompileHandler {
             execReqParams.runtimeTools = execParams.runtimeTools;
             backendOptions = requestOptions.compilerOptions || {};
             filters = {...compiler.getDefaultFilters(), ...requestOptions.filters};
-            tools = requestOptions.tools;
+            inputTools = requestOptions.tools || [];
             libraries = requestOptions.libraries || [];
         } else if (req.body && req.body.compiler) {
             const textRequest = req.body as CompileRequestTextBody;
@@ -441,17 +442,16 @@ export class CompileHandler implements ICompileHandler {
             backendOptions.skipPopArgs = query.skipPopArgs === 'true';
         }
         const executeParameters: ExecutionParams = {
-            args: Array.isArray(execReqParams.args)
-                ? execReqParams.args || ''
-                : utils.splitArguments(execReqParams.args),
+            args: Array.isArray(execReqParams.args) ? execReqParams.args || '' : splitArguments(execReqParams.args),
             stdin: execReqParams.stdin || '',
             runtimeTools: execReqParams.runtimeTools || [],
         };
 
-        tools = tools || [];
-        for (const tool of tools) {
-            tool.args = utils.splitArguments(tool.args);
-        }
+        const tools: ActiveTool[] = inputTools.map(tool => {
+            // expand tools.args to an array using utils.splitArguments if it was a string
+            if (typeof tool.args === 'string') tool.args = splitArguments(tool.args);
+            return tool as ActiveTool;
+        });
 
         // Backwards compatibility: bypassCache used to be a boolean.
         // Convert a boolean input to an enum's underlying numeric value
@@ -459,7 +459,7 @@ export class CompileHandler implements ICompileHandler {
 
         return {
             source,
-            options: utils.splitArguments(options),
+            options: splitArguments(options),
             backendOptions,
             filters,
             bypassCache,
@@ -492,7 +492,7 @@ export class CompileHandler implements ICompileHandler {
             if (data.presplit) {
                 return data.usedOptions;
             } else {
-                return utils.splitArguments(data.usedOptions);
+                return splitArguments(data.usedOptions);
             }
         }
         return false;
diff --git a/lib/options-handler.ts b/lib/options-handler.ts
index d29b520e0..8dc5c086a 100755
--- a/lib/options-handler.ts
+++ b/lib/options-handler.ts
@@ -30,6 +30,7 @@ import semverParser from 'semver';
 import _ from 'underscore';
 
 import {AppDefaultArguments} from '../app.js';
+import {splitArguments} from '../shared/common-utils.js';
 import {CompilerInfo} from '../types/compiler.interfaces.js';
 import type {LanguageKey} from '../types/languages.interfaces.js';
 import type {Source} from '../types/source.interfaces.js';
@@ -39,7 +40,7 @@ import {logger} from './logger.js';
 import type {PropertyGetter, PropertyValue} from './properties.interfaces.js';
 import {CompilerProps} from './properties.js';
 import {BaseTool, getToolTypeByKey} from './tooling/index.js';
-import {asSafeVer, getHash, splitArguments, splitIntoArray} from './utils.js';
+import {asSafeVer, getHash, splitIntoArray} from './utils.js';
 
 // TODO: Figure out if same as libraries.interfaces.ts?
 export type VersionInfo = {
diff --git a/lib/toolchain-utils.ts b/lib/toolchain-utils.ts
index b0a621ba5..061de8795 100644
--- a/lib/toolchain-utils.ts
+++ b/lib/toolchain-utils.ts
@@ -26,11 +26,10 @@ import path from 'path';
 
 import fs from 'fs-extra';
 
+import {splitArguments} from '../shared/common-utils.js';
 import {CompilerOverrideOptions} from '../types/compilation/compiler-overrides.interfaces.js';
 import {PreliminaryCompilerInfo} from '../types/compiler.interfaces.js';
 
-import {splitArguments} from './utils.js';
-
 export const clang_style_toolchain_flag = '--gcc-toolchain=';
 export const icc_style_toolchain_flag = '--gxx-name=';
 export const clang_style_sysroot_flag = '--sysroot=';
diff --git a/lib/tooling/clang-tidy-tool.ts b/lib/tooling/clang-tidy-tool.ts
index 5d674d63c..5d8f9a0a4 100644
--- a/lib/tooling/clang-tidy-tool.ts
+++ b/lib/tooling/clang-tidy-tool.ts
@@ -26,10 +26,10 @@ import path from 'path';
 
 import fs from 'fs-extra';
 
+import {splitArguments} from '../../shared/common-utils.js';
 import {CompilationInfo} from '../../types/compilation/compilation.interfaces.js';
 import {ToolInfo} from '../../types/tool.interfaces.js';
 import {OptionsHandlerLibrary} from '../options-handler.js';
-import * as utils from '../utils.js';
 
 import {ToolEnv} from './base-tool.interface.js';
 import {BaseTool} from './base-tool.js';
@@ -75,7 +75,7 @@ export class ClangTidyTool extends BaseTool {
         //     -> before my patchup this was done only for non-clang compilers
         //  3) options manually specified in the compiler tab (options)
         //  *) indepenent are `args` from the clang-tidy tab
-        let compileFlags = utils.splitArguments(compilationInfo.compiler.options);
+        let compileFlags = splitArguments(compilationInfo.compiler.options);
         compileFlags = compileFlags.concat(includeflags);
         compileFlags = compileFlags.concat(libOptions);
 
diff --git a/lib/tooling/compiler-dropin-tool.ts b/lib/tooling/compiler-dropin-tool.ts
index 0acff3118..bf26d0069 100644
--- a/lib/tooling/compiler-dropin-tool.ts
+++ b/lib/tooling/compiler-dropin-tool.ts
@@ -22,11 +22,11 @@
 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 // POSSIBILITY OF SUCH DAMAGE.
 
+import {splitArguments} from '../../shared/common-utils.js';
 import {CompilationInfo} from '../../types/compilation/compilation.interfaces.js';
 import {ToolInfo, ToolResult} from '../../types/tool.interfaces.js';
 import {OptionsHandlerLibrary} from '../options-handler.js';
 import {getToolchainPath} from '../toolchain-utils.js';
-import * as utils from '../utils.js';
 
 import {ToolEnv} from './base-tool.interface.js';
 import {BaseTool} from './base-tool.js';
@@ -64,9 +64,7 @@ export class CompilerDropinTool extends BaseTool {
 
         const toolchainPath = this.getToolchainPath(compilationInfo);
 
-        let compilerOptions = compilationInfo.compiler.options
-            ? utils.splitArguments(compilationInfo.compiler.options)
-            : [];
+        let compilerOptions = compilationInfo.compiler.options ? splitArguments(compilationInfo.compiler.options) : [];
 
         if (toolchainPath) {
             // note: needs toolchain argument twice as the first time its sometimes ignored
diff --git a/lib/tooling/microsoft-analysis-tool.ts b/lib/tooling/microsoft-analysis-tool.ts
index 8be7a9041..ebb1c8be8 100644
--- a/lib/tooling/microsoft-analysis-tool.ts
+++ b/lib/tooling/microsoft-analysis-tool.ts
@@ -24,11 +24,11 @@
 
 import path from 'path';
 
+import {splitArguments} from '../../shared/common-utils.js';
 import {ToolInfo} from '../../types/tool.interfaces.js';
 import {unwrap} from '../assert.js';
 import {logger} from '../logger.js';
 import {OptionsHandlerLibrary} from '../options-handler.js';
-import * as utils from '../utils.js';
 
 import {ToolEnv} from './base-tool.interface.js';
 import {BaseTool} from './base-tool.js';
@@ -88,7 +88,7 @@ export class MicrosoftAnalysisTool extends BaseTool {
         const libOptions = super.getLibraryOptions(compilationInfo.libraries, unwrap(supportedLibraries));
         const includeflags = super.getIncludeArguments(compilationInfo.libraries, unwrap(supportedLibraries));
 
-        let compileFlags = utils.splitArguments(compilationInfo.compiler.options);
+        let compileFlags = splitArguments(compilationInfo.compiler.options);
         compileFlags = compileFlags.concat(includeflags, libOptions);
 
         const manualCompileFlags = options.filter((option: string) => option !== sourcefile);
diff --git a/lib/tooling/pvs-studio-tool.ts b/lib/tooling/pvs-studio-tool.ts
index d28107143..dac36e40c 100644
--- a/lib/tooling/pvs-studio-tool.ts
+++ b/lib/tooling/pvs-studio-tool.ts
@@ -26,6 +26,7 @@ import path from 'path';
 
 import fs from 'fs-extra';
 
+import {splitArguments} from '../../shared/common-utils.js';
 import {CompilationInfo} from '../../types/compilation/compilation.interfaces.js';
 import {ToolInfo} from '../../types/tool.interfaces.js';
 import {assert} from '../assert.js';
@@ -60,7 +61,7 @@ export class PvsStudioTool extends BaseTool {
         const sourceDir = path.dirname(inputFilepath);
 
         // Collecting the flags of compilation
-        let compileFlags = utils.splitArguments(compilationInfo.compiler.options);
+        let compileFlags = splitArguments(compilationInfo.compiler.options);
 
         const includeflags = super.getIncludeArguments(compilationInfo.libraries, compilationInfo.compiler);
         compileFlags = compileFlags.concat(includeflags);
diff --git a/lib/tooling/sonar-tool.ts b/lib/tooling/sonar-tool.ts
index 94bca8091..10540ed73 100644
--- a/lib/tooling/sonar-tool.ts
+++ b/lib/tooling/sonar-tool.ts
@@ -24,6 +24,7 @@
 
 import path from 'path';
 
+import {splitArguments} from '../../shared/common-utils.js';
 import {CompilationInfo, ExecutionOptions} from '../../types/compilation/compilation.interfaces.js';
 import type {
     Fix,
@@ -187,7 +188,7 @@ export class SonarTool extends BaseTool {
 
         // Collecting the flags of compilation
 
-        let compileFlags: string[] = utils.splitArguments(compilationInfo.compiler.options);
+        let compileFlags: string[] = splitArguments(compilationInfo.compiler.options);
         const includeflags = super.getIncludeArguments(compilationInfo.libraries, supportedLibraries || {});
         compileFlags = compileFlags.concat(includeflags);
         const libOptions = super.getLibraryOptions(compilationInfo.libraries, supportedLibraries || {});
diff --git a/lib/utils.ts b/lib/utils.ts
index 25b27f489..2b630183a 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -31,7 +31,6 @@ import {fileURLToPath} from 'url';
 import fs from 'fs-extra';
 import {ComponentConfig, ItemConfigType} from 'golden-layout';
 import semverParser from 'semver';
-import {parse as quoteParse} from 'shell-quote';
 import _ from 'underscore';
 
 import type {CacheableValue} from '../types/cache.interfaces.js';
@@ -442,14 +441,6 @@ export function splitIntoArray(input?: string, defaultArray: string[] = []): str
     }
 }
 
-export function splitArguments(options = ''): string[] {
-    // escape hashes first, otherwise they're interpreted as comments
-    const escapedOptions = options.replaceAll('#', '\\#');
-    return _.chain(quoteParse(escapedOptions).map((x: any) => (typeof x === 'string' ? x : (x.pattern as string))))
-        .compact()
-        .value();
-}
-
 /***
  * Absolute path to the root of the application
  */
diff --git a/shared/common-utils.ts b/shared/common-utils.ts
index f4654575a..d63318e1c 100644
--- a/shared/common-utils.ts
+++ b/shared/common-utils.ts
@@ -22,6 +22,8 @@
 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 // POSSIBILITY OF SUCH DAMAGE.
 
+import {parse as quoteParse} from 'shell-quote';
+
 export function isString(x: any): x is string {
     return typeof x === 'string' || x instanceof String;
 }
@@ -99,3 +101,11 @@ function splitIntoChunks(s: string, chunkSize: number): string[] {
 export function addDigitSeparator(n: string, digitSeparator: string, chunkSize: number): string {
     return splitIntoChunks(n, chunkSize).join(digitSeparator);
 }
+
+export function splitArguments(options = ''): string[] {
+    // escape hashes first, otherwise they're interpreted as comments
+    const escapedOptions = options.replace(/#/g, '\\#');
+    return quoteParse(escapedOptions)
+        .map((x: any) => (typeof x === 'string' ? x : (x.pattern as string)))
+        .filter(Boolean);
+}
diff --git a/static/panes/compiler.ts b/static/panes/compiler.ts
index cf984b3e0..fecc495c3 100644
--- a/static/panes/compiler.ts
+++ b/static/panes/compiler.ts
@@ -78,7 +78,7 @@ import {CompilerShared} from '../compiler-shared.js';
 import {SentryCapture} from '../sentry.js';
 import {LLVMIrBackendOptions} from '../compilation/ir.interfaces.js';
 import {InstructionSet} from '../instructionsets.js';
-import {escapeHTML} from '../../shared/common-utils.js';
+import {splitArguments, escapeHTML} from '../../shared/common-utils.js';
 import {CompilerVersionInfo, setCompilerVersionPopoverForPane} from '../widgets/compiler-version-info.js';
 import {ClangirBackendOptions} from '../compilation/clangir.interfaces.js';
 import {LanguageKey} from '../languages.interfaces.js';
@@ -1205,7 +1205,7 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
             if (content.componentState.id === this.id) {
                 tools.push({
                     id: content.componentState.toolId,
-                    args: content.componentState.args,
+                    args: splitArguments(content.componentState.args),
                     stdin: content.componentState.stdin,
                 });
             }
@@ -1225,7 +1225,7 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
         if (newToolSettings) {
             tools.push({
                 id: newToolSettings.toolId,
-                args: newToolSettings.args ? newToolSettings.args.split('\n') : [],
+                args: splitArguments(newToolSettings.args),
                 stdin: newToolSettings.stdin ?? '',
             });
         }
diff --git a/types/compilation/compilation.interfaces.ts b/types/compilation/compilation.interfaces.ts
index 9a30ce038..5afde6e75 100644
--- a/types/compilation/compilation.interfaces.ts
+++ b/types/compilation/compilation.interfaces.ts
@@ -49,6 +49,9 @@ export type ActiveTool = {
     stdin: string;
 };
 
+// This is a legacy type that allows a single string to be passed as args but is otherwise identical to ActiveTool:
+export type LegacyCompatibleActiveTool = Exclude<ActiveTool, 'args'> & {args: string | string[]};
+
 export type UnparsedExecutionParams = {
     args?: string | string[];
     stdin?: string;


Test patch:
diff --git a/lib/compilers/fake-for-test.ts b/lib/compilers/fake-for-test.ts
index 699e010d6..528e4fc1d 100644
--- a/lib/compilers/fake-for-test.ts
+++ b/lib/compilers/fake-for-test.ts
@@ -98,6 +98,7 @@ export class FakeCompiler {
                 backendOptions: backendOptions,
                 filters: filters,
                 files: files,
+                tools: tools,
             },
         };
 
diff --git a/test/base-compiler-tests.ts b/test/base-compiler-tests.ts
index be1010017..39aa320f2 100644
--- a/test/base-compiler-tests.ts
+++ b/test/base-compiler-tests.ts
@@ -31,7 +31,7 @@ import {ClangCompiler} from '../lib/compilers/clang.js';
 import {RustCompiler} from '../lib/compilers/rust.js';
 import {Win32Compiler} from '../lib/compilers/win32.js';
 import * as props from '../lib/properties.js';
-import {splitArguments} from '../lib/utils.js';
+import {splitArguments} from '../shared/common-utils.js';
 import {CompilerOverrideType, ConfiguredOverrides} from '../types/compilation/compiler-overrides.interfaces.js';
 import {CompilerInfo} from '../types/compiler.interfaces.js';
 
diff --git a/test/common-utils-tests.ts b/test/common-utils-tests.ts
index 3b581fa83..6fcd97970 100644
--- a/test/common-utils-tests.ts
+++ b/test/common-utils-tests.ts
@@ -24,7 +24,7 @@
 
 import {describe, expect, it} from 'vitest';
 
-import {addDigitSeparator, escapeHTML} from '../shared/common-utils.js';
+import {addDigitSeparator, escapeHTML, splitArguments} from '../shared/common-utils.js';
 
 describe('HTML Escape Test Cases', () => {
     it('should prevent basic injection', () => {
@@ -57,3 +57,34 @@ describe('digit separator', () => {
         expect(addDigitSeparator('-123456789', '_', 3)).toEqual('-123_456_789');
     });
 });
+
+describe('argument splitting', () => {
+    it('should handle normal things', () => {
+        expect(splitArguments('-hello --world etc --std=c++20')).toEqual(['-hello', '--world', 'etc', '--std=c++20']);
+    });
+
+    it('should handle hash chars', () => {
+        expect(splitArguments('-Wno#warnings -Wno-#pragma-messages')).toEqual([
+            '-Wno#warnings',
+            '-Wno-#pragma-messages',
+        ]);
+    });
+
+    it('should handle doublequoted args', () => {
+        expect(splitArguments('--hello "-world etc"')).toEqual(['--hello', '-world etc']);
+    });
+
+    it('should handle singlequoted args', () => {
+        expect(splitArguments("--hello '-world etc'")).toEqual(['--hello', '-world etc']);
+    });
+
+    it('should handle cheekyness part 1', () => {
+        /* eslint-disable no-useless-escape */
+        expect(splitArguments('hello #veryfancy etc')).toEqual(['hello', '#veryfancy', 'etc']);
+        /* eslint-enable no-useless-escape */
+    });
+
+    it('should handle cheekyness part 2', () => {
+        expect(splitArguments('hello \\#veryfancy etc')).toEqual(['hello', '\\']);
+    });
+});
diff --git a/test/handlers/compile-tests.ts b/test/handlers/compile-tests.ts
index 3990513bc..fb016d112 100644
--- a/test/handlers/compile-tests.ts
+++ b/test/handlers/compile-tests.ts
@@ -22,13 +22,13 @@
 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 // POSSIBILITY OF SUCH DAMAGE.
 
-import express from 'express';
+import express, {Express} from 'express';
 import request from 'supertest';
 import {beforeAll, describe, expect, it} from 'vitest';
 
 import {CompileHandler, SetTestMode} from '../../lib/handlers/compile.js';
 import {fakeProps} from '../../lib/properties.js';
-import {BypassCache} from '../../types/compilation/compilation.interfaces.js';
+import {ActiveTool, BypassCache, LegacyCompatibleActiveTool} from '../../types/compilation/compilation.interfaces.js';
 import {makeCompilationEnvironment} from '../utils.js';
 
 SetTestMode();
@@ -40,7 +40,7 @@ const languages = {
 };
 
 describe('Compiler tests', () => {
-    let app, compileHandler;
+    let app: Express, compileHandler;
 
     beforeAll(() => {
         const compilationEnvironment = makeCompilationEnvironment({languages});
@@ -226,6 +226,7 @@ describe('Compiler tests', () => {
                         filters: {},
                         options: [],
                         source: 'I am a program',
+                        tools: [],
                     },
                     stderr: [{text: 'Something from stderr'}],
                     stdout: [{text: 'Something from stdout'}],
@@ -244,6 +245,31 @@ describe('Compiler tests', () => {
             expect(res.body.input.filters).toEqual({a: true, b: true, c: true});
         });
 
+        it('parses tools with array args', async () => {
+            await setFakeResult();
+            const res = await makeFakeJson('I am a program', {
+                tools: [{id: 'tool', args: ['one', 'two', 'and three'], stdin: ''} as ActiveTool],
+            })
+                .expect('Content-Type', /json/)
+                .expect(200);
+            expect(res.body.input.tools).toEqual([{id: 'tool', args: ['one', 'two', 'and three'], stdin: ''}]);
+        });
+        it('parses tools with string args', async () => {
+            await setFakeResult();
+            const res = await makeFakeJson('I am a program', {
+                tools: [{id: 'tool', args: 'one two "and three" and string', stdin: ''} as LegacyCompatibleActiveTool],
+            })
+                .expect('Content-Type', /json/)
+                .expect(200);
+            expect(res.body.input.tools).toEqual([
+                {
+                    id: 'tool',
+                    args: ['one', 'two', 'and three', 'and', 'string'],
+                    stdin: '',
+                },
+            ]);
+        });
+
         it('parses extra files', async () => {
             await setFakeResult();
             const res = await makeFakeWithExtraFilesJson(
diff --git a/test/utils-tests.ts b/test/utils-tests.ts
index 89ce583ec..d47cfbd8e 100644
--- a/test/utils-tests.ts
+++ b/test/utils-tests.ts
@@ -535,39 +535,3 @@ describe('safe semver', () => {
         expect(utils.asSafeVer('123.456.789 TEXT')).toEqual('123.456.789');
     });
 });
-
-describe('argument splitting', () => {
-    it('should handle normal things', () => {
-        expect(utils.splitArguments('-hello --world etc --std=c++20')).toEqual([
-            '-hello',
-            '--world',
-            'etc',
-            '--std=c++20',
-        ]);
-    });
-
-    it('should handle hash chars', () => {
-        expect(utils.splitArguments('-Wno#warnings -Wno-#pragma-messages')).toEqual([
-            '-Wno#warnings',
-            '-Wno-#pragma-messages',
-        ]);
-    });
-
-    it('should handle doublequoted args', () => {
-        expect(utils.splitArguments('--hello "-world etc"')).toEqual(['--hello', '-world etc']);
-    });
-
-    it('should handle singlequoted args', () => {
-        expect(utils.splitArguments("--hello '-world etc'")).toEqual(['--hello', '-world etc']);
-    });
-
-    it('should handle cheekyness part 1', () => {
-        /* eslint-disable no-useless-escape */
-        expect(utils.splitArguments('hello #veryfancy etc')).toEqual(['hello', '#veryfancy', 'etc']);
-        /* eslint-enable no-useless-escape */
-    });
-
-    it('should handle cheekyness part 2', () => {
-        expect(utils.splitArguments('hello \\#veryfancy etc')).toEqual(['hello', '\\']);
-    });
-});