Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
49ae654
[ListSort, Workflow] Add sort logic for winget list output
Madhusudhan-MSFT Apr 28, 2026
a210423
[ListSortTests, Refactor] Extract sort logic into testable header and…
Madhusudhan-MSFT Apr 28, 2026
918fe25
[ListSortHelper, Refactor] Move sort implementations from header to c…
Madhusudhan-MSFT Apr 29, 2026
7469608
[ListSort, Doc] Update list command documentation with sort options
Madhusudhan-MSFT Apr 29, 2026
6f7773f
[ListSort, Validation] Add argument validation for --sort values
Madhusudhan-MSFT Apr 29, 2026
813c8af
[ListSort, SpellCheck] Add listsort to spelling expect list
Madhusudhan-MSFT Apr 29, 2026
a32a676
[ListSort, Design] Respect settings sort with queries per reviewer fe…
Madhusudhan-MSFT Apr 29, 2026
46a5707
[ListSort, Fix] Code correctness per reviewer feedback
Madhusudhan-MSFT Apr 30, 2026
dc65788
[ListSort, Design] Default sort by name when no settings or query
Madhusudhan-MSFT Apr 30, 2026
ea81abd
[ListSort, Refactor] Redesign helper as production sort pipeline
Madhusudhan-MSFT Apr 30, 2026
0d86ce9
[ListSort, Rename] Rename ListSortHelper to PackageTableSortHelper
Madhusudhan-MSFT Apr 30, 2026
aaf8e9c
[ListSort, Optimize] Skip unused field computation via bitmask
Madhusudhan-MSFT May 1, 2026
8ac341d
[ListSort, Polish] Compile-time drift guard, code cleanup, and docs
Madhusudhan-MSFT May 1, 2026
c19ce84
[ListSort] Address PR review feedback for sort implementation
Madhusudhan-MSFT May 6, 2026
4065b64
[ListSort] Address round 2 PR review feedback
Madhusudhan-MSFT May 7, 2026
dc2542d
[ListSort, Refactor] Consolidate sort parameter resolution into SortP…
Madhusudhan-MSFT May 8, 2026
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
1 change: 1 addition & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ LEBOM
lhs
LIBYAML
liv
listsort
liwpx
localizationpriority
localsource
Expand Down
4 changes: 4 additions & 0 deletions doc/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ Added a user setting (`logging.fileNameStrategy`) for controlling the default na
| guid | The log name is a GUID |
| shortguid | The log name is the first 8 characters of a GUID |

### Sortable `list` output

`winget list` now supports sorting results via `--sort <field>` (repeatable for multi-field sorting), `--ascending`/`--descending` direction flags, and a persistent `output.sortOrder` setting. Available sort fields: `name`, `id`, `version`, `source`, `available`, `relevance`. By default, results are sorted alphabetically by name when no query is present; use `--sort relevance` to preserve the previous source-determined ordering.

## Bug Fixes

* `winget export` now works when the destination path is a hidden file
Expand Down
39 changes: 39 additions & 0 deletions doc/windows/package-manager/winget/list.md
Comment thread
Madhusudhan-MSFT marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ The options allow you to customize the list experience to meet your needs.
| **--upgrade-available** | Lists only packages which have an upgrade available. |
| **-u,--unknown,--include-unknown** | List packages even if their current version cannot be determined. Can only be used with the --upgrade-available argument. |
| **--pinned,--include-pinned** | List packages even if they have a pin that prevents upgrade. Can only be used with the --upgrade-available argument. |
| **--sort** | Sort results by a property. Can be repeated for multi-field sorting (e.g., `--sort source --sort name`). Valid values: `name`, `id`, `version`, `source`, `available`, `relevance`. |
| **--ascending,--asc** | Sort results in ascending order (default). |
| **--descending,--desc** | Sort results in descending order. |
| **-?,--help** | Get additional help on this command. |
| **--wait** | Prompts the user to press any key before exiting. |
| **--logs,--open-logs** | Open the default logs location. |
Expand All @@ -70,6 +73,42 @@ The following example limits the output of list to 9 apps.

![list count command](images/list-count.png)

## Sorting output

By default, results are sorted by name in ascending order. When a query argument is used (for example, `winget list foo`), results preserve relevance ordering from the package source. You can override either default through command-line arguments or user settings.

### Sort via command-line arguments

Use `--sort` to sort by one or more fields. When multiple `--sort` options are specified, results are sorted by the first field, then ties are broken by the second field, and so on.

```cmd
winget list --sort name
winget list --sort source --sort name
winget list --sort name --descending
```

### Sort via user settings

