testsAndMisc/linux_configuration/scripts/single_use/utils/lookup_docs.sh
Krzysztof Rudnicki b83ce5d3c7
Some checks failed
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
billsplit CI / quality (push) Has been cancelled
reverse-survivors CI / check (push) Has been cancelled
Fail the build on any shellcheck finding; fix the 222 the old gate hid
The shellcheck hook ran with --severity=warning, so info- and style-level
findings never failed a commit. 222 had accumulated across 94 of 244 shell
files. They were not all style noise -- six were bugs:

  - Five scripts in single_use/misc/testsAndMisc-bash/ sourced
    $SCRIPT_DIR/../../lib/common.sh, one directory too shallow. Nothing exists
    at that path, so all five failed at runtime.
  - setup_personal_website.sh and setup_kcd2_dice_solver.sh called usage()
    (which ends in `exit 0`) and then `exit 1` for an unknown command. The
    exit 1 was unreachable, so a typo'd subcommand reported SUCCESS.
  - Six i3blocks scripts had a `# shellcheck source=` directive missing the
    scripts/periodic_background/ path segment. The stale path had silently
    disabled source-following for all of them.
  - diagnose_arch_performance.sh word-split a newline-joined file list; when
    the find came back empty, awk got no file operands, fell through to
    reading stdin, and the diagnostic hung.
  - lib/common.sh's handle_arg_help_or_unknown ended in an unreachable
    `return 0` while documenting "Returns: 0 if handled" -- a contract no code
    path could satisfy.
  - deploy.sh's SCRIPT_DIR is reassigned by config.sh, which it sources at top
    level, and config.sh can repoint it at the on-device path
    /data/local/tmp/focus_mode. Renamed to DEPLOY_DIR.

The SC1091 bulk (81 findings) was resolved rather than silenced: .shellcheckrc
sets external-sources + source-path=SCRIPTDIR so sourced files are actually
followed and analysed. That is what surfaced the i3blocks and deploy.sh bugs.

17 `# shellcheck disable=` directives remain, each with a written reason, and
only where the finding is provably wrong: injection-test payloads that must not
expand, literal source text being grepped for, POSIX sh scripts on the phone
where arrays do not exist, and source paths that only exist at runtime.
Everything else was fixed in code.

Verified in a CLEAN worktree, not just my own tree. config.sh sources the
gitignored config_secrets.sh, which exists locally but not in a fresh checkout;
a working-tree-only check passed while ci_mirror.sh -- which lints HEAD in a
clean worktree, exactly as CI does -- failed on it.

The gate is verified to bite: a file whose only finding is SC2016 (info) fails
the hook, while the old --severity=warning gate exits 0 on it.

third_party/ is excluded -- vendored upstream, matching the prettier hook.

Not fixed, reported: check_polling_antipatterns.sh carries a pre-existing
SC2034 disable that hides a real bug -- its `patterns` array is never used, so
the checker detects nothing. Confirmed unchanged by this commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 19:49:11 +02:00

943 lines
26 KiB
Bash
Executable File

