Skip to content

Implement sort logic for winget list output #6191

Open
Madhusudhan-MSFT wants to merge 7 commits intomicrosoft:masterfrom
Madhusudhan-MSFT:user/masudars/list-sort-logic
Open

Implement sort logic for winget list output #6191
Madhusudhan-MSFT wants to merge 7 commits intomicrosoft:masterfrom
Madhusudhan-MSFT:user/masudars/list-sort-logic

Conversation

@Madhusudhan-MSFT
Copy link
Copy Markdown
Contributor

@Madhusudhan-MSFT Madhusudhan-MSFT commented Apr 29, 2026

Summary

Implements the sort logic for winget list output, completing the feature requested in #4238. This builds on PR #6177 (settings parsing + CLI argument handling) to add the actual sorting behavior.

Changes

Sort orchestration (WorkflowBase.cpp):

  • SortInstalledPackagesTableLines — resolves sort fields from CLI --sort args > settings output.sortOrder > default (no sort), determines direction, projects rows into SortablePackageEntry, sorts via std::stable_sort, and applies the permutation back.
  • Default relevance preservation: when the free-text query argument is present and no sort preference is configured in settings, relevance ordering is preserved. If the user has configured output.sortOrder, it is respected even with queries.

Sort helper (ListSortHelper.h/.cpp):

  • SortablePackageEntry — lightweight struct holding the sortable string fields.
  • CompareByField — case-insensitive ordinal comparison via FoldCase for a given SortField.
  • SortEntries — standalone stable_sort wrapper (used by unit tests).

CLI integration (ListCommand.cpp):

  • Registers --sort (repeatable, limit 7), --ascending/--asc, --descending/--desc arguments on the list command.

Argument validation (Command.cpp):

Documentation (list.md):

  • Adds --sort, --ascending, --descending to the options table.
  • New "Sorting output" section documenting CLI usage, settings configuration, resolution order, and relevance protection.

Design decisions

