Skip to content

Instantly share code, notes, and snippets.

@DimitriGilbert
Created May 19, 2025 20:09
Show Gist options
  • Select an option

  • Save DimitriGilbert/da597f24e99047c72c2cd01be01c5ca5 to your computer and use it in GitHub Desktop.

Select an option

Save DimitriGilbert/da597f24e99047c72c2cd01be01c5ca5 to your computer and use it in GitHub Desktop.
#!/bin/bash
# @parseArger-begin
# @parseArger-help "Display file contents with separators" --option "help" --short-option "h"
# @parseArger-verbose --option "verbose" --level "0" --quiet-option "quiet"
# @parseArger-leftovers leftovers
_has_colors=0
if [ -t 1 ]; then # Check if stdout is a terminal
ncolors=$(tput colors 2>/dev/null)
if [ -n "$ncolors" ] && [ "$ncolors" -ge 8 ]; then
_has_colors=1
fi
fi
# @parseArger-declarations
# @parseArger opt separator "Separator to use between file contents" --default-value "========"
# @parseArger opt save "File to save the output" --short s
# @parseArger opt before "write stuff before" --short b --repeat
# @parseArger opt after "write stuff after" --short a --repeat
# @parseArger opt grep "Add files matching the pattern (grep -l)" --short g --repeat
# @parseArger opt grep-exclude "exclude pattern/dir for grep search" --short x --repeat
# @parseArger opt ts_error_fix "Check if TS error fix was good" --short t
# @parseArger opt file "file to add" --short f --repeat
# @parseArger-declarations-end
# @parseArger-utils
_helpHasBeenPrinted=1;
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)";
# @parseArger-utils-end
# @parseArger-parsing
__cli_arg_count=$#;
die()
{
local _ret=1
if [[ -n "$2" ]] && [[ "$2" =~ ^[0-9]+$ ]]; then
_ret="$2"
fi
test "${_PRINT_HELP:-no}" = yes && print_help >&2
log "$1" -3 >&2
exit "${_ret}"
}
begins_with_short_option()
{
local first_option all_short_options=''
first_option="${1:0:1}"
test "$all_short_options" = "${all_short_options/$first_option/}" && return 1 || return 0
}
# POSITIONALS ARGUMENTS
_positionals=();
_optional_positionals=();
# OPTIONALS ARGUMENTS
_arg_separator="========"
_arg_save=
_arg_before=()
_arg_after=()
_arg_grep=()
_arg_grep_exclude=()
_arg_ts_error_fix=
_arg_file=()
# FLAGS
# NESTED
# LEFTOVERS
_arg_leftovers=()
_verbose_level="0";
print_help()
{
_triggerSCHelp=1;
if [[ "$_helpHasBeenPrinted" == "1" ]]; then
_helpHasBeenPrinted=0;
echo -e "Display file contents with separators:"
echo -e " --separator <separator>: Separator to use between file contents [default: ' ======== ']"
echo -e " -s, --save <save>: File to save the output"
echo -e " -b, --before <before>: write stuff before, repeatable"
echo -e " -a, --after <after>: write stuff after, repeatable"
echo -e " -g, --grep <grep>: Add files matching the pattern (grep -l), repeatable"
echo -e " -x, --grep-exclude <grep-exclude>: exclude pattern/dir for grep search, repeatable"
echo -e " -t, --ts_error_fix <ts_error_fix>: Check if TS error fix was good"
echo -e " -f, --file <file>: file to add, repeatable"
echo -e "Usage :
$0 [--separator <value>] [--save <value>] [--before <value>] [--after <value>] [--grep <value>] [--grep-exclude <value>] [--ts_error_fix <value>] [--file <value>]";
fi
}
log() {
local _arg_msg="${1}";
local _arg_level="${2:-0}";
if [ "${_arg_level}" -le "${_verbose_level}" ]; then
case "$_arg_level" in
-3)
_arg_COLOR="\033[0;31m";
;;
-2)
_arg_COLOR="\033[0;33m";
;;
-1)
_arg_COLOR="\033[1;33m";
;;
1)
_arg_COLOR="\033[0;32m";
;;
2)
_arg_COLOR="\033[1;36m";
;;
3)
_arg_COLOR="\033[0;36m";
;;
*)
_arg_COLOR="\033[0m";
;;
esac
if [ "${_has_colors}" == "1" ]; then
echo -e "${_arg_COLOR}${_arg_msg}\033[0m";
else
echo "${_arg_msg}";
fi
fi
}
parse_commandline()
{
_positionals_count=0
while test $# -gt 0
do
_key="$1"
case "$_key" in
--separator)
test $# -lt 2 && die "Missing value for the option: '$_key'" 1
_arg_separator="$2"
shift
;;
--separator=*)
_arg_separator="${_key##--separator=}"
;;
-s|--save)
test $# -lt 2 && die "Missing value for the option: '$_key'" 1
_arg_save="$2"
shift
;;
--save=*)
_arg_save="${_key##--save=}"
;;
-s*)
_arg_save="${_key##-s}"
;;
-b|--before)
test $# -lt 2 && die "Missing value for the option: '$_key'" 1
_arg_before+=("$2")
shift
;;
--before=*)
_arg_before+=("${_key##--before=}")
;;
-b*)
_arg_before+=("${_key##-b}")
;;
-a|--after)
test $# -lt 2 && die "Missing value for the option: '$_key'" 1
_arg_after+=("$2")
shift
;;
--after=*)
_arg_after+=("${_key##--after=}")
;;
-a*)
_arg_after+=("${_key##-a}")
;;
-g|--grep)
test $# -lt 2 && die "Missing value for the option: '$_key'" 1
_arg_grep+=("$2")
shift
;;
--grep=*)
_arg_grep+=("${_key##--grep=}")
;;
-g*)
_arg_grep+=("${_key##-g}")
;;
-x|--grep-exclude)
test $# -lt 2 && die "Missing value for the option: '$_key'" 1
_arg_grep_exclude+=("$2")
shift
;;
--grep-exclude=*)
_arg_grep_exclude+=("${_key##--grep-exclude=}")
;;
-x*)
_arg_grep_exclude+=("${_key##-x}")
;;
-t|--ts_error_fix)
test $# -lt 2 && die "Missing value for the option: '$_key'" 1
_arg_ts_error_fix="$2"
shift
;;
--ts_error_fix=*)
_arg_ts_error_fix="${_key##--ts_error_fix=}"
;;
-t*)
_arg_ts_error_fix="${_key##-t}"
;;
-f|--file)
test $# -lt 2 && die "Missing value for the option: '$_key'" 1
_arg_file+=("$2")
shift
;;
--file=*)
_arg_file+=("${_key##--file=}")
;;
-f*)
_arg_file+=("${_key##-f}")
;;
-h|--help)
print_help;
exit 0;
;;
-h*)
print_help;
exit 0;
;;
--verbose)
if [ $# -lt 2 ];then
_verbose_level="$((_verbose_level + 1))";
else
_verbose_level="$2";
shift;
fi
;;
--quiet)
if [ $# -lt 2 ];then
_verbose_level="$((_verbose_level - 1))";
else
_verbose_level="-$2";
shift;
fi
;;
*)
_last_positional="$1"
_positionals+=("$_last_positional")
_positionals_count=$((_positionals_count + 1))
;;
esac
shift
done
}
handle_passed_args_count()
{
local _required_args_string=""
if [ "${_positionals_count}" -lt 0 ] && [ "$_helpHasBeenPrinted" == "1" ];then
_PRINT_HELP=yes die "FATAL ERROR: Not enough positional arguments - we require at least 0 (namely: $_required_args_string), but got only ${_positionals_count}.
${_positionals[*]}" 1;
fi
}
assign_positional_args()
{
local _positional_name _shift_for=$1;
_positional_names="";
_leftovers_count=$((${#_positionals[@]} - 0))
for ((ii = 0; ii < _leftovers_count; ii++));do
_positional_names="$_positional_names _arg_leftovers[$((ii + 0))]";
done
shift "$_shift_for"
for _positional_name in ${_positional_names};do
test $# -gt 0 || break;
if ! [[ "$_positional_name" =~ "_arg_leftovers" ]];then
eval "if [ \"\$_one_of${_positional_name}\" != \"\" ];then [[ \"\${_one_of${_positional_name}[*]}\" =~ \"\${1}\" ]];fi" || die "${_positional_name} must be one of: $(eval "echo \"\${_one_of${_positional_name}[*]}\"")" 1;
fi
eval "$_positional_name=\${1}" || die "Error during argument parsing, possibly an ParseArger bug." 1;
shift;
done
}
print_debug()
{
print_help
# shellcheck disable=SC2145
echo "DEBUG: $0 $@";
echo -e " separator: ${_arg_separator}";
echo -e " save: ${_arg_save}";
echo -e " before: ${_arg_before[*]}";
echo -e " after: ${_arg_after[*]}";
echo -e " grep: ${_arg_grep[*]}";
echo -e " grep-exclude: ${_arg_grep_exclude[*]}";
echo -e " ts_error_fix: ${_arg_ts_error_fix}";
echo -e " file: ${_arg_file[*]}";
echo -e " leftovers: ${_arg_leftovers[*]}";
}
on_interrupt() {
die Process aborted! 130;
}
parse_commandline "$@";
handle_passed_args_count;
assign_positional_args 1 "${_positionals[@]}";
trap on_interrupt INT;
# @parseArger-parsing-end
# print_debug "$@"
# @parseArger-end
# --- Simple and Elegant Implementation ---
shopt -s globstar # Enable ** recursive globbing
separator="$_arg_separator"
output_file="$_arg_save"
declare -a raw_file_specs=() # All specified files/dirs/grep results before expansion/unique
declare -a final_file_specs=() # Final list of file specs (path:cmds) to process
# --- Helper Function: Process a single file spec ---
# Input: file_spec (e.g., "path/to/file.txt" or "path/to/file.txt:sed 's/a/b/',grep 'c'")
# Output: Formatted string with separators and (transformed) content, or empty string on error
process_single_file_spec() {
local file_spec="$1"
local real_filepath=""
local commands_str=""
local _content=""
local IFS=',' # For splitting commands
# Separate filepath and commands
if [[ "$file_spec" == *:* ]]; then
real_filepath="${file_spec%%:*}"
commands_str="${file_spec#*:}"
else
real_filepath="$file_spec"
fi
# Check if file exists and is readable
if [[ ! -f "$real_filepath" ]]; then
log "Skipping: '$real_filepath' not found or not a regular file." -1 >&2
return 1
fi
if [[ ! -r "$real_filepath" ]]; then
log "Skipping: '$real_filepath' not readable." -1 >&2
return 1
fi
log "Processing: '$real_filepath'" 2
# Read file content
_content=$(cat -- "$real_filepath")
if [ $? -ne 0 ]; then
log "Error reading file: '$real_filepath'" -2 >&2
return 1
fi
# Apply commands sequentially if any
if [[ -n "$commands_str" ]]; then
local -a commands_arr
read -r -a commands_arr <<< "$commands_str" # Split commands by comma
unset IFS # Restore IFS
log "Applying commands to '$real_filepath': ${commands_arr[*]}" 3
local cmd
for cmd in "${commands_arr[@]}"; do
if [[ -z "$cmd" ]]; then
log "Skipping empty command for '$real_filepath'" -1 >&2
continue
fi
log " Executing: echo <content> | $cmd" 3
# Use a subshell and pipe content to the command
# Using eval is generally risky, but necessary here to interpret pipes/redirects within the command string
# Ensure the command string is reasonably trusted or sanitized if needed.
_content=$(echo "$_content" | eval "$cmd" 2> >(log "Command stderr for '$cmd': $(cat)" -1 >&2))
local cmd_status=$?
if [ $cmd_status -ne 0 ]; then
log "Command failed (exit code $cmd_status) for '$real_filepath': $cmd" -2 >&2
# Option: return 1 here to stop processing this file on error
fi
done
else
unset IFS # Restore IFS if no commands were present
fi
# Format output for this file
printf "\n%s\n%s\n%s\n%s" "$separator" "$real_filepath" "$separator" "$_content"
return 0
}
# --- Main Logic ---
# 1. Collect initial file specifications from positional args
log "Collecting files from arguments..." 1
raw_file_specs+=("${_arg_leftovers[@]}")
# 2. Add files from grep results
if [ "${#_arg_grep[@]}" -gt 0 ]; then
log "Searching for files via grep..." 1
# --- Build grep command array ---
# Start with the basic command
# Using standard grep options: recursive, list files, exclude common dirs, ignore binary
grep_base_cmd=(grep -rl --exclude-dir={.git,node_modules,dist,build,vendor,target} --binary-files=without-match)
# Check if grep exists *before* adding excludes
if ! command -v "${grep_base_cmd[0]}" > /dev/null 2>&1; then
die "FATAL: Cannot find grep command ('${grep_base_cmd[0]}'). Is it installed and in your PATH?" 127
fi
log "Found grep command: $(command -v "${grep_base_cmd[0]}")" 3
# Add user-defined excludes carefully
declare -a grep_exclude_opts=()
for exclude_pattern in "${_arg_grep_exclude[@]}"; do
# Ensure the pattern is not empty
if [[ -n "$exclude_pattern" ]]; then
# Simple check: if it looks like a dir path, use --exclude-dir, else --exclude
# Check if it exists as a directory OR contains a slash (heuristic for path)
if [[ -d "$exclude_pattern" ]] || [[ "$exclude_pattern" == */* ]]; then
grep_exclude_opts+=(--exclude-dir="$exclude_pattern")
log "Adding grep exclude dir: $exclude_pattern" 2
else
# Treat as a file pattern exclude
grep_exclude_opts+=(--exclude="$exclude_pattern")
log "Adding grep exclude pattern: $exclude_pattern" 2
fi
else
log "Skipping empty exclude pattern." -1
fi
done
# --- End build grep command array ---
# Run grep for each pattern provided via -g or --grep
for pattern in "${_arg_grep[@]}"; do
# Combine base command, excludes, pattern option, pattern, and target dir (.)
# Ensure pattern is treated as a single argument with -e
# The target directory is the current directory '.'
grep_cmd=("${grep_base_cmd[@]}" "${grep_exclude_opts[@]}" -e "$pattern" .)
# --- Debugging: Print the exact command array being executed ---
# This log runs if verbosity level is 2 or higher (-vv or --verbose 2)
if (( _verbose_level >= 2 )); then
log "Constructed grep command array for pattern '$pattern':" 2
# Use printf for safer expansion, especially if elements contain spaces/special chars
# This prints each argument on a new line, prefixed with its index
printf " Arg %d: [%s]\n" "${!grep_cmd[@]}" "${grep_cmd[@]}" >&2
fi
log "Running grep for pattern '$pattern'..." 1 # Log before execution
# --- Simplified Execution ---
# Execute grep, capture stdout (list of files). Let stderr go to script's stderr.
# Using process substitution $(...) captures stdout.
found_files_str=$( "${grep_cmd[@]}" )
grep_status=$? # Capture exit status *immediately* after the command finishes
# --- End Simplified Execution ---
# --- Status Handling ---
# Use arithmetic evaluation ((...)) for status check - safer than [ ... ] for numbers
if (( grep_status == 0 )); then
# Success - grep found one or more matching files
log "Grep pattern '$pattern' found matches." 1 # Log level 1 is sufficient
# Process the captured stdout (list of files)
while IFS= read -r line; do
# Ensure line isn't empty before adding to the list of files to process
[[ -n "$line" ]] && raw_file_specs+=("$line")
done <<< "$found_files_str" # Feed the captured string to the loop
elif (( grep_status == 1 )); then
# Normal exit code when grep finds no matches
log "Grep pattern '$pattern' found no matches." 1
else
# Actual error (e.g., status 2 for syntax error, 127 for command not found)
log "Grep command failed with status $grep_status for pattern '$pattern'." -3 # Log as critical error (-3)
# Provide the exact command again in case of error, even if not verbose, for easier debugging
log "Failed command array was:" -3 >&2
printf " Arg %d: [%s]\n" "${!grep_cmd[@]}" "${grep_cmd[@]}" >&2
# Optional: Consider exiting the script if a grep error is fatal
# die "Grep failed with status $grep_status, cannot continue." $grep_status
fi
# --- End Status Handling ---
done # End loop over patterns
fi # End if grep patterns were provided
# 3. Expand directories and create the final list (handling :cmd syntax)
log "Expanding directories and resolving specs..." 1
declare -A seen_paths # Track real paths to ensure uniqueness
for spec in "${raw_file_specs[@]}"; do
filepath_part="${spec%%:*}"
cmd_suffix=""
[[ "$spec" == *:* ]] && cmd_suffix=":${spec#*:}"
if [[ -d "$filepath_part" ]]; then
log "Expanding directory: $filepath_part" 2
# Use globstar to find files recursively
for f in "$filepath_part"/**/*; do
# Check if it's a file and not a directory/symlink etc.
if [[ -f "$f" && ! -L "$f" ]]; then
# Check uniqueness based on the actual file path
if [[ -z "${seen_paths[$f]}" ]]; then
final_file_specs+=("${f}${cmd_suffix}") # Append command suffix if any
seen_paths["$f"]=1
log " Added: ${f}${cmd_suffix}" 3
else
log " Skipping duplicate file from dir expansion: $f" 3
fi
fi
done
elif [[ -e "$filepath_part" ]]; then # Check if it exists (could be file or symlink)
if [[ -f "$filepath_part" || -L "$filepath_part" ]]; then # Allow files and symlinks to files
# Check uniqueness based on the spec itself (allows file.txt and file.txt:cmd)
# For more robust uniqueness (e.g. symlink -> original), realpath could be used,
# but sticking to simpler approach for now.
resolved_path="$filepath_part" # Basic uniqueness check
if [[ -z "${seen_paths[$resolved_path]}" ]]; then
final_file_specs+=("$spec")
seen_paths["$resolved_path"]=1
log " Added: $spec" 3
else
log " Skipping duplicate spec: $spec" 3
fi
else
log "Skipping non-file/non-directory: $filepath_part" -1 >&2
fi
else
log "Skipping non-existent path: $filepath_part" -1 >&2
fi
done
# 4. Sort the final list for predictable order (optional but nice)
IFS=$'\n' final_file_specs=($(sort <<<"${final_file_specs[*]}"))
unset IFS
log "Final unique file specs to process (${#final_file_specs[@]}):" 1
log "${final_file_specs[*]}" 2
# 5. Build the final output string
output=""
# Add 'before' content
if [ "${#_arg_before[@]}" -gt 0 ]; then
log "Adding 'before' content..." 1
for before in "${_arg_before[@]}"; do
output+="$before\n"
done
output+="\n$separator$separator\n" # Separator between before/after and main content
fi
# Process each unique file spec
log "Processing files..." 1
processed_count=0
for file_spec in "${final_file_specs[@]}"; do
processed_content=$(process_single_file_spec "$file_spec")
if [ $? -eq 0 ]; then
output+="$processed_content"
processed_count=$((processed_count + 1))
fi
done
log "Successfully processed $processed_count files." 1
# Add 'after' content
if [ "${#_arg_after[@]}" -gt 0 ]; then
log "Adding 'after' content..." 1
output+="\n$separator$separator\n" # Separator between main content and after
for after in "${_arg_after[@]}"; do
output+="$after\n" # Use printf for safer newline handling
done
fi
# 6. Output or save the result
if [[ -n "$output_file" ]]; then
log "Saving output to: $output_file" 1
echo -e "$output" > "$output_file"
if [ $? -ne 0 ]; then
die "Failed to write to output file: $output_file" 1
fi
log "Output saved successfully." 1
else
echo -e "$output"
fi
exit 0 # Success
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment