#!/bin/bash ## rename-ext # > changes file-extension based on mime-type ## also # - removes `*?|^:"<>` characters from the file-name # - reducing multiple spaces, dots, and underscores # - adds rename-date suffix to ensure uniquness of the filename # - strips exec-flag from non-executive types (usual leftover from FAT/NTFS) # - optimizes images while at it (shows reduction size in `kb` and `%`) ## example # rename-ext /Data/Pictures/_unsorted/**/* ## requires # - `cargo install sd` – better sed # - pngcrush – to compress PNGs (losless) # // - pngquant – to compress PNGs (lossy) # - jpegoptim – to compress JPEGs ALL_EXTS=`echo \ 'gif|jpe?g|jp2|jfif|a?png|web(p|m)|svg |eps|tga|tiff?|psd|ico|xcf|heic |eot|otf|ttf|epub|doc|docx|xls|swf|pdf|odt |flac|opus|ogg|m4a|wav |mpe?g|mp\d|mov|mkv|avif?|asf|3gpp? |html?|sh|py|php ' \ | tr -d '[:space:]' \ ` mkdir -p ~/tmp OPTIONS='' CHX_CHANGELOG='' NR='1' calc_reduction() { SIZE_PRE="$1" SIZE_POST="$2" PERECENT=` echo "scale=2; 100 - (100 * $SIZE_POST / $SIZE_PRE)" | bc ` RESULT="$(( $SIZE_PRE / 1000 ))-$(( ( $SIZE_PRE - $SIZE_POST ) / 1000 )) kb \ ($PERECENT%) " echo "$RESULT" } ch_x() { FILENAME="$1" # echo "FILENAME = '$FILENAME'" CHX_CHANGELOG='' IS_X_INTENDED='0' if [ ! -z "$2" ]; then IS_X_INTENDED='1' fi # echo "IS_X_INTENDED = '$IS_X_INTENDED'" IS_X=`stat -c '%A' "$FILENAME" | grep 'x' | wc -l` # echo "IS_X = '$IS_X'" MODE='-x' if [[ '1' == "$IS_X_INTENDED" ]]; then MODE='+x' fi # echo "MODE = '$MODE'" if [[ ! "$OPTIONS" =~ 'test-run' ]]; then # echo '(not a test-run)' if [[ ! "$OPTIONS" =~ 'only-ext' ]]; then # echo '(not only-ext)' if [[ ! "$IS_X" == "$IS_X_INTENDED" ]]; then CHX_CHANGELOG="chmod $MODE" fi chmod "$MODE" "$FILENAME" fi fi } rename_ext() { NAME="$1" EXT="$2" NOTE='' TARGET='' ch_x "$FILENAME" if [[ "$OPTIONS" =~ 'only-ext' ]]; then NAME_NO_EXT=` echo "$NAME" \ | sd -f i "(\.+($ALL_EXTS))$" '' \ ` TARGET="$NAME_NO_EXT.$EXT" else if [[ ! "$OPTIONS" =~ 'test-run' ]]; then SIZE_PRE=` stat "$FILENAME" -c '%s' ` [ 'jpg' = "$EXT" ] \ && jpegoptim "$FILENAME" -qsftp if [ 'png' = "$EXT" ]; then mogrify "$FILENAME" pngcrush -nolimits -s "$FILENAME" "$FILENAME.tmp.png" # pngquant "$FILENAME" --output "$FILENAME" \ # --quality=100 --force --skip-if-larger --speed 1 if [ -f "$FILENAME.tmp.png" ] \ && [ -s "$FILENAME.tmp.png" ] then mv "$FILENAME.tmp.png" "$FILENAME" else rm "$FILENAME.tmp.png" fi fi SIZE_POST=` stat "$FILENAME" -c '%s' ` [ $SIZE_PRE -gt $SIZE_POST ] \ && NOTE=` calc_reduction "$SIZE_PRE" "$SIZE_POST" ` fi DATE=` date -r "$NAME" -u "+%Y%m%d%H%M%S" ` TCLEAN=` echo "$NAME" \ | sd '[\\\?\*\|^:"<>]+' '_' \ | sd '_\d{12,}\.' '.' \ | sd -f i "(\.+($ALL_EXTS))+$" '' \ ` TARGET=` echo "${TCLEAN}_$DATE.$EXT" \ | sd '([\s\.,_]){3,}' '$1' \ ` fi IS_SAME_NAME='0' if [[ "$NAME" == "$TARGET" ]]; then IS_SAME_NAME='1' fi # echo "IS_SAME_NAME = '$IS_SAME_NAME'" # echo "CHX_CHANGELOG = '$CHX_CHANGELOG'" if [[ '1' == "$IS_SAME_NAME" ]] \ && [ -z "$CHX_CHANGELOG" ] then return fi echo "$NR:" NR="$(($NR+1))" echo " $FILENAME" if [ ! -z "$CHX_CHANGELOG" ]; then echo " $CHX_CHANGELOG" CHX_CHANGELOG='' fi if [[ '1' == "$IS_SAME_NAME" ]]; then return fi if [[ "$OPTIONS" =~ 'test-run' ]]; then if [ -f "$TARGET" ]; then if [[ "$PARAMS" =~ 'force-replace' ]]; then echo " >> replacing existing file >>" else echo " !! file already exist !!" fi fi echo "$TARGET" else if [ ! -f "$TARGET" ]; then echo " >> $NOTE >>" echo " $TARGET" mv "$NAME" "$TARGET" else if [[ "$PARAMS" =~ 'force-replace' ]]; then echo " >> $NOTE ## replacing existing file >>" echo " $TARGET" mv "$NAME" "$TARGET" else echo ' !! File already exist, use `--force-replace` to overwrite !! ' echo " $TARGET" fi fi fi } for PARAM in "$@"; do # echo "PARAM = '$PARAM'" if [[ '--only-ext' == "$PARAM" ]]; then OPTIONS="$OPTIONS;only-ext" # echo "OPTIONS = '$OPTIONS'" elif [[ '--test-run' == "$PARAM" ]]; then OPTIONS="$OPTIONS;test-run" # echo "OPTIONS = '$OPTIONS'" elif [[ '--force-replace' == "$PARAM" ]]; then OPTIONS="$OPTIONS;force-replace" # echo "OPTIONS = '$OPTIONS'" fi done [ ! -z "$OPTIONS" ] \ && echo "OPTIONS: '$OPTIONS'" # echo "@ = '$@'" for FILENAME in "$@"; do # echo "FILENAME = '$FILENAME'" # NOT_F_=`[ ! -f "$FILENAME" ]` # echo "NOT_F_ = '$NOT_F_'" # [ ! -f "$FILENAME" ] \ # && continue FILE_TYPE=` file -b --mime-type "$FILENAME" ` # echo "FILE_TYPE = '$FILE_TYPE'" case "$FILE_TYPE" in # pixel-based 'image/gif') rename_ext "$FILENAME" 'gif' ;; 'image/jpeg') rename_ext "$FILENAME" 'jpg' ;; 'image/jp2') rename_ext "$FILENAME" 'jp2' ;; 'image/png') rename_ext "$FILENAME" 'png' ;; 'image/webp') rename_ext "$FILENAME" 'webp' ;; # vector-based 'image/svg+xml') rename_ext "$FILENAME" 'svg' ;; 'image/x-eps') rename_ext "$FILENAME" 'eps' ;; # some less common image formats 'image/heic') rename_ext "$FILENAME" 'heic' ;; 'image/tiff') rename_ext "$FILENAME" 'tiff' ;; 'image/vnd.adobe.photoshop') rename_ext "$FILENAME" 'psd' ;; 'image/vnd.microsoft.icon') rename_ext "$FILENAME" 'ico' ;; 'image/x-tga') rename_ext "$FILENAME" 'tga' ;; 'image/x-xcf') rename_ext "$FILENAME" 'xcf' ;; # fonts 'application/vnd.ms-fontobject') rename_ext "$FILENAME" 'eot' ;; 'application/vnd.ms-opentype') rename_ext "$FILENAME" 'otf' ;; 'font/sfnt') rename_ext "$FILENAME" 'ttf' ;; # documents 'application/epub+zip') rename_ext "$FILENAME" 'epub' ;; 'application/msword') rename_ext "$FILENAME" 'doc' ;; 'application/pdf') ch_x "$FILENAME" ;; # can be .ai 'application/vnd.ms-excel') rename_ext "$FILENAME" 'xls' ;; 'application/vnd.oasis.opendocument.text') rename_ext "$FILENAME" 'odt' ;; 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') rename_ext "$FILENAME" 'docx' ;; 'application/x-shockwave-flash') rename_ext "$FILENAME" 'swf' ;; # audio 'audio/flac') rename_ext "$FILENAME" 'flac' ;; 'audio/mpeg') rename_ext "$FILENAME" 'mp3' ;; 'audio/ogg') rename_ext "$FILENAME" 'ogg' ;; 'audio/x-m4a') rename_ext "$FILENAME" 'm4a' ;; 'audio/x-wav') rename_ext "$FILENAME" 'wav' ;; # video 'video/3gpp') rename_ext "$FILENAME" '3gp' ;; 'video/av1') rename_ext "$FILENAME" 'avi' ;; 'video/MP2T') rename_ext "$FILENAME" 'mp2' ;; 'video/mp4') rename_ext "$FILENAME" 'mp4' ;; 'video/quicktime') rename_ext "$FILENAME" 'mov' ;; 'video/webm') rename_ext "$FILENAME" 'webm' ;; 'video/x-m4v') rename_ext "$FILENAME" 'mp4' ;; 'video/x-matroska') rename_ext "$FILENAME" 'mkv' ;; 'video/x-ms-asf') rename_ext "$FILENAME" 'asf' ;; # text 'application/json') ch_x "$FILENAME" ;; # can be any other language 'text/html') ch_x "$FILENAME" ;; # can be .htm, .htc, .mht, ... 'text/xml') ch_x "$FILENAME" ;; # can be .xml, .opml, .rss, ... 'text/plain') ;; # can be any file with not enough text 'text/x-python') ;; # can be .py, .py3, have no .ext 'text/x-shellscript') ;; # can be .sh, .zsh, have no .ext # binady data 'application/x-sqlite3') ch_x "$FILENAME" ;; # can be .db, .sql, .sq3 # special 'application/octet-stream') ;; # can be anything 'inode/directory') # echo "$FILENAME is a folder" # echo "about to ch_x" ch_x "$FILENAME" '+' ;; # a folder 'inode/x-empty') ch_x "$FILENAME" ;; # zero-size # default *) echo "?? $FILENAME : $FILE_TYPE" ;; esac done # if [[ "@#" -eq 1 ]]; then # if [[ `file -b --mime-type "$1"` == "inode/directory" ]]; then # find "$1" # fi # fi