You can set a default sort order in your [settings](https://aka.ms/winget-settings) under `output.sortOrder`:

```json
{
"output": {
"sortOrder": ["source", "name"]
}
}
```

An empty array (`[]`) results in default sorting (sorted by name when listing, relevance preserved when querying).

### Resolution order

When both settings and command-line arguments are present, the following priority applies:

1. **`--sort` command-line argument** — takes highest priority, overrides settings.
2. **`output.sortOrder` in settings** — used when no `--sort` argument is provided. If the user has configured a sort order in settings, it is applied even when a query is present.
3. **Default** — sorted by name in ascending order. When a query is used, relevance ordering is preserved instead.

## List with Update

As stated above, the **list** command allows you to see what apps you have installed that have updates available.
Expand Down
2 changes: 2 additions & 0 deletions src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@
<ClInclude Include="Workflows\SourceFlow.h" />
<ClInclude Include="Workflows\UninstallFlow.h" />
<ClInclude Include="Workflows\UpdateFlow.h" />
<ClInclude Include="Workflows\PackageTableSortHelper.h" />
<ClInclude Include="Workflows\WorkflowBase.h" />
</ItemGroup>
<ItemGroup>
Expand Down Expand Up @@ -459,6 +460,7 @@
<ClCompile Include="Workflows\UninstallFlow.cpp" />
<ClCompile Include="Workflows\UpdateFlow.cpp" />
<ClCompile Include="Workflows\WorkflowBase.cpp" />
<ClCompile Include="Workflows\PackageTableSortHelper.cpp" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
Expand Down
6 changes: 6 additions & 0 deletions src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
<ClInclude Include="Workflows\ShellExecuteInstallerHandler.h">
<Filter>Workflows</Filter>
</ClInclude>
<ClInclude Include="Workflows\PackageTableSortHelper.h">
<Filter>Workflows</Filter>
</ClInclude>
<ClInclude Include="Workflows\WorkflowBase.h">
<Filter>Workflows</Filter>
</ClInclude>
Expand Down Expand Up @@ -322,6 +325,9 @@
<ClCompile Include="Workflows\WorkflowBase.cpp">
<Filter>Workflows</Filter>
</ClCompile>
<ClCompile Include="Workflows\PackageTableSortHelper.cpp">
<Filter>Workflows</Filter>
</ClCompile>
<ClCompile Include="Commands\ShowCommand.cpp">
<Filter>Commands</Filter>
</ClCompile>
Expand Down
13 changes: 13 additions & 0 deletions src/AppInstallerCLICore/Command.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,19 @@ namespace AppInstaller::CLI
}
}

if (execArgs.Contains(Execution::Args::Type::Sort))
{
for (const auto& arg : *execArgs.GetArgs(Execution::Args::Type::Sort))
{
if (!Settings::ConvertToSortField(arg))
{
auto validOptions = Utility::Join(", "_liv, std::vector<Utility::LocIndString>{
"name"_lis, "id"_lis, "version"_lis, "source"_lis, "available"_lis, "relevance"_lis });
throw CommandException(Resource::String::InvalidArgumentValueError(ArgumentCommon::ForType(Execution::Args::Type::Sort).Name, validOptions));
}
}
}

Argument::ValidateExclusiveArguments(execArgs);

ValidateArgumentsInternal(execArgs);
Expand Down
5 changes: 4 additions & 1 deletion src/AppInstallerCLICore/Commands/ListCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ namespace AppInstaller::CLI

