You can use globstar.
With the globstar
shell option enabled, all you need is gzip -vk **/*.{css,html}
.
The Bash shell has a globstar
option that lets you write recursive globs with **
. shopt -s globstar
enables it. But you might not want to do that for other commands you run later, so you can run it and your gzip
command in a subshell instead.
This command gzip
s all .css
and .html
files in the current directory any of its subdirectories, any of their subdirectories, etc., keeping the original files (-k
) and telling you what it's doing (-v
):
(shopt -s globstar; gzip -vk **/*.{css,html})
If you want to match filenames case-insensitively so those extensions with some or all letters capitalized are included, then you can also enable the nocaseglob
shell option:
(shopt -s globstar nocaseglob; gzip -vk **/*.{css,html})
;
separates the two commands, and the outer (
)
cause them to be run in a subshell. Setting a shell option in a subshell does not cause it to be set in the calling shell. If you do want to enable globstar
then you can run shopt -s globstar
; then you can just run the command:
gzip -vk **/*.{css,html}
You can disable globstar
with shopt -u globstar
. You can check if it's currently enabled with shopt globstar
.
How It Works
The key to how this gzip
command works is that the shell performs expansions on it to produce a list of each file in the directory hierarchy with a matching name, then passes each of these filenames as arguments to gzip
.
- Brace expansion turns
**/*.{css,html}
into **/*.css **/*.html
.
- Then globbing expands those two patterns into the names of files accessible under the current directory (
**
, due to globstar
) whose filenames consist of anything (*
) followed by the specified suffix (.css
or .html
in this case).
This does not match files whose names start with .
or those that reside in directories named this way. You probably don't have any such HTML and CSS files and, if you do, you probably don't want to include them. But if you do want to include them, then you can match them explicitly depending on your needs. For example, changing **/*.{css,html}
to **/{,.}*.{css,html}
includes files that start with .
while still not searching in folders that do.
If you want both files whose names start with .
and files in directories whose names start with .
to be included, there's a cleaner and simpler way: enable the dotglob
shell option.
(shopt -s globstar dotglob; gzip -vk **/*.{css,html})
Or if you want case-insensitive matching and matching of filenames that start with .
:
(shopt -s globstar nocaseglob dotglob; gzip -vk **/*.{css,html})
It's possible, though very rare, for **
to expand to something too long.
If you have a huge number of files named this way, then this may fail with an error message explaining that the shell cannot build the command line because it would be too long. (Even with thousands of files, this usually is not a problem.)
gzip
won't be called at all, so you won't get a half-done job.
If this error happens, or if you're worried about it, you can use find
with -exec
, either as steeldriver describes (with {} \;
) or as I describe below (with {} +
).
You can use find
with the -exec
action and +
for efficiency.
The gzip
command supports being given names of multiple files to be compressed. But this find
command, although it works well and won't be slow unless you have many files, runs the gzip
command once for each file:
find . \( -name \*.css -o -name \*.html \) -exec gzip -vk {} \;
This works, and you can definitely use it. (.
searches from the current directory. Besides that, it's really a slightly different way of writing the command in steeldriver's very good answer; you can use whichever style you prefer.)
You can also make find
pass multiple filenames to gzip
and run it only as many times as necessary--which is nearly always just once. To do that, use +
instead of \;
. The +
argument should come just after {}
. find
replaces +
with additional filenames, if any.
find . \( -name \*.css -o -name \*.html \) -exec gzip -vk {} +
It's fine to use +
even if there are only a few matching files, and when there are many of them, it can be noticeably faster than having a separate gzip
invocation for each file.
As steeldriver mentions, you can use -iname
instead of -name
to match files whose name end like .css
or .html
but with different capitalization. This corresponds to enabling nocaseglob
in the globstar
-based method described above.
Finally, you probably don't have any matching files or directories that start with .
. But if you do, find
automatically includes them. If you want to exclude them (as happens with the globstar
-based method detailed above when dotglob
is off), you can:
find . -not -path '*/.*' \( -name \*.css -o -name \*.html \) -exec gzip -vk {} +
The globstar
-based way described above is simpler to write, especially if you're excluding directories and files that begin with .
, since that's the default.
What not to do...
Filenames can contain any character except the path separator /
and the null character. Many techniques that break on weird filenames exist, and they are usually more complicated than techniques that always just work. So I suggest avoiding them even when you know (or think you know) they're okay in your specific situation. And of course you must not use them if you might have filenames with characters that may be treated specially, including spaces.
It is possible to safely pipe the output of find
to another command that processes it if you use -print0
or a similar action to cause it to place a null character between paths instead of a newline, and not otherwise. Filenames can contain newlines (though I discourage you from deliberately naming files with them). A find
command with the -print
action--including find commands with no explicit action, since then -print
is the default--does not produce output that can safely be piped or otherwise provided to another command that performs an action on the files.
The output find
produces with the -print0
action may safely be piped to xargs -0
(the -0
flag tells xargs
to expect null-separated input).
-r
works as designed. From man gzip: Travel the directory structure recursively. If any of the file names specified on the command line are directories, gzip will descend into the directory and compress all the files it finds there (or decompress them in the case of gunzip). (emphasis mine)