Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,53 @@ go get github.com/github/go-spdx@latest

- [spdxexp](https://pkg.go.dev/github.com/github/go-spdx/spdxexp) - Expression package validates licenses and determines if a license expression is satisfied by a list of licenses. Validity of a license is determined by the SPDX license list.

## CLI: spdx-validate

`spdx-validate` is a command-line tool that validates SPDX license expressions.

### Building

```sh
go build -o spdx-validate ./cmd/spdx-validate/
```

### Usage

**Validate a single expression on stdin:**

```sh
echo "MIT" | ./spdx-validate
echo "Apache-2.0 OR MIT" | ./spdx-validate
```

The tool exits with code 0 if the expression is valid, or code 1 (with an error message on stderr) if it is invalid.

```sh
$ echo "BOGUS" | ./spdx-validate
invalid SPDX expression: "BOGUS"
$ echo $?
1
```

**Validate a file of expressions with `-f`/`--file`:**

```sh
./spdx-validate -f licenses.txt
```

The file should contain one SPDX expression per line. Blank lines are skipped. The tool reports each invalid expression to stderr, prints a summary, and exits with code 0 if all pass or code 1 if any fail.

```sh
$ cat licenses.txt
MIT
NOT-A-LICENSE
Apache-2.0

$ ./spdx-validate -f licenses.txt
line 2: invalid SPDX expression: "NOT-A-LICENSE"
1 of 3 expressions failed validation
```

## Public API

_NOTE: The public API is initially limited to the Satisfies and ValidateLicenses functions. If
Expand Down
127 changes: 127 additions & 0 deletions cmd/spdx-validate/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package main

import (
"bufio"
"fmt"
"io"
"os"
"strings"

"github.com/github/go-spdx/v2/spdxexp"
"github.com/spf13/cobra"
)

var filePath string

var rootCmd = &cobra.Command{
Use: "spdx-validate",
Short: "Validate SPDX license expressions",
Long: `spdx-validate reads SPDX license expressions and validates them.

By default it reads a single expression from stdin. Use -f/--file to read
a newline-separated list of expressions from a file.

Exits 0 if all expressions are valid, or 1 if any are invalid.

Examples:
echo "MIT" | spdx-validate
echo "Apache-2.0 OR MIT" | spdx-validate
spdx-validate -f licenses.txt`,
RunE: func(cmd *cobra.Command, args []string) error {
if filePath != "" {
f, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("unable to open file: %w", err)
}
defer f.Close()
ok, err := validateExpressions(f, os.Stderr)
if err != nil {
return err
}
if !ok {
os.Exit(1)
}
return nil
}
ok, err := validateSingleExpression(os.Stdin, os.Stderr)
if err != nil {
return err
}
if !ok {
os.Exit(1)
}
return nil
},
SilenceUsage: true,
SilenceErrors: true,
}

func init() {
rootCmd.Flags().StringVarP(&filePath, "file", "f", "", "path to a newline-separated file of SPDX expressions")
}

// validateSingleExpression reads one line from r, validates it as an SPDX
// expression, and writes an error message to w if invalid. Returns (true, nil)
// when valid, (false, nil) when invalid, or (false, err) on read errors.
func validateSingleExpression(r io.Reader, w io.Writer) (bool, error) {
scanner := bufio.NewScanner(r)
if !scanner.Scan() {
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

validateSingleExpression treats any scanner.Scan() failure as "no input provided" without checking scanner.Err(). This can hide real read errors (including bufio.ErrTooLong when the line exceeds the scanner token limit). Check scanner.Err() when Scan() returns false and return a wrapped read error when present; consider increasing the scanner buffer if long lines are plausible.

Suggested change
scanner := bufio.NewScanner(r)
if !scanner.Scan() {
scanner := bufio.NewScanner(r)
// Increase the scanner buffer to better handle long expressions.
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
if !scanner.Scan() {
if err := scanner.Err(); err != nil {
return false, fmt.Errorf("failed to read input: %w", err)
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

there's not going to be a 64mb spdx expression.

return false, fmt.Errorf("no input provided")
}
input := strings.TrimSpace(scanner.Text())
if input == "" {
return false, fmt.Errorf("empty input")
}

valid, _ := spdxexp.ValidateLicenses([]string{input})
if !valid {
fmt.Fprintf(w, "invalid SPDX expression: %q\n", input)
return false, nil
}
return true, nil
}

// validateExpressions reads newline-separated SPDX expressions from r,
// validates each one, and writes error messages to w for any that are invalid.
// Returns (true, nil) when all are valid, (false, nil) when any are invalid, or
// (false, err) on read errors or when no expressions are found.
func validateExpressions(r io.Reader, w io.Writer) (bool, error) {
scanner := bufio.NewScanner(r)
lineNum := 0
failures := 0

for scanner.Scan() {
lineNum++
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
valid, _ := spdxexp.ValidateLicenses([]string{line})
if !valid {
failures++
fmt.Fprintf(w, "line %d: invalid SPDX expression: %q\n", lineNum, line)
}
}

if err := scanner.Err(); err != nil {
return false, fmt.Errorf("error reading file: %w", err)
}

if lineNum == 0 || (lineNum > 0 && failures == lineNum) {
return false, fmt.Errorf("no valid expressions found")
}

if failures > 0 {
fmt.Fprintf(w, "%d of %d expressions failed validation\n", failures, lineNum)
return false, nil
}

return true, nil
}

func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Loading