std::vector<Argument> ListCommand::GetArguments() const
{
// Compile-time count of sort field values, used to cap --sort argument count.
static constexpr size_t s_sortFieldCount = AppInstaller::GetExponentialEnumValuesCount(Settings::SortField::None);

return {
Argument::ForType(Execution::Args::Type::Query),
Argument::ForType(Execution::Args::Type::Id),
Expand All @@ -32,7 +35,7 @@ namespace AppInstaller::CLI
Argument{ Execution::Args::Type::IncludeUnknown, Resource::String::IncludeUnknownInListArgumentDescription, ArgumentType::Flag },
Argument{ Execution::Args::Type::IncludePinned, Resource::String::IncludePinnedInListArgumentDescription, ArgumentType::Flag},
Argument::ForType(Execution::Args::Type::ListDetails),
Argument{ Execution::Args::Type::Sort, Resource::String::SortArgumentDescription, ArgumentType::Standard },
Argument{ Execution::Args::Type::Sort, Resource::String::SortArgumentDescription, ArgumentType::Standard }.SetCountLimit(s_sortFieldCount),
Argument{ Execution::Args::Type::SortAscending, Resource::String::SortAscendingArgumentDescription, ArgumentType::Flag },
Argument{ Execution::Args::Type::SortDescending, Resource::String::SortDescendingArgumentDescription, ArgumentType::Flag },
};
Expand Down
172 changes: 172 additions & 0 deletions src/AppInstallerCLICore/Workflows/PackageTableSortHelper.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include "pch.h"
#include "PackageTableSortHelper.h"
#include "ExecutionContext.h"

namespace AppInstaller::CLI::Workflow
{
using namespace Settings;

SortablePackageEntry::SortablePackageEntry(
size_t originalIndex,
std::string_view name,
std::string_view id,
std::string_view installedVersion,
std::string_view availableVersion,
std::string_view source,
SortField fieldMask)
: OriginalIndex(originalIndex)
{
if (WI_IsFlagSet(fieldMask, SortField::Name))
{
FoldedName = Utility::FoldCase(name);
}
if (WI_IsFlagSet(fieldMask, SortField::Id))
{
FoldedId = Utility::FoldCase(id);
}
if (WI_IsFlagSet(fieldMask, SortField::Source))
{
FoldedSource = Utility::FoldCase(source);
}
if (WI_IsFlagSet(fieldMask, SortField::Version))
{
ParsedInstalledVersion = Utility::Version{ std::string{ installedVersion } };
}
if (WI_IsFlagSet(fieldMask, SortField::Available))
{
if (!availableVersion.empty())
{
ParsedAvailableVersion = Utility::Version{ std::string{ availableVersion } };
}
}
}

SortField ComputeSortFieldMask(const std::vector<SortField>& sortFields)
{
SortField mask = SortField::None;
for (const auto& f : sortFields)
{
mask |= f;
}
return mask;
}

SortParameters::SortParameters(const Execution::Context& context)
{
if (context.Args.Contains(Execution::Args::Type::Sort))
{
for (const auto& arg : *context.Args.GetArgs(Execution::Args::Type::Sort))
{
auto field = ConvertToSortField(arg);
WI_ASSERT(field.has_value());
if (field.has_value())
{
Fields.emplace_back(field.value());
}
}
}
else
{
Fields = User().Get<Setting::OutputSortOrder>();

if (Fields.empty())
{
bool hasQuery = context.Args.Contains(Execution::Args::Type::Query) ||
context.Args.Contains(Execution::Args::Type::MultiQuery);

if (hasQuery)
{
// Preserve relevance ordering when a free-text query is present
// and no explicit sort preference is configured.
return; // ShouldSort remains false
}

// No settings, no query — default to name sort.
Fields.emplace_back(SortField::Name);
}
}

// Relevance-only means preserve source ordering.
if (Fields.size() == 1 && Fields[0] == SortField::Relevance)
{
return; // ShouldSort remains false
}

// Resolve direction: CLI flags override settings
Direction = context.Args.Contains(Execution::Args::Type::SortDescending) ? SortDirection::Descending :
context.Args.Contains(Execution::Args::Type::SortAscending) ? SortDirection::Ascending :
User().Get<Setting::OutputSortDirection>();

ShouldSort = true;
}

int CompareByField(const SortablePackageEntry& a, const SortablePackageEntry& b, SortField field)
{
switch (field)
{
case SortField::Name:
return a.FoldedName.compare(b.FoldedName);
case SortField::Id:
return a.FoldedId.compare(b.FoldedId);
case SortField::Version:
{
if (a.ParsedInstalledVersion < b.ParsedInstalledVersion) return -1;
if (b.ParsedInstalledVersion < a.ParsedInstalledVersion) return 1;
return 0;
}
case SortField::Source:
return a.FoldedSource.compare(b.FoldedSource);
case SortField::Available:
{
bool aHas = a.ParsedAvailableVersion.has_value();
bool bHas = b.ParsedAvailableVersion.has_value();

// Has-version sorts before no-version in ascending order.
if (aHas != bHas)
{
return aHas ? -1 : 1;
}

// Both have versions — compare normally.
if (aHas && bHas)
{
if (a.ParsedAvailableVersion.value() < b.ParsedAvailableVersion.value()) return -1;
if (b.ParsedAvailableVersion.value() < a.ParsedAvailableVersion.value()) return 1;
}
return 0;
}
case SortField::Relevance:
// Relevance has no precomputed sort key — preserve original ordering.
return 0;
default:
WI_ASSERT(false);
return 0;
}
}

void SortEntries(
std::vector<SortablePackageEntry>& entries,
const SortParameters& sortParams)
{
if (entries.size() <= 1 || !sortParams.ShouldSort)
{
return;
}

std::stable_sort(entries.begin(), entries.end(),
[&sortParams](const SortablePackageEntry& a, const SortablePackageEntry& b)
{
for (const auto& field : sortParams.Fields)
{
int cmp = CompareByField(a, b, field);
if (cmp != 0)
{
return sortParams.Direction == SortDirection::Ascending ? (cmp < 0) : (cmp > 0);
}
}
return false;
});
}
}
Loading