Erste Schritte mit Rust

Vor ein paar Tagen wollte ich die Programmiersprache Rust ausprobieren. Bei Rust handelt es sich um eine Sprache, welche syntaktisch stark an C angelehnt ist und besonderes Augenmerk auf Sicherheit legt. Die erste Frage, die sich mir stellte, ist, ob es für Rust, eine vernünftige IDE-Unterstützung existiert. Fündig geworden bin ich bei IntelliJ IDEA, welches nach Installation des Rust-Plugins zur Programmierung in der Sprache genutzt werden kann. Debugging von Rust-Programmen wird aus technischen Gründen nur in der JetBrains IDE CLion unterstützt, sodass hier einige Abstriche gemacht werden müssen. Neben der Rust-Integration für IntelliJ IDEA gibt es ebenfalls ein Plugin für Visual Studio Code, welches hier allerdings nicht weiter behandelt werden soll.

Mit dem passenden Plugin beherrscht IntelliJ IDEA Rust.

Neben der eigentlichen IDE wird Rust benötigt. Dieses kann über die offizielle Seite der Sprache bezogen werden. Nach der Installation kann in der IDE ein erstes Projekt angelegt werden. Anschließend findet sich in einem Ordner mit dem Namen src eine Datei mit dem Namen main.rs:

fn main() {
    println!("Hello, world!");
}

In dieser Datei findet sich ein minimales Hello world-Programm. Das Ausrufezeichen hinter dem println zeigt unter Rust an, das es sich um ein Makro handelt. Damit können Methoden und Makros einfach auseinander gehalten werden. Da ich Hello world-Programme immer etwas sinnfrei finde was das Lernen einer neuen Programmiersprache angeht, wollte ich für den ersten Versuch das Spiel Zahlenraten programmieren. Ziel des Spieles ist es eine Zahl zwischen 0 und 1000 zu erraten, welche der Rechner sich ausgedacht hat. Dazu muss im ersten Schritt eine Variable definiert werden, in welcher die zufällige Zahl gespeichert wird. Grundsätzlich sieht eine Variablendefinition und Deklaration unter Rust wie folgt aus:

let name: Typ = Wert;

Variablen in Rust sind immer Konstanten, wenn sie nicht explizit als veränderlich angegeben werden. Möglich ist dies mit dem Schlüsselwort mut:

let mut name: Typ = Wert;

Die Benennung von Variablen und anderen Elementen folgt in Rust einem bestimmten Schema. So werden Variablen snake_case benannt. Neben der Definition der Variable muss eine zufällige Zahl zwischen 0 und 1000 generiert werden. Dazu dient ein Zufallsgenerator, welcher über ein use eingebunden werden muss:

use rand::Rng;

Anschließend kann die Variable definiert und deklariert werden:

let number: u32 = rand::thread_rng().gen_range(0, 1000);

In diesem Fall wird ein vorzeichenloser Integer mit 32 Bit Breite als Datentyp definiert. Wird nun versucht das Rust-Programm zu kompilieren, so wird die Fehlermeldung:

error[E0432]: unresolved import `rand`
 --> src\main.rs:1:5
  |
1 | use rand::Rng;
  |     ^^^^ use of undeclared type or module `rand`

error[E0433]: failed to resolve: use of undeclared type or module `rand`
 --> src\main.rs:4:23
  |
4 |     let number: u32 = rand::thread_rng().gen_range(0, 1000);
  |                       ^^^^ use of undeclared type or module `rand`

error: aborting due to 2 previous errors

auftauchen. Grund hierfür ist, dass das genutzte Paket über den Paketmanager von Rust bezogen werden muss. Pakete werden über den Paketmanager Cargo bezogen. Die entsprechenden Pakete des offiziellen Repositorys sind unter crates.io zu finden. In der Datei Cargo.toml müssen die entsprechenden Abhängigkeiten eingebunden werden:

[dependencies]
rand = "0.7.3"
text_io = "0.1.8"

Neben dem Pseudozufallszahlengenerator, wurde gleich noch das Paket text_io eingebunden, welches später für die Eingabe von Text benötigt wird. Dieses Paket stellt hierbei das Makro read zur Verfügung, mit dessen Hilfe eine Eingabe realisiert werden kann:

let user_number: u32 = read!();

Damit sind die Grundlagen für das Spiel Zahlenraten gelegt und der Rest des Quellcodes ergibt praktisch sich von selbst:

use rand::Rng;
use text_io::read;

fn main() {

    let number: u32 = rand::thread_rng().gen_range(0, 1000);

    let mut running = true;

    println!("Ich habe mir eine Zahl zwischen 0 und 1000 ausgedacht.");

    while running {

        println!("Dein Vorschlag: ");
        let user_number: u32 = read!();

        if number > user_number {
            println!("Meine Zahl ist größer.");
        } else if number < user_number {
            println!("Meine Zahl ist kleiner.");
        } else {
            running = false;
        }
    }

    println!("Du hast die Zahl erraten. Es war die {}.", number);
}

