- 1. Scripts
- 1.1. General
- 1.2. Certificates
- 1.3. Docker
- 1.4. Git
- 1.5. Gradle
- 1.6. Java
- 1.7. Keycloak
- 1.8. Node.js
- 1.9. Web
- 1.9.1. compress-brotli
- 1.9.2. compress-gzip
- 1.9.3. compress-zstd
- 1.9.4. create-build-info-js
- 1.9.5. create-build-info-json.sh
- 1.9.6. create-build-info-ts
- 1.9.7. minify-css
- 1.9.8. minify-gif
- 1.9.9. minify-html
- 1.9.10. minify-jpeg
- 1.9.11. minify-json
- 1.9.12. minify-json-tags
- 1.9.13. minify-png
- 1.9.14. minify-robots
- 1.9.15. minify-svg
- 1.9.16. minify-traffic-advice
- 1.9.17. minify-webmanifest
- 1.9.18. minify-xml
- 2. Functions
- 3. License
- 4. Contribution
- 5. Code of Conduct
- 6. Development Environment Setup
Miscellaneous shell-related scripts and functions.
This section contains generally useful scripts:
- counter
-
create a counter
- create-timestamp-file
-
create a file with a timestamp
- loop
-
repeat a script repeatedly
- hash-filename
-
insert a hash into a filename
- shellscript-check
-
shellcheck
*.sh
files in the given directory
This script will create a counter with the given name.
The optional second positive integer parameter will stop the counter when the current count is equal or larger than the given argument.
Invoking this script will print the current count to stdout unless the counter has been removed.
The exit code of the script will be 100
when the count has been increased or 0
when the counter has been removed.
The count is persisted in a file in a temporary directory or COUNTER_DIR
if set in the environment.
#!/usr/bin/env sh
scripts/general/counter.sh toggle 1 1>/dev/null
if [ $? -eq 100 ]; then
echo 'on'
else
echo 'off'
fi
#!/usr/bin/env sh
COUNTER_DIR="${XDG_STATE_HOME:=${HOME}}/retry" scripts/general/counter.sh retry 3 1>/dev/null
if [ $? -ne 100 ]; then
echo 'tried enough times' >&2
exit 50
fi
$ scripts/general/counter.sh my-counter 2
1
$ echo $?
100
$ scripts/general/counter.sh my-counter 2
2
$ echo $?
100
$ scripts/general/counter.sh my-counter 2
$ echo $?
0
$ ./toggle.sh
on
$ ./toggle.sh
off
$ ./toggle.sh
on
$ mkdir -p "${XDG_STATE_HOME:=${HOME}}/retry"
$ ./retry.sh
$ ls "${XDG_STATE_HOME:=${HOME}}/retry"
counter-retry
$ cat /home/example/.local/state/retry/counter-retry
1
$ ./retry.sh
$ cat /home/example/.local/state/retry/counter-retry
2
$ ./retry.sh
$ cat /home/example/.local/state/retry/counter-retry
3
$ ./retry.sh
tried enough times
$ ls "${XDG_STATE_HOME:=${HOME}}/retry"
$ rm -rf "${XDG_STATE_HOME:=${HOME}}/retry"
-
$ scripts/general/loop.sh 1 0 scripts/general/counter.sh my-counter 5 12345
This script will create a file with the given name; the content will be the RFC 3339 timestamp of the file’s creation, e.g.:
2024-01-16T16:33:12Z
$ scripts/general/create-timestamp-file.sh .timestamp
$ cat .timestamp
2024-02-19T10:37:02Z
This script will rename a given file; the new filename will have a hash inserted, e.g.:
test.txt
⇒ test.da39a3e.txt
Use the optional second parameter -e
to print the new filename to stdout.
$ scripts/general/hash-filename.sh test.txt
$ scripts/general/hash-filename.sh test-echo.txt -e
test-echo.da39a3e.txt
$ find . \( -type f -name '*.jpg' -o -name '*.png' \) -exec scripts/general/hash-filename.sh {} ';'
This script will invoke the given script repeatedly with a given delay between invocations and an initial delay.
The loop will finish when the given script has an exit code other than 100
.
#!/usr/bin/env sh
if [ ... ]; then
exit 0 # finish loop
fi
#!/usr/bin/env sh
exit 100 # infinite loop
$ scripts/general/loop.sh 10 10 some-script.sh
$ scripts/general/loop.sh 5 0 some-otherscript-with-parameters.sh a 1
-
$ scripts/general/loop.sh 1 0 scripts/general/counter.sh my-counter 5 12345
This script will invoke shellcheck on *.sh
files in the given directory ($PWD
if not given) and its subdirectories.
ℹ️
|
|
💡
|
If you copy this script into a Node.js-based project you should exclude the
|
$ scripts/general/shellscript-check.sh
$ scripts/general/shellscript-check.sh /tmp
This section contains scripts related to certificates:
- create-self-signed-cert
-
create a private key and self-signed certificate
- delete-self-signed-cert
-
delete the private key and self-signed certificate
- verify-self-signed-cert
-
verify the self-signed certificate
This script will create a private key key.pem
and a self-signed certificate cert.pem
in the given directory ($PWD
if not given).
The given directory will be created if it does not exit yet.
The optional second positive integer parameter (range: [1, 24855]) specifies the number of days the generated certificate is valid for; the default is 30 days.
The optional third parameter is the common name (localhost
if not given) of the certificate to be added.
On macOS, the certificate will be added to the "login" keychain also.
|
Both If the given directory is inside a Git working tree the script will offer to modify the .gitignore file: WARNING: key.pem and/or cert.pem is not ignored in '/Users/example/tmp/.gitignore'
Do you want me to modify your .gitignore file (Y/N)? Related Script: git-cleanup |
ℹ️
|
The certificate created by this script is useful if you do not use mutual TLS, the HTTP-client can be configured to ignore self-signed certificates, or if you can add the certificate to your trust store. $ curl --insecure ...
$ wget --no-check-certificate ...
$ http --verify=no ... |
💡
|
Copy the script into your Node.js project and add it as a custom script to your package.json
{
...
"scripts": {
"cert:create": "scripts/create-self-signed-cert.sh certs"
}
} $ npm run cert:create |
$ scripts/cert/create-self-signed-cert.sh
$ scripts/cert/create-self-signed-cert.sh dist/etc/nginx
$ scripts/cert/create-self-signed-cert.sh . 10
Adding 'localhost' certificate (expires on: 2024-03-09) to keychain /Users/example/Library/Keychains/login.keychain-db ...
$ date -Idate
2024-02-28
$ stat -f '%A %N' *.pem
600 cert.pem
600 key.pem
$ scripts/cert/create-self-signed-cert.sh ~/.local/secrets/certs/https.local 30 https.local
Adding 'https.local' certificate (expires on: 2024-03-29) to keychain /Users/example/Library/Keychains/login.keychain-db ...
Check your login keychain in Keychain Access; Secure Sockets Layer (SSL) should be set to "Always Trust":
ℹ️
|
Chrome and Safari need no further configuration. |
You need to bypass the self-signed certificate warning by clicking on "Advanced" and then "Accept the Risk and Continue":
$ scripts/cert/create-self-signed-cert.sh ~/.local/secrets/certs/localhost
$ docker run --rm httpd:2.4.58-alpine cat /usr/local/apache2/conf/httpd.conf > httpd.conf.orig
$ sed -e 's/^#\(Include .*httpd-ssl.conf\)/\1/' \
-e 's/^#\(LoadModule .*mod_ssl.so\)/\1/' \
-e 's/^#\(LoadModule .*mod_socache_shmcb.so\)/\1/' \
httpd.conf.orig > httpd.conf
$ mkdir -p htdocs
$ printf '<!doctype html><title>Test</title><h1>Test' > htdocs/index.html
$ docker run -i -t --rm -p 3000:443 \
-v "$PWD/htdocs:/usr/local/apache2/htdocs:ro" \
-v "$PWD/httpd.conf:/usr/local/apache2/conf/httpd.conf:ro" \
-v "$HOME/.local/secrets/certs/localhost/cert.pem:/usr/local/apache2/conf/server.crt:ro" \
-v "$HOME/.local/secrets/certs/localhost/key.pem:/usr/local/apache2/conf/server.key:ro" \
httpd:2.4.58-alpine
$ scripts/cert/create-self-signed-cert.sh ~/.local/secrets/certs/localhost
$ printf 'server {
listen 443 ssl;
listen [::]:443 ssl;
ssl_certificate /etc/ssl/certs/server.crt;
ssl_certificate_key /etc/ssl/private/server.key;
location / {
root /usr/share/nginx/html;
index index.html;
}
}' > nginx.conf
$ mkdir -p html
$ printf '<!doctype html><title>Test</title><h1>Test' > html/index.html
$ docker run -i -t --rm -p 3000:443 \
-v "$PWD/html:/usr/share/nginx/html:ro" \
-v "$PWD/nginx.conf:/etc/nginx/conf.d/default.conf:ro" \
-v "$HOME/.local/secrets/certs/localhost/cert.pem:/etc/ssl/certs/server.crt:ro" \
-v "$HOME/.local/secrets/certs/localhost/key.pem:/etc/ssl/private/server.key:ro" \
nginx:1.25.4-alpine3.18-slim
func main() {
const port = 3000
server := http.Server{
Addr: fmt.Sprintf(":%d", port),
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
IdleTimeout: 5 * time.Second,
Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, err := w.Write([]byte("<!doctype html><title>Test</title><h1>Test"))
if err != nil {
slog.Error("handle response", slog.Any("error", err))
}
}),
}
defer func(server *http.Server) {
if err := server.Close(); err != nil {
slog.Error("server close", slog.Any("error", err))
os.Exit(70)
}
}(&server)
slog.Info(fmt.Sprintf("Listen local: https://localhost:%d", port))
if err := server.ListenAndServeTLS("cert.pem", "key.pem"); err != nil {
slog.Error("listen", slog.Any("error", err))
os.Exit(70)
}
}
$ cd scripts/cert/go
$ ../create-self-signed-cert.sh
$ go run server.go
['uncaughtException', 'unhandledRejection'].forEach((s) =>
process.once(s, (e) => {
console.error(e);
process.exit(70);
}),
);
['SIGINT', 'SIGTERM'].forEach((s) => process.once(s, () => process.exit(0)));
let https;
try {
https = await import('node:https');
} catch {
console.error('https support is disabled');
process.exit(78);
}
const port = 3000;
const server = https.createServer(
{
key: readFileSync('key.pem'),
cert: readFileSync('cert.pem'),
},
(_, w) => {
w.writeHead(200).end('<!doctype html><title>Test</title><h1>Test');
},
);
server.keepAliveTimeout = 5000;
server.requestTimeout = 5000;
server.timeout = 5000;
server.listen(port);
console.log(`Listen local: https://localhost:${port}`);
$ cd scripts/cert/nodejs
$ ../create-self-signed-cert.sh
$ node server.mjs
public final class Server {
public static void main(String[] args) throws Exception {
var port = 3000;
var server =
HttpsServer.create(
new InetSocketAddress(port),
0,
"/",
exchange -> {
var response = "<!doctype html><title>Test</title><h1>Test";
exchange.sendResponseHeaders(HTTP_OK, response.length());
try (var body = exchange.getResponseBody()) {
body.write(response.getBytes());
} catch (IOException e) {
LOGGER.log(SEVERE, "handle response", e);
}
});
server.setHttpsConfigurator(new HttpsConfigurator(newSSLContext()));
server.setExecutor(newVirtualThreadPerTaskExecutor());
server.start();
LOGGER.info(format("Listen local: https://localhost:%d", port));
}
static {
System.setProperty("sun.net.httpserver.maxReqTime", "5");
System.setProperty("sun.net.httpserver.maxRspTime", "5");
System.setProperty("sun.net.httpserver.idleInterval", "5000");
}
private static final Logger LOGGER = getLogger(MethodHandles.lookup().lookupClass().getName());
private static SSLContext newSSLContext() throws Exception {
var keyStorePath = requireNonNull(getenv("KEYSTORE_PATH"), "keystore path");
var keyStorePassword =
requireNonNull(getenv("KEYSTORE_PASS"), "keystore password").toCharArray();
var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(newInputStream(Path.of(keyStorePath)), keyStorePassword);
var keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, keyStorePassword);
var trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
var sslContext = SSLContext.getInstance("TLS");
sslContext.init(
keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
return sslContext;
}
}
$ cd scripts/cert/java
$ ../create-self-signed-cert.sh
$ openssl pkcs12 -export -in cert.pem -inkey key.pem -out certificate.p12 -name localhost -password pass:changeit
$ keytool -importkeystore -srckeystore certificate.p12 -srcstoretype pkcs12 -srcstorepass changeit -destkeystore keystore.jks -deststorepass changeit
$ KEYSTORE_PATH=keystore.jks KEYSTORE_PASS=changeit java Server.java
@SpringBootApplication
public class Server {
@RestController
public static class Controller {
@GetMapping("/")
public String index() {
return "<!doctype html><title>Test</title><h1>Test";
}
}
public static void main(String[] args) {
SpringApplication.run(Server.class, args);
}
}
server.port=3000
server.tomcat.connection-timeout=5s
server.ssl.bundle=https
spring.ssl.bundle.pem.https.reload-on-update=true
spring.ssl.bundle.pem.https.keystore.certificate=cert.pem
spring.ssl.bundle.pem.https.keystore.private-key=key.pem
$ cd scripts/cert/spring-boot
$ ../create-self-signed-cert.sh
$ ./gradlew bootRun
@Path("/")
public class Server {
@GET
@Produces(TEXT_HTML)
@RunOnVirtualThread
public String index() {
return "<!doctype html><title>Test</title><h1>Test";
}
}
quarkus.http.ssl-port=3000
quarkus.http.idle-timeout=5s
quarkus.http.read-timeout=5s
quarkus.http.ssl.certificate.reload-period=30s
quarkus.http.ssl.certificate.files=cert.pem
quarkus.http.ssl.certificate.key-files=key.pem
$ cd scripts/cert/quarkus
$ ../create-self-signed-cert.sh
$ ./gradlew quarkusDev
This script will delete the private key key.pem
and the self-signed certificate cert.pem
from the given directory ($PWD
if not given).
If the given directory is not $PWD
and is empty after the removal it will be removed as well.
The optional second parameter is the common name (localhost
if not given) of the certificate to be removed.
On macOS, the certificate will be removed from the "login" keychain also.
ℹ️
|
Chrome and Safari need no further configuration. |
💡
|
Copy the script into your Node.js project and add it as a custom script to your package.json
{
...
"scripts": {
"cert:delete": "scripts/delete-self-signed-cert.sh certs"
}
} $ npm run cert:delete |
$ scripts/cert/delete-self-signed-cert.sh
Removing 'localhost' certificate from keychain /Users/example/Library/Keychains/login.keychain-db ...
$ scripts/cert/delete-self-signed-cert.sh ~/.local/secrets/certs/localhost
Removing 'localhost' certificate from keychain /Users/example/Library/Keychains/login.keychain-db ...
$ scripts/cert/delete-self-signed-cert.sh ~/.local/secrets/certs/https.local https.local
Removing 'https.local' certificate from keychain /Users/example/Library/Keychains/login.keychain-db ...
You can delete the certificate via Firefox > Preferences > Privacy & Security > Certificates
; click "View Certificates…":
Click on the "Servers" tab:
This script will verify the self-signed certificate cert.pem
in the given directory ($PWD
if not given).
The optional second parameter is the common name (localhost
if not given) of the certificate to verify.
On macOS, the certificate will be verified in the "login" keychain also.
💡
|
Copy the script into your Node.js project and add it as a custom script to your package.json
{
...
"scripts": {
"cert:verify": "scripts/verify-self-signed-cert.sh certs"
}
} $ npm run cert:verify |
$ scripts/cert/verify-self-signed-cert.sh
$ scripts/cert/verify-self-signed-cert.sh ~/.local/secrets/certs/localhost
keychain: "/Users/example/Library/Keychains/login.keychain-db"
...
"labl"<blob>="localhost"
...
/Users/example/.local/secrets/certs/localhost/cert.pem
Certificate:
...
Issuer: CN=localhost, UID=example, O=Sebastian Davids
Validity
Not Before: Feb 28 11:54:32 2024 GMT
Not After : Mar 29 11:54:32 2024 GMT
Subject: CN=localhost, UID=example, O=Sebastian Davids
...
X509v3 Subject Alternative Name:
DNS:localhost
...
$ scripts/cert/verify-self-signed-cert.sh ~/.local/secrets/certs/https.local https.local
keychain: "/Users/example/Library/Keychains/login.keychain-db"
...
"labl"<blob>="https.local"
/Users/example/.local/secrets/certs/https.local/cert.pem
Certificate:
...
Issuer: CN=https.local, UID=example, O=Sebastian Davids
Validity
Not Before: Feb 28 11:49:00 2024 GMT
Not After : Mar 29 11:49:00 2024 GMT
Subject: CN=https.local, UID=example, O=Sebastian Davids
...
X509v3 Subject Alternative Name:
DNS:https.local
...
This section contains scripts related to Docker:
- docker-build
-
build the image
- docker-cleanup
-
remove all project-related containers, images, networks, and volumes
- docker-health
-
query the health status of the container
- docker-inspect
-
display detailed information on the container
- docker-remove
-
remove the container and associated unnamed volumes
- docker-start
-
start the image
- docker-sh
-
open a shell into the running container
- docker-stop
-
stop the container
The scripts should be copied into a project, e.g.:
<project root directory>
├── Dockerfile
└── scripts
├── docker-build.sh
├── docker-cleanup.sh
├── ...
And then invoked from the directory containing the Dockerfile
:
$ scripts/docker-build.sh
ℹ️
|
All scripts need |
❗
|
You should modify the readonly container_name="sdavids-shell-misc-docker-example"
readonly label_group='de.sdavids.docker.group'
readonly namespace='de.sdavids'
readonly repository='sdavids-shell-misc' The scripts expect the image to be named The scripts expect the container to be named |
💡
|
You can try the scripts with the example Dockerfile: $ scripts/docker/docker-build.sh -d scripts/docker/Dockerfile
$ scripts/docker/docker-start.sh $ scripts/docker/docker-inspect.sh
$ scripts/docker/docker-sh.sh
$ scripts/docker/docker-health.sh $ scripts/docker/docker-stop.sh
$ scripts/docker/docker-remove.sh
$ scripts/docker/docker-cleanup.sh |
This script will build the ${namespace}/${repository}
image, i.e. the project’s image.
The following parameters are supported:
d
-
the path to the Dockerfile (
$PWD/Dockerfile
if not given) to be used n
-
do not use the cache when building the image
t
-
one of the two image’s tags (
local
if not given); the image will always be tagged withlatest
The script will pass the hash of the HEAD commit of the checked out branch as a build argument named git_commit
to the Dockerfile; the suffix -next
will be appended if the working tree is dirty.
The script will pass the creation timestamp of the HEAD commit of the checked out branch as a build argument named created_at
to the Dockerfile; the current time will be used if the working tree is dirty.
Alternatively, you can use the SOURCE_DATE_EPOCH environment variable to pass in the timestamp.
ℹ️
|
See the general notes of the Docker section. |
$ scripts/docker/docker-build.sh
...
=> => naming to docker.io/sdavids-shell-misc/sdavids-shell-misc-docker:latest
=> => naming to docker.io/sdavids-shell-misc/sdavids-shell-misc-docker:local
...
$ scripts/docker/docker-build.sh -d scripts/docker/Dockerfile -n -t 1.2.3
...
=> => naming to docker.io/sdavids-shell-misc/sdavids-shell-misc-docker:latest
=> => naming to docker.io/sdavids-shell-misc/sdavids-shell-misc-docker:1.2.3
...
"org.opencontainers.image.created":"2024-05-05T11:05:50Z"
...
"org.opencontainers.image.revision":"46cca5eff61eabb008ed43e81988e6a9099aa469"
...
$ touch dirty-repo
$ SOURCE_DATE_EPOCH=0 scripts/docker/docker-build.sh -d scripts/docker/Dockerfile -n -t 1.2.3
...
=> => naming to docker.io/sdavids-shell-misc/sdavids-shell-misc-docker:latest
=> => naming to docker.io/sdavids-shell-misc/sdavids-shell-misc-docker:1.2.3
...
"org.opencontainers.image.created":"1970-01-01T00:00:00Z"
...
"org.opencontainers.image.revision":"84f750065776d8748211f2fab7f58def67d2af85-next"
...
This script removes all containers, images, networks, and volumes with the label ${label_group}=${repository}
, i.e. all project-related Docker artifacts.
ℹ️
|
The related scripts will ensure the See the general notes of the Docker section. |
$ scripts/docker/docker-cleanup.sh
This script will query the health status of the running container named ${container_name}
, i.e. the project’s container.
ℹ️
|
See the general notes of the Docker section. |
$ scripts/docker/docker-health.sh
This script will display detailed information on the container named ${container_name}
, i.e. the project’s container.
ℹ️
|
See the general notes of the Docker section. |
$ scripts/docker/docker-inspect.sh
This script will remove the ${container_name}
container and any unnamed volumes associated with it, i.e. the project’s container and volumes.
The container will be stopped before removal.
ℹ️
|
See the general notes of the Docker section. |
$ scripts/docker/docker-remove.sh
This script will open a shell into the running container named ${container_name}
, i.e. the project’s container.
ℹ️
|
See the general notes of the Docker section. |
$ scripts/docker/docker-sh.sh
This script will start the ${image_name}
image with the tag local
, i.e. the project’s locally built image.
The container will be named ${container_name}
and labeled with ${label_group}=${repository}
.
ℹ️
|
See the general notes of the Docker section. |
❗
|
This script is a starting point—modify it to your project’s needs in conjunction with its Dockerfile. |
💡
|
The provided example Dockerfile will start a simple HTTP server. |
$ scripts/docker/docker-start.sh
This script will stop the ${container_name}
container, i.e. the project’s container.
ℹ️
|
See the general notes of the Docker section. |
$ scripts/docker/docker-stop.sh
This section contains scripts related to Git:
- git-author-date-initial
-
displays the initial author dates of the committed files
- git-author-date-last
-
displays the last author dates of the committed files
- git-cleanup
-
remove untracked files from the working tree and optimize a local repository
- git-get-hash
-
return the hash of the HEAD commit
- git-get-short-hash
-
return the short hash of the HEAD commit
- git-is-working-tree-clean
-
check whether the Git working tree is clean
This script will display the initial author dates of the files of the given Git repository directory ($PWD
if not given).
If you use the optional second parameter then only the author date of the given file path will be displayed.
ℹ️
|
The initial author date is the date the original author added and committed the file to the Git repository. |
💡
|
You can use this script to verify the initial publication year of your copyright statements. |
$ scripts/git/git-author-date-initial.sh /tmp/example
2022-04-16T15:59:50+02:00 a.txt
2022-04-16T15:59:50+02:00 b.txt
2022-04-16T16:00:14+02:00 c/d.txt
2023-04-16T16:00:41+02:00 e.txt
$ scripts/git/git-author-date-initial.sh /tmp/example | cut -c 1-4,26-
2022 a.txt
2022 b.txt
2022 c/d.txt
2023 e.txt
$ tree --noreport -a -I .git /tmp/example
/tmp/example
├── a.txt
├── b.txt
├── c
│ └── d.txt
└── e.txt
$ (cd /tmp/example && git --no-pager log --format=%aI --name-status)
2024-04-16T16:01:19+02:00
M a.txt
2023-04-16T16:00:41+02:00
A e.txt
2022-04-16T16:00:14+02:00
A c/d.txt
2022-04-16T15:59:50+02:00
A a.txt
A b.txt
$ (cd /tmp/example && git --no-pager log --format=%aI --name-status a.txt)
2024-04-16T16:01:19+02:00
M a.txt
2022-04-16T15:59:50+02:00
A a.txt
-
$ scripts/git/git-author-date-initial.sh /tmp/example 2022-04-16T15:59:50+02:00 a.txt 2022-04-16T15:59:50+02:00 b.txt 2022-04-16T16:00:14+02:00 c/d.txt 2023-04-16T16:00:41+02:00 e.txt $ scripts/git/git-author-date-last.sh /tmp/example 2024-04-16T16:01:19+02:00 a.txt 2022-04-16T15:59:50+02:00 b.txt 2022-04-16T16:00:14+02:00 c/d.txt 2023-04-16T16:00:41+02:00 e.txt $ scripts/git/git-author-date-initial.sh /tmp/example | cut -c 1-4,26- > initial.txt $ scripts/git/git-author-date-last.sh /tmp/example | cut -c 1-4,26- > last.txt $ diff initial.txt last.txt 1c1 < 2022 a.txt --- > 2024 a.txt
This script will display the last author dates of the files of the given Git repository directory ($PWD
if not given).
If you use the optional second parameter then only the author date of the given file path will be displayed.
ℹ️
|
The last author date is the date of the last Git status change to a committed file of a Git repository. |
💡
|
You can use this script to verify the latest publication year of your copyright statements. |
$ scripts/git/git-author-date-last.sh /tmp/example
2024-04-16T16:01:19+02:00 a.txt
2022-04-16T15:59:50+02:00 b.txt
2022-04-16T16:00:14+02:00 c/d.txt
2023-04-16T16:00:41+02:00 e.txt
$ scripts/git/git-author-date-last.sh /tmp/example | cut -c 1-4,26-
2024 a.txt
2022 b.txt
2022 c/d.txt
2023 e.txt
$ scripts/git/git-author-date-last.sh /tmp/example a.txt
2024-04-16T16:01:19+02:00 a.txt
$ tree --noreport -a -I .git /tmp/example
/tmp/example
├── a.txt
├── b.txt
├── c
│ └── d.txt
└── e.txt
$ (cd /tmp/example && git --no-pager log --format=%aI --name-status)
2024-04-16T16:01:19+02:00
M a.txt
2023-04-16T16:00:41+02:00
A e.txt
2022-04-16T16:00:14+02:00
A c/d.txt
2022-04-16T15:59:50+02:00
A a.txt
A b.txt
$ (cd /tmp/example && git --no-pager log --format=%aI --name-status a.txt)
2024-04-16T16:01:19+02:00
M a.txt
2022-04-16T15:59:50+02:00
A a.txt
-
$ scripts/git/git-author-date-initial.sh /tmp/example 2022-04-16T15:59:50+02:00 a.txt 2022-04-16T15:59:50+02:00 b.txt 2022-04-16T16:00:14+02:00 c/d.txt 2023-04-16T16:00:41+02:00 e.txt $ scripts/git/git-author-date-last.sh /tmp/example 2024-04-16T16:01:19+02:00 a.txt 2022-04-16T15:59:50+02:00 b.txt 2022-04-16T16:00:14+02:00 c/d.txt 2023-04-16T16:00:41+02:00 e.txt $ scripts/git/git-author-date-initial.sh /tmp/example | cut -c 1-4,26- > initial.txt $ scripts/git/git-author-date-last.sh /tmp/example | cut -c 1-4,26- > last.txt $ diff initial.txt last.txt 1c1 < 2022 a.txt --- > 2024 a.txt
This script will do the following:
-
remove untracked files from the working tree
-
cleanup remote branches
-
cleanup unnecessary files and optimize the local repository
It should be copied into a Git repository.
|
This script will remove all untracked files. Sometimes you have untracked files which you do not want to be cleaned up. For example:
Add them to the exclusions to ensure that they will not be removed: scripts/git-cleanup.sh
git clean -qfdx \
+ -e .env \
-e .fleet \
-e .idea \
-e .classpath \
-e .project \
-e .settings \
-e .vscode \
+ -e *.pem \
. |
ℹ️
|
By default, the metadata files of Eclipse, JetBrains IDEs, and Visual Studio Code are not removed. |
$ scripts/git/git-cleanup.sh
This script will return the hash of the HEAD commit of the checked out branch of the given Git repository directory ($PWD
if not given).
The suffix -dirty
will be appended if the working tree is dirty.
$ scripts/git/git-get-hash.sh
844881d148be35d7c0a9bcbf5ba23ab79cf14c6e
$ touch a
$ scripts/git/git-get-hash.sh
844881d148be35d7c0a9bcbf5ba23ab79cf14c6e-dirty
This script will return the short hash of the HEAD commit of the checked out branch of the given Git repository directory ($PWD
if not given).
The suffix -dirty
will be appended if the working tree is dirty.
The length of the hash can be configured via the optional second parameter (range: [4, 40] for SHA-1 object names or [4, 64] for SHA-256 object names); the default is determined by the core.abbrev
Git configuration variable.
💡
|
To get a consistent hash length across systems you should either
|
$ scripts/git/git-get-short-hash.sh
437f01f
$ scripts/git/git-get-short-hash.sh path/to/git/repository
dbd0ffb
$ scripts/git/git-get-short-hash.sh . 10
437f01f904
$ git config --local core.abbrev 20
$ scripts/git/git-get-short-hash.sh
437f01f904c1c2839408
$ touch a
$ scripts/git/git-get-short-hash.sh
437f01f904c1c2839408-dirty
This script will check whether the Git working tree in the given directory ($PWD
if not given) is clean.
$ scripts/git/git-is-working-tree-clean.sh
$ echo $?
- 0
-
the Git working tree of the given directory is clean
- 1
-
the Git working tree of the given directory is dirty
- 2
-
the given directory is not a Git repository
This section contains scripts related to Gradle:
- check-reproducible-build-gradle
-
checks whether a Gradle build produces reproducible JARs
Related: Gradle Functions
This script will check whether the Gradle build in the given directory ($PWD
if not given) produces reproducible JARs.
In case of a non-reproducible build, the output of this script will show the affected JARs:
--- .checksums/build-1 2024-03-11 03:40:49
+++ .checksums/build-2 2024-03-11 03:40:50
@@ -1,2 +1,2 @@
-62f0ce3946967ff3be58d74b68d40fd438a4cb56d9ec9d3a434b1943db92ca55 ./lib/build/libs/lib-sources.jar
-8cf6cb254443141ca847ec73c6402581e8d37bab59ceefd88926c521812c4390 ./lib/build/libs/lib.jar
+099cebb5a0d6faa8700782877f0c09ef3891bdc861636a81839dd3e7024963f5 ./lib/build/libs/lib-sources.jar
+e2d5ad0d51a030fe23f94b039e3572b54af5a35c4943eaad4e340b91edc3ab2c ./lib/build/libs/lib.jar
💡
|
Copy the script into your Gradle project: .
├── scripts
│ └── check-reproducible-build-gradle.sh
└── gradlew $ scripts/check-reproducible-build-gradle.sh |
💡
|
Here are snippets for a reproducible Gradle build: build.gradle.kts
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter.ISO_LOCAL_DATE
import java.time.format.DateTimeFormatter.ISO_OFFSET_TIME
import java.time.temporal.ChronoUnit.SECONDS
// https://reproducible-builds.org/docs/source-date-epoch/
val buildTimeAndDate: OffsetDateTime = OffsetDateTime.ofInstant(
(System.getenv("SOURCE_DATE_EPOCH") ?: "").toLongOrNull()?.let {
Instant.ofEpochSecond(it)
} ?: Instant.now().truncatedTo(SECONDS),
ZoneOffset.UTC,
)
tasks.withType<AbstractArchiveTask>().configureEach {
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
filePermissions {
unix(644)
}
dirPermissions {
unix(755)
}
}
tasks.withType<Jar>().configureEach {
manifest {
attributes(
"Build-Date" to ISO_LOCAL_DATE.format(buildTimeAndDate),
"Build-Time" to ISO_OFFSET_TIME.format(buildTimeAndDate),
)
}
} build.sh
#!/usr/bin/env sh
set -eu
# https://reproducible-builds.org/docs/source-date-epoch/#git
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git log --max-count=1 --pretty=format:%ct)}"
export SOURCE_DATE_EPOCH
./gradlew \
--configuration-cache \
--no-build-cache \
clean \
build $ env SOURCE_DATE_EPOCH="$(git log --max-count=1 --pretty=format:%ct)" ./gradlew --configuration-cache --no-build-cache clean build .github/workflows/ci.yaml
# ...
jobs:
build:
# ...
steps:
# ...
- name: Set SOURCE_DATE_EPOCH
run: |
echo "SOURCE_DATE_EPOCH=$(git log --max-count=1 --pretty=format:%ct)" >> "$GITHUB_ENV"
- name: Run build
run: ./gradlew build |
$ scripts/gradle/check-reproducible-build-gradle.sh
$ scripts/gradle/check-reproducible-build-gradle.sh /tmp/gradle-example-project
This section contains scripts related to Java:
- jar-java-versions
-
display Java and class file versions contained in a JAR
Related: Java Functions
This script will display the Java and class file versions used by the classes within the given JAR file.
If you use the optional second positive integer parameter (range: [5, n)) only non-matching versions will be displayed and if there is at least one mismatch the exit code will be 100
instead of 0
.
ℹ️
|
|
💡
|
This script is useful to verify that you have not inadvertently forgotten the release option while building your classes if you want to target a specific Java version. |
$ curl -O -s https://repo1.maven.org/maven2/org/junit/jupiter/junit-jupiter-api/5.10.2/junit-jupiter-api-5.10.2.jar
$ jar_is_multi_release junit-jupiter-api-5.10.2.jar
0
$ scripts/java/jar-java-versions.sh junit-jupiter-api-5.10.2.jar
Java Version: 8; Class File Version: 52
$ scripts/java/jar-java-versions.sh junit-jupiter-api-5.10.2.jar 8
$ echo $?
0
$ scripts/java/jar-java-versions.sh junit-jupiter-api-5.10.2.jar 11
Java Version: 8; Class File Version: 52
$ echo $?
100
$ curl -O -s https://repo1.maven.org/maven2/net/bytebuddy/byte-buddy/1.14.12/byte-buddy-1.14.12.jar
$ jar_is_multi_release byte-buddy-1.14.12.jar
1
$ scripts/java/jar-java-versions.sh byte-buddy-1.14.12.jar
Java Version: 5; Class File Version: 49
Java Version: 6; Class File Version: 50
$ scripts/java/jar-java-versions.sh byte-buddy-1.14.12.jar 5
Java Version: 6; Class File Version: 50
$ echo $?
100
This section contains scripts related to Keycloak:
- access-token
-
retrieve a Keycloak JWT access token
- access-token-decoded
-
retrieve and decode a Keycloak JWT access token
- decode-access-token
-
decode a Keycloak JWT access token
This script will retrieve a Keycloak JWT access token for the given user.
❗
|
You should change the realm, scope, and client ID: scripts/keycloak/access-token.sh
readonly realm='my-realm'
readonly realm_scope='my-realm-scope'
readonly realm_client_id='my-realm-client' Depending on your setup, you might have to change the protocol, host, port, or proxy path prefix, e.g. if your Keycloak instance is accessible at scripts/keycloak/access-token.sh
readonly keycloak_protocol='http'
readonly keycloak_host='localhost'
readonly keycloak_port=9050
readonly keycloak_proxy_path_prefix='/keycloak' |
$ scripts/keycloak/access-token.sh my-user
Password:
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJhSGJ2MFdqT2RsR19wM1BEb0ZvLU1KQ3NuWEk0Ny0xOGdhTjcycndkTnlBIn0.eyJleHAiOjE3MDY0NzI0MTIsImlhdCI6MTcwNjQ3MjExMiwianRpIjoiY2FhZGZhNjUtNWQ5NC00YTk2LWE3YmYtNGI3ODFlY2NjZjlkIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9teS1yZWFsbSIsInN1YiI6ImMxYmYwOTRmLWIzOTctNGYxMy05Y2VhLTUyYTdjYmNlNjRkMCIsInR5cCI6IkJlYXJlciIsImF6cCI6Im15LXJlYWxtLWNsaWVudCIsInNlc3Npb25fc3RhdGUiOiI0NWYyMzE2YS01ZjNiLTRkYzMtYmRiYy0yZmRjYThjODA1NGQiLCJhbGxvd2VkLW9yaWdpbnMiOlsiLyoiXSwic2NvcGUiOiJteS1yZWFsbS1zY29wZSIsInNpZCI6IjQ1ZjIzMTZhLTVmM2ItNGRjMy1iZGJjLTJmZGNhOGM4MDU0ZCJ9.TDGa-i6ipWmxnfFMOehc2j86p3oa5laNlytBc5PFcJeyfgNOYc7SLJZo5OCV7pVyz4VHiv8BKkG2JI56Usg_1fmP-GtFjPojWjf7gQ5FgtncL7RxTKzPtzDQiYRvqS6agHzfd_Q2zP91NVxhU7_-rKnqV3O5Ka8x5qxEaqwvwsT1aZP5KhNDS8haRlOLLSRmTB5Nx2OZSkms6Aok4NGr461xEXu_bxFzbnlLOndG7frbQyY272Oyo6ahtClxbj414tlEsdUMzE8MApPdsWVtW7afMgKBOXyn25RJck7yoHoLgT9pfe9j32aR6syYUaSfSU-ODdCUhxFMZ7lfaFvREA
This script will retrieve a Keycloak JWT access token for the given user and decode it.
ℹ️
|
This script combines access-token and decode-access-token. |
$ scripts/keycloak/access-token-decoded.sh my-user
Password:
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJhSGJ2MFdqT2RsR19wM1BEb0ZvLU1KQ3NuWEk0Ny0xOGdhTjcycndkTnlBIn0.eyJleHAiOjE3MDY0NzIzNDksImlhdCI6MTcwNjQ3MjA0OSwianRpIjoiNDgyMTAxM2MtYjQ0NC00MjM2LWFkOTUtOWM2MmQyNzc4OGFlIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9teS1yZWFsbSIsInN1YiI6ImMxYmYwOTRmLWIzOTctNGYxMy05Y2VhLTUyYTdjYmNlNjRkMCIsInR5cCI6IkJlYXJlciIsImF6cCI6Im15LXJlYWxtLWNsaWVudCIsInNlc3Npb25fc3RhdGUiOiI0MGM2YjdlZi02MjBlLTQ0MGYtOTQ0Mi05Nzc0MWYyYjhkMjMiLCJhbGxvd2VkLW9yaWdpbnMiOlsiLyoiXSwic2NvcGUiOiJteS1yZWFsbS1zY29wZSIsInNpZCI6IjQwYzZiN2VmLTYyMGUtNDQwZi05NDQyLTk3NzQxZjJiOGQyMyJ9.EOEaOq_HFsQ8_yAPu-zszw2dOM0gS7cUNRhXmKdnGlD1TFVA33rT2cUiXnVVGNGtXXcIbghp3uCSZLUwYrGwDPUnYJbrNycPsPy6iah07oUaakEhsTnYqGmdYgXVw9T7Q2xoGhwtD5_hpgwwvkHCMBbJ8tZBefDXzy1nCS2rzJCgVsZylvfGMPwHO5gAQr5RYrD1o_9TTPLTjDPNtCvYXp1MaVat7fqibiH_ioXFAm2NxIIOrwVGRZH5jW1rdX6gURjoyfYXi9w56SVbzIh4lgZI48rnnxHjRLop8ZuWFcmtx6ykY45MtMFUCE6gNTZFgJmTlYLGQIe9tYmO6Kngow
{
"alg": "RS256",
"typ": "JWT",
"kid": "aHbv0WjOdlG_p3PDoFo-MJCsnXI47-18gaN72rwdNyA"
}
{
"exp": 1706472349,
"iat": 1706472049,
"jti": "4821013c-b444-4236-ad95-9c62d27788ae",
"iss": "http://localhost:8080/realms/my-realm",
"sub": "c1bf094f-b397-4f13-9cea-52a7cbce64d0",
"typ": "Bearer",
"azp": "my-realm-client",
"session_state": "40c6b7ef-620e-440f-9442-97741f2b8d23",
"allowed-origins": [
"/*"
],
"scope": "my-realm-scope",
"sid": "40c6b7ef-620e-440f-9442-97741f2b8d23"
}
This script will decode the given Keycloak JWT access token.
ℹ️
|
|
💡
|
Online JWT Decoder |
$ scripts/keycloak/decode-access-token.sh eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJhSGJ2MFdqT2RsR19wM1BEb0ZvLU1KQ3NuWEk0Ny0xOGdhTjcycndkTnlBIn0.eyJleHAiOjE3MDY0NzI0MTIsImlhdCI6MTcwNjQ3MjExMiwianRpIjoiY2FhZGZhNjUtNWQ5NC00YTk2LWE3YmYtNGI3ODFlY2NjZjlkIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9teS1yZWFsbSIsInN1YiI6ImMxYmYwOTRmLWIzOTctNGYxMy05Y2VhLTUyYTdjYmNlNjRkMCIsInR5cCI6IkJlYXJlciIsImF6cCI6Im15LXJlYWxtLWNsaWVudCIsInNlc3Npb25fc3RhdGUiOiI0NWYyMzE2YS01ZjNiLTRkYzMtYmRiYy0yZmRjYThjODA1NGQiLCJhbGxvd2VkLW9yaWdpbnMiOlsiLyoiXSwic2NvcGUiOiJteS1yZWFsbS1zY29wZSIsInNpZCI6IjQ1ZjIzMTZhLTVmM2ItNGRjMy1iZGJjLTJmZGNhOGM4MDU0ZCJ9.TDGa-i6ipWmxnfFMOehc2j86p3oa5laNlytBc5PFcJeyfgNOYc7SLJZo5OCV7pVyz4VHiv8BKkG2JI56Usg_1fmP-GtFjPojWjf7gQ5FgtncL7RxTKzPtzDQiYRvqS6agHzfd_Q2zP91NVxhU7_-rKnqV3O5Ka8x5qxEaqwvwsT1aZP5KhNDS8haRlOLLSRmTB5Nx2OZSkms6Aok4NGr461xEXu_bxFzbnlLOndG7frbQyY272Oyo6ahtClxbj414tlEsdUMzE8MApPdsWVtW7afMgKBOXyn25RJck7yoHoLgT9pfe9j32aR6syYUaSfSU-ODdCUhxFMZ7lfaFvREA
{
"alg": "RS256",
"typ": "JWT",
"kid": "aHbv0WjOdlG_p3PDoFo-MJCsnXI47-18gaN72rwdNyA"
}
{
"exp": 1706472412,
"iat": 1706472112,
"jti": "caadfa65-5d94-4a96-a7bf-4b781ecccf9d",
"iss": "http://localhost:8080/realms/my-realm",
"sub": "c1bf094f-b397-4f13-9cea-52a7cbce64d0",
"typ": "Bearer",
"azp": "my-realm-client",
"session_state": "45f2316a-5f3b-4dc3-bdbc-2fdca8c8054d",
"allowed-origins": [
"/*"
],
"scope": "my-realm-scope",
"sid": "45f2316a-5f3b-4dc3-bdbc-2fdca8c8054d"
}
This section contains scripts related to Node.js:
- clean-node
-
delete
node_modules
andpackage-lock.json
- preinstall
-
exclude
node_modules
from Time Machine backups and Spotlight indexing
This script will delete both the node_modules
directory and the package-lock.json
file in the given directory ($PWD
if not given).
This is useful to get a clean slate after dependency updates.
💡
|
Copy the script into your Node.js project and add it as a custom script to your package.json
{
...
"scripts": {
"clean:node": "scripts/clean-node.sh"
}
} $ npm run clean:node
$ npm i |
$ scripts/nodejs/clean-node.sh
$ scripts/nodejs/clean-node.sh /tmp/nodejs-example-project
This script will exclude the node_modules
directory from Time Machine backups and prevent its Spotlight indexing.
-
Copy the script to your Node.js project.
-
Register the script as a
preinstall
life cycle script:package.json{ ... "scripts": { "preinstall": "scripts/preinstall.sh" } }
This section contains scripts related to Web development:
- compress-brotli
-
compress a file with brotli
- compress-gzip
-
compress a file with gzip
- compress-zstd
-
compress a file with zstd
- create-build-info-js
-
create a JavaScript build information file
- create-build-info-json
-
create a JSON build information file
- create-build-info-ts
-
create a TypeScript build information file
- minify-css
-
minify CSS files
- minify-gif
-
minify GIF files
- minify-html
-
minify HTML files
- minify-jpeg
-
minify JPEG files
- minify-json
-
minify JSON files
- minify-json-tags
-
minify JSON-structured script tags
- minify-png
-
minify PNG files
- minify-robots
-
minify the robots.txt file
- minify-svg
-
minify SVG files
- minify-traffic-advice
-
minify the private prefetch proxy traffic control file
- minify-webmanifest
-
minify the web application manifest
- minify-xml
-
minify XML files
This script will compress the given file with brotli.
ℹ️
|
|
💡
|
Here is a fragment to be placed into your
|
$ scripts/web/compress-brotli.sh test.txt
$ find dist \( -type f -name '*.html' -o -name '*.css' \) -exec scripts/web/compress-brotli.sh {} ';'
This script will compress the given file with gzip.
💡
|
Here is a fragment to be placed into your
|
$ scripts/web/compress-gzip.sh test.txt
$ find dist \( -type f -name '*.html' -o -name '*.css' \) -exec scripts/web/compress-gzip.sh {} ';'
This script will compress the given file with zstd.
ℹ️
|
|
💡
|
Here is a fragment to be placed into your
|
$ scripts/web/compress-zstd.sh test.txt
$ find dist \( -type f -name '*.html' -o -name '*.css' \) -exec scripts/web/compress-zstd.sh {} ';'
This script will create a file with the given name containing build information accessible by JavaScript code.
ℹ️
|
The value of
|
ℹ️
|
The value of |
$ scripts/web/create-build-info-js.sh src/build-info.mjs
⇓
export const buildInfo = {
build: {
id: '1710116787',
time: '2024-03-11T00:26:27Z',
},
git: {
branch: 'main',
commit: {
id: '4768a3cf26cecc00a23be6acdf430809e4bb67a7',
time: '2024-03-11T00:25:48Z',
},
},
};
This script will create a JSON file with the given name containing build information.
ℹ️
|
The value of
|
ℹ️
|
The value of |
$ scripts/web/create-build-info-json.sh src/build-info.json
⇓
{"build":{"id":"1710116654","time":"2024-03-11T00:24:14Z"},"git":{"branch":"main","commit":{"id":"b530d501d059e1bbda58d96d78359014effa5584","time":"2024-03-11T00:22:45Z"}}}
This script will create a file with the given name containing build information accessible by TypeScript code.
ℹ️
|
The value of
|
ℹ️
|
The value of |
$ scripts/web/create-build-info-ts.sh src/build-info.ts
⇓
export type BuildInfo = {
// ...
};
export const buildInfo: BuildInfo = {
build: {
id: '1710116078',
time: '2024-03-11T00:14:38Z',
},
git: {
branch: 'main',
commit: {
id: '95189bb08fa918576f10339eb15303d152ade2aa',
time: '2024-03-10T23:52:54Z',
},
},
};
This script will minify and transpile the *.css
files in the given directory ($PWD
if not given) and its subdirectories.
This script uses browserslist to determine the transpilation targets.
ℹ️
|
|
💡
|
If you do not want the defaults you have several options to change them. For example via the following file: .browserslistrc
|
$ scripts/web/minify-css.sh
$ scripts/web/minify-css.sh dist
This script will minify the *.gif
files in the given directory ($PWD
if not given) and its subdirectories.
ℹ️
|
|
💡
|
If you are using macOS you might want to use ImageOptim instead of using this script. |
💡
|
It is advisable to minimize image files before adding them to a Git repository. Minimizing image files during a build is usually bad idea unless the build generates images files. Also, you might want to add a hash to the minified image file before adding it to a Git repository. |
$ scripts/web/minify-gif.sh
$ scripts/web/minify-gif.sh dist
This script will minify the *.html
files in the given directory ($PWD
if not given) and its subdirectories.
ℹ️
|
|
$ scripts/web/minify-html.sh
$ scripts/web/minify-html.sh dist
This script will minify the *.jpg
and *.jpeg
files in the given directory ($PWD
if not given) and its subdirectories.
ℹ️
|
|
💡
|
If you are using macOS you might want to use ImageOptim instead of using this script. |
💡
|
It is advisable to minimize image files before adding them to a Git repository. Minimizing image files during a build is usually bad idea unless the build generates images files. Also, you might want to add a hash to the minified image file before adding it to a Git repository. |
$ scripts/web/minify-jpeg.sh
$ scripts/web/minify-jpeg.sh dist
This script will minify the *.json
files in the given directory ($PWD
if not given) and its subdirectories.
ℹ️
|
|
$ scripts/web/minify-json.sh
$ scripts/web/minify-json.sh dist
This script will minify JSON-structured script tags in the given HTML file.
<html>
…
<script type="importmap">
{
"imports": {
"utils": "/j/utils.mjs"
}
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"url": "https://sdavids.de/"
}
</script>
…
</html>
⇓
<html>
…
<script type="importmap">{"imports":{"utils":"/j/utils.mjs"}}</script>
<script type="application/ld+json">{"@context":"https://schema.org","@type":"Organization","url":"https://sdavids.de/"}</script>
…
</html>
ℹ️
|
Afterward, you need to install the dependencies of this script: $ npm i --save-dev domutils dom-serializer htmlparser2 |
$ scripts/web/minify-json-tags.mjs dist/index.html
$ find dist -type f -name '*.html' -exec scripts/web/minify-json-tags.mjs {} ';'
This script will minify the *.png
files in the given directory ($PWD
if not given) and its subdirectories.
💡
|
If you are using macOS you might want to use ImageOptim instead of using this script. |
💡
|
It is advisable to minimize image files before adding them to a Git repository. Minimizing image files during a build is usually bad idea unless the build generates images files. Also, you might want to add a hash to the minified image file before adding it to a Git repository. |
$ scripts/web/minify-png.sh
$ scripts/web/minify-png.sh dist
This script will minify the robots.txt
file in the given directory ($PWD
if not given).
$ scripts/web/minify-html.sh
$ scripts/web/minify-robots.sh dist
This script will minify the *.svg
files in the given directory ($PWD
if not given) and its subdirectories.
ℹ️
|
|
💡
|
If you are using macOS you might want to use ImageOptim instead of using this script. |
💡
|
It is advisable to minimize image files before adding them to a Git repository. Minimizing image files during a build is usually bad idea unless the build generates images files. Also, you might want to add a hash to the minified image file before adding it to a Git repository. |
$ scripts/web/minify-svg.sh
$ scripts/web/minify-svg.sh dist
This script will minify the private prefetch proxy traffic control file.
ℹ️
|
|
$ scripts/web/minify-traffic-advice.sh dist/.well-known/traffic-advice
This script will minify the given web application manifest file.
ℹ️
|
|
$ scripts/web/minify-webmanifest.sh dist/site.webmanifest
This script will minify the *.xml
files in the given directory ($PWD
if not given) and its subdirectories.
ℹ️
|
|
$ scripts/web/minify-xml.sh
$ scripts/web/minify-xml.sh dist
The functions need to be copied into an $FPATH directory.
❗
|
The filename needs to match the name of the function. |
💡
|
Example zsh setup: $ mkdir ~/.zfunc ~/.zshrc
readonly ext_func="${HOME}/.zfunc"
export FPATH="${ext_func}:${FPATH}"
for f in ${ext_func}; do
# shellcheck disable=SC2046
autoload -Uz $(ls "${f}")
done The functions should be copied into |
This section contains generally useful functions:
- color_stderr
-
color errors red
- ls_extensions
-
displays all file extensions
This function will display stderr output in red.
#!/usr/bin/env sh
echo 'error' >&2
$ color_stderr ./with-stderr-output.sh
error
ℹ️
|
GitHub unfortunately does not show the "error" above in red. |
This function will display all file extensions (case-insensitive) and their count in the given directory ($PWD
if not given) and its subdirectories.
$ ls_extensions
5 sh
$ ls_extensions /tmp/example
3 txt
1 png
$ tree --noreport -a /tmp/example
/tmp/example
├── a.b.txt
├── a.txt
├── b.TXT
└── d
├── .ignored
└── e.png
This section contains functions related to Git:
- ls_extensions_git
-
display all file extensions for tracked files
This function will display all file extensions (case-insensitive) of tracked files and their count in the given Git directory ($PWD
if not given) and its subdirectories.
💡
|
This script, in conjunction with the ls_extensions script, is helpful in determining whether you have covered your files properly in your .gitattributes file. $ tree --noreport -a -I .git .
.
├── gradle
│ └── wrapper
│ └── gradle-wrapper.jar
└── gradlew.bat
$ ls_extensions
1 jar
1 bat
$ git check-attr -a gradlew.bat (1)
$ git check-attr -a gradle/wrapper/gradle-wrapper.jar
$ printf '*.bat text eol=crlf\n*.jar binary\n' > .gitattributes (2)
$ cat .gitattributes
*.bat text eol=crlf
*.jar binary
$ git check-attr -a gradlew.bat
gradlew.bat: text: set
gradlew.bat: eol: crlf
$ git check-attr -a gradle/wrapper/gradle-wrapper.jar
gradle/wrapper/gradle-wrapper.jar: binary: set
gradle/wrapper/gradle-wrapper.jar: diff: unset
gradle/wrapper/gradle-wrapper.jar: merge: unset
gradle/wrapper/gradle-wrapper.jar: text: unset
$ ls_extensions_git (3)
$ git add gradlew.bat gradle/wrapper/gradle-wrapper.jar (4)
$ ls_extensions_git (5)
1 jar
1 bat |
-
Both
gradlew.bat
andgradle-wrapper.jar
have no attributes set—if we would add them to the Git index at this point they would not be handled properly by Git. -
Add the appropriate attributes for JAR and Windows batch files.
-
Nothing has been added to the Git index yet: So
ls_extensions_git
shows no file extensions. -
Add both files to the Git index.
-
Both file extensions will be reported once they are in the Git index.
$ ls_extensions_git
5 sh
$ ls_extensions_git /tmp/example
3 txt
1 png
$ tree --noreport -a -I .git /tmp/example
/tmp/example
├── a.b.txt
├── a.txt
├── b.TXT
├── d
│ ├── .ignored
│ └── e.png
└── out.txt
$ git ls-files
a.b.txt
a.txt
b.txt
d/.ignored
d/e.png
This section contains functions related to GitHub CLI:
- repo_new_gh
-
create and checkout a private GitHub repository
- repo_new_local
-
create a new local repository based on a GitHub template repository
This function will create and checkout a new private GitHub repository from a GitHub template repository with the given name.
❗
|
You should change the template being used: zfunc/repo_new_gh
- readonly template='sdavids/sdavids-project-template'
+ readonly template='my-github-user/my-template' |
ℹ️
|
|
ℹ️
|
This script uses Git commit signing; you need to:
Alternatively, you can remove zfunc/repo_new_gh
git commit \
--quiet \
- --gpg-sign \
--signoff \ |
$ repo_new_gh my-new-repo
This function will create a new local repository based on a GitHub template repository with the given name.
❗
|
This function needs the GitHub $ gh auth refresh -h github.com -s delete_repo |
❗
|
You should change the GitHub user and template being used: zfunc/repo_new_local
- readonly template='sdavids/sdavids-project-template'
+ readonly template='my-github-user/my-template'
- readonly gh_user_id='sdavids'
+ readonly gh_user_id='my-github-user' |
ℹ️
|
|
ℹ️
|
This script uses Git commit signing; you need to configure your local git config. Alternatively, you can remove zfunc/repo_new_local
git commit \
--quiet \
- --gpg-sign \
--signoff \ |
$ repo_new_local my-new-local-repo
This section contains functions related to Java:
- jar_is_multi_release
-
display whether a JAR is a multi-release JAR
- jar_manifest
-
display the manifest of a JAR
This function will display whether the given JAR file is a multi-release JAR file (1
) or not (0
).
ℹ️
|
The exit code of this function is the inverse of the displayed value. |
$ curl -O -s https://repo1.maven.org/maven2/org/junit/jupiter/junit-jupiter-api/5.10.2/junit-jupiter-api-5.10.2.jar
$ jar_is_multi_release junit-jupiter-api-5.10.2.jar
0
$ echo $?
1
$ curl -O -s https://repo1.maven.org/maven2/net/bytebuddy/byte-buddy/1.14.12/byte-buddy-1.14.12.jar
$ jar_is_multi_release byte-buddy-1.14.12.jar
1
$ echo $?
0
$ jar_manifest byte-buddy-1.14.12.jar | grep Multi
Multi-Release: true
This function will display the manifest of the given JAR file.
$ jar_manifest apiguardian-api-1.1.2.jar
Manifest-Version: 1.0
Bnd-LastModified: 1624798392241
Build-Date: 2021-06-27
Build-Revision: aa952a1b9d5b4e9cc0af853e2c140c2455b397be
Build-Time: 14:53:10.089+0200
Built-By: @API Guardian Team
Bundle-Description: @API Guardian
Bundle-DocURL: https://github.com/apiguardian-team/apiguardian
Bundle-ManifestVersion: 2
Bundle-Name: apiguardian-api
Bundle-SymbolicName: org.apiguardian.api
Bundle-Vendor: apiguardian.org
Bundle-Version: 1.1.2
Created-By: 11.0.11 (AdoptOpenJDK)
Export-Package: org.apiguardian.api;version="1.1.2"
Implementation-Title: apiguardian-api
Implementation-Vendor: apiguardian.org
Implementation-Version: 1.1.2
Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.6))"
Specification-Title: apiguardian-api
Specification-Vendor: apiguardian.org
Specification-Version: 1.1.2
Tool: Bnd-5.3.0.202102221516
This section contains functions related to Gradle:
- gradle_new_java_library
-
creates a new Gradle Java library project with sensible, modern defaults
This function will create a new Gradle Java library project with sensible, modern defaults and the given name.
The optional second parameter is the directory ($PWD
if not given) the project is created in.
ℹ️
|
|
ℹ️
|
A Git repository will also be initialized for the project if This script uses Git commit signing; you need to:
Alternatively, you can remove zfunc/gradle_new_java_library
git commit \
--quiet \
- --gpg-sign \
--signoff \ |
💡
|
The generated default package will be You can change the default by adding printf 'org.gradle.buildinit.source.package=my.org' >> "${GRADLE_USER_HOME:=${HOME}}/gradle.properties" |
💡
|
You might want to customize the defaults for the created zfunc/gradle_new_java_library
cat << 'EOF' > .editorconfig
# http://EditorConfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
+ trim_trailing_whitespace = true
+ indent_style = space
+ indent_size = 2
+ max_line_length = 80
+
+ [*.md]
+ trim_trailing_whitespace = false
+ max_line_length = off
+
+ [*.properties]
+ max_line_length = off
+
+ [*.{java,kts}]
+ max_line_length = 100
EOF |
$ gradle_new_java_library example-java-library
$ gradle_new_java_library other-java-library /tmp
$ tree --noreport .
.
├── gradle
│ ├── libs.versions.toml
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── lib
│ ├── build.gradle.kts
│ └── src
│ ├── main
│ │ ├── java
│ │ │ └── org
│ │ │ └── example
│ │ │ └── Library.java
│ │ └── resources
│ └── test
│ ├── java
│ │ └── org
│ │ └── example
│ │ └── LibraryTest.java
│ └── resources
└── settings.gradle.kts
$ git status
On branch main
nothing to commit, working tree clean
Apache License, Version 2.0 (Apache-2.0.txt or https://www.apache.org/licenses/LICENSE-2.0).
We abide by the Contributor Covenant, Version 2.1 and ask that you do as well.
For more information, please see CODE_OF_CONDUCT.adoc.
❗
|
After initializing the repository you need to install the Git hooks via: $ git config core.hooksPath .githooks |
$ sudo apt-get install curl
Install Docker.
There are several different JDKs and multiple options of installing them.
The recommended way is to install via SDKMAN!.
First install a JDK.
There are multiple options of installing Gradle.
Recommended:
-
Homebrew
$ brew install gradle
if command -v fnm > /dev/null 2>&1; then
eval "$(fnm env --use-on-cd)"
fi
export NVM_DIR="${HOME}/.nvm"
[ -s "${NVM_DIR}/nvm.sh" ] && . "${NVM_DIR}/nvm.sh"
[ -s "${NVM_DIR}/bash_completion" ] && . "${NVM_DIR}/bash_completion"
if command -v nvm > /dev/null 2>&1; then
autoload -U add-zsh-hook
load-nvmrc() {
local nvmrc_path="$(nvm_find_nvmrc)"
if [ -n "${nvmrc_path}" ]; then
local nvmrc_node_version=$(nvm version "$(cat "${nvmrc_path}")")
if [ "${nvmrc_node_version}" = "N/A" ]; then
nvm install
elif [ "${nvmrc_node_version}" != "$(nvm version)" ]; then
nvm use
fi
elif [ -n "$(PWD=$OLDPWD nvm_find_nvmrc)" ] && [ "$(nvm version)" != "$(nvm version default)" ]; then
echo "Reverting to nvm default version"
nvm use default
fi
}
add-zsh-hook chpwd load-nvmrc
load-nvmrc
fi