#!/bin/bash
#==============================================================================
# Offline Documentation Lookup
# Searches downloaded documentation for terms
#
# Usage: ./lookup_docs.sh <term> [language] [--open] [--extract]
#
# Examples:
# ./lookup_docs.sh Path python # Find Path in Python docs
# ./lookup_docs.sh vector c_cpp # Find vector in C++ docs
# ./lookup_docs.sh map # Find map in all languages
# ./lookup_docs.sh --batch imports.txt # Lookup multiple terms from file
#==============================================================================
set -e
# Configuration
DOCS_DIR="${OFFLINE_DOCS_DIR:-$HOME/.local/share/offline-docs}"
INDEX_DIR="$DOCS_DIR/.index"
# Colors - only use if stdout is a terminal
if [ -t 1 ]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
else
RED=''
GREEN=''
BLUE=''
YELLOW=''
CYAN=''
NC=''
fi
#==============================================================================
# Python-specific lookup
#==============================================================================
lookup_python() {
local term="$1"
local in_module="$2" # Optional: look for term within this module
local doc_dir="$DOCS_DIR/python"
local result=""
local desc=""
# Normalize term (preserve case for True/False/None)
local term_lower
term_lower=$(echo "$term" | tr '[:upper:]' '[:lower:]')
# If looking for a term within a specific module
if [ -n "$in_module" ]; then
local module_lower
module_lower=$(echo "$in_module" | tr '[:upper:]' '[:lower:]')
if [ -f "$doc_dir/library/${module_lower}.html" ]; then
# Find anchor for the specific item in the module
local anchor
anchor=$(grep -oP "id=\"[^\"]*${term}[^\"]*\"" "$doc_dir/library/${module_lower}.html" 2>/dev/null | head -1 | sed 's/id="//;s/"//')
if [ -n "$anchor" ]; then
result="$doc_dir/library/${module_lower}.html#$anchor"
desc="Python: $in_module.$term"
else
# Just link to the module
result="$doc_dir/library/${module_lower}.html"
desc="Python: $term in module $in_module"
fi
echo "$result|$desc"
return 0
fi
fi
#--------------------------------------------------------------------------
# PRIORITY 1: Python keywords - map to exact documentation locations
#--------------------------------------------------------------------------
# Compound statements (reference/compound_stmts.html)
case "$term_lower" in
if | elif | else)
result="$doc_dir/reference/compound_stmts.html#if"
desc="Python: if statement"
;;
for)
result="$doc_dir/reference/compound_stmts.html#for"
desc="Python: for statement"
;;
while)
result="$doc_dir/reference/compound_stmts.html#while"
desc="Python: while statement"
;;
def)
result="$doc_dir/reference/compound_stmts.html#def"
desc="Python: function definition"
;;
class)
result="$doc_dir/reference/compound_stmts.html#class"
desc="Python: class definition"
;;
try | except | finally)
result="$doc_dir/reference/compound_stmts.html#try"
desc="Python: try statement"
;;
with)
result="$doc_dir/reference/compound_stmts.html#with"
desc="Python: with statement"
;;
async)
result="$doc_dir/reference/compound_stmts.html#async"
desc="Python: async definition"
;;
match | case)
result="$doc_dir/reference/compound_stmts.html#match"
desc="Python: match statement"
;;
esac
# Simple statements (reference/simple_stmts.html)
if [ -z "$result" ]; then
case "$term_lower" in
return)
result="$doc_dir/reference/simple_stmts.html#return"
desc="Python: return statement"
;;
pass)
result="$doc_dir/reference/simple_stmts.html#pass"
desc="Python: pass statement"
;;
break)
result="$doc_dir/reference/simple_stmts.html#break"
desc="Python: break statement"
;;
continue)
result="$doc_dir/reference/simple_stmts.html#continue"
desc="Python: continue statement"
;;
import | from)
result="$doc_dir/reference/simple_stmts.html#import"
desc="Python: import statement"
;;
raise)
result="$doc_dir/reference/simple_stmts.html#raise"
desc="Python: raise statement"
;;
assert)
result="$doc_dir/reference/simple_stmts.html#assert"
desc="Python: assert statement"
;;
yield)
result="$doc_dir/reference/simple_stmts.html#yield"
desc="Python: yield expression"
;;
del)
result="$doc_dir/reference/simple_stmts.html#del"
desc="Python: del statement"
;;
global)
result="$doc_dir/reference/simple_stmts.html#global"
desc="Python: global statement"
;;
nonlocal)
result="$doc_dir/reference/simple_stmts.html#nonlocal"
desc="Python: nonlocal statement"
;;
type)
result="$doc_dir/reference/simple_stmts.html#type"
desc="Python: type alias statement"
;;
esac
fi
# Expressions/operators (reference/expressions.html)
if [ -z "$result" ]; then
case "$term_lower" in
and)
result="$doc_dir/reference/expressions.html#and"
desc="Python: and operator"
;;
or)
result="$doc_dir/reference/expressions.html#or"
desc="Python: or operator"
;;
not)
result="$doc_dir/reference/expressions.html#not"
desc="Python: not operator"
;;
in)
result="$doc_dir/reference/expressions.html#in"
desc="Python: in operator"
;;
is)
result="$doc_dir/reference/expressions.html#is"
desc="Python: is operator"
;;
lambda)
result="$doc_dir/reference/expressions.html#lambda"
desc="Python: lambda expression"
;;
await)
result="$doc_dir/reference/expressions.html#await"
desc="Python: await expression"
;;
esac
fi
# Built-in constants (library/constants.html) - case-sensitive!
if [ -z "$result" ]; then
case "$term" in
True | False)
result="$doc_dir/library/constants.html#$term"
desc="Python: $term constant"
;;
None)
result="$doc_dir/library/constants.html#None"
desc="Python: None constant"
;;
Ellipsis)
result="$doc_dir/library/constants.html#Ellipsis"
desc="Python: Ellipsis constant"
;;
NotImplemented)
result="$doc_dir/library/constants.html#NotImplemented"
desc="Python: NotImplemented constant"
;;
esac
fi
# Verify file exists for keyword lookups
if [ -n "$result" ] && [ ! -f "${result%%#*}" ]; then
result=""
desc=""
fi
#--------------------------------------------------------------------------
# PRIORITY 2: Check if it's a module (pathlib, os, sys, etc.)
#--------------------------------------------------------------------------
if [ -z "$result" ] && [ -f "$doc_dir/library/${term_lower}.html" ]; then
result="$doc_dir/library/${term_lower}.html"
desc="Python module: $term"
fi
#--------------------------------------------------------------------------
# PRIORITY 3: Built-in functions (library/functions.html)
#--------------------------------------------------------------------------
if [ -z "$result" ] && [ -f "$doc_dir/library/functions.html" ]; then
if grep -q "id=\"$term_lower\"" "$doc_dir/library/functions.html" 2>/dev/null; then
result="$doc_dir/library/functions.html#$term_lower"
desc="Python built-in function: $term"
fi
fi
#--------------------------------------------------------------------------
# PRIORITY 4: Built-in types (library/stdtypes.html)
#--------------------------------------------------------------------------
if [ -z "$result" ]; then
case "$term_lower" in
str | string)
result="$doc_dir/library/stdtypes.html#str"
desc="Python: str type"
;;
int | integer)
result="$doc_dir/library/stdtypes.html#int"
desc="Python: int type"
;;
float)
result="$doc_dir/library/stdtypes.html#float"
desc="Python: float type"
;;
list)
result="$doc_dir/library/stdtypes.html#list"
desc="Python: list type"
;;
dict | dictionary)
result="$doc_dir/library/stdtypes.html#dict"
desc="Python: dict type"
;;
set)
result="$doc_dir/library/stdtypes.html#set"
desc="Python: set type"
;;
tuple)
result="$doc_dir/library/stdtypes.html#tuple"
desc="Python: tuple type"
;;
bool | boolean)
result="$doc_dir/library/stdtypes.html#boolean-values"
desc="Python: bool type"
;;
bytes)
result="$doc_dir/library/stdtypes.html#bytes"
desc="Python: bytes type"
;;
esac
fi
#--------------------------------------------------------------------------
# PRIORITY 5: Check for class/function in module docs (exact id match)
#--------------------------------------------------------------------------
if [ -z "$result" ]; then
local found_in
# Look for exact id match first
found_in=$(grep -l "id=\"$term\"" "$doc_dir/library/"*.html 2>/dev/null | head -1)
if [ -n "$found_in" ]; then
result="$found_in#$term"
local module
module=$(basename "$found_in" .html)
desc="Python: $term in module $module"
fi
fi
#--------------------------------------------------------------------------
# PRIORITY 6: Search in index
#--------------------------------------------------------------------------
if [ -z "$result" ] && [ -f "$INDEX_DIR/python_index.txt" ]; then
local index_match
index_match=$(grep -i "^$term " "$INDEX_DIR/python_index.txt" 2>/dev/null | head -1)
if [ -n "$index_match" ]; then
result=$(echo "$index_match" | cut -d' ' -f2-)
desc="Python: $term (from index)"
fi
fi
# NO full-text search fallback - it produces garbage results
# If we can't find a specific doc, return nothing (will fall back to online)
if [ -n "$result" ]; then
echo "$result|$desc"
fi
}
#==============================================================================
# C/C++ specific lookup
#==============================================================================
lookup_cpp() {
local term="$1"
local doc_dir="$DOCS_DIR/c_cpp"
local result=""
local desc=""
# Resolve symlink if present (system package installs to c_cpp/system/)
[ -L "$doc_dir/system" ] && doc_dir="$doc_dir/system"
# Common C headers
case "$term" in
stdio.h | stdio)
[ -f "$doc_dir/reference/cstdio/index.html" ] && result="$doc_dir/reference/cstdio/index.html"
[ -f "$doc_dir/en/c/io.html" ] && result="$doc_dir/en/c/io.html"
desc="C standard I/O header"
;;
stdlib.h | stdlib)
[ -f "$doc_dir/reference/cstdlib/index.html" ] && result="$doc_dir/reference/cstdlib/index.html"
[ -f "$doc_dir/en/c/memory.html" ] && result="$doc_dir/en/c/memory.html"
desc="C standard library header"
;;
string.h | cstring)
[ -f "$doc_dir/reference/cstring/index.html" ] && result="$doc_dir/reference/cstring/index.html"
desc="C string handling header"
;;
math.h | cmath)
[ -f "$doc_dir/reference/cmath/index.html" ] && result="$doc_dir/reference/cmath/index.html"
desc="C math header"
;;
esac
# C++ STL containers
case "$term" in
vector)
[ -f "$doc_dir/reference/vector/index.html" ] && result="$doc_dir/reference/vector/index.html"
[ -f "$doc_dir/en/cpp/container/vector.html" ] && result="$doc_dir/en/cpp/container/vector.html"
desc="C++ std::vector container"
;;
map)
[ -f "$doc_dir/reference/map/index.html" ] && result="$doc_dir/reference/map/index.html"
desc="C++ std::map container"
;;
string)
[ -f "$doc_dir/reference/string/index.html" ] && result="$doc_dir/reference/string/index.html"
desc="C++ std::string"
;;
iostream)
[ -f "$doc_dir/reference/iostream/index.html" ] && result="$doc_dir/reference/iostream/index.html"
desc="C++ iostream header"
;;
esac
# C keywords
case "$term" in
if | else | for | while | do | switch | case | break | continue | return | goto)
[ -f "$doc_dir/en/c/language/$term.html" ] && result="$doc_dir/en/c/language/$term.html"
[ -f "$doc_dir/en/cpp/language/$term.html" ] && result="$doc_dir/en/cpp/language/$term.html"
desc="C/C++ keyword: $term"
;;
int | char | float | double | void | long | short | unsigned | signed)
[ -f "$doc_dir/en/c/language/type.html" ] && result="$doc_dir/en/c/language/type.html"
desc="C/C++ type: $term"
;;
struct | union | enum | typedef)
[ -f "$doc_dir/en/c/language/$term.html" ] && result="$doc_dir/en/c/language/$term.html"
desc="C/C++ keyword: $term"
;;
esac
# Search in files if not found (use -L to follow symlinks)
if [ -z "$result" ]; then
local found
found=$(find -L "$doc_dir" -name "*${term}*" -type f 2>/dev/null | head -1)
if [ -n "$found" ]; then
result="$found"
desc="C/C++: $term"
fi
fi
if [ -n "$result" ]; then
echo "$result|$desc"
fi
}
#==============================================================================
# JavaScript/MDN specific lookup
# Searches the cloned MDN content repository
#==============================================================================
lookup_js() {
local term="$1"
local mdn_dir="$DOCS_DIR/mdn-content/files/en-us"
# Normalize term for searching
local term_lower
term_lower=$(echo "$term" | tr '[:upper:]' '[:lower:]')
# Handle common statement aliases (MDN uses if...else, try...catch, etc.)
local statement_aliases=(
"if:if...else"
"else:if...else"
"try:try...catch"
"catch:try...catch"
"finally:try...catch"
"do:do...while"
"while:while"
"for:for"
"switch:switch"
"case:switch"
"default:switch"
)
for alias in "${statement_aliases[@]}"; do
local key="${alias%%:*}"
local value="${alias##*:}"
if [ "$term_lower" = "$key" ]; then
local stmt_dir="$mdn_dir/web/javascript/reference/statements/$value"
if [ -d "$stmt_dir" ] && [ -f "$stmt_dir/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$stmt_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$stmt_dir/index.md|${title:-$term}"
return 0
fi
fi
done
# Handle boolean/null literals
case "$term_lower" in
true | false)
local bool_dir="$mdn_dir/web/javascript/reference/global_objects/boolean"
if [ -d "$bool_dir" ] && [ -f "$bool_dir/index.md" ]; then
echo "$bool_dir/index.md|Boolean ($term)"
return 0
fi
;;
null)
local null_dir="$mdn_dir/web/javascript/reference/operators/null"
if [ -d "$null_dir" ] && [ -f "$null_dir/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$null_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$null_dir/index.md|${title:-null}"
return 0
fi
;;
undefined)
local undef_dir="$mdn_dir/web/javascript/reference/global_objects/undefined"
if [ -d "$undef_dir" ] && [ -f "$undef_dir/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$undef_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$undef_dir/index.md|${title:-undefined}"
return 0
fi
;;
esac
# Search JavaScript reference directory structure (priority order)
local search_dirs=(
"$mdn_dir/web/javascript/reference/statements"
"$mdn_dir/web/javascript/reference/operators"
"$mdn_dir/web/javascript/reference/global_objects"
"$mdn_dir/web/javascript/reference/functions"
"$mdn_dir/web/javascript/reference/classes"
)
for search_dir in "${search_dirs[@]}"; do
if [ -d "$search_dir" ]; then
# Look for exact directory match (MDN uses directories with index.md)
local found_dir
found_dir=$(find "$search_dir" -maxdepth 2 -type d -iname "$term" 2>/dev/null | head -1)
if [ -n "$found_dir" ] && [ -f "$found_dir/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$found_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$found_dir/index.md|${title:-$term}"
return 0
fi
fi
done
# Search Web APIs - prioritize *_api directories for common terms
if [ -d "$mdn_dir/web/api" ]; then
# First try <term>_api directory (e.g., fetch_api, console_api)
local api_dir="$mdn_dir/web/api/${term_lower}_api"
if [ -d "$api_dir" ] && [ -f "$api_dir/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$api_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$api_dir/index.md|${title:-$term API}"
return 0
fi
# Then try exact top-level API interface (e.g., Console, Document, Element)
local found
found=$(find "$mdn_dir/web/api" -maxdepth 1 -type d -iname "$term" 2>/dev/null | head -1)
if [ -n "$found" ] && [ -f "$found/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$found/index.md|${title:-$term}"
return 0
fi
# Try window/<term> for global functions like alert, confirm, etc.
local window_method="$mdn_dir/web/api/window/${term_lower}"
if [ -d "$window_method" ] && [ -f "$window_method/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$window_method/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$window_method/index.md|${title:-Window.$term()}"
return 0
fi
# Search nested API methods
found=$(find "$mdn_dir/web/api" -maxdepth 3 -type d -iname "$term" 2>/dev/null | head -1)
if [ -n "$found" ] && [ -f "$found/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$found/index.md|${title:-$term}"
return 0
fi
fi
# Now try partial matches in Global Objects (e.g., Array.from, Object.keys)
if [ -d "$mdn_dir/web/javascript/reference/global_objects" ]; then
local found
found=$(find "$mdn_dir/web/javascript/reference/global_objects" -maxdepth 2 -type d -iname "*${term}*" 2>/dev/null | head -1)
if [ -n "$found" ] && [ -f "$found/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$found/index.md|${title:-$term}"
return 0
fi
fi
# Glossary as last resort
if [ -d "$mdn_dir/glossary" ]; then
local found
found=$(find "$mdn_dir/glossary" -maxdepth 1 -type d -iname "$term" 2>/dev/null | head -1)
if [ -n "$found" ] && [ -f "$found/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$found/index.md|${title:-$term}"
return 0
fi
fi
return 1
}
#==============================================================================
# Rust specific lookup
#==============================================================================
lookup_rust() {
local term="$1"
local result=""
local desc=""
if command -v rustup &>/dev/null; then
# Use rustup doc to get path
local rust_doc_path
rust_doc_path=$(rustup doc --path 2>/dev/null | head -1 | xargs dirname 2>/dev/null)
# Search in std docs
if [ -d "$rust_doc_path/std" ]; then
local found
found=$(find "$rust_doc_path/std" -name "*${term}*" -type f 2>/dev/null | head -1)
if [ -n "$found" ]; then
result="$found"
desc="Rust: $term"
fi
fi
fi
if [ -n "$result" ]; then
echo "$result|$desc"
fi
}
#==============================================================================
# Go specific lookup
#==============================================================================
lookup_go() {
local term="$1"
local result=""
local desc=""
if command -v go &>/dev/null; then
# Check if it's a stdlib package
if go doc "$term" &>/dev/null; then
result="go doc $term"
desc="Go package: $term (use 'go doc $term' to view)"
fi
fi
if [ -n "$result" ]; then
echo "$result|$desc"
fi
}
#==============================================================================
# Shell specific lookup
#==============================================================================
lookup_shell() {
local term="$1"
local doc_dir="$DOCS_DIR/shell"
local result=""
local desc=""
# Check bash builtins
if [ -f "$doc_dir/bash_builtins.txt" ]; then
if grep -q "=== $term ===" "$doc_dir/bash_builtins.txt" 2>/dev/null; then
result="$doc_dir/bash_builtins.txt"
desc="Bash builtin: $term"
fi
fi
# Check common commands
if [ -z "$result" ] && [ -f "$doc_dir/common_commands.txt" ]; then
if grep -q "^$term" "$doc_dir/common_commands.txt" 2>/dev/null; then
local cmd_desc
cmd_desc=$(grep "^$term" "$doc_dir/common_commands.txt" | head -1)
result="$doc_dir/common_commands.txt"
desc="Shell command: $cmd_desc"
fi
fi
# Try man page
if [ -z "$result" ]; then
local man_path
man_path=$(man -w "$term" 2>/dev/null)
if [ -n "$man_path" ]; then
result="man $term"
desc="Manual page: $term (use 'man $term' to view)"
fi
fi
if [ -n "$result" ]; then
echo "$result|$desc"
fi
}
#==============================================================================
# Generic lookup (searches all languages)
#==============================================================================
lookup_all() {
local term="$1"
# Try each language
for lang in python cpp js rust go shell; do
local result
result=$(lookup_$lang "$term" 2>/dev/null)
if [ -n "$result" ]; then
echo "$lang: $result"
fi
done
}
#==============================================================================
# Parse Python import and lookup the actual imported item
#==============================================================================
parse_python_import() {
local import_line="$1"
# Handle "from X import Y" format
if [[ $import_line =~ ^from[[:space:]]+([^[:space:]]+)[[:space:]]+import[[:space:]]+(.+) ]]; then
local module="${BASH_REMATCH[1]}"
local items="${BASH_REMATCH[2]}"
# Clean up items (remove parentheses, commas, etc.)
items=$(echo "$items" | sed 's/[(),]//g' | awk '{print $1}')
# Output: module and first imported item
echo "$module|$items"
return 0
fi
# Handle "import X" format
if [[ $import_line =~ ^import[[:space:]]+([^[:space:],]+) ]]; then
local module="${BASH_REMATCH[1]}"
echo "$module|"
return 0
fi
return 1
}
#==============================================================================
# Smart lookup for imports
#==============================================================================
lookup_import() {
local import_line="$1"
local lang="$2"
case "$lang" in
python)
local parsed
parsed=$(parse_python_import "$import_line")
if [ -n "$parsed" ]; then
local module item
module=$(echo "$parsed" | cut -d'|' -f1)
item=$(echo "$parsed" | cut -d'|' -f2)
# For "from X import Y", look up Y within module X's documentation
if [ -n "$item" ] && [ -n "$module" ]; then
local result
# Pass both item and module to lookup_python
result=$(lookup_python "$item" "$module")
if [ -n "$result" ]; then
echo "$result"
return 0
fi
fi
# Fall back to module documentation
lookup_python "$module"
fi
;;
c_cpp)
# Extract header name from #include <header> or #include "header"
local header
header=$(echo "$import_line" | sed -E 's/#include\s*[<"]([^">]+)[">]/\1/' | sed 's/\.h$//')
lookup_cpp "$header"
;;
javascript | typescript)
# Extract module from import/require
local module=""
# Match: from "module" or from 'module'
module=$(echo "$import_line" | grep -oP "from\s+['\"]\\K[^'\"]+")
if [ -z "$module" ]; then
# Match: require("module") or require('module')
module=$(echo "$import_line" | grep -oP "require\\(['\"]\\K[^'\"]+")
fi
[ -n "$module" ] && lookup_js "$module"
;;
*)
echo "Unknown language: $lang"
;;
esac
}
#==============================================================================
# Extract documentation content
#==============================================================================
extract_doc_content() {
local file="$1"
local term="$2"
local max_lines="${3:-20}"
if [[ $file == *.html ]]; then
# Extract text from HTML, find section about term
if command -v html2text &>/dev/null; then
html2text "$file" 2>/dev/null | grep -A"$max_lines" -i "$term" | head -"$max_lines"
elif command -v lynx &>/dev/null; then
lynx -dump -nolist "$file" 2>/dev/null | grep -A"$max_lines" -i "$term" | head -"$max_lines"
else
# Basic extraction
sed 's/<[^>]*>//g' "$file" | grep -A"$max_lines" -i "$term" | head -"$max_lines"
fi
elif [[ $file == *.json ]]; then
# Pretty print JSON section
grep -A5 "\"$term\"" "$file" 2>/dev/null
else
# Plain text
grep -A"$max_lines" -i "$term" "$file" | head -"$max_lines"
fi
}
#==============================================================================
# Main
#==============================================================================
usage() {
cat <<EOF
Usage: $0 <term> [language] [options]
Search offline documentation for a term.
Languages: python, cpp, c_cpp, js, javascript, rust, go, shell, all
Options:
--open Open the documentation file (requires xdg-open)
--extract Extract and display relevant content
--import Parse and lookup an import statement
--batch Process multiple terms from a file
Examples:
$0 Path python # Find Path in Python docs
$0 vector cpp # Find vector in C++ docs
$0 map # Find map in all languages
$0 --import "from pathlib import Path" python
$0 --batch imports.txt python
EOF
}
main() {
if [ $# -eq 0 ]; then
usage
exit 0
fi
local term=""
local lang=""
local action="lookup"
local open_file=false
local extract=false
while [ $# -gt 0 ]; do
case "$1" in
--open)
open_file=true
shift
;;
--extract)
extract=true
shift
;;
--import)
action="import"
shift
term="$1"
shift
;;
--batch)
action="batch"
shift
term="$1" # This is the file
shift
;;
--help | -h)
usage
exit 0
;;
python | cpp | c_cpp | c | js | javascript | ts | typescript | tsx | jsx | rust | go | shell | bash | all)
lang="$1"
shift
;;
*)
if [ -z "$term" ]; then
term="$1"
fi
shift
;;
esac
done
# Normalize language
case "$lang" in
c) lang="cpp" ;;
javascript | js | typescript | ts | jsx | tsx) lang="js" ;;
bash) lang="shell" ;;
"") lang="all" ;;
esac
case "$action" in
lookup)
if [ "$lang" = "all" ]; then
lookup_all "$term"
else
result=$("lookup_$lang" "$term" 2>/dev/null)
if [ -n "$result" ]; then
local file desc
file=$(echo "$result" | cut -d'|' -f1)
desc=$(echo "$result" | cut -d'|' -f2)
echo -e "${GREEN}Found:${NC} $desc"
echo -e "${BLUE}File:${NC} $file"
if $extract; then
echo ""
echo -e "${YELLOW}--- Content ---${NC}"
extract_doc_content "$file" "$term"
fi
if $open_file && [ -f "$file" ]; then
xdg-open "$file" 2>/dev/null &
fi
else
echo -e "${RED}Not found:${NC} $term in $lang documentation"
fi
fi
;;
import)
result=$(lookup_import "$term" "$lang")
if [ -n "$result" ]; then
echo -e "${GREEN}Import lookup:${NC} $term"
echo "$result"
else
echo -e "${RED}Could not parse import:${NC} $term"
fi
;;
batch)
if [ ! -f "$term" ]; then
echo "File not found: $term"
exit 1
fi
while IFS= read -r line || [ -n "$line" ]; do
[ -z "$line" ] && continue
[[ $line =~ ^# ]] && continue
echo -e "${CYAN}Looking up:${NC} $line"
lookup_import "$line" "$lang"
echo ""
done <"$term"
;;
esac
}
main "$@"