From aeb271e60467029c29292254f56c9a3793d0ed84 Mon Sep 17 00:00:00 2001 From: Konstantin Gredeskoul Date: Fri, 19 May 2023 09:18:11 -0700 Subject: [PATCH] Adding several file-based functions & tree-rename (#141) --- .version | 2 +- lib/file.sh | 60 +++++++++-------- lib/files-normalize.sh | 145 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 32 deletions(-) create mode 100644 lib/files-normalize.sh diff --git a/.version b/.version index 944880f..e4604e3 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -3.2.0 +3.2.1 diff --git a/lib/file.sh b/lib/file.sh index 5853c50..23eab82 100644 --- a/lib/file.sh +++ b/lib/file.sh @@ -3,8 +3,7 @@ source "${BASHMATIC_HOME}/lib/file-helpers.sh" -# @description Creates a temporary file and returns it as STDOUT -# shellcheck disable=SC2120 +# @description Creates a temporary file and returns it as STDOUT # shellcheck disable=SC2120 function file.temp() { local host="${HOST:-${HOSTNAME:-$(hostname)}}" local user="${USER:-"$(whoami)"}" @@ -34,9 +33,9 @@ function dir.temp() { trap "rm -rf ${dir}" EXIT } -file.print-normalized-name() { +function file.print-normalized-name() { local file="$1" - echo "${file}" | tr '[:upper:]' '[:lower:]' | sed -E 's/ /-/g; s/[^A-Za-z0-9.\-]/-/g; s/---+/--/g' + echo -e "${file}" | ascii-pipe | tr '[:upper:]' '[:lower:]' | sed -E 's/ /-/g; s/[^A-Za-z0-9.\-]/-/g; s/---+/--/g' | tr -d '\r\n' } # @description This function will rename all files passed to it as follows: spaces @@ -47,7 +46,7 @@ file.print-normalized-name() { # file.normalize-files "My Word Document.docx" # # my-word-document.docx # -file.normalize-files() { +function file.normalize-files() { trap 'return 1' INT run.set-all abort-on-error local file @@ -78,15 +77,14 @@ file.normalize-files() { } # Replaces a given regex with a string -file.gsub() { +function file.gsub() { local file="$1" shift local find="$1" shift local replace="$1" shift - local - runtime_options="$*" + local runtime_options="$*" [[ ! -s "${file}" || -z "${find}" || -z "${replace}" ]] && { error "Invalid usage of file.sub — " \ @@ -110,7 +108,7 @@ function file.first-is-newer-than-second() { # Usage: # (( $(file.exists-and-newer-than "/tmp/file.txt" 30) )) && echo "Yes!" -file.exists-and-newer-than() { +function file.exists-and-newer-than() { local file="${1}" shift local minutes="${1}" @@ -123,7 +121,7 @@ file.exists-and-newer-than() { } # @description Ask the user whether to overwrite the file -file.ask.if-exists() { +function file.ask.if-exists() { local file="$1" shift local message="$*" @@ -148,7 +146,7 @@ file.ask.if-exists() { # @example # file.install-with-backup conf/.psqlrc ~/.psqlrc backup-strategy-function # -file.install-with-backup() { +function file.install-with-backup() { local source="$1"; shift if [[ ! -f "${source}" ]]; then error "file ${source} can not be found" @@ -195,22 +193,22 @@ file.install-with-backup() { } # @description Prints the file's last modified date -file.last-modified-date() { +function file.last-modified-date() { stat -f "%Sm" -t "%Y-%m-%d" "$1" } # @description Prints the year of the file's last modified date -file.last-modified-year() { +function file.last-modified-year() { stat -f "%Sm" -t "%Y" "$1" } # @description Prints the file's last modified date expressed as millisecondsd -file.last-modified-millis() { +function file.last-modified-millis() { echo -n "$(/usr/bin/stat -f %m "$1")000" } # Return one field of stat -s call on a given file. -file.stat() { +function file.stat() { local file="$1" local field="$2" @@ -232,7 +230,7 @@ file.stat() { } # @description Returns the file size in bytes -file.size() { +function file.size() { util.os if [[ ${BASHMATIC_OS} =~ linux ]]; then stat -c %s "$1" @@ -242,7 +240,7 @@ file.size() { } # @description Prints the file size expressed in Mb (and up to 1 decimal point) -file.size.mb() { +function file.size.mb() { local file="$1" shift local s=$(file.size "${file}") @@ -251,7 +249,7 @@ file.size.mb() { } # @description Prints the file size expressed in Gb (and up to 1 decimal point) -file.size.gb() { +function file.size.gb() { local file="$1" shift local s=$(file.size "${file}") @@ -260,20 +258,20 @@ file.size.gb() { } # @description For each argument prints only those that represent existing files -file.list.filter-existing() { +function file.list.filter-existing() { for file in "$@"; do [[ -f "${file}" ]] && echo "${file}" done } # @description For each argument prints only those that represent non-emtpy files -file.list.filter-non-empty() { +function file.list.filter-non-empty() { for file in "$@"; do [[ -s "${file}" ]] && echo "${file}" done } -file.source-if-exists() { +function file.source-if-exists() { local file for file in "$@"; do [[ -f "${file}" ]] && source "${file}" @@ -334,16 +332,16 @@ files.map.shell-scripts() { files.map "$1" '*.sh' "$2" } -file.extension.remove() { +function file.extension.remove() { local filename="$1" printf "${filename%.*}" } -file.strip.extension() { +function file.strip.extension() { file.extension.remove "$@" } -file.extension() { +function file.extension() { local filename="$1" printf "${filename##*.}" } @@ -352,7 +350,7 @@ file.extension() { # file.extension.replace .sh $(find lib -type f -name '*.bash') # replaces all files under lib/ mathcing *.sh and renames them # to the given extension. -file.extension.replace() { +function file.extension.replace() { local ext="$1" shift @@ -372,37 +370,37 @@ file.extension.replace() { } # @description Prints the number of lines in the file -file.count.lines() { +function file.count.lines() { [[ -f "$1" ]] || return 1 wc -l "$1" | awk '{print $1}' | tr -d '\n' } # @description Prints the number of lines in the file -file.count.words() { +function file.count.words() { [[ -f "$1" ]] || return 1 wc -w "$1" | awk '{print $1}' | tr -d '\n' } # @description Invokes UNIX find command searching for files (not folders) # matching the first argument in the name. -file.find() { +function file.find() { find . -name "*$1*" -type f -print } # @description Invokes UNIX find command searching for folders (not files) # matching the first argument in the name. -dir.find() { +function dir.find() { find . -name "*$1*" -type d -print } # @description Prints all folders sorted by size, and size printed in Mb -ls.mb(){ +function ls.mb(){ # du -k | grep -v '\''./.*\/'\' | sort -n | awk '{ printf("%20.1fMb %s\n", $1/1024, $2 )}' | tail -10 du -m -d 1 "$@" | sort -rn } # @description Prints all folders sorted by size, and size printed in Gb -ls.gb(){ +function ls.gb(){ # du -k | grep -v '\''./.*\/'\' | sort -n | awk '{ printf("%20.1fGb %s\n", $1/1024/1024, $2 )}' | tail -10 du -g -d 1 "$@" | sort -rn } diff --git a/lib/files-normalize.sh b/lib/files-normalize.sh new file mode 100644 index 0000000..f902522 --- /dev/null +++ b/lib/files-normalize.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# @description Renames files matching the input parameters to `find` by +# replacing spaces with dashes and lower casing the file. +function files.normalize-tree() { + local command="__files.normalize-tree" + ( "${command}" "$@" ) +} + +function __files.normalize-tree() { + local dry_run=false + local verbose=false + local interactive=false + local -a delete_files=( .DS_Store ) + local -a skip_files=( Icon Icon$(echo -e "\r") ) + local -a find_args + find_args=() + + while :; do + [[ -z $1 ]] && break + case $1 in + --dry-run|-n) + dry_run=true + shift + ;; + --verbose|-v) + verbose=true + shift + ;; + --interactive|-i) + interactive=true + shift + ;; + --help|-h) + printf "${bldgrn}USAGE:${bldylw} + files.normalize-tree [ --verbose | -v ] [ --dry-run | -n ] + [ --interactive | -i ] + < additional find arguments to find command> + +${bldgrn}DESCRIPTION: + ${clr}Given the search pattern to find, this function will rename all of the + files matching find parameteres (relative to the current directory) + to a lower case and replace spaces with dashes.\n\n" + return + ;; + *) + find_args+=( "$1" ) + shift + ;; + esac + done + + local find_command + if [[ ${find_args[*]} =~ find ]] ; then + find_command="${find_args[*]}" + else + find_command="find . -type f ${find_args[*]}" + fi + + info "Files will be searched using the following command:" + h1 " ❯ ${bldylw}${find_command}" + + local -a files + mapfile -t files < <(eval "${find_command}") + if [[ ${#files[@]} -eq 0 ]]; then + error "No files mathed search pattern [$*]" \ + "Please make sure that you escape any single quotes, like so:" \ + "files.normalize-tree -spaces [ --dry-run ] -name \'*.wav\'" + return 1 + else + h4 "Total of ${#files[@]} files matched." + fi + + ${interactive} && run.ui.ask "Should I proceed with the rename?" + + local show_warning=true + run.set-all abort-on-error show-output-on + for file in "${files[@]}"; do + ${verbose} && printf "processing file ${bldgrn}%s${clr}\n" "${file}" + [[ -f "${file}" ]] || continue + local command + local file_basename="$(basename "${file}")" + if array.includes "${skip_files[@]}" "${file_basename}"; then + info "File matched one of the skip files, skipping." + continue + elif array.includes "${delete_files[@]}" "${file_basename}"; then + info "File matched one of the delete files, deleting ." + command="rm -fv ${file}" + else + local f="$(echo "${file}" | tr -d '\n')"; + local newname="$(echo -e "${f}" | ascii-pipe | tr ' ' '-' | sed 's/--*/-/g' | tr '[:upper:]' '[:lower:]' | tr -d '\r\n')"; + local dir="$(dirname "${newname}")" + command="[[ -d \"${dir}\" ]] || mkdir -p \"${dir}\"; mv -vi \"${file}\" \"${newname}\"" + fi + + ${dry_run} && { + info "[dry-run] ❯ ${bldylw}${command}" + continue + } + + if ${interactive} ; then + local answer + ${show_warning} && { + h3bg "NOTE: if you answer 'a' or 'all' to any of the following questions" \ + "the rest of the files will be renamed as if the interactive mode was disabled." + show_warning=false + } + if [[ "${f}" == "${newname}" ]]; then + info "File [$f] is already normalized, skipping." + continue + fi + info "About to rename [$f] into [${newname}]..." + run.ui.ask-user-value answer "Rename the file? ${bldylw}(yes,y/no,n/all,a/quit,q): " + case ${answer} in + q|quit|Q|Quit) + info "Aborting the rename as requested." + exit 1 + ;; + y|Y|yes|Yes) + run "${command}" + ;; + n|N|no|No) + info "Skipping file ${f}..." + continue + ;; + a|all|A|All) + info "Turning off interactive mode..." + interactive=false + run "${command}" + ;; + *) + error "Answer ${answer} is invalid. Try again." + exit 1 + ;; + esac + else + if ${verbose}; then + run "${command}" + else + eval "${command}" + fi + sleep 0.05 + fi + done + return 0 +}