[Short Tip] Doing for-loops in Nushell

Nushell 0.32 added support for typical for loops:

With the new for..in command, you can write more natural iterating loops:

Nushell 0.32 release notes
> for $x in 1..3 { echo ($x + 10) }
───┬────
 0 │ 11 
 1 │ 12 
 2 │ 13 
───┴────

Compared to what we have in Bash and others (and given my limited understanding) the most notable difference is that there is no “do”, but instead a curly bracket defining what should be done.

Also, remember that Nushell has an understanding of various data types, so the iterator in the example above is indeed of type “int”. Just stitching it together with another string doesn’t work:

❯ for $i in 1..3 {echo ("/home/" + $i) }
error: Coercion error
   ┌─ shell:31:23
   │
31 │ for $i in 1..3 {echo ("/home/" + $i) }
   │                       ^^^^^^^^   -- integer
   │                       │           
   │                       string

Instead, make sure to echo the iterator:

❯ for $i in 1..3 {echo $"/home/(echo $i)" }
───┬─────────
 0 │ /home/1 
 1 │ /home/2 
 2 │ /home/3 
───┴─────────

For comparison, if you have a set of strings you can provide them in a table and stitch them together easily:

❯ for $i in [a b c d] {echo ("/home/" + $i) }
───┬─────────
 0 │ /home/a 
 1 │ /home/b 
 2 │ /home/c 
 3 │ /home/d 
───┴─────────

[Short Tip] Access system variables in Nushell

Working with variables in Nushell works mostly like you would expect it. If you want to define a variable, you need the keyword let:

❯ let my_variable = "hello blog readers"

❯ echo $my_variable
hello blog readers

Note that you need the spaces around the equal sign character!

❯ let my_variable="hello blog readers"
error: let requires the equals sign parameter
   ┌─ shell:21:1
   │
21 │ let my_variable="hello blog readers"
   │ ^^^ requires the equals sign parameter

But apart from defining your own variables, there is a surprised when you try to access typical system variables:

❯ echo $HOME
error: Variable not in scope
   ┌─ shell:25:6
   │
25 │ echo $HOME
   │      ^^^^^ unknown variable: $HOME

The reason for that error is that Nushell places system environment variables underneath the main variable $nu:

❯ echo $nu|pivot
───┬─────────────────┬────────────────────────────────────────────
 # │     Column0     │                  Column1                   
───┼─────────────────┼────────────────────────────────────────────
 0 │ env             │ [row 34 columns]                           
 1 │ history-path    │ /home/liquidat/.local/share/nu/history.txt 
 2 │ config          │ [row path prompt startup]                  
 3 │ config-path     │ /home/liquidat/.config/nu/config.toml      
 4 │ path            │ [table 5 rows]                             
 5 │ cwd             │ /home/liquidat
 6 │ home-dir        │ /home/liquidat
 7 │ temp-dir        │ /tmp                                       
 8 │ keybinding-path │ /home/liquidat/.config/nu/keybindings.yml  
───┴─────────────────┴────────────────────────────────────────────

❯ echo $nu.env|pivot
────┬──────────────────────────┬────────────────────────────────────────────────────
 #  │         Column0          │                      Column1                       
────┼──────────────────────────┼────────────────────────────────────────────────────
  0 │ CMD_DURATION_MS          │ 2                                                  
  1 │ HOME                     │ /home/liquidat                                            
  2 │ SYSTEMD_EXEC_PID         │ 2958                                               
  3 │ SSH_AUTH_SOCK            │ /run/user/1000/keyring/ssh                         
  4 │ SESSION_MANAGER          │ local/unix:@/tmp/.ICE-unix/2557,unix/unix:/tmp/.IC 
    │                          │ E-unix/2557                                        
  5 │ GNOME_TERMINAL_SCREEN    │ /org/gnome/Terminal/screen/71e37281_4af2_4c9b_be19 
[...]

❯ echo $nu.env.HOME
/home/liquidat

The environment variables are generated from the environment Nushell was started in. If you need to update those for the current environment, use the command let-env:

❯ echo $nu.env.LANG
en_US.UTF-8

❯ let-env LANG = C

❯ echo $nu.env.LANG
C

Image by Susanne Westphal from Pixabay

[Short Tip] Add a path entry to Nushell

Adding a path in nushell is pretty straight forward: the configuration is done in ~/config/nu/config.toml in the [path] section.

If you don’t have it, make sure that the default entries are listed there as well when you start bringing in your own directories. The fastest way to populate your configuration with the default entries is to ask nushell to do it: config set path $nu.path

Next, add the directories you need:

path = ["/usr/local/bin", "/usr/local/sbin", "/usr/bin", "/usr/sbin","/home/rwolters/go/bin"]

In the above example I added the default go binary directory to the list.

[Short Tip] Define an alias in Nushell

A few days ago I decided to switch my main shell to nushell. It offers next generation capabilities similar to other modern shells like Powershell:

Rather than thinking of files and services as raw streams of text, Nu looks at each input as something with structure. For example, when you list the contents of a directory, what you get back is a table of rows, where each row represents an item in that directory.

Nushell Philosophy

This switch feels like the biggest shell related change for me since I switched from Windows Command Prompt to Bash. Even the switch from Bash to ZSH was small in comparison.

