Pastebin launched a little side project called VERYVIRAL.com, check it out ;-) Want more features on Pastebin? Sign Up, it's FREE!

optimg

By: h3xx on Jul 19th, 2011  |  syntax: Bash  |  size: 20.01 KB  |  views: 372  |  expires: Never
download  |  raw  |  embed  |  report abuse  |  print
Text below is selected. Please press Ctrl+C to copy to your clipboard. (⌘+C on Mac)
  1. #!/bin/sh
  2.  
  3. # optimize image files for size
  4.  
  5. # CHANGES:
  6. #
  7. # 2011-1211-1210 :
  8. #       * Add `-C' option to not reduce the color type. I've encountered some
  9. #         programs that will not read images with a color palette (namely
  10. #         stella and sometimes even gimp). This method is preferable to using
  11. #         the PREFER_PNGCRUSH method.
  12. #
  13. # 2011-1124-1225 :
  14. #       * Added support for using pngcrush over optipng(1) via the environment
  15. #         variable `PREFER_PNGCRUSH'. pngcrush may not work as well, but it
  16. #         still has its merits as it doesn't insist on re-factoring the color
  17. #         map, a process which can cause errors in some programs.
  18. #
  19. # 2011-0814-1808 :
  20. #       * Added support for omitting the preservation of timestamps via the
  21. #         `-T' option.
  22. #       * Fixed bug in reporting section that was causing individual filenames
  23. #         not to be printed.
  24. #       * Errs out if it can't preserve EXIF information. This is because the
  25. #         user specifically requests to save this information, and if we can't
  26. #         do it, the user would most likely be pissed off we optimized the
  27. #         image but dumped the EXIF.
  28. #
  29. # 2011-0802-2333 :
  30. #       * Commented out MNG support, as advmng(1) seems to kill the image's
  31. #         usability.
  32. #
  33. # 2011-0802-2308 :
  34. #       * [REDACTED] Now supports optimizing MNG files via advmng(1).
  35. #
  36. # 2011-0725-2201 :
  37. #       * Tests for the existence of jhead(1), jpegtran(1) and gifsicle(1) as
  38. #         they aren't necessarily commonly-distributed programs. jpegtran(1)
  39. #         has the highest probability of being installed as it is part of the
  40. #         libjpeg package.
  41. #       * Specifying `-r' will now recursively process ONLY directories and
  42. #         not specifying it will process ONLY files. Perhaps one day, that
  43. #         piped file list I mention in BUGS will allow both, but for now
  44. #         recursive and non-recursive are mutually exclusive.
  45. #       * Fixed bugs relating to recursing directories starting with a hyphen.
  46. #       * More efficient file size counting in file_size() via better stat(1)
  47. #         arguments.
  48. #       * Fixed text formatting of the help message.
  49. #       * Hardier cpu core count detection for if the kernel hackers ever
  50. #         change the format of /proc/cpuinfo. This also fixes a potentially
  51. #         dangerous bug related to piping a big list of files to xargs(1) when
  52. #         `--max-procs=0', which will cause xargs to process ALL the jobs at
  53. #         once.
  54. #       * Short-circuited the parallel processing operation if the number of
  55. #         jobs/CPU cores is equal to 1.
  56. #       * Recursive operation actually works well now.
  57. #       * More GNU-style messages.
  58. #
  59. # 2011-0718-2328 :
  60. #       * More and better documentation. Now all the functions have header
  61. #         documentation.
  62. #       * Longer options to incidental program calls.
  63. #       * Numerous warnings about recursion pitfalls and inherent bugs.
  64. #       * Bug workaround:
  65. #               pngcrush(1), jpegtran(1) and jhead(1) have no way of accepting
  66. #               file arguments starting with a dash (`-'). This is not my
  67. #               fault.
  68. #         Now the filename is passed through readlink(1) so that it only
  69. #         processes absolute paths, which [should] never start with a hyphen.
  70. #       * Workarounds for a few minor security holes.
  71. #       * Outputs human-readable sizes.
  72. #       * Short-circuited the file_size() function so it's not doing shell math
  73. #         where it doesn't need to.
  74. #       * Recursive operation via find(1) (recursive_selfcall()) now self-calls
  75. #         with `-q' if it was specified when starting the script.
  76. #
  77. # 2010-0715-1300 :
  78. #       * Now counts freed bytes via stat(1).
  79. #       * Included -q option for quiet operation.
  80. #       * Included -r and -x EXTENSION option for recursive scheduling.
  81. #         Recursive scheduling now supercedes normal scheduling.
  82. #       * Removed -s option for single files (previously used when scheduling);
  83. #         now relies on value of `$#' to determine whether more than one file
  84. #         was indicated on the command line.
  85. #
  86. # 2010-0321-2044 :
  87. #       * Now uses parallel processes via xargs(1).
  88. #
  89. # 2010-0125-2015 :
  90. #       * Fixed some bugs/modularity issues regarding local variables in
  91. #         functions.
  92. #       * Included support for [more advanced] optipng(1). It is now preferred
  93. #         over pngcrush(1) as it seems to do a better job.
  94. #
  95. # BUGS:
  96. #       * Doesn't do that great a job of counting freed bytes on larger
  97. #         operations using the `-r' (recursive) flag. This is due to find(1)
  98. #         sending only-so-many file names to the command when using the `-exec
  99. #         [COMMAND] [ARGS] {} +' syntax. Perhaps implementing a piped file
  100. #         list would solve it.
  101. #       * hr_size() function does not round its figures; it only truncates
  102. #         them. This is due to its only using the built-in shell math
  103. #         functionality, which is only capable of integer math.
  104. #
  105.  
  106. HELP_MESSAGE() {
  107.         local EXIT_CODE="${1:-0}"
  108.         cat <<EOF
  109. Usage: $(basename -- "$0") [OPTIONS] [--] FILE...
  110.        $(basename -- "$0") [OPTIONS] -r [--] [DIR]...
  111. Optimize image files for size.
  112.  
  113.   -h            Show this help message.
  114.   -e            Preserve Exif metadata in JPEG files using jhead(1).
  115.   -j NUM        Run at maximum NUM simultaneous operations. This defaults to
  116.                   the auto-detected number of processors/virtual cores in the
  117.                   system.
  118.   -q            Be quiet. Produce only error messages on STDERR.
  119.   -r            Optimize all images found by recursing DIRs. File arguments
  120.                   are ignored.
  121.   -x SUF        When optimizing recursively via the \`-r' flag, only operate
  122.                   on files whose names match /.*SUF/. May be specified
  123.                   multiple times. Also accepts POSIX-compatible regular
  124.                   expressions. If not specified, recursive operations will
  125.                   operate on all files.
  126.   -T            Do not preserve timestamp information on optimized images.
  127.                   Default is to preserve as much as possible.
  128.   -C            Be careful when optimizing PNGs. Sometimes reducing a PNG's
  129.                   color map can cause it to be displayed incorrectly. Most
  130.                   programs can handle it, however.
  131.   --            Terminate options list.
  132.  
  133. Currently, this script will only work on the following types of files:
  134.   * JPEG (\`image/jpeg')                         Using jpegtran(1)
  135.   * Portable Network Graphics (\`image/png')     Using optipng(1)/pngcrush
  136.   * Mult-image Network Graphics (\`video/x-mng') Using advmng(1) [not working]
  137.   * Graphics Interchange Format (\`image/gif')   Using gifsicle(1)
  138.  
  139.   If the environment variable PREFER_PNGCRUSH is set to a non-empty value, then
  140.   pngcrush is preferred over optipng(1).
  141.  
  142. Copyright (C) 2010-2011 Dan Church.
  143. License GPLv3+: GNU GPL version 3 or later (http://gnu.org/licenses/gpl.html).
  144. This is free software: you are free to change and redistribute it. There is NO
  145. WARRANTY, to the extent permitted by law.
  146. EOF
  147.         exit "$EXIT_CODE"
  148. }
  149.  
  150. KEEP_EXIF=0
  151. # determine number of jobs from number of CPU cores in the system
  152. NUM_JOBS="$(
  153.         cpudetect=0
  154.         if [ -r '/proc/cpuinfo' ]; then
  155.                 cpudetect="$(
  156.                         grep -c '^processor' '/proc/cpuinfo'
  157.                 )"
  158.         fi
  159.         if [ "$cpudetect" -gt 0 ]; then
  160.                 echo "$cpudetect"
  161.         else
  162.                 echo 1
  163.         fi
  164. )"
  165. QUIET=0
  166. RECURSIVE=0
  167. EXTS=()
  168. PRESERVE_TIMESTAMP=1
  169. CAREFUL=0
  170.  
  171. # XXX : Note to self: when adding options, make sure to update
  172. #       recursive_selfcall() and schedule_process()
  173. while getopts 'hej:qrx:TC-' flag; do
  174.         case "$flag" in
  175.                 'e')
  176.                         KEEP_EXIF=1
  177.                         ;;
  178.                 'j')
  179.                         NUM_JOBS="$OPTARG"
  180.                         ;;
  181.                 'q')
  182.                         QUIET=1
  183.                         ;;
  184.                 'r')
  185.                         RECURSIVE=1
  186.                         ;;
  187.                 'x')
  188.                         EXTS+=("$OPTARG")
  189.                         ;;
  190.                 'T')
  191.                         PRESERVE_TIMESTAMP=0
  192.                         ;;
  193.                 'C')
  194.                         CAREFUL=1
  195.                         ;;
  196.                 'h')
  197.                         HELP_MESSAGE 0
  198.                         ;;
  199.                 *)
  200.                         HELP_MESSAGE 1
  201.                         ;;
  202.         esac
  203. done
  204.  
  205. shift "$((OPTIND-1))"
  206.  
  207. TEMP_FILES=()
  208.  
  209. cleanup() {
  210.         rm -f -- "${TEMP_FILES[@]}"
  211. }
  212.  
  213. trap 'cleanup'  EXIT
  214.  
  215. # prints out the sum of the sizes, in bytes of all files passed to it
  216. file_size() {
  217.         if [ "$#" -gt 1 ]; then
  218.                 # adds all the file sizes by doing a stat on all of them
  219.                 # (separated by `\n'), transforming them into
  220.                 # `LINE+LINE+...LINE+0' and passing them to the Bash expression
  221.                 # evaluator
  222.                 echo "$(($(
  223.                         stat \
  224.                                 --printf='%s+' \
  225.                                 --dereference \
  226.                                 -- \
  227.                                 "$@" 2>/dev/null
  228.                 )0))"
  229.         else
  230.                 # only got one file to check; do it simply
  231.                 stat \
  232.                         --format='%s' \
  233.                         --dereference \
  234.                         -- \
  235.                         "$@" 2>/dev/null
  236.         fi
  237. }
  238.  
  239. # produces a human-readable size from the byte count passed to it
  240. hr_size() {
  241.         local \
  242.                 bytes="$1" \
  243.                 units \
  244.                 fact \
  245.                 thresh \
  246.                 decimals \
  247.                 exp \
  248.                 hr_val \
  249.                 hr_unit \
  250.  
  251.         #units=(B KB MB GB TB PB EB ZB YB) # shell math can only go so far...
  252.         units=(B KB MB GB TB)
  253.         fact=1024
  254.         thresh=9/10
  255.         decimals=1
  256.  
  257.         # cycle through units from largest to smallest, exiting when it finds
  258.         # the largest applicable unit
  259.         for ((exp = ${#units[@]} - 1; exp > -1; --exp)); do
  260.                 # check if the unit is close enough to the unit's size, within
  261.                 # the threshold
  262.                 if [ "$bytes" -gt "$((fact**exp*${thresh}))" ]; then
  263.                         # we found the applicable unit
  264.  
  265.                         # must multiply by a factor of 10 here to not truncate
  266.                         # the given number of decimal places after the point
  267.                         hr_val="$((bytes*10**decimals/fact**exp))"
  268.  
  269.                         # put the decimal point in
  270.                         if [ "$decimals" -gt 0 ]; then
  271.                                 # left: divide and truncate
  272.                                 # right: modulus
  273.                                 hr_val="$((hr_val/10**decimals)).$((hr_val%10**decimals))"
  274.                         fi
  275.  
  276.                         hr_unit="${units[$exp]}"
  277.                         break
  278.                 fi
  279.         done
  280.  
  281.         if [ -z "$hr_unit" ]; then
  282.                 hr_val="$bytes"
  283.                 hr_unit="${units[0]}"
  284.         fi
  285.  
  286.         echo "${hr_val} ${hr_unit}"
  287. }
  288.  
  289. # prints out the simple mimetype (e.g. `image/jpeg') of a file's contents
  290. get_mimetype() {
  291.         # XXX : file(1) doesn't always produce the correct mimetype for files;
  292.         #       possible workaround would be to include
  293.         #       `--magic-file /etc/file/magic/images'
  294.         file \
  295.                 --preserve-date \
  296.                 --dereference \
  297.                 --brief \
  298.                 --mime-type \
  299.                 -- \
  300.                 "$@" 2>/dev/null
  301. }
  302.  
  303. # copies $2 over to $1 if $2 is smaller than $1
  304. use_smaller() {
  305.         # if `$temp' isn't empty and it's of a smaller size than `$file',
  306.         # preserve every attribute and replace `$file' with `$temp'
  307.         local \
  308.                 file="$1" \
  309.                 temp="$2" \
  310.                 origsize \
  311.                 tempsize
  312.        
  313.         origsize="$(file_size "$file")"
  314.         tempsize="$(file_size "$temp")"
  315.  
  316.         if [ -f "$temp" -a \
  317.                 "$tempsize" -gt 0 -a \
  318.                 "$tempsize" -lt "$origsize" ]; then
  319.  
  320.                 # preserve attributes by copying them from the original file to
  321.                 # the temporary one
  322.                 chmod \
  323.                         --reference="$file" \
  324.                         -- \
  325.                         "$temp" &&
  326.  
  327.                 (if [ "$PRESERVE_TIMESTAMP" -ne 0 ]; then
  328.                         touch \
  329.                                 --reference="$file" \
  330.                                 -- \
  331.                                 "$temp" ||
  332.                                 exit "$?"
  333.                 fi) &&
  334.  
  335.                 (if [ "$UID" -eq 0 ]; then
  336.                         # we're root, so we can chown(1) things
  337.                         chown \
  338.                                 --reference="$file" \
  339.                                 -- \
  340.                                 "$temp" ||
  341.  
  342.                                 exit "$?"
  343.                 fi) &&
  344.  
  345.                 cp \
  346.                         --preserve=mode,ownership,timestamps \
  347.                         -- \
  348.                         "$temp" \
  349.                         "$file" &&
  350.  
  351.                 local err="$?"
  352.                 case "$err" in
  353.                         '0')
  354.                                 echo "optimized image \`$file'" >&2
  355.                                 ;;
  356.                         *)
  357.                                 echo "failed to optimize \`$file'!" >&2
  358.                                 ;;
  359.                 esac
  360.  
  361.         fi
  362.  
  363.         # protect against unsuccessful following file writes to our temp file
  364.         rm \
  365.                 --force \
  366.                 -- \
  367.                 "$temp"
  368.  
  369. }
  370.  
  371. # optimize a single[*] image file--any image file it can
  372. #
  373. # *: can handle multiple files
  374. optimize_image() {
  375.         local \
  376.                 image_file \
  377.                 temp
  378.  
  379.         for image_file; do
  380.  
  381.                 [ -f "$image_file" ] || continue
  382.  
  383.                 # get the absolute path of the image file so that ALL filenames
  384.                 # can be handled, even ones beginning with a hyphen
  385.                 image_file="$(readlink -f -- "$image_file")"
  386.  
  387.                 temp="$(mktemp -t "$(basename -- "$0").XXXXXX")"
  388.                 TEMP_FILES+=("$temp")
  389.  
  390.                 case "$(get_mimetype "$image_file")" in
  391.  
  392. 'image/jpeg')
  393.  
  394.         # XXX : jpegtran and jhead can't handle input files starting with `-'
  395.         (if [ -n "$(type -Pt 'jpegtran')" ]; then
  396.                 jpegtran \
  397.                         -optimize \
  398.                         -outfile "$temp" \
  399.                         "$image_file" >&2 ||
  400.  
  401.                         exit "$?"
  402.         else
  403.                 echo 'No supported JPEG optimizer found.' >&2
  404.                 exit 1
  405.         fi) &&
  406.  
  407.         (if [ "$KEEP_EXIF" -ne 0 ]; then
  408.                 if [ -n "$(type -Pt 'jhead')" ]; then
  409.                         # copy EXIF information from original file to the
  410.                         # temporary file
  411.                         # note: there is no long option for `-te'
  412.                         jhead \
  413.                                 -te "$image_file" \
  414.                                 "$temp" >&2 ||
  415.  
  416.                                 exit "$?"
  417.                 else
  418.                         echo 'Error: Cannot preserve EXIF information.' >&2
  419.                         # the user specifically requested to save EXIF
  420.                         # information, so if we can't do it, might as well
  421.                         # give an error
  422.                         exit 1
  423.                 fi
  424.         fi) &&
  425.  
  426.         use_smaller "$image_file" "$temp"
  427.  
  428.         ;;
  429.  
  430. 'image/png')
  431.  
  432.         # truth table:
  433.         # prefpc| has_pngcrush  | has_optipng   | method
  434.         # 0     | 0             | 0             | exit
  435.         # 0     | 0             | 1             | optipng
  436.         # 0     | 1             | 0             | pngcrush
  437.         # 0     | 1             | 1             | optipng
  438.         # 1     | 0             | 0             | exit
  439.         # 1     | 0             | 1             | optipng
  440.         # 1     | 1             | 0             | pngcrush
  441.         # 1     | 1             | 1             | pngcrush
  442.         #
  443.         # optipng = (!prefpc -o !has_pngcrush) && has_optipng
  444.         (if [ \( -z "$PREFER_PNGCRUSH" -o -z "$(type -Pt 'pngcrush')" \) -a -n "$(type -Pt 'optipng')" ]; then
  445.  
  446.                 # XXX : optipng refuses to overwrite files (even though the man
  447.                 #       page says `-force' overrides this)
  448.                 rm -f -- "$temp" &&
  449.  
  450.                 optipng \
  451.                         $([ "$QUIET" -eq 0 ] || echo ' -quiet') \
  452.                         $([ "$CAREFUL" -eq 0 ] || echo ' -nc') \
  453.                         -o7 \
  454.                         -fix \
  455.                         -force \
  456.                         -out "$temp" \
  457.                         -- \
  458.                         "$image_file" >&2 ||
  459.  
  460.                         exit "$?"
  461.  
  462.         elif [ -n "$(type -Pt 'pngcrush')" ]; then
  463.  
  464.                 # XXX : pngcrush can't handle input files starting with `-'
  465.                 pngcrush \
  466.                         $([ "$QUIET" -eq 0 ] || echo '-q') \
  467.                         -brute \
  468.                         "$image_file" \
  469.                         "$temp" >&2 ||
  470.  
  471.                         exit "$?"
  472.  
  473.         else
  474.                 echo 'No supported PNG optimizer found.' >&2
  475.                 exit 1
  476.         fi) &&
  477.  
  478.         use_smaller "$image_file" "$temp"
  479.  
  480.         ;;
  481.  
  482. 'video/x-mng')
  483.  
  484.         #(if [ -n "$(type -Pt 'advmng')" ]; then
  485.         #       # advmng optimizes files in-place, but we want to be more
  486.         #       # careful than that
  487.         #       cp -- "$image_file" "$temp" &&
  488.         #
  489.         #       advmng \
  490.         #               --recompress \
  491.         #               --shrink-fast \
  492.         #               --force \
  493.         #               $([ "$QUIET" -eq 0 ] || echo '--verbose') \
  494.         #               -- \
  495.         #               "$temp" ||
  496.         #
  497.         #               exit "$?"
  498.         #
  499.         #       # XXX : sometimes advmng fucks up the MNG so work around
  500.         #       #       it by having it re-check its work
  501.         #       advmng \
  502.         #               --list \
  503.         #               -- \
  504.         #               "$temp" ||
  505.         #
  506.         #               exit "$?"
  507.         #
  508.         #else
  509.         #       echo 'No supported MNG optimizer found' >&2
  510.         #       exit 1
  511.         #fi) &&
  512.         #
  513.         #use_smaller "$image_file" "$temp"
  514.  
  515.         # FIXME : advmng is really, REALLY old and is kind of shit at the whole
  516.         #         not-fucking-up-your-images thing
  517.         echo 'Sorry, support for MNG is not yet implemented.' >&2
  518.  
  519.         ;;
  520.  
  521. 'image/gif')
  522.  
  523.         (if [ -n "$(type -Pt 'gifsicle')" ]; then
  524.                 # XXX : this opens up a [very minor] symlink attack vector: if
  525.                 #       "$temp" is replaced with a symlink during execution,
  526.                 #       its target will be clobbered with the output from
  527.                 #       gifsicle(1)
  528.                 rm --force -- "$temp" &&
  529.  
  530.                 gifsicle \
  531.                         --optimize=3 \
  532.                         --output "$temp" \
  533.                         "$image_file" >&2 ||
  534.  
  535.                         exit "$?"
  536.         else
  537.                 echo 'No supported GIF optimizer found.' >&2
  538.                 exit 1
  539.         fi) &&
  540.  
  541.         use_smaller "$image_file" "$temp"
  542.  
  543.         ;;
  544.  
  545. *)
  546.  
  547.         echo "Image type of \`$image_file' not recognized." >&2
  548.         ;;
  549.  
  550.                 esac
  551.         done
  552. }
  553.  
  554. # process multiple files in parallel processes
  555. #
  556. # schedules the filenames passed to the function to execute in parallel by
  557. # calling this script for each file
  558. #
  559. # this function only runs if there are multiple files specified on the
  560. # command line
  561. #
  562. # XXX : it is VERY IMPORTANT that the self-calls initiated here are given only
  563. #       one file argument or this script will loop recursively forever
  564. schedule_process() {
  565.         local \
  566.                 xargs_opts \
  567.                 self_args \
  568.                 file
  569.  
  570.         # intial options
  571.         xargs_opts=(
  572.                 '--null'                # accept null-terminated list
  573.                 '--max-args=1'          # send only one list item for each
  574.                                         # process (VERY IMPORTANT!!!)
  575.                 "--max-procs=$NUM_JOBS" # execute on parallel process per core
  576.                 '--'                    # terminate option list
  577.         )
  578.  
  579.         self_args=()
  580.  
  581.         if [ "$KEEP_EXIF" -ne 0 ]; then
  582.                 self_args+=('-e')
  583.         fi
  584.  
  585.         if [ "$PRESERVE_TIMESTAMP" -eq 0 ]; then
  586.                 self_args+=('-T')
  587.         fi
  588.  
  589.         if [ "$QUIET" -ne 0 ]; then
  590.                 self_args+=('-q')
  591.         fi
  592.  
  593.         if [ "$CAREFUL" -ne 0 ]; then
  594.                 self_args+=('-C')
  595.         fi
  596.  
  597.         self_args+=('--')       # terminate options list
  598.  
  599.         self_exec=(
  600.                 "$0"            # call self
  601.                 "${self_args[@]}"
  602.         )
  603.  
  604.         # add the self-execution to what xargs(1) executes
  605.         xargs_opts+=(
  606.                 "${self_exec[@]}"
  607.         )
  608.  
  609.         # translate into null-separated list for maximum xargs security
  610.         for file; do
  611.                 #echo -n "$file"$'\0'   # <~ this won't work
  612.                 echo -n "$file"
  613.                 echo -ne '\0'
  614.         done |
  615.  
  616.         # pipe it to xargs which will perform the scheduling, passing one
  617.         # file to another running copy of this script
  618.         # XXX : it is VERY important that only ONE file get passed or the
  619.         #       script will get stuck in an infinite recursion loop
  620.         xargs "${xargs_opts[@]}"
  621. }
  622.  
  623. # process a single file
  624. #
  625. # should run only if the script is given only one argument, which happens when
  626. # the script is self-executed via xargs(1) from the schedule_process() function
  627. single_process() {
  628.         optimize_image "$@"
  629. }
  630.  
  631. # process directories recursively
  632. #
  633. # sends all[*] files, optionally matching a given regex, located in the
  634. # directories given to a recursive call to this script, whereby the
  635. # schedule_process() function receives them and processes them in parallel
  636. #
  637. # *: does not send all files at once; there ARE system limits you know...
  638. #    but that doesn't mean that not all files get processed--the list just
  639. #    gets split up and sent to multiple self-invocations
  640. recursive_selfcall() {
  641.         local \
  642.                 dirnames \
  643.                 dir \
  644.                 find_params \
  645.                 self_exec \
  646.                 self_args
  647.  
  648.         # transform directories passed in into absolute paths in order to
  649.         # correctly process directory arguments beginning with a hyphen
  650.         dirnames=()
  651.         for dir; do
  652.                 dirnames+=("$(readlink -f -- "$dir")")
  653.         done
  654.  
  655.         # intial options
  656.         find_params=(
  657.                 '-P'            # don't dereference symlinks
  658.                 "${dirnames[@]:-.}" # search directories (expands to either
  659.                                     # quoted arguments to this function, or
  660.                                     # `.')
  661.                 '-type' 'f'
  662.         )
  663.  
  664.         # add regex to match file extensions if specified with `-x'
  665.         if [ "${#EXTS[*]}" -gt 0 ]; then
  666.                 find_params+=(
  667.                         '-regextype'    'posix-egrep'
  668.                         '-regex'        '.*\.('"${EXTS[*]/#.}"')'
  669.                 )
  670.         fi
  671.  
  672.         self_args=(
  673.                 '-j'    "$NUM_JOBS"     # always-set option
  674.         )
  675.  
  676.         # pass option switches to self (except for `-r' and `-x`)
  677.         if [ "$KEEP_EXIF" -ne 0 ]; then
  678.                 self_args+=('-e')
  679.         fi
  680.  
  681.         if [ "$PRESERVE_TIMESTAMP" -eq 0 ]; then
  682.                 self_args+=('-T')
  683.         fi
  684.  
  685.         if [ "$QUIET" -ne 0 ]; then
  686.                 self_args+=('-q')
  687.         fi
  688.  
  689.         if [ "$CAREFUL" -ne 0 ]; then
  690.                 self_args+=('-C')
  691.         fi
  692.  
  693.         self_args+=('--')       # terminate options list
  694.  
  695.         self_exec=(
  696.                 '-exec' "$0"    # the self-call
  697.                 "${self_args[@]}"
  698.                 '{}'    '+'     # make sure our clone gets sent all the
  699.                                 # matching files
  700.         )
  701.  
  702.         find_params+=(
  703.                 "${self_exec[@]}"
  704.         )
  705.  
  706.         # now the call to find(1) is simple! (haha)
  707.         find "${find_params[@]}"
  708. }
  709.  
  710. # separate directory arguments from files
  711. arg_dirs=()
  712. arg_files=()
  713. for arg; do
  714.         if [ -d "$arg" ]; then
  715.                 arg_dirs+=("$arg")
  716.         elif [ -f "$arg" ]; then
  717.                 arg_files+=("$arg")
  718.         else
  719.                 echo "Unknown filetype \`$arg'" >&2
  720.         fi
  721. done
  722.  
  723. # if only a single filename was passed in, then we're [most likely] being
  724. # called from xargs; otherwise, we're scheduling
  725.  
  726. if [ "$RECURSIVE" -ne 0 ]; then
  727.         if [ "$QUIET" -eq 0 ]; then
  728.                 recursive_selfcall "${arg_dirs[@]}"
  729.         else
  730.                 recursive_selfcall "${arg_dirs[@]}" >/dev/null 2>&1
  731.         fi
  732. else
  733.         # no arguments; print out help message like the friendlier
  734.         # command-line utilities
  735.         if [ "$#" -eq 0 ]; then
  736.                 HELP_MESSAGE 0
  737.         fi
  738.  
  739.         if [ "${#arg_files[@]}" -eq 0 ]; then
  740.                 echo 'No files to process.' >&2
  741.                 if [ "${#arg_dirs[@]}" -ne 0 ]; then
  742.                         echo '(Use `-r'\'' to process directories.)' >&2
  743.                 fi
  744.                 # obligatory stereotypical GNU-style message
  745.                 echo "Try \`$(basename -- "$0") -h' for more information." >&2
  746.                 exit 2
  747.         fi
  748.  
  749.         begin_filesize="$(file_size "${arg_files[@]}")"
  750.  
  751.         if [ "${#arg_files[@]}" -eq 1 -o "$NUM_JOBS" -eq 1 ]; then
  752.  
  753.                 # only one file or one job specified; no need to run parallel
  754.                 # processes
  755.                 single_process "${arg_files[@]}"
  756.  
  757.                 if [ "${#arg_files[@]}" -eq 1 ]; then
  758.                         report_name="${arg_files[0]}"
  759.                 else
  760.                         report_name='all'
  761.                 fi
  762.  
  763.         else
  764.                 # multiple files specified; process them in parallel
  765.  
  766.                 # schedule jobs
  767.                 if [ "$QUIET" -eq 0 ]; then
  768.                         schedule_process "${arg_files[@]}"
  769.                 else
  770.                         schedule_process "${arg_files[@]}" >/dev/null 2>&1
  771.                 fi
  772.  
  773.                 report_name='all'
  774.  
  775.         fi
  776.  
  777.         end_filesize="$(file_size "${arg_files[@]}")"
  778.  
  779.         freed="$((begin_filesize-end_filesize))"
  780.         freed_hr="$(hr_size "$freed")"
  781.  
  782.         # note: this must occur on the same line, because otherwise it gets
  783.         # separated during asynchronous operations
  784.         echo "${report_name}: freed ${freed} bytes (${freed_hr})"
  785. fi