Skip to content

Miscellaneous shell-related scripts and functions

Notifications You must be signed in to change notification settings

sdavids/sdavids-shell-misc

Repository files navigation

sdavids-shell-misc

Apache License Contributor Covenant Code Style: Google OSS Lifecycle Maintenance GitHub last commit Resolution Time Open Issues

Table of Contents

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.

toggle.sh
#!/usr/bin/env sh
scripts/general/counter.sh toggle 1 1>/dev/null
if [ $? -eq 100 ]; then
  echo 'on'
else
  echo 'off'
fi
retry.sh
#!/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"
  1. loop

    $ 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.txttest.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.

with-exit-condition.sh
#!/usr/bin/env sh
if [ ... ]; then
  exit 0 # finish loop
fi
infinite.sh
#!/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
  1. counter

    $ 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.

ℹ️

shellcheck needs to be installed.

💡

If you copy this script into a Node.js-based project you should exclude the node_modules directory:

find … -name '.sh' -not -path '/node_modules/*' -print0 …

$ 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 key.pem and cert.pem should not be checked into version control!

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 file:

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":

self signed macos
ℹ️

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":

self signed firefox
$ 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 file:

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…​":

self signed firefox delete 1

Click on the "Servers" tab:

self signed firefox delete 2

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 file:

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 Docker to be installed.

You should modify the container_name, label_group, namespace, and repository shell variables in the copied scripts—​the values need to match in all scripts:

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 ${namespace}/${repository} having a label ${label_group}=${repository}.

The scripts expect the container to be named ${container_name}.

💡

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 with latest

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 ${label_group}=${repository} label has been set.

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
  1. git-author-date-last

    $ 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
  1. git-author-date-initial

    $ 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:

  • .env or .envrc files

  • *.crt, *.pem or *.key self-signed certificate files

  • IDE metadata

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

  1. ensure that core.abbrev is set on the repository after initialization:

    $ git config --local core.abbrev 20

    Unfortunately, these settings are not under version control.

  2. explicitly set the length when invoking the script:

    $ scripts/git/git-get-short-hash.sh . 20
$ 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.

ℹ️

javap needs to be installed; it is supplied with a JDK.

💡

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 http://localhost:9050/keycloak you should adjust the script as follows:

scripts/keycloak/access-token.sh
readonly keycloak_protocol='http'
readonly keycloak_host='localhost'
readonly keycloak_port=9050
readonly keycloak_proxy_path_prefix='/keycloak'
ℹ️

curl needs to be installed.

jq needs to be installed.

$ 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.

ℹ️

jq needs to be installed.

💡

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 and package-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 file:

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.

  1. Copy the script to your Node.js project.

  2. 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.

ℹ️

brotli needs to be installed.

💡

Here is a fragment to be placed into your .htaccess or Apache HTTPD server configuration file:

<IfModule mod_headers.c>
  RewriteCond "%{HTTP:Accept-encoding}" "br"
  RewriteCond "%{REQUEST_FILENAME}.br" -s
  RewriteRule "^(.*)\.(css|html|js|mjs|svg)$" "/$1.$2.br" [QSA]

  RewriteRule "\.css\.br$" "-" [T=text/css,E=no-brotli:1,E=no-gzip:1,E=no-zstd:1]
  RewriteRule "\.html\.br$" "-" [T=text/html,E=no-brotli:1,E=no-gzip:1,E=no-zstd:1]
  RewriteRule "\.js\.br$" "-" [T=text/javascript,E=no-brotli:1,E=no-gzip:1,E=no-zstd:1]
  RewriteRule "\.mjs\.br$" "-" [T=text/javascript,E=no-brotli:1,E=no-gzip:1,E=no-zstd:1]
  RewriteRule "\.svg\.br$" "-" [T=text/javascript,E=no-brotli:1,E=no-gzip:1,E=no-zstd:1]

  <FilesMatch "(\.css|\.html|\.js|\.mjs|\.svg)\.br$">
    Header append Content-Encoding br
    Header append Vary Accept-Encoding
  </FilesMatch>
</IfModule>
$ 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 .htaccess or Apache HTTPD server configuration file:

