Skip to main content

weavatrix_parse/shell/
mod.rs

1//! Structural extraction for shell scripts.
2//!
3//! Shell is where a repository keeps the things nothing else records. A CI job
4//! or a deploy script sources its helpers, invokes its siblings, and calls
5//! services by their address - so a script is often the only place an endpoint
6//! is written down at all. No other extractor here can see any of that, and
7//! most tools do not read shell at all.
8//!
9//! Words are rebuilt from the token stream by byte adjacency rather than by
10//! splitting the line, because that is what keeps a `#` inside a string from
11//! ending the line and a quoted URL in one piece.
12
13use crate::facts::{Declaration, DeclarationKind, Facts, Import, Reference, ReferenceKind, Span};
14use crate::syntax::Language;
15use crate::token::{Mode, Token, TokenKind, Tokenizer};
16
17/// Extracts structural facts from one shell script.
18#[must_use]
19pub fn extract(source: &str) -> Facts {
20    let tokens = Tokenizer::new(source, Language::Bash)
21        .mode(Mode::Lite)
22        .collect::<Vec<_>>();
23    let mut state = Extractor {
24        source,
25        tokens: &tokens,
26        facts: Facts::default(),
27        function: None,
28        depth: 0,
29    };
30    state.run();
31    state.facts
32}
33
34/// Commands that address a service, and so name an endpoint.
35const CLIENTS: &[&str] = &[
36    "curl", "wget", "http", "https", "xh", "httpie", "nc", "ncat", "grpcurl", "ab", "hey", "siege",
37    "wrk",
38];
39
40/// Commands whose first argument is another script to run.
41const RUNNERS: &[&str] = &["source", ".", "bash", "sh", "zsh", "ksh"];
42
43/// Words that begin a statement, so the word after one is a command.
44const KEYWORDS: &[&str] = &[
45    "then", "do", "else", "elif", "fi", "done", "if", "while", "until", "for", "case", "esac",
46    "in", "function", "return", "local", "export", "declare", "readonly", "eval", "exec", "time",
47];
48
49struct Extractor<'source, 'tokens> {
50    source: &'source str,
51    tokens: &'tokens [Token],
52    facts: Facts,
53    /// The function whose body the walk is inside.
54    function: Option<(String, i32)>,
55    depth: i32,
56}
57
58/// The addresses and method a client command was given.
59///
60/// A URL in a shell script is usually written unquoted, so it arrives as one
61/// word rather than as a string literal - which is why arguments are rebuilt
62/// before they are read.
63fn endpoints(arguments: &[String]) -> Vec<String> {
64    let mut found = Vec::new();
65    let mut expecting_method = false;
66    for argument in arguments {
67        if expecting_method {
68            found.push(argument.clone());
69            expecting_method = false;
70            continue;
71        }
72        if argument == "-X" || argument == "--request" {
73            expecting_method = true;
74            continue;
75        }
76        if argument.contains("://") || argument.starts_with("localhost:") {
77            found.push(argument.clone());
78        }
79    }
80    found
81}
82
83mod extractor;
84
85#[cfg(test)]
86mod tests;