Add sort flags to list/ls: -t (time), -U (unsorted so by name), -S (size), -i (ID), -r (reversed)
This commit is contained in:
76
cmd/cmd.go
76
cmd/cmd.go
@@ -49,6 +49,20 @@ import (
|
|||||||
|
|
||||||
const ConnectInstructions = "To sign in, navigate to:\n %s\n\n"
|
const ConnectInstructions = "To sign in, navigate to:\n %s\n\n"
|
||||||
|
|
||||||
|
type ListSortOrder int
|
||||||
|
const (
|
||||||
|
ListByTime ListSortOrder = iota // -t
|
||||||
|
ListBySize // -S
|
||||||
|
ListByName // -U
|
||||||
|
ListByID // -i
|
||||||
|
)
|
||||||
|
var listSortByTime bool
|
||||||
|
var listSortBySize bool
|
||||||
|
var listSortByName bool
|
||||||
|
var listSortByID bool
|
||||||
|
var listSortReverse bool
|
||||||
|
var listSortGrouped bool
|
||||||
|
|
||||||
// ensureThinkingSupport emits a warning if the model does not advertise thinking support
|
// ensureThinkingSupport emits a warning if the model does not advertise thinking support
|
||||||
func ensureThinkingSupport(ctx context.Context, client *api.Client, name string) {
|
func ensureThinkingSupport(ctx context.Context, client *api.Client, name string) {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
@@ -702,9 +716,11 @@ func ListHandler(cmd *cobra.Command, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sorted := sortListModels(models)
|
||||||
|
|
||||||
var data [][]string
|
var data [][]string
|
||||||
|
|
||||||
for _, m := range models.Models {
|
for _, m := range sorted {
|
||||||
if len(args) == 0 || strings.HasPrefix(strings.ToLower(m.Name), strings.ToLower(args[0])) {
|
if len(args) == 0 || strings.HasPrefix(strings.ToLower(m.Name), strings.ToLower(args[0])) {
|
||||||
var size string
|
var size string
|
||||||
if m.RemoteModel != "" {
|
if m.RemoteModel != "" {
|
||||||
@@ -731,6 +747,58 @@ func ListHandler(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sortListModels(models *api.ListResponse) []*api.ListModelResponse {
|
||||||
|
sorted := make([]*api.ListModelResponse, len(models.Models))
|
||||||
|
for i := 0; i < len(models.Models); i++ {
|
||||||
|
sorted[i] = &models.Models[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
var sortFn func(i, j int) bool
|
||||||
|
switch {
|
||||||
|
case listSortByName:
|
||||||
|
sortFn = func(i, j int) bool {
|
||||||
|
return sorted[i].Name < sorted[j].Name
|
||||||
|
}
|
||||||
|
case listSortBySize:
|
||||||
|
sortFn = func(i, j int) bool {
|
||||||
|
return sorted[i].Size < sorted[j].Size
|
||||||
|
}
|
||||||
|
case listSortByID:
|
||||||
|
sortFn = func(i, j int) bool {
|
||||||
|
return sorted[i].Digest < sorted[j].Digest
|
||||||
|
}
|
||||||
|
case listSortByTime:
|
||||||
|
listSortReverse = !listSortReverse
|
||||||
|
}
|
||||||
|
|
||||||
|
if sortFn != nil {
|
||||||
|
sort.Slice(sorted, sortFn)
|
||||||
|
}
|
||||||
|
if listSortReverse {
|
||||||
|
slices.Reverse(sorted)
|
||||||
|
}
|
||||||
|
if listSortGrouped {
|
||||||
|
groups := make([]string, 0, len(sorted))
|
||||||
|
grouped := make(map[string][]*api.ListModelResponse)
|
||||||
|
for _, model := range sorted {
|
||||||
|
group := strings.Split(model.Name, ":")[0]
|
||||||
|
ms, ok := grouped[group]
|
||||||
|
if !ok {
|
||||||
|
groups = append(groups, group)
|
||||||
|
}
|
||||||
|
grouped[group] = append(ms, model)
|
||||||
|
}
|
||||||
|
i := 0
|
||||||
|
for _, group := range groups {
|
||||||
|
for _, model := range grouped[group] {
|
||||||
|
sorted[i] = model
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sorted
|
||||||
|
}
|
||||||
|
|
||||||
func ListRunningHandler(cmd *cobra.Command, args []string) error {
|
func ListRunningHandler(cmd *cobra.Command, args []string) error {
|
||||||
client, err := api.ClientFromEnvironment()
|
client, err := api.ClientFromEnvironment()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1811,6 +1879,12 @@ func NewCLI() *cobra.Command {
|
|||||||
PreRunE: checkServerHeartbeat,
|
PreRunE: checkServerHeartbeat,
|
||||||
RunE: ListHandler,
|
RunE: ListHandler,
|
||||||
}
|
}
|
||||||
|
listCmd.Flags().BoolVarP(&listSortByTime, "time", "t", false, "Sort by date/time, chronologically (default reversed).")
|
||||||
|
listCmd.Flags().BoolVarP(&listSortBySize, "size", "S", false, "Sort by file size, smallest first.")
|
||||||
|
listCmd.Flags().BoolVarP(&listSortByName, "name", "U", false, "Sort by name in alphabetical order.")
|
||||||
|
listCmd.Flags().BoolVarP(&listSortByID, "id", "i", false, "Sort by ID in alphabetical order.")
|
||||||
|
listCmd.Flags().BoolVarP(&listSortReverse, "reverse", "r", false, "Reverse the sort order.")
|
||||||
|
listCmd.Flags().BoolVarP(&listSortGrouped, "grouped", "g", false, "Group models.")
|
||||||
|
|
||||||
psCmd := &cobra.Command{
|
psCmd := &cobra.Command{
|
||||||
Use: "ps",
|
Use: "ps",
|
||||||
|
|||||||
135
cmd/cmd_test.go
135
cmd/cmd_test.go
@@ -9,6 +9,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -962,6 +963,93 @@ func TestListHandler(t *testing.T) {
|
|||||||
expectedOutput: "NAME ID SIZE MODIFIED \n" +
|
expectedOutput: "NAME ID SIZE MODIFIED \n" +
|
||||||
"model1 sha256:abc12 1.0 KB 24 hours ago \n",
|
"model1 sha256:abc12 1.0 KB 24 hours ago \n",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "list all models ordered by name",
|
||||||
|
args: []string{"--name"},
|
||||||
|
serverResponse: []api.ListModelResponse{
|
||||||
|
{Name: "model1", Digest: "sha256:abc123", Size: 1024, ModifiedAt: time.Now().Add(-24 * time.Hour)},
|
||||||
|
{Name: "model2", Digest: "sha256:def456", Size: 2048, ModifiedAt: time.Now().Add(-48 * time.Hour)},
|
||||||
|
{Name: "model0", Digest: "sha256:bcd789", Size: 1536, ModifiedAt: time.Now().Add(-72 * time.Hour)},
|
||||||
|
},
|
||||||
|
expectedOutput: "NAME ID SIZE MODIFIED \n" +
|
||||||
|
"model0 sha256:bcd78 1.5 KB 3 days ago \n" +
|
||||||
|
"model1 sha256:abc12 1.0 KB 24 hours ago \n" +
|
||||||
|
"model2 sha256:def45 2.0 KB 2 days ago \n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list all models ordered by ID",
|
||||||
|
args: []string{"--id"},
|
||||||
|
serverResponse: []api.ListModelResponse{
|
||||||
|
{Name: "model1", Digest: "sha256:abc123", Size: 1024, ModifiedAt: time.Now().Add(-24 * time.Hour)},
|
||||||
|
{Name: "model2", Digest: "sha256:def456", Size: 2048, ModifiedAt: time.Now().Add(-48 * time.Hour)},
|
||||||
|
{Name: "model0", Digest: "sha256:bcd789", Size: 1536, ModifiedAt: time.Now().Add(-72 * time.Hour)},
|
||||||
|
},
|
||||||
|
expectedOutput: "NAME ID SIZE MODIFIED \n" +
|
||||||
|
"model1 sha256:abc12 1.0 KB 24 hours ago \n" +
|
||||||
|
"model0 sha256:bcd78 1.5 KB 3 days ago \n" +
|
||||||
|
"model2 sha256:def45 2.0 KB 2 days ago \n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list all models ordered by size",
|
||||||
|
args: []string{"--size"},
|
||||||
|
serverResponse: []api.ListModelResponse{
|
||||||
|
{Name: "model1", Digest: "sha256:abc123", Size: 1024, ModifiedAt: time.Now().Add(-24 * time.Hour)},
|
||||||
|
{Name: "model2", Digest: "sha256:def456", Size: 2048, ModifiedAt: time.Now().Add(-48 * time.Hour)},
|
||||||
|
{Name: "model0", Digest: "sha256:bcd789", Size: 1536, ModifiedAt: time.Now().Add(-72 * time.Hour)},
|
||||||
|
},
|
||||||
|
expectedOutput: "NAME ID SIZE MODIFIED \n" +
|
||||||
|
"model1 sha256:abc12 1.0 KB 24 hours ago \n" +
|
||||||
|
"model0 sha256:bcd78 1.5 KB 3 days ago \n" +
|
||||||
|
"model2 sha256:def45 2.0 KB 2 days ago \n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list all models reversed",
|
||||||
|
args: []string{"--reverse"},
|
||||||
|
serverResponse: []api.ListModelResponse{
|
||||||
|
{Name: "model1", Digest: "sha256:abc123", Size: 1024, ModifiedAt: time.Now().Add(-24 * time.Hour)},
|
||||||
|
{Name: "model2", Digest: "sha256:def456", Size: 2048, ModifiedAt: time.Now().Add(-48 * time.Hour)},
|
||||||
|
{Name: "model0", Digest: "sha256:bcd789", Size: 1536, ModifiedAt: time.Now().Add(-72 * time.Hour)},
|
||||||
|
},
|
||||||
|
expectedOutput: "NAME ID SIZE MODIFIED \n" +
|
||||||
|
"model0 sha256:bcd78 1.5 KB 3 days ago \n" +
|
||||||
|
"model2 sha256:def45 2.0 KB 2 days ago \n" +
|
||||||
|
"model1 sha256:abc12 1.0 KB 24 hours ago \n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list all models grouped",
|
||||||
|
args: []string{"--grouped"},
|
||||||
|
serverResponse: []api.ListModelResponse{
|
||||||
|
{Name: "b:model4", Digest: "sha256:cde357", Size: 3072, ModifiedAt: time.Now().Add(-12 * time.Hour)},
|
||||||
|
{Name: "a:model1", Digest: "sha256:abc123", Size: 1024, ModifiedAt: time.Now().Add(-24 * time.Hour)},
|
||||||
|
{Name: "a:model2", Digest: "sha256:def456", Size: 2048, ModifiedAt: time.Now().Add(-48 * time.Hour)},
|
||||||
|
{Name: "b:model0", Digest: "sha256:bcd789", Size: 1536, ModifiedAt: time.Now().Add(-72 * time.Hour)},
|
||||||
|
},
|
||||||
|
expectedOutput: "NAME ID SIZE MODIFIED \n" +
|
||||||
|
"b:model4 sha256:cde35 3.1 KB 12 hours ago \n" +
|
||||||
|
"b:model0 sha256:bcd78 1.5 KB 3 days ago \n" +
|
||||||
|
"a:model1 sha256:abc12 1.0 KB 24 hours ago \n" +
|
||||||
|
"a:model2 sha256:def45 2.0 KB 2 days ago \n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list help",
|
||||||
|
args: []string{"--help"},
|
||||||
|
expectedOutput: "Usage:\n" +
|
||||||
|
" ollama list [flags]\n" +
|
||||||
|
"\n" +
|
||||||
|
"Aliases:\n" +
|
||||||
|
" list, ls\n" +
|
||||||
|
"\n" +
|
||||||
|
"Flags:\n" +
|
||||||
|
" -g, --grouped Group models.\n" +
|
||||||
|
" -i, --id Sort by ID in alphabetical order.\n" +
|
||||||
|
" -U, --name Sort by name in alphabetical order.\n" +
|
||||||
|
" -r, --reverse Reverse the sort order.\n" +
|
||||||
|
" -S, --size Sort by file size, smallest first.\n" +
|
||||||
|
" -t, --time Sort by date/time, chronologically (default reversed).\n" +
|
||||||
|
"\n" +
|
||||||
|
"Environment Variables:\n" +
|
||||||
|
" OLLAMA_HOST IP Address for the ollama server (default 127.0.0.1:11434)\n",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "server error",
|
name: "server error",
|
||||||
args: []string{},
|
args: []string{},
|
||||||
@@ -992,20 +1080,47 @@ func TestListHandler(t *testing.T) {
|
|||||||
|
|
||||||
t.Setenv("OLLAMA_HOST", mockServer.URL)
|
t.Setenv("OLLAMA_HOST", mockServer.URL)
|
||||||
|
|
||||||
cmd := &cobra.Command{}
|
cmd := func() *cobra.Command {
|
||||||
|
cli := NewCLI()
|
||||||
|
listCmd, _, err := cli.Find([]string{"list"})
|
||||||
|
if err != nil || listCmd == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return listCmd
|
||||||
|
}()
|
||||||
|
if cmd == nil {
|
||||||
|
t.Fatal("list command not found - did cmd package change?")
|
||||||
|
}
|
||||||
|
isHelp := slices.Equal(tt.args, []string{"--help"})
|
||||||
|
var args []string
|
||||||
|
if !isHelp {
|
||||||
|
if err := cmd.ParseFlags(tt.args); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
args = cmd.Flags().Args()
|
||||||
|
}
|
||||||
cmd.SetContext(t.Context())
|
cmd.SetContext(t.Context())
|
||||||
|
|
||||||
// Capture stdout
|
var output []byte
|
||||||
oldStdout := os.Stdout
|
var err error
|
||||||
r, w, _ := os.Pipe()
|
if isHelp {
|
||||||
os.Stdout = w
|
var buf bytes.Buffer
|
||||||
|
cmd.SetOut(&buf)
|
||||||
|
err = cmd.Usage()
|
||||||
|
output = buf.Bytes()
|
||||||
|
} else {
|
||||||
|
// Capture stdout
|
||||||
|
oldStdout := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
err := ListHandler(cmd, tt.args)
|
err = ListHandler(cmd, args)
|
||||||
|
|
||||||
// Restore stdout and get output
|
// Restore stdout and get output
|
||||||
w.Close()
|
w.Close()
|
||||||
os.Stdout = oldStdout
|
os.Stdout = oldStdout
|
||||||
output, _ := io.ReadAll(r)
|
output, _ = io.ReadAll(r)
|
||||||
|
}
|
||||||
|
|
||||||
if tt.expectedError == "" {
|
if tt.expectedError == "" {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user