JavaScript*, 177 bytes (LL1 rotated 177°)
s=>(f=(x,i)=>x<"A"||x>"Z"?v=s[i]==x&&[i+1,x]:s.split`
`.some((r,a,j)=>v=r[0]==x&&[j=i,a=[a],...r].slice(3).every(y=>f(y,j)&&a.push(([j]=v)[1]))&&[j,a]))("S",0)&&s[v[0]]==0&&v[1]
Try it online!
Outputs false
if the string does not match, otherwise outputs a nested array of rule indices and terminals:
[2,"(",[1,[3,"a"]],"+",[3,"a"],")"]
Ungolfed
str =>
(applyRule = (ruleName, index) =>
ruleName < "A" || ruleName > "Z"
? result = str[index] == ruleName && [index + 1, ruleName]
: str.split("\n").some(
(rule, appliedRules, jndex) => result =
rule[0] == ruleName &&
(
jndex = index,
appliedRules = [appliedRules],
[...rule].slice(1).every(char =>
applyRule(char, jndex)
&& appliedRules.push(([jndex] = result)[1])
)
&& [jndex, appliedRules]
)
)
)("S", 0)
&& str[result[0]] == 0 // "\n" == 0
&& result[1]
function main(input) {
const [str, ...rules] = input.split("\n")
const result = applyRule("S", 0)
if(result && result[0] === str.length)
return result[1]
function applyRule(ruleName, index) {
if (ruleName === ruleName.toLowerCase())
return str[index] === ruleName ? [index + 1, ruleName] : null
main: for (const [ruleIndex, rule] of rules.entries()) {
if (!rule.startsWith(ruleName))
continue
let curIndex = index
let appliedRules = [ruleIndex + 1]
for (const char of rule.slice(1)) {
let result = applyRule(char, curIndex)
if (!result) continue main
curIndex = result[0]
appliedRules.push(result[1])
}
return [curIndex, appliedRules]
}
return null
}
}
* This uses JS features likely introduced since the question's posting in 2015