Created
May 19, 2025 20:09
-
-
Save DimitriGilbert/da597f24e99047c72c2cd01be01c5ca5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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