<IfModule mod_headers.c>
  RewriteCond "%{HTTP:Accept-encoding}" "gzip"
  RewriteCond "%{REQUEST_FILENAME}.gz" -s
  RewriteRule "^(.*)\.(css|html|js|mjs|svg)$" "/$1.$2.gz" [QSA]

  RewriteRule "\.css\.gz$" "-" [T=text/css,E=no-brotli:1,E=no-gzip:1,E=no-zstd:1]
  RewriteRule "\.html\.gz$" "-" [T=text/html,E=no-brotli:1,E=no-gzip:1,E=no-zstd:1]
  RewriteRule "\.js\.gz$" "-" [T=text/javascript,E=no-brotli:1,E=no-gzip:1,E=no-zstd:1]
  RewriteRule "\.mjs\.gz$" "-" [T=text/javascript,E=no-brotli:1,E=no-gzip:1,E=no-zstd:1]
  RewriteRule "\.svg\.gz$" "-" [T=text/javascript,E=no-brotli:1,E=no-gzip:1,E=no-zstd:1]

  <FilesMatch "(\.css|\.html|\.js|\.mjs|\.svg)\.gz$">
    Header append Content-Encoding gzip
    Header append Vary Accept-Encoding
  </FilesMatch>
</IfModule>
$ 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.

ℹ️

zstd needs to be installed.

💡

Here is a fragment to be placed into your .htaccess or Apache HTTPD server configuration file:

<IfModule mod_headers.c>
  RewriteCond "%{HTTP:Accept-encoding}" "zstd"
  RewriteCond "%{REQUEST_FILENAME}.zst" -s
  RewriteRule "^(.*)\.(css|html|js|mjs|svg)$" "/$1.$2.zst" [QSA]

  RewriteRule "\.css\.zst$" "-" [T=text/css,E=no-brotli:1,E=no-gzip:1,E=no-zstd:1]
  RewriteRule "\.html\.zst$" "-" [T=text/html,E=no-brotli:1,E=no-gzip:1,E=no-zstd:1]
  RewriteRule "\.js\.zst$" "-" [T=text/javascript,E=no-brotli:1,E=no-gzip:1,E=no-zstd:1]
  RewriteRule "\.mjs\.zst$" "-" [T=text/javascript,E=no-brotli:1,E=no-gzip:1,E=no-zstd:1]
  RewriteRule "\.svg\.zst$" "-" [T=text/javascript,E=no-brotli:1,E=no-gzip:1,E=no-zstd:1]

  <FilesMatch "(\.css|\.html|\.js|\.mjs|\.svg)\.zst$">
    Header append Content-Encoding zstd
    Header append Vary Accept-Encoding
  </FilesMatch>
</IfModule>
$ 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 build.id is depending on where this script is run:

locally

the current timestamp

AppVeyor

the value of the APPVEYOR_BUILD_ID environment variable

Bitbucket

the value of the BITBUCKET_BUILD_NUMBER environment variable

CircleCI

the value of the CIRCLE_WORKFLOW_ID environment variable

GitHub

the value of the GITHUB_RUN_ID environment variable

GitLab

the value of the CI_PIPELINE_ID environment variable

Jenkins

the value of the BUILD_ID environment variable

TeamCity

the value of the BUILD_NUMBER environment variable

Travis

the value of the TRAVIS_BUILD_ID environment variable

ℹ️

The value of build.time is either the value of the SOURCE_DATE_EPOCH environment variable or the current timestamp.

$ scripts/web/create-build-info-js.sh src/build-info.mjs

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 build.id is depending on where this script is run:

locally

the current timestamp

AppVeyor

the value of the APPVEYOR_BUILD_ID environment variable

Bitbucket

the value of the BITBUCKET_BUILD_NUMBER environment variable

CircleCI

the value of the CIRCLE_WORKFLOW_ID environment variable

GitHub

the value of the GITHUB_RUN_ID environment variable

GitLab

the value of the CI_PIPELINE_ID environment variable

Jenkins

the value of the BUILD_ID environment variable

TeamCity

the value of the BUILD_NUMBER environment variable

Travis

the value of the TRAVIS_BUILD_ID environment variable

ℹ️

The value of build.time is either the value of the SOURCE_DATE_EPOCH environment variable or the current timestamp.

