Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public CliCommand(PSCmdlet psCmdlet)
public void EnableSetting(string name)
{
Utilities.VerifyAdmin();
_ = this.Run("settings", $"--enable \"{name}\"");
_ = this.Run(new WinGetCLICommandBuilder("settings").AppendOption("enable", name));
}

/// <summary>
Expand All @@ -43,7 +43,7 @@ public void EnableSetting(string name)
public void DisableSetting(string name)
{
Utilities.VerifyAdmin();
_ = this.Run("settings", $"--disable \"{name}\"");
_ = this.Run(new WinGetCLICommandBuilder("settings").AppendOption("disable", name));
}

/// <summary>
Expand All @@ -52,7 +52,7 @@ public void DisableSetting(string name)
/// <param name="asPlainText">Return as string.</param>
public void GetSettings(bool asPlainText)
{
var result = this.Run("settings", "export");
var result = this.Run(new WinGetCLICommandBuilder("settings").AppendSubCommand("export"));

if (asPlainText)
{
Expand All @@ -75,24 +75,27 @@ public void GetSettings(bool asPlainText)
public void AddSource(string name, string arg, string type, string trustLevel, bool isExplicit)
{
Utilities.VerifyAdmin();
string parameters = $"add --name \"{name}\" --arg \"{arg}\"";
var builder = new WinGetCLICommandBuilder("source")
.AppendSubCommand("add")
.AppendOption("name", name)
.AppendOption("arg", arg);

if (!string.IsNullOrEmpty(type))
{
parameters += $" --type \"{type}\"";
builder.AppendOption("type", type);
}

if (!string.IsNullOrEmpty(trustLevel))
{
parameters += $" --trust-level \"{trustLevel}\"";
builder.AppendOption("trust-level", trustLevel);
}

if (isExplicit)
{
parameters += " --explicit";
builder.AppendSwitch("explicit");
}

_ = this.Run("source", parameters, 300000);
_ = this.Run(builder, 300000);
}

/// <summary>
Expand All @@ -102,7 +105,7 @@ public void AddSource(string name, string arg, string type, string trustLevel, b
public void RemoveSource(string name)
{
Utilities.VerifyAdmin();
_ = this.Run("source", $"remove --name \"{name}\"");
_ = this.Run(new WinGetCLICommandBuilder("source").AppendSubCommand("remove").AppendOption("name", name));
}

/// <summary>
Expand All @@ -112,7 +115,7 @@ public void RemoveSource(string name)
public void ResetSourceByName(string name)
{
Utilities.VerifyAdmin();
_ = this.Run("source", $"reset --name \"{name}\" --force");
_ = this.Run(new WinGetCLICommandBuilder("source").AppendSubCommand("reset").AppendOption("name", name).AppendSwitch("force"));
}

/// <summary>
Expand All @@ -121,13 +124,13 @@ public void ResetSourceByName(string name)
public void ResetAllSources()
{
Utilities.VerifyAdmin();
_ = this.Run("source", $"reset --force");
_ = this.Run(new WinGetCLICommandBuilder("source").AppendSubCommand("reset").AppendSwitch("force"));
}

private WinGetCLICommandResult Run(string command, string parameters, int timeOut = 60000)
private WinGetCLICommandResult Run(WinGetCLICommandBuilder builder, int timeOut = 60000)
{
var wingetCliWrapper = new WingetCLIWrapper();
var result = wingetCliWrapper.RunCommand(this, command, parameters, timeOut);
var result = wingetCliWrapper.RunCommand(this, builder, timeOut);
result.VerifyExitCode();

return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public UserSettingsCommand(PSCmdlet psCmdlet)
if (winGetSettingsFilePath == null)
{
var wingetCliWrapper = new WingetCLIWrapper();
var settingsResult = wingetCliWrapper.RunCommand(this, "settings", "export");
var settingsResult = wingetCliWrapper.RunCommand(this, new WinGetCLICommandBuilder("settings").AppendSubCommand("export"));

// Read the user settings file property.
var userSettingsFile = Utilities.ConvertToHashtable(settingsResult.StdOut)["userSettingsFile"] ?? throw new ArgumentNullException("userSettingsFile");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// -----------------------------------------------------------------------------
// <copyright file="WinGetCLICommandBuilder.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
// -----------------------------------------------------------------------------

namespace Microsoft.WinGet.Client.Engine.Helpers
{
using System;
using System.Linq;
using System.Text;

/// <summary>
/// Represents a builder for WinGet CLI commands.
/// </summary>
public class WinGetCLICommandBuilder
{
private readonly StringBuilder builder;

/// <summary>
/// Initializes a new instance of the <see cref="WinGetCLICommandBuilder"/> class.
/// </summary>
/// <param name="command">Optional main command (e.g., settings, source).</param>
public WinGetCLICommandBuilder(string? command = null)
{
this.builder = new StringBuilder();
this.Command = command;
}

/// <summary>
/// Gets the main command (e.g., settings, source).
/// </summary>
public string? Command { get; }

/// <summary>
/// Gets the constructed parameters string.
/// </summary>
public string Parameters => this.builder.ToString();

/// <summary>
/// Appends a switch to the command.
/// </summary>
/// <param name="switchName">The name of the switch to append.</param>
/// <returns>The current instance of <see cref="WinGetCLICommandBuilder"/>.</returns>
public WinGetCLICommandBuilder AppendSwitch(string switchName)
{
this.AppendToken($"--{switchName}");
return this;
}

/// <summary>
/// Appends a sub-command to the command.
/// </summary>
/// <param name="subCommand">The sub-command to append.</param>
/// <returns>The current instance of <see cref="WinGetCLICommandBuilder"/>.</returns>
public WinGetCLICommandBuilder AppendSubCommand(string subCommand)
{
this.AppendToken(subCommand);
return this;
}

/// <summary>
/// Appends an option with its value to the command.
/// </summary>
/// <param name="option">The name of the option to append.</param>
/// <param name="value">The value of the option to append.</param>
/// <returns>The current instance of <see cref="WinGetCLICommandBuilder"/>.</returns>
public WinGetCLICommandBuilder AppendOption(string option, string value)
{
this.AppendToken($"--{option} \"{this.Escape(value)}\"");
return this;
}

/// <summary>
/// Converts the command builder to its string representation.
/// </summary>
/// <returns>The string representation of the command.</returns>
public override string ToString()
{
if (string.IsNullOrEmpty(this.Command))
{
return this.Parameters;
}

return $"{this.Command} {this.Parameters}";
}

/// <summary>
/// Escapes a command-line argument according to Windows command-line parsing rules.
/// </summary>
/// <param name="arg">The argument to escape.</param>
/// <returns>The escaped argument.</returns>
private string Escape(string arg)
Copy link
Contributor Author

@AmelBawa-msft AmelBawa-msft Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn’t find a good spot to add unit tests for this, so I’m thinking of creating a new csproj just for the tests

{
if (string.IsNullOrEmpty(arg))
{
return "\"\"";
}

// Determine if the argument needs quotes
// - Contains whitespace, or
// - Contains double quotes, or
// - Ends with a backslash
var needsQuotes = arg.Any(char.IsWhiteSpace) || arg.Contains('"') || arg.EndsWith("\\", StringComparison.Ordinal);
if (!needsQuotes)
{
return arg;
}

var sb = new StringBuilder(arg.Length + 2);
sb.Append('"');

int bs = 0;
foreach (char c in arg)
{
if (c == '\\')
{
bs++;
}
else if (c == '"')
{
sb.Append('\\', (bs * 2) + 1);
sb.Append('"');
bs = 0;
}
else
{
sb.Append('\\', bs);
sb.Append(c);
bs = 0;
}
}

if (bs > 0)
{
sb.Append('\\', bs * 2);
}

sb.Append('"');
return sb.ToString();
}

/// <summary>
/// Appends a token to the command.
/// </summary>
/// <param name="token">The token to append.</param>
private void AppendToken(string token)
{
if (this.builder.Length > 0)
{
this.builder.Append(' ');
}

this.builder.Append(token);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public WinGetVersion(string version)
public static WinGetCLICommandResult RunWinGetVersionFromCLI(PowerShellCmdlet pwshCmdlet, bool fullPath = true)
{
var wingetCliWrapper = new WingetCLIWrapper(fullPath);
return wingetCliWrapper.RunCommand(pwshCmdlet, "--version");
return wingetCliWrapper.RunCommand(pwshCmdlet, new WinGetCLICommandBuilder().AppendSwitch("--version"));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,9 @@ public static string WinGetFullPath
/// <param name="parameters">Parameters.</param>
/// <param name="timeOut">Time out.</param>
/// <returns>WinGetCommandResult.</returns>
public WinGetCLICommandResult RunCommand(PowerShellCmdlet pwshCmdlet, string command, string? parameters = null, int timeOut = 60000)
internal WinGetCLICommandResult RunCommand(PowerShellCmdlet pwshCmdlet, WinGetCLICommandBuilder builder, int timeOut = 60000)
{
string args = command;
if (!string.IsNullOrEmpty(parameters))
{
args += ' ' + parameters;
}

var args = builder.ToString();
pwshCmdlet.Write(StreamType.Verbose, $"Running {this.wingetPath} with {args}");

Process p = new ()
Expand All @@ -96,14 +91,14 @@ public WinGetCLICommandResult RunCommand(PowerShellCmdlet pwshCmdlet, string com
if (p.WaitForExit(timeOut))
{
return new WinGetCLICommandResult(
command,
parameters,
builder.Command,
builder.Parameters,
p.ExitCode,
p.StandardOutput.ReadToEnd(),
p.StandardError.ReadToEnd());
}

throw new WinGetCLITimeoutException(command, parameters);
throw new WinGetCLITimeoutException(builder.Command, builder.Parameters);
}
}
}
Loading