Decision Rationale
Default relevance preservation When the free-text query (-q) is used and no output.sortOrder is configured, results keep their relevance ranking. If the user has explicitly configured a sort preference in settings, it is respected even with queries — the user's explicit choice takes priority. Filter arguments (--id, --name, --tag etc.) don't affect relevance since they use exact/substring matching.
stable_sort Preserves relative order of equal elements, which is critical for multi-field sorting and relevance preservation (e.g., --sort source --sort relevance groups by source while maintaining relevance order within each group).
relevance as sort field Returns 0 from CompareByField (no-op), making it a placeholder for future match-score support. Currently serves as a way to preserve source-defined ordering in multi-field sorts.
Sort fields from settings ConvertToSortField (from PR #6177) converts string values to SortField enum. Invalid values are silently skipped with a warning logged.

How validated

Unit tests — 19 test cases (286 lines) in ListSort.cpp covering:

  • Single-field sorting (name, id, version, source, available) in both directions
  • Multi-field sorting (source→name, source→id)
  • Empty/single-element input (no-op edge cases)
  • Relevance field (no-op behavior, multi-field with relevance)
  • Case-insensitive comparison via FoldCase

Manual testing — 34 scenarios on wingetdev (Debug x64 deployed to AppX):

# Category Scenario Result
1-2 Defaults No sort = source order; query = relevance preserved
3-7 Single field --sort name/id/version/source/available
8-9 Direction --descending, --ascending (explicit)
10-11 Multi-field --sort source --sort name, --sort source --sort id
12-13 Query + sort Query with explicit --sort overrides relevance; without --sort preserves it
14-16 Edge cases Single result, no results, repeated sort fields
17-18 Source filter Sort with --source winget, --source devsource
19-21 Relevance --sort relevance (no-op), source→relevance (groups by source, relevance within), relevance→name (effectively alphabetical)
22-23 Settings sortOrder: ["name"], sortOrder: ["source","name"]
24 Settings + CLI Settings ["name"] + CLI --sort id → CLI wins
25 Settings + query Settings ["name"] + query → settings sort applied (user preference respected)
26-27 Settings relevance ["relevance"] (no-op), ["source","relevance"] (Trenly's scenario)
28 Invalid setting ["invalid_field"] → graceful fallback to no sort
29 Invalid CLI arg --sort foobar → clear error listing valid values
30 Direction exclusivity --ascending --descending → mutual exclusivity error
31 Settings + direction Settings ["source"] + CLI --descending → sort by source descending
32 No settings + query No settings configured + query → relevance preserved (default unchanged)
33 Settings + query + CLI Settings ["name"] + query + --sort id → CLI wins over settings
34 Filter args + settings --id filter + settings ["source","name"] → settings applied (not blocked)

Key behavior to note: When no sort preference is configured and a free-text query is used, relevance ordering is preserved by default. If the user explicitly configures output.sortOrder in settings, that preference is respected even with queries. Only the positional query argument (-q) has meaningful relevance — filter args like --id/--name don't block settings sort. The --ascending/--descending direction flags apply to whichever sort fields are active (CLI or settings).

Fixes #4238

Madhusudhan-MSFT and others added 5 commits April 28, 2026 16:18
…pp file

## Summary

Every other Workflow header in the project follows the declarations-only convention with
a matching .cpp file. ListSortHelper.h was the only outlier using inline implementations.
This refactor aligns it with the codebase convention and eliminates the duplicated sort
loop between ListSortHelper.h and WorkflowBase.cpp.

## Changes

- **ListSortHelper.h → declarations only**: Remove inline implementations of CompareByField
  and SortEntries, keeping only the struct definition and function signatures
- **ListSortHelper.cpp**: New compilation unit with the extracted implementations of
  CompareByField and SortEntries
- **WorkflowBase.cpp sort integration**: Replace the duplicated inline sort loop with an
  index-based permutation sort that projects InstalledPackagesTableLine into
  SortablePackageEntry and delegates field comparison to the shared CompareByField
- Remove the local CompareByField adapter that was creating temporary SortablePackageEntry
  copies per comparison call
- Align include path and grouping with repo conventions (bare sibling name, local before system)
- Simplify GetArgs access to match established MultiQueryFlow pattern

## Impact

- Sort logic now lives in a single shared location, reusable by future commands (search,
  upgrade, font list) without duplication
- Follows the established Workflows/ header convention (24/24 other headers use .h + .cpp)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add --sort, --ascending, and --descending options to the options table.
Add new 'Sorting output' section documenting:
- CLI sort arguments with multi-field examples
- Settings-based sortOrder configuration
- Resolution order (CLI > settings > default)
- Relevance protection behavior for queries
Validate --sort argument values in Command::ValidateArguments following the
same pattern as --scope and --authentication-mode validation. Invalid values
produce a clear error message listing valid options (name, id, version,
source, available, relevance). Centralized in Command.cpp so any future
command adding --sort gets validation automatically.
@github-actions

This comment has been minimized.

@Trenly
Copy link
Copy Markdown
Contributor

Trenly commented Apr 29, 2026

Is this only applicable to winget list or to any command that presents the user with a table of packages (upgrade, search, pin, etc.)

@Trenly
Copy link
Copy Markdown
Contributor

Trenly commented Apr 29, 2026

Key behavior to note: Settings sortOrder is intentionally NOT applied when query/filter arguments are present. Only explicit --sort CLI arguments can override relevance ordering. The --descending flag applies to whichever sort fields are active (CLI or settings).

This seems like it would be counter to what a user would want. If I did winget list Microsoft and my default sort order in settings is Name I'd expect both the filter and the sorting to work

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Madhusudhan-MSFT
Copy link
Copy Markdown
Contributor Author

Madhusudhan-MSFT commented Apr 29, 2026

Is this only applicable to winget list or to any command that presents the user with a table of packages (upgrade, search, pin, etc.)

The current implementation is scoped to winget list per the feature request in #4238. The sort infrastructure (schema, settings, argument definitions) was designed to be reusable, so extending to other table-outputting commands like upgrade or search would be straightforward if there's demand for it.

@Madhusudhan-MSFT
Copy link
Copy Markdown
Contributor Author

Madhusudhan-MSFT commented Apr 29, 2026

This seems like it would be counter to what a user would want. If I did winget list Microsoft and my default sort order in settings is Name I'd expect both the filter and the sorting to work

This was discussed and agreed upon during the initial design. The intent is to preserve source relevance ordering for queries since a settings-based sort could push the best match down without the user realizing why. Explicit --sort always works with queries (e.g., winget list Microsoft --sort name).

@denelon — I believe this aligns with what we discussed during the design phase, would you mind confirming?

@Trenly
Copy link
Copy Markdown
Contributor

Trenly commented Apr 29, 2026

if there's demand for it.

I think there is, but agree with the approach of limiting the scope for now and letting Demitrius figure out when to bring the rest in

@JohnMcPMS
Copy link
Copy Markdown
Member

This seems like it would be counter to what a user would want. If I did winget list Microsoft and my default sort order in settings is Name I'd expect both the filter and the sorting to work

This was discussed and agreed upon during the initial design. The intent is to preserve source relevance ordering for queries since a settings-based sort could push the best match down without the user realizing why. Explicit --sort always works with queries (e.g., winget list Microsoft --sort name).

@denelon — I believe this aligns with what we discussed during the design phase, would you mind confirming?

From the design portion, my feedback was that the default behavior should be to remain unchanged with a query but could have a new ordering without a query. If the user does supply a setting value, my expectation would be that it would be used with or without a query unless overridden with a command line argument.

What constitutes a query? Probably only the actual query argument, not id or other fields. Relevance would largely by the same for those already.

…edback

- Remove HasRelevanceAffectingArgs function (overly broad filter)
- Narrow relevance preservation to only the free-text query argument
- Settings output.sortOrder now applies even when query is present
  (user explicitly configured sort preference takes priority)
- Filter args (--id, --name, --tag etc.) no longer block settings sort
  since they use exact/substring matching with no meaningful relevance
- Update list.md documentation to reflect new behavior
- Resolution: CLI --sort > settings (always) > default relevance (query only)
@Madhusudhan-MSFT Madhusudhan-MSFT changed the title Implement sort logic for winget list output Implement sort logic for winget list output Apr 29, 2026
@Madhusudhan-MSFT
Copy link
Copy Markdown
Contributor Author

From the design portion, my feedback was that the default behavior should be to remain unchanged with a query but could have a new ordering without a query. If the user does supply a setting value, my expectation would be that it would be used with or without a query unless overridden with a command line argument.

What constitutes a query? Probably only the actual query argument, not id or other fields. Relevance would largely by the same for those already.

Thanks for the clarification, John. Updated in 70bd157e:

  • Default unchanged — without output.sortOrder configured, positional query preserves relevance as before
  • Settings always honored — if user configures output.sortOrder, it applies regardless of arguments (user explicitly opted in)
  • "Query" narrowed — only the positional query argument (not --id/--name/--tag) is considered relevance-affecting in the no-settings case

Removed HasRelevanceAffectingArgs in favor of sortFields.empty() && Args.Contains(Query).

@Madhusudhan-MSFT Madhusudhan-MSFT marked this pull request as ready for review April 29, 2026 23:42
@Madhusudhan-MSFT Madhusudhan-MSFT requested a review from a team as a code owner April 29, 2026 23:42

## Sorting output

By default, results are displayed in the order returned by the package source (no sorting applied). You can control sorting through command-line arguments or user settings.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This depends on whether there is a query or not, correct?

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 },
// 7 = one more than the number of user-facing SortField values (Relevance, Name, Id, Version, Source, Available),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If there is going to be a limit, it should be the number of options. A test that provides all of them can detect a failure to update that limit if more are added.

namespace AppInstaller::CLI::Workflow
{
// Lightweight sortable representation of a package line in list output.
// Decoupled from ICompositePackage/IPackageVersion to enable unit testing.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
// Decoupled from ICompositePackage/IPackageVersion to enable unit testing.
// Decoupled from ICompositePackage/IPackageVersion to ease unit testing.

Claiming that interfaces make it impossible to unit test is a bit untrue.

switch (field)
{
case SortField::Name:
return Utility::FoldCase(std::string_view{ a.Name.get() }).compare(Utility::FoldCase(std::string_view{ b.Name.get() }));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Operations like FoldCase are expensive, the SortablePackageEntry should contain precomputed comparable values for all fields rather than computing them every single time.

}
}

// Returns true if the execution context contains filter arguments that
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Doesn't appear to return a value.

// have equivalent relevance.
// If the user explicitly configured output.sortOrder, respect it even
// with queries — that is an explicit user preference.
if (sortFields.empty() && context.Args.Contains(Execution::Args::Type::Query))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

When sortFields is empty but no query is provided, shouldn't we be setting the actual default sort fields here?

sortable.reserve(lines.size());
for (const auto& line : lines)
{
sortable.push_back({ line.Name, line.Id, line.InstalledVersion, line.AvailableVersion, line.Source });
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

From another comment, we actually want to construct all of the comparable properties, not just copy. Have a constructor that does so.

Additionally, we could optimize by only constructing the properties that we would actually compare based on the sortFields. If the enum values were converted to flag bits, it could be efficiently checked.

Comment on lines +541 to +542
std::vector<size_t> indices(lines.size());
std::iota(indices.begin(), indices.end(), 0);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why not have the entries just contain their related index? Then the separated sort function makes more sense (and can easily be called from this one).

#include "WorkflowBase.h"
#include "ExecutionContext.h"
#include <winget/ManifestComparator.h>
#include "ListSortHelper.h"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
#include "ListSortHelper.h"
#include "PackageTableSortHelper.h"

Eventually not used exclusively for list.

// produce relevance-ordered results (query, name, id, moniker, tag, command).
// Sorts a vector of InstalledPackagesTableLine according to the user's sort preferences.
// Resolution order: CLI args (--sort) > settings (output.sortOrder) > query-aware default.
void SortInstalledPackagesTableLines(Execution::Context& context, std::vector<InstalledPackagesTableLine>& lines)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This function should absolutely be in the sort helper, templated. And then it should be refactored to extract the sort fields and order determination out from the only part that needs to be templated: conversion from input vector to sortables, then sorting of the input vector via the indices.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enable sorting the output of WinGet List

3 participants