Skip to content

Commit

Permalink
cmd: use tdewolff/argp for argument parsing; support --ext for mappin…
Browse files Browse the repository at this point in the history
…g custom extensions, see #438
  • Loading branch information
tdewolff committed Oct 6, 2023
1 parent 39b0c09 commit 38f0e88
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 96 deletions.
11 changes: 11 additions & 0 deletions cmd/minify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,17 @@ A trailing slash in the source path will copy all files inside the directory, wh

A trailing slash in the destination path forces writing into a directory. This removes ambiguity when minifying a single file which would otherwise write to a file.

#### Map custom extensions
You can map other extensions to a minifier by using the `--ext` option, which maps a filename extension to a filetype or mimetype, which is associated with a minifier.

```sh
$ minify -r -o out/ --ext.scss=text/css --ext.xjs=js src/
```
or
```sh
$ minify -r -o out/ --ext {scss:text/css xjs:js} src/
```

### Concatenate
When multiple inputs are given and the output is either standard output or a single file, it will concatenate the files together if you use the bundle option.

Expand Down
2 changes: 1 addition & 1 deletion cmd/minify/bash_completion
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ _minify_complete() {
local cur prev flags mimes types
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
flags="-a --all --bundle --exclude --include -l --list --match -o --output -p --preserve -q --quiet -r --recursive --type --url -v --verbose --version -w --watch --css-precision --html-keep-comments --html-keep-conditional-comments --html-keep-default-attrvals --html-keep-document-tags --html-keep-end-tags --html-keep-quotes --html-keep-whitespace --js-precision --js-keep-var-names --js-version --json-precision --json-keep-numbers --svg-keep-comments --svg-precision -s --sync --xml-keep-whitespace"
flags="-a --all --bundle --exclude --ext --include -l --list --match -o --output -p --preserve -q --quiet -r --recursive --type --url -v --verbose --version -w --watch --css-precision --html-keep-comments --html-keep-conditional-comments --html-keep-default-attrvals --html-keep-document-tags --html-keep-end-tags --html-keep-quotes --html-keep-whitespace --js-precision --js-keep-var-names --js-version --json-precision --json-keep-numbers --svg-keep-comments --svg-precision -s --sync --xml-keep-whitespace"
types="css html js json svg xml text/css text/html text/javascript application/javascript application/json image/svg+xml text/xml application/xml"

if echo "${cur}" | grep -Eq '^-'; then
Expand Down
202 changes: 107 additions & 95 deletions cmd/minify/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"os"
"os/signal"
"path/filepath"
"reflect"
"regexp"
"runtime"
"sort"
Expand All @@ -20,7 +21,7 @@ import (
"github.com/djherbis/atime"
humanize "github.com/dustin/go-humanize"
"github.com/matryer/try"
flag "github.com/spf13/pflag"
"github.com/tdewolff/argp"
min "github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/css"
"github.com/tdewolff/minify/v2/html"
Expand Down Expand Up @@ -56,6 +57,7 @@ var (
matchesRegexp []*regexp.Regexp
filters []string
filtersRegexp []*regexp.Regexp
extensions map[string]string
recursive bool
quiet bool
verbose int
Expand All @@ -72,6 +74,36 @@ var (
oldmimetype string
)

type Includes struct {
filters *[]string
}

func (scanner Includes) Scan(s []string) (int, error) {
v := ""
n, err := argp.ScanVar(reflect.ValueOf(&v).Elem(), s)
*scanner.filters = append(*scanner.filters, "+"+v)
return n, err
}

func (typenamer Includes) TypeName() string {
return "[]string"
}

type Excludes struct {
filters *[]string
}

func (scanner Excludes) Scan(s []string) (int, error) {
v := ""
n, err := argp.ScanVar(reflect.ValueOf(&v).Elem(), s)
*scanner.filters = append(*scanner.filters, "-"+v)
return n, err
}

func (typenamer Excludes) TypeName() string {
return "[]string"
}

// Task is a minify task.
type Task struct {
root string
Expand Down Expand Up @@ -105,8 +137,9 @@ func main() {
}

func run() int {
output := ""
siteurl := ""
var inputs []string
var output string
var siteurl string

cssMinifier := &css.Minifier{}
htmlMinifier := &html.Minifier{}
Expand All @@ -115,80 +148,84 @@ func run() int {
svgMinifier := &svg.Minifier{}
xmlMinifier := &xml.Minifier{}

f := flag.NewFlagSet("minify", flag.ContinueOnError)
f.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [options] [input]\n\nOptions:\n", os.Args[0])
f.PrintDefaults()
fmt.Fprintf(os.Stderr, "\nInput:\n Files or directories, leave blank to use stdin. Specify --type to use stdin and stdout.\n")
}

f.BoolVarP(&help, "help", "h", false, "Show usage")
f.StringVarP(&output, "output", "o", "", "Output file or directory (must have trailing slash), leave blank to use stdout")
f.StringVar(&oldmimetype, "mime", "", "Mimetype (eg. text/css), optional for input filenames (DEPRECATED, use --type)")
f.StringVar(&mimetype, "type", "", "Filetype (eg. css or text/css), optional for input filenames")
f.String("match", "", "Filename matching pattern, only matching files are processed")
f.String("include", "", "Filename inclusion pattern, includes files previously excluded")
f.String("exclude", "", "Filename exclusion pattern, excludes files from being processed")
f.BoolVarP(&recursive, "recursive", "r", false, "Recursively minify directories")
f.BoolVarP(&hidden, "all", "a", false, "Minify all files, including hidden files and files in hidden directories")
f.BoolVarP(&list, "list", "l", false, "List all accepted filetypes")
f.BoolVarP(&quiet, "quiet", "q", false, "Quiet mode to suppress all output")
f.CountVarP(&verbose, "verbose", "v", "Verbose mode, set twice for more verbosity")
f.BoolVarP(&watch, "watch", "w", false, "Watch files and minify upon changes")
f.BoolVarP(&sync, "sync", "s", false, "Copy all files to destination directory and minify when filetype matches")
f.StringSliceVarP(&preserve, "preserve", "p", nil, "Preserve options (mode, ownership, timestamps, links, all)")
f.Lookup("preserve").NoOptDefVal = "mode,ownership,timestamps"
f.BoolVarP(&bundle, "bundle", "b", false, "Bundle files by concatenation into a single file")
f.BoolVar(&version, "version", false, "Version")

f.StringVar(&siteurl, "url", "", "URL of file to enable URL minification")
f.IntVar(&cssMinifier.Precision, "css-precision", 0, "Number of significant digits to preserve in numbers, 0 is all")
f.BoolVar(&htmlMinifier.KeepComments, "html-keep-comments", false, "Preserve all comments")
f.BoolVar(&htmlMinifier.KeepConditionalComments, "html-keep-conditional-comments", false, "Preserve all IE conditional comments")
f.BoolVar(&htmlMinifier.KeepDefaultAttrVals, "html-keep-default-attrvals", false, "Preserve default attribute values")
f.BoolVar(&htmlMinifier.KeepDocumentTags, "html-keep-document-tags", false, "Preserve html, head and body tags")
f.BoolVar(&htmlMinifier.KeepEndTags, "html-keep-end-tags", false, "Preserve all end tags")
f.BoolVar(&htmlMinifier.KeepWhitespace, "html-keep-whitespace", false, "Preserve whitespace characters but still collapse multiple into one")
f.BoolVar(&htmlMinifier.KeepQuotes, "html-keep-quotes", false, "Preserve quotes around attribute values")
f.IntVar(&jsMinifier.Precision, "js-precision", 0, "Number of significant digits to preserve in numbers, 0 is all")
f.BoolVar(&jsMinifier.KeepVarNames, "js-keep-var-names", false, "Preserve original variable names")
f.IntVar(&jsMinifier.Version, "js-version", 0, "ECMAScript version to toggle supported optimizations (e.g. 2019, 2020), by default 0 is the latest version")
f.IntVar(&jsonMinifier.Precision, "json-precision", 0, "Number of significant digits to preserve in numbers, 0 is all")
f.BoolVar(&jsonMinifier.KeepNumbers, "json-keep-numbers", false, "Preserve original numbers instead of minifying them")
f.BoolVar(&svgMinifier.KeepComments, "svg-keep-comments", false, "Preserve all comments")
f.IntVar(&svgMinifier.Precision, "svg-precision", 0, "Number of significant digits to preserve in numbers, 0 is all")
f.BoolVar(&xmlMinifier.KeepWhitespace, "xml-keep-whitespace", false, "Preserve whitespace characters but still collapse multiple into one")
if len(os.Args) == 1 {
f := argp.New("minify")
f.AddRest(&inputs, "inputs", "Input files or directories, leave blank to use stdin")
f.AddOpt(&output, "o", "output", nil, "Output file or directory, leave blank to use stdout")
f.AddOpt(&oldmimetype, "", "mime", nil, "Mimetype (eg. text/css), optional for input filenames (DEPRECATED, use --type)")
f.AddOpt(&mimetype, "", "type", nil, "Filetype (eg. css or text/css), optional for input filenames")
f.AddOpt(&matches, "", "match", nil, "Filename matching pattern, only matching files are processed")
f.AddOpt(Includes{&filters}, "", "include", nil, "Filename inclusion pattern, includes files previously excluded")
f.AddOpt(Excludes{&filters}, "", "exclude", nil, "Filename exclusion pattern, excludes files from being processed")
f.AddOpt(&extensions, "", "ext", nil, "Filename extension mapping to filetype (eg. css or text/css)")
f.AddOpt(&recursive, "r", "recursive", false, "Recursively minify directories")
f.AddOpt(&hidden, "a", "all", false, "Minify all files, including hidden files and files in hidden directories")
f.AddOpt(&list, "l", "list", false, "List all accepted filetypes")
f.AddOpt(&quiet, "q", "quiet", false, "Quiet mode to suppress all output")
f.AddOpt(argp.Count{&verbose}, "v", "verbose", nil, "Verbose mode, set twice for more verbosity")
f.AddOpt(&watch, "w", "watch", false, "Watch files and minify upon changes")
f.AddOpt(&sync, "s", "sync", false, "Copy all files to destination directory and minify when filetype matches")
f.AddOpt(&preserve, "p", "preserve", []string{"mode", "ownership", "timestamps"}, "Preserve options (mode, ownership, timestamps, links, all)")
f.AddOpt(&bundle, "b", "bundle", false, "Bundle files by concatenation into a single file")
f.AddOpt(&version, "", "version", false, "Version")

f.AddOpt(&siteurl, "", "url", nil, "URL of file to enable URL minification")
f.AddOpt(&cssMinifier.Precision, "", "css-precision", 0, "Number of significant digits to preserve in numbers, 0 is all")
f.AddOpt(&htmlMinifier.KeepComments, "", "html-keep-comments", false, "Preserve all comments")
f.AddOpt(&htmlMinifier.KeepConditionalComments, "", "html-keep-conditional-comments", false, "Preserve all IE conditional comments")
f.AddOpt(&htmlMinifier.KeepDefaultAttrVals, "", "html-keep-default-attrvals", false, "Preserve default attribute values")
f.AddOpt(&htmlMinifier.KeepDocumentTags, "", "html-keep-document-tags", false, "Preserve html, head and body tags")
f.AddOpt(&htmlMinifier.KeepEndTags, "", "html-keep-end-tags", false, "Preserve all end tags")
f.AddOpt(&htmlMinifier.KeepWhitespace, "", "html-keep-whitespace", false, "Preserve whitespace characters but still collapse multiple into one")
f.AddOpt(&htmlMinifier.KeepQuotes, "", "html-keep-quotes", false, "Preserve quotes around attribute values")
f.AddOpt(&jsMinifier.Precision, "", "js-precision", 0, "Number of significant digits to preserve in numbers, 0 is all")
f.AddOpt(&jsMinifier.KeepVarNames, "", "js-keep-var-names", false, "Preserve original variable names")
f.AddOpt(&jsMinifier.Version, "", "js-version", 0, "ECMAScript version to toggle supported optimizations (e.g. 2019, 2020), by default 0 is the latest version")
f.AddOpt(&jsonMinifier.Precision, "", "json-precision", 0, "Number of significant digits to preserve in numbers, 0 is all")
f.AddOpt(&jsonMinifier.KeepNumbers, "", "json-keep-numbers", false, "Preserve original numbers instead of minifying them")
f.AddOpt(&svgMinifier.KeepComments, "", "svg-keep-comments", false, "Preserve all comments")
f.AddOpt(&svgMinifier.Precision, "", "svg-precision", 0, "Number of significant digits to preserve in numbers, 0 is all")
f.AddOpt(&xmlMinifier.KeepWhitespace, "", "xml-keep-whitespace", false, "Preserve whitespace characters but still collapse multiple into one")
f.Parse()

if version {
if !quiet {
fmt.Printf("minify: must specify --type in order to use stdin and stdout\n")
fmt.Printf("Try 'minify --help' for more information\n")
fmt.Printf("minify %s\n", Version)
}
return 1
return 0
}

for ext, filetype := range extensions {
if mimetype, ok := extMap[filetype]; ok {
filetype = mimetype
}
extMap[ext] = filetype
}
err := f.ParseAll(os.Args[1:], func(flag *flag.Flag, value string) error {
if flag.Name == "match" || flag.Name == "include" || flag.Name == "exclude" {
for _, filter := range strings.Split(value, ",") {
if flag.Name == "match" {
matches = append(matches, filter)
} else if flag.Name == "include" {
filters = append(filters, "+"+filter)
} else {
filters = append(filters, "-"+filter)

if list {
if !quiet {
n := 0
var keys []string
for k := range extMap {
keys = append(keys, k)
if n < len(k) {
n = len(k)
}
}
return nil
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k + strings.Repeat(" ", n-len(k)+1) + extMap[k])
}
}
return f.Set(flag.Name, value)
})
if err != nil {
return 0
}

if len(inputs) == 0 && mimetype == "" && oldmimetype == "" {
if !quiet {
fmt.Printf("minify: %v\n", err)
fmt.Printf("minify: must specify --type in order to use stdin and stdout\n")
fmt.Printf("Try 'minify --help' for more information\n")
}
return 1
}
inputs := f.Args()
if len(inputs) == 1 && inputs[0] == "-" {
} else if len(inputs) == 1 && inputs[0] == "-" {
inputs = inputs[:0]
} else if output == "-" {
output = ""
Expand All @@ -208,33 +245,8 @@ func run() int {
}
}

if help {
f.Usage()
return 0
}

if version {
if !quiet {
fmt.Printf("minify %s\n", Version)
}
return 0
}

if list {
if !quiet {
var keys []string
for k := range extMap {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k + "\t" + extMap[k])
}
}
return 0
}

// compile matches and regexps
var err error
if 0 < len(matches) {
matchesRegexp = make([]*regexp.Regexp, len(matches))
for i, pattern := range matches {
Expand All @@ -256,7 +268,7 @@ func run() int {

// detect mimetype, mimetype=="" means we'll infer mimetype from file extensions
if oldmimetype != "" {
Error.Println("deprecated use of '--mime %v', please use '--type %v' instead", oldmimetype, oldmimetype)
Error.Printf("deprecated use of '--mime %v', please use '--type %v' instead", oldmimetype, oldmimetype)
mimetype = oldmimetype
}
if slash := strings.Index(mimetype, "/"); slash == -1 && 0 < len(mimetype) {
Expand Down Expand Up @@ -299,7 +311,7 @@ func run() int {
} else {
Info.Println("use mimetype", mimetype)
}
if len(preserve) != 0 {
if f.IsSet("preserve") {
if bundle {
Error.Println("--preserve cannot be used together with --bundle")
return 1
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/fsnotify/fsnotify v1.6.0
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2
github.com/spf13/pflag v1.0.5
github.com/tdewolff/argp v0.0.0-20231006010547-9c5fed0deeda
github.com/tdewolff/parse/v2 v2.6.8
github.com/tdewolff/test v1.0.9
)
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2 h1:JAEbJn3j/FrhdWA9jW8
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/tdewolff/argp v0.0.0-20230622205231-8a4234db046f h1:l5DhJGDkk4qI60fmBURYxC9cr7hgEtrrc4P1WJKp38E=
github.com/tdewolff/argp v0.0.0-20230622205231-8a4234db046f/go.mod h1:fF+gnKbmf3iMG+ErLiF+orMU/InyZIEnKVVigUjfriw=
github.com/tdewolff/argp v0.0.0-20231006010547-9c5fed0deeda h1:6CfJwZHxYOIpEYRiqU7z34yWc99balM7xxSFYz/ke8U=
github.com/tdewolff/argp v0.0.0-20231006010547-9c5fed0deeda/go.mod h1:fF+gnKbmf3iMG+ErLiF+orMU/InyZIEnKVVigUjfriw=
github.com/tdewolff/parse/v2 v2.6.8 h1:mhNZXYCx//xG7Yq2e/kVLNZw4YfYmeHbhx+Zc0OvFMA=
github.com/tdewolff/parse/v2 v2.6.8/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM=
github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tdewolff/test v1.0.9 h1:SswqJCmeN4B+9gEAi/5uqT0qpi1y2/2O47V/1hhGZT0=
github.com/tdewolff/test v1.0.9/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down

0 comments on commit 38f0e88

Please sign in to comment.