github.com/blueinnovationsgroup/can-go@v0.0.0-20230518195432-d0567cda0028/cmd/cantool/main.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "errors" 6 "fmt" 7 "io" 8 "os" 9 "path" 10 "path/filepath" 11 "sort" 12 "strings" 13 "text/scanner" 14 15 "github.com/blueinnovationsgroup/can-go/internal/generate" 16 "github.com/blueinnovationsgroup/can-go/pkg/dbc" 17 "github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis" 18 "github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/definitiontypeorder" 19 "github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/intervals" 20 "github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/lineendings" 21 "github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/messagenames" 22 "github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/multiplexedsignals" 23 "github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/newsymbols" 24 "github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/nodereferences" 25 "github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/noreservedsignals" 26 "github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/requireddefinitions" 27 "github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/signalbounds" 28 "github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/signalnames" 29 "github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/singletondefinitions" 30 "github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/siunits" 31 "github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/uniquenodenames" 32 "github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/uniquesignalnames" 33 "github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/unitsuffixes" 34 "github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/valuedescriptions" 35 "github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/version" 36 "github.com/fatih/color" 37 "gopkg.in/alecthomas/kingpin.v2" 38 ) 39 40 func main() { 41 app := kingpin.New("cantool", "CAN tool for Go programmers") 42 generateCommand(app) 43 lintCommand(app) 44 kingpin.MustParse(app.Parse(os.Args[1:])) 45 } 46 47 type SignalFilters map[string]map[string]*bool 48 49 func generateCommand(app *kingpin.Application) { 50 command := app.Command("generate", "generate CAN messages") 51 inputFileOrDir := command. 52 Arg("input-file-or-dir", "input directory"). 53 Required(). 54 ExistingFileOrDir() 55 outputDir := command. 56 Arg("output-dir", "output directory"). 57 Required(). 58 String() 59 filter := command. 60 Flag("filter", "comma-separated list of filters (<message>:[<signal>])"). 61 String() 62 filterFile := command. 63 Flag("filter-file", "path to file containing messages to include, one per line"). 64 File() 65 packageNameOpt := command. 66 Flag("package-name", "package name of generated file(s)"). 67 Short('p'). 68 String() 69 70 command.Action(func(c *kingpin.ParseContext) error { 71 var signalFilters SignalFilters 72 if *filter != "" { 73 signalFilters = make(map[string]map[string]*bool) 74 for _, e := range strings.Split(*filter, ",") { 75 err := parseFilter(e, signalFilters) 76 if err != nil { 77 return err 78 } 79 } 80 } else if *filterFile != nil { 81 signalFilters = make(map[string]map[string]*bool) 82 scan := bufio.NewScanner(*filterFile) 83 for scan.Scan() { 84 err := parseFilter(scan.Text(), signalFilters) 85 if err != nil { 86 return err 87 } 88 } 89 if scan.Err() != nil { 90 fmt.Println("Failed to parse message file", scan.Err()) 91 } 92 } 93 if signalFilters != nil { 94 fmt.Println("Using filters (case-insensitive):") 95 for msg, sigs := range signalFilters { 96 fmt.Printf("\t%s:\n", msg) 97 for sig := range sigs { 98 fmt.Printf("\t\t%s\n", sig) 99 } 100 } 101 defer func() { 102 for msg, sigs := range signalFilters { 103 for sig, found := range sigs { 104 if !*found { 105 fmt.Printf("Warning: no signal found matching filter '%s:%s'\n", msg, sig) 106 } 107 } 108 } 109 }() 110 } 111 112 return filepath.Walk(*inputFileOrDir, func(p string, i os.FileInfo, err error) error { 113 if err != nil { 114 return err 115 } 116 if i.IsDir() || filepath.Ext(p) != ".dbc" { 117 return nil 118 } 119 var relPath string 120 if *inputFileOrDir == p { 121 relPath = i.Name() 122 } else { 123 relPath, err = filepath.Rel(*inputFileOrDir, p) 124 if err != nil { 125 return err 126 } 127 } 128 outputFile := relPath + ".go" 129 outputPath := filepath.Join(*outputDir, outputFile) 130 131 var packageName string 132 if *packageNameOpt != "" { 133 packageName = *packageNameOpt 134 } else { 135 packageName = strings.TrimSuffix(path.Base(p), path.Ext(p)) + "can" 136 // Remove illegal characters from package name 137 packageName = strings.ReplaceAll(packageName, ".", "") 138 packageName = strings.ReplaceAll(packageName, "-", "") 139 packageName = strings.ReplaceAll(packageName, "_", "") 140 } 141 142 return genGo(p, outputPath, packageName, signalFilters) 143 }) 144 }) 145 } 146 func boolPtr(b bool) *bool { 147 return &b 148 } 149 func parseFilter(entry string, filters SignalFilters) error { 150 pieces := strings.Split(entry, ":") 151 if len(pieces) > 2 { 152 return fmt.Errorf("invalid filter entry: '%s', format is <message>[:<signal>]", entry) 153 } 154 message := strings.ToLower(pieces[0]) 155 signalSet, ok := filters[message] 156 if !ok { 157 signalSet = make(map[string]*bool) 158 } 159 if len(pieces) == 2 { 160 signal := strings.ToLower(pieces[1]) 161 signalSet[signal] = boolPtr(false) 162 } 163 filters[message] = signalSet 164 return nil 165 } 166 167 func lintCommand(app *kingpin.Application) { 168 command := app.Command("lint", "lint DBC files") 169 fileOrDir := command. 170 Arg("file-or-dir", "DBC file or directory"). 171 Required(). 172 ExistingFileOrDir() 173 command.Action(func(context *kingpin.ParseContext) error { 174 filesToLint, err := resolveFileOrDirectory(*fileOrDir) 175 if err != nil { 176 return err 177 } 178 var hasFailed bool 179 for _, lintFile := range filesToLint { 180 f, err := os.Open(lintFile) 181 if err != nil { 182 return err 183 } 184 source, err := io.ReadAll(f) 185 if err != nil { 186 return err 187 } 188 p := dbc.NewParser(f.Name(), source) 189 if err := p.Parse(); err != nil { 190 printError(source, err.Position(), err.Reason(), "parse") 191 continue 192 } 193 for _, a := range analyzers() { 194 pass := &analysis.Pass{ 195 Analyzer: a, 196 File: p.File(), 197 } 198 if err := a.Run(pass); err != nil { 199 return err 200 } 201 hasFailed = hasFailed || len(pass.Diagnostics) > 0 202 for _, d := range pass.Diagnostics { 203 printError(source, d.Pos, d.Message, a.Name) 204 } 205 } 206 } 207 if hasFailed { 208 return errors.New("one or more lint errors") 209 } 210 return nil 211 }) 212 } 213 214 func analyzers() []*analysis.Analyzer { 215 return []*analysis.Analyzer{ 216 // TODO: Re-evaluate if we want boolprefix.Analyzer(), since it creates a lot of churn in vendor schemas 217 definitiontypeorder.Analyzer(), 218 intervals.Analyzer(), 219 lineendings.Analyzer(), 220 messagenames.Analyzer(), 221 multiplexedsignals.Analyzer(), 222 newsymbols.Analyzer(), 223 nodereferences.Analyzer(), 224 noreservedsignals.Analyzer(), 225 requireddefinitions.Analyzer(), 226 signalbounds.Analyzer(), 227 signalnames.Analyzer(), 228 singletondefinitions.Analyzer(), 229 siunits.Analyzer(), 230 uniquenodenames.Analyzer(), 231 uniquesignalnames.Analyzer(), 232 unitsuffixes.Analyzer(), 233 valuedescriptions.Analyzer(), 234 version.Analyzer(), 235 } 236 } 237 238 func genGo(inputFile, outputFile, packageName string, filters SignalFilters) error { 239 if err := os.MkdirAll(filepath.Dir(outputFile), 0o755); err != nil { 240 return err 241 } 242 input, err := os.ReadFile(inputFile) 243 if err != nil { 244 return err 245 } 246 result, err := generate.Compile(inputFile, packageName, input) 247 if err != nil { 248 return err 249 } 250 for _, warning := range result.Warnings { 251 return warning 252 } 253 254 if filters != nil { 255 skips := make(map[string][]string) 256 // Filter in-place for only the messages and signals matching the filter 257 allMessages := result.Database.Messages 258 result.Database.Messages = result.Database.Messages[:0] 259 for _, msg := range allMessages { 260 if signalSet, msgMatch := filters[strings.ToLower(msg.Name)]; msgMatch { 261 allSignals := msg.Signals 262 msg.Signals = msg.Signals[:0] 263 for _, sig := range allSignals { 264 if sigFound, sigMatch := signalSet[strings.ToLower(sig.Name)]; sigMatch { 265 *sigFound = true 266 msg.Signals = append(msg.Signals, sig) 267 } else { 268 skips[msg.Name] = append(skips[msg.Name], sig.Name) 269 } 270 } 271 result.Database.Messages = append(result.Database.Messages, msg) 272 } else { 273 skips[msg.Name] = make([]string, 0) 274 } 275 } 276 if len(skips) > 0 { 277 fmt.Printf("The following messages/signals in %s were ignored due to filtering:\n", inputFile) 278 sortedMsgs := make([]string, 0, len(skips)) 279 for msg := range skips { 280 sortedMsgs = append(sortedMsgs, msg) 281 } 282 sort.Strings(sortedMsgs) 283 for _, msg := range sortedMsgs { 284 sigs := skips[msg] 285 if len(sigs) > 0 { 286 sort.Strings(sigs) 287 for _, sig := range sigs { 288 fmt.Printf("\t%s:%s\n", msg, sig) 289 } 290 } else { 291 fmt.Printf("\t%s:*\n", msg) 292 } 293 } 294 } 295 } 296 output, err := generate.Database(result.Database) 297 if err != nil { 298 return err 299 } 300 if err := os.WriteFile(outputFile, output, 0o600); err != nil { 301 return err 302 } 303 fmt.Println("wrote:", outputFile) 304 return nil 305 } 306 307 func resolveFileOrDirectory(fileOrDirectory string) ([]string, error) { 308 fileInfo, err := os.Stat(fileOrDirectory) 309 if err != nil { 310 return nil, err 311 } 312 if !fileInfo.IsDir() { 313 return []string{fileOrDirectory}, nil 314 } 315 var files []string 316 if err := filepath.Walk(fileOrDirectory, func(path string, info os.FileInfo, err error) error { 317 if !info.IsDir() && filepath.Ext(path) == ".dbc" { 318 files = append(files, path) 319 } 320 return nil 321 }); err != nil { 322 return nil, err 323 } 324 return files, nil 325 } 326 327 func printError(source []byte, pos scanner.Position, msg, name string) { 328 fmt.Printf("\n%s: %s (%s)\n", pos, color.RedString("%s", msg), name) 329 fmt.Printf("%s\n", getSourceLine(source, pos)) 330 fmt.Printf("%s\n", caretAtPosition(pos)) 331 } 332 333 func getSourceLine(source []byte, pos scanner.Position) []byte { 334 lineStart := pos.Offset 335 for lineStart > 0 && source[lineStart-1] != '\n' { 336 lineStart-- 337 } 338 lineEnd := pos.Offset 339 for lineEnd < len(source) && source[lineEnd] != '\n' { 340 lineEnd++ 341 } 342 return source[lineStart:lineEnd] 343 } 344 345 func caretAtPosition(pos scanner.Position) string { 346 return strings.Repeat(" ", pos.Column-1) + color.YellowString("^") 347 }