mtnr

A tech blog with fries on the side

Tag: terminal

  • Building My First CLI Tool in Go: A TODO Manager

    I decided to get acquainted with Go by building something practical: a command-line TODO manager. No better way to understand a language than to build a real tool with it.

    What I Built

    todo-cli is a simple, lightweight TODO list manager that runs entirely in your terminal. It stores your tasks locally in JSON format and supports all the essential operations you’d expect:

    • Add new tasks
    • List active, completed, or all tasks
    • Mark tasks as complete
    • Delete tasks you no longer need

    Everything is stored in ~/.todo-cli/todos.json, so your data stays local and under your control.

    Why Go?

    Go’s reputation for simplicity and its dominance in the CLI tool space made it an obvious choice. Tools like Docker, Kubernetes, and Hugo are all written in Go, and I wanted to understand why.

    The experience was refreshing. Go’s straightforward syntax, built-in tooling, and focus on pragmatism over abstraction made it easy to get productive quickly. No build tools to configure, no frameworks to choose between—just go build and you’re done.

    What I Learned

    This weekend project taught me:

    • Go fundamentals: structs, interfaces, methods, and error handling
    • CLI development with Cobra
    • Go’s approach to project structure and package organization
    • Testing patterns in Go
    • Working with JSON marshaling and file I/O

    The interface-based storage design means I can easily swap out JSON for SQLite or another backend later if needed.

    Quick Example

    bash

    # Add some tasks
    $ todo-cli add "Review pull requests"
    ✓ Added todo #1: Review pull requests
    
    $ todo-cli add "Write blog post"
    ✓ Added todo #2: Write blog post
    
    # See what's on your plate
    $ todo-cli list
    Active TODOs:
    
    ☐ [1] Review pull requests (created: Feb 8, 2026)
    ☐ [2] Write blog post (created: Feb 8, 2026)
    
    # Mark one complete
    $ todo-cli complete 2
    ✓ Completed todo #2: Write blog post

    Try It Yourself

    The project is open source under the MIT license and available on Codeberg. If you’re interested in learning Go, building a CLI tool is a great first project—it’s practical, achievable in a weekend, and teaches you the fundamentals without getting bogged down in web frameworks or complex architectures.

    Happy coding!

  • Install Java on macOS Using SDKMAN!

    Update: I previously recommended using Homebrew and jEnv for managing Java installations, but I’ve since discovered SDKMAN! and find it to be a superior solution. It’s more streamlined, handles multiple SDKs beyond just Java, and doesn’t require Homebrew as a dependency.

    Managing multiple Java versions on macOS can be challenging, especially when different projects require different JDK versions. SDKMAN! (Software Development Kit Manager) offers an elegant solution that goes beyond just Java management.

    What is SDKMAN!?

    SDKMAN! is a tool for managing parallel versions of multiple Software Development Kits on Unix-based systems. Originally known as GVM (Groovy enVironment Manager), it provides a simple command-line interface for installing, switching, and managing various JVM-related tools. What makes SDKMAN! particularly attractive is that it works seamlessly on macOS, Linux, and Windows Subsystem for Linux (WSL).

    Installing SDKMAN!

    Installation is straightforward. Open your terminal and run:

    curl -s "https://get.sdkman.io" | bash

    After installation completes, either restart your terminal or run:

    source "$HOME/.sdkman/bin/sdkman-init.sh"

    Verify the installation by checking the version:

    sdk version

    Installing Java

    With SDKMAN! installed, you can now easily install Java. First, let’s see what versions are available:

    sdk list java

    This command displays all available Java distributions and versions. To install a specific version, use:

    sdk install java 17.0.13-tem

    You can install multiple versions and switch between them easily:

    sdk install java 21.0.5-tem
    sdk use java 21.0.5-tem

    To set a default Java version globally:

    sdk default java 17.0.13-tem

    Beyond Java: Other Development Tools

    One of SDKMAN!’s biggest advantages over jEnv is its ability to manage numerous other development tools. You can install build tools, frameworks, and other SDKs with the same simple commands:

    • Gradle: sdk install gradle
    • Maven: sdk install maven
    • Kotlin: sdk install kotlin
    • Scala: sdk install scala
    • Spring Boot CLI: sdk install springboot
    • Groovy: sdk install groovy

    To see all available SDKs:

    sdk list

    Useful Commands

    Here are some handy SDKMAN! commands to know:

    • sdk current – Show currently active SDK versions
    • sdk current java – Show currently active Java version
    • sdk upgrade – Upgrade all installed SDKs
    • sdk uninstall java 17.0.13-tem – Remove a specific version
    • sdk env – Switch to project-specific versions defined in .sdkmanrc

    Why SDKMAN! Over jEnv?

    While jEnv served me well, SDKMAN! offers several advantages:

    • No Homebrew dependency – SDKMAN! is self-contained
    • Manages more than just Java – one tool for your entire JVM ecosystem
    • Simpler installation and configuration
    • Active development and community support
    • Built-in update mechanism for both the tool and SDKs

    SDKMAN! has become my go-to tool for managing Java and related development tools on macOS. Its simplicity and comprehensive SDK support make it an excellent choice for Java developers working across multiple projects with varying requirements.

    Happy coding!

  • 📊 Diagnosing Java Applications: Creating and Analyzing Thread & Heap Dumps

    When your Java application starts misbehaving – hanging, crashing, or consuming too much memory – thread and heap dumps are your go-to tools for root cause analysis. This post walks through how to create these dumps using the jstack, jmap, and jcmd tools, and suggests utilities for analyzing them effectively. Use jps to identify the process ID you’re interested in.


    🧵 Thread Dumps: Capturing Thread States

    1. Using jstack

    jstack <PID> > threaddump.txt
    
    • Produces a snapshot of all thread states (blocked, runnable, waiting).
    • Great for diagnosing deadlocks, thread contention, and CPU spikes.

    2. Using jcmd

    jcmd <PID> Thread.print > threaddump.txt
    
    • More modern and flexible than jstack.
    • Lists thread locks and monitors.

    🧠 Heap Dumps: Capturing Memory Snapshots

    1. Using jmap

    jmap -dump:live,format=b,file=heapdump.hprof <PID>
    
    • Dumps the live heap memory (excluding unreachable objects).
    • Useful for analyzing memory leaks and object retention.

    2. Using jcmd

    jcmd <PID> GC.heap_dump heapdump.hprof
    
    • Recommended over jmap for modern JVMs (especially with limited access).
    • Can work even when jmap fails due to permissions or restrictions.

    🧰 Tools to Analyze Dumps

    🔍 Thread Dump Analysis

    • jstack.review – Analyze java thread dumps from within the browser. No data will leave your computer when you click Analyze.
    • FastThread.io – Upload your dump and get a categorized summary.
    • TDA (Thread Dump Analyzer) – Desktop tool for visualizing thread interactions.

    🧠 Heap Dump Analysis

    • Eclipse MAT (Memory Analyzer Tool) – Industry-standard tool for analyzing .hprof files.
    • VisualVM – Bundled with the JDK or available separately, it provides a GUI for live analysis and offline dump inspection.
    • YourKit – Commercial profiler with advanced heap analysis and leak detection.

    📝 Final Thoughts

    Thread and heap dumps are crucial artifacts for JVM troubleshooting. While tools like jstack and jmap provide raw diagnostic data, leveraging jcmd offers greater flexibility and reliability on modern JVMs. Combined with powerful analysis tools, these techniques can help you uncover performance bottlenecks and memory issues before they escalate into outages.

    Happy debugging! 🐞

  • Mask private SSH key in GitLab CI/CD

    When using Ansible for automating deployment from a GitLab CI/CD pipeline, you must provide a private SSH key via a GitLab CI/CD variable.

    GitLab CI/CD variables can be setup to be masked which prevents them from appearing in log files. Furthermore, they can be configured to be hidden so you have no way of accessing its value manually.

    Masked in job logs, and can never be revealed in the CI/CD settings after the variable is saved.

    This makes it perfect for storing secrets like the aforementioned private SSH key.

    However, the functionality comes with a flaw, as it is not possible to use it, when the secret value contains forbidden characters (e.g., blank spaces).

    Private SSH keys start and end with a comment that contains blank spaces and must not be altered or deleted as this would render the key useless.

    -----BEGIN OPENSSH PRIVATE KEY-----
    ...
    -----END OPENSSH PRIVATE KEY-----

    To mitigate the issue, you can encode the key with Base64. First create a new key pair and save it wherever you like. We will not keep the keys on our local system.

    $ ssh-keygen -t ed25519 -b 4096 -C "GitLab CI/CD"

    Don’t provide a passphrase when asked.

    For the sake of this article, we’ll assume the generated files will reside in ~/gitlab/id_ed25519 and ~/gitlab/id_ed25519.pub.

    Now to encode the private key you can use the following command.

    $ base64 -w0 -i ~/gitlab/id_ed25519

    base64 uses -i to specify the input file. In our case, the private SSH key. But what does -w0 signify?

    By default, the base64 command wraps lines at 76 characters when encoding data. The -w0 option is used to output the encoded result in a single line without any line breaks.

    Copy the output of the command and add it to a new masked and hidden CI/CD variable named SSH_PRIVATE_KEY in your GitLab project.

    Let’s have a look at a simple pipeline script on how to use the private SSH key.

    # Excerpt
      before_script:
        # Make sure that the image used provides an ssh-agent, install one, otherwise.
        - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
        - eval $(ssh-agent -s)
        # Read the private SSH key from the variable, decode it, remove line breaks,
        # and add it to the SSH agent.
        - echo "$SSH_PRIVATE_KEY" | base64 -d | tr -d '\r' | ssh-add - > /dev/null
      script:
        - # Ansible can now use the provided private SSH key

    Provided, that you added the public key to the ~/.ssh/authorized_keys file on the target system(s), there is only one thing left to do.

    Gather the SSH public keys from the target system(s) and make them available to your scripts known hosts.

    $ ssh-keyscan example.com

    Replace example.com with the actual fully qualified domain name or the IP address of your target system and add the output to a new GitLab CI/CD variable called SSH_KNOWN_HOSTS.

    Revisiting our script from earlier, we can now add the final piece of the puzzle.

    # Excerpt
      before_script:
        # Make sure that the image used provides an ssh-agent, install one, otherwise.
        - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
        - eval $(ssh-agent -s)
        # Read the private SSH key from the variable, decode it, remove line breaks,
        # and add it to the SSH agent.
        - echo "$SSH_PRIVATE_KEY" | base64 -d | tr -d '\r' | ssh-add - > /dev/null
        # Make sure the target system is known
        - mkdir -p ~/.ssh
        - chmod 700 ~/.ssh
        - echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
        - chmod 644 ~/.ssh/known_hosts
      script:
        - # Ansible can now use the provided private SSH key

    Happy deploying!

    Further reading:

  • Install Java using Homebrew and jEnv on MacOS

    Sometimes it’s desirable to use different Java versions. Here’s one way how to do it on a Mac.

    Prerequisites

    If you haven’t already, install Homebrew as a first step.

    $ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

    Now, that Homebrew is installed, install jEnv, next. It’ll enable you to manage multiple Java installations and to switch easily amongst them.

    $ brew install jenv

    Follow the steps listed in the official jEnv documentation if you’re using a shell other than zsh to configure jEnv.

    You’ll find the steps necessary to configure zsh below.

    $ echo 'export PATH="$HOME/.jenv/bin:$PATH"' >> ~/.zshrc
    $ echo 'eval "$(jenv init -)"' >> ~/.zshrc

    Install JDKs

    Now, that all pieces are in place, let’s install a JDK. You may chose whichever version you need. There is a list of available JDKs under https://formulae.brew.sh/formula/openjdk.

    $ brew install openjdk@17

    Manage JDKs

    In order to manage the newly installed JDK with jEnv, let’s make jEnv aware of it.

    $ jenv add /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home/

    You can chose to install additional JDKs now but we’ll check the available versions with jEnv, first.

    $ jenv versions
    * system (set by ~/.jenv/version)
      17
      17.0
      17.0.13
      openjdk64-17.0.13

    You can easily switch between versions using the following command.

    $ jenv global openjdk64-17.0.13

    This will enable the selected JDK on a global level. You may also chose to only enable it in a given directory by using the local instead of global command.

    You must restart you terminal for the changes to take effect.

    Happy coding!

  • Upgrading from RSA to ED25519

    Just out of curiosity, ascertain what keys you have on your machine by issuing the following command:

    for key in ~/.ssh/id_*; do ssh-keygen -l -f "${key}"; done | uniq

    Generate new ED25519 key pair

    ssh-keygen -o -a 256 -t ed25519 -C "$(hostname)-$(date +'%d-%m-%Y')"

    Executing the command above will generate a new pair of Ed25519 keys. When asked, provide a strong password for the key pair.

    $ ~/.ssh/id_ed25519     #Private key
    $ ~/.ssh/id_ed25519.pub #Public key

    Let’s have a brief look at each option.

    -o will use OpenSSH format for the new keys
    -a specifies the number (amount) of key derivation rounds (KDF)
    -t specifies the type; in this case Ed25519
    -C adds an optional comment that helps with identifying the key

    Using the new keys

    Now, simply add the public key to the authorized keys of the machine you would like to login to. In order to retrieve the public key, use the following command and copy & paste the output of said command.

    cat ~/.ssh/id_ed25519.pub

    Sprinkle a bit of convenience on top

    Now if you’re like me and are using a Mac, you may use the Keychain to store your password, so you don’t have to always type it out when logging in to your server via ssh.

    I added the following to ~/.ssh/config:

    Host mtnr
        HostName mtnr.cloud
        UseKeychain yes
        IdentityFile ~/.ssh/id_ed25519

    Now, when calling ssh mtnr, I can ssh into my server without specifying anything extra like e.g. which pair of keys to use for authentication and, I only have to type out the password once. All subsequent attempts will use the password stored in my Keychain.

    Neat!

    Further reading/sources:

  • How to set the timezone in Ubuntu

    You may list all available timezones via the following command:

    timedatectl list-timezones

    To update the timezone of your machine use

    sudo timedatectl set-timezone Europe/Berlin

    After this the current settings can be inspected like so:

    timedatectl
                   Local time: Sat 2024-06-01 12:46:55 CEST
               Universal time: Sat 2024-06-01 10:46:55 UTC
                     RTC time: Sat 2024-06-01 10:46:55
                    Time zone: Europe/Berlin (CEST, +0200)
    System clock synchronized: yes
                  NTP service: active
              RTC in local TZ: no

    Have a look at https://www.digitalocean.com/community/tutorials/how-to-set-up-time-synchronization-on-ubuntu-20-04 for a more detailled explanation.

  • Adding an administrative account in Ubuntu

    If, for some reason, no default administrative user was created during the server installation process, the first thing I do is to create a personal user and deactivate the root user, if necessary.

    Usually, a pristine Ubuntu installation comes with a default user that was added to the group of sudoers.

    However, when acquring a server with my current hoster, root was equipped with a public key for accessing the server via SSH after the setup was completed.

    So the first order of business after logging in as root was to create a new user as follows:

    adduser <USERNAME>

    Replace <USERNAME> with the name of the user (i.e., in my case timo) and follow the onscreen instructions.

    In order to enable the user to install software and allow for other maintanance tasks, add it to the group of sudoers with the following command as root:

    usermod -aG sudo <USERNAME>

    When you are already logged on as another sudo user, you may issue the same command prefixed with sudo.

    And that’s all there is to it. Now you can login with your new account and use the sudo command when you must perform maintance or other administrative tasks like installing software for example.

    For more details on usermanagement (e.g., how to disable the root user) I highly reccomend the official documentation on the matter.

  • Warp: The terminal I didn’t know I needed

    Warp: The terminal I didn’t know I needed

    Quick reminder to myself to never use another terminal other than Warp again. Check it out here.