Skip to content

Commit 170a935

Browse files
Merge pull request #407 from viveksahu26/refactor/limit-to-one-feature
limit to single feature and support list autocompletion for list cmd
2 parents cab012d + 3b0c9a2 commit 170a935

File tree

3 files changed

+159
-69
lines changed

3 files changed

+159
-69
lines changed

cmd/list.go

Lines changed: 93 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ import (
2929
// userListCmd holds the configuration for the list command
3030
type userListCmd struct {
3131
// Input control
32-
path []string
32+
path string
3333

3434
// Filter control
35-
features []string
36-
missing bool
35+
feature string
36+
missing bool
3737

3838
// Output control
3939
basic bool
@@ -48,42 +48,39 @@ type userListCmd struct {
4848
// listCmd lists components or SBOM properties based on specified features
4949
var listCmd = &cobra.Command{
5050
Use: "list",
51-
Short: "List components or SBOM properties based on features",
51+
Short: "List components or SBOM properties based on feature",
5252
SilenceUsage: true,
53-
Example: ` sbomqs list --features <features> --option <path-to-sbom-file>
53+
Example: ` sbomqs list --feature <feature> --option <path-to-sbom-file>
5454
5555
# List all components with suppliers
56-
sbomqs list --features comp_with_supplier samples/sbomqs-spdx-syft.json
56+
sbomqs list --feature comp_with_supplier samples/sbomqs-spdx-syft.json
5757
5858
# List all components missing suppliers
59-
sbomqs list --features comp_with_supplier --missing samples/sbomqs-spdx-syft.json
59+
sbomqs list --feature comp_with_supplier --missing samples/sbomqs-spdx-syft.json
6060
6161
# List all components with valid licenses
62-
sbomqs list --features comp_valid_licenses samples/sbomqs-spdx-syft.json
62+
sbomqs list --feature comp_valid_licenses samples/sbomqs-spdx-syft.json
6363
6464
# List all components with invalid licenses
65-
sbomqs list --features comp_valid_licenses --missing samples/sbomqs-spdx-syft.json
65+
sbomqs list --feature comp_valid_licenses --missing samples/sbomqs-spdx-syft.json
6666
67-
# List all components of SBOM with comp_with_licenses as well as comp_with_version
68-
sbomqs list --features="comp_with_licenses,comp_with_version" samples/photon.spdx.json
69-
70-
# List all components for both SBOM with comp_with_licenses as well as comp_with_version
71-
sbomqs list --features="comp_with_licenses,comp_with_version" samples/photon.spdx.json samples/sbomqs-cdx-cgomod.json
72-
73-
# component features:
67+
# Component features:
7468
[comp_with_name, comp_with_version, comp_with_supplier, comp_with_uniq_ids, comp_valid_licenses, comp_with_any_vuln_lookup_id,
7569
comp_with_deprecated_licenses, comp_with_multi_vuln_lookup_id, comp_with_primary_purpose, comp_with_restrictive_licenses,
7670
comp_with_checksums, comp_with_licenses]
7771
78-
# sbom features:
72+
# SBOM features:
7973
[sbom_creation_timestamp, sbom_authors, sbom_with_creator_and_version, sbom_with_primary_component, sbom_dependencies,
80-
sbom_sharable, sbom_parsable, sbom_spec, sbom_spec_file_format, sbom_spec_version ]
74+
sbom_sharable, sbom_parsable, sbom_spec, sbom_spec_file_format, sbom_spec_version]
8175
`,
8276

8377
Args: func(_ *cobra.Command, args []string) error {
8478
if len(args) < 1 {
8579
return fmt.Errorf("requires a path to an SBOM file or directory of SBOM files")
8680
}
81+
if len(args) > 1 {
82+
return fmt.Errorf("only one file path is allowed, got %d: %v", len(args), args)
83+
}
8784
return nil
8885
},
8986
RunE: func(cmd *cobra.Command, args []string) error {
@@ -97,7 +94,6 @@ var listCmd = &cobra.Command{
9794
ctx := logger.WithLogger(context.Background())
9895
uCmd := parseListParams(cmd, args)
9996
if err := validateparsedListCmd(uCmd); err != nil {
100-
logger.FromContext(ctx).Errorf("Invalid command parameters: %v", err)
10197
return err
10298
}
10399

@@ -112,12 +108,11 @@ func parseListParams(cmd *cobra.Command, args []string) *userListCmd {
112108
uCmd := &userListCmd{}
113109

114110
// Input control
115-
uCmd.path = args
111+
uCmd.path = args[0]
116112

117113
// Filter control
118-
feature, _ := cmd.Flags().GetString("features")
119-
features := strings.Split(feature, ",")
120-
uCmd.features = features
114+
feature, _ := cmd.Flags().GetString("feature")
115+
uCmd.feature = feature
121116

122117
missing, _ := cmd.Flags().GetBool("missing")
123118
uCmd.missing = missing
@@ -130,7 +125,6 @@ func parseListParams(cmd *cobra.Command, args []string) *userListCmd {
130125
uCmd.json = json
131126

132127
detailed, _ := cmd.Flags().GetBool("detailed")
133-
134128
uCmd.detailed = detailed
135129

136130
color, _ := cmd.Flags().GetBool("color")
@@ -145,8 +139,8 @@ func parseListParams(cmd *cobra.Command, args []string) *userListCmd {
145139

146140
func fromListToEngineParams(uCmd *userListCmd) *engine.Params {
147141
return &engine.Params{
148-
Path: uCmd.path,
149-
Features: uCmd.features,
142+
Path: []string{uCmd.path},
143+
Features: []string{uCmd.feature},
150144
Missing: uCmd.missing,
151145
Basic: uCmd.basic,
152146
JSON: uCmd.json,
@@ -160,54 +154,95 @@ func init() {
160154
rootCmd.AddCommand(listCmd)
161155

162156
// Filter Control
163-
listCmd.Flags().StringP("features", "f", "", "filter by feature (e.g. 'sbom_authors', 'comp_with_name', 'sbom_creation_timestamp') ")
164-
err := listCmd.MarkFlagRequired("features")
157+
listCmd.Flags().String("feature", "", "Filter by feature (e.g., 'sbom_authors', 'comp_with_name', 'sbom_creation_timestamp'); if repeated, last value is used")
158+
err := listCmd.MarkFlagRequired("feature")
165159
if err != nil {
166160
log.Fatal(err)
167161
}
168-
listCmd.Flags().BoolP("missing", "m", false, "list components or properties missing the specified feature")
162+
listCmd.Flags().BoolP("missing", "m", false, "List components or properties missing the specified feature")
169163

170164
// Output Control
171-
listCmd.Flags().BoolP("basic", "b", false, "results in single-line format")
172-
listCmd.Flags().BoolP("json", "j", false, "results in json")
173-
listCmd.Flags().BoolP("detailed", "d", true, "results in table format, default")
174-
listCmd.Flags().BoolP("color", "l", false, "output in colorful")
165+
listCmd.Flags().BoolP("basic", "b", false, "Results in single-line format")
166+
listCmd.Flags().BoolP("json", "j", false, "Results in JSON")
167+
listCmd.Flags().BoolP("detailed", "d", true, "Results in table format, default")
168+
listCmd.Flags().BoolP("color", "l", false, "Output in color")
175169

176170
// Debug Control
177-
listCmd.Flags().BoolP("debug", "D", false, "enable debug logging")
171+
listCmd.Flags().BoolP("debug", "D", false, "Enable debug logging")
172+
173+
// Register flag completion for --feature
174+
err = listCmd.RegisterFlagCompletionFunc("feature", func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
175+
var completions []string
176+
for feature := range isFeaturePresent {
177+
if strings.HasPrefix(feature, toComplete) {
178+
completions = append(completions, feature)
179+
}
180+
}
181+
return completions, cobra.ShellCompDirectiveNoFileComp
182+
})
183+
if err != nil {
184+
log.Fatalf("Failed to register flag completion for --feature: %v", err)
185+
}
178186
}
179187

180188
func validateparsedListCmd(uCmd *userListCmd) error {
189+
// Check path
181190
if len(uCmd.path) <= 0 {
182-
fmt.Println("Error: path is required")
183191
return errors.New("path is required")
184192
}
185193

186-
if len(uCmd.features) == 0 {
187-
fmt.Println("Error: feature is required")
188-
log.Fatal("at least one feature must be specified")
194+
// Validate feature
195+
feature := uCmd.feature
196+
if feature == "" {
197+
return errors.New("feature is required")
198+
}
199+
200+
// Reject comma-separated lists or any commas
201+
if strings.Contains(feature, ",") {
202+
if !strings.HasSuffix(strings.TrimSpace(feature), ",") {
203+
return fmt.Errorf("--feature expects a single value, got comma-separated list: %q", feature)
204+
}
205+
return fmt.Errorf("--feature expects a single value, contains comma: %q", feature)
206+
}
207+
208+
// Trim spaces
209+
cleaned := strings.TrimSpace(feature)
189210

211+
uCmd.feature = cleaned
212+
213+
// Validate against supported features
214+
if _, ok := isFeaturePresent[cleaned]; !ok {
215+
var supportedFeatures []string
216+
for f := range isFeaturePresent {
217+
supportedFeatures = append(supportedFeatures, f)
218+
}
219+
return fmt.Errorf("feature %q is not supported; supported features are: %s", cleaned, strings.Join(supportedFeatures, ", "))
190220
}
191-
// we want to cover these cases:
192-
// 1. --feature=" comp_with_name" ---> this is totally fine as it has only 1 feature
193-
// 2. --feature=" comp_with_name " ---> this is also fine as it has only 1 feature
194-
// 3. --feature="comp_with_name comp_with_version" ---> this is not fine as it has 2 features
195-
// 4. --feature="comp_with_name, comp_with_version" ---> this is also not fine as it has 2 features
196-
197-
// TODO: validation of feature
198-
// // Check if the feature is valid
199-
// validFeatures := []string{"comp_with_supplier", "comp_valid_licenses", "sbom_authors"}
200-
// featureFound := false
201-
// for _, validFeature := range validFeatures {
202-
// if strings.TrimSpace(uCmd.feature) == validFeature {
203-
// featureFound = true
204-
// break
205-
// }
206-
// }
207-
// if !featureFound {
208-
// fmt.Printf("Error: invalid feature '%s'. Valid features are: %v\n", uCmd.feature, validFeatures)
209-
// return fmt.Errorf("invalid feature '%s'", uCmd.feature)
210-
// }
211221

212222
return nil
213223
}
224+
225+
var isFeaturePresent = map[string]bool{
226+
"comp_with_name": true,
227+
"comp_with_version": true,
228+
"comp_with_supplier": true,
229+
"comp_with_uniq_ids": true,
230+
"comp_valid_licenses": true,
231+
"comp_with_any_vuln_lookup_id": true,
232+
"comp_with_deprecated_licenses": true,
233+
"comp_with_multi_vuln_lookup_id": true,
234+
"comp_with_primary_purpose": true,
235+
"comp_with_restrictive_licenses": true,
236+
"comp_with_checksums": true,
237+
"comp_with_licenses": true,
238+
"sbom_creation_timestamp": true,
239+
"sbom_authors": true,
240+
"sbom_with_creator_and_version": true,
241+
"sbom_with_primary_component": true,
242+
"sbom_dependencies": true,
243+
"sbom_sharable": true,
244+
"sbom_parsable": true,
245+
"sbom_spec": true,
246+
"sbom_spec_file_format": true,
247+
"sbom_spec_version": true,
248+
}

docs/list.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,61 @@ The `sbomqs list` command allows users to list components or SBOM fileds based o
88
sbomqs list [flags] <SBOM file>
99
```
1010

11+
### Autocompletion for `--feature` Flag
12+
13+
- **For Bash**:
14+
15+
```bash
16+
sbomqs completion bash > sbomqs_completion.sh
17+
```
18+
19+
- **For Zsh**:
20+
21+
```bash
22+
sbomqs completion zsh > sbomqs_completion.sh
23+
```
24+
25+
This creates a file (`sbomqs_completion.sh`) with the completion logic.
26+
27+
To enable autocompletion, source the script in your shell session:
28+
29+
- **Temporary (Current Session)**:
30+
31+
```bash
32+
source sbomqs_completion.sh
33+
```
34+
35+
- **Permanent (All Sessions)**:
36+
37+
- Move the script to a directory in your shell’s path:
38+
39+
```bash
40+
mv sbomqs_completion.sh ~/.zsh/ # For Zsh, or ~/.bash/ for Bash
41+
```
42+
43+
- Add it to your shell configuration:
44+
- **Bash**: Edit `~/.bashrc` or `~/.bash_profile`:
45+
46+
```bash
47+
echo "source ~/.bash/sbomqs_completion.sh" >> ~/.bashrc
48+
source ~/.bashrc
49+
```
50+
51+
- **Zsh**: Edit `~/.zshrc`:
52+
53+
```bash
54+
echo "source ~/.zsh/sbomqs_completion.sh" >> ~/.zshrc
55+
source ~/.zshrc
56+
```
57+
58+
For Zsh, ensure completion is initialized by adding `autoload -Uz compinit && compinit` to `~/.zshrc` if not already present.
59+
60+
Run the following command and press `<Tab>`:
61+
62+
```bash
63+
sbomqs list --feature=<Tab>
64+
```
65+
1166
### Flags
1267

1368
- `--features, -f <feature>`: Specifies the feature to list (required). See supported features below.

pkg/list/report.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -110,19 +110,19 @@ func (r *Report) detailedReport() {
110110
fmt.Println(" No components found")
111111
fmt.Println()
112112
continue
113-
} else {
114-
// List components
115-
for _, comp := range result.Components {
116-
if r.Color {
117-
featureCol1 := color.New(color.FgHiMagenta).Sprint(featureCol)
118-
nameCol := color.New(color.FgHiCyan).Sprint(comp.Name)
119-
versionCol := color.New(color.FgHiGreen).Sprint(comp.Version)
120-
table.Append([]string{featureCol1, nameCol, versionCol})
121-
} else {
122-
table.Append([]string{featureCol, comp.Name, comp.Version})
123-
}
113+
}
114+
// List components
115+
for _, comp := range result.Components {
116+
if r.Color {
117+
featureCol1 := color.New(color.FgHiMagenta).Sprint(featureCol)
118+
nameCol := color.New(color.FgHiCyan).Sprint(comp.Name)
119+
versionCol := color.New(color.FgHiGreen).Sprint(comp.Version)
120+
table.Append([]string{featureCol1, nameCol, versionCol})
124121
}
122+
table.Append([]string{featureCol, comp.Name, comp.Version})
123+
125124
}
125+
126126
} else {
127127
// SBOM-based feature
128128
featureCol := fmt.Sprintf("%s (%s)", result.Feature, presence)

0 commit comments

Comments
 (0)