diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5c12290
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+# Ignore files generated by script
+*.hst
+*.mac
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a46746e..936f210 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,14 @@
+## Changelog for v4.0 release
+
+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`)
+- Extended `undo` command to allow undoing/redoing entire macros
+- Reworked several commands to accomodate new commands (e.g. `start-macro` + `end-macro` -> `record-macro`)
+- Applied various minor code modification (variables, comments, errors, etc.)
+
## Changelog for v3.11 release
Initial release (versions prior to v3.11 have not been published).
@@ -12,4 +23,4 @@ Features:
##
-_Last updated: 09/13/23_
+_Last updated: 09/25/23_
diff --git a/README.md b/README.md
index d27c42c..9c4c735 100644
--- a/README.md
+++ b/README.md
@@ -6,65 +6,137 @@ I'm striving to become a full-time developer of [Free and open-source software (
+## Requirements
+**Dependencies:**
+_Bash (>=v4.0)_, _GNU find_ (part of [findutils](https://www.gnu.org/software/findutils/))
+
+**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.
+## 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 run _infiren_, use the following commands (requires _Bash >= v4.0_):
+To start _infiren_, run the following commands:
```
-$ cd infiren-vX.Y
-$ ./infiren.sh [START-FOLDER]
+$ cd infiren-v4.0
+$ ./infiren.sh [INITIAL-DIRECTORY]
```
-Within _infiren_, use `help` to list available editing commands:
+Within _infiren_, use `help` to list available commands:
```
Available commands:
-rs, replace-string Replace string with replacement
-re, replace-regex Match regular expression and replace
- matching string according to template
- Example: re "([0-9]+)x([0-9]+)" "S\1E\2"
-pr, pre, prepend Prepend string
-ap, post, append Append string
-rd, replace-dots Replace single dots with spaces
-id, insert-dash Insert dash after first word
-ca, capitalize Capitalize space-separated words
-up, upper, uppercase Convert all characters to uppercase
-lo, lower, lowercase Convert all characters to lowercase
-tr, trim, st, strip Trim leading & trailing whitespace
-
-sm, start-macro Start recording macro
-em, end-macro Stop recording macro
-vm, view-macro View macro contents
-cm, clear-macro Clear macro contents
-rm, replay-macro Replay commands from macro
-
-hm, history-macro Create macro from command history
-vh, view-history View command history
-ch, clear-history Clear command history
-
-sf, set-filter Set filter to and reload
-if, invert-filter Invert filter and reload
-cf, case-filter Toggle filter case and reload
-vf, view-filter View current filter state
-rf, reset-filter Reset filter and reload
-
-ed, edit Manually edit entry with index
-ud, undo Undo last name-altering operation
-rc, recursive Toggle recursive mode and reload
-cd, chdir Change to directory and reload
-
-apply, save Apply changes (i.e. rename files)
-reset, reload Discard changes and reload file names
-
-help, usage Display this help/usage text
-exit, quit Exit program (shortcut: CTRL+D)
+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
+ (e.g. re "([0-9]+)x([0-9]+)" "S\1E\2")
+pr, pre, prepend STR Prepend string STR
+po, post, append STR Append string STR
+rd, replace-dots Replace single dots with spaces
+id, insert-dash Insert dash after first word
+ca, capitalize Capitalize space-separated words
+up, upper, uppercase Convert all characters to uppercase
+lo, lower, lowercase Convert all characters to lowercase
+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)
+
+sm, save-macro NAME Save macro using name NAME to macro file
+lm, load-macro NAME Load macro named NAME from macro file
+dm, delete-macro NAME Delete macro named NAME from macro file
+im, list-macros List all macros stored in macro file
+
+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
+if, invert-filter Invert filter and reload files
+fc, filter-case Toggle filter case and reload files
+vf, view-filter View current filter state
+rf, reset-filter Reset filter and reload files
+
+ed, edit INDEX Manually edit entry with index INDEX
+ud, undo Undo/redo last name-altering operation
+rc, recursive Toggle recursive mode and reload files
+cd, chdir PATH Change directory to PATH and reload files
+
+apply, save Apply changes (i.e. rename files)
+reload, reset Discard changes and reload files
+
+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)
+## Configuration Options & Defaults
+
+Configuration options and current defaults:
+```sh
+# infiren.conf
+
+# ------------------------------------------------------------------------------
+# -
+# Interactive File Renamer (InFiRen) -
+# -
+# Created by Fonic -
+# Date: 04/23/19 - 09/25/23 -
+# -
+# ------------------------------------------------------------------------------
+
+# Initial directory (if empty, current working directory is used)
+INITIAL_DIRECTORY=""
+
+# Initial filter pattern (see 'man find', option '-name pattern' for syntax;
+# '*' == all files)
+FILTER_PATTERN="*"
+
+# Initial filter invert setting ('true'/'false'; 'true' == inversion enabled)
+FILTER_INVERT="false"
+
+# Initial filter case setting ('true'/'false'; 'true' == case sensitive)
+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"
+
+# Options passed to 'sort' when sorting file/folder listings (see 'man sort'
+# for valid/available options)
+SORT_OPTS=("-V")
+
+# Load/save command history from/to file on startup/exit ('true'/'false')
+PERSISTENT_HISTORY="true"
+
+# File used to store command history (only if PERSISTENT_HISTORY is enabled)
+# ${APP_DIR}: directory where app executable ('infiren.sh') is stored
+# ${APP_NAME}: name of app executable ('infiren.sh') without extension
+# ${HOME}: home directory of user running/executing the application
+#HISTORY_FILE="${HOME}/.config/${APP_NAME}/${APP_NAME}.hst"
+HISTORY_FILE="${APP_DIR}/${APP_NAME}.hst"
+
+# File used to store macros (managed via commands 'save-macro'/'load-macro')
+# ${APP_DIR}: directory where app executable ('infiren.sh') is stored
+# ${APP_NAME}: name of app executable ('infiren.sh') without extension
+# ${HOME}: home directory of user running/executing the application
+#MACROS_FILE="${HOME}/.config/${APP_NAME}/${APP_NAME}.mac"
+MACROS_FILE="${APP_DIR}/${APP_NAME}.mac"
+```
##
-_Last updated: 09/13/23_
+_Last updated: 09/25/23_
diff --git a/infiren.conf b/infiren.conf
new file mode 100644
index 0000000..13e7e45
--- /dev/null
+++ b/infiren.conf
@@ -0,0 +1,51 @@
+# infiren.conf
+
+# ------------------------------------------------------------------------------
+# -
+# Interactive File Renamer (InFiRen) -
+# -
+# Created by Fonic -
+# Date: 04/23/19 - 09/25/23 -
+# -
+# ------------------------------------------------------------------------------
+
+# Initial directory (if empty, current working directory is used)
+INITIAL_DIRECTORY=""
+
+# Initial filter pattern (see 'man find', option '-name pattern' for syntax;
+# '*' == all files)
+FILTER_PATTERN="*"
+
+# Initial filter invert setting ('true'/'false'; 'true' == inversion enabled)
+FILTER_INVERT="false"
+
+# Initial filter case setting ('true'/'false'; 'true' == case sensitive)
+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"
+
+# Options passed to 'sort' when sorting file/folder listings (see 'man sort'
+# for valid/available options)
+SORT_OPTS=("-V")
+
+# Load/save command history from/to file on startup/exit ('true'/'false')
+PERSISTENT_HISTORY="true"
+
+# File used to store command history (only if PERSISTENT_HISTORY is enabled)
+# ${APP_DIR}: directory where app executable ('infiren.sh') is stored
+# ${APP_NAME}: name of app executable ('infiren.sh') without extension
+# ${HOME}: home directory of user running/executing the application
+#HISTORY_FILE="${HOME}/.config/${APP_NAME}/${APP_NAME}.hst"
+HISTORY_FILE="${APP_DIR}/${APP_NAME}.hst"
+
+# File used to store macros (managed via commands 'save-macro'/'load-macro')
+# ${APP_DIR}: directory where app executable ('infiren.sh') is stored
+# ${APP_NAME}: name of app executable ('infiren.sh') without extension
+# ${HOME}: home directory of user running/executing the application
+#MACROS_FILE="${HOME}/.config/${APP_NAME}/${APP_NAME}.mac"
+MACROS_FILE="${APP_DIR}/${APP_NAME}.mac"
diff --git a/infiren.sh b/infiren.sh
index 3df7885..534ca2e 100755
--- a/infiren.sh
+++ b/infiren.sh
@@ -1,13 +1,13 @@
#!/usr/bin/env bash
-# -------------------------------------------------------------------------
-# -
-# Interactive File Renamer (InFiRen) -
-# -
-# Created by Fonic -
-# Date: 04/23/19 - 09/13/23 -
-# -
-# -------------------------------------------------------------------------
+# ------------------------------------------------------------------------------
+# -
+# Interactive File Renamer (InFiRen) -
+# -
+# Created by Fonic -
+# Date: 04/23/19 - 09/25/23 -
+# -
+# ------------------------------------------------------------------------------
# --------------------------------------
@@ -16,60 +16,116 @@
# -
# --------------------------------------
-# Script version
-SCRIPT_VERSION="3.11"
+# Application info
+APP_TITLE="Interactive File Renamer (InFiRen)"
+APP_VERSION="4.0 (09/25/23)"
+APP_DIR="$(dirname -- "$(realpath -- "$0")")"
+APP_FILE="$(basename -- "$(realpath -- "$0")")"
+APP_NAME="${APP_FILE%.*}"
+APP_CONFIG="${APP_DIR}/${APP_NAME}.conf"
# Input/edit prompt
PROMPT_CMD="cmd> "
PROMPT_EDIT="edit> "
-# Sort options used when generating file/folder listings
-SORT_OPTS=("-V")
-
# Help/usage text explaining available commands
read -r -d '' HELP_COMMANDS <<- EOD
- rs, replace-string Replace string with replacement
- re, replace-regex Match regular expression and replace
- matching string according to template
- Example: re "([0-9]+)x([0-9]+)" "S\\1E\\2"
- pr, pre, prepend Prepend string
- ap, post, append Append string
- rd, replace-dots Replace single dots with spaces
- id, insert-dash Insert dash after first word
- ca, capitalize Capitalize space-separated words
- up, upper, uppercase Convert all characters to uppercase
- lo, lower, lowercase Convert all characters to lowercase
- tr, trim, st, strip Trim leading & trailing whitespace
-
- sm, start-macro Start recording macro
- em, end-macro Stop recording macro
- vm, view-macro View macro contents
- cm, clear-macro Clear macro contents
- rm, replay-macro Replay commands from macro
-
- hm, history-macro Create macro from command history
- vh, view-history View command history
- ch, clear-history Clear command history
-
- sf, set-filter Set filter to and reload
- if, invert-filter Invert filter and reload
- cf, case-filter Toggle filter case and reload
- vf, view-filter View current filter state
- rf, reset-filter Reset filter and reload
-
- ed, edit Manually edit entry with index
- ud, undo Undo last name-altering operation
- rc, recursive Toggle recursive mode and reload
- cd, chdir Change to directory and reload
-
- apply, save Apply changes (i.e. rename files)
- reset, reload Discard changes and reload file names
-
- help, usage Display this help/usage text
- exit, quit Exit program (shortcut: CTRL+D)
+ 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
+ (e.g. re "([0-9]+)x([0-9]+)" "S\\1E\\2")
+ pr, pre, prepend STR Prepend string STR
+ po, post, append STR Append string STR
+ rd, replace-dots Replace single dots with spaces
+ id, insert-dash Insert dash after first word
+ ca, capitalize Capitalize space-separated words
+ up, upper, uppercase Convert all characters to uppercase
+ lo, lower, lowercase Convert all characters to lowercase
+ 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)
+
+ sm, save-macro NAME Save macro using name NAME to macro file
+ lm, load-macro NAME Load macro named NAME from macro file
+ dm, delete-macro NAME Delete macro named NAME from macro file
+ im, list-macros List all macros stored in macro file
+
+ 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
+ if, invert-filter Invert filter and reload files
+ fc, filter-case Toggle filter case and reload files
+ vf, view-filter View current filter state
+ rf, reset-filter Reset filter and reload files
+
+ ed, edit INDEX Manually edit entry with index INDEX
+ ud, undo Undo/redo last name-altering operation
+ rc, recursive Toggle recursive mode and reload files
+ cd, chdir PATH Change directory to PATH and reload files
+
+ apply, save Apply changes (i.e. rename files)
+ reload, reset Discard changes and reload files
+
+ help, usage Display this help/usage text
+ exit, quit Exit program (shortcut: CTRL+D)
EOD
+# --------------------------------------
+# -
+# Configuration -
+# -
+# --------------------------------------
+
+# Initial directory (if empty, current working directory is used)
+INITIAL_DIRECTORY=""
+
+# Initial filter pattern (see 'man find', option '-name pattern' for syntax;
+# '*' == all files)
+FILTER_PATTERN="*"
+
+# Initial filter invert setting ('true'/'false'; 'true' == inversion enabled)
+FILTER_INVERT="false"
+
+# Initial filter case setting ('true'/'false'; 'true' == case sensitive)
+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"
+
+# Options passed to 'sort' when sorting file/folder listings (see 'man sort'
+# for valid/available options)
+SORT_OPTS=("-V")
+
+# Load/save command history from/to file on startup/exit ('true'/'false')
+PERSISTENT_HISTORY="true"
+
+# File used to store command history (only if PERSISTENT_HISTORY is enabled)
+# ${APP_DIR}: directory where app executable ('infiren.sh') is stored
+# ${APP_NAME}: name of app executable ('infiren.sh') without extension
+# ${HOME}: home directory of user running/executing the application
+#HISTORY_FILE="${HOME}/.config/${APP_NAME}/${APP_NAME}.hst"
+HISTORY_FILE="${APP_DIR}/${APP_NAME}.hst"
+
+# File used to store macros (managed via commands 'save-macro'/'load-macro')
+# ${APP_DIR}: directory where app executable ('infiren.sh') is stored
+# ${APP_NAME}: name of app executable ('infiren.sh') without extension
+# ${HOME}: home directory of user running/executing the application
+#MACROS_FILE="${HOME}/.config/${APP_NAME}/${APP_NAME}.mac"
+MACROS_FILE="${APP_DIR}/${APP_NAME}.mac"
+
+
# --------------------------------------
# -
# Functions -
@@ -84,18 +140,21 @@ 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]
+# Ask 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
- echo -en "\n\e[1;33m$1 [y/n]:\e[0m "
+ local input result
+ [[ "${2:-"true"}" == "true" ]] && echo
+ echo -en "\e[1;33m$1 [y/n]:\e[0m "
while true; do
read -s -n 1 input
case "${input}" in
- y|Y) echo -e "\e[1;33myes\e[0m"; return 0; ;;
- n|N) echo -e "\e[1;33mno\e[0m"; return 1; ;;
+ y|Y) echo -e "\e[1;33myes\e[0m"; result=0; break; ;;
+ n|N) echo -e "\e[1;33mno\e[0m"; result=1; break; ;;
esac
done
+ [[ "${3:-"false"}" == "true" ]] && echo
+ 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]
@@ -150,7 +209,7 @@ function compare_arrays() {
return 0
}
-# Split string into array [$1: string, $2: separator character, $3: escape character, $4: maximum items, $5: name of target array variable]
+# 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)
@@ -276,7 +335,7 @@ function replace_regex() {
_out+="${_in}" # add remainder of input string to output
}
-# Replace single dots with spaces [$1: string, $2: name of target array variable]
+# Replace single dots with spaces [$1: string, $2: name of target array]
function replace_dots() {
local _in="$1"
local -n _out="$2"; _out=""
@@ -295,28 +354,122 @@ 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
+function save_macro() {
+ local file="$1" name="$2" contents=("${@:3}")
+ local lines=() i starti=-1 endi=-1
+ if [[ -f "${file}" ]]; then
+ readarray -t lines < "${file}" || return 1
+ for ((i=0; i < "${#lines[@]}"; i++)); do
+ if (( ${starti} == -1 )); then
+ [[ "${lines[i]}" == "[${name}]" ]] && starti=$i # '[...]' -> start of macro
+ continue
+ fi
+ [[ "${lines[i]}" == "" ]] && { endi=$i; break; } # empty line -> end of macro
+ done
+ fi
+ if (( ${#contents[@]} > 0 )); then # macro non-empty? -> replace or append macro
+ contents=("${contents[@]//"["/"\["}"); contents=("${contents[@]//"]"/"\]"}") # escape square brackets
+ if (( ${starti} != -1 && ${endi} != -1 )); then
+ ask_yes_no "Macro '${name}' already exists. Overwrite?" "false" || return 2 # replace macro
+ lines=("${lines[@]:0:starti}" "[${name}]" "${contents[@]}" "" "${lines[@]:endi+1}")
+ else
+ lines+=("[${name}]") # append 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
+ 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)]
+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
+ 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
+ return 0
+}
+
+# Delete macro from macro file [$1: macro file, $2: macro name]
+# NOTE: simply a 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)]
+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)
+}
+
# --------------------------------------
# -
-# Main -
+# Initialization -
# -
# --------------------------------------
# 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?
+# Usage information requested? (NOTE: this refers to the command line usage
+# information, NOT the interactive commands usage information)
if [[ -n "${1+set}" ]] && [[ "$1" == "-h" || "$1" == "--help" ]]; then
- printn "\e[1mUsage:\e[0m ${0##*/} [START-FOLDER]"
+ printn "\e[1mUsage:\e[0m ${0##*/} [INITIAL-DIRECTORY]"
exit 0
fi
-# Change directory and run 'cd .' to reset initial destination of 'cd -'
-if [[ -n "${1+set}" ]] && ! cd -- "$1"; then
- printe "Error: failed to change directory to '$1', aborting"; exit 1
+# Load configuration from file
+if ! source "${APP_CONFIG}"; then
+ printe "Error: failed to load configuration from '${APP_CONFIG}', aborting."
+ 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"
+
+#
+# TODO:
+# Check, verify and normalize config settings/items here; sort options can be
+# verified by running 'sort "${SORT_OPTS[@]}" <<< ""' and checking exit code
+#
+
+# 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
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; }
+fi
+
# Initialize reset lists flag
reset_lists="true"
@@ -331,25 +484,35 @@ macro_replay="false"
macro_index=0
# Initialize filter state
-filter_pattern="*"
-filter_invert="false"
-filter_case="false"
+filter_pattern="${FILTER_PATTERN}"
+filter_invert="${FILTER_INVERT}"
+filter_case="${FILTER_CASE}"
# Initialize recursive mode state
-recursive_mode="false"
+recursive_mode="${RECURSIVE_MODE}"
+
+# Initialize macro playback delay
+macro_delay="${MACRO_DELAY}"
# Set up exit handler (for cosmetic reasons) and CTRL+C handler (for read
# calls, see https://stackoverflow.com/a/63713771/1976617)
trap "printn" EXIT
trap ":" INT
-# Main loop
+
+# --------------------------------------
+# -
+# Main -
+# -
+# --------------------------------------
+
+# Command input loop
infos=("Enter 'help' to display available commands.")
while true; do
- # Clear screen and print header
+ # Clear screen and print application header
clear
- printh "--==[ Interactive File Renamer (InFiRen) v${SCRIPT_VERSION} ]==--"
+ printh "--==[ ${APP_TITLE} v${APP_VERSION} ]==--"
printn
# Reset folder and file lists if requested
@@ -384,7 +547,7 @@ while true; do
infos=()
fi
- # Currently replaying macro?
+ # Currently (re-)playing macro?
if [[ "${macro_replay}" == "false" ]]; then
# Prompt user for command input
input=$(read -e -r -p "${PROMPT_CMD}" input && echo "${input}") || { # https://stackoverflow.com/a/63713771/1976617
@@ -394,12 +557,23 @@ while true; do
esac
}
[[ "${input}" == "" ]] && continue # take shortcut if there was no input
- history -s "${input}" # add input to history
+ [[ "${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
+ continue
+ fi
# Use next item from macro as command input
input="${macro_storage[${macro_index}]}"
macro_index=$((macro_index + 1))
- (( ${macro_index} >= ${#macro_storage[@]} )) && macro_replay="false"
+ 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
@@ -410,7 +584,7 @@ while true; do
replace-string|rs| \
replace-regex|re| \
prepend|pre|pr| \
- append|post|ap| \
+ append|post|po| \
replace-dots|rd| \
insert-dash|id| \
capitalize|ca| \
@@ -447,7 +621,7 @@ while true; do
fi
name="${args[0]}${name}"
;;
- append|post|ap)
+ append|post|po)
if (( ${#args[@]} != 1 )); then
errors=("Error: append: invalid number of arguments (expected 1, got ${#args[@]})")
continue
@@ -488,26 +662,19 @@ while true; do
done
;;
- # Macro commands
- start-macro|sm)
- if [[ "${macro_record}" == "true" ]]; then
- errors=("Error: start-macro: can't start recording, already started")
- continue
- fi
- macro_record="true"
- (( ${#macro_storage[@]} > 0 )) && infos=("Macro recording started. Appending existing contents.") || infos=("Macro recording started. Macro is empty.")
- ;;
- end-macro|em)
+ # Macro commands (1)
+ record-macro|rm)
if [[ "${macro_record}" == "false" ]]; then
- errors=("Error: end-macro: can't stop recording, already stopped")
- continue
- fi
- macro_record="false"
- if (( ${#macro_storage[@]} > 0 )); then
- infos=("Macro recording stopped. Macro contents:")
- for line in "${macro_storage[@]}"; do infos+=("${line}"); done
+ macro_record="true"
+ (( ${#macro_storage[@]} > 0 )) && infos=("Macro recording started (adding to existing contents).") || infos=("Macro recording started (macro is empty).")
else
- infos=("Macro recording stopped. Macro is empty.")
+ macro_record="false"
+ if (( ${#macro_storage[@]} > 0 )); then
+ infos=("Macro recording stopped. Macro contents:")
+ for line in "${macro_storage[@]}"; do infos+=("${line}"); done
+ else
+ infos=("Macro recording stopped (macro is empty).")
+ fi
fi
;;
view-macro|vm)
@@ -515,24 +682,120 @@ while true; do
[[ "${macro_record}" == "true" ]] && infos=("Macro contents (still recording):") || infos=("Macro contents:")
for line in "${macro_storage[@]}"; do infos+=("${line}"); done
else
- [[ "${macro_record}" == "true" ]] && infos=("Macro is empty. Still recording.") || infos=("Macro is empty.")
+ [[ "${macro_record}" == "true" ]] && infos=("Macro is empty (still recording).") || infos=("Macro is empty.")
fi
;;
clear-macro|cm)
macro_storage=()
- [[ "${macro_record}" == "true" ]] && infos=("Macro contents cleared. Still recording.") || infos=("Macro contents cleared.")
+ [[ "${macro_record}" == "true" ]] && infos=("Macro contents cleared (still recording).") || infos=("Macro contents cleared.")
;;
- replay-macro|rm)
+ play-macro|pm)
if [[ "${macro_record}" == "true" ]]; then
- errors=("Error: replay-macro: can't replay macro while recording macro")
+ errors=("Error: play-macro can't (re-)play macro while recording")
continue
fi
if (( ${#macro_storage[@]} == 0 )); then
- errors=("Error: replay-macro: can't replay empty macro")
+ errors=("Error: play-macro can't (re-)play 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)
+ ;;
+ macro-delay|md)
+ if (( ${#args[@]} != 1 )); then
+ errors=("Error: macro-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")
+ continue
+ fi
+ macro_delay="${args[0]}"
+ infos=("Macro playback delay set to ${macro_delay}s.")
+ ;;
+
+ # Macro commands (2)
+ save-macro|sm)
+ if (( ${#args[@]} != 1 )); then
+ errors=("Error: save-macro: invalid number of arguments (expected 1, got ${#args[@]})")
+ continue
+ fi
+ if [[ "${args[0]}" == "" ]]; then
+ errors=("Error: save-macro: name argument must not be empty")
+ continue
+ fi
+ if (( ${#macro_storage[@]} == 0 )); then
+ errors=("Error: save-macro: can't save empty macro")
+ continue
+ fi
+ name="${args[0]}"; printn
+ if save_macro "${MACROS_FILE}" "${name}" "${macro_storage[@]}"; then
+ infos=("Saved macro '${name}' (${#macro_storage[@]} commands).")
+ else
+ if (( $? == 2 )); then
+ 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 || :
+ fi
+ ;;
+ load-macro|lm)
+ if (( ${#args[@]} != 1 )); then
+ errors=("Error: load-macro: invalid number of arguments (expected 1, got ${#args[@]})")
+ continue
+ fi
+ if [[ "${args[0]}" == "" ]]; then
+ errors=("Error: load-macro: name argument must not be empty")
+ continue
+ fi
+ if (( ${#macro_storage[@]} > 0 )); then
+ ask_yes_no "Macro is non-empty, contents will be replaced. Continue?" || continue
+ fi
+ name="${args[0]}"; printn
+ if load_macro "${MACROS_FILE}" "${name}" macro_storage; then
+ infos=("Loaded macro '${name}':")
+ for line in "${macro_storage[@]}"; do infos+=("${line}"); done
+ else
+ if (( $? == 2 )); then
+ 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 || :
+ fi
+ ;;
+ delete-macro|dm)
+ if (( ${#args[@]} != 1 )); then
+ errors=("Error: delete-macro: invalid number of arguments (expected 1, got ${#args[@]})")
+ continue
+ fi
+ if [[ "${args[0]}" == "" ]]; then
+ errors=("Error: delete-macro: name argument must not be empty")
+ continue
+ fi
+ name="${args[0]}"; printn
+ if delete_macro "${MACROS_FILE}" "${name}"; then
+ infos=("Deleted macro '${name}'.")
+ else
+ if (( $? == 2 )); then
+ 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 || :
+ fi
+ ;;
+ list-macros|im)
+ 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 || :
+ fi
;;
# History commands
@@ -549,7 +812,7 @@ while true; do
replace-string|rs| \
replace-regex|re| \
prepend|pre|pr| \
- append|post|ap| \
+ append|post|po| \
replace-dots|rd| \
insert-dash|id| \
capitalize|ca| \
@@ -576,9 +839,9 @@ while true; do
;;
# Filter commands
- set-filter|sf)
+ filter-pattern|fp)
if (( ${#args[@]} != 1 )); then
- errors=("Error: set-filter: invalid number of arguments (expected 1, got ${#args[@]})")
+ errors=("Error: filter-pattern: invalid number of arguments (expected 1, got ${#args[@]})")
continue
fi
if ! compare_arrays files_in files_out; then # if there are unsaved changes ...
@@ -597,7 +860,7 @@ while true; do
#infos=("Filter invert set to '${filter_invert}'.")
[[ "${filter_invert}" == "false" ]] && infos=("Filter invert disabled.") || infos=("Filter invert enabled.")
;;
- case-filter|cf)
+ filter-case|fc)
if ! compare_arrays files_in files_out; then # if there are unsaved changes ...
ask_yes_no "There are unsaved changes. Continue anyway?" || continue # ... prompt user before continuing
fi
@@ -657,6 +920,7 @@ while true; do
copy_array files_out files_temp # this ...
copy_array files_undo files_out # undo last operation
copy_array files_temp files_undo # ... and this allows to undo undo
+ unset files_temp
;;
rc|recursive)
if ! compare_arrays files_in files_out; then # if there are unsaved changes ...
@@ -708,18 +972,18 @@ while true; do
# 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)
- #mkdir -p -- "$(dirname "${dst}")" && mv -i -- "${src}" "${dst}" && rmdir --parents --ignore-fail-on-non-empty -- "$(dirname "${src}")" || errcnt=$((errcnt + 1))
+ #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."
+ printe "Error: failed to rename ${errcnt} file(s), hit ENTER to continue"
read -s || :
fi
reset_lists="true" # request lists reset
;;
- reset|reload)
+ reload|reset)
#generate_folder_list folders # this block would allow to undo reset ...
#copy_array files_out files_undo # ... disabled but keeping it for future reference
#generate_file_list files_in
@@ -749,5 +1013,11 @@ while true; do
done
+# Save command history to file (if enabled)
+if [[ "${PERSISTENT_HISTORY}" == "true" ]]; then
+ #mkdir -p -- "$(dirname -- "${HISTORY_FILE}")" && history -w -- "${HISTORY_FILE}" || { printn; printe "Error: failed to save command history to '${HISTORY_FILE}'"; exit 1; }
+ mkdir -p -- "$(dirname -- "${HISTORY_FILE}")" && history -w -- "${HISTORY_FILE}" || { printn; printe "Error: failed to save command history to '${HISTORY_FILE}'"; }
+fi
+
# Return home safely
exit 0