## Bash/Shell validator for Validatrix.
##
## Validates Bash/shell scripts, checking structural balance and common issues.
import std/[json, strformat, tables]
{.warning[UnusedImport]:off.}
import ../core/types, ../core/tokenizerbase, ../core/validatorbase
import ../tokenizers/bash_tokenizer
type
BashValidator* = ref object of ValidatorBase
## Validator for Bash/shell scripts.
# ---------------------------------------------------------------------------
# Construction
# ---------------------------------------------------------------------------
proc newBashValidator*(source: string, options: ValidationOptions = newValidationOptions(), sourcePath: string = ""): BashValidator =
## Create a new Bash validator.
result = BashValidator(
source: source,
sourcePath: sourcePath,
options: options,
result: newValidationResult(),
startTime: epochTime()
)
result.result.flavor = options.flavor
# ---------------------------------------------------------------------------
# Tokenizer creation
# ---------------------------------------------------------------------------
method createTokenizer*(self: BashValidator): TokenizerBase =
## Create the Bash tokenizer.
result = newBashTokenizer(self.source, self.options)
# ---------------------------------------------------------------------------
# Structural analysis
# ---------------------------------------------------------------------------
method analyzeTokens*(self: BashValidator) =
## Analyze Bash-specific patterns.
when defined(validatrixDebug):
if self.options.debugMode:
debugEnter("BASH_ANALYZE", "Analyzing Bash structure")
procCall ValidatorBase(self).analyzeTokens()
let tokens = self.tokenizer.tokens
# Check for unclosed if/fi, do/done, case/esac
var keywords: seq[string] = @[]
for tok in tokens:
if tok.kind == tkKeyword:
keywords.add(tok.value)
if tok.value in ["if", "case", "for", "while", "until"]:
self.result.moduleInfo.functions.add(FunctionInfo(
name: tok.value & "_block",
kind: "block",
position: tok.position
))
elif tok.kind == tkComment:
self.result.moduleInfo.comments += 1
# Check for fi/done/esac matching
var ifCount, forCount, caseCount = 0
var fiCount, doneCount, esacCount = 0
for kw in keywords:
case kw
of "if": ifCount += 1
of "fi": fiCount += 1
of "for", "while", "until": forCount += 1
of "done": doneCount += 1
of "case": caseCount += 1
of "esac": esacCount += 1
else: discard
if ifCount != fiCount:
if ifCount > fiCount:
self.result.errors.add(newValidationError(
esError, &"Unclosed 'if' block: {ifCount} 'if' but {fiCount} 'fi'",
ErrUnclosedBlock,
newSourcePosition(1, 1, 0),
newSourceRange(newSourcePosition(1, 1, 0), newSourcePosition(1, 1, 0)),
hint = "Add missing 'fi' to close the if block"
))
if forCount != doneCount:
if forCount > doneCount:
self.result.errors.add(newValidationError(
esError, &"Unclosed loop: {forCount} loop starters but {doneCount} 'done'",
ErrUnclosedBlock,
newSourcePosition(1, 1, 0),
newSourceRange(newSourcePosition(1, 1, 0), newSourcePosition(1, 1, 0)),
hint = "Add missing 'done' to close the loop"
))
if caseCount != esacCount:
if caseCount > esacCount:
self.result.errors.add(newValidationError(
esError, &"Unclosed 'case' block: {caseCount} 'case' but {esacCount} 'esac'",
ErrUnclosedBlock,
newSourcePosition(1, 1, 0),
newSourceRange(newSourcePosition(1, 1, 0), newSourcePosition(1, 1, 0)),
hint = "Add missing 'esac' to close the case block"
))
when defined(validatrixDebug):
if self.options.debugMode:
debugLog("BASH_ANALYZE", &"if/fi: {ifCount}/{fiCount}, for/done: {forCount}/{doneCount}, case/esac: {caseCount}/{esacCount}")
debugLeave("BASH_ANALYZE")
# ---------------------------------------------------------------------------
# Registration
# ---------------------------------------------------------------------------
proc init*() =
## Register the Bash/Shell validator.
registerValidator("bash", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase =
result = newBashValidator(source, options, sourcePath)
)
registerValidator("shell", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase =
result = newBashValidator(source, options, sourcePath)
)
registerValidator("sh", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase =
result = newBashValidator(source, options, sourcePath)
)
# Auto-init
init()