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