$ scripts/web/create-build-info-json.sh src/build-info.json

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 build.id is depending on where this script is run:

locally

the current timestamp

AppVeyor

the value of the APPVEYOR_BUILD_ID environment variable

Bitbucket

the value of the BITBUCKET_BUILD_NUMBER environment variable

CircleCI

the value of the CIRCLE_WORKFLOW_ID environment variable

GitHub

the value of the GITHUB_RUN_ID environment variable

GitLab

the value of the CI_PIPELINE_ID environment variable

Jenkins

the value of the BUILD_ID environment variable

TeamCity

the value of the BUILD_NUMBER environment variable

Travis

the value of the TRAVIS_BUILD_ID environment variable

ℹ️

The value of build.time is either the value of the SOURCE_DATE_EPOCH environment variable or the current timestamp.

$ scripts/web/create-build-info-ts.sh src/build-info.ts

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.

ℹ️

npx needs to be installed.

💡

If you do not want the defaults you have several options to change them.

For example via the following file:

.browserslistrc
last 2 versions
$ 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.

ℹ️

gifsicle needs to be installed.

💡

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.

ℹ️

npx needs to be installed.

$ 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.

ℹ️

jpegoptim needs to be installed.

💡

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.

ℹ️

jq needs to be installed.

$ 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>
ℹ️

npm needs to be installed.

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.

ℹ️

This script will invoke optipng and/or oxipng; therefore install optipng and/or oxipng.

💡

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.

ℹ️

npx needs to be installed.

💡

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
ℹ️

jq needs to be installed.

$ scripts/web/minify-traffic-advice.sh dist/.well-known/traffic-advice

This script will minify the given web application manifest file.

ℹ️

jq needs to be installed.

$ 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.

ℹ️

npx needs to be installed.

$ 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 ~/.zfunc.

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.

with-stderr-output.sh
#!/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
  1. Both gradlew.bat and gradle-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.

  2. Add the appropriate attributes for JAR and Windows batch files.

  3. Nothing has been added to the Git index yet: So ls_extensions_git shows no file extensions.

  4. Add both files to the Git index.

  5. 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'
ℹ️

GitHub CLI needs to be installed.

ℹ️

This script uses Git commit signing; you need to:

Alternatively, you can remove --gpg-sign:

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 delete_repo permission.

$ 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'
ℹ️

GitHub CLI needs to be installed.

ℹ️

This script uses Git commit signing; you need to configure your local git config.

Alternatively, you can remove --gpg-sign:

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.

ℹ️

gradle needs to be installed.

ℹ️

A Git repository will also be initialized for the project if git is installed.

This script uses Git commit signing; you need to:

Alternatively, you can remove --gpg-sign:

zfunc/gradle_new_java_library
  git commit \
    --quiet \
-   --gpg-sign \
    --signoff \
💡

The generated default package will be org.example.

You can change the default by adding org.gradle.buildinit.source.package to your gradle.properties:

printf 'org.gradle.buildinit.source.package=my.org' >> "${GRADLE_USER_HOME:=${HOME}}/gradle.properties"
💡

You might want to customize the defaults for the created gradle.properties, .gitignore, .gitattributes, or .editorconfig, e.g.:

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

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 brotli
$ brew install brotli
$ sudo apt-get install curl
$ sudo apt install gifsicle
$ brew install gifsicle

Install GitHub CLI.

$ brew install gh

Install hadolint.

$ brew install hadolint

There are several different JDKs and multiple options of installing them.

$ sudo apt install jpegoptim
$ brew install jpegoptim
$ sudo apt-get install jq
$ brew install jq

First install a JDK.

There are multiple options of installing Gradle.

Recommended:

  • SDKMAN!

  • Homebrew

    $ brew install gradle

Install fnm or NVM.

~/.zprofile
if command -v fnm > /dev/null 2>&1; then
  eval "$(fnm env --use-on-cd)"
fi
~/.zshrc
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
$ sudo apt install optipng
$ brew install optipng

Install oxipng.

$ brew install oxipng
$ sudo apt-get install shellcheck
$ brew install shellcheck
$ sudo apt-get install yamllint
$ brew install yamllint
$ sudo apt install zstd
$ brew install zstd

About

Miscellaneous shell-related scripts and functions

Topics

Resources

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published