Hervorzuheben ist, das unter Rust in if-Statements und Schleifen keine Klammern benötigt werden. Werden diese trotzdem genutzt, so warnt der Compiler entsprechend und bittet die überflüssigen Klammern zu entfernen.

Neben der entsprechenden Dokumentation auf der offiziellen Seite empfiehlt sich das Rust Cookbook als Quelle für die ersten Schritte unter Rust. Wer Rust einfach mal im Browser ausprobieren möchte, kann hierfür den Rust Playground nutzen.

Restic für Backups nutzen

Für Backups nutze ich seit vielen Jahren rsync-time-backup. Allerdings hörte ich in letzter Zeit viel gutes über die freie Software Restic. Restic selbst wird über GitHub entwickelt und ist unter der BSD-Lizenz in der Zweiklausel-Version lizenziert. Unter Linux und macOS kann Restic einfach über entsprechende Paketmanager installiert werden:

brew install restic

Restic arbeiten mit sogenannten Repositories. In einem Repository befindet sich das entsprechende Backup mit all seinen Versionen. Um ein solchen Repository anzugelegen wirde der Befehl:

restic init --repo ./

genutzt. Bei Restic ist jedes Backup automatisch verschlüsselt, sodass bereits beim Anlegen eines Backups ein entsprechendes Passwort vergeben werden muss. Die Daten werden mit AES, bei 256 Bit, verschlüsselt.

restic.net

Danach kann theoretisch mit dem ersten Backup begonnen werden:

restic -r /Volumes/Volume/ResticRepository backup /Users/User

In diesem Fall würde der Ordner /Users/User/ in das Restic-Repository gesichert. Bevor das Backup startet, muss das entsprechende Passwort des Repositorys eingegeben werden. Anschließend wird der Nutzer über den Fortschritt des Prozesses informiert:

repository 567f35fa opened successfully, password is correct
created new cache in /Users/User/Library/Caches/restic
[0:09] 10 files 4.296 MiB, total 451 files 224.551 MiB, 0 errors
/Users/User/System/btrfstune
/Users/User/System/busybox
...

Nach dem Abschluss des Backups erscheint eine entsprechende Meldung im Terminal:

Files:          25 new,     0 changed,     0 unmodified
Dirs:            2 new,     0 changed,     0 unmodified
Added to the repo: 616.694 KiB

processed 25 files, 615.493 KiB in 0:00
snapshot 6c0d7af6 saved

Nun verfügt der Nutzer über ein Backup Repository mit einem bzw. mehreren Snapshots. Die angelegten Snapshots können über dem Befehl:

restic -r /Volumes/Volume/ResticRepository snapshots

angezeigt werden. Der Nutzer erhält eine entsprechende Ausgabe im Terminal:

repository fd5947c7 opened successfully, password is correct
ID        Time                 Host        Tags        Paths
------------------------------------------------------------------------------
6c0d7af6  2020-01-05 10:09:15  Earth.local             /Users/User/System
31d3160f  2020-01-05 10:11:30  Earth.local             /Users/User/System
38d6cbca  2020-01-05 10:15:09  Earth.local             /Users/User/System
ea96fa22  2020-01-05 10:15:20  Earth.local             /Users/User/System
------------------------------------------------------------------------------
4 snapshots

Das beste Backup nutzt nichts, wenn es nicht wiederhergestellt werden kann. Dazu wird die Option restore genutzt:

restic -r /Volumes/Volume/ResticRepository restore 38d6cbca --target /Users/User/System

Anschließend wird der gewünschte Snapshop wieder hergestellt:

repository fd5947c7 opened successfully, password is correct
restoring  to /Users/User/System

Soll anstatt eines bestimmten Snapshot der letzte Snapshot wiederhergestellt werden so wird anstatt einer Snapshot-ID einfach latest als Wert angegeben. Soll nur eine einzelne Datei wiederhergestellt werden, ist das komplette zurückspielen eines Backup eher suboptimal. Für einen solchen Fall können die Snapshots im Dateisystem gemountet werden.

restic -r /Volumes/Volume/ResticRepository mount /Volumes/Volume/ResticRepositoryMounted

Anschließend wird das Repository im Dateisystem unter dem angegebenen Mountpoint eingebunden:

repository fd5947c7 opened successfully, password is correct
Now serving the repository at /Volumes/Volume/ResticRepositoryMounted
When finished, quit with Ctrl-c or umount the mountpoint.

Im Gegensatz zu den Befehlen zur Wiederherstellung des Backups muss beim Mounten keine Snapshot-ID angegeben werden. In der gemounteten Struktur werden stattdessen alle Snapshots angezeigt. Die gewünschte Datei zur Wiederherstellung kann somit gesucht und wiederhergestellt werden.

