305db55cd7
- Surface the exception type and message in the error display by prepending the formatted error string to the traceback array. - Prevent the host from hiding the actual error by ensuring the traceback is not empty, consistent with other language runners.
358 lines
17 KiB
Julia
Executable File
358 lines
17 KiB
Julia
Executable File
#!/usr/bin/env julia
|
|
# bash_corpus_stats.jl
|
|
# Extract every bash execution from ~/.omp/stats.db and analyze it.
|
|
# sources: (1) bash-tool calls (2) full-file writes to *.sh
|
|
# Reports: (1) bash constructs (2) CLI utilities (3) flags per utility
|
|
#
|
|
# julia bash_corpus_stats.jl
|
|
# deps: import Pkg; Pkg.add(["SQLite","JSON3","DBInterface"])
|
|
|
|
using SQLite, JSON3, DBInterface, Printf
|
|
|
|
# ─────────────────────────── extraction ───────────────────────────
|
|
const DB = SQLite.DB(joinpath(homedir(), ".omp", "stats.db"))
|
|
|
|
field(j, k) = try
|
|
v = get(JSON3.read(j), k, nothing); v isa AbstractString ? String(v) : nothing
|
|
catch; nothing end
|
|
|
|
function extract()
|
|
bash, sh = String[], String[]
|
|
for r in DBInterface.execute(DB, "SELECT arg_json FROM ss_tool_calls WHERE tool_name IN " *
|
|
"('bash','Bash','shell','bash_exec','execute_command','run_command')")
|
|
c = field(r.arg_json, :command); c !== nothing && !isempty(strip(c)) && push!(bash, c)
|
|
end
|
|
for r in DBInterface.execute(DB, "SELECT arg_json FROM ss_tool_calls WHERE tool_name IN " *
|
|
"('write','write_file','create_file','file_write','replace_file','edit_file') " *
|
|
"AND json_extract(arg_json,'\$.path') LIKE '%.sh'")
|
|
c = field(r.arg_json, :content); c !== nothing && !isempty(strip(c)) && push!(sh, c)
|
|
end
|
|
return bash, sh
|
|
end
|
|
|
|
# ─────────────────────────── lexer ───────────────────────────
|
|
# Splits a bash string into words + control/redirection operators, tracks
|
|
# expansion/construct features, and captures the inner text of command &
|
|
# process substitutions (and shell-source heredoc bodies) for recursion.
|
|
const WS = (' ', '\t')
|
|
const SHELLS = Set(["sh","bash","zsh","dash","ksh","mksh","ash"])
|
|
struct Tok; kind::Symbol; text::String; end # :W word :O operator :C comment
|
|
|
|
function read_balanced(cs::Vector{Char}, j::Int, openc::Char, closec::Char)
|
|
n = length(cs); depth = 1; buf = Char[]
|
|
while j <= n && depth > 0
|
|
c = cs[j]
|
|
if c == '\\' && j+1 <= n
|
|
push!(buf, c, cs[j+1]); j += 2; continue
|
|
elseif c == '\''
|
|
k = j+1; while k <= n && cs[k] != '\''; k += 1; end
|
|
if k > n; append!(buf, @view cs[j:n]); return String(buf), n+1; end
|
|
append!(buf, @view cs[j:k]); j = k+1; continue
|
|
elseif c == '"'
|
|
k = j+1
|
|
while k <= n && cs[k] != '"'; cs[k] == '\\' ? (k += 2) : (k += 1); end
|
|
kk = min(k, n); append!(buf, @view cs[j:kk]); j = kk+1; continue
|
|
end
|
|
if c == openc; depth += 1
|
|
elseif c == closec; depth -= 1; depth == 0 && return String(buf), j+1
|
|
end
|
|
push!(buf, c); j += 1
|
|
end
|
|
return String(buf), j
|
|
end
|
|
|
|
function lex(s::String)
|
|
cs = collect(s); n = length(cs)
|
|
toks = Tok[]; feats = String[]; subs = String[]
|
|
hd_queue = Tuple{String,Bool}[]
|
|
word = Char[]; started = false
|
|
expect_delim::Union{Nothing,Bool} = nothing
|
|
i = 1
|
|
|
|
flush!() = begin
|
|
if started
|
|
w = String(word)
|
|
if expect_delim !== nothing
|
|
d = replace(strip(strip(w), ['\'','"']), "\\"=>"")
|
|
push!(hd_queue, (String(d), expect_delim)); expect_delim = nothing
|
|
end
|
|
push!(toks, Tok(:W, w))
|
|
end
|
|
empty!(word); started = false
|
|
end
|
|
line_is_shell() = begin
|
|
e = length(toks) - 1; st = e - 1
|
|
while st >= 1 && !(toks[st].kind == :O && toks[st].text == "\n"); st -= 1; end
|
|
for idx in (st+1):e
|
|
t = toks[idx]
|
|
t.kind == :W && (last(split(t.text, '/')) in SHELLS) && return true
|
|
if t.kind == :O && (t.text == ">" || t.text == ">>") && idx < e
|
|
nt = toks[idx+1]
|
|
nt.kind == :W && endswith(strip(nt.text, ['\'','"']), ".sh") && return true
|
|
end
|
|
end
|
|
false
|
|
end
|
|
consume_heredocs(j) = begin
|
|
shell = line_is_shell()
|
|
for (delim, stripmode) in hd_queue
|
|
j += 1; body = Char[]; first = true
|
|
while j <= n
|
|
e = j; while e <= n && cs[e] != '\n'; e += 1; end
|
|
ln = String(@view cs[j:min(e-1,n)])
|
|
test = stripmode ? lstrip(ln, '\t') : ln
|
|
if test == delim; j = e; break; end
|
|
first ? (first = false) : push!(body, '\n'); append!(body, collect(ln))
|
|
if e > n; j = n+1; break; end
|
|
j = e + 1
|
|
end
|
|
shell && !isempty(body) && push!(subs, String(body))
|
|
end
|
|
empty!(hd_queue); j
|
|
end
|
|
|
|
while i <= n
|
|
c = cs[i]
|
|
if c == '\\' && i+1 <= n && cs[i+1] == '\n'; i += 2; continue; end
|
|
if c == '\\' && i+1 <= n; push!(word, c, cs[i+1]); started = true; i += 2; continue; end
|
|
if c in WS; flush!(); i += 1; continue; end
|
|
if c == '\n'
|
|
flush!(); push!(toks, Tok(:O, "\n"))
|
|
i = isempty(hd_queue) ? i+1 : consume_heredocs(i); continue
|
|
end
|
|
if c == '#' && !started
|
|
j = i; while j <= n && cs[j] != '\n'; j += 1; end
|
|
push!(toks, Tok(:C, String(@view cs[i:j-1]))); i = j; continue
|
|
end
|
|
if c == '\''
|
|
k = i+1; while k <= n && cs[k] != '\''; k += 1; end
|
|
kk = min(k, n); append!(word, @view cs[i:kk]); started = true; i = kk+1; continue
|
|
end
|
|
if c == '"'
|
|
j = i+1; push!(word, '"')
|
|
while j <= n && cs[j] != '"'
|
|
if cs[j] == '\\' && j+1 <= n; push!(word, cs[j], cs[j+1]); j += 2; continue; end
|
|
if cs[j] == '$' && j+1 <= n && cs[j+1] == '('
|
|
inner, j2 = read_balanced(cs, j+2, '(', ')')
|
|
push!(feats, "cmdsub_\$()"); push!(subs, inner)
|
|
append!(word, collect("\$(" * inner * ")")); j = j2; continue
|
|
end
|
|
if cs[j] == '$' && j+1 <= n && cs[j+1] == '{'
|
|
inner, j2 = read_balanced(cs, j+2, '{', '}')
|
|
push!(feats, "paramexp_\${}"); append!(word, collect("\${" * inner * "}")); j = j2; continue
|
|
end
|
|
if cs[j] == '`'
|
|
k = j+1; while k <= n && cs[k] != '`'; cs[k] == '\\' ? (k += 2) : (k += 1); end
|
|
kk = min(k, n); push!(feats, "cmdsub_backtick"); push!(subs, String(@view cs[j+1:min(kk-1,n)]))
|
|
append!(word, @view cs[j:kk]); j = kk+1; continue
|
|
end
|
|
cs[j] == '$' && push!(feats, "var_in_dq")
|
|
push!(word, cs[j]); j += 1
|
|
end
|
|
push!(word, '"'); started = true; i = min(j+1, n+1); continue
|
|
end
|
|
if c == '$' && i+1 <= n
|
|
nx = cs[i+1]
|
|
if nx == '('
|
|
if i+2 <= n && cs[i+2] == '('
|
|
inner, j = read_balanced(cs, i+3, '(', ')')
|
|
push!(feats, "arith_\$(())"); append!(word, collect("\$((" * inner * "))")); started = true
|
|
i = (j <= n && cs[j] == ')') ? j+1 : j; continue
|
|
end
|
|
inner, j = read_balanced(cs, i+2, '(', ')')
|
|
push!(feats, "cmdsub_\$()"); push!(subs, inner)
|
|
append!(word, collect("\$(" * inner * ")")); started = true; i = j; continue
|
|
end
|
|
if nx == '{'
|
|
inner, j = read_balanced(cs, i+2, '{', '}')
|
|
push!(feats, "paramexp_\${}"); append!(word, collect("\${" * inner * "}")); started = true; i = j; continue
|
|
end
|
|
if nx == '\''
|
|
k = i+2; while k <= n && cs[k] != '\''; cs[k] == '\\' ? (k += 2) : (k += 1); end
|
|
kk = min(k, n); push!(feats, "ansi_c_quote"); append!(word, @view cs[i:kk]); started = true; i = kk+1; continue
|
|
end
|
|
push!(feats, "var_\$"); push!(word, '$'); started = true; i += 1; continue
|
|
end
|
|
if c == '`'
|
|
k = i+1; while k <= n && cs[k] != '`'; cs[k] == '\\' ? (k += 2) : (k += 1); end
|
|
kk = min(k, n); push!(feats, "cmdsub_backtick"); push!(subs, String(@view cs[i+1:min(kk-1,n)]))
|
|
append!(word, @view cs[i:kk]); started = true; i = kk+1; continue
|
|
end
|
|
if c in ('|','&',';','<','>','(',')')
|
|
flush!()
|
|
if (c == '<' || c == '>') && i+1 <= n && cs[i+1] == '('
|
|
inner, j = read_balanced(cs, i+2, '(', ')')
|
|
push!(feats, c == '<' ? "procsub_<()" : "procsub_>()"); push!(subs, inner)
|
|
push!(toks, Tok(:W, string(c) * "(" * inner * ")")); i = j; continue
|
|
end
|
|
three = String(@view cs[i:min(i+2,n)]); two = String(@view cs[i:min(i+1,n)])
|
|
if three == "<<<"; push!(toks, Tok(:O, "<<<")); i += 3; continue; end
|
|
if two == "<<"
|
|
op = three == "<<-" ? "<<-" : "<<"
|
|
push!(toks, Tok(:O, op)); expect_delim = (op == "<<-"); i += length(op); continue
|
|
end
|
|
if three == "&>>"; push!(toks, Tok(:O, "&>>")); i += 3; continue; end
|
|
if two in ("&&","||",";;","|&",">>","<&",">&","&>","<>"); push!(toks, Tok(:O, two)); i += 2; continue; end
|
|
push!(toks, Tok(:O, string(c))); i += 1; continue
|
|
end
|
|
push!(word, c); started = true; i += 1
|
|
end
|
|
flush!()
|
|
return toks, feats, subs
|
|
end
|
|
|
|
# ─────────────────────────── analyzer ───────────────────────────
|
|
const KW = Set(["if","then","else","elif","fi","for","while","until","do","done",
|
|
"case","esac","select","function","in","time","coproc"])
|
|
const WRAPPERS = Set(["sudo","env","timeout","command","builtin","exec","nohup","nice",
|
|
"ionice","stdbuf","setsid","doas","xargs","watch"])
|
|
const REDIRS = Set(["<",">",">>","<<","<<-","<<<","<&",">&","&>","&>>","<>"])
|
|
const OPC = Dict(
|
|
"|"=>"pipe |","|&"=>"pipe |&","&&"=>"and &&","||"=>"or ||",";"=>"seq ;",";;"=>"case-clause ;;",
|
|
"&"=>"background &",">"=>"redir >",">>"=>"redir-append >>","<"=>"redir-in <","<&"=>"fd-dup <&",
|
|
">&"=>"fd-dup >&","&>"=>"redir-both &>","&>>"=>"redir-both-append &>>","<>"=>"redir-rw <>",
|
|
"<<<"=>"herestring <<<","<<"=>"heredoc <<","<<-"=>"heredoc <<-")
|
|
const FEATC = Dict(
|
|
"cmdsub_\$()"=>"cmdsub \$()","cmdsub_backtick"=>"cmdsub ``","arith_\$(())"=>"arith-exp \$(())",
|
|
"paramexp_\${}"=>"paramexp \${}","procsub_<()"=>"procsub <()","procsub_>()"=>"procsub >()",
|
|
"var_\$"=>"var \$x","var_in_dq"=>"var-in-dquotes","ansi_c_quote"=>"ansi-c \$'..'")
|
|
|
|
inc!(d, k) = (d[k] = get(d, k, 0) + 1)
|
|
|
|
function normutil(w::AbstractString)
|
|
isempty(w) && return nothing
|
|
(startswith(w,"\$")||startswith(w,"`")||occursin("\$(",w)||occursin("\${",w)||
|
|
startswith(w,"\"")||startswith(w,"'")) && return "<dynamic>"
|
|
base = String(last(split(w, '/')))
|
|
isempty(base) ? nothing : base
|
|
end
|
|
function recordflag!(flags, util, w)
|
|
util == "" && return
|
|
f = w; eq = findfirst('=', f); eq !== nothing && (f = f[1:prevind(f,eq)])
|
|
(isempty(f) || f == "-" || f == "--") && return
|
|
d = get!(flags, util, Dict{String,Int}()); d[f] = get(d,f,0)+1
|
|
end
|
|
function dwc!(w, constructs)
|
|
u = replace(w, r"'[^']*'"=>"", "\""=>"")
|
|
startswith(w, "~") && inc!(constructs, "tilde ~")
|
|
(occursin(r"\{[^{}]*\.\.[^{}]*\}", u) || occursin(r"\{[^{}]*,[^{}]*\}", u)) && inc!(constructs, "brace-expansion {}")
|
|
(occursin('*', u) || occursin('?', u) || occursin(r"\[[^]]+\]", u)) && inc!(constructs, "glob */?/[]")
|
|
end
|
|
function unquote_script(w) # strip outer quotes of a `bash -c '<script>'` arg
|
|
n = lastindex(w)
|
|
if length(w) >= 2 && ((startswith(w,"'") && endswith(w,"'")) || (startswith(w,"\"") && endswith(w,"\"")))
|
|
return w[nextind(w,1):prevind(w,n)]
|
|
elseif startswith(w,"-")
|
|
return nothing
|
|
else
|
|
return w
|
|
end
|
|
end
|
|
|
|
function analyze!(s::String, constructs, utils, flags, depth::Int=0)
|
|
depth > 25 && return
|
|
toks, feats, subs = lex(s)
|
|
for f in feats; inc!(constructs, get(FEATC, f, f)); end
|
|
expect = true; skip_target = 0; curutil = ""; wrapper = false; ddash = false; want_script = false
|
|
i = 1; N = length(toks)
|
|
while i <= N
|
|
t = toks[i]
|
|
if t.kind == :O
|
|
op = t.text
|
|
if op == "("
|
|
if i < N && toks[i+1].kind == :O && toks[i+1].text == "("
|
|
inc!(constructs,"arith-cmd (( ))"); i+=2; expect=false; curutil=""; ddash=false; want_script=false; continue
|
|
end
|
|
inc!(constructs,"subshell ( )"); expect=true; curutil=""; ddash=false; want_script=false; i+=1; continue
|
|
elseif op == ")" || op == "\n"
|
|
expect=true; curutil=""; ddash=false; want_script=false; i+=1; continue
|
|
elseif haskey(OPC, op)
|
|
inc!(constructs, OPC[op])
|
|
if op in ("|","|&","&&","||",";",";;","&"); expect=true; curutil=""; ddash=false; want_script=false
|
|
else; skip_target += 1; end
|
|
i+=1; continue
|
|
else; i+=1; continue; end
|
|
elseif t.kind == :C
|
|
i+=1; continue
|
|
end
|
|
w = t.text
|
|
if skip_target > 0; skip_target -= 1; i+=1; continue; end
|
|
if expect && occursin(r"^\d+$", w) && i < N && toks[i+1].kind==:O && toks[i+1].text in REDIRS
|
|
i+=1; continue # leading fd (2>&1)
|
|
end
|
|
if expect
|
|
if w in KW
|
|
inc!(constructs,"kw "*w); expect = !(w in ("for","select","case")); i+=1; continue
|
|
elseif w == "{"; inc!(constructs,"group { }"); expect=true; i+=1; continue
|
|
elseif w == "}"; expect=true; i+=1; continue
|
|
elseif w == "!"; inc!(constructs,"negation !"); expect=true; i+=1; continue
|
|
elseif startswith(w,"[["); inc!(constructs,"test [[ ]]"); expect=false; i+=1; continue
|
|
elseif w == "["; inc!(constructs,"test [ ]"); expect=false; i+=1; continue
|
|
elseif occursin(r"^[A-Za-z_][A-Za-z0-9_]*(\[[^]]*\])?\+?=", w)
|
|
inc!(constructs,"assignment VAR="); expect=true; i+=1; continue
|
|
else
|
|
u = normutil(w)
|
|
if u !== nothing; inc!(utils,u); curutil=u; wrapper=(u in WRAPPERS)
|
|
else; curutil=""; wrapper=false; end
|
|
expect=false; ddash=false; want_script=false; i+=1; continue
|
|
end
|
|
else
|
|
dwc!(w, constructs)
|
|
if w == "--"; ddash = true; i+=1; continue; end # option terminator
|
|
if want_script
|
|
want_script = false
|
|
src = unquote_script(w)
|
|
src !== nothing && analyze!(String(src), constructs, utils, flags, depth+1)
|
|
i+=1; continue
|
|
end
|
|
if wrapper # sudo/env/timeout/xargs: count, then find real subcommand
|
|
if startswith(w,"-"); recordflag!(flags,curutil,w); i+=1; continue
|
|
elseif occursin(r"^[A-Za-z_][A-Za-z0-9_]*=", w) || occursin(r"^\d+(\.\d+)?[smhd]?$", w); i+=1; continue
|
|
else
|
|
u2 = normutil(w)
|
|
if u2 !== nothing; inc!(utils,u2); curutil=u2; wrapper=(u2 in WRAPPERS)
|
|
else; curutil=""; wrapper=false; end
|
|
ddash=false; i+=1; continue
|
|
end
|
|
end
|
|
if !ddash && startswith(w,"-") && length(w)>1 && curutil!=""
|
|
recordflag!(flags,curutil,w)
|
|
(curutil in SHELLS && occursin(r"^-[a-z]*c$", w)) && (want_script = true) # bash -c <script>
|
|
end
|
|
i+=1; continue
|
|
end
|
|
end
|
|
for sub in subs; analyze!(sub, constructs, utils, flags, depth+1); end
|
|
end
|
|
|
|
# ─────────────────────────── reporting ───────────────────────────
|
|
sortd(d) = sort(collect(d), by=x->-x[2])
|
|
|
|
function main()
|
|
bash, sh = extract()
|
|
corpus = vcat(bash, sh); total = length(corpus)
|
|
constructs = Dict{String,Int}(); utils = Dict{String,Int}(); flags = Dict{String,Dict{String,Int}}()
|
|
for s in corpus; analyze!(s, constructs, utils, flags); end
|
|
|
|
println("corpus: $(length(bash)) bash calls + $(length(sh)) .sh writes = $total snippets\n")
|
|
println("="^60, "\n1) BASH CONSTRUCTS\n", "="^60)
|
|
for (k,v) in sortd(constructs)
|
|
@printf(" %-26s %9d %5.1f%%\n", k, v, 100v/total)
|
|
end
|
|
ut = sum(values(utils))
|
|
println("\n", "="^60, "\n2) CLI UTILITIES ($(length(utils)) distinct)\n", "="^60)
|
|
for (k,v) in sortd(utils)[1:min(60,length(utils))]
|
|
@printf(" %-22s %9d %5.1f%%\n", k, v, 100v/ut)
|
|
end
|
|
println("\n", "="^60, "\n3) FLAGS PER UTILITY\n", "="^60)
|
|
flagtot = Dict(u => sum(values(d)) for (u,d) in flags)
|
|
for (u, ft) in sort(collect(flagtot), by=x->-x[2])[1:min(30,length(flagtot))]
|
|
fl = sortd(flags[u]); shown = fl[1:min(12,length(fl))]
|
|
println(" ", rpad(u,12), "[$ft uses, $(length(fl)) distinct]")
|
|
println(" ", join(["$k·$v" for (k,v) in shown], " "),
|
|
length(fl) > 12 ? " (+$(length(fl)-12))" : "")
|
|
end
|
|
end
|
|
|
|
main()
|