#!/bin/sh # # Shell command language minifier # # Copyright (C) 2015 Patrick "P. J." McDermott # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . set -u VERSION='0.1.0' HT=' ' LF=' ' c= output='' die() { local fmt="${1}" shift 1 printf "shmin: ${fmt}\n" "${@}" exit 2 } fgetc() { # Damn command substitutions eat trailing whitespace. Even if # whitespace is the /only/ thing printed and exactly what we want from # the command. c="$(dd bs=1 count=1 <&3 2>/dev/null; printf '.')" c="${c%.}" } minify() { local input="${1}" local buffer='' local escaped=false # Open input file. if ! exec 3<"${input}"; then die 'Cannot open file "%s"' "${input}" fi # Check for magic number or other first-line comment. fgetc if [ "x${c}" = 'x#' ]; then # Comment fgetc if [ "x${c}" = 'x!' ]; then # Magic number buffer="${buffer}#!" while :; do fgetc buffer="${buffer}${c}" if [ "x${c}" = "x${LF}" ]; then break fi done else while :; do fgetc if [ "x${c}" = "x${LF}" ]; then break fi done fi fi # Strip leading whitespace on the first line. while :; do case "${c}" in ' ' | "${HT}" | "${LF}") ;; *) break ;; esac fgetc done # Parse the rest of the file. # Like any good parser, this is some very ugly and fragile code. # Whitespace code must come before comment code must come before newline # code must come before everything else. while :; do if [ "x${c}" = 'x' ]; then break fi if [ "x${c}" = 'x ' ] || [ "x${c}" = "x${HT}" ]; then # Whitespace while :; do fgetc case "${c}" in ' ' | "${HT}") ;; "${LF}" | '#') # Eat trailing whitespace. break ;; *) # Condense whitespace. buffer="${buffer} " break ;; esac done fi if [ "x${c}" = 'x#' ]; then # Comment while :; do fgetc if [ "x${c}" = "x${LF}" ]; then break fi done fi if [ "x${c}" = "x${LF}" ]; then # Strip empty lines and leading whitespace. while :; do fgetc case "${c}" in ' ' | "${HT}" | "${LF}") ;; *) buffer="${buffer}${LF}" break ;; esac done fi if [ "x${c}" = 'x\' ]; then # Backslash buffer="${buffer}${c}" fgetc buffer="${buffer}${c}" continue fi if [ "x${c}" = "x'" ]; then # Single quotes buffer="${buffer}${c}" while :; do fgetc buffer="${buffer}${c}" if [ "x${c}" = "x'" ]; then break fi done fgetc continue fi if [ "x${c}" = 'x"' ]; then # Double quotes buffer="${buffer}${c}" while :; do fgetc buffer="${buffer}${c}" if ${escaped}; then escaped=false continue fi if [ "x${c}" = 'x\' ]; then escaped=true continue fi if [ "x${c}" = 'x"' ]; then break fi done fgetc continue fi buffer="${buffer}${c}" fgetc done # We made it. Now close input file. if ! exec 3<&-; then die 'Cannot close file "%s"' "${input}" fi # Write the output. if ! printf '%s' "${buffer}" >"${output}~"; then die 'Cannot write file "%s"' "${output}~" fi # Set output file name. if ! cat "${output}~" >"${output}"; then die 'Cannot rename file to "%s"' "${output}" fi if ! rm "${output}~"; then die 'Cannot remove file "%s"' "${output}~" fi } usage() { printf 'Usage: %s [option ...] \n' "${0}" } help() { usage cat < Write the minified output to , instead of replacing the existing file EOF } version() { cat <. This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. EOF } main() { local input= while getopts 'hVo:' opt; do case "${opt}" in 'h') help exit ;; 'V') version exit ;; 'o') output="${OPTARG}" ;; esac done shift $(($OPTIND - 1)) if [ ${#} -ne 1 ]; then usage >&2 exit 1 fi input="${1}" if [ "x${output}" = 'x' ]; then output="${input}" fi minify "${input}" } main "${@}"