Beim Backup sollen in vielen Fällen bestimmte Dateien nicht gesichert werden. Dies können z.B. temporäre Dateien oder Caches sein. Um diese Datei vom Backup auszuschließen, kann ein sogenanntes Exclude File genutzt werden:

restic -r /Volumes/Volume/ResticRepository backup /Users/User/System --exclude-file="/Users/User/excludes.txt"

Eine solche Datei könnte z.B. wie folgt aussehen:

+ /etc/
+ /home/
+ /root/
+ /srv/
+ /usr/local/
+ /var/

- /*
- /var/cache/*
- /var/lib/lxcfs/*
- /var/log/*
- /var/tmp/*

Neben diesen Basisfunktionalitäten, verfügt Restic über weitere Funktionen, so z.B. zum Löschen alter Snapshots nach bestimmten Regeln. Alles in allem wirkt Restic für mich wie eine durchdachte Backup-Lösung, deren Nutzung durchaus ins Auge gefasst werden kann.

apt vs. apt-get

Wurde früher unter Debian oder Ubuntu ein Paket installiert, so wurde das Kommando apt-get dazu genutzt:

apt-get install mc

Seit Ubuntu 14.04 gibt es neben den Kommandos rund um apt-get, das Kommando apt. Ab Ubuntu 16.04 wurde offiziell empfohlen apt anstatt von apt-get und apt-cache zu nutzen. Da stellt sich natürlich die Frage wie sich die Kommandos voneinander unterscheiden? Bei den alten Kommandos musste für die Paketverwaltung mit apt-get und apt-cache gearbeitet werden, je nachdem welche Operation benötigt wurde. Die Installation wurde mit apt-get install vorgenommen. Für eine Suche über die Pakete wurde stattdessen apt-cache benötigt:

apt-cache search mc

Mit dem damals neu eingeführten Kommando apt gibt es nun ein einheitliches Interface für die Paketverwaltung:

apt install mc
apt search mc

Neben der Vereinheitlichung, bietet apt einige weitere Vorteile, so kann unter anderem der Fortschritt einer Operation angezeigt werden.

apt zeigt unter anderem den Fortschritt der Operation an

Im Grunde ist apt eine Zusammenführung der am häufigsten genutzten Kommandos zur Paketverwaltung unter einem Kommando. Bestimmte obskure Low-Level-Operationen, welche bei apt-get und apt-cache noch zu finden waren, wurden bei apt zugunsten der Benutzbarkeit weggelassen. Zusätzlich dazu sind die Standardeinstellungen von apt sinnvoller gesetzt.

Partitionierung auf dem Terminal unter macOS

Für macOS war ich auf der Suche nach einem Tool für die Partitionierung von Datenträgern, welches sich für die Nutzung im Terminal eignete. Am liebsten wäre mir eine Variante von cfdisk gewesen, welches durch die einfache Bedienung glänzt. fdisk auf dem Terminal ist in vielen Fällen doch etwas zu spartanisch. Fündig geworden bin ich schlussendlich bei gptfdisk, welches über den Paketmanager Homebrew installiert werden kann:

brew install gptfdisk

Mit diesem Paket werden drei Anwendungen installiert: gdisk, sgdisk und cgdisk. Während die ersten beiden Anwendungen reine Kommandozeilentools sind, verfügt cgdisk über eine interaktive Oberfläche.

cgdisk ist ein Clone von cfdisk

Nach dem Aufruf der Anwendung mit dem passenden Laufwerk:

sudo cgdisk /dev/disk4

können die entsprechenden Operationen und Aktion über das Menü vorgenommen werden. Nach dem Abschluss der Operationen kann der Nutzer die Änderungen über das Write-Kommando auf die Festplatte schreiben und anschließend das Programm beenden.

Browserify unter Windows benutzen

Browserify ist ein unter der MIT-Lizenz stehendes Projekt, mit welchem Bundles aus modularen JavaScript-Projekten erstellt werden können. Auf der Projektseite wird die Nutzung von Browserify erklärt. Im ersten Schritt muss Browserify über den Paketmanager npm global installiert werden:

npm install -g browserify

Anschließend kann das Kommando genutzt werden:

browserify main.js -o bundle.js

Das Problem daran ist, dass das Kommando browserify nicht bekannt ist.

browserify.org

Stattdessen erhält der Nutzer folgende Fehlermeldung:

Der Befehl “browserify” ist entweder falsch geschrieben oder konnte nicht gefunden werden.

Damit Browserify genutzt werden kann, muss es direkt über das installierte Modul mittels Node.js aufgerufen werden:

node C:\Users\username\AppData\Roaming\npm\node_modules\browserify\bin\cmd.js src\index.js -o bundle.js

Alternativ kann Browserify direkt im Projekt installiert werden:

npm install
npm install browserify

Anschließend kann es von dort genutzt werden:

node node_modules\browserify\bin\cmd.js src\index.js -o bundle.js