[{"content":"As a longtime Maven user, I have quite often been annoyed by one particular feature: Repository authentication. Since the beginning of Maven 2, this has been done the same way, by having a \u0026lt;server\u0026gt; node in the $HOME/.m2/settings.xml. This post will explore a new Maven extension I have created that attempts to avoid hard-coding these credentials in a static xml file. Before we get into that, however, we will take a look at how this works today.\nThe current state of affairs There are a few things to know here:\nThe order of definition matters, since that is an implicit order of priority. The values in \u0026lt;id\u0026gt; elements must be unique within the set of repository types. There are 3 main types of repositories:\ndependency plugin distributionManagement The most basic thing we need is being able to resolve libraries (dependencies), from either public or private sources. For this example we\u0026rsquo;ll use gitlab packages.\npom.xml snippet:\n\u0026lt;repositories\u0026gt; \u0026lt;repository\u0026gt; \u0026lt;id\u0026gt;gitlab-group-1\u0026lt;/id\u0026gt; \u0026lt;url\u0026gt;https://gitlab.example.com/api/v4/groups/1/-/packages/maven\u0026lt;/url\u0026gt; \u0026lt;/repository\u0026gt; \u0026lt;repository\u0026gt; \u0026lt;id\u0026gt;gitlab-group-2\u0026lt;/id\u0026gt; \u0026lt;url\u0026gt;https://gitlab.example.com/api/v4/groups/2/-/packages/maven\u0026lt;/url\u0026gt; \u0026lt;/repository\u0026gt; \u0026lt;/repositories\u0026gt; For a more complex build, like having private plugins, we also need plugin repositories:\n\u0026lt;pluginRepositories\u0026gt; \u0026lt;pluginRepository\u0026gt; \u0026lt;id\u0026gt;gitlab-group-1\u0026lt;/id\u0026gt; \u0026lt;url\u0026gt;https://gitlab.example.com/api/v4/groups/1/-/packages/maven\u0026lt;/url\u0026gt; \u0026lt;/releases\u0026gt; \u0026lt;/pluginRepository\u0026gt; \u0026lt;pluginRepository\u0026gt; \u0026lt;id\u0026gt;gitlab-group-2\u0026lt;/id\u0026gt; \u0026lt;url\u0026gt;https://gitlab.example.com/api/v4/groups/2/-/packages/maven\u0026lt;/url\u0026gt; \u0026lt;/pluginRepository\u0026gt; \u0026lt;/pluginRepository\u0026gt; If we want to publish the project to the maven repository allowing other projects depend on that. We need:\n\u0026lt;distributionManagement\u0026gt; \u0026lt;repository\u0026gt; \u0026lt;id\u0026gt;gitlab-project\u0026lt;/id\u0026gt; \u0026lt;url\u0026gt;https://gitlab.example.com/api/v4/projects/1000/packages/maven\u0026lt;/url\u0026gt; \u0026lt;/repository\u0026gt; \u0026lt;snapshotRepository\u0026gt; \u0026lt;id\u0026gt;gitlab-project\u0026lt;/id\u0026gt; \u0026lt;url\u0026gt;https://gitlab.example.com/api/v4/projects/1000/packages/maven\u0026lt;/url\u0026gt; \u0026lt;/snapshotRepository\u0026gt; \u0026lt;/distributionManagement\u0026gt; This is all well and good. But we still need authentication of the remote repositories. With Maven we need to add each private repository id to a corresponding \u0026lt;server\u0026gt; node like this in $HOME/.m2/settings.xml\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;settings xmlns=\u0026#34;http://maven.apache.org/SETTINGS/1.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd\u0026#34;\u0026gt; \u0026lt;servers\u0026gt; \u0026lt;server\u0026gt; \u0026lt;id\u0026gt;gitlab-project\u0026lt;/id\u0026gt; \u0026lt;username\u0026gt;gitlabusername\u0026lt;/username\u0026gt; \u0026lt;password\u0026gt;personal-access-token\u0026lt;/password\u0026gt; \u0026lt;/server\u0026gt; \u0026lt;server\u0026gt; \u0026lt;id\u0026gt;gitlab-group-1\u0026lt;/id\u0026gt; \u0026lt;username\u0026gt;gitlabusername\u0026lt;/username\u0026gt; \u0026lt;password\u0026gt;personal-access-token\u0026lt;/password\u0026gt; \u0026lt;/server\u0026gt; \u0026lt;server\u0026gt; \u0026lt;id\u0026gt;gitlab-group-2\u0026lt;/id\u0026gt; \u0026lt;username\u0026gt;gitlabusername\u0026lt;/username\u0026gt; \u0026lt;password\u0026gt;personal-access-token\u0026lt;/password\u0026gt; \u0026lt;/server\u0026gt; \u0026lt;/servers\u0026gt; \u0026lt;/settings\u0026gt; So what is the problem? All the authentication information in a cleartext file. There is also a lot of duplication, since we have to repeat this information for each server element.\nI am sure there are some solution in Maven 3 or Maven 4 to that.\nThe main issue, in my view, is that we need relatively static passwords for all these \u0026lt;server\u0026gt; nodes. Users will create tokens that are long-lived and this may cause a security nightmare. The issue can be slightly improved by using interpolated environment variables, but this just moves the problem somewhere else. The duplication issue remains.\nOne possible solution to this could be to have developer tooling that rewrites this file every so often, but this assumes that the file is managed by a security team. Unfortunately this file is too useful for local tooling to make that really a viable solution. Developer workstations are usually not the most uniform in their setup, so there must be a better way.\nAside: Docker authentication If we look at how docker cli handles authentication, we see a very similar setup. The configuration can be found found in $HOME/.docker/config.json\nIf you for instance login to a docker respository using the cli, there will a corresponding auths section. For now we will ignore the credHelpers and credsStore properties we will get back to them in a bit.\n{ \u0026#34;auths\u0026#34;: { \u0026#34;registry.github.example.com\u0026#34;: { \u0026#34;auth\u0026#34;: \u0026#34;\u0026lt;base64-encoded\u0026gt;\u0026#34; }, \u0026#34;ghcr.io\u0026#34;: { \u0026#34;auth\u0026#34;: \u0026#34;\u0026lt;base64-encoded\u0026gt;\u0026#34; }, \u0026#34;https://index.docker.io/v1/\u0026#34;: { \u0026#34;auth\u0026#34;: \u0026#34;\u0026lt;base64-encoded\u0026gt;\u0026#34; } } } This has exactly the same problem as maven, users will create long-lived tokens for these.\nDocker has one feature which improves cleartext storage slightly and that is the credsStore which uses the system keychain or pass. But for now, we can consider docker authentication the same as maven authentication.\nDocker\u0026rsquo;s solution Docker introduces a protocol for handling credentials.\nThe way it works is by outsourcing the problem to a command line interface (CLI) hook on the PATH, thereby making the Authentication a coding problem instead of a configuration problem.\nThis hook is not meant to be used by humans, but by the docker cli process.\nThere are multiple implementations of this interface by vendors like google, gitlab, github (multiple) and amazon to name a few.\nHaving a separate command for authentication introduces complexity. We now need another program to be made available to users and put that in the PATH. Luckily there is a solution for most of these, and that is by using mise. I will not go into details of how that works as the documentation is quite good and my friend Karl Yngve Lervåg has written about it.\nRegistration We register a new credential helper for docker by using the $HOME/.docker/config.json as before:\n{ \u0026#34;credHelpers\u0026#34;: { \u0026#34;registry.gitlab.example.com\u0026#34;: \u0026#34;glab\u0026#34;, \u0026#34;ghcr.io\u0026#34;: \u0026#34;ghcr-login\u0026#34; } } The short names are are actually references to the command docker-credential-\u0026lt;shortname\u0026gt; which MUST be found in the system PATH.\nExample: docker-credential-glab found in $HOME/.local/bin\nMy solution to the Maven Authentication Problem Based on the information above I realized that I could build a maven extension that uses the exact same protocol. The way the extension works is by hooking into the maven lifecycle and enhancing the Authentication lookup mechanism.\nThis would allow us to have short lived tokens, and get them controlled by the team that manages the repository. It would allow platform- and security teams to greatly increase the security, but not impeding the developers by having them do manual labour.\nWe get the same increased complexity by having to install a separate command-hook for authentication, but that is that manageable.\nSetting it up We assume that we have a maven project located at /home/myuser/Projects/my-maven-project. In the rest of this post we will reference this path as ${project.basedir}.\nWe will need to register the extension in ${project.basedir}/.mvn/extensions.xml. This must be done since we need to hook into Maven before it has loaded any projects.\n\u0026lt;extensions xmlns=\u0026#34;http://maven.apache.org/EXTENSIONS/1.1.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/EXTENSIONS/1.1.0 https://maven.apache.org/xsd/core-extensions-1.1.0.xsd\u0026#34;\u0026gt; \u0026lt;extension\u0026gt; \u0026lt;groupId\u0026gt;net.hamnaberg\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-credential-helper\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;version-from-badge-below\u0026lt;/version\u0026gt; \u0026lt;/extension\u0026gt; \u0026lt;/extensions\u0026gt; This sets up Maven to be able to use a maven-credential-helper, but none are currently registered. So let us do just that.\nWe need to define a new file ${project.basedir}/.mvn/credhelpers.json, which we expect users to be able to share. That means that we need to use the shorthand syntax referenced in the documentation.\n{ \u0026#34;https://gitlab.example.com\u0026#34;: \u0026#34;glab\u0026#34; } Right now there is no defined maven-credential-glab anywhere, so we\u0026rsquo;ll have to write it ourselves. For simplicity this is written in bash.\nWe start by adding some dependencies to the global mise toolset.\nmise use -g glab Then we need to login to our gitlab instance.\nglab auth login --hostname gitlab.example.com Lastly we will write our script.\n#!/bin/bash # $HOME/bin/maven-credential-glab HOSTNAME=\u0026#34;gitlab.example.com\u0026#34; # this command ensures we have an up-to-date token in the config file if we use oauth2 tokens. status=$(glab auth status --hostname \u0026#34;$HOSTNAME\u0026#34; 2\u0026gt;\u0026amp;1) if [[ $? != 0 ]]; then echo $status \u0026gt;\u0026amp;2 exit 2 fi MAVEN_TARGET_URL=\u0026#34;https://$HOSTNAME\u0026#34; # Hack to fix issue in glab, which looks up the USER environment variable. This is not what we want here. OLD_USER=$USER unset $USER MAVEN_USERNAME=\u0026#34;$(glab config get --host \u0026#34;$HOSTNAME\u0026#34; user)\u0026#34; export USER=$OLD_USER # End hack ## Get the token from glab MAVEN_SECRET=\u0026#34;$(glab config get --host \u0026#34;$HOSTNAME\u0026#34; token)\u0026#34; # The maven credential helper extension passes the command (get, list) as the first argument COMMAND=$1 case \u0026#34;$COMMAND\u0026#34; in get) # Docker sends the registry URL via stdin read -r INPUT_URL # Exercise for the reader is to improve the regex to make sure we have a maven Repository url here. if [[ \u0026#34;$MAVEN_TARGET_URL\u0026#34; == *\u0026#34;$INPUT_URL\u0026#34;* ]] || [[ \u0026#34;$INPUT_URL\u0026#34; == *\u0026#34;$MAVEN_TARGET_URL\u0026#34;* ]]; then # Return JSON with the credentials printf \u0026#39;{\u0026#34;ServerURL\u0026#34;:\u0026#34;%s\u0026#34;,\u0026#34;Username\u0026#34;:\u0026#34;%s\u0026#34;,\u0026#34;Secret\u0026#34;:\u0026#34;%s\u0026#34;}\\n\u0026#39; \\ \u0026#34;$INPUT_URL\u0026#34; \u0026#34;$MAVEN_USERNAME\u0026#34; \u0026#34;$MAVEN_SECRET\u0026#34; else # If the URL doesn\u0026#39;t match, return an error message to stderr # and exit with a non-zero code so Docker knows it\u0026#39;s not here. echo \u0026#34;Credentials not found for $INPUT_URL\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi ;; list) printf \u0026#39;{\u0026#34;%s\u0026#34;:\u0026#34;%s\u0026#34;}\\n\u0026#39; \u0026#34;$MAVEN_TARGET_URL\u0026#34; \u0026#34;$MAVEN_USERNAME\u0026#34; ;; *) exit 0 ;; esac Save this as $HOME/bin/maven-credential-glab and make it executable chmod 755 $HOME/bin/maven-credential-glab.\nLets test it:\necho \u0026quot;https://gitlab.example.com/api/v4/groups/2/-/packages/maven\u0026quot; | maven-credential-glab get\nNow we can resolve the dependencies for all the configured repositories that are part of gitlab.example.com.\nConclusion Using the docker-credential-helper protocol has enabled us to avoid hard-coding tokens in the settings.xml file. We avoid repeating ourselves, keeping things DRY. We also eliminate error prone updates, since we avoid repeating ourselves.\nI find this very useful, and will start using it for my current project. I am sure other uses may appear down the road. I can imagine this can be useful for npm authentication or similar things.\n","permalink":"https://www.hamnis.org/posts/2026-maven-credential-helper/","summary":"\u003cp\u003eAs a longtime Maven user, I have quite often been annoyed by one particular feature: Repository authentication.\nSince the beginning of Maven 2, this has been done the same way, by having a \u003ccode\u003e\u0026lt;server\u0026gt;\u003c/code\u003e node in the \u003ca href=\"https://maven.apache.org/settings.html#Servers\"\u003e\u003ccode\u003e$HOME/.m2/settings.xml\u003c/code\u003e\u003c/a\u003e.\nThis post will explore a new \u003ca href=\"https://codeberg.org/hamnis/maven-credential-helper\"\u003eMaven extension\u003c/a\u003e I have created that attempts to avoid hard-coding these credentials in a static xml file.\nBefore we get into that, however, we will take a look at how this works today.\u003c/p\u003e","title":"Maven Credential Helper"},{"content":"Working with the command line I have been a developer for quite a few years now, and I have been using Linux for most of the time. Being a Linux user I am not afraid of using the command line, in fact, a lot of the time is spent using the terminal. Over the years I’ve spent a lot of time in the terminal, and there’s still a lot of things I don’t remember by heart.\nWe need to be able to use a lot of command line tools as developers, like coreutils for basic handling of files and other functionalty. Build systems for the JVM like maven, sbt or frontend adjecent tooling like npm, nodejs, deno.\nFinding documentation The documentation for these tools are often built-in. Most CLI (Command Line Interface) tools have a lot of options like for instance curl. Most tools also have a -h or --help option.\nA lof of modern tools also have a subcommand structure, each of these may have a --help option, but many of them also have an own help subcommand.\nTools that are packaged by linux distributions also usually have man pages or info. These are pager searchable documentation pages which are directly available in the shell.\ntry it out:\nman bash or man curl\nThe beautiful thing about shell based help functions is that it is available right where we are, no need to context shift to an online help guide.\nAs I wrote in a previous post, about shell completions, the need for remembering every option is not really needed. However, sometimes that is not sufficient, so docs are still needed.\nConclusion Do not be afraid of asking the command about --help. It wants you to succeed.\n","permalink":"https://www.hamnis.org/posts/2025-a-little-help-from-my-friends/","summary":"\u003ch2 id=\"working-with-the-command-line\"\u003eWorking with the command line\u003c/h2\u003e\n\u003cp\u003eI have been a developer for quite a few years now, and I have been using Linux for most of the time.\nBeing a Linux user I am not afraid of using the command line, in fact, a lot of the time is spent using the terminal.\nOver the years I’ve spent a lot of time in the terminal, and there’s still a lot of things I don’t remember by heart.\u003c/p\u003e","title":"with a little --help from my friends"},{"content":"Introduction Have you ever felt like you’re making too many shell scripts? Some may say that every line of bash is too much bash. An example of why bash may be not the best solution for writing scripts is shellcheck. There are too many possible mistakes.\nIn the last few years I have been experimenting with different scripting languages, and I think I have found a few good ones.\nA non-exhaustive list is:\nammonite Scala cli scripts Deno You may notice that all of these are using strongly typed languages, and you may rightly ask why. We have type inference in most strongly typed languages. This means that we rarely have to declare the types, we can\u0026rsquo;t all be Java.\nIn my experience, using strongly typed languages actually helps drive the scripts along. Also since we have a real programming language, we can create abstractions to help doing the correct things. We can also test the code, since we have a built-in test-runner.\nNote Technically you can do this in bash\nMy latest discovery is the typescript runtime Deno. This post will explore a few ways of why you may want to use something like Deno for some of your scripting needs.\nCommand line parsing I am sure you have tried parsing command line arguments in bash. This is surprisingly difficult and error prone.\nHow can we do this in deno? In short; we use Cliffy.\nFirst we need to add this dependency:\ndeno add \u0026quot;jsr:@cliffy/command@^1.0.0-rc.7\u0026quot;\nNote Cliffy is only available as an RC at the time of writing.\nDeno will now create two new files called deno.json and deno.lock in the working directory.\nCreate a new file called args.ts and paste the following code into your favourite $EDITOR.\nimport { Command } from \u0026#34;@cliffy/command\u0026#34;; type Options = { verbose: unknown[]; }; await new Command() .name(\u0026#34;args\u0026#34;) .version(\u0026#34;1.0.0\u0026#34;) .option(\u0026#34;--verbose, -v\u0026#34;, \u0026#34;set verbosity level\u0026#34;, { collect: true, default: [] }) .action((opt: Options) =\u0026gt; { const verboseLevel = opt.verbose.length; console.log(\u0026#34;Verbosity level is\u0026#34;, verboseLevel); }) .parse(Deno.args); Test it out by running it:\ndeno run args.ts\nVerbosity level is 0 deno run args.ts -v\nVerbosity level is 1 deno run args.ts -vvv\nVerbosity level is 3 Error handling Lets try running this with an incorrect option.\ndeno run args.ts -vs\nThis is a quite nice error message!\nWe can immediately see what we did wrong, and we get a nice help screen.\nBut can we improve this? We cannot say that the type Options has a lot of useful information, we are using an unknown array here, which is not really anything useful. Let\u0026rsquo;s refactor this a bit. Let\u0026rsquo;s call this new file args1.ts.\nimport { Command, EnumType } from \u0026#34;@cliffy/command\u0026#34;; enum Level { off = \u0026#34;off\u0026#34;, error = \u0026#34;error\u0026#34;, warn = \u0026#34;warn\u0026#34;, info = \u0026#34;info\u0026#34;, debug = \u0026#34;debug\u0026#34;, trace = \u0026#34;trace\u0026#34;, } type Options = { verbose: Level; }; await new Command() .name(\u0026#34;args\u0026#34;) .version(\u0026#34;1.0.0\u0026#34;) .type(\u0026#34;level\u0026#34;, new EnumType(Level)) .option(\u0026#34;--verbose, -v \u0026lt;level:level\u0026gt;\u0026#34;, \u0026#34;set verbosity level\u0026#34;, { default: Level.off }) .action((opt: Options) =\u0026gt; { const verboseLevel = opt.verbose; console.log(\u0026#34;Verbosity level is\u0026#34;, verboseLevel); }).parse(Deno.args); Now we can immediately see an improvement in typesafety, and we get the possible levels available in the help output. Let us test this out:\ndeno run args1.ts --help\nReusable Commands Expanding on the previous section, we can improve the usage of the command defined there.\nLets modify args1.ts into a new file args2.ts:\nimport { Command, EnumType } from \u0026#34;@cliffy/command\u0026#34;; export enum Level { off = \u0026#34;off\u0026#34;, error = \u0026#34;error\u0026#34;, warn = \u0026#34;warn\u0026#34;, info = \u0026#34;info\u0026#34;, debug = \u0026#34;debug\u0026#34;, trace = \u0026#34;trace\u0026#34;, } export type Options = { verbose: Level; }; export const args = new Command() .name(\u0026#34;args\u0026#34;) .version(\u0026#34;1.0.0\u0026#34;) .type(\u0026#34;level\u0026#34;, new EnumType(Level)) .option(\u0026#34;--verbose, -v \u0026lt;level:level\u0026gt;\u0026#34;, \u0026#34;set verbosity level\u0026#34;, { default: Level.off }) .action((opt: Options) =\u0026gt; { const verboseLevel = opt.verbose; console.log(\u0026#34;Verbosity level is\u0026#34;, verboseLevel); }); if (import.meta.main) { await args.parse(Deno.args); } We also now export the Level, Options and args members so we can use the command elsewhere. Adding the if (import.meta.main) as guard, allows us to still run this as a main function.\nSubcommands Info This is a pattern in scripting to have multiple executable commands grouped together as a single command.\nWe create a new file called sub-commands.ts\nimport { Command } from \u0026#34;@cliffy/command\u0026#34;; import { HelpCommand } from \u0026#34;@cliffy/command/help\u0026#34;; import { CompletionsCommand } from \u0026#34;@cliffy/command/completions\u0026#34;; import { args } from \u0026#34;./args2.ts\u0026#34;; const generate = new Command() .description(\u0026#34;Generate some code\u0026#34;) .action(() =\u0026gt; { console.log(\u0026#34;Would generate some code here\u0026#34;); }); const commit = new Command() .description(\u0026#34;Commits to git repository\u0026#34;) .action(() =\u0026gt; { console.log(\u0026#34;Would commit to git repo here\u0026#34;); }); const sub = new Command() .name(\u0026#34;sub-commands\u0026#34;) .version(\u0026#34;1.0.0\u0026#34;) .action(function () { this.showHelp(); }) .command(\u0026#34;help\u0026#34;, new HelpCommand().global()) .command(\u0026#34;completions\u0026#34;, new CompletionsCommand()) .command(\u0026#34;generate\u0026#34;, generate) .command(\u0026#34;commit\u0026#34;, commit) .command(\u0026#34;exec\u0026#34;, args.description(\u0026#34;execute program\u0026#34;)); if (import.meta.main) { await sub.parse(Deno.args); } First we import two built-in commands that grants us shell completion and a help command. Next we define two new commands, called generate and commit.\nLastly we compose these together to form a new program, combining it with the const args value we created in the previous section.\nRunning deno run sub-commands.ts with no arguments yields:\nShell completion To use the built-in shell completion in cliffy you have to first activate it. Info We need to run the script as a standalone command. The reason for this is that the completion system expects a known command. Our name for the command is sub-commands\nHere are two different ways we can do that:\nUse a shebang Prepend #!/usr/bin/env deno to sub-commands.ts then rename it to sub-commands\nWe also need to make the new script executable chmod 755 sub-commands\nCompile the program into an executable. deno compile sub-commands.ts\nThis will produce a binary for the current platform. Another benefit from compilation is that we also solve the distrbution problem.\nNext we need to load the completions into the current shell.\nsource \u0026lt;(./sub-commands completions zsh) Once this is loaded successfully we can complete in:\n./sub-commands \u0026lt;tab\u0026gt; Run an executable (in Bash) OK. Running a process in bash is really easy. We just do this:\nlocal out=$(foo bar) This will capture the output of stdout into the variable out.\nbut what if it fails\u0026hellip; We might need to check the exit code of the previously run command.\nFor bash you need to deal with problem in a intuitive way.\nWhat happens to the output of stderr? Should that also be read into the variable?\nRun an executable (in Deno) const out = new Deno.Command(\u0026#34;foo\u0026#34;, {args: [\u0026#34;bar\u0026#34;]}).outputSync() const td = new TextDecoder(); const stdout = td.decode(out.stdout).trim(); const stderr = td.decode(out.stderr).trim(); This is more code than the bash simple case, but we get a few benefits:\nThe command, and its scope is clear from the type Deno.Command We can capture both the stdout and stderr We can query the exit status by checking out.success But this is not everything, we can redirect the output directly into the current process' stdout or stderr as needed. We can pipe stdout into stdin in other commands, just like in bash.\nTake a look at the API for more uses.\nConclusion I hope this post has inspired you to take a look at other languages for your scripting needs. Deno has a lot going for it, and I think it is accessible for most people. The code for this post is also available as a git repo here\n","permalink":"https://www.hamnis.org/posts/2025-deno-scripting/","summary":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eHave you ever felt like you’re making too many shell scripts? Some may say that every line of \u003ccode\u003ebash\u003c/code\u003e is too much \u003ccode\u003ebash\u003c/code\u003e.\nAn example of why bash may be not the best solution for writing scripts is shellcheck. There are \u003ca href=\"https://gist.github.com/eggplants/9fbe03453c3f3fd03295e88def6a1324\"\u003etoo many possible mistakes\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eIn the last few years I have been experimenting with different scripting languages, and I think I have found a few good ones.\u003c/p\u003e\n\u003cp\u003eA non-exhaustive list is:\u003c/p\u003e","title":"Deno scripting"}]