[DEPRECATED] Go implementation of plcbundle

cmd ls

+293
+292
cmd/plcbundle/commands/ls.go
··· 1 + package commands 2 + 3 + import ( 4 + "fmt" 5 + "strings" 6 + "time" 7 + 8 + "github.com/spf13/cobra" 9 + "tangled.org/atscan.net/plcbundle/internal/bundleindex" 10 + ) 11 + 12 + func NewLsCommand() *cobra.Command { 13 + var ( 14 + last int 15 + reverse bool 16 + format string 17 + noHeader bool 18 + separator string 19 + ) 20 + 21 + cmd := &cobra.Command{ 22 + Use: "ls [flags]", 23 + Short: "List bundles (machine-readable)", 24 + Long: `List bundles in machine-readable format 25 + 26 + Outputs bundle information in a clean, parseable format suitable for 27 + piping to sed, awk, grep, or other text processing tools. 28 + 29 + No colors, no pager - just consistent, tab-separated data perfect for 30 + shell scripts and automation.`, 31 + 32 + Example: ` # List all bundles 33 + plcbundle ls 34 + 35 + # Last 10 bundles 36 + plcbundle ls -n 10 37 + 38 + # Oldest first 39 + plcbundle ls --reverse 40 + 41 + # Custom format 42 + plcbundle ls --format "bundle,hash,date,size" 43 + 44 + # CSV format 45 + plcbundle ls --separator "," 46 + 47 + # Scripting examples 48 + plcbundle ls | awk '{print $1}' # Just bundle numbers 49 + plcbundle ls | grep 000150 # Find specific bundle 50 + plcbundle ls -n 5 | cut -f1,4 # First and 4th columns 51 + plcbundle ls --format bundle,hash # Custom columns 52 + plcbundle ls --separator "," > bundles.csv # Export to CSV`, 53 + 54 + RunE: func(cmd *cobra.Command, args []string) error { 55 + mgr, _, err := getManagerFromCommand(cmd, "") 56 + if err != nil { 57 + return err 58 + } 59 + defer mgr.Close() 60 + 61 + return listBundles(mgr, lsOptions{ 62 + last: last, 63 + reverse: reverse, 64 + format: format, 65 + noHeader: noHeader, 66 + separator: separator, 67 + }) 68 + }, 69 + } 70 + 71 + // Flags 72 + cmd.Flags().IntVarP(&last, "last", "n", 0, "Show only last N bundles (0 = all)") 73 + cmd.Flags().BoolVar(&reverse, "reverse", false, "Show oldest first") 74 + cmd.Flags().StringVar(&format, "format", "bundle,hash,date,ops,dids,size", 75 + "Output format: bundle,hash,date,ops,dids,size,uncompressed,ratio,timespan") 76 + cmd.Flags().BoolVar(&noHeader, "no-header", false, "Omit header row") 77 + cmd.Flags().StringVar(&separator, "separator", "\t", "Field separator (default: tab)") 78 + 79 + return cmd 80 + } 81 + 82 + type lsOptions struct { 83 + last int 84 + reverse bool 85 + format string 86 + noHeader bool 87 + separator string 88 + } 89 + 90 + func listBundles(mgr BundleManager, opts lsOptions) error { 91 + index := mgr.GetIndex() 92 + bundles := index.GetBundles() 93 + 94 + if len(bundles) == 0 { 95 + return nil 96 + } 97 + 98 + // Apply limit 99 + displayBundles := bundles 100 + if opts.last > 0 && opts.last < len(bundles) { 101 + displayBundles = bundles[len(bundles)-opts.last:] 102 + } 103 + 104 + // Reverse if not --reverse (default is newest first, like log) 105 + if !opts.reverse { 106 + reversed := make([]*bundleindex.BundleMetadata, len(displayBundles)) 107 + for i, b := range displayBundles { 108 + reversed[len(displayBundles)-1-i] = b 109 + } 110 + displayBundles = reversed 111 + } 112 + 113 + // Parse format string 114 + fields := parseFormatString(opts.format) 115 + 116 + // Print header (unless disabled) 117 + if !opts.noHeader { 118 + printHeader(fields, opts.separator) 119 + } 120 + 121 + // Print each bundle 122 + for _, meta := range displayBundles { 123 + printBundleFields(meta, fields, opts.separator) 124 + } 125 + 126 + return nil 127 + } 128 + 129 + // parseFormatString parses comma-separated field names 130 + func parseFormatString(format string) []string { 131 + parts := strings.Split(format, ",") 132 + fields := make([]string, 0, len(parts)) 133 + for _, p := range parts { 134 + field := strings.TrimSpace(p) 135 + if field != "" { 136 + fields = append(fields, field) 137 + } 138 + } 139 + return fields 140 + } 141 + 142 + // printHeader prints the header row 143 + func printHeader(fields []string, sep string) { 144 + headers := make([]string, len(fields)) 145 + for i, field := range fields { 146 + headers[i] = getFieldHeader(field) 147 + } 148 + fmt.Println(strings.Join(headers, sep)) 149 + } 150 + 151 + // getFieldHeader returns the header name for a field 152 + func getFieldHeader(field string) string { 153 + switch field { 154 + case "bundle": 155 + return "bundle" 156 + case "hash": 157 + return "hash" 158 + case "content": 159 + return "content_hash" 160 + case "parent": 161 + return "parent_hash" 162 + case "date", "time": 163 + return "date" 164 + case "age": 165 + return "age" 166 + case "ops", "operations": 167 + return "ops" 168 + case "dids": 169 + return "dids" 170 + case "size", "compressed": 171 + return "size" 172 + case "uncompressed": 173 + return "uncompressed" 174 + case "ratio": 175 + return "ratio" 176 + case "timespan", "duration": 177 + return "timespan" 178 + case "start": 179 + return "start_time" 180 + case "end": 181 + return "end_time" 182 + default: 183 + return field 184 + } 185 + } 186 + 187 + // printBundleFields prints a bundle's fields 188 + func printBundleFields(meta *bundleindex.BundleMetadata, fields []string, sep string) { 189 + values := make([]string, len(fields)) 190 + 191 + for i, field := range fields { 192 + values[i] = getFieldValue(meta, field) 193 + } 194 + 195 + fmt.Println(strings.Join(values, sep)) 196 + } 197 + 198 + // getFieldValue returns the value for a specific field 199 + func getFieldValue(meta *bundleindex.BundleMetadata, field string) string { 200 + switch field { 201 + case "bundle": 202 + return fmt.Sprintf("%06d", meta.BundleNumber) 203 + 204 + case "hash": 205 + return meta.Hash 206 + 207 + case "hash_short": 208 + if len(meta.Hash) >= 12 { 209 + return meta.Hash[:12] 210 + } 211 + return meta.Hash 212 + 213 + case "content": 214 + return meta.ContentHash 215 + 216 + case "content_short": 217 + if len(meta.ContentHash) >= 12 { 218 + return meta.ContentHash[:12] 219 + } 220 + return meta.ContentHash 221 + 222 + case "parent": 223 + return meta.Parent 224 + 225 + case "parent_short": 226 + if len(meta.Parent) >= 12 { 227 + return meta.Parent[:12] 228 + } 229 + return meta.Parent 230 + 231 + case "date", "time": 232 + return meta.EndTime.Format("2006-01-02T15:04:05Z") 233 + 234 + case "date_short": 235 + return meta.EndTime.Format("2006-01-02") 236 + 237 + case "timestamp", "unix": 238 + return fmt.Sprintf("%d", meta.EndTime.Unix()) 239 + 240 + case "age": 241 + age := time.Since(meta.EndTime) 242 + return formatDurationShort(age) 243 + 244 + case "age_seconds": 245 + return fmt.Sprintf("%.0f", time.Since(meta.EndTime).Seconds()) 246 + 247 + case "ops", "operations": 248 + return fmt.Sprintf("%d", meta.OperationCount) 249 + 250 + case "dids": 251 + return fmt.Sprintf("%d", meta.DIDCount) 252 + 253 + case "size", "compressed": 254 + return fmt.Sprintf("%d", meta.CompressedSize) 255 + 256 + case "size_mb": 257 + return fmt.Sprintf("%.2f", float64(meta.CompressedSize)/(1024*1024)) 258 + 259 + case "uncompressed": 260 + return fmt.Sprintf("%d", meta.UncompressedSize) 261 + 262 + case "uncompressed_mb": 263 + return fmt.Sprintf("%.2f", float64(meta.UncompressedSize)/(1024*1024)) 264 + 265 + case "ratio": 266 + if meta.CompressedSize > 0 { 267 + ratio := float64(meta.UncompressedSize) / float64(meta.CompressedSize) 268 + return fmt.Sprintf("%.2f", ratio) 269 + } 270 + return "0" 271 + 272 + case "timespan", "duration": 273 + duration := meta.EndTime.Sub(meta.StartTime) 274 + return formatDurationShort(duration) 275 + 276 + case "timespan_seconds": 277 + duration := meta.EndTime.Sub(meta.StartTime) 278 + return fmt.Sprintf("%.0f", duration.Seconds()) 279 + 280 + case "start": 281 + return meta.StartTime.Format("2006-01-02T15:04:05Z") 282 + 283 + case "end": 284 + return meta.EndTime.Format("2006-01-02T15:04:05Z") 285 + 286 + case "created": 287 + return meta.CreatedAt.Format("2006-01-02T15:04:05Z") 288 + 289 + default: 290 + return "" 291 + } 292 + }
+1
cmd/plcbundle/main.go
··· 55 55 // Status & info (root level) 56 56 cmd.AddCommand(commands.NewStatusCommand()) 57 57 cmd.AddCommand(commands.NewLogCommand()) 58 + cmd.AddCommand(commands.NewLsCommand()) 58 59 //cmd.AddCommand(commands.NewGapsCommand()) 59 60 cmd.AddCommand(commands.NewVerifyCommand()) 60 61 cmd.AddCommand(commands.NewDiffCommand())