diff --git a/completions/makefile.completion.sh b/completions/makefile.completion.sh index d94054f03..a290f3fe5 100644 --- a/completions/makefile.completion.sh +++ b/completions/makefile.completion.sh @@ -1,4 +1,503 @@ #! bash oh-my-bash.module -# Add completion for Makefile -# see http://stackoverflow.com/a/38415982/1472048 -complete -W "\$(shopt -u nullglob; shopt -s nocaseglob; command grep -oE '^[a-zA-Z0-9_-]+:([^=]|\$)' *makefile 2>/dev/null | command sed 's/[^a-zA-Z0-9_-]*\$//')" make +# +# Makefile Completion with Descriptions +# ------------------------------------- +# +# Collects targets and variable names from Makefiles and included files in the +# current directory and provides tab completion with descriptions on repeated +# Tab presses. +# +# Requirements: +# - grep +# - awk +# - sed +# - column +# - bash (with shopt support) +# - terminal that supports ANSI escape codes for colorization +# +# Usage: +# - Press Tab once to complete targets +# - Press Tab twice to see target descriptions +# +# Example Makefile Structure: +# +# Makefile +# ├── scripts +# │ ├── ai.mk +# │ └── ... +# +# +# Makefile content: +# _____________________________________________________ +# +# include scripts/usage.mk +# include scripts/ai.mk +# include scripts/go.mk +# _____________________________________________________ +# +# +# Included content: +# _____________________________________________________ +# +# ## AI-related tasks +# .PHONY: ⚙️ # make all non-file targets phony +# +# ai-init: ⚙️ ## Initialize AI environment +# @echo "Setting up AI environment..." +# +# ai-files: ⚙️ ## Ensure AI agent files match provider-recommended structure +# @echo "Checking AI agent files..." +# +# # ... other targets ... +# _____________________________________________________ +# +# +# Example Output: +# _____________________________________________________ +# +# 17:52:16 user@machine myrepo ±|main ✗|→ make ai 👈️ 1st tab press +# ai_chat= ai-files ai-init 👈️ short completion list +# 17:52:16 user@machine myrepo ±|main ✗|→ make ai░ 👈️ 2nd tab press (adds repeat indicator) +# --Makefile-- and shows descriptions (if defined) +# --scripts/usage.mk-- +# --scripts/ai.mk-- +# ai-init Initialize AI environment +# ai-files Ensure AI agent files match provider-recommended structure +# ai-review Use AI to review recent changes in the repository +# ai-tech-only Use AI to identify and remove of non-technical content +# ai-update-docs Use AI to update documentation based on code changes +# ai_chat=code chat 👈️ also shows variable values +# --scripts/go.mk-- +# +# ai_chat= ai-files ai-init 👈️ still short completion list +# 17:52:16 user@machine myrepo ±|main ✗|→ make ai +# _____________________________________________________ +# +# Output will be colorized using ANSI escape codes. + + +# Bash Style Guide +# ---------------- +# This file uses an opinionated Bash style for better readability and maintainability. +# +# General Guidelines: +# - Use '_omb_completion_' prefix for function/variable names to avoid conflicts +# - Use '_omb_term_*' variables for terminal colors +# - Use 'local' variables where possible +# - Return early from functions to avoid deep nesting +# +# Human-Readable Bash: +# - Use 'test ' for conditionals instead of clumsy '[[ ... ]]'. +# Always prefer plain English keywords for best readability. +# - Break before 'then', 'do', 'else' to avoid complicated semi-colon lines +# - With 'then' and 'else' on new lines, write commands directly following them. +# This provides best readability for short if-then-else blocks, e.g.: +# +# if test "$condition" +# then command1 +# else command2 +# fi +# +# - Indent with 5 spaces for best alignment and readability. This matches best +# with the 'then' and 'else' blocks on new lines, e.g.: +# +# if test "$condition" +# then command1 +# command2 +# else command3 +# command3 +# fi +# +# - Use 'case' statements for multi-way branching instead of 'if-elif-else'; +# Use opening and closing parentheses for case patterns and put pattern at +# the beginning of the line: +# +# case "$var" in +# (pattern1) command1;; +# (pattern2) command2;; +# (*) command3;; +# esac +# +# - Use 'command ' to avoid alias/function conflicts +# - Use 'shopt' to set/unset shell options as needed +# - Use 'awk' for text processing where possible for better performance, +# readability, and flexibility. Break the awk script into multiple lines for +# better readability: +# command awk -F '' -v var="value" ' +# function () { } +# BEGIN { ... } # if needed +# { } # describe block +# ' +# + +# Find included Makefile paths (.mk) from given Makefiles. +_omb_completion_makefile_find_includes() { + shopt -u nullglob + shopt -s nocaseglob + local makefiles="$*" + if test -z "$makefiles" + then makefiles="*makefile" + fi + + local f="" + for f in $makefiles + do + # Included files are relative to the entrypoints. + # We need to combine the path of the entrypoints with the include paths. + local cwd="$(pwd)" + local dir="$(cd "$(dirname $f)" && pwd || echo "$cwd")" + if test "$dir" = "$cwd" + then dir="" # makefile is in current dir + else dir="$dir/" # makefile is somewhere else, includes need to be prefixed + fi + command awk -v dir="$dir" ' + /^include[[:space:]]/ {print dir $2;} + ' "$f" + done 2>/dev/null +} + +_omb_completion_makefile_entrypoints() { + local cwd="$(pwd)" + local dir="$cwd" + local file="*makefile" + while test $# -gt 0 + do + case "$1" in + (-f|--file) file="$2"; shift;; + (-C|--directory) dir="$2"; shift;; + (--file=*) file="${2#--file=}";; + (--directory=*) dir="${2#--directory=}";; + esac + shift + done + shopt -u nullglob + shopt -s nocaseglob + if test "$dir" = "$cwd" + then echo "$file" # makefile is in current dir + else echo "$dir/$file" # makefile is somewhere else + fi +} + +_omb_completion_makefile_files() { + local entrypoints="$(_omb_completion_makefile_entrypoints $COMP_LINE)" + local makefiles="$(_omb_completion_makefile_find_includes $entrypoints)" + shopt -u nullglob + shopt -s nocaseglob + # globber all files now + echo $entrypoints $makefiles +} + +# Complete regular files when there are no matches in the Makefiles. +# NOTE: This function is unused. We can use -o filenames and -o plusdirs +# to have the same effect. +_omb_completion_makefile_comp_glob() { + local path="$1" + shopt -u nullglob + shopt -s nocaseglob + local f="" + for f in $path* + do + if test -d "$f" + then echo "$f/" # dirs are completed with "/" + elif test -e "$f" + then echo "$f" # files are completed with "/" + # else: invalid path + fi + done +} + +# AWK command with args for colorized output and Makefile parsing. +# This is the main AWK program, with helper functions for parsing comments, +# variable assignments, and targets declarations. By default, it skips recipe +# lines and full-line comments starting with "#". +_omb_completion_makefile_awk() { + local red=$_omb_term_red + local grn=$_omb_term_green + local blu=$_omb_term_blue + local gry=$_omb_term_gray + local cyn=$_omb_term_cyan + local pur=$_omb_term_purple + local rst=$_omb_term_reset + local query="${COMP_WORDS[COMP_CWORD]}" + + local want_flag + case "$query" in + (-*) want_flag=1;; # user is querying for a flag with '-' or '--' + (*) ;; + esac + + command awk \ + -v red="$red" -v grn="$grn" -v blu="$blu" -v gry="$gry" -v cyn="$cyn" -v pur="$pur" -v rst="$rst" \ + -v TARGET="TARGET" -v VAR="VAR" -v FLAG="FLAG" -v OPT="OPT" \ + -v word="$query" \ + -v query="^$query" \ + -v want_flag="$want_flag" \ + -v exp_var='[a-zA-Z_][a-zA-Z0-9_]*' \ + -v exp_assign='^[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*[+:?]?=' \ + -v exp_export='^export[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*' \ + -v exp_target='^[a-z0-9_!-]+:.*' \ + -v exp_header='^---- .* ----$' \ + -v exp_comment='^#' \ + -v exp_recipe='^\t' \ + -v fs_assign='[[:space:]]*[+:?]?=[[:space:]]*|[[:space:]]*#+[[:space:]]*' \ + -v fs_target=':[[:space:]]*|[[:space:]]*#+[[:space:]]*' \ + -v fs_comment='[[:space:]]*#+[[:space:]]*' \ + -i <(echo ' + function trunc(s, w) { + if (length(s) > w) { return substr(s, 1, w-3) "..."; } + else { return s; } + } + function varname(line) { + n = split(line, parts, fs_assign); + return parts[1]; + } + function varvalue(line) { + n = split(line, parts, fs_assign); + return parts[2]; + } + function comment(line) { + if (line !~ /#/) { return ""; } + n = split(line, parts, fs_comment) + return parts[n] + } + function usage_header(line) { + return gry line rst + } + function usage_variable(name, line) { + val=trunc(varvalue(line), 20) + return " " cyn name rst "=" gry val rst "\t" comment(line) + } + function usage_target(name, line) { + return " " blu name rst "\t" comment(line) + } + function usage_flag(line) { + n = split(line, parts, fs_comment); + return " " pur parts[1] rst "\t" parts[2] + } + BEGIN { IGNORECASE=1; } + $0 ~ exp_recipe {next;} # skip recipe lines + $0 ~ exp_comment {next;} # skip comment lines + ') "$@" | uniq +} + +# Get targets with descriptions from a Makefile. +# Parses targets by splitting ":" and "#". +# Format: TARGET # +_omb_completion_makefile_targets() { + if test -n "$1" + then _omb_completion_makefile_awk ' + BEGIN { FS=fs_target; } + $0 ~ exp_target {printf "TARGET %s # %s\n", $1, $3;} + ' "$1" 2>/dev/null + fi +} + +# Get variables with descriptions from a Makefile. +# Parses targets by splitting "=" and "#". +# Format: VAR = # +_omb_completion_makefile_vars() { + if test -n "$1" + then _omb_completion_makefile_awk ' + $0 ~ exp_assign {printf "VAR %s = %s # %s\n", varname($0), varvalue($0), comment($0); } + ' "$1" 2>/dev/null + fi +} + +# Get make flags/options with descriptions. Format: +# FLAG - -- # +# OPT -