diff --git a/CHANGELOG.md b/CHANGELOG.md
index 936f210..954cd4a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,19 +1,48 @@
-## Changelog for v4.0 release
-Changes:
+## Changelog for v4.2 release (04/25/24)
+**Changes:**
+- Added multi-platform support (_FreeBSD_, _macOS_ and _Windows_)
+- Improved error detection and handling
+- Updated `README.md` to include information for newly supported platforms
+- Reformatted `README.md` and `CHANGELOG.md` to improve readability when using
+ plain text viewers/editors (i.e. without Markdown rendering)
+- Cleaned up and refactored code, applied various minor changes (functions,
+ comments, variables, prints, etc.)
+
+
+## Changelog for v4.1-dev release (04/13/24)
+
+Intermediate development release (has not been published).
+
+**Changes:**
+- Separated macro handling code/state into macro and command playback
+- Added support for using CTRL+D to abort currently running command playback
+- Added feature to optionally specify commands to be executed right after
+ startup via command line
+- Performed code cleanup and refactoring for all functions (underscore
+ variables, return values)
+- Applied various minor code modification (variables, comments, errors, etc.)
+
+
+## Changelog for v4.0 release (09/25/23)
+
+**Changes:**
- Added support for user-editable configuration file (i.e. `infiren.conf`)
- Added feature to load/save command history from/to file on startup/exit
-- Added feature to save/load macros to/from file (commands `save-macro`/`load-macro`)
+- Added feature to save/load macros to/from file (commands `save-macro`/
+ `load-macro`)
- Extended `undo` command to allow undoing/redoing entire macros
-- Reworked several commands to accomodate new commands (e.g. `start-macro` + `end-macro` -> `record-macro`)
+- Reworked several commands to accommodate new commands (e.g. `start-macro` +
+ `end-macro` -> `record-macro`)
- Applied various minor code modification (variables, comments, errors, etc.)
-## Changelog for v3.11 release
+
+## Changelog for v3.11 release (09/13/23)
Initial release (versions prior to v3.11 have not been published).
-Features:
+**Features:**
- Various editing commands (including regular expressions)
- File filtering (pattern, invert, case sensitive/insensitive)
- Recursive mode (to include files of subfolders)
@@ -21,6 +50,7 @@ Features:
- Undo/redo of last name-altering operation
- Actual renaming only occurs when when issuing `apply`/`save`
+
##
-_Last updated: 09/25/23_
+_Last updated: 04/25/24_
diff --git a/README.md b/README.md
index 9c4c735..4a12b1e 100644
--- a/README.md
+++ b/README.md
@@ -1,34 +1,89 @@
+
# Interactive File Renamer (InFiRen)
-Interactively rename multiple files from the command line. Especially useful to organize large collections (images, videos, music, etc.).
+
+Interactively rename multiple files from the command line. Especially useful to
+organize large collections of images, videos, music, etc.
+
## Donations
-I'm striving to become a full-time developer of [Free and open-source software (FOSS)](https://en.wikipedia.org/wiki/Free_and_open-source_software). Donations help me achieve that goal and are highly appreciated.
-
+I'm striving to become a full-time developer of [Free and open-source software
+(FOSS)](https://en.wikipedia.org/wiki/Free_and_open-source_software). Donations
+help me achieve that goal and are highly appreciated.
+
+
+
+
+
## Requirements
+
+**Operating System:**
+_Linux_, _FreeBSD_, _macOS_, _Windows_
+
**Dependencies:**
-_Bash (>=v4.0)_, _GNU find_ (part of [findutils](https://www.gnu.org/software/findutils/))
+_Bash_ (>=v5.0), _GNU find_ (part of [findutils](https://www.gnu.org/software/findutils/))
+-or- _BSD find_ (available on FreeBSD/macOS), _coreutils_ (provide basic tools
+like `dirname`, `basename`, `mkdir`, `sort`, etc.)
+
+**Note:**
+
+_macOS_ users might want to use [Homebrew](https://brew.sh/) to install
+missing dependencies.
+
+_Windows_ users need to set up a suitable runtime environment:
+[Cygwin](https://www.cygwin.com/),
+[MSYS2](https://www.msys2.org/),
+[Git for Windows](https://git-scm.com/download/win) or
+[Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/about)
+should all work fine.
+[Git for Windows](https://git-scm.com/download/win)
+might be a good choice to get started - it is reasonably lightweight, easy to
+to set up, meets all requirements out of the box and is also available as a
+portable version.
-**Platforms:**
-_Linux_ (NOTE: support for macOS/FreeBSD/Windows will be added in a future release)
## Download & Installation
-Refer to the [releases](https://github.com/fonic/infiren/releases) section for downloads links. There is no installation required. Simply extract the downloaded archive to a folder of your choice.
+
+Refer to the [releases](https://github.com/fonic/infiren/releases) section for
+downloads links. There is no actual installation required. Simply extract the
+downloaded archive to a folder of your choice.
+
## Configuration
-Open `infiren.conf` in your favorite text editor and adjust the settings to your liking. Refer to embedded comments for details. Refer to [this section](#configuration-options--defaults) for a listing of all configuration options and current defaults.
-## Usage
-To start _infiren_, run the following commands:
+Open `infiren.conf` in your favorite text editor and adjust the settings to
+your liking. Refer to embedded comments for details. Refer to
+[this section](#configuration-options) for a list of configuration options and
+current defaults.
+
+
+## Quick Start
+
+To start _InFiRen_, use the following command (specifying an initial directory
+is optional):
```
-$ cd infiren-v4.0
-$ ./infiren.sh [INITIAL-DIRECTORY]
+$ infiren.sh [INITIAL-DIRECTORY]
```
-Within _infiren_, use `help` to list available commands:
+Within _InFiRen_, enter `help` or refer to [this section](#interactive-commands)
+for a list of available _interactive commands_.
+
+Run `infiren.sh --help` or refer to [this section](#command-line-options) for
+a list of available _command line options_.
+
+
+## Showcase
+
+
+
+
+
+
+## Interactive Commands
+
+Available interactive commands:
```
-Available commands:
rs, replace-string STR REP Replace string STR with replacement REP
re, replace-regex RE TMP Match regular expression RE and replace
matching string according to template TMP
@@ -45,9 +100,10 @@ tr, trim, st, strip Trim leading and trailing whitespace
rm, record-macro Start/stop recording macro
vm, view-macro View macro contents
cm, clear-macro Clear macro contents
-pm, play-macro (Re-)Play commands from macro
-md, macro-delay VALUE Set delay in between commands for macro play-
- back to VALUE (in seconds, supports fractions)
+pm, play-macro Play back commands stored in macro
+pd, playback-delay VALUE Set delay in between commands for command
+ playback to VALUE (in seconds, fractions
+ are supported)
sm, save-macro NAME Save macro using name NAME to macro file
lm, load-macro NAME Load macro named NAME from macro file
@@ -58,7 +114,8 @@ hm, history-macro Create macro from command history
vh, view-history View command history
ch, clear-history Clear command history
-fp, filter-pattern PATTERN Set filter pattern to PATTERN and reload files
+fp, filter-pattern PATTERN Set filter pattern to PATTERN and reload
+ files
if, invert-filter Invert filter and reload files
fc, filter-case Toggle filter case and reload files
vf, view-filter View current filter state
@@ -76,25 +133,20 @@ help, usage Display this help/usage text
exit, quit Exit program (shortcut: CTRL+D)
```
-## Showcase
-![Animated GIF](https://raw.githubusercontent.com/fonic/infiren/master/SHOWCASE.gif)
+## Command Line Options
-## Configuration Options & Defaults
+Available command line options:
+```
+Usage: infiren.sh [INITIAL-DIRECTORY] [CMD]...
+Note: Commands are executed right after startup
+```
-Configuration options and current defaults:
-```sh
-# infiren.conf
-# ------------------------------------------------------------------------------
-# -
-# Interactive File Renamer (InFiRen) -
-# -
-# Created by Fonic -
-# Date: 04/23/19 - 09/25/23 -
-# -
-# ------------------------------------------------------------------------------
+## Configuration Options
+Available configuration options (and defaults):
+```sh
# Initial directory (if empty, current working directory is used)
INITIAL_DIRECTORY=""
@@ -111,9 +163,9 @@ FILTER_CASE="false"
# Initial recursive mode setting ('true'/'false'; 'true' == recursion enabled)
RECURSIVE_MODE="false"
-# Initial macro playback delay (in seconds, fractions are supported; '0' == no
-# delay)
-MACRO_DELAY="0.25"
+# Initial command playback delay (in seconds, fractions are supported; '0' ==
+# no delay)
+PLAYBACK_DELAY="0.25"
# Options passed to 'sort' when sorting file/folder listings (see 'man sort'
# for valid/available options)
@@ -137,6 +189,7 @@ HISTORY_FILE="${APP_DIR}/${APP_NAME}.hst"
MACROS_FILE="${APP_DIR}/${APP_NAME}.mac"
```
+
##
-_Last updated: 09/25/23_
+_Last updated: 04/25/24_
diff --git a/SHOWCASE.gif b/SHOWCASE.gif
index fb98820..c5b6c66 100644
Binary files a/SHOWCASE.gif and b/SHOWCASE.gif differ
diff --git a/infiren.conf b/infiren.conf
index 13e7e45..b67e758 100644
--- a/infiren.conf
+++ b/infiren.conf
@@ -5,7 +5,7 @@
# Interactive File Renamer (InFiRen) -
# -
# Created by Fonic -
-# Date: 04/23/19 - 09/25/23 -
+# Date: 04/23/19 - 04/25/24 -
# -
# ------------------------------------------------------------------------------
@@ -25,9 +25,9 @@ FILTER_CASE="false"
# Initial recursive mode setting ('true'/'false'; 'true' == recursion enabled)
RECURSIVE_MODE="false"
-# Initial macro playback delay (in seconds, fractions are supported; '0' == no
-# delay)
-MACRO_DELAY="0.25"
+# Initial command playback delay (in seconds, fractions are supported; '0' ==
+# no delay)
+PLAYBACK_DELAY="0.25"
# Options passed to 'sort' when sorting file/folder listings (see 'man sort'
# for valid/available options)
diff --git a/infiren.sh b/infiren.sh
index 534ca2e..61b31ff 100755
--- a/infiren.sh
+++ b/infiren.sh
@@ -5,22 +5,49 @@
# Interactive File Renamer (InFiRen) -
# -
# Created by Fonic -
-# Date: 04/23/19 - 09/25/23 -
+# Date: 04/23/19 - 04/25/24 -
# -
# ------------------------------------------------------------------------------
+# --------------------------------------
+# -
+# Early birds -
+# -
+# --------------------------------------
+
+# Check if running Bash and required version (NOTE: this check does not rely on
+# Bashism to make sure it is guaranteed to work on any POSIX shell)
+if [ -z "${BASH_VERSION}" ] || [ "${BASH_VERSION%%.*}" -lt 5 ]; then
+ echo -e "\e[1;31mError: this script requires Bash >= v5.0 to run, aborting.\e[0m"
+ exit 1
+fi
+
+# Set up error handler (exit on unbound variables and on unhandled errors)
+set -ueE; trap "echo -e \"\e[1;31mError: an unhandled error occurred on line \${LINENO}, aborting.\e[0m\"; exit 1" ERR
+
+
# --------------------------------------
# -
# Globals -
# -
# --------------------------------------
-# Application info
+# Application info (NOTE: realpath is not guaranteed to be available, e.g.
+# macOS does not have it while FreeBSD does; stripping trailing '/' off of
+# APP_DIR to account for root directory, i.e. '/'; could use ${APP_DIR%/}
+# instead to build paths, but that would be needlessly confusing for users
+# when being used within config file)
APP_TITLE="Interactive File Renamer (InFiRen)"
-APP_VERSION="4.0 (09/25/23)"
-APP_DIR="$(dirname -- "$(realpath -- "$0")")"
-APP_FILE="$(basename -- "$(realpath -- "$0")")"
+APP_VERSION="4.2 (04/25/24)"
+if command -v realpath >/dev/null; then
+ APP_DIR="$(dirname -- "$(realpath -- "$0")")"
+ APP_FILE="$(basename -- "$(realpath -- "$0")")"
+else
+ APP_DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
+ APP_FILE="$(basename -- "$0")"
+fi
+APP_DIR="${APP_DIR%/}"
APP_NAME="${APP_FILE%.*}"
APP_CONFIG="${APP_DIR}/${APP_NAME}.conf"
@@ -28,8 +55,9 @@ APP_CONFIG="${APP_DIR}/${APP_NAME}.conf"
PROMPT_CMD="cmd> "
PROMPT_EDIT="edit> "
-# Help/usage text explaining available commands
-read -r -d '' HELP_COMMANDS <<- EOD
+# Help/usage text explaining available commands (NOTE: masking errors as
+# read will encounter EOF, which will trip error handler if left unmasked)
+read -r -d '' HELP_COMMANDS <<- EOD || :
rs, replace-string STR REP Replace string STR with replacement REP
re, replace-regex RE TMP Match regular expression RE and replace
matching string according to template TMP
@@ -46,9 +74,10 @@ read -r -d '' HELP_COMMANDS <<- EOD
rm, record-macro Start/stop recording macro
vm, view-macro View macro contents
cm, clear-macro Clear macro contents
- pm, play-macro (Re-)Play commands from macro
- md, macro-delay VALUE Set delay in between commands for macro play-
- back to VALUE (in seconds, supports fractions)
+ pm, play-macro Play back commands stored in macro
+ pd, playback-delay VALUE Set delay in between commands for command
+ playback to VALUE (in seconds, fractions
+ are supported)
sm, save-macro NAME Save macro using name NAME to macro file
lm, load-macro NAME Load macro named NAME from macro file
@@ -59,7 +88,8 @@ read -r -d '' HELP_COMMANDS <<- EOD
vh, view-history View command history
ch, clear-history Clear command history
- fp, filter-pattern PATTERN Set filter pattern to PATTERN and reload files
+ fp, filter-pattern PATTERN Set filter pattern to PATTERN and reload
+ files
if, invert-filter Invert filter and reload files
fc, filter-case Toggle filter case and reload files
vf, view-filter View current filter state
@@ -100,9 +130,9 @@ FILTER_CASE="false"
# Initial recursive mode setting ('true'/'false'; 'true' == recursion enabled)
RECURSIVE_MODE="false"
-# Initial macro playback delay (in seconds, fractions are supported; '0' == no
-# delay)
-MACRO_DELAY="0.25"
+# Initial command playback delay (in seconds, fractions are supported; '0' ==
+# no delay)
+PLAYBACK_DELAY="0.25"
# Options passed to 'sort' when sorting file/folder listings (see 'man sort'
# for valid/available options)
@@ -133,6 +163,7 @@ MACROS_FILE="${APP_DIR}/${APP_NAME}.mac"
# --------------------------------------
# Print normal/hilite/good/warn/error/debug message [$*: message]
+# NOTE: warnings/errors are NOT sent to stderr (as script is interactive)
function printn() { echo -e "$*"; }
function printh() { echo -e "\e[1m$*\e[0m"; }
function printg() { echo -e "\e[1;32m$*\e[0m"; }
@@ -140,7 +171,8 @@ function printw() { echo -e "\e[1;33m$*\e[0m"; }
function printe() { echo -e "\e[1;31m$*\e[0m"; }
function printd() { echo -e "\e[1;30m$*\e[0m"; }
-# Ask yes/no question [$1: question, $2: newline before ('true'/'false'; default: 'true'), $3: newline after ('true'/'false'; default: 'false')]
+# Ask user yes/no question [$1: question, $2: newline before ('true'/'false';
+# default: 'true'), $3: newline after ('true'/'false'; default: 'false')]
# Return value: 0 == yes, 1 == no
function ask_yes_no() {
local input result
@@ -157,21 +189,45 @@ function ask_yes_no() {
return ${result}
}
-# Generate list of files in current directory [$1: name of target array, $2: recursive mode, $3: filter invert, $4: filter case, $5: filter pattern]
+# Ask user to hit ENTER to continue [$1: name of print function, $2: message]
+# Return value: 0 == ENTER, 1 == CTRL+D
+function ask_hit_enter() {
+ "$1" "$2"
+ read -s || return 1
+ return 0
+}
+
+# Generate list of files in current directory [$1: name of target array, $2:
+# recursive mode, $3: filter invert, $4: filter case, $5: filter pattern]
+# NOTE:
+# Using workaround to account for non-GNU find, which does NOT support option
+# '-printf': without '-printf', find results are prepended with './', which
+# messes up sorting (version sort in particular) and thus needs to be stripped
+# BEFORE sorting is performed; leaving original solution as comment for future
+# reference
function generate_file_list() {
local _recursive_opts=() _filter_opts=()
[[ "$2" == "false" ]] && _recursive_opts+=("-maxdepth" "1")
[[ "$3" == "true" ]] && _filter_opts+=("-not")
[[ "$4" == "true" ]] && _filter_opts+=("-name" "$5") || _filter_opts+=("-iname" "$5")
- readarray -t "$1" < <(find . -mindepth 1 "${_recursive_opts[@]}" -type f "${_filter_opts[@]}" -printf "%P\n" | sort "${SORT_OPTS[@]}")
+ #readarray -t "$1" < <(find . -mindepth 1 "${_recursive_opts[@]}" -type f "${_filter_opts[@]}" -printf "%P\n" | sort "${SORT_OPTS[@]}")
+ readarray -t "$1" < <(find . -mindepth 1 "${_recursive_opts[@]}" -type f "${_filter_opts[@]}" | cut -c 3- | sort "${SORT_OPTS[@]}")
}
# Generate list of folders in current directory [$1: name of target array]
+# NOTE:
+# Using workaround to account for non-GNU find, which does NOT support option
+# '-printf': without '-printf', find results are prepended with './', which
+# messes up sorting (version sort in particular) and thus needs to be stripped
+# BEFORE sorting is performed; leaving original solution as comment for future
+# reference; using '-mindepth 1' to keep '.' out of find results
function generate_folder_list() {
- readarray -t "$1" < <(find . -mindepth 1 -maxdepth 1 -type d -printf "%f\n" | sort "${SORT_OPTS[@]}")
+ #readarray -t "$1" < <(find . -mindepth 1 -maxdepth 1 -type d -printf "%f\n" | sort "${SORT_OPTS[@]}")
+ readarray -t "$1" < <(find . -mindepth 1 -maxdepth 1 -type d | cut -c 3- | sort "${SORT_OPTS[@]}")
}
-# Print array (one line per element) [$1: name of array, $2: display indices (true/false)]
+# Print array (one line per element) [$1: name of array, $2: display indices
+# (true/false)]
function print_array() {
local -n _array="$1"
if [[ "$2" == "true" ]]; then
@@ -199,17 +255,39 @@ function copy_array() {
# Return value: 0 == equal, 1 == not equal
# NOTE: arrays are equal if same amount of items and all items match
function compare_arrays() {
- local -n _arr1="$1"
- local -n _arr2="$2"
+ local -n _array1="$1"
+ local -n _array2="$2"
local _i
- (( ${#_arr1[@]} == ${#_arr2[@]} )) || return 1
- for (( _i=0; _i < ${#_arr1[@]}; _i++ )); do
- [[ "${_arr1[_i]}" == "${_arr2[_i]}" ]] || return 1
+ (( ${#_array1[@]} == ${#_array2[@]} )) || return 1
+ for (( _i=0; _i < ${#_array1[@]}; _i++ )); do
+ [[ "${_array1[_i]}" == "${_array2[_i]}" ]] || return 1
done
return 0
}
-# Split string into array [$1: string, $2: separator character, $3: escape character, $4: maximum items, $5: name of target array]
+# Print command about to be played back [$1: cmd, $2: delay]
+# Return value: 0 == continue playback, 1 == abort playback
+# NOTE:
+# - 'if ! read ...; then if (( $? == 1 )); then ... fi; fi' will NOT work as
+# expected as $? will always be 0, thus second 'if' is never triggered (not
+# sure why); using 'read ... || { (( $? == 1 )) && ...; }' instead, which
+# works as expected
+# - For delay == 0, 'read -t 0 ...' would return 1, which would trip CTRL+D
+# detection -> added shortcut (which relies on normalized delay argument,
+# e.g. '000.000' or '0.0' -> normalized to just '0') to bypass read calls
+function playback_print() {
+ local cmd="$1" delay="$2"
+ [[ "${delay}" == "0" ]] && { echo "${PROMPT_CMD}${cmd}"; return 0; } # shortcut for delay == 0
+ echo -n "${PROMPT_CMD}" # print command prompt
+ read -s -t "${delay}" || { (( $? == 1 )) && return 1; } # delay, detect CTRL-D (abort playback)
+ echo -n "${cmd}" # print command
+ read -s -t "${delay}" || { (( $? == 1 )) && return 1; } # delay, detect CTRL-D (abort playback)
+ echo
+ return 0
+}
+
+# Split string into array [$1: string, $2: separator character, $3: escape
+# character, $4: maximum items, $5: name of target array]
# NOTE:
# - Maximum items: splitting ends after this many items, rest of string is
# stored as last item in array; set to 0 to disable (i.e. split untils EOS)
@@ -217,7 +295,7 @@ function compare_arrays() {
# string to disable escaping
function split_string() {
local _string="$1" _schar="$2" _echar="$3" _maxitems="$4"
- local -n _array="$5"; _array=()
+ local -n _dstarr="$5"; _dstarr=()
local _i _char="" _item="" _items=0 _escape=0 _quote=0 _qchar=""
for (( _i=0; _i < ${#_string}; _i++ )); do
_char="${_string:_i:1}"
@@ -241,8 +319,8 @@ function split_string() {
fi
fi
if [[ "${_char}" == "${_schar}" ]] && (( ${_quote} == 0 )); then
- #[[ -n "${_item}" ]] && _array+=("${_item}")
- _array+=("${_item}")
+ #[[ -n "${_item}" ]] && _dstarr+=("${_item}")
+ _dstarr+=("${_item}")
_item=""
_items=$((_items + 1))
if (( ${_maxitems} > 0 && ${_items} >= ${_maxitems} )); then
@@ -253,10 +331,11 @@ function split_string() {
fi
_item+="${_char}"
done
- [[ -n "${_item}" ]] && _array+=("${_item}") || : # '|| :' is required for this to work with 'set -e' (could use 'return 0' instead)
+ [[ -n "${_item}" ]] && _dstarr+=("${_item}") || : # '|| :' is required for this to work with 'set -e' (could use 'return 0' instead)
}
-# Regular expression string replace [$1: input string, $2: regex to search for and replace, $3: replacement template, $4: name of output variable]
+# Regular expression string replace [$1: input string, $2: regex to search
+# for and replace, $3: replacement template, $4: name of output variable]
# NOTE:
# - Replacement template uses KDE's Kate's syntax format, for example:
# string '10x04', regex '([0-9]{2})x([0-9]{2})', template 'S\1E\2'
@@ -273,69 +352,69 @@ function replace_regex() {
local _in="$1" _re="$2" _reptmp="$3"
local -n _out="$4"; _out=""
local _repstr _escape _grpidx _i _char _match
- if [[ -z "${_re}" ]]; then # trivial case: empty regex -> output = input, no further processing
+ if [[ -z "${_re}" ]]; then # trivial case: empty regex -> output = input, no further processing
_out="${_in}"
return
fi
- while [[ -n "${_in}" ]] && [[ "${_in}" =~ ${_re} ]]; do # loop while there is still input left and regex matches
- _repstr=""; _escape=0; _grpidx="" # generate replacement string from replacement template and regex matches
- for (( _i=0; _i < ${#_reptmp}; _i++ )); do # by processing template character by character
+ while [[ -n "${_in}" ]] && [[ "${_in}" =~ ${_re} ]]; do # loop while there is still input left and regex matches
+ _repstr=""; _escape=0; _grpidx="" # generate replacement string from replacement template and regex matches
+ for (( _i=0; _i < ${#_reptmp}; _i++ )); do # by processing template character by character
_char="${_reptmp:${_i}:1}"
- if (( ${_escape} == 1 )); then # if escaping is active and current character is ...
- if [[ "${_char}" == [0-9] ]]; then # ... a digit:
- _grpidx+="${_char}" # found group reference (e.g. '\12') -> add digit to group index, cycle loop
+ if (( ${_escape} == 1 )); then # if escaping is active and current character is ...
+ if [[ "${_char}" == [0-9] ]]; then # ... a digit:
+ _grpidx+="${_char}" # found group reference (e.g. '\12') -> add digit to group index, cycle loop
continue
- elif [[ "${_char}" == "\\" ]]; then # ... a backslash:
- if (( ${#_grpidx} > 0 )); then # if there is a group index: found backslash after group reference (e.g. '\12\')
- _repstr+="${BASH_REMATCH[_grpidx]:-}" # -> add group match to output, reset group index, leave escaping active
+ elif [[ "${_char}" == "\\" ]]; then # ... a backslash:
+ if (( ${#_grpidx} > 0 )); then # if there is a group index: found backslash after group reference (e.g. '\12\')
+ _repstr+="${BASH_REMATCH[_grpidx]:-}" # -> add group match to output, reset group index, leave escaping active
_grpidx=""
- else # if there is no group index: found escaped backslash (i.e. '\\')
- _repstr+="\\" # -> add backslash to output, disable escaping
+ else # if there is no group index: found escaped backslash (i.e. '\\')
+ _repstr+="\\" # -> add backslash to output, disable escaping
_escape=0
fi
- continue # cycle loop
- else # ... something else:
- if (( ${#_grpidx} > 0 )); then # if there is a group index: found end of group reference (e.g. '\12x' at 'x')
- _repstr+="${BASH_REMATCH[_grpidx]:-}" # -> add group match to output
- else # if there is no group index: found non-group-reference escape sequence (e.g. '\r')
- _repstr+="\\" # -> add backslash to output
+ continue # cycle loop
+ else # ... something else:
+ if (( ${#_grpidx} > 0 )); then # if there is a group index: found end of group reference (e.g. '\12x' at 'x')
+ _repstr+="${BASH_REMATCH[_grpidx]:-}" # -> add group match to output
+ else # if there is no group index: found non-group-reference escape sequence (e.g. '\r')
+ _repstr+="\\" # -> add backslash to output
fi
- _escape=0 # disable escaping, continue normally
+ _escape=0 # disable escaping, continue normally
fi
fi
- if [[ "${_char}" = "\\" ]]; then # if current character is a backslash: found start of escape sequence (i.e. '\...')
- _escape=1 # -> enable escaping, reset group index, cycle loop
+ if [[ "${_char}" = "\\" ]]; then # if current character is a backslash: found start of escape sequence (i.e. '\...')
+ _escape=1 # -> enable escaping, reset group index, cycle loop
_grpidx=""
continue
fi
- _repstr+="${_char}" # add current character to replacement string
+ _repstr+="${_char}" # add current character to replacement string
done
- if (( ${_escape} == 1 )); then # same as 'something else' above for end of template (condensed to single line)
+ if (( ${_escape} == 1 )); then # same as 'something else' above for end of template (condensed to single line)
(( ${#_grpidx} > 0 )) && _repstr+="${BASH_REMATCH[_grpidx]:-}" || _repstr+="\\" # '||' part: treat trailing backslash as literal backslash
fi
- _match="${BASH_REMATCH[0]}" # substring matching ENTIRE regex -> to be replaced with replacement string
- if [[ "${_re:0:1}" == "^" ]]; then # special handling for regex starting with '^' (e.g. re='^' or re='^.')
- _out="${_repstr}${_in#*"${_match}"}" # output is replacement string + substring after match
- _in="" # no futher input, break loop
+ _match="${BASH_REMATCH[0]}" # substring matching ENTIRE regex -> to be replaced with replacement string
+ if [[ "${_re:0:1}" == "^" ]]; then # special handling for regex starting with '^' (e.g. re='^' or re='^.')
+ _out="${_repstr}${_in#*"${_match}"}" # output is replacement string + substring after match
+ _in="" # no futher input, break loop
break
- elif [[ "${_re: -1}" == "\$" ]]; then # special handling for regex ending with '$' (e.g. re='$' or re='.$')
- _out="${_in%"${_match}"*}${_repstr}" # output is substring before match + replacement string
- _in="" # no futher input, break loop
+ elif [[ "${_re: -1}" == "\$" ]]; then # special handling for regex ending with '$' (e.g. re='$' or re='.$')
+ _out="${_in%"${_match}"*}${_repstr}" # output is substring before match + replacement string
+ _in="" # no futher input, break loop
break
fi
- if [[ -z "${_match}" ]]; then # if match is empty, prevent endless loop by advancing processing by the smallest
- _out+="${_in:0:1}" # amount possible (i.e. move one character from input to output and cycle loop);
- _in="${_in:1}" # empty matches are quite common, e.g. in='abc123def457ghi', re='[0-9]*'
+ if [[ -z "${_match}" ]]; then # if match is empty, prevent endless loop by advancing processing by the smallest
+ _out+="${_in:0:1}" # amount possible (i.e. move one character from input to output and cycle loop);
+ _in="${_in:1}" # empty matches are quite common, e.g. in='abc123def457ghi', re='[0-9]*'
continue
fi
- _out+="${_in%%"${_match}"*}${_repstr}" # add substring before match + replacement string to output
- _in="${_in#*"${_match}"}" # substring after match is input for next loop iteration
+ _out+="${_in%%"${_match}"*}${_repstr}" # add substring before match + replacement string to output
+ _in="${_in#*"${_match}"}" # substring after match is input for next loop iteration
done
- _out+="${_in}" # add remainder of input string to output
+ _out+="${_in}" # add remainder of input string to output
}
-# Replace single dots with spaces [$1: string, $2: name of target array]
+# Replace single dots with spaces [$1: string, $2: name of output variable]
function replace_dots() {
local _in="$1"
local -n _out="$2"; _out=""
@@ -354,8 +433,13 @@ function replace_dots() {
(( ${_dots} == 1 )) && _out+=" " || for ((_j=0; _j < _dots; _j++)); do _out+="."; done
}
-# Save macro to macro file [$1: macro file, $2: macro name, $3..$n: macro contents]
-# NOTE: if no macro contents are provided, macro is DELETED from macro file
+# Save macro to macro file [$1: macro file, $2: macro name, $3..$n: macro
+# contents]
+# Return value:
+# 0 == macro saved (when saving) / macro deleted (when deleting)
+# 1 == error occurred
+# 2 == saving aborted (when saving) / macro not found (when deleting)
+# NOTE: if NO macro contents are provided, macro is DELETED from macro file
function save_macro() {
local file="$1" name="$2" contents=("${@:3}")
local lines=() i starti=-1 endi=-1
@@ -379,49 +463,58 @@ function save_macro() {
lines+=("${contents[@]}")
lines+=("")
fi
- else # no macro contents -> delete macro
- (( ${starti} != -1 && ${endi} != -1 )) || return 2 # macro not found
- lines=("${lines[@]:0:starti}" "${lines[@]:endi+1}") # delete macro
+ else # no macro contents -> delete macro
+ (( ${starti} != -1 && ${endi} != -1 )) || return 2 # macro not found
+ lines=("${lines[@]:0:starti}" "${lines[@]:endi+1}") # delete macro
fi
mkdir -p -- "$(dirname -- "${file}")" && printf "%s\n" "${lines[@]}" > "${file}" || return 1
return 0
}
-# Load macro from macro file [$1: macro file, $2: macro name, $3: name of target array (macro contents)]
+# Load macro from macro file [$1: macro file, $2: macro name, $3: name of
+# target array (macro contents)]
+# Return value: 0 == macro loaded, 1 == error occurred, 2 == macro not found
function load_macro() {
- local file="$1" name="$2"; local -n arrref="$3"
- local line contents=() gotit="false"
- while read -r line; do
- if [[ "${gotit}" == "false" ]]; then
- [[ "${line}" == "[${name}]" ]] && gotit="true" # '[...]' -> start of macro
+ local _file="$1" _name="$2"
+ local -n _dstarr="$3" #; _dstarr=()
+ local _line _contents=() _gotit="false"
+ #[[ ! -f "${_file}" ]] && return 1 # no macro file -> nothing to load macro from (DISABLED as this would mask errors)
+ while read -r _line; do
+ if [[ "${_gotit}" == "false" ]]; then
+ [[ "${_line}" == "[${_name}]" ]] && _gotit="true" # '[...]' -> start of macro
continue
fi
- [[ "${line}" == "" ]] && break # empty line -> end of macro
- line="${line//"\["/"["}"; line="${line//"\]"/"]"}" # unescape square brackets
- contents+=("${line}")
- done < "${file}" || return 1
- [[ "${gotit}" == "false" ]] && return 2 # macro not found
- arrref=("${contents[@]}") # assign macro contents to target variable
+ [[ "${_line}" == "" ]] && break # empty line -> end of macro
+ _line="${_line//"\["/"["}"; _line="${_line//"\]"/"]"}" # unescape square brackets
+ _contents+=("${_line}")
+ done < "${_file}" || return 1
+ [[ "${_gotit}" == "false" ]] && return 2 # macro not found
+ _dstarr=("${_contents[@]}") # assign macro contents to target variable
return 0
}
# Delete macro from macro file [$1: macro file, $2: macro name]
-# NOTE: simply a wrapper for 'save_macro()' for the sake clarity
+# Return value: 0 == macro deleted, 1 == error occurred, 2 == macro not found
+# NOTE: convenience wrapper for 'save_macro()' for the sake clarity
function delete_macro() {
save_macro "$1" "$2" && return $? || return $?
}
-# List macros stored in macro file [$1: macro file, $2: name of target array (output lines)]
+# List macros stored in macro file [$1: macro file, $2: name of target array
+# (to store listing lines)]
+# Return value: 0 == output generated, 1 == error occurred
function list_macros() {
- local file="$1"; local -n arrref="$2"
- local line output=()
- [[ ! -f "${file}" ]] && return 0 # no macro file -> no macros to list
- while read -r line; do
- [[ "${line}" =~ ^\[(.+)\]$ ]] && { output+=("Macro '${BASH_REMATCH[1]}':"); continue; }
- line="${line//"\["/"["}"; line="${line//"\]"/"]"}" # square brackets are escaped
- output+=("${line}")
- done < "${file}" || return 1
- arrref=("${output[@]::${#output[@]}-1}"); return 0 # assign output to target variable, exclude last line (which is empty)
+ local _file="$1"
+ local -n _dstarr="$2" #; _dstarr=()
+ local _line _output=()
+ #[[ ! -f "${_file}" ]] && return 0 # no macro file -> no macros to list (DISABLED as this would mask errors)
+ while read -r _line; do
+ [[ "${_line}" =~ ^\[(.+)\]$ ]] && { _output+=("Macro '${BASH_REMATCH[1]}':"); continue; }
+ _line="${_line//"\["/"["}"; _line="${_line//"\]"/"]"}" # unescape square brackets
+ _output+=("${_line}")
+ done < "${_file}" || return 1
+ _dstarr=("${_output[@]::${#_output[@]}-1}") # assign output to target variable, exclude last line (which is empty)
+ return 0
}
@@ -431,13 +524,12 @@ function list_macros() {
# -
# --------------------------------------
-# Set up error handler (exit on unbound variables and on unhandled errors)
-set -ueE; trap "printe \"[BUG] Error: an unhandled error occurred on line \${LINENO}, aborting\"; exit 1" ERR
-
# Usage information requested? (NOTE: this refers to the command line usage
-# information, NOT the interactive commands usage information)
+# information, NOT to the interactive commands usage information, which may
+# be displayed via command 'help'/'usage')
if [[ -n "${1+set}" ]] && [[ "$1" == "-h" || "$1" == "--help" ]]; then
- printn "\e[1mUsage:\e[0m ${0##*/} [INITIAL-DIRECTORY]"
+ printn "\e[1mUsage:\e[0m ${0##*/} [INITIAL-DIRECTORY] [CMD]..."
+ printn "\e[1mNote:\e[0m Commands are executed right after startup"
exit 0
fi
@@ -447,27 +539,29 @@ if ! source "${APP_CONFIG}"; then
exit 1
fi
-# Process command line (NOTE: currently, there is only ONE single command line
-# argument; if/when adding more in the future, design those to augment config
-# variables and make them configurable via the config file)
-[[ -n "${1+set}" ]] && INITIAL_DIRECTORY="$1"
+# Process command line (TODO: implement command line parser and offer command
+# line options to allow changing all items currently configurable via config
+# file -> augment config variables)
+[[ -n "${1+set}" ]] && { INITIAL_DIRECTORY="$1"; shift; }
#
# TODO:
# Check, verify and normalize config settings/items here; sort options can be
-# verified by running 'sort "${SORT_OPTS[@]}" <<< ""' and checking exit code
+# verified by running 'sort "${SORT_OPTS[@]}" <<< ""' and checking exit code;
+# especially needed for PLAYBACK_DELAY, which needs to be normalized before
+# being passed to 'playback_print()'
#
# Change directory to initial directory (if specified/set) and then run 'cd .'
# to reset initial destination of 'cd -'
if [[ "${INITIAL_DIRECTORY}" != "" ]] && ! cd -- "${INITIAL_DIRECTORY}"; then
- printe "Error: failed to change to initial directory '${INITIAL_DIRECTORY}', aborting"; exit 1
+ printe "Error: failed to change to initial directory '${INITIAL_DIRECTORY}', aborting."; exit 1
fi
cd .
# Load command history from file (if enabled)
if [[ "${PERSISTENT_HISTORY}" == "true" && -f "${HISTORY_FILE}" ]]; then
- history -r -- "${HISTORY_FILE}" || { printe "Error: failed to load command history from '${HISTORY_FILE}', aborting"; exit 1; }
+ history -r -- "${HISTORY_FILE}" || { printe "Error: failed to load command history from '${HISTORY_FILE}', aborting."; exit 1; }
fi
# Initialize reset lists flag
@@ -480,8 +574,6 @@ infos=()
# Initialize macro storage/state
macro_storage=()
macro_record="false"
-macro_replay="false"
-macro_index=0
# Initialize filter state
filter_pattern="${FILTER_PATTERN}"
@@ -491,8 +583,12 @@ filter_case="${FILTER_CASE}"
# Initialize recursive mode state
recursive_mode="${RECURSIVE_MODE}"
-# Initialize macro playback delay
-macro_delay="${MACRO_DELAY}"
+# Initialize command playback buffer/state, set up playback of commands
+# specified on command line
+playback_delay="${PLAYBACK_DELAY}"
+playback_buffer=("$@")
+playback_index=0
+(( ${#playback_buffer[@]} > 0 )) && playback_enabled="true" || playback_enabled="false"
# Set up exit handler (for cosmetic reasons) and CTRL+C handler (for read
# calls, see https://stackoverflow.com/a/63713771/1976617)
@@ -521,6 +617,7 @@ while true; do
generate_file_list files_in "${recursive_mode}" "${filter_invert}" "${filter_case}" "${filter_pattern}"
copy_array files_in files_out
copy_array files_out files_undo
+ copy_array files_out files_backup # back up file list (allows undo/redo of played back commands up to last reset -or- macro play start)
reset_lists="false"
fi
@@ -547,8 +644,8 @@ while true; do
infos=()
fi
- # Currently (re-)playing macro?
- if [[ "${macro_replay}" == "false" ]]; then
+ # Currently playing back commands?
+ if [[ "${playback_enabled}" == "false" ]]; then
# Prompt user for command input
input=$(read -e -r -p "${PROMPT_CMD}" input && echo "${input}") || { # https://stackoverflow.com/a/63713771/1976617
case $? in
@@ -559,21 +656,27 @@ while true; do
[[ "${input}" == "" ]] && continue # take shortcut if there was no input
[[ "${input}" != "exit" && "${input}" != "quit" ]] && history -s -- "${input}" # add input to history
else
- # End of macro reached?
- if (( ${macro_index} >= ${#macro_storage[@]} )); then
- infos=("(Re-)Play of macro finished.")
- macro_replay="false"
- copy_array files_macro files_undo # write file list backup created before playback started to undo list -> allows undo/redo of ENTIRE macro
- unset files_macro
+ # End of playback buffer reached?
+ if (( ${playback_index} >= ${#playback_buffer[@]} )); then
+ infos=("Command playback finished (${#playback_buffer[@]} commands).")
+ playback_enabled="false"
+ playback_buffer=()
+ copy_array files_backup files_undo # write file list backup to undo list (allows undo/redo of played back commands up to last reset -or- macro play start)
+ continue
+ fi
+
+ # Use next item from buffer as command input
+ printw "Playing back commands (command $((playback_index+1)) of ${#playback_buffer[@]}, delay ${playback_delay}s), hit CTRL+D to abort..."
+ printn # using 'printw' instead of 'infos' here to have only one single place where this needs to be done
+ input="${playback_buffer[${playback_index}]}"
+ playback_index=$((playback_index + 1))
+ if ! playback_print "${input}" "${playback_delay}"; then # CTRL+D -> abort playback
+ infos=("Command playback aborted (at command ${playback_index} of ${#playback_buffer[@]}).")
+ playback_enabled="false"
+ playback_buffer=()
+ copy_array files_backup files_undo # write file list backup to undo list (allows undo/redo of played back commands up to last reset -or- macro play start)
continue
fi
- # Use next item from macro as command input
- input="${macro_storage[${macro_index}]}"
- macro_index=$((macro_index + 1))
- echo -n "${PROMPT_CMD}${input}"
- read -s -t "${macro_delay}" || :
- echo
- infos=("(Re-)Playing macro ($(( ${#macro_storage[@]} - ${macro_index} )) commands left)...")
fi
# Evaluate command input
@@ -597,6 +700,7 @@ while true; do
[[ -n "${error+set}" ]] && break # break loop if error was set in previous iteration
file="${files_out[i]}"
[[ "${file}" == */* ]] && dir="${file%/*}/" || dir="" # extract leading directory part if existing
+ #name="${file##*/}"
name="${file##*/}"; name="${name%.*}" # extract file name part (without directory and extension)
[[ "${file}" == *.* ]] && ext=".${file##*.}" || ext="" # extract trailing extension part if existing
case "${cmd}" in
@@ -658,6 +762,7 @@ while true; do
name="${name%"${name##*[![:space:]]}"}" # trim trailing whitespace
;;
esac
+ #files_out[i]="${dir}${name}"
files_out[i]="${dir}${name}${ext}"
done
;;
@@ -691,29 +796,38 @@ while true; do
;;
play-macro|pm)
if [[ "${macro_record}" == "true" ]]; then
- errors=("Error: play-macro can't (re-)play macro while recording")
+ errors=("Error: play-macro: can't play back macro while recording")
continue
fi
if (( ${#macro_storage[@]} == 0 )); then
- errors=("Error: play-macro can't (re-)play empty macro")
+ errors=("Error: play-macro: can't play back empty macro")
continue
fi
- infos=("(Re-)Playing macro (${#macro_storage[@]} commands left)...")
- macro_replay="true"
- macro_index=0
- copy_array files_out files_macro # save current file list to allow undo/redo of ENTIRE macro (see 'End of macro reached?' above)
+ if [[ "${playback_enabled}" == "true" ]]; then # already playing back commands? -> inject macro contents into existing playback buffer stream
+ playback_buffer=("${playback_buffer[@]::${playback_index}}" "${macro_storage[@]}" "${playback_buffer[@]:${playback_index}}")
+ else # currently not playing back commands -> set up and start command playback
+ copy_array files_out files_backup # back up current file list to allow undo/redo of ENTIRE macro (see 'End of playback buffer reached?' above)
+ playback_buffer=("${macro_storage[@]}")
+ playback_index=0
+ playback_enabled="true"
+ fi
+ infos=("Starting playback of macro (${#macro_storage[@]} commands).")
;;
- macro-delay|md)
+ playback-delay|pd)
if (( ${#args[@]} != 1 )); then
- errors=("Error: macro-delay: invalid number of arguments (expected 1, got ${#args[@]})")
+ errors=("Error: playback-delay: invalid number of arguments (expected 1, got ${#args[@]})")
continue
fi
if ! [[ "${args[0]}" =~ ^[0-9]+$ || "${args[0]}" =~ ^[0-9]+\.[0-9]+$ ]]; then
- errors=("Error: macro-delay: value argument must be positive integer or fraction")
+ errors=("Error: playback-delay: value argument must be positive integer or fraction")
continue
fi
- macro_delay="${args[0]}"
- infos=("Macro playback delay set to ${macro_delay}s.")
+ if [[ "${args[0]}" =~ ^[0]+$ || "${args[0]}" =~ ^[0]+\.[0]+$ ]]; then # if specified delay == 0 (might be '000', '0.0' or '00.00') ...
+ playback_delay="0" # ... normalize to just '0' for easier handling within 'playback_print()'
+ else
+ playback_delay="${args[0]}"
+ fi
+ infos=("Command playback delay set to ${playback_delay}s.")
;;
# Macro commands (2)
@@ -738,8 +852,8 @@ while true; do
infos=("Saving macro '${name}' was aborted.")
continue
fi
- errors=("Error: save-macro: failed to save macro '${name}'")
- printe "Error: save-macro: failed to save macro '${name}', hit ENTER to continue"; read -s || :
+ #errors=("Error: save-macro: failed to save macro '${name}'")
+ printn; ask_hit_enter printe "Error: save-macro: failed to save macro '${name}', hit ENTER to continue" || :
fi
;;
load-macro|lm)
@@ -763,8 +877,8 @@ while true; do
errors=("Error: load-macro: no macro named '${name}' found")
continue
fi
- errors=("Error: load-macro: failed to load macro '${name}'")
- printe "Error: load-macro: failed to load macro '${name}', hit ENTER to continue"; read -s || :
+ #errors=("Error: load-macro: failed to load macro '${name}'")
+ printn; ask_hit_enter printe "Error: load-macro: failed to load macro '${name}', hit ENTER to continue" || :
fi
;;
delete-macro|dm)
@@ -784,17 +898,18 @@ while true; do
errors=("Error: delete-macro: no macro named '${name}' found")
continue
fi
- errors=("Error: delete-macro: failed to delete macro '${name}'")
- printe "Error: delete-macro: failed to delete macro '${name}', hit ENTER to continue"; read -s || :
+ #errors=("Error: delete-macro: failed to delete macro '${name}'")
+ printn; ask_hit_enter printe "Error: delete-macro: failed to delete macro '${name}', hit ENTER to continue" || :
fi
;;
list-macros|im)
+ printn
if list_macros "${MACROS_FILE}" infos; then
#(( ${#infos[@]} > 0 )) || infos=("No macros stored in macro file.")
(( ${#infos[@]} > 0 )) && infos=("Macros stored in macro file:" "" "${infos[@]}") || infos=("No macros stored in macro file.")
else
- errors=("Error: list-macros: failed to list macros")
- printe "Error: list-macros: failed to list macros, hit ENTER to continue"; read -s || :
+ #errors=("Error: list-macros: failed to list macros")
+ printn; ask_hit_enter printe "Error: list-macros: failed to list macros, hit ENTER to continue" || :
fi
;;
@@ -906,11 +1021,13 @@ while true; do
printn
file="${files_out[index-1]}"
[[ "${file}" == */* ]] && dir="${file%/*}/" || dir="" # extract leading directory part if existing
- name="${file##*/}"; name="${name%.*}" # extract file name part (without directory and extension)
- [[ "${file}" == *.* ]] && ext=".${file##*.}" || ext="" # extract trailing extension part if existing
+ #name="${file##*/}"; name="${name%.*}" # extract file name part (without directory and extension)
+ #[[ "${file}" == *.* ]] && ext=".${file##*.}" || ext="" # extract trailing extension part if existing
+ name="${file##*/}"
name=$(history -c; read -e -r -p "${PROMPT_EDIT}" -i "${name}" name && echo "${name}") || continue # clear history in subshell to allow for clean editing
copy_array files_out files_undo # save undo data
- files_out[index-1]="${dir}${name}${ext}" # modify entry
+ #files_out[index-1]="${dir}${name}${ext}" # modify entry
+ files_out[index-1]="${dir}${name}" # modify entry
;;
undo|ud)
if compare_arrays files_out files_undo; then # if arrays match, there's nothing to undo
@@ -962,24 +1079,24 @@ while true; do
if [[ -e "${dst}" ]]; then # if destination already exists ...
name="${dst%.*}" # ... split into name ...
[[ "${dst}" == *.* ]] && ext=".${dst##*.}" || ext="" # ... and extension, ...
- for (( j=1; ; j++ )); do
+ for (( j=1; ; j++ )); do # CAUTION: could result in a endless loop on overflow due to LOTS of files
dst="${name}_${j}${ext}" # ... add postfix to name ('_1', '_2', etc.) ...
[[ ! -e "${dst}" ]] && break # ... and break once collision is resolved
done
fi
# NOTE:
- # Should the need arise to move files to different directories,
- # something like below would do the trick (draft only, needs
- # additional handling for edge cases; probably best to wrap it
- # in a function that allows for rollback in case of errors)
+ # Should the need arise to move files to different directories (e.g. due to
+ # directory part becoming editable), something like this would do the trick
+ # (draft only, likely needs additional checks for edge cases; probably best
+ # to wrap this in a function to allow for rollback in case of errors):
#mkdir -p -- "$(dirname -- "${dst}")" && mv -i -- "${src}" "${dst}" && rmdir --parents --ignore-fail-on-non-empty -- "$(dirname -- "${src}")" || errcnt=$((errcnt + 1))
mv -i -- "${src}" "${dst}" || errcnt=$((errcnt + 1)) # rename file, count errors
done
if (( ${errcnt} == 0 )); then
infos=("Succesfully renamed ${#files_in[@]} file(s).")
else # prompt user if error(s) occurred when renaming file(s)
- printe "Error: failed to rename ${errcnt} file(s), hit ENTER to continue"
- read -s || :
+ #errors=("Error: failed to rename ${errcnt} file(s)")
+ printn; ask_hit_enter printe "Error: failed to rename ${errcnt} file(s), hit ENTER to continue" || :
fi
reset_lists="true" # request lists reset
;;