#!/usr/bin/env bash # shellcheck disable=SC2001 # This file contains common functions that the scripts will source. set -u # ----------------------------------------------------------------------------- # CONSTANTS # ----------------------------------------------------------------------------- ACCESSED_RECENTLY_DIR="$ROOT_DIR/Accessed recently" ACCESSED_RECENTLY_LINKS_TO_KEEP=10 FIELD_SEPARATOR=$'\r' # The main field separator. GUI_BOX_HEIGHT=550 # Height of the GUI dialog boxes. GUI_BOX_WIDTH=900 # Width of the GUI dialog boxes. GUI_INFO_WIDTH=400 # Width of the GUI small dialog boxes. IGNORE_FIND_PATH="*.git/*" # Path to ignore in the 'find' command. PREFIX_ERROR_LOG_FILE="Errors" # Name of 'error' directory. PREFIX_OUTPUT_DIR="Output" # Name of 'output' directory. TEMP_DIR=$(mktemp --directory) # Temp directories for use in scripts. TEMP_DIR_ITEMS_TO_REMOVE="$TEMP_DIR/items_to_remove" TEMP_DIR_LOGS="$TEMP_DIR/logs" TEMP_DIR_STORAGE_TEXT="$TEMP_DIR/storage_text" TEMP_DIR_TASK="$TEMP_DIR/task" WAIT_BOX_CONTROL_KDE="$TEMP_DIR/wait_box_control_kde" WAIT_BOX_CONTROL="$TEMP_DIR/wait_box_control" WAIT_BOX_FIFO="$TEMP_DIR/wait_box_fifo" readonly \ ACCESSED_RECENTLY_DIR \ ACCESSED_RECENTLY_LINKS_TO_KEEP \ FIELD_SEPARATOR \ GUI_BOX_HEIGHT \ GUI_BOX_WIDTH \ GUI_INFO_WIDTH \ IGNORE_FIND_PATH \ PREFIX_ERROR_LOG_FILE \ PREFIX_OUTPUT_DIR \ TEMP_DIR \ TEMP_DIR_LOGS \ TEMP_DIR_STORAGE_TEXT \ TEMP_DIR_TASK \ WAIT_BOX_CONTROL \ WAIT_BOX_CONTROL_KDE \ WAIT_BOX_FIFO # ----------------------------------------------------------------------------- # GLOBAL VARIABLES # ----------------------------------------------------------------------------- IFS=$FIELD_SEPARATOR INPUT_FILES=$* TEMP_DATA_TASK="" # ----------------------------------------------------------------------------- # BUILD THE STRUCTURE OF THE 'TEMP_DIR' # ----------------------------------------------------------------------------- # Store the path of temporary items to be removed after exit. mkdir -p "$TEMP_DIR_ITEMS_TO_REMOVE" mkdir -p "$TEMP_DIR_LOGS" # Store 'error logs'. mkdir -p "$TEMP_DIR_STORAGE_TEXT" # Store the output from parallel processes. mkdir -p "$TEMP_DIR_TASK" # Store temporary files of scripts. # ----------------------------------------------------------------------------- # FUNCTIONS # ----------------------------------------------------------------------------- _cleanup_on_exit() { # This function performs cleanup tasks when the script exits. It is # designed to safely and efficiently remove temporary directories or files # that were created during the script's execution. # Remove local temporary dirs or files. local items_to_remove="" items_to_remove=$(cat -- "$TEMP_DIR_ITEMS_TO_REMOVE/"* 2>/dev/null) # Escape single quotes in filenames to handle them correctly in 'xargs' # with 'bash -c'. items_to_remove=$(sed -z "s|'|'\\\''|g" <<<"$items_to_remove") printf "%s" "$items_to_remove" | xargs \ --no-run-if-empty \ --delimiter="$FIELD_SEPARATOR" \ --max-procs="$(_get_max_procs)" \ --replace="{}" \ bash -c "{ chmod -R u+rw -- '{}' && rm -rf -- '{}'; } &>/dev/null" # Remove the main temporary dir. rm -rf -- "$TEMP_DIR" &>/dev/null if ! _is_gui_session; then printf "End of the script.\n" >&2 fi } trap _cleanup_on_exit EXIT _check_dependencies() { # This function ensures that all required dependencies are available for # the scripts to run. It verifies the presence of specified commands or # packages and prompts the user to install missing ones. # # Parameters: # - $1 (dependencies): A list of dependencies to check, formatted as a # "|" delimited string. Each dependency can specify: # - "command": The name of a command to check for in the shell. # - "package": The package associated with the command (if different). # - "pkg_manager": Optional. The specific package manager. # # Example: # - _check_dependencies " # command=ffmpeg; pkg_manager=apt; package=ffmpeg | # command=ffmpeg; pkg_manager=dnf; package=ffmpeg-free | # command=ffmpeg; pkg_manager=pacman; package=ffmpeg | # command=ffmpeg; pkg_manager=zypper; package=ffmpeg" local dependencies=$1 local packages_to_install="" local pkg_manager_installed="" [[ -z "$dependencies" ]] && return # Skip duplicated dependencies in the input list. dependencies=$(tr "|" "\n" <<<"$dependencies") dependencies=$(tr -d " " <<<"$dependencies") dependencies=$(sort --unique <<<"$dependencies") dependencies=$(_text_remove_empty_lines "$dependencies") [[ -z "$dependencies" ]] && return # Get the name of the installed package manager. pkg_manager_installed=$(_pkg_get_package_manager) if [[ -z "$pkg_manager_installed" ]]; then _display_error_box "Could not find a package manager!" _exit_script fi # Check all dependencies. dependencies=$(tr "\n" "$FIELD_SEPARATOR" <<<"$dependencies") local dependency="" for dependency in $dependencies; do local command="" local package="" local pkg_manager="" # Evaluate the values parameters from the 'dependency' variable. eval "$dependency" # Ignore installing the dependency if there is a command in the shell. if [[ -n "$command" ]] && _command_exists "$command"; then continue fi # Ignore installing the dependency if the installed # package managers differ. if [[ -n "$pkg_manager" ]] && [[ "$pkg_manager_installed" != "$pkg_manager" ]]; then continue fi # Ignore installing the dependency if the package is already installed # (packages that do not have a command). if [[ -n "$package" ]] && [[ -z "$command" ]] && _pkg_is_package_installed "$pkg_manager_installed" "$package"; then continue fi # If the package is not specified, use the command name as # the package name. if [[ -z "$package" ]] && [[ -n "$command" ]]; then package=$command fi # Add the package to the list to install. if [[ -n "$package" ]]; then packages_to_install+=" $package" fi done # Ask the user to install the packages. if [[ -n "$packages_to_install" ]]; then local message="These packages were not found:" message+=$(sed "s| |\n- |g" <<<"$packages_to_install") message+=$'\n'$'\n' message+="Would you like to install them?" if _display_question_box "$message"; then _pkg_install_packages \ "$pkg_manager_installed" "${packages_to_install/ /}" else _exit_script fi fi } _check_output() { # This function validates the success of a command or process based on its # exit code and output. It logs errors if the command fails or if an # expected output file is missing. # # Parameters: # - $1 (exit_code): The exit code returned by the command or process. # - $2 (std_output): The standard output or error from the command. # - $3 (input_file): The input file associated (if applicable). # - $4 (output_file): The expected output file to verify its existence. # # Returns: # - "0" (true): If the command was successful and the output file exists. # - "1" (false): If the command failed or the output file does not exist. local exit_code=$1 local std_output=$2 local input_file=$3 local output_file=$4 # Check the 'exit_code' and log the error. if ((exit_code != 0)); then _log_error "Command failed with a non-zero exit code." \ "$input_file" "$std_output" "$output_file" return 1 fi # Check if the output file exists. if [[ -n "$output_file" ]] && [[ ! -e "$output_file" ]]; then _log_error "The output file does not exist." \ "$input_file" "$std_output" "$output_file" return 1 fi return 0 } _command_exists() { # This function checks whether a given command is available on the system. # # Parameters: # - $1 (command_check): The name of the command to verify. # # Returns: # - "0" (true): If the command is available. # - "1" (false): If the command is not available. local command_check=$1 if command -v "$command_check" &>/dev/null; then return 0 fi return 1 } _convert_delimited_string_to_text() { # This function converts a delimited string of items into # newline-separated text. # # Parameters: # - $1 (input_items): A string containing items separated by the # 'FIELD_SEPARATOR' variable. # # Returns: # - A string containing the items separated by newlines. local input_items=$1 local new_line="'\$'\\\n''" input_items=$(sed -z "s|\n|$new_line|g; s|$new_line$||g" <<<"$input_items") input_items=$(tr "$FIELD_SEPARATOR" "\n" <<<"$input_items") printf "%s" "$input_items" } _directory_pop() { # This function pops the top directory off the directory stack and changes # to the previous directory. # # Returns: # - "0" (true): If the directory was successfully popped and changed. # - "1" (false): If there was an error popping the directory. popd &>/dev/null || { _log_error "Could not pop a directory." "" "" "" return 1 } return 0 } _directory_push() { # This function pushes the specified directory onto the directory stack and # changes to it. # # Parameters: # - $1 (directory): The target directory to push onto the directory stack # and navigate to. # # Returns: # - "0" (true): If the directory was successfully pushed and changed. # - "1" (false): If there was an error pushing the directory. local directory=$1 pushd "$directory" &>/dev/null || { _log_error "Could not push the directory '$directory'." "" "" "" return 1 } return 0 } _convert_text_to_delimited_string() { # This function converts newline-separated text into a delimited string of # items. # # Parameters: # - $1 (input_items): A string containing items separated by newlines. # # Returns: # - A string containing the items separated by the 'FIELD_SEPARATOR' # variable. local input_items=$1 local new_line="'\$'\\\n''" input_items=$(tr "\n" "$FIELD_SEPARATOR" <<<"$input_items") input_items=$(sed -z "s|$new_line|\n|g" <<<"$input_items") input_items=$(_str_remove_empty_tokens "$input_items") printf "%s" "$input_items" } _display_dir_selection_box() { # This function presents a graphical interface to allow the user to select # one or more directories. local input_files="" if _command_exists "zenity"; then input_files=$(zenity --title "$(_get_script_name)" \ --file-selection --multiple --directory \ --separator="$FIELD_SEPARATOR" 2>/dev/null) || _exit_script elif _command_exists "kdialog"; then input_files=$(kdialog --title "$(_get_script_name)" \ --getexistingdirectory 2>/dev/null) || _exit_script # Use parameter expansion to remove the last space. input_files=${input_files% } input_files=${input_files// \//$FIELD_SEPARATOR/} fi input_files=$(_str_remove_empty_tokens "$input_files") printf "%s" "$input_files" } # shellcheck disable=SC2120 _display_file_selection_box() { # This function presents a graphical interface to allow the user to select # a file. # # Parameters: # - $1 (file_filter): Optional. File filter pattern to restrict the types # of files shown. # - $2 (title): Optional. Title of the window. local file_filter=${1:-""} local title=${2:-"$(_get_script_name)"} local input_files="" if _command_exists "zenity"; then input_files=$(zenity --title "$title" \ --file-selection \ ${file_filter:+--file-filter="$file_filter"} \ --separator="$FIELD_SEPARATOR" 2>/dev/null) || _exit_script elif _command_exists "kdialog"; then input_files=$(kdialog --title "$title" \ --getopenfilename 2>/dev/null) || _exit_script # Use parameter expansion to remove the last space. input_files=${input_files% } input_files=${input_files// \//$FIELD_SEPARATOR/} fi input_files=$(_str_remove_empty_tokens "$input_files") printf "%s" "$input_files" } _display_error_box() { # This function displays an error message to the user, adapting to the # available environment. # # Parameters: # - $1 (message): The error message to display. local message=$1 if ! _is_gui_session; then printf "Error: %s\n" "$message" >&2 elif [[ -n "$DBUS_SESSION_BUS_ADDRESS" ]]; then _gdbus_notify "dialog-error" "$(_get_script_name)" "$message" elif _command_exists "zenity"; then zenity --title "$(_get_script_name)" --error \ --width="$GUI_INFO_WIDTH" --text "$message" &>/dev/null elif _command_exists "kdialog"; then kdialog --title "$(_get_script_name)" --error "$message" &>/dev/null elif _command_exists "xmessage"; then xmessage -title "$(_get_script_name)" "Error: $message" &>/dev/null fi } _display_info_box() { # This function displays an information message to the user, adapting to # the available environment. # # Parameters: # - $1 (message): The information message to display. local message=$1 if ! _is_gui_session; then printf "Info: %s\n" "$message" >&2 elif [[ -n "$DBUS_SESSION_BUS_ADDRESS" ]]; then _gdbus_notify "dialog-information" "$(_get_script_name)" "$message" elif _command_exists "zenity"; then zenity --title "$(_get_script_name)" --info \ --width="$GUI_INFO_WIDTH" --text "$message" &>/dev/null elif _command_exists "kdialog"; then kdialog --title "$(_get_script_name)" --msgbox "$message" &>/dev/null elif _command_exists "xmessage"; then xmessage -title "$(_get_script_name)" "Info: $message" &>/dev/null fi } _display_list_box() { # This function displays a list box with selectable items, adapting to the # available environment. # # Parameters: # - $1 (message): A string containing the items to display in the list. # - $2 (columns): Column definitions for the list, typically in the # format "--column=;--column=". # - $3 (item_name): A string representing the name of the items in the # list. If not provided, the default value is "items". # - $4 (resolve_links): A boolean-like string ("true" or "false") # indicating whether symbolic links in item paths should be resolved to # their target locations when opening the item's location. Defaults to # "true". local message=$1 local columns=$2 local item_name=${3:-"items"} local resolve_links=${4:-"true"} local columns_count=0 local items_count=0 local selected_item="" local message_select="" _close_wait_box _logs_consolidate "" if [[ -n "$message" ]]; then items_count=$(tr -cd "\n" <<<"$message" | wc -c) message_select=" Select an item to open its location:" fi # Count the number of columns. columns_count=$(grep --only-matching "column=" <<<"$columns" | wc -l) if ! _is_gui_session; then if [[ -z "$message" ]]; then message="(Empty result)" printf "%s\n" "$message" >&2 else message=$(tr "$FIELD_SEPARATOR" " " <<<"$message") printf "%s\n" "$message" fi elif _command_exists "zenity"; then if [[ -z "$message" ]]; then # NOTE: Some versions of Zenity crash if the # message is empty (Segmentation fault). message=" " fi columns=$(tr ";" "$FIELD_SEPARATOR" <<<"$columns") message=$(tr "\n" "$FIELD_SEPARATOR" <<<"$message") # shellcheck disable=SC2086 selected_item=$(zenity --title "$(_get_script_name)" --list \ --editable --multiple --separator="$FIELD_SEPARATOR" \ --width="$GUI_BOX_WIDTH" --height="$GUI_BOX_HEIGHT" \ --print-column "$columns_count" \ --text "Total of $items_count $item_name.$message_select" \ $columns $message 2>/dev/null) || _exit_script if ((items_count != 0)) && [[ -n "$selected_item" ]]; then # Open the directory of the clicked item in the list. _open_items_locations "$selected_item" "$resolve_links" fi elif _command_exists "kdialog"; then columns=$(sed "s|--column=||g" <<<"$columns") columns=$(tr ";" "\t" <<<"$columns") message=$(tr "$FIELD_SEPARATOR" "\t" <<<"$message") message="$columns"$'\n'$'\n'"$message" kdialog --title "$(_get_script_name)" \ --geometry "${GUI_BOX_WIDTH}x${GUI_BOX_HEIGHT}" \ --textinputbox "" "$message" &>/dev/null || _exit_script elif _command_exists "xmessage"; then columns=$(sed "s|--column=||g" <<<"$columns") columns=$(tr ";" "\t" <<<"$columns") message=$(tr "$FIELD_SEPARATOR" "\t" <<<"$message") message="$columns"$'\n'$'\n'"$message" xmessage -title "$(_get_script_name)" \ "$message" &>/dev/null || _exit_script fi } _display_password_box() { # This function prompts the user to enter a password, either via the # terminal or a graphical dialog box. # # Parameters: # - $1 (message): A message to display as a prompt for the password. local message=$1 local password="" # Ask the user for the 'password'. if ! _is_gui_session; then read -r -p "$message " password >&2 elif _command_exists "zenity"; then sleep 0.2 # Avoid 'wait_box' open before. password=$(zenity \ --title="Password" --entry --hide-text --width="$GUI_INFO_WIDTH" \ --text "$message" 2>/dev/null) || return 1 elif _command_exists "kdialog"; then sleep 0.2 # Avoid 'wait_box' open before. password=$(kdialog --title "Password" \ --password "$message" 2>/dev/null) || return 1 fi printf "%s" "$password" } _display_password_box_define() { # This function prompts the user to enter a password and ensures the # password is not empty. local message="Type your password:" local password="" password=$(_display_password_box "$message") || return 1 # Check if the 'password' is not empty. if [[ -z "$password" ]]; then _display_error_box "The password can not be empty!" return 1 fi printf "%s" "$password" } _display_question_box() { # This function prompts the user with a yes/no question and returns the # user's response. # # Parameters: # - $1 (message): The question message to display to the user. local message=$1 local response="" if ! _is_gui_session; then read -r -p "$message [Y/n] " response [[ ${response,,} == *"n"* ]] && return 1 elif _command_exists "zenity"; then zenity --title "$(_get_script_name)" --question \ --width="$GUI_INFO_WIDTH" --text="$message" &>/dev/null || return 1 elif _command_exists "kdialog"; then kdialog --title "$(_get_script_name)" \ --yesno "$message" &>/dev/null || return 1 elif _command_exists "xmessage"; then xmessage -title "$(_get_script_name)" \ -buttons "Yes:0,No:1" "$message" &>/dev/null || return 1 fi return 0 } _display_text_box() { # This function displays a message to the user in a text box, either in the # terminal or using a GUI dialog. # # Parameters: # - $1 (message): The message to display. If empty, a default message # "(Empty result)" is shown. local message=$1 _close_wait_box _logs_consolidate "" if [[ -z "$message" ]]; then message="(Empty result)" fi if ! _is_gui_session; then printf "%s\n" "$message" elif _command_exists "zenity"; then zenity --title "$(_get_script_name)" --text-info --no-wrap \ --width="$GUI_BOX_WIDTH" --height="$GUI_BOX_HEIGHT" \ <<<"$message" &>/dev/null || _exit_script elif _command_exists "kdialog"; then kdialog --title "$(_get_script_name)" \ --geometry "${GUI_BOX_WIDTH}x${GUI_BOX_HEIGHT}" \ --textinputbox "" \ "$message" &>/dev/null || _exit_script elif _command_exists "xmessage"; then xmessage -title "$(_get_script_name)" \ "$message" &>/dev/null || _exit_script fi } _display_result_box() { # This function displays a result summary at the end of a process, # including error checking and output directory information. # # Parameters: # - $1 (output_dir): The directory where output files are stored or # expected to be. local output_dir=$1 _close_wait_box _logs_consolidate "$output_dir" # If 'output_dir' parameter is defined. if [[ -n "$output_dir" ]]; then # Try to remove the output directory (if it is empty). if [[ "$output_dir" == *"/$PREFIX_OUTPUT_DIR"* ]]; then rmdir "$output_dir" &>/dev/null fi # Check if the output directory still exists. if [[ -d "$output_dir" ]]; then local dir_label="" dir_label=$(_str_human_readable_path "$output_dir") _display_info_box \ "Finished! The output files are in the $dir_label directory." else _display_info_box "Finished, but there is nothing to do." fi else _display_info_box "Finished!" fi } _display_wait_box() { # This function displays a wait box to inform the user that a task is # running and they need to wait. # # Parameters: # - $1 (open_delay): Optional. The delay (in seconds) before the wait box # is shown. Defaults to 2 seconds if not provided. local open_delay=${1:-"2"} local message="Running the task. Please, wait..." _display_wait_box_message "$message" "$open_delay" } _display_wait_box_message() { # This function displays a wait box (progress indicator) to inform the user # that a task is in progress. # # Parameters: # - $1 (message): The message to display inside the wait box (e.g., # "Running the task. Please, wait..."). # - $2 (open_delay): Optional. The delay (in seconds) before the wait box # is shown. Defaults to 2 seconds if not provided. local message=$1 local open_delay=${2:-"2"} if ! _is_gui_session; then # For non-GUI sessions, simply print the message to the console. printf "%s\n" "$message" >&2 # Check if the Zenity is available. elif _command_exists "zenity"; then # Control flag to inform that a 'wait_box' will open # (if the task takes over 2 seconds). touch "$WAIT_BOX_CONTROL" # Create the FIFO for communication with Zenity 'wait_box'. if [[ ! -p "$WAIT_BOX_FIFO" ]]; then mkfifo "$WAIT_BOX_FIFO" fi # Launch a background thread for Zenity 'wait_box': # - Waits for the specified delay. # - Opens the Zenity 'wait_box' if the control flag still exists. # - If Zenity 'wait_box' fails or is cancelled, exit the script. # shellcheck disable=SC2002 sleep "$open_delay" && [[ -f "$WAIT_BOX_CONTROL" ]] && tail -f -- "$WAIT_BOX_FIFO" | (zenity \ --title="$(_get_script_name)" --progress \ --width="$GUI_INFO_WIDTH" \ --pulsate --auto-close --text="$message" || _exit_script) & # Check if the KDialog is available. elif _command_exists "kdialog"; then _get_qdbus_command || return 0 # Control flag to inform that a 'wait_box' will open # (if the task takes over 2 seconds). touch "$WAIT_BOX_CONTROL" # Launch a background thread for KDialog 'wait_box': # - Waits for the specified delay. # - Opens the KDialog 'wait_box' if the control flag still exists. sleep "$open_delay" && [[ -f "$WAIT_BOX_CONTROL" ]] && kdialog --title="$(_get_script_name)" \ --progressbar "$message" 0 >"$WAIT_BOX_CONTROL_KDE" & # Launch another background thread to monitor the KDialog 'wait_box': # - Periodically checks if the dialog has been closed or cancelled. # - If KDialog 'wait_box' is cancelled, exit the script. ( while [[ -f "$WAIT_BOX_CONTROL" ]] || [[ -f "$WAIT_BOX_CONTROL_KDE" ]]; do if [[ -f "$WAIT_BOX_CONTROL_KDE" ]]; then # Extract the D-Bus reference for the KDialog instance. local dbus_ref="" dbus_ref=$(cut -d " " -f 1 <"$WAIT_BOX_CONTROL_KDE") if [[ -n "$dbus_ref" ]]; then # Check if the user has cancelled the wait box. $(_get_qdbus_command) "$dbus_ref" "/ProgressDialog" \ "wasCancelled" 2>/dev/null || _exit_script fi fi sleep 0.2 done ) & fi } _close_wait_box() { # This function is responsible for closing any open "wait boxes" (progress # indicators) that were displayed during the execution of a task. It checks # for both Zenity and KDialog wait boxes and handles their closure. # Check if 'wait_box' will open. if [[ -f "$WAIT_BOX_CONTROL" ]]; then rm -f -- "$WAIT_BOX_CONTROL" # Cancel the future open. fi # Check if Zenity 'wait_box' is open (waiting for an input in the FIFO). if pgrep -fl "$WAIT_BOX_FIFO" &>/dev/null; then # Close the Zenity using the FIFO. printf "100\n" >"$WAIT_BOX_FIFO" fi # Check if KDialog 'wait_box' is open. while [[ -f "$WAIT_BOX_CONTROL_KDE" ]]; do # Extract the D-Bus reference for the KDialog instance. local dbus_ref="" dbus_ref=$(cut -d " " -f 1 <"$WAIT_BOX_CONTROL_KDE") if [[ -n "$dbus_ref" ]]; then # Close the KDialog 'wait_box'. $(_get_qdbus_command) "$dbus_ref" "/ProgressDialog" \ "close" 2>/dev/null rm -f -- "$WAIT_BOX_CONTROL_KDE" fi sleep 0.2 done } _exit_script() { # This function is responsible for safely exiting the script by terminating # all child processes associated with the current script and printing an # exit message to the terminal. _close_wait_box local child_pids="" local script_pid=$$ if ! _is_gui_session; then printf "Exiting the script...\n" >&2 fi # Get the process ID (PID) of all child processes. child_pids=$(pstree -p "$script_pid" | grep --only-matching --perl-regexp "\(+\K[^)]+") # NOTE: Use 'xargs' and kill to send the SIGTERM signal to all child # processes, including the current script. # See the: https://www.baeldung.com/linux/safely-exit-scripts xargs kill <<<"$child_pids" &>/dev/null } _find_filtered_files() { # This function filters a list of files or directories based on various # user-specified criteria, such as file type, extensions, and recursion. # # Parameters: # - $1 (input_files): A space-separated string containing file or # directory paths to filter. These paths are passed to the "find" # command. # - $2 (par_type): A string specifying the type of file to search for. It # can be: # - "file": To search for files and symbolic links. # - "directory": To search for directories and symbolic links. # - $3 (par_skip_extension): A string of file extensions to exclude from # the search. Only files with extensions not matching this list will be # included. # - $4 (par_select_extension): A string of file extensions to include in # the search. Only files with matching extensions will be included. # - $5 (par_find_parameters): Optional. Additional parameters to be # passed directly to the "find" command. # # Example: # - Input: "dir1 dir2", "file", "", "txt|pdf", "true" # - Output: A list of files with extensions ".txt" or ".pdf" from the # directories "dir1" and "dir2", searched recursively. local input_files=$1 local par_type=$2 local par_skip_extension=$3 local par_select_extension=$4 local par_find_parameters=$5 local filtered_files="" local find_command="" input_files=$(sed "s|'|'\"'\"'|g" <<<"$input_files") input_files=$(sed "s|$FIELD_SEPARATOR|' '|g" <<<"$input_files") # Build a 'find' command. find_command="find '$input_files'" if [[ -n "$par_find_parameters" ]]; then find_command+=" $par_find_parameters" fi # Expand the directories with the 'find' command. case "$par_type" in "file") find_command+=" \( -type l -o -type f \)" ;; "directory") find_command+=" \( -type l -o -type d \)" ;; esac if [[ -n "$par_select_extension" ]]; then find_command+=" -regextype posix-extended " find_command+=" -regex \".*\.($par_select_extension)$\"" fi if [[ -n "$par_skip_extension" ]]; then find_command+=" -regextype posix-extended " find_command+=" ! -regex \".*\.($par_skip_extension)$\"" fi find_command+=" ! -path \"$IGNORE_FIND_PATH\"" # shellcheck disable=SC2089 find_command+=" -print0" # shellcheck disable=SC2086 filtered_files=$(eval $find_command 2>/dev/null | tr "\0" "$FIELD_SEPARATOR") filtered_files=$(_str_remove_empty_tokens "$filtered_files") # Return the filtered files. printf "%s" "$filtered_files" } _gdbus_notify() { # This function sends a desktop notification using the "gdbus" tool, which # interfaces with the D-Bus notification system (specifically the # "org.freedesktop.Notifications" service). # # Parameters: # - $1 (icon): The icon to display with the notification. # - $2 (title): The title of the notification. # - $3 (body): The main message to be displayed in the notification. local icon=$1 local title=$2 local body=$3 local app_name=$title local method="Notify" local interface="org.freedesktop.Notifications" local object_path="/org/freedesktop/Notifications" # Use 'gdbus' to send the notification. gdbus call --session --dest "$interface" --object-path "$object_path" \ --method "$interface.$method" "$app_name" 0 "$icon" "$title" "$body" \ "[]" '{"urgency": <1>}' 5000 &>/dev/null } _get_filename_dir() { # This function extracts the directory path from a given file path. # # Parameters: # - $1 (input_filename): The full path or relative path to the file. local input_filename=$1 local dir="" dir=$(cd -- "$(dirname -- "$input_filename")" &>/dev/null && pwd) printf "%s" "$dir" } _get_filename_extension() { # This function extracts the file extension from a given filename. # # Parameters: # - $1 (filename): The input filename (can be absolute or relative). local filename=$1 filename=$(sed -E "s|.*/(\.)*||g" <<<"$filename") filename=$(sed -E "s|^(\.)*||g" <<<"$filename") printf "%s" "$filename" | grep --ignore-case --only-matching --perl-regexp \ "(\.tar)?\.[a-z0-9_~-]{0,15}$" || true } _get_filename_full_path() { # This function returns the full absolute path of a given filename. # # Parameters: # - $1 (input_filename): The input filename or relative path. local input_filename=$1 local full_path=$input_filename local dir="" if [[ $input_filename != "/"* ]]; then dir=$(_get_filename_dir "$input_filename") full_path=$dir/$(basename -- "$input_filename") fi printf "%s" "$full_path" } _get_filename_next_suffix() { # This function generates a unique filename by adding a numeric suffix to # the base filename if a file with the same name already exists. It ensures # that the new filename does not overwrite an existing file. # # Parameters: # - $1 (filename): The input filename or path. This can be an absolute or # relative filename. If the input file has an extension, it will be # stripped for the purpose of generating the new filename. local filename=$1 local filename_result=$filename local filename_base="" local filename_extension="" # Directories do not have an extension. if [[ -d "$filename" ]]; then filename_base=$filename else filename_base=$(_strip_filename_extension "$filename") filename_extension=$(_get_filename_extension "$filename") fi # Avoid overwriting a file. If there is a file with the same name, # try to add a suffix, as 'file (1)', 'file (2)', ... local count=1 while [[ -e "$filename_result" ]]; do filename_result="$filename_base ($count)$filename_extension" ((count++)) done printf "%s" "$filename_result" } _get_filenames_filemanager() { # This function retrieves a list of selected filenames or URIs from a file # manager (such as Caja, Nemo, or Nautilus) and processes the input # accordingly. If no selection is detected, it falls back to using a # standard input file list. local input_files="" # Try to use the information provided by the file manager. if [[ -v "CAJA_SCRIPT_SELECTED_URIS" ]]; then input_files=$CAJA_SCRIPT_SELECTED_URIS elif [[ -v "NEMO_SCRIPT_SELECTED_URIS" ]]; then input_files=$NEMO_SCRIPT_SELECTED_URIS elif [[ -v "NAUTILUS_SCRIPT_SELECTED_URIS" ]]; then input_files=$NAUTILUS_SCRIPT_SELECTED_URIS fi if [[ -n "$input_files" ]]; then # Replace '\n' with 'FIELD_SEPARATOR'. input_files=$(_convert_text_to_delimited_string "$input_files") # Decode the URI list. input_files=$(_text_uri_decode "$input_files") else input_files=$INPUT_FILES # Standard input. input_files=$(_str_remove_empty_tokens "$input_files") fi printf "%s" "$input_files" } _get_files() { # This function retrieves a list of files or directories based on the # provided parameters and performs various filtering, validation, and # sorting operations. The input files can be filtered by type, extension, # mime type, etc. It also supports recursive directory expansion and # validation of file conflicts. # # Parameters: # - $1 (parameters): A string containing key-value pairs that configure # the function's behavior. Example: 'par_type=file; par_min_items=2'. # # Parameters options: # - "par_type": Specifies the type of items to filter: # - "file" (default): Filters files. # - "directory": Filters directories. # - "all": Includes both files and directories. # - "par_recursive": Specifies whether to expand directories recursively: # - "false" (default): Does not expand directories. # - "true": Expands directories recursively. # - "par_get_pwd": If "true", returns the current working directory if no # files are selected. # - "par_max_items", "par_min_items": Limits the number of files. # - "par_select_extension": Filters by file extension. # - "par_select_mime": Filters by MIME type. # - "par_skip_extension": Skips files with specific extensions. # - "par_skip_encoding": Skips files with specific encodings. # - "par_sort_list": If "true", sorts the list of files. # - "par_validate_conflict": If "true", validates for filenames with the # same base name. local parameters=$1 local input_files="" input_files=$(_get_filenames_filemanager) # Default values for input parameters. local par_get_pwd="false" local par_max_items="" local par_min_items="" local par_recursive="false" local par_select_extension="" local par_select_mime="" local par_skip_encoding="" local par_skip_extension="" local par_sort_list="false" local par_type="file" local par_validate_conflict="" # Evaluate the values from the 'parameters' variable. eval "$parameters" # Check if there are input files. if (($(_get_items_count "$input_files") == 0)); then if [[ "$par_get_pwd" == "true" ]]; then # Return the current working directory if no files have been # selected. input_files=$(_get_working_directory) else # Try selecting the files by opening a file selection box. if [[ "$par_type" == "directory" ]]; then input_files=$(_display_dir_selection_box) else input_files=$(_display_file_selection_box) fi fi fi # If the items are in a remote server, translate the addresses to 'gvfs'. if [[ "$input_files" == *"://"* ]]; then local working_directory="" working_directory=$(_get_working_directory) input_files=$(sed \ "s|[a-z0-9\+_-]*://[^$FIELD_SEPARATOR]*/|$working_directory/|g" \ <<<"$input_files") fi local initial_items_count=0 initial_items_count=$(_get_items_count "$input_files") local find_parameters="" if ((initial_items_count == 1)) && [[ -d "$input_files" ]] && [[ "$par_recursive" == "false" ]] && printf "%s" "$(basename -- "$input_files")" | grep --quiet --ignore-case --word-regexp "batch"; then # This workaround allows the scripts to handle cases with a large input # list of files. In this case, just select a single directory with a # name that includes the word 'batch'. Then, the scripts operate on the # files within the selected directory. This addresses the GNOME error: # "Could not start application: Failed to execute child process # "/bin/sh" (Argument list too long)". find_parameters="-mindepth 1 -maxdepth 1" elif [[ "$par_recursive" == "false" ]]; then find_parameters="-maxdepth 0" fi # Pre-select the input files. input_files=$(_find_filtered_files \ "$input_files" \ "$par_type" \ "$par_skip_extension" \ "$par_select_extension" \ "$find_parameters") # Return the current working directory if no directories have been # selected. if (($(_get_items_count "$input_files") == 0)); then if [[ "$par_get_pwd" == "true" ]] && [[ "$par_type" == "directory" ]]; then input_files=$(_get_working_directory) fi fi # Validates the mime or encoding of the file. if [[ -n "$par_select_mime" ]] || [[ -n "$par_skip_encoding" ]]; then input_files=$(_validate_file_mime_parallel \ "$input_files" \ "$par_select_mime" \ "$par_skip_encoding") fi # Validates the number of valid files. _validate_files_count \ "$input_files" \ "$par_type" \ "$par_select_extension" \ "$par_select_mime" \ "$par_min_items" \ "$par_max_items" \ "$par_recursive" # Sort the list by filename. if [[ "$par_sort_list" == "true" ]]; then input_files=$(printf "%s" "$input_files" | tr "$FIELD_SEPARATOR" "\0" | sort --zero-terminated --version-sort | tr "\0" "$FIELD_SEPARATOR") input_files=$(_str_remove_empty_tokens "$input_files") fi # Validates filenames with the same base name. if [[ "$par_validate_conflict" == "true" ]]; then _validate_conflict_filenames "$input_files" fi printf "%s" "$input_files" } _get_file_encoding() { # This function retrieves the MIME encoding of a specified file. # # Parameters: # - $1 (filename): The path to the file whose encoding is to be # determined. local filename=$1 local std_output="" std_output=$(file --dereference --brief --mime-encoding \ -- "$filename" 2>/dev/null) if [[ "$std_output" == "cannot"* ]]; then return fi printf "%s" "$std_output" } _get_file_mime() { # This function retrieves the MIME type of a specified file. # # Parameters: # - $1 (filename): The path to the file whose MIME is to be determined. local filename=$1 local std_output="" std_output=$(file --dereference --brief --mime-type \ -- "$filename" 2>/dev/null) if [[ "$std_output" == "cannot"* ]]; then return fi printf "%s" "$std_output" } _get_items_count() { # This function counts the number of items in a string, where items are # separated by a specific field separator. It assumes that the input string # contains a list of items separated by the value of the variable # $FIELD_SEPARATOR. # # Parameters: # - $1 (input_files): A string containing a list of items separated by # the defined $FIELD_SEPARATOR. local input_files=$1 local files_count=0 if [[ -n "$input_files" ]]; then files_count=$(tr -cd "$FIELD_SEPARATOR" <<<"$input_files" | wc -c) ((files_count++)) fi printf "%s" "$files_count" } _get_max_procs() { # This function returns the maximum number of processing units (CPU cores) # available on the system. nproc --all 2>/dev/null } _get_output_dir() { # This function determines and returns the appropriate output directory # path based on the provided parameters and the system's available # directories. # # Parameters: # - $1 (parameters): A string containing key-value pairs that configure # the function's behavior. Example: 'par_use_same_dir=true'. # # Parameters options: # - "par_use_same_dir": If set to "true", the function uses the base # directory (e.g., current working directory or an alternative with write # permissions) as the output directory. If "false" or not set, a new # subdirectory is created for the output. local parameters=$1 local base_dir="" local output_dir="" # Default values for input parameters. local par_use_same_dir="" # Evaluate the values from the 'parameters' variable. eval "$parameters" # Check directories available to put the 'output' dir. base_dir=$(_get_working_directory) [[ ! -w "$base_dir" ]] && base_dir=$HOME [[ ! -w "$base_dir" ]] && base_dir="/tmp" if [[ ! -w "$base_dir" ]]; then _display_error_box "Could not find a directory with write permissions!" _exit_script fi if [[ "$par_use_same_dir" == "true" ]]; then printf "%s" "$base_dir" return fi output_dir="$base_dir/$PREFIX_OUTPUT_DIR" # If the file already exists, add a suffix. output_dir=$(_get_filename_next_suffix "$output_dir") mkdir --parents "$output_dir" printf "%s" "$output_dir" } _get_output_filename() { # This function generates the output filename based on the given input # file, output directory, and a set of optional parameters. It allows for # customization of the output filename by adding prefixes, suffixes, and # selecting how the file extension should be handled. # # Parameters: # - $1 (input_file): The input file for which the output filename will be # generated. # - $2 (output_dir): The directory where the output file will be placed. # - $3 (parameters): A string containing optional parameters that define # how the output filename should be constructed. It supports the # following options: # - par_extension_opt: Specifies how to handle the file extension. # Options are: # - "append": Append a new extension "par_extension" to the existing # file extension. # - "preserve": Keep the original file extension. # - "replace": Replace the current extension with a new one # "par_extension". # - "strip": Remove the file extension entirely. # - par_extension: The extension to use when "par_extension_opt" is set # to "append" or "replace". This value is ignored for the "preserve" # and "strip" options. # - par_prefix: A string to be added as prefix to the output filename. # - par_suffix: A string to be added as suffix to the output # filename, placed before the extension. local input_file=$1 local output_dir=$2 local parameters=$3 local output_file="" local filename="" # Default values for input parameters. local par_extension_opt="preserve" local par_extension="" local par_prefix="" local par_suffix="" # Evaluate the values from the 'parameters' variable. eval "$parameters" filename=$(basename -- "$input_file") # Start constructing the output file path with the output directory. output_file="$output_dir/" [[ -n "$par_prefix" ]] && output_file+="$par_prefix " if [[ -d "$input_file" ]]; then # Handle case where input_file is a directory. case "$par_extension_opt" in "append" | "replace") output_file+=$filename output_file+=".$par_extension" ;; "preserve" | "strip") output_file+=$filename ;; esac else # Handle case where input_file is a regular file. case "$par_extension_opt" in "append") # Append the new extension to the existing one. output_file+=$(_strip_filename_extension "$filename") [[ -n "$par_suffix" ]] && output_file+=" $par_suffix" output_file+=$(_get_filename_extension "$filename") output_file+=".$par_extension" ;; "preserve") # Preserve the original extension. output_file+=$(_strip_filename_extension "$filename") [[ -n "$par_suffix" ]] && output_file+=" $par_suffix" output_file+=$(_get_filename_extension "$filename") ;; "replace") # Replace the existing extension with the new one. output_file+=$(_strip_filename_extension "$filename") [[ -n "$par_suffix" ]] && output_file+=" $par_suffix" output_file+=".$par_extension" ;; "strip") # Remove the extension. output_file+=$(_strip_filename_extension "$filename") [[ -n "$par_suffix" ]] && output_file+=" $par_suffix" ;; esac fi # If the file already exists, add a suffix. output_file=$(_get_filename_next_suffix "$output_file") printf "%s" "$output_file" } _get_qdbus_command() { # This function retrieves the first command matching the pattern "qdbus". # The command may vary depending on the Linux distribution and could be # "qdbus", "qdbus-qt6", "qdbus6", or similar variations. # # Returns: # - "0" (true): If a command matching the pattern "qdbus" is found. # - "1" (false): If no command matching the pattern "qdbus" is found. compgen -c | grep --perl-regexp -m1 "^qdbus" || return 1 return 0 } _get_script_name() { # This function returns the name of the currently executing script. It uses # the "basename" command to extract the script's filename from the full # path provided by "$0". basename -- "$0" } _get_temp_dir_local() { # This function creates a temporary directory in a specified location and # returns its path. The directory is created using "mktemp", with a custom # prefix ("basename"). It also generates a temporary file to track the # directory to be removed later. # # Parameters: # - $1 (output_dir): The directory where the temporary directory will be # created. # - $2 (basename): The prefix for the temporary directory name. # # Output: # - The full path to the newly created temporary directory. local output_dir=$1 local basename=$2 local temp_dir="" temp_dir=$(mktemp --directory \ --tmpdir="$output_dir" "$basename.XXXXXXXX.tmp") # Remember to remove this directory after exit. item_to_remove=$(mktemp --tmpdir="$TEMP_DIR_ITEMS_TO_REMOVE") printf "%s$FIELD_SEPARATOR" "$temp_dir" >"$item_to_remove" printf "%s" "$temp_dir" } _get_temp_file() { # This function creates a temporary file in a specified temporary directory # and returns its path. The file is created using "mktemp", and the # directory for the temporary file is specified by the $TEMP_DIR_TASK # variable. # # Output: # - The full path to the newly created temporary file. local temp_file="" temp_file=$(mktemp --tmpdir="$TEMP_DIR_TASK") printf "%s" "$temp_file" } _get_temp_file_dry() { # This function simulates the creation of a temporary file in a specified # directory without actually creating the file. It uses the "--dry-run" # option with "mktemp", which allows checking what the file path would be # if it were to be created. # # Output: # - The path of the temporary file, without actually creating the file. local temp_file="" temp_file=$(mktemp --dry-run --tmpdir="$TEMP_DIR_TASK") printf "%s" "$temp_file" } _get_working_directory() { # This function attempts to determine the current working directory in a # variety of ways, based on the available environment variables or input # files. It first checks if the file manager (e.g., Caja, Nemo, or # Nautilus) provides the current URI (directory) of the script. If not, it # falls back to other methods. If the working directory cannot be obtained # from the file manager or input files, it defaults to the current # directory. # # Output: # - The determined working directory. local working_directory="" # Try to use the information provided by the file manager. if [[ -v "CAJA_SCRIPT_CURRENT_URI" ]]; then working_directory=$CAJA_SCRIPT_CURRENT_URI elif [[ -v "NEMO_SCRIPT_CURRENT_URI" ]]; then working_directory=$NEMO_SCRIPT_CURRENT_URI elif [[ -v "NAUTILUS_SCRIPT_CURRENT_URI" ]]; then working_directory=$NAUTILUS_SCRIPT_CURRENT_URI fi if [[ -n "$working_directory" ]] && [[ "$working_directory" == "file://"* ]]; then working_directory=$(_text_uri_decode "$working_directory") else # Files selected in the search screen (or other possible cases). working_directory="" fi if [[ -z "$working_directory" ]]; then # NOTE: The working directory can be detected by using the directory # name of the first input file. Some file managers do not send the # working directory for the scripts, so it is not precise to use the # 'pwd' command. local item_1="" item_1=$(cut -d "$FIELD_SEPARATOR" -f 1 <<<"$INPUT_FILES") if [[ -n "$item_1" ]]; then working_directory=$(_get_filename_dir "$item_1") else working_directory=$(pwd) fi fi printf "%s" "$working_directory" } _is_directory_empty() { # This function checks if a given directory is empty. # # Parameters: # - $1 (directory): The path of the directory to check. # # Returns: # - "0" (true): If the directory is empty. # - "1" (false): If the directory contains any files or subdirectories. local directory=$1 if ! find "$directory" -mindepth 1 -maxdepth 1 -print -quit | grep --quiet .; then return 0 fi return 1 } _is_gui_session() { # This function checks whether the script is running in a graphical user # interface (GUI) session. It does so by checking if the DISPLAY # environment variable is set, which is typically present in GUI sessions # (e.g., X11 or Wayland). # # Returns: # - "0" (true): If is a GUI session. # - "1" (false): If is not a GUI session. if env | grep --quiet "^DISPLAY"; then return 0 fi return 1 } _log_error() { # This function writes an error log entry with a specified message, # including details such as the input file, output file, and terminal # output. The entry is saved to a temporary log file. # # Parameters: # - $1 (message): The error message to be logged. # - $2 (input_file): The path of the input file associated with the # operation. # - $3 (std_output): The standard output or result from the operation # that will be logged. # - $4 (output_file): The path of the output file associated with the # operation. local message=$1 local input_file=$2 local std_output=$3 local output_file=$4 local log_temp_file="" log_temp_file=$(mktemp --tmpdir="$TEMP_DIR_LOGS") { printf "[%s]\n" "$(date "+%Y-%m-%d %H:%M:%S")" if [[ -n "$input_file" ]]; then printf " > Input file: %s\n" "$input_file" fi if [[ -n "$output_file" ]]; then printf " > Output file: %s\n" "$output_file" fi printf " > %s\n" "Error: $message" if [[ -n "$std_output" ]]; then printf " > Standard output:\n" printf "%s\n" "$std_output" fi printf "\n" } >"$log_temp_file" } _logs_consolidate() { # This function gathers all error logs from a temporary directory and # compiles them into a single consolidated log file. If any error logs are # found, it displays an error message indicating the location of the # consolidated log file and terminates the script. # # Parameters: # $1 (output_dir): Optional. The directory where the consolidated log # file will be saved. If not specified, a default directory is used. local output_dir=$1 local log_file_output="$output_dir/$PREFIX_ERROR_LOG_FILE.log" local log_files_count="" # Do nothing if there are no error log files. log_files_count="$(find "$TEMP_DIR_LOGS" -type f 2>/dev/null | wc -l)" if ((log_files_count == 0)); then return 0 fi if [[ -z "$output_dir" ]]; then output_dir=$(_get_output_dir "par_use_same_dir=true") fi log_file_output="$output_dir/$PREFIX_ERROR_LOG_FILE.log" # If the file already exists, add a suffix. log_file_output=$(_get_filename_next_suffix "$log_file_output") # Compile log errors in a single file. { printf "Script: '%s'.\n" "$(_get_script_name)" printf "Total errors: %s.\n\n" "$log_files_count" cat -- "$TEMP_DIR_LOGS/"* 2>/dev/null } >"$log_file_output" local log_file="" log_file=$(_str_human_readable_path "$log_file_output") _display_error_box "Finished with errors! See the $log_file for details." _exit_script } _move_file() { # This function moves a file from the source location to the destination, # with options to handle conflicts when the destination file already # exists. # # Parameters: # - $1 (par_when_conflict): Optional, default: "skip". Defines the # behavior when the destination file already exists. # - "overwrite": Overwrite the destination file. # - "rename": Rename the source file to avoid conflicts by adding a # suffix to the destination filename. # - "skip": Skip moving the file if the destination file exists (logs a # error). # - $2 (file_src): The path to the source file to be moved. # - $3 (file_dst): The destination path where the file should be moved. # # Returns: # - "0" (true): If the operation is successful or if the source and # destination are the same file. # - "1" (false): If any required parameters are missing, if the move # fails, or if an invalid conflict parameter is provided. local par_when_conflict=${1:-"skip"} local file_src=$2 local file_dst=$3 # Check for empty parameters. if [[ -z "$file_src" ]] || [[ -z "$file_dst" ]]; then return 1 fi # Add the './' prefix in the path. if [[ ! "$file_src" == "/"* ]] && [[ ! "$file_src" == "./"* ]] && [[ ! "$file_src" == "." ]]; then file_src="./$file_src" fi if [[ ! "$file_dst" == "/"* ]] && [[ ! "$file_dst" == "./"* ]] && [[ ! "$file_dst" == "." ]]; then file_dst="./$file_dst" fi # Ignore moving to the same file. if [[ "$file_src" == "$file_dst" ]]; then return 0 fi # Process the parameter "when_conflict": what to do when the 'file_dst' # already exists. case "$par_when_conflict" in "overwrite") mv -f -- "$file_src" "$file_dst" 2>/dev/null ;; "rename") # Rename the file (add a suffix). file_dst=$(_get_filename_next_suffix "$file_dst") mv -n -- "$file_src" "$file_dst" 2>/dev/null ;; "skip") # Do not move the file if the destination file already exists. mv -n -- "$file_src" "$file_dst" 2>/dev/null ;; esac if [[ -e "$file_src" ]]; then if [[ -e "$file_dst" ]]; then _log_error "The destination file already exists." \ "$file_src" "" "$file_dst" else _log_error "Unable to move." \ "$file_src" "" "$file_dst" fi return 1 fi return 0 } _move_temp_file_to_output() { # This function moves a temporary file to a specified output location, # handling various conditions to ensure the move is safe and proper. # # Parameters: # - $1 (input_file): The path to the original input file, which may be # backed up during the process. # - $2 (temp_file): The temporary file to be moved to the output # location. # - $3 (output_file): The target path where the temp file should be # moved. local input_file=$1 local temp_file=$2 local output_file=$3 local std_output="" # Skip empty files. if [[ ! -s "$temp_file" ]]; then return 1 fi # Skip files if the content of 'temp_file' is equal to the 'input_file'. if cmp --silent -- "$temp_file" "$input_file"; then return 1 fi # If 'input_file' equals 'output_file', create a backup of 'input_file'. if [[ "$input_file" == "$output_file" ]]; then _move_file "rename" "$input_file" "$input_file.bak" || return 1 fi # Move the 'temp_file' to 'output_file'. _move_file "rename" "$temp_file" "$output_file" || return 1 # Preserve the same permissions of 'input_file'. std_output=$(chmod --reference="$input_file" -- "$output_file" 2>&1) _check_output "$?" "$std_output" "$input_file" "$output_file" || return 1 return 0 } _open_items_locations() { # This function opens the locations of selected items in the appropriate # file manager. # # Parameters: # - $1 (items): A space-separated list of file or directory paths whose # locations will be opened. Paths can be relative or absolute. # - $2 (resolve_links): A boolean-like string ("true" or "false") # indicating whether symbolic links in the provided paths should be # resolved to their target locations before opening. local items=$1 local resolve_links=$2 # Exit if no items are provided. if [[ -z "$items" ]]; then return fi # Detect the currently running file manager. local file_manager="" if [[ -v "CAJA_SCRIPT_SELECTED_URIS" ]]; then file_manager="caja" elif [[ -v "NEMO_SCRIPT_SELECTED_URIS" ]]; then file_manager="nemo" elif [[ -v "NAUTILUS_SCRIPT_SELECTED_URIS" ]]; then file_manager="nautilus" else file_manager=$(pgrep -o -l \ "caja|dolphin|nautilus|nemo|pcmanfm|pcmanfm-qt|thunar" | cut -f 2 -d " ") # If no file manager is detected, fall back to the system default. if [[ -z "$file_manager" ]]; then file_manager=$(_xdg_get_default_app "inode/directory") fi fi # Restore absolute paths for items if relative paths are used. local working_directory="" working_directory=$(_get_working_directory) items=$(sed "s|\./|$working_directory/|g" <<<"$items") # Prepare items to be opened by the file manager. local items_open="" local item="" for item in $items; do # Skip the root directory ("/") since opening it is redundant. if [[ "$item" == "/" ]]; then continue fi # Resolve symbolic links to their target locations if requested. if [[ "$resolve_links" == "true" ]] && [[ -L "$item" ]]; then item=$(readlink -f "$item") fi items_open+="$item$FIELD_SEPARATOR" done # Remove the trailing field separator. items_open=$(_str_remove_empty_tokens "$items_open") # Open the items using the detected file manager. case "$file_manager" in "nautilus" | "caja" | "dolphin") # Open the directory of each item and select it. # shellcheck disable=SC2086 $file_manager --select $items_open & ;; "nemo" | "thunar") # Open the directory of each item (selection not supported). # shellcheck disable=SC2086 $file_manager $items_open & ;; *) # For other file managers (e.g., "pcmanfm-qt"), open the directory of # each item. local dir="" for item in $items_open; do # Open the directory of the item. dir=$(_get_filename_dir "$item") if [[ -z "$dir" ]]; then continue fi $file_manager "$dir" & done ;; esac } _pkg_get_package_manager() { # This function detects and returns the name of the package manager # available on the system. # # Output: # - Prints the name of the detected package manager. Possible values are: # - "apt": Indicates that "apt-get" (Debian/Ubuntu) is available. # - "dnf": Indicates that "dnf" (Fedora/RHEL) is available. # - "pacman": Indicates that "pacman" (Arch Linux) is available. # - "zypper": Indicates that "zypper" (openSUSE) is available. # - If no supported package manager is found, the output is an empty # string. local pkg_manager="" # Check for an installed package manager. if _command_exists "apt-get"; then pkg_manager="apt" elif _command_exists "dnf"; then pkg_manager="dnf" elif _command_exists "pacman"; then pkg_manager="pacman" elif _command_exists "zypper"; then pkg_manager="zypper" fi printf "%s" "$pkg_manager" } _pkg_install_packages() { # This function installs specified packages using the given package # manager. # # Parameters: # - $1 (pkg_manager): The package manager to use for installation. # Supported values are: # - "apt": For Debian/Ubuntu systems. # - "dnf": For Fedora/RHEL systems. # - "pacman": For Arch Linux systems. # - "zypper": For openSUSE systems. # - $2 (packages): A space-separated list of package names to install. local pkg_manager=$1 local packages=$2 _display_wait_box_message "Installing the packages. Please, wait..." # Install the packages. if ! _command_exists "pkexec"; then _display_error_box \ "Could not run the installer with administrator permission!" _exit_script fi case "$pkg_manager" in "apt") pkexec bash -c \ "apt-get update; apt-get -y install $packages &>/dev/null" ;; "dnf") pkexec bash -c \ "dnf check-update; dnf -y install $packages &>/dev/null" ;; "pacman") pkexec bash -c \ "pacman -Syy; pacman --noconfirm -S $packages &>/dev/null" ;; "zypper") pkexec bash -c \ "zypper refresh; zypper --non-interactive install $packages &>/dev/null" ;; esac _close_wait_box # Check if all packages were installed. packages=$(tr " " "$FIELD_SEPARATOR" <<<"$packages") local package="" for package in $packages; do if ! _pkg_is_package_installed "$pkg_manager" "$package"; then _display_error_box "Could not install the package '$package'!" _exit_script fi done _display_info_box "The packages have been successfully installed!" } _pkg_is_package_installed() { # This function checks if a specific package is installed using the given # package manager. # # Parameters: # - $1 (pkg_manager): The package manager to use for the check. # Supported values are: # - "apt": For Debian/Ubuntu systems. # - "dnf": For Fedora/RHEL systems. # - "pacman": For Arch Linux systems. # - "zypper": For openSUSE systems. # - $2 (package): The name of the package to check. # # Returns: # - "0" (true): If the package is installed. # - "1" (false): If the package is not installed or an error occurs. local pkg_manager=$1 local package=$2 case "$pkg_manager" in "apt") if dpkg -s "$package" &>/dev/null; then return 0 fi ;; "dnf") if dnf repoquery --installed | grep --quiet "$package"; then return 0 fi ;; "pacman") if pacman -Q "$package" &>/dev/null; then return 0 fi ;; "zypper") if zypper search --installed-only "$package" | grep --quiet "^i"; then return 0 fi ;; esac return 1 } _recent_scripts_organize() { # This function organizes the directory containing recently accessed # scripts ('ACCESSED_RECENTLY_DIR'). This function manages symbolic links # in the directory by: # 1. Keeping only the 'ACCESSED_RECENTLY_LINKS_TO_KEEP' most recently # accessed scripts. # 2. Renaming retained links with numeric prefixes (e.g., "01", "02") to # maintain chronological order. local links=() local link="" while IFS= read -r -d $'\0' link; do # Check if the link is broken. if [[ ! -e "$link" ]]; then # Remove broken links. rm -f -- "$link" else links+=("$link") fi done < <( find "$ACCESSED_RECENTLY_DIR" -maxdepth 1 -type l -print0 2>/dev/null | sort --zero-terminated --numeric-sort ) # Process the links, keeping only the 'ACCESSED_RECENTLY_LINKS_TO_KEEP' # most recent. local count=1 local link_name="" for link in "${links[@]}"; do if ((count <= ACCESSED_RECENTLY_LINKS_TO_KEEP)); then # Rename the link with a numeric prefix for ordering. link_name=$(basename "$link" | sed --regexp-extended 's|^.?[0-9]{2} ||') mv -f -- "$link" \ "$ACCESSED_RECENTLY_DIR/$(printf '%02d' "$count") $link_name" \ 2>/dev/null ((count++)) else # Remove excess links. rm -f -- "$link" fi done } _recent_scripts_add() { # This function adds the running script to the history of recently accessed # scripts ('ACCESSED_RECENTLY_DIR'). local running_script=$0 if [[ "$0" != "/"* ]]; then # If $0 is a relative path, resolve it relative to the current working # directory. running_script="$SCRIPT_DIR/"$(basename -- "$0") fi # Resolve symbolic links to their target locations. if [[ -L "$running_script" ]]; then running_script=$(readlink -f "$running_script") fi # Create 'ACCESSED_RECENTLY_DIR' if it does not exist. if [[ ! -d $ACCESSED_RECENTLY_DIR ]]; then mkdir -p "$ACCESSED_RECENTLY_DIR" fi _directory_push "$ACCESSED_RECENTLY_DIR" || return 1 # Remove any existing links pointing to the same script. find "$ACCESSED_RECENTLY_DIR" -lname "$running_script" \ -exec rm -f -- "{}" + # Create a new symbolic link with a ".00" prefix. ln -s -- "$running_script" ".00 $(basename -- "$running_script")" _directory_pop || return 1 } _run_task_parallel() { # This function runs a task in parallel for a set of input files, using a # specified output directory for results. # # Parameters: # - $1 (input_files): A field-separated list of file paths to process. # - $2 (output_dir): The directory where the output files will be stored. local input_files=$1 local output_dir=$2 # Export variables to be used inside new shells (when using 'xargs'). export \ FIELD_SEPARATOR \ GUI_BOX_HEIGHT \ GUI_BOX_WIDTH \ GUI_INFO_WIDTH \ IGNORE_FIND_PATH \ INPUT_FILES \ TEMP_DATA_TASK \ TEMP_DIR_ITEMS_TO_REMOVE \ TEMP_DIR_LOGS \ TEMP_DIR_STORAGE_TEXT \ TEMP_DIR_TASK # Export functions to be used inside new shells (when using 'xargs'). export -f \ _check_output \ _command_exists \ _convert_delimited_string_to_text \ _convert_text_to_delimited_string \ _directory_pop \ _directory_push \ _display_password_box \ _exit_script \ _get_file_encoding \ _get_file_mime \ _get_filename_dir \ _get_filename_extension \ _get_filename_full_path \ _get_filename_next_suffix \ _get_max_procs \ _get_output_filename \ _get_temp_dir_local \ _get_temp_file \ _get_temp_file_dry \ _get_working_directory \ _is_directory_empty \ _is_gui_session \ _log_error \ _main_task \ _move_file \ _move_temp_file_to_output \ _storage_text_write \ _storage_text_write_ln \ _str_remove_empty_tokens \ _strip_filename_extension \ _text_remove_pwd \ _text_uri_decode # Escape single quotes in filenames to handle them correctly in 'xargs' # with 'bash -c'. input_files=$(sed -z "s|'|'\\\''|g" <<<"$input_files") printf "%s" "$input_files" | xargs \ --no-run-if-empty \ --delimiter="$FIELD_SEPARATOR" \ --max-procs="$(_get_max_procs)" \ --replace="{}" \ bash -c "_main_task '{}' '$output_dir'" } _storage_text_clean() { # This function clears all temporary text storage files. rm -f -- "$TEMP_DIR_STORAGE_TEXT/"* &>/dev/null } _storage_text_read_all() { # This function concatenates and outputs the content of all temporary text # storage files. cat -- "$TEMP_DIR_STORAGE_TEXT/"* 2>/dev/null } _storage_text_write() { # This function writes a given input text to temporary text storage files. # # Parameters: # - $1 (input_text): The text to be stored in a temporary file. local input_text=$1 local temp_file="" if [[ -z "$input_text" ]] || [[ "$input_text" == $'\n' ]]; then return fi # Save the text to be compiled into a single file. temp_file=$(mktemp --tmpdir="$TEMP_DIR_STORAGE_TEXT") printf "%s" "$input_text" >"$temp_file" } _storage_text_write_ln() { # This function writes a given input text, followed by a newline character, # to a temporary text storage file. local input_text=$1 if [[ -z "$input_text" ]]; then return fi _storage_text_write "$input_text"$'\n' } _str_human_readable_path() { # This function transforms a given file path into a more human-readable # format. # # Parameters: # - $1 (input_path): The input file path to process. local input_path=$1 local output_path="" output_path=$(_text_remove_pwd "$input_path") output_path=$(_text_remove_home "$output_path") output_path=$(sed "s|^\./||g" <<<"$output_path") if [[ "$output_path" == "." ]]; then output_path="same" elif [[ "$output_path" == "~" ]]; then output_path="home" elif [[ "$output_path" == "" ]]; then output_path="(none)" else output_path="'$output_path'" fi printf "%s" "$output_path" } _str_remove_empty_tokens() { # This function removes empty tokens from a string, ensuring it is compact # and well-formed. # # Parameters: # - $1 (input_str): The input string containing tokens separated by # $FIELD_SEPARATOR. local input_str=$1 input_str=$(tr -s "$FIELD_SEPARATOR" <<<"$input_str") input_str=$(sed "s|$FIELD_SEPARATOR$||" <<<"$input_str") printf "%s" "$input_str" } _strip_filename_extension() { # This function removes the file extension from a given filename, if one # exists. # # Parameters: # - $1 (filename): The filename from which to strip the extension. # # Returns: # - The filename without its extension. local filename=$1 local extension="" extension=$(_get_filename_extension "$filename") if [[ -z "$extension" ]]; then printf "%s" "$filename" return 0 fi local len_extension=${#extension} printf "%s" "${filename::-len_extension}" } _text_remove_empty_lines() { local input_text=$1 grep -v "^\s*$" <<<"$input_text" || true } _text_remove_home() { # This function replaces the user's home directory path in a given string # with the tilde ("~") symbol for brevity. # # Parameters: # - $1 (input_text): The input string that may contain the user's home # directory path. # # Returns: # - The modified string with the home directory replaced by "~", or the # original string if "$HOME" is not defined. # # Examples: # - Input: "/home/user/documents/file.txt" (assuming $HOME is # "/home/user") # - Output: "~/documents/file.txt" # # - Input: "/etc/config" (assuming $HOME is "/home/user") # - Output: "/etc/config" local input_text=$1 if [[ -n "$HOME" ]]; then sed "s|$HOME|~|g" <<<"$input_text" else printf "%s" "$input_text" fi } _text_remove_pwd() { # This function replaces the current working directory path in a given # string with a dot (".") for brevity. # # Parameters: # - $1 (input_text): The input string that may contain the current # working directory path. # # Returns: # - The modified string with the working directory replaced by ".", or # the original string if the working directory is not found. # # Examples: # - Input: "/home/user/project/file.txt" (assuming current directory is # "/home/user/project") # - Output: "./file.txt" # # - Input: "/etc/config" (assuming current directory is # "/home/user/project") # - Output: "/etc/config" local input_text=$1 local working_directory="" working_directory=$(_get_working_directory) if [[ "$working_directory" != "/" ]]; then sed "s|$working_directory|.|" <<<"$input_text" else printf "%s" "$input_text" fi } _text_sort() { # This function sorts the lines of a given text input in a version-aware # manner. # # Parameters: # - $1 (input_text): The input text to be sorted, where each line is # treated as a separate string. # # Returns: # - The sorted text with each line in the correct order. local input_text=$1 sort --version-sort <<<"$input_text" } _text_uri_decode() { # This function decodes a URI-encoded string by converting percent-encoded # characters back to their original form. # # Parameters: # - $1 (uri_encoded): The URI-encoded string that needs to be decoded. # # Returns: # - The decoded URI string, with percent-encoded characters replaced and # the "file://" prefix removed. # # Example: # - Input: "file:///home/user%20name/file%20name.txt" # - Output: "/home/user name/file name.txt" local uri_encoded=$1 uri_encoded=${uri_encoded//%/\\x} uri_encoded=${uri_encoded//file:\/\//} # shellcheck disable=SC2059 printf "$uri_encoded" } _unset_global_variables_file_manager() { # This function unset global variables that may have been set by different # file managers (Caja, Nautilus, Nemo) during script execution. unset \ CAJA_SCRIPT_CURRENT_URI \ CAJA_SCRIPT_NEXT_PANE_CURRENT_URI \ CAJA_SCRIPT_NEXT_PANE_SELECTED_FILE_PATHS \ CAJA_SCRIPT_NEXT_PANE_SELECTED_URIS \ CAJA_SCRIPT_SELECTED_FILE_PATHS \ CAJA_SCRIPT_SELECTED_URIS \ CAJA_SCRIPT_WINDOW_GEOMETRY \ NAUTILUS_SCRIPT_CURRENT_URI \ NAUTILUS_SCRIPT_NEXT_PANE_CURRENT_URI \ NAUTILUS_SCRIPT_NEXT_PANE_SELECTED_FILE_PATHS \ NAUTILUS_SCRIPT_NEXT_PANE_SELECTED_URIS \ NAUTILUS_SCRIPT_SELECTED_FILE_PATHS \ NAUTILUS_SCRIPT_SELECTED_URIS \ NAUTILUS_SCRIPT_WINDOW_GEOMETRY \ NEMO_SCRIPT_CURRENT_URI \ NEMO_SCRIPT_NEXT_PANE_CURRENT_URI \ NEMO_SCRIPT_NEXT_PANE_SELECTED_FILE_PATHS \ NEMO_SCRIPT_NEXT_PANE_SELECTED_URIS \ NEMO_SCRIPT_SELECTED_FILE_PATHS \ NEMO_SCRIPT_SELECTED_URIS \ NEMO_SCRIPT_WINDOW_GEOMETRY } _validate_conflict_filenames() { # This function checks if there are any files with the same base name in # the provided list of input files. # # Parameters: # - $1 (input_files): A string containing a list of file paths, where # each file path can include extensions. This list is checked for # duplicates based on the base file name (excluding extensions). # # Example: # - Input: "file1.txt file2.txt file1.jpg" # - Output: An error box will be displayed indicating that "file1" is # duplicated. local input_files=$1 local filenames="" filenames=$(printf "%s" "$input_files" | tr "$FIELD_SEPARATOR" "\0" | sed -z "s|/\.|//|" | # Ignore hidden files without extension. sed -z --regexp-extended \ "s|(\.tar)?\.[a-z0-9_~-]{0,15}$||I" | # Remove file extensions. sort --zero-terminated --version-sort | # Sort files. uniq --zero-terminated --repeated | # Find duplicate base names. tr "\0" "$FIELD_SEPARATOR") # If duplicates are found, display an error and exit the script. if [[ -n "$filenames" ]]; then local basenames="" # shellcheck disable=SC2086 basenames=$(basename --multiple -- $filenames) _display_error_box \ "There are selected files with the same base name:\n$basenames" _exit_script fi } _validate_file_mime() { # This function validates the MIME type and optionally the encoding of a # given file. It checks whether the file's MIME type matches a specified # pattern (regex) and, if provided, whether the file's encoding matches # another specified pattern. # # Parameters: # - $1 (input_file): The path to the file that is being validated. This # is the file whose MIME type will be checked. # - $2 (par_select_mime): A MIME type pattern (or regular expression) # used to validate the file's MIME type. If this parameter is empty, # MIME type validation is skipped. # - $3 (par_skip_encoding): An optional encoding pattern (or regular # expression) used to validate the file's encoding. If this parameter # is empty, encoding validation is skipped. local input_file=$1 local par_select_mime=$2 local par_skip_encoding=$3 # Validate MIME type if a pattern is provided. if [[ -n "$par_select_mime" ]]; then local file_mime="" file_mime=$(_get_file_mime "$input_file") par_select_mime=${par_select_mime//+/\\+} grep --quiet --ignore-case --perl-regexp \ "($par_select_mime)" <<<"$file_mime" || return fi # Validate encoding if a pattern is provided. if [[ -n "$par_skip_encoding" ]]; then local file_encoding="" file_encoding=$(_get_file_encoding "$input_file") par_skip_encoding=${par_skip_encoding//+/\\+} grep --quiet --ignore-case --perl-regexp \ "($par_skip_encoding)" <<<"$file_encoding" && return fi # Create a temp file containing the name of the valid file. _storage_text_write "$input_file$FIELD_SEPARATOR" } _validate_file_mime_parallel() { # This function validates the MIME types of multiple files in parallel, # based on a specified MIME type pattern. It processes a list of file paths # concurrently using `xargs` and calls the `_validate_file_mime` function # for each file. # # Parameters: # - $1 (input_files): A space-separated string containing the paths of # the files to validate. These files will be checked for the MIME type # pattern. # - $2 (par_select_mime): The MIME type pattern (or regular expression) # used to validate the files' MIME types. If this parameter is empty, # no MIME type validation is performed, and all input files are # returned as valid. # - $3 (par_skip_encoding): An optional encoding pattern (or regular # expression) used to validate the files' encodings. If this # parameter is empty, no encoding validation is performed. # # Example: # - Input: File paths "file1.txt file2.png", MIME pattern "text/plain". # - Output: If "file1.txt" has a MIME type of "text/plain", it will be # included in the output, but "file2.png" will be excluded if its MIME # type doesn't match. local input_files=$1 local par_select_mime=$2 local par_skip_encoding=$3 # Export variables to be used inside new shells (when using 'xargs'). export FIELD_SEPARATOR TEMP_DIR_STORAGE_TEXT # Export functions to be used inside new shells (when using 'xargs'). export -f \ _get_file_encoding \ _get_file_mime \ _storage_text_write \ _validate_file_mime # Escape single quotes in filenames to handle them correctly in 'xargs' # with 'bash -c'. input_files=$(sed -z "s|'|'\\\''|g" <<<"$input_files") # Execute the function '_validate_file_mime' for each file in parallel # (using 'xargs'). printf "%s" "$input_files" | xargs \ --no-run-if-empty \ --delimiter="$FIELD_SEPARATOR" \ --max-procs="$(_get_max_procs)" \ --replace="{}" \ bash -c \ "_validate_file_mime '{}' '$par_select_mime' '$par_skip_encoding'" # Compile valid files in a single list. input_files=$(_storage_text_read_all) _storage_text_clean input_files=$(_str_remove_empty_tokens "$input_files") printf "%s" "$input_files" } _validate_files_count() { # This function validates the number of selected files or directories based # on several criteria, such as type, extension, MIME type, and minimum or # maximum item count. # # Parameters: # - $1 (input_files): A space-separated string containing the paths of # files or directories to be validated. # - $2 (par_type): A string indicating the type of items to validate. # Possible values: # - "file": Validate files only. # - "directory": Validate directories only. # - "all": Validate both files and directories. # - $3 (par_select_extension): A pipe-separated list of file extensions # to filter the files. Files must have one of these extensions. # - $4 (par_select_mime): A string representing MIME types to filter the # files by. Only files with matching MIME types are selected. # - $5 (par_min_items): The minimum number of valid items required. If # fewer valid items are selected, an error is displayed. # - $6 (par_max_items): The maximum number of valid items allowed. If # more valid items are selected, an error is displayed. # - $7 (par_recursive): A string indicating whether the validation should # be recursive. If "true", directories will be searched recursively. # # Example: # - Input: "dir1 dir2", "file", "txt|pdf", "", 1, 5, "true" # - Output: The function checks if the directories "dir1" and "dir2" # contain at least 1 and no more than 5 ".txt" or ".pdf" files, # recursively. local input_files=$1 local par_type=$2 local par_select_extension=$3 local par_select_mime=$4 local par_min_items=$5 local par_max_items=$6 local par_recursive=$7 # Define a label for a valid file. local valid_file_label="valid files" if [[ "$par_type" == "directory" ]]; then valid_file_label="directories" elif [[ -n "$par_select_mime" ]]; then valid_file_label="$par_select_mime" valid_file_label=$(sed "s|\|| or |g" <<<"$par_select_mime") valid_file_label=$(sed "s|/$||g; s|/ | |g" <<<"$valid_file_label") valid_file_label+=" files" elif [[ "$par_type" == "file" ]]; then valid_file_label="files" elif [[ "$par_type" == "all" ]]; then valid_file_label="files or directories" fi # Count the number of valid files. local valid_items_count=0 valid_items_count=$(_get_items_count "$input_files") # Define a label for the extension. local extension_label="" if [[ -n "$par_select_extension" ]]; then extension_label="'.${par_select_extension//|/\' or \'.}'" fi # Check if there is at least one valid file. if ((valid_items_count == 0)); then if [[ "$par_recursive" == "true" ]]; then if [[ -n "$par_select_extension" ]]; then _display_error_box \ "No files with extension: $extension_label were selected!" else _display_error_box "No $valid_file_label were selected!" fi else if [[ -n "$par_select_extension" ]]; then _display_error_box \ "You must select files with extension: $extension_label!" else _display_error_box "You must select $valid_file_label!" fi fi _exit_script fi if [[ -n "$par_min_items" ]] && ((valid_items_count < par_min_items)); then _display_error_box \ "You must select at least $par_min_items $valid_file_label!" _exit_script fi if [[ -n "$par_max_items" ]] && ((valid_items_count > par_max_items)); then _display_error_box \ "You must select up to $par_max_items $valid_file_label!" _exit_script fi } _xdg_get_default_app() { # This function retrieves the default application associated with a # specific MIME type on a Linux system using the "xdg-mime" command. # # Parameters: # - $1 (mime): The MIME type (e.g., "application/pdf", "image/png") for # which to find the default application. # # Example: # - Input: "application/pdf" # - Output: The function prints the default application's executable for # opening PDF files (e.g., "evince" or "okular"). local mime=$1 local desktop_file="" local default_app="" desktop_file=$(xdg-mime query default "$mime" 2>/dev/null) default_app=$(grep -m1 "^Exec" "/usr/share/applications/$desktop_file" | sed "s|Exec=||g" | cut -d " " -f 1) if [[ -z "$default_app" ]]; then _display_error_box \ "Could not find the default application to open '$mime' files!" _exit_script fi printf "%s" "$default_app" } # Execute functions to organize the recently accessed scripts. _recent_scripts_add _recent_scripts_organize