diff --git a/cmd/minify/README.md b/cmd/minify/README.md index 7ab4acc97b..ebb2818d5a 100644 --- a/cmd/minify/README.md +++ b/cmd/minify/README.md @@ -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. diff --git a/cmd/minify/bash_completion b/cmd/minify/bash_completion index 73e0a6ec05..e117c67029 100644 --- a/cmd/minify/bash_completion +++ b/cmd/minify/bash_completion @@ -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 diff --git a/cmd/minify/main.go b/cmd/minify/main.go index 9bf44b9727..00e37021a6 100644 --- a/cmd/minify/main.go +++ b/cmd/minify/main.go @@ -11,6 +11,7 @@ import ( "os" "os/signal" "path/filepath" + "reflect" "regexp" "runtime" "sort" @@ -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" @@ -56,6 +57,7 @@ var ( matchesRegexp []*regexp.Regexp filters []string filtersRegexp []*regexp.Regexp + extensions map[string]string recursive bool quiet bool verbose int @@ -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 @@ -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{} @@ -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 = "" @@ -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 { @@ -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) { @@ -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 diff --git a/go.mod b/go.mod index 1932e72935..4de437a588 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index b2e0805a71..511c427b8a 100644 --- a/go.sum +++ b/go.sum @@ -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=