commit 248d7077f4bd68a20739284a623800e138238613 Author: Ethan P Date: Wed Jun 19 14:15:10 2019 -0700 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c05dc8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# System +.DS_Store +._* + +# Project +.download +bin + diff --git a/README.md b/README.md new file mode 100644 index 0000000..1095db5 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# bat-extras + +Bash scripts that integrate [bat](https://github.com/sharkdp/bat) with various command line tools. + + + +## Scripts + +- [`batgrep`](doc/batgrep.md) (ripgrep + bat) + + + +## Installation + +The scripts in this repository are designed to run as-is, provided that they aren't moved around. +This means that you're free to just symlink `src/[script].sh` to your local bin folder. + +If you would rather have self-contained scripts that you can place any run anywhere, you can use the `build.sh` script to create (and optionally install) them. + +**Build:** + +```bash +./build.sh [OPTIONS...] +``` + +This will combine and preprocess each script under the `src` directory, and create corresponding self-contained scripts in the `bin` folder. Any library scripts that are sourced using `source "${LIB}/[NAME].sh"` will be embedded automatically. + + + +**Minification:** + +There are three different options for minification: + +| Option | Description | +| --------------- | ---------------------------------------------------- | +| `--minify=none` | Nothing will be minified. | +| `--minify=lib` | Embedded library scripts will be minified. [default] | +| `--minify=all` | Everything will be minified. | + + + +This uses [bash_minifier](https://github.com/precious/bash_minifier) to perform minification, and requires Python 2 to be installed as either `python2` or `python`. + + + +**Installation:** + +You can also specify `--install` and `--prefix=PATH` to have the build script automatically install the scripts for all users on the system. You may need to run the build script as root. + diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..a8e8d5f --- /dev/null +++ b/build.sh @@ -0,0 +1,239 @@ +#!/usr/bin/env bash +# ----------------------------------------------------------------------------- +# bat-extras | Copyright (C) 2019 eth-p | MIT License +# +# Repository: https://github.com/eth-p/bat-extras +# Issues: https://github.com/eth-p/bat-extras/issues +# ----------------------------------------------------------------------------- +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DL="$HERE/.download" +BIN="$HERE/bin" +SRC="$HERE/src" +LIB="$HERE/lib" +source "${LIB}/print.sh" +source "${LIB}/opt.sh" +# ----------------------------------------------------------------------------- + +# Runs the next build step. +# +# Arguments: +# 1 -- The build step function name. +# @ -- The function arguments. +# +# Input: +# The unprocessed file data. +# +# Output: +# The processed file data. +next() { + local buffer="$(cat)" + "$@" <<< "$buffer" + return $? +} + +# Prints a build step message. +smsg() { + case "$2" in + "SKIP") printc " %{YELLOW} %{DIM}%s [skipped]%{CLEAR}\n" "$1" 1>&2;; + *) printc " %{YELLOW} %s...%{CLEAR}\n" "$1" 1>&2;; + esac +} + +# Build step: read +# Reads the file from its source. +# +# Arguments: +# 1 -- The source file. +# +# Output: +# The file contents. +step_read() { + smsg "Reading" + cat "$1" +} + +# Build step: preprocess +# Preprocesses the script. +# +# This will embed library scripts. +# +# Input: +# The original file contents. +# +# Output: +# The processed file contents. +step_preprocess() { + smsg "Preprocessing" + + local line + while IFS='' read -r line; do + # Skip certain lines. + [[ "$line" =~ ^LIB=.*$ ]] && continue + + # Embed library scripts. + if [[ "$line" =~ ^[[:space:]]*source[[:space:]]+[\"\']\$\{?LIB\}/([a-z-]+\.sh)[\"\'] ]]; then + echo "# --- BEGIN LIBRARY FILE: ${BASH_REMATCH[1]} ---" + cat "$LIB/${BASH_REMATCH[1]}" | { + if [[ "$OPT_MINIFY" = "lib" ]]; then + pp_strip_comments | pp_minify + else + cat + fi + } + echo "# --- END LIBRARY FILE ---" + continue + fi + + # Forward data. + echo "$line" + done +} + +# Build step: minify +# Minifies the output script. +# +# Input: +# The original file contents. +# +# Output: +# The minified file contents. +step_minify() { + if [[ "$OPT_MINIFY" != "all" ]]; then + smsg "Minifying" "SKIP" + cat + return 0 + fi + + smsg "Minifying" + printf "#!/usr/bin/env bash\n" + pp_minify +} + +# Build step: write +# Writes the output script to a file. +# +# Arguments: +# 1 -- The file to write to. +# +# Input: +# The file contents. +# +# Output: +# The file contents. +step_write() { + smsg "Building" + tee "$1" + chmod +x "$1" +} + +# Build step: write +# Optionally writes the output script to a file. +# +# Arguments: +# 1 -- The file to write to. +# +# Input: +# The file contents. +# +# Output: +# The file contents. + +step_write_install() { + if [[ "$OPT_INSTALL" != true ]]; then + smsg "Installing" "SKIP" + cat + return 0 + fi + + smsg "Installing" + tee "$1" + chmod +x "$1" +} + +# ----------------------------------------------------------------------------- +# Preprocessor. + +# Strips comments from a Bash source file. +pp_strip_comments() { + sed '/^[[:space:]]*#.*$/d' +} + +# Minify a Bash source file. +# https://github.com/precious/bash_minifier +pp_minify() { + local python="python" + if command -v python2 &>/dev/null; then + python="python2" + fi + + "$python" "$DL/minifier.py" + printf "\n" +} + +# ----------------------------------------------------------------------------- +# Options. +OPT_INSTALL=false +OPT_MINIFY="lib" +OPT_PREFIX="/usr/local" + +while shiftopt; do + case "$OPT" in + --install) OPT_INSTALL=true;; + --prefix) shiftval; OPT_PREFIX="$OPT_VAL";; + --minify) shiftval; OPT_MINIFY="$OPT_VAL";; + + *) printc "%{RED}%s: unknown option '%s'%{CLEAR}" "$PROGRAM" "$OPT"; + exit 1;; + esac +done + +if [[ "$OPT_INSTALL" = true ]]; then + printc "%{YELLOW}Installing to %{MAGENTA}%s%{YELLOW}.%{CLEAR}\n" "$OPT_PREFIX" 1>&2 +else + printc "%{YELLOW}This will not install the script.%{CLEAR}\n" 1>&2 + printc "%{YELLOW}Use %{BLUE}--install%{YELLOW} for a global install.%{CLEAR}\n\n" 1>&2 +fi + +# ----------------------------------------------------------------------------- +# Download resources. + +[[ -d "$DL" ]] || mkdir "$DL" +[[ -d "$BIN" ]] || mkdir "$BIN" + +if [[ "$OPT_MINIFY" != "none" ]] && ! [[ -f "$DL/minifier.py" ]]; then + printc "%{YELLOW}Downloading %{BLUE}https://github.com/precious/bash_minifier%{YELLOW}...%{CLEAR}\n" 1>&2 + curl -sL "https://gitcdn.xyz/repo/precious/bash_minifier/master/minifier.py" > "$DL/minifier.py" +fi + +# ----------------------------------------------------------------------------- +# Find files. + +SOURCES=() + +printc "%{YELLOW}Preparing scripts...%{CLEAR}\n" 1>&2 +for file in "$SRC"/*.sh; do + SOURCES+=("$file") +done + +# ----------------------------------------------------------------------------- +# Build files. + + +printc "%{YELLOW}Building scripts...%{CLEAR}\n" 1>&2 +file_i=0 +file_n="${#SOURCES[@]}" +for file in "${SOURCES[@]}"; do + ((file_i++)) + + filename="$(basename "$file" .sh)" + + printc " %{YELLOW}[%s/%s] %{MAGENTA}%s%{CLEAR}\n" "$file_i" "$file_n" "$file" 1>&2 + step_read "$file" |\ + next step_preprocess |\ + next step_minify |\ + next step_write "${BIN}/${filename}" |\ + next step_write_install "${OPT_PREFIX}/bin/${filename}" |\ + cat >/dev/null +done + + diff --git a/doc/batgrep.md b/doc/batgrep.md new file mode 100644 index 0000000..c13d31d --- /dev/null +++ b/doc/batgrep.md @@ -0,0 +1,66 @@ +# bat-extras: batgrep + +A script that combines [ripgrep](https://github.com/burntsushi/ripgrep) with bat's syntax highlighting and output formatting. + + + +## Command Line + +**Synopsis:** + +- `batgrep [OPTIONS] PATTERN [PATH...] ` + + + +**Options:** + +| Short | Long | Description | +| ----- | -------------------------- | ---------------------------------------------------------- | +| `-i` | `--ignore-case` | Use case insensitive searching. | +| `-A` | `--after-context=[LINES]` | Display the next *n* lines after a matched line. | +| `-B` | `--before-context=[LINES]` | Display the previous `n` lines before a matched line. | +| `-C` | `--context=[LINES]` | A combination of `--after-context` and `--before-context`. | +| | `--no-follow` | Do not follow symlinks. | + + + +**Options (Passthrough)**: +The following options are passed directly to ripgrep, and are not handled by this script. + +| Short | Long | Notes | +| ----- | -------------------------- | ------------------------------------------------------------ | +| `-F` | `--fixed-strings` | | +| `-U` | `--multiline` | | +| `-P` | `--pcre2` | | +| `-z` | `--search-zip` | | +| `-w` | `--word-regexp` | | +| | `--one-file-system` | | +| | `--multiline-dotall` | | +| | `--ignore` / `--no-ignore` | | +| | `--crlf` / `--no-crlf` | | +| | `--hidden` / `--no-hidden` | | +| `-E` | `--encoding` | This is unsupported by `bat`, and may cause issues when trying to display unsupported encodings. | +| `-g` | `--glob` | | +| `-t` | `--type` | | +| `-T` | `--type-not` | | +| `-m` | `--max-count` | | +| | `--max-depth` | | +| | `--iglob` | | +| | `--ignore-file` | | + + + +## Caveats + +**Differences from ripgrep:** + +- `--follow` is enabled by default for `batgrep`. + +- Not all the `ripgrep` options are supported. + + + +## Issues? + +If you find an issue or have a feature suggestion, make a pull request or issue through GitHub! +Contributions are always welcome. \ No newline at end of file diff --git a/lib/opt.sh b/lib/opt.sh new file mode 100644 index 0000000..ce25077 --- /dev/null +++ b/lib/opt.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# ----------------------------------------------------------------------------- +# bat-extras | Copyright (C) 2019 eth-p | MIT License +# +# Repository: https://github.com/eth-p/bat-extras +# Issues: https://github.com/eth-p/bat-extras/issues +# ----------------------------------------------------------------------------- +PROGRAM="$(basename "${BASH_SOURCE[0]}")" + +# Gets the next option passed to the script. +# +# Variables: +# OPT -- The option name. +# +# Returns: +# 0 -- An option was read. +# 1 -- No more options were read. +# +# Example: +# while shiftopt; do +# shiftval +# echo "$OPT = $OPT_VAL" +# done +shiftopt() { + # Ensure _ARGV exists and has the program arguments. + if [[ -z ${_ARGV+x} ]]; then + _ARGV=("${BASH_ARGV[@]}") + _ARGV_INDEX="$((${#_ARGV[@]} - 1))" + fi + + # Read the top of _ARGV. + [[ "$_ARGV_INDEX" -lt 0 ]] && return 1 + OPT="${_ARGV[$_ARGV_INDEX]}" + unset OPT_VAL + + if [[ "$OPT" =~ ^--[a-zA-Z-]+=.* ]]; then + OPT_VAL="${OPT#*=}" + OPT="${OPT%%=*}" + fi + + # Pop array. + ((_ARGV_INDEX--)) + return 0 +} + +# Gets the value for the current option. +# +# Variables: +# OPT_VAL -- The option value. +# +# Returns: +# 0 -- An option value was read. +# EXIT 1 -- No option value was available. +shiftval() { + # Skip if a value was already provided. + if [[ -z "${ARG_VAL+x}" ]]; then + return 0 + fi + + OPT_VAL="${_ARGV[$_ARGV_INDEX]}" + ((_ARGV_INDEX--)) + + # Error if no value is provided. + if [[ "$OPT_VAL" =~ -.* ]]; then + printc "%{RED}%s: '%s' requires a value%{CLEAR}\n" "$PROGRAM" "$ARG" + exit 1 + fi +} + diff --git a/lib/print.sh b/lib/print.sh new file mode 100644 index 0000000..a5ae8b0 --- /dev/null +++ b/lib/print.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# ----------------------------------------------------------------------------- +# bat-extras | Copyright (C) 2019 eth-p | MIT License +# +# Repository: https://github.com/eth-p/bat-extras +# Issues: https://github.com/eth-p/bat-extras/issues +# ----------------------------------------------------------------------------- + +# Printf, but with optional colors. +# This uses the same syntax and arguments as printf. +# +# Example: +# printc "%{RED}This is red %s.%{CLEAR}\n" "text" +# +printc() { + printf "$(sed "$_PRINTC_PATTERN" <<< "$1")" "${@:2}" +} + +# Initializes the color tags for printc. +# +# Arguments: +# color=on -- Turns on color output. +# color=off -- Turns off color output. +printc_init() { + case "$1" in + color=on) _PRINTC_PATTERN="$_PRINTC_PATTERN_ANSI";; + color=off) _PRINTC_PATTERN="$_PRINTC_PATTERN_PLAIN";; + + "") { + _PRINTC_PATTERN_ANSI="" + _PRINTC_PATTERN_PLAIN="" + + local name + local ansi + while read -r name ansi; do + if [[ -z "${name}" && -z "${ansi}" ]] || [[ "${name:0:1}" = "#" ]]; then + continue + fi + + ansi="$(sed 's/\\/\\\\/' <<< "$ansi")" + + _PRINTC_PATTERN_PLAIN="${_PRINTC_PATTERN_PLAIN}s/%{${name}}//g;" + _PRINTC_PATTERN_ANSI="${_PRINTC_PATTERN_ANSI}s/%{${name}}/${ansi}/g;" + done + + if [ -t 1 ]; then + _PRINTC_PATTERN="$_PRINTC_PATTERN_ANSI" + else + _PRINTC_PATTERN="$_PRINTC_PATTERN_PLAIN" + fi + };; + esac +} + +# Initialization. +printc_init <&2 + exit 1 + };; + + # Search + *) { + if [ -z "$PATTERN" ]; then + PATTERN="$OPT" + else + FILES+=("$OPT") + fi + };; + esac +done + +if [[ -z "$PATTERN" ]]; then + printc "%{RED}%s: no pattern provided%{CLEAR}\n" "$PROGRAM" 1>&2 + exit 1 +fi + +if "$OPT_FOLLOW"; then + RG_ARGS+=("--follow") +fi + +# Invoke ripgrep. +FOUND_FILES=() +FOUND=0 +FIRST_PRINT=true +LAST_LR=() +LAST_LH=() +LAST_FILE='' + +do_print() { + [[ -z "$LAST_FILE" ]] && return 0 + + # Print the separator. + "$FIRST_PRINT" && echo "$SEP" + FIRST_PRINT=false + + # Print the file. + unset LAST_LR[0] + unset LAST_LH[0] + bat "${BAT_ARGS[@]}" \ + "${LAST_LR[@]}" \ + "${LAST_LH[@]}" \ + --style="header,numbers" \ + "$LAST_FILE" + + # Print the separator. + echo "$SEP" +} + +while IFS=':' read -r file line column; do + ((FOUND++)) + + if [[ "$LAST_FILE" != "$file" ]]; then + do_print + LAST_FILE="$file" + LAST_LR='' + LAST_LH='' + fi + + # Calculate the context line numbers. + line_start=$((line - OPT_CONTEXT_BEFORE)) + line_end=$((line + OPT_CONTEXT_AFTER)) + [[ "$line_start" -gt 0 ]] || line_start='' + + LAST_LR+=("--line-range=${line_start}:${line_end}") + LAST_LH+=("--highlight-line=${line}") +done < <(rg --with-filename --vimgrep "${RG_ARGS[@]}" --sort path "$PATTERN" "${FILES[@]}") +do_print + +# Exit. +if [[ "$FOUND" -eq 0 ]]; then + exit 2 +fi +