As part of this I have to relearn how to do a lot of things. For example, defining an alias is totally different.

The nushell configuration takes place in ~.config/nu/config.toml. To add an alias there, add the section startup if it is not there already, and add a list item there:

startup = [
    "alias evince = flatpak run org.gnome.Evince",
    "alias ll = ls -la",
    ]

In the above example two aliases are given, one to call ls, and one for launching evince via flatpaks.

Image by julia roman from Pixabay

[Howto] Using systemd timers instead of /etc/cron entries

Executing certain commands at given intervals or times is a very typical task for system administrators. In the past it was common to use cron or some variation for this, in some way or the other.

Background

Cron does the job it was written for. But this was years ago, and these days Kernels offer neat things like CPU quotas and memory limits. Cron has no means to use those – but other tools have.

Additionally, newer tools provide dependencies, a proper configuration language (instead of hard-to-maintain bash lines), multiple triggers, randomized delays and real logging.

Especially the last bit, real logging, is essential: Cron can forward log messages it thinks needs to be forwarded. But without real kernel backed process management (cgroups) there is no real way for Cron to see if a job is running or has finished, and what log lines belong to it.

Systemd has all this – and thus it makes sense to create new recurrent jobs in Systemd and even migrate old ones sometimes.

Setting up the timer

What is needed are two things: a service file describing WHAT should be done, and a timer file describing WHEN to do it.

Let’s start with the WHAT: we create a typical service file named backupjob.service executing a backup bash script:

[Unit]
Description=Backup job

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh

Note that we do not enable the service here! We can start it for quick and easy debugging – which is also way easier than with Cron.

Keep in mind that this is a typical systemd service. You can also add requirements, dependencies, performance options and so on in a standardized fashion. With Cron this is not possible, would have to be done in the bash script itself and thus would be messy, hard to maintain, and most likely duplicate work between multiple Cron jobs. And btw.: that way not at all following the UNIX philosophy!

Next, the WHEN. We need a timer file, something which describes when to execute the service, backupjob.timer.

[Unit]
Description=Run backup jobs regularly

[Timer]
OnCalendar=daily
AccuracySec=1h
Unit=backupjob.service

[Install]
WantedBy=timers.target

As you can see, this job is scheduled to run daily, with an accuracy of 1 hour. The accuracy is an interesting bit: systemd tries to avoid starting all services at the exact same time, to avoid massive system load at :00. Just recently a technician at a large cloud provider mentioned that data centers could be designed way more efficient if not everyone would put their Cron jobs to the full minute.

Speaking about, besides OnCalendar there is also a way to start jobs relative to the boot up time, or relative to when the timer was run last. For example, to run something every 15 minutes, set OnActiveSec=15min. More information can be found in the timer documentation.

Starting and stopping the timer

As mentioned above, the service unit files are not activated, instead the timers are: sudo systemctl enable --now backupjob.timer

Stopping a timer is equally simple: systemctl stop backupjob.timer . If you want to avoid that it is started again during the next boot, also disable it: systemctl disable backupjob.timer.

Additional tooling

One of the great things of using the systemd ecosystem is that it is very easy to work with timers: with systemctl list-timers a nice and clear overview of the current state and time of next execution is given:

❯ sudo systemctl list-timers
[sudo] password for liquidat: 
NEXT                         LEFT          LAST                         PASSED       UNIT                         ACTIVATES                     
Thu 2021-04-15 18:06:34 CEST 1h 14min left Thu 2021-04-15 16:09:32 CEST 42min ago    dnf-makecache.timer          dnf-makecache.service         
Fri 2021-04-16 00:00:00 CEST 7h left       Thu 2021-04-15 00:01:01 CEST 16h ago      logrotate.timer              logrotate.service             
Fri 2021-04-16 00:00:00 CEST 7h left       Thu 2021-04-15 00:01:01 CEST 16h ago      mlocate-updatedb.timer       mlocate-updatedb.service      
Fri 2021-04-16 00:00:00 CEST 7h left       Thu 2021-04-15 00:01:01 CEST 16h ago      unbound-anchor.timer         unbound-anchor.service        
Fri 2021-04-16 12:32:32 CEST 19h left      Thu 2021-04-15 12:32:32 CEST 4h 19min ago systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service
Mon 2021-04-19 01:34:34 CEST 3 days left   Mon 2021-04-12 15:46:39 CEST 3 days ago   fstrim.timer                 fstrim.service                

6 timers listed.
Pass --all to see loaded but inactive timers, too.

At the same time, you can get the detailed status with systemctl status logrotate.timer – or of all of them via systemctl status *timer.

Logs are simply available via journalctl -u logrotate.timer – and the logs for the executed service can be read via journalctl -u logrotate.service.

And if you just don’t want to deal with systemd files right now – but nevertheless want all the goods from it, you can also launch systemd services or general commands with a one-time execution:

systemd-run --on-active="10h 30m" --unit myonetimescript.service

Final words

Writing systemd timers instead of traditional Cron jobs makes operating, maintaining and even writing of recurrent jobs much easier.

The only thing you might be missing is easily sending out stuff via mail.

Image by Ryan McGuire from Pixabay