Paketmanager – Software-Installation und Aktualisierung

Paketmanager haben den Prozess der Software-Installation und Aktualisierung vereinfacht und viele Probleme der Vergangenheit gelöst. Heute sind sie ein integraler Bestandteil vieler Systeme. Während der durchschnittliche Windows-Nutzer wahrscheinlich weniger mit der Begrifflichkeit eines Paketmanagers anfangen kann, sieht es bei Nutzern unixoider Systeme meist anders aus.

Vor allem bei Linux-Systemen sind sie ein essenzieller Bestandteil der allermeisten Distributionen und kommen hier in den unterschiedlichsten Formen vor. Mittlerweile haben sie sich allerdings darüber hinaus ausgebreitet und sind heute auch unter macOS und Windows zu finden.

Über diese Grenzen hinaus haben sich Paketmanager auch in anderen Bereichen etabliert hat, beispielsweise im Paket- und Abhängigkeitsmanagement innerhalb der Softwareentwicklung.

Unabhängig vom spezifischen Paketmanager folgen diese in der Regel einem ähnlichen Ablauf: Ein Nutzer beabsichtigt, eine Anwendung bzw. ein Paket zu installieren. Der Paketmanager identifiziert die erforderlichen Abhängigkeiten und installiert diese zusammen mit der gewünschten Software.

Definition

Doch was zeichnet einen Paketmanager aus? Grundsätzlich handelt es sich bei einem solchen um ein Werkzeug oder eine Sammlung an Werkzeugen, die dazu dient Software zu installieren, sie zu aktualisieren und wieder zu entfernen. Im Idealfall ist diese Entfernung rückstandslos. Auch die Konfiguration der Software ist eine Fähigkeit, welche von vielen Paketmanagementsystemen beherrscht wird.

Ziel ist es meist, die manuelle Installation und Verwaltung von Software unnötig zu machen, sodass diese im Idealfall immer über den Paketmanager bezogen werden kann.

Neben der eigentlichen Bereitstellung der gewünschten Software, ist ein wichtiger Teil des Paketmanagements die Installation und Verwaltung der Abhängigkeiten, welche von der Software benötigt werden. Dies umfasst beispielsweise die Handhabung verschiedener Versionen von Bibliotheken, die erforderlich sind, wenn mehrere installierte Anwendungen unterschiedliche Versionen einer Bibliothek benötigen.

Ein Paket umfasst in solchen Systemen, neben der eigentlichen Anwendung, eine Reihe von zusätzlichen Metadaten, welche als Informationen über das Paket und der Verwaltung dienen.

Daneben führen die Systeme Buch über installierte Software, was z. B. bei der Aktualisierung installierter Anwendungen von Belang ist.

Typen von Paketmanagern

Paketmanager lassen sich in verschiedene Typen einteilen. Einerseits existieren systemgebundene Paketmanager wie das Advanced Packaging Tool (APT), die integraler Bestandteil des jeweiligen Betriebssystems sind und eine konfliktfreie Installation von Anwendungen gewährleisten.

Ziel dieser Paketmanager ist die Softwareverwaltung für den Nutzer des Systems. Auch ist es bei diesen systemspezifischen Paketmanagern in den meisten Fällen so, dass Abhängigkeiten wie Bibliotheken im Idealfall nur einmal installiert werden.

Eine weitere Art von Paketmanagementsystemen sind App Stores. Hier steht jede Applikation für sich und wird mitsamt ihrer Abhängigkeiten installiert. Das bedeutet, dass z. B. Bibliotheken immer wieder mitgeliefert werden. Hier wird in der Theorie Speicherplatz verschenkt, da häufig verwendete Bibliotheken mehrfach vorhanden sein können.

Eine letzte und trotzdem in ihrer Wichtigkeit nicht zu unterschätzende Kategorie von Paketmanagern sind sprachspezifische Paketmanager. Bei diesen geht es um das Paket- und Abhängigkeitsmanagement von Bibliotheken im Rahmen der Softwareentwicklung. Beispiele für diese Manager sind Maven, Cargo und NPM. Sie werden vor allem in den vergangenen Jahren verstärkt eingesetzt. Einen Überblick über diese sprachspezifischen Paketmanager bietet die Webseite libraries.io.

Am Anfang war der Code

In frühen Systemen existierten keine Paketmanager im heutigen Sinne. Entweder wurden die mitgelieferten Systemwerkzeuge genutzt, oder die benötigte Software lag im Quelltext vor und wurde anschließend kompiliert und installiert.

Eine Applikation wird kompiliert

Im Laufe der Zeit wurden nicht nur die Systeme komplexer, sondern auch die auf ihr genutzten Anwendungen. Mithilfe von Build Automation Tools wie Make, wurde es möglich Software anhand des sogenannten Makefiles zu bauen. Allerdings wurde auch hier vorausgesetzt, dass die benötigten Abhängigkeiten auf dem System vorhanden waren.

Über den Befehl make kann der entsprechende Vorgang angestoßen werden. Damit vereinfachten Makefiles die Erzeugung der Software. Statt den Compiler, dazugehörige Linker und weitere Werkzeuge selbst aufrufen zu müssen, fungiert das Makefile als Mittler.

Abhängigkeiten

Im ersten Moment scheint es, als ob die Installation der Software aus dem Quelltext leicht von der Hand geht. Der Quelltext muss bezogen werden und anschließend kann die Applikation kompiliert und installiert werden.

Allerdings steht ein Programm meist nicht für sich, sondern ist auf gewisse Abhängigkeiten, wie verwendete Bibliotheken angewiesen. Sind diese Abhängigkeiten in einer falschen Version installiert, oder nicht vorhanden, schlägt die Erstellung der Applikation fehl.

Ein weiteres Problem ist, dass unixoide Systeme nicht unbedingt identisch sind und sich in kleineren und größeren Feinheiten unterschieden. Eine Lösung für diese Probleme bieten Werkzeuge wie autoconf vom GNU-Projekt und später CMake.

Über diese Build-Automatisierungstools wird das benötigte Makefile generiert, welches dann auf die Eigenheiten des eigentlichen Systems angepasst ist. So wird unter anderem überprüft, ob die benötigten Abhängigkeiten vorhanden sind und unter Umständen abgebrochen, wenn dies nicht der Fall ist.

Bislang nicht betrachtet wurde die Deinstallation einer Anwendung. Neben den eigentlichen Anwendungsdateien, eventuellen Bibliotheken und Konfigurationsdateien können hierzu auch Dateien zählen, welche während der Laufzeit der Anwendung erzeugt wurden.

Diese von Hand zu entfernen, ist im besten Fall ein zeitaufwendiger Prozess. Spätestens an dieser Stelle erweist sich ein funktionierendes Paketmanagementsystem als Segen.

Quell- vs. Binärpakete

Bei dem oben beschriebenen Verfahren wurde die Software direkt auf dem System kompiliert. Dies hat einige Vorteile. So kann die jeweilige Software mit entsprechender CPU-Optimierung kompiliert werden und somit optimal auf das System abgestimmt werden. Allerdings nimmt ein solcher Vorgang Zeit in Anspruch, vorwiegend bei der Kompilierung größerer Softwarepakete wie einem Browser.

Bei Paketmanagern wird hier die Unterscheidung zwischen Quell- und Binärpaketen getroffen. Quellpakete enthalten den Quellcode der Anwendung und werden direkt auf dem Rechner des Nutzers kompiliert.

Binärpakete hingegen enthalten eine vorkompilierte Anwendung, welche auf eine bestimmte Architektur optimiert ist. Damit muss das Paket nur noch vom Paketmanager heruntergeladen, entpackt und installiert werden. Neben der fehlenden Optimierung auf den konkreten CPU-Typ haben Binärpakete weitere Nachteile. Viele Anwendungen verfügen über bestimmte Schalter zu Compile-Zeit, um bestimmte Module und Funktionalitäten in die Anwendung zu integrieren. Ist dies während der Erstellung des Binärpaketes nicht geschehen, so kann das Modul bzw. die gewünschte Funktionalität nicht ohne Weiteres genutzt werden.

Anfänge der Paketmanager

Mit der Idee der Paketierung war der Gedanke zu einem Paketmanager nicht mehr weit. Auch wenn solche Manager unter Linux gängig wurden, gab es sie in Ansätzen bereits davor.

Einer der ersten Paketmanager war das System Management Interface Tool (SMIT) für AIX, welches mit der Version 3.0 von AIX im Jahr 1989 Einzug hielt. Unter der Oberfläche wurde für diese Aufgabe installp als Backend genutzt.

Im Linux-Bereich zählte das package management system (pms) zu den ersten Paketmanagern. Dieses erschien in Version 1.0 Mitte des Jahres 1994. Genutzt wurde dieses in der Distribution Bogus Linux. Dies führte historisch betrachtet unter anderem zum RPM-Paketmanager, welcher ursprünglich von Red Hat stammt und 1995 mit Red Hat Linux 2.0 ausgeliefert wurde.

In einen ähnlichen Zeitrahmen fallen die Entwicklung des Debian Package Managers, der vom StopAlop, einem weiteren Paketmanager aus der Frühzeit der Paketmanager, inspiriert wurde.

Die erste Version des Debian Package Managers wurde 1994 von Ian Murdock entwickelt, damals noch in Form eines Shellskriptes. Aus diesem entstand im Laufe der Jahre das dpkg der Neuzeit.

Aus diesen Low-Level-Paketmanagern entwickelten sich schließlich Systeme, welche Repositorys der verfügbaren Software bereithielten und diese zur Installation derselben nutzten, sodass auch das Problem der Paketbeschaffung bzw. der eigenen Paketierung in den meisten Fällen gelöst war.

Im Jahr 1995 begannen viele Paketmanager mit der Implementierung eines Workflows, der mit dem Herunterladen des Pakets beginnt und die automatische Auflösung sowie Installation von Abhängigkeiten beinhaltet.

Neben eigentlichen Applikationen wurden Systempaketmanager auch genutzt, um Bibliotheken und andere Funktionalität bestimmter Programmiersprachen wie Python über diese zu installieren. Heutzutage wird dies mehrheitlich über sprachspezifische Paketmanager gelöst.

Low-Level- und High-Level-Paketmanager

Werkzeuge wie dpkg, zählen wie oben bereits erwähnt zu den Low-Level-Paketmanagern. Zwar vereinfachen sie die Installation von Paketen, aber aus Sicht des Nutzers, sind immer noch viele manuelle Schritte notwendig, um das System auf einem aktuellen Stand zu halten.

Low-Level-Paketmanager samt Herkunft und High-Level-Paketmanager

Hier kommen High-Level-Paketmanager ins Spiel. Diese vereinfachen die Bedienung und dienen sozusagen als Frontend für den eigentlichen Nutzer. Daneben gruppieren sie Operationen der zugrundeliegenden Low-Level-Paketmanager.

Neben dem traditionellen Weg, Software als Archiv auszuliefern, wurde mit den Paketmanagern für die jeweiligen Distributionen ein zentrales Repository mit Software geschaffen, welches von der jeweiligen Distribution gepflegt wurde.

Interessant an diesem zentralen Repositorys ist, dass die Software, welche in diesen vorliegt, technisch betrachtet ein Fork der Originale ist. Der Vorteil dieser Vorgehensweise ist die Entkopplung, sodass eine Distribution eigene Aktualisierungen für eine Anwendung bereitstellen kann. Dies gilt auch für den Fall, dass die Software nicht mehr aktiv unterstützt wird.

Auch wenn von einem zentralen Repository die Rede ist, sieht es in den eigentlichen Distributionen meist etwas differenzierter aus. Unter Ubuntu z. B. existieren die Repositories Main, Universe, Restricted und Multiverse.

Das Main-Repository umfasst von Canonical unterstützte freie Software, die als grundlegend und essenziell für das System angesehen wird. Das Universe-Repository wird von der Community gepflegt und enthält ebenfalls freie Software, die von Nutzern beigetragen und verwaltet wird.

Im Restricted-Repository finden sich proprietäre Treiber für Geräte, die aus lizenzrechtlichen Gründen nicht im Main-Repository enthalten sind. Schließlich existiert noch das Multiverse-Repository, das Software beinhaltet, die durch Urheberrecht oder andere rechtliche Fragen eingeschränkt ist und deshalb spezielle Vereinbarungen für die Nutzung oder Verbreitung erfordert.

Daneben liegen für die unterschiedlichen Repositorys verschiedene Spiegelserver vor, welche die Pakete redundant und geografisch verteilt vorhalten.

Auch Abhängigkeiten werden von High-Level-Paketmanagern wesentlich sinnvoller behandelt. Während eine Paketinstallation per dpkg verlangt, dass alle Abhängigkeiten installiert sind, übernimmt apt diese Aufgabe automatisch. Hierbei werden die Abhängigkeiten in die korrekte Reihenfolge gebracht, bezogen und anschließend installiert.

Durch die zentralen Repositorys ist es über wenige Befehle möglich, den kompletten Softwarebestand zu aktualisieren. Auch der Wegfall von Abhängigkeiten wird bemerkt und so werden nicht mehr benötigte Pakete auf Wunsch automatisch deinstalliert.

Anatomie eines Paketmanagmentsystems

Einige Eigenschaften, welche ein Paket ausmachen, wurden bereits beschrieben. Trotzdem soll an dieser Stelle genauer auf die Anatomie eines Pakets und des Managementsystems dahinter eingegangen werden. Hierbei wird dpkg als Beispiel herangezogen.

Der Debian Package Manager ist dafür verantwortlich, ein Paket zu installieren und wieder zu deinstallieren. Hierzu wird die DEB-Datei, welche das Paket darstellt, im ersten Schritt entpackt und anschließend ein Pre-Install-Skript ausgeführt.

Nach dessen Ausführung werden die Komponenten des Paketes an die korrekten Stellen im Dateisystem kopiert und anschließend das Post-Install-Skript ausgeführt. Bei der Deinstallation läuft dieser Vorgang ähnlich ab. Auch hier werden wieder Pre– und Post-Remove-Skripte durchgeführt. Daneben verwaltet dpkg eine Datenbank der installierten Pakete.

Im Einzelnen besteht ein DEB-Paket aus dem sogenannten Debian-Binary. In dieser Datei ist die Version des Dateiformates hinterlegt. Dies sollte bei aktuellen Distributionen immer 2.0 sein. Trotz ihres Namens handelt es sich um eine gewöhnliche Textdatei.

Ein entpacktes DEB-Archiv mit dem control-Ordner

Anschließend folgen zwei Archive, eines für die Meta-Informationen und eines für die eigentlichen Daten. Bei den Archiven werden zwei unterschiedliche Archivierungsverfahren unterstützt. So kann im Falle des control-Archivs das Archiv als control.tar.gz oder control.tar.xz vorliegen.

Das control-Archiv enthält mehrere wichtige Dateien. Die erste Datei ist die Datei control. Diese enthält Informationen über das Paket wie Paketname, Version, Abhängigkeiten, Konflikte, Beschreibung und mehr.

Für das Paket nginx-common sieht diese Datei beispielhaft wie folgt aus:

Package: nginx-common
Source: nginx
Version: 1.24.0-2
Architecture: all
Maintainer: Debian Nginx Maintainers 
Installed-Size: 306
Depends: debconf (>= 0.5) | debconf-2.0, nginx (>= 1.24.0-2), nginx (<< 1.24.0-2.1~)
Suggests: fcgiwrap, nginx-doc, ssl-cert
Breaks: nginx (<< 1.22.1-8)
Replaces: nginx (<< 1.22.1-8)
Section: httpd
Priority: optional
Multi-Arch: foreign
Homepage: https://nginx.org
Description: small, powerful, scalable web/proxy server - common files
 Nginx ("engine X") is a high-performance web and reverse proxy server
 created by Igor Sysoev. It can be used both as a standalone web server
 and as a proxy to reduce the load on back-end HTTP or mail servers.
 .
 This package contains base configuration files used by all versions of
 nginx.

Daneben sind die entsprechenden Pre- und Postskripte enthalten (preinst, postinst, prerm, postrm). Diese Skripte werden verwendet, um spezielle Aufgaben auszuführen, die für das Paket notwendig sind, wie das Konfigurieren von Systemdiensten oder das Aktualisieren von Konfigurationsdateien.

Die Datei conffiles enthält eine Liste von Konfigurationsdateien, die vom Paketmanagementsystem während einer Aktualisierung behandelt werden, um benutzerdefinierte Änderungen zu erhalten.

Über die Datei md5sums, eine Liste von MD5-Prüfsummen für die Dateien, die im Paket enthalten sind, kann die Integrität dieser überprüft werden.

Der entpackte data-Ordner in einem DEB-Archiv

Die eigentlichen Daten des DEB-Archives finden sich im data-Archiv (data.tar.gz oder data.tar.xz). Dieses Archiv enthält die Dateien, die zum System hinzugefügt werden, wenn das Paket installiert wird. Die Dateien in diesem Archiv werden relativ zum Wurzelverzeichnis des Ziel-Dateisystems extrahiert.

Paketdatenbank

Neben den eigentlichen Paketen nimmt die Paketdatenbank einen großen Stellenwert ein. Die Paketdatenbank von Debian und darauf basierenden Distributionen wird von dpkg verwaltet und speichert Informationen über alle installierten, gelöschten oder sonst wie bekannten Pakete auf dem System. Die Datenbank befindet sich im Verzeichnis /var/lib/dpkg/ und besteht aus mehreren Dateien und Verzeichnissen, die verschiedene Aspekte der Paketverwaltung abdecken.

Die Datei /var/lib/dpkg/status enthält den aktuellen Status aller Pakete. Sie listet Pakete auf, die installiert sind, deren Installation erwartet wird, die zur Deinstallation oder vollständigen Entfernung markiert sind, und so weiter. Für jedes Paket enthält diese Datei Metadaten wie Version, Architektur, Abhängigkeiten, Beschreibung und vieles mehr.

Die Paketdatenbank des Debian Package Managers

Die Datei /var/lib/dpkg/available enthält Informationen über verfügbare Pakete, aus den Repositorys. Diese Datei wird z. B. durch den Befehl apt update aktualisiert.

Das Verzeichnis /var/lib/dpkg/info/ enthält spezifische Dateien für jedes Paket, wie Konfigurationsskripte. Diese Dateien werden von dpkg während der Installation und Deinstallation verwendet, um sicherzustellen, dass diese Prozesse korrekt durchgeführt werden.

Die Paketdatenbank wird von dpkg und anderen Frontends wie APT, Aptitude oder Synaptic verwendet, um Paketoperationen durchzuführen. Es ist wichtig, dass diese Datenbank konsistent und unbeschädigt bleibt, da Inkonsistenzen zu Problemen bei der Paketverwaltung führen können.

Die Konsistenz der Paketdatenbank in Debian-basierten Systemen wird durch eine Kombination aus Designentscheidungen, Dateisystemtransaktionen und Sperrmechanismen sichergestellt.

Welche Version darf es sein?

Eine Paketverwaltung im Distributionsumfeld, kann trotz ihrer Vorteile, einige Herausforderungen mit sich bringen. Je nach der Politik der gewählten Distribution kann es sein, dass nur bestimmte und unter Umständen veraltete Versionen gepflegt werden. Dies ist z. B. bei Debian Stable der Fall, während bei anderen Distributionen wie bei Arch Linux immer die neusten Anwendungen, dank des Rolling Releases-Prozesses, mitgeliefert werden.

Die Nutzung eines Paketmanagers kann dazu führen, dass Nutzer weniger Kontrolle über spezifische Konfigurationen der installierten Software haben, da viele Einstellungen bereits festgelegt wurden.

Die Sicherheit hängt zudem von der Vertrauenswürdigkeit der Softwarequellen, den sogenannten Repositorys, ab. Eine Kompromittierung eines Repositorys kann die Verbreitung schädlicher Software begünstigen. Trotz verschiedener Sicherheitsmaßnahmen kann ein solches Risiko nicht gänzlich ausgeschlossen werden.

Paketmanager je Betriebssystem

Neben der grauen Theorie werden Paketmanager natürlich auch genutzt. Hierfür stehen je nach Betriebssystem unterschiedlichste Paketmanager zur Verfügung, von deinen einige nachfolgend vorgestellt werden sollen.

Linux

Unter Linux existieren eine Vielzahl an Paketmanagern. Zu den häufigeren verwendeten gehört sicherlich dpkg mitsamt seiner Frontends, wie APT. Auf Debian basierende Distributionen wie Ubuntu nutzten dieses System ebenfalls.

Ein weiterer bekannter Paketmanager ist RPM, welcher unter anderem bei Red Hat Linux zum Tragen kommt. RPM steht hierbei für RPM Package Manager, welcher ursprünglich als Red Hat Package Manager bezeichnet wurde.

Im Laufe der Zeit wurde der RPM Package Manager weiterentwickelt und verbessert. Das RPM-Format selbst wurde standardisiert, und es wurden Werkzeuge wie YUM (Yellowdog Updater, Modified) und später DNF (Dandified Yum) entwickelt, die als Frontends für RPM dienen und zusätzliche Funktionen wie einfachere Abhängigkeitsauflösung und automatische Updates bieten.

Daneben existieren weitere Paketmanagementsysteme wie Pacman unter Arch Linux. Üblicherweise ist das Paketverwaltungssystem eines der Systeme, die näher betrachtet werden, wenn sich intensiver mit einer Distribution auseinandersetzt wird.

Neben diesen gewöhnlichen Paketmanagern gibt es auch neue Konzepte, wie Nix und NixOS, welche deklarative Ansätze für die Paketverwaltung nutzen.

Snap, Flatpak und Co.

In der Linux-Welt sind zusätzlich zu den Systempaketmanagern weitere Paketformate entstanden, die darauf abzielen, Softwarepakete unabhängiger von den einzelnen Distributionen zu gestalten.

Unter Ubuntu ist das Snap-Format stark vertreten, bei anderen Distributionen hingegen Flatpack. Snap und Flatpak sind moderne Paketmanagement- und Bereitstellungssysteme, die das Ziel haben, die Installation und Verwaltung von Software auf Linux-Systemen zu vereinfachen und zu vereinheitlichen. Sie ergänzen traditionelle Paketmanager wie APT und bieten einige Vorteile.

Snap ist ein Paketformat, das von Canonical entwickelt wurde. Snap-Pakete sind in sich geschlossene Softwarepakete, die alle notwendigen Abhängigkeiten enthalten, um auf einer Vielzahl von Linux-Distributionen zu laufen. Das Snap-System verwendet ein zentrales Repository namens Snap Store, in dem der Nutzer Software suchen und installieren kann.

Snaps sind in der Regel größer als traditionelle Pakete, da sie alle Abhängigkeiten enthalten, bieten dafür aber andere Vorteile. So laufen Snaps in einer Sandbox-Umgebung, die die Sicherheit erhöht, indem sie den Zugriff der Anwendung auf das System beschränkt.

Durch den Dienst snapd, werden Snaps automatisch aktualisiert, was die Wartung vereinfacht. Aus Entwicklersicht können Anwendungen leichter veröffentlicht und aktualisiert werden, da nicht diese nicht auf die Paketverwaltung der einzelnen Distributionen angewiesen sind.

Flatpak ist ein ähnliches System, entwickelt von der unabhängigen Community. Es zielt ebenfalls darauf ab, distributionsübergreifend Software bereitzustellen und verwendet für die Verteilung von Softwarepaketen sogenannte Remotes wie Flathub. Flatpaks können auf einer Vielzahl von Linux-Distributionen laufen. Ähnlich wie Snaps bieten Flatpaks eine Sandbox-Umgebung, die die Sicherheit verbessern soll.

Beide Systeme, Snap und Flatpak, tragen in der Theorie dazu bei, die Fragmentierung im Linux-Ökosystem zu verringern und die Softwareverteilung zu vereinfachen. Sie bieten eine Plattform für Entwickler, um ihre Anwendungen einem breiteren Publikum zur Verfügung zu stellen. Der Nutzer kann über diese Systeme Anwendungen unabhängig von der spezifischen Linux-Distribution installieren.

AppImage ist ein weiteres Format für portable Softwarepakete unter Linux. Im Gegensatz zu Snap und Flatpak wird bei AppImages keine Installation durchgeführt. Stattdessen sind AppImages eigenständige ausführbare Dateien, die alle Abhängigkeiten enthalten und direkt ausgeführt werden können.

macOS

Unter macOS existieren neben dem integrierten App Store, welcher 2011 eingeführt wurde, weitere Paketmanager, bei denen es sich um Community-Projekte handelt.

Der App Store unter macOS

Der App Store selbst ist im Gegensatz zu seinem iOS-Pendant nicht verpflichtend zu nutzen. Auch wenn unsignierte Software mittlerweile nur nach einigen Warnmeldungen gestartet werden kann.

Bei den Community-Projekten stechen die Werkzeuge MacPorts und Homebrew hervor. MacPorts, früher unter dem Namen DarwinPorts bekannt, ist seit 2002 verfügbar und liegt mittlerweile in Version 2.8.1 vor.

MacPorts ist darauf ausgelegt, für jeden Port alle Abhängigkeiten selbst aus dem Quelltext zu kompilieren und zu verwalten. Dies führt zu einer größeren Isolation und Konsistenz, kann aber auch die Nutzung von mehr Speicherplatz und längere Installationszeiten bedeuten.

Der Paketmanager Homebrew wurde 2009 von Max Howell entwickelt. Homebrew versucht, wo möglich, vorhandene Systembibliotheken zu nutzen und installiert nur Abhängigkeiten die darüber hinaus benötigt werden. Dies kann zu schnelleren Installationen führen, birgt aber auch das Risiko eines Konfliktes mit Systembibliotheken.

Homebrew wird oft als benutzerfreundlicher wahrgenommen, mit einfacheren Befehlen und einer einfacheren Installation. Daneben verfügt es über eine breite Unterstützung für Binärpakete, die auf eine schnelle Installation abzielen und das Softwareangebot erweitern.

Die Anwendung der jeweiligen Paketmanager bleibt hierbei dem Nutzer überlassen, je nach dem gewünschten Anwendungszweck. So benötigt MacPorts Administratorrechte, während Homebrew in den meisten Fällen ohne solche auskommt. Genutzt werden beide Paketmanager über das Terminal.

Windows

An Windows ist der Erfolg der Paketmanager ebenfalls nicht vorbeigegangen. So wurde schon seit Windows Vista die Applikation Pkgmgr.exe mitgeliefert. Dabei handelte es sich um einen Paketmanager zur Installation und Deinstallation von Paketen.

Allerdings war dieser Paketmanager nicht für den Nutzer des Systems gedacht. Stattdessen diente er dazu, Komponenten des Betriebssystems zu installieren. Später wurde dieses System insbesondere durch DISM (Deployment Image Servicing and Management) abgelöst.

Neben dem Microsoft Store, welcher als App Store fungiert, existieren auch für Windows eine Reihe von Community getriebenen Paketmanagern. Hier wären unter anderem Chocolatey und Scoop zu nennen. Microsoft hat mit dem Windows Package Manager (winget) ebenfalls einen solchen Paketmanager vorgestellt. Eine detaillierte Betrachtung dieser Paketmanager findet sich auf Golem.de.

Daneben existieren auch Client-Managment-Plattformen, wie ACMP, welche für die Nutzer die Softwareinstallation aus einem Katalog ermöglichen und meist im geschäftlichen Umfeld zu finden sind.

Mobile Systeme

Während Systeme wie die PDAs von Palm überwiegend von Hand mit Apps bestückt wurden, sah dies bei den großen mobilen Systemen der Neuzeit, namentlich Android und iOS anders aus. Hier gab es bereits zu Beginn entsprechende
App Stores.

Google Play

Unter Android war dies der Android Market, welcher schließlich in Google Play aufging, unter iOS der App Store, welcher mit der Version iOS 2 (iPhone OS 2.0) seinen ersten Auftritt hatte.

Software kann über diese App Stores installiert, aktualisiert und deinstalliert werden. Im Unterschied zu reinen Paketmanagement-Lösungen bieten diese App Stores zusätzliche Dienste. Sie wickeln unter anderem Zahlungen ab, was sowohl In-App-Käufe als auch Abonnements einschließt.

Im Android-Bereich existieren daneben weitere alternative App Stores, wie der F-Droid App Store, welcher auf freie Software spezialisiert ist. Unter iOS ist dies bislang nicht ohne Jailbreak möglich. Dies soll sich allerdings durch den Digital Markets Act in der EU ändern.

Auch andere mobile Ökosysteme nutzen ihre jeweiligen App Stores wie Amazon, Samsung und Huawei.

Fazit

Ian Murdock, einer der Mitbegründer des Debian-Projektes, nannte Paketmanager einmal den größten einzelnen Fortschritt, welchen Linux der Industrie bescherte.

Sie erleichtern die Handhabung von Abhängigkeiten und Kompatibilitätsproblemen, die sonst für den Nutzer eine Herausforderung darstellen könnten. Mit dieser Idee haben sie viele Domänen erobert.

So begegnet uns das Konzept der Paketierung immer wieder, z. B. bei Docker-Containern. Auch bei neuen Programmiersprachen, wie Rust, wird das Paketmanagement gleich mitgedacht.

Paketmanager nehmen eine wichtige Rolle ein, indem sie die Installation, Aktualisierung und Entfernung von Softwarepaketen auf effiziente und benutzerfreundliche Weise ermöglichen und uns so auch in Zukunft begleiten werden.

In Zukunft wird auch verstärkt der Fokus auf unveränderliche Systeme und containerisierte Anwendungen gerichtet sein. Dieser Ansatz hat in jüngster Zeit mit Technologien wie Podman und Co. den Weg zurück auf den Desktop gefunden und spiegelt die wachsende Präferenz für isolierte, konsistente und portable Anwendungsumgebungen wider.

Dieser Artikel erschien ursprünglich auf Golem.de und ist hier in einer alternativen Variante zu finden.

Mainboard-Informationen unter Linux ermitteln

Manchmal ist es notwendig im Terminal unter Linux einige Informationen über das verbaute Mainboard zu ermitteln. Hierfür dient der Befehl:

dmidecode -t 2

Diese liest die entsprechenden Daten auf dem sysfs und zeigt sie an:

# dmidecode 3.3
Getting SMBIOS data from sysfs.
SMBIOS 3.2.1 present.

Handle 0x0002, DMI type 2, 15 bytes
Base Board Information
	Manufacturer: ASUSTeK COMPUTER INC.
	Product Name: WS C246 DC
	Version: Rev 1.xx
	Serial Number: 190856070000221
	Asset Tag: Default string
	Features:
		Board is a hosting board
		Board is replaceable
	Location In Chassis: Default string
	Chassis Handle: 0x0003
	Type: Motherboard
	Contained Object Handles: 0

Verwaiste Screen-Session entfernen

Der Befehl screen dient dazu Terminal-Sitzungen zu erzeugen, welche im Hintergrund weiterlaufen können. Allerdings kann es hierbei unter Umständen passieren, das eine solche Session verweist bzw. nicht mehr reagiert. In einem solchen Fall kann die Session beendet werden. Tritt dies bei mehreren Sitzungen auf, kann der Befehl:

screen -wipe

genutzt werden. Der Befehl überprüft alle laufenden screen-Sitzungen und entfernt die Einträge von abgestürzten oder nicht mehr existierenden screen-Sitzungen.

Coding Conventions

Die Entwicklung von Software zeichnet sich in der heutigen Welt oft dadurch aus, dass sie unter Mitwirkung unterschiedlichster Entwickler bewerkstelligt wird. Im Rahmen einer solchen Entwicklung kommt es darauf an, bestimmte Standards und Best Practices einzuhalten.

Neben dem passenden Workflow kommen hier Coding Conventions zum Tragen und bilden einen wichtigen Baustein um Quelltext effizienter, lesbarer und zuverlässiger zu gestalten.

Was sind Coding Convention?

Eine Coding Convention definiert sich über bestimmte Stilregeln und Best Practices, bezogen auf eine Programmiersprache. Innerhalb der Konvention werden viele Aspekte der Programmiersprache und ihrer sprachlichen Elemente behandelt. Dies fängt bei Regeln zur Formatierung an, führt sich fort mit der Benamung von Variablen und anderen Strukturen und erstreckt sich auch auf andere Bereiche, wie Reihenfolgen und Zeilenlängen.

Warum werden sie benötigt?

Nun kann sich natürlich die Frage gestellt werden, warum eigentlich Coding Conventions benötigt werden?

Neben offensichtlichen Gründen, dass sie vielleicht eine Anforderung des Kunden sind, gibt es auch andere Gründe für diese Konventionen. So werden die meisten Projekte nicht von einer einzelnen Person betreut und für die Entwickler eines Produktes ist es einfacher, wenn der Quelltext nach identischen Standards entwickelt wurde. Dies ermöglicht eine schnellere Einarbeitung und hilft auch bei anderen Dingen, wie der Verminderung von Merge-Konflikten bei der Arbeit mit Versionskontrollsystemen.

Damit tragen diese Konventionen dazu bei, die Zusammenarbeit zwischen Entwicklern zu erleichtern, indem sie eine einheitliche und konsistente Basis schaffen.

Eine Welt ohne Coding Conventions

Natürlich können Programme auch ohne Coding Conventions geschrieben werden. Dies kann zu interessanten Programmen führen:

#include 

...

yeet Yeet Yeeet yeeeT 
Yeeeet
yEet yEEt yeEt yyeet yeett yeetT
yeeT yet yeetT
yeeeeT

Bei diesem Programm stellen sich bei der Betrachtung mehrere Fragen. In welcher Sprache ist es geschrieben? Ist es überhaupt lauffähig? Und was ist der eigentliche Zweck des Quelltextes?

In diesem Beispiel wurde das C-Programm so gestaltet, dass es möglichst unlesbar ist, indem mit entsprechenden Definitionen gearbeitet wurde, welche anschließend im Quelltext genutzt werden.

#define yeet int
#define Yeet main
#define yEet std
#define yeEt cout
#define yeeT return
#define Yeeet (
#define yeeeT )
#define Yeeeet {
#define yeeeeT }
#define yyeet <<
#define yet 0
#define yeett "Yeet!"
#define yeetT ;
#define yEEt ::

Es zeigt auf, dass ohne einheitliche Coding Conventions im besten Fall Chaos droht. Auf die Spitze treibt das auch der International Obfuscated C Code Contest, bei welchem es darum geht, Quelltext möglichst so zu verschleiern, dass nur schwer zu erraten ist, welche Funktion dieser am Ende in sich trägt.

Eine Implementation des zcat-Kommandos

In diesem Beispiel wird der Befehl zcat zur Darstellung mittels gz-komprimierter Daten implementiert. Auch ohne solche Extrembeispiele würde in einer Welt ohne Coding Conventions eine Menge an inkonsistentem Code entstehen:

int counterServer = 1               ;
int counterClient = 2               ;
int counterDevice = 3               ;
int test1 = 4                       ;

Natürlich kann ein Quelltext so formatiert werden, aber in den meisten Fällen erschwert dies die Lesbarkeit des Quelltextes enorm. Auch die Nutzung von Whitespaces und falscher Einrückung kann zu Problemen führen:

if        (system==true) {
    doSomething()        ;
doFoobar                       ();
}

Auch Richtlinien über Komplexität sind ein wichtiger Bestandteil, solcher Konventionen. Gegeben sei folgendes Programm:

#include

main(){
  int x=0,y[14],*z=&y;*(z++)=0x48;*(z++)=y[x++]+0x1D;
  *(z++)=y[x++]+0x07;*(z++)=y[x++]+0x00;*(z++)=y[x++]+0x03;
  *(z++)=y[x++]-0x43;*(z++)=y[x++]-0x0C;*(z++)=y[x++]+0x57;
  *(z++)=y[x++]-0x08;*(z++)=y[x++]+0x03;*(z++)=y[x++]-0x06;
  *(z++)=y[x++]-0x08;*(z++)=y[x++]-0x43;*(z++)=y[x]-0x21;
  x=*(--z);while(y[x]!=NULL)putchar(y[x++]);
}

Bei diesem handelt es sich um ein einfaches Hello World-Programm, aber das Verständnis wird durch die Umsetzung, genauer gesagt dessen unnötige Komplexität, sehr erschwert.

Auch wenn sich auf Einrückungen geeinigt wird, ist dies nicht immer sinnvoll:

function f() {
  doThings();
  doMoreThings();
             }

Bei diesem Beispiel wird eine Einrückung genutzt, welche ungebräuchlich ist und bei den meisten Entwicklern wahrscheinlich auf Ablehnung stoßen wird und nicht dazu führt, dass der Quelltext übersichtlicher wird.

Die Benamung von Elementen ist ein wichtiger Teil von Coding Conventions:

void doSomeTHING() {
  int test1 = 1;
  int TEST2 = 2;
  int teST3 = 3;
}

void DoSomething() {
  int tEST4 = 4;
}

Wird bei dieser nicht auf Konsistenz geachtet, trägt dies nicht zum besseren Verständnis bei. Auch Kommentare, bzw. das Schreiben derselben sind eine Aufgabe, bei der sorgfältig gearbeitet werden sollte:

try {
  ...
} catch(Exception e) {
  // Gotta Catch 'Em All
}

Natürlich ist Humor im echten Leben wichtig, aber in einem Quelltext sollte er nichts zu suchen haben. Stattdessen sollte sich hier auf die Fachlichkeit bezogen werden.

Auch die Nutzung unüblicher Vorgehensweisen bzw. das Verstecken bestimmter Operationen erschwert das Verständnis eines Quelltextes:

int main() {

  String helloWorld = "Hello World!";
  cout << helloWorld << endl;

  String hello = (helloWorld, 5);
  cout << hello << endl;

  system("pause");
  return 0;
}

Ohne weitere Informationen ist es relativ schwer herauszufinden, was in diesem Beispiel passiert. Hier werden die ersten fünf Stellen der Zeichenkette Hello World! zurückgegeben. In diesem Fall geschieht dies über die Überladung des Komma-Operators:

using namespace std;

#define String string

String operator,(String lhs, int rhs) {
  lhs.erase(lhs.begin() + rhs, lhs.end());
  return lhs;
}

Auch eine schlechte Benennung und ein Sprachmischmasch kann das Verständnis des Quelltextes erschweren:

#include 

gibHalloWeltAus()
{
    // use cout for output
    cout << "Hello World!" << endl;

    // Rückgabereturn
    return 0;
}

Ziele von Coding Conventions

Wenn sich diese Beispiele aus der Welt ohne Coding Conventions angeschaut werden, können aus diesen einige Ziele für entsprechende Konventionen abgeleitet werden.

Es geht darum, dass Coding Conventions bewährte Praktiken abbilden und für einen lesbaren und verständlichen Quelltext sorgen. Sie sollen die Zusammenarbeit im Team erleichtern und eine gewisse Einheitlichkeit herstellen.

Daneben sind Coding Conventions und das Konzept von Clean Code miteinander verbunden. Clean Code ist ein Konzept, das sich auf die Softwareproduktion und das Programmierdesign bezieht. Es legt fest, dass Quelltext so geschrieben werden sollte, dass er einfach zu lesen, zu verstehen und zu warten ist. Die Einhaltung von Coding Conventions kann dazu beitragen, diese Kriterien einzuhalten.

Elemente von Coding Conventions

Doch woraus genau bestehen Coding Conventions im Einzelnen? Im ersten Schritt sollte sich bewusst gemacht werden, dass sich solche Konventionen von Sprache zu Sprache unterscheiden. Auch wenn sich im Laufe der Zeit einige Standards herauskristallisiert haben, können diese nicht immer eins zu eins auf die eigenen Anforderungen angewendet werden.

Unterschiedliche Elemente von Coding Conventions

Im Einzelnen setzen sich Coding Conventions aus Elementen zusammen, welche im folgenden genauer besprochen werden sollen.

Benamung

Ein essenzielles Element ist die Benamung innerhalb eines Entwicklungsprojektes. Dies fängt bei Dateinamen und Verzeichnissen an, zieht sich hin zu Bezeichnern, wie den Namen von Variablen, Klassen und vielen anderen Elementen.

Grundsätzlich sollte bei der Benamung von Elementen immer so viel wie nötig und so wenig wie möglich benannt werden.

Dateinamen

Da Coding Conventions sich von Sprache zu Sprache unterscheiden, existieren bereits Unterschiede auf Ebene der Dateinamen. Während Dateien von C-Programmen meist in Kleinschreibung benannt werden:

main.c
tilerenderer.c

sieht dies bei Java-Applikationen anders aus:

Main.java
TileRenderer.java

Neben den Dateinamen bezieht sich dies auch auf die Benamung und Struktur von Verzeichnissen. In C würde dies wie folgt aussehen:

src
  engine
  renderer
  utils

während in Java meist die Struktur des Packages abgebildet wird. Bei dem Package com.example.transformer.html würde die entsprechende Verzeichnisstruktur wie folgt aussehen:

src
  main
    java
      com
        example
          transformer
            html
            markdown
  test

Eine weitere Eigenart von Java ist, dass die Namen der Packages eine Domain-Struktur abbilden und z. B. mit der Domain der Firma beginnen.

Neben den Coding Conventions für die jeweilige Sprache gehen bei der Strukturierung des Projektes auch noch andere Aspekte ein. Wird z. B. mit dem Build-Werkzeug Maven gearbeitet, so gilt dort Konvention vor Konfiguration.

Bei Maven bedeutet dies, dass eine Reihe von Standardregeln existieren, die vom Benutzer des Werkzeuges befolgt werden müssen, um ein Projekt erfolgreich zu erstellen. So muss ein Projekt in einer bestimmten Struktur organisiert sein, damit Maven es erfolgreich verarbeiten kann. Diese Standards erleichtern es, ein Projekt mit Maven zu erstellen, da der Benutzer nicht jeden einzelnen Schritt konfigurieren muss.

Sprechende Namen

Auch bei der Benamung sollten gewissen Standards eingehalten werden:

int a = getSum();

In diesem Fall wird eine Methode mit dem Namen getSum aufgerufen und das Ergebnis in der Variable a gespeichert. Hier sollte mit sprechenden Namen gearbeitet werden. Solche Namen zeichnen sich dadurch aus, dass sie beim Lesen bereits Aufschluss über ihre Fachlichkeit und deren Bedeutung geben:

int sum = getSum();

Damit wird klar, dass sich in der Variable sum eine entsprechende Summe befindet. Theoretisch kann die Benennung natürlich noch weiter spezifiziert werden:

int sumArticles = getSum();

Unter Umständen können Namen hierbei etwas länger werden, aber dafür wird Klarheit gewonnen. Diese Art der Benamung sollte nicht nur für Variablen, sondern generell für Bezeichner, wie Klassennamen gelten.

Allerdings keine Regel ohne Ausnahme, z. B. bei Exceptions unter Java:

try {
  // Try some funky stuff
} catch(Exception e) {
  // Handle exception
}

Dort hat es sich eingebürgert, einer Exception den Namen e bzw. ex zu geben. Sind solche Konventionen vorhanden und weitverbreitet, sollten diese entsprechend eingehalten werden. Auch hier dient das Einhalten dieser Regeln dazu, die Lesbarkeit und Wartbarkeit des Quelltextes zu erhöhen.

Verbotene Bezeichner

Es gibt eine Reihe von Bezeichnungen, welche in der Theorie, je nach verwendeter Sprache, verwendet werden können, es aber nicht sollten.

So ist es in Sprachen wie C# möglich, mit einem vorgestellten At-Zeichen Schlüsselwörter der Sprache als Bezeichner verwenden zu können. Um Verwirrung und darauf aufbauende Probleme zu vermeiden, sollte dies unterlassen werden.

Andere Bezeichner, wie handle, sollten nur in einem eng begrenzten Kontext oder einer entsprechenden Fachlichkeit benutzt werden.

Auch die Nutzung von Variablen mit dem Namen temp oder tmp sollte unterlassen werden, da meist eine entsprechend sinnvollere fachliche Benamung möglich ist.

Schleifen und Benamung

Wie bei Exceptions haben sich auch bei Schleifen bestimmte Konventionen zur Benamung eingebürgert, an welche sich gehalten werden sollte:

for(int i = 0; i < 10; i++) {

    for(int j = 0; j < 10; j++) {

      // Do stuff
    }
}

So wird die Zählervariable bei Schleifen mehrheitlich mit dem Namen i benannt und wenn in der Schleife weitere Schleifen geschachtelt werden, so werden diese fortlaufend mit j, k und so weiter benannt.

Aber auch hier kann in Ausnahmen davon abgewichen werden. Ein Beispiel hierfür wäre z. B. die Verarbeitung eines Bildes:

for(int y = 0; y < image.height; y++) {

    for(int x = 0; x < image.width; x++) {

      // Do image stuff
    }
}

Hier wird sich auf die x- und y-Achse des Bildes bezogen und durch die entsprechende Benamung kann sinnvoll mit diesen in der eigentlichen Logik der Schleife gearbeitet werden.

Kamele, Dromedare und Schlangen

Bezeichner können wie bei obigem Beispiel einfache Namen bestehend aus einem Wort sein, bestehen aber in vielen Fällen aus mehreren Wörtern.

Unterschiedlichste Schreibvarianten bei zusammengesetzten Bezeichnern

Um diese sinnvoll miteinander zu verbinden werden je nach Sprache unterschiedliche Varianten von Binnenmajuskeln benutzt, welche je nach Verwendung treffende Namen wie CamelCase und Ähnliche tragen. Diese Schreibweise sorgt letztlich für eine bessere Lesbarkeit, da sie einzelne Wörter sinnvoll voneinander abgrenzt.

Binde- und Unterstriche

Neben der Schreibweise mittels Binnenmajuskeln existieren auch andere Schreibweisen, was sich im Beispiel wie folgt darstellt:

do_things_fast();
do-things-fast();

So wird in Sprachen wie C und Perl auch auf Unterstriche zurückgegriffen und auch in PHP war dies bis zur Version 4 der Fall. Die Schreibweise mit dem Bindestrich, welche auch als lisp-case bekannt ist, wurde unter anderem in COBOL und Lisp genutzt.

Auch bei Rust wird teilweise auf Unterstriche als auch auf CamelCase gesetzt.

Ausnahmen bei der Benamung

Je nach Sprache wird damit meist eine bestimmte Schreibweise für Bezeichner wie den Namen von Variablen genutzt, allerdings existieren hiervon auch Ausnahmen bzw. Abweichungen, wie bei der Definition von Konstanten:

public static final String SECRET_TOKEN = "X7z4nhty3287";

Diese werden in vielen Fällen komplett großgeschrieben und meist mit Unterstrichen unterteilt. Auch hier gilt wieder, dass solche Konstanten möglichst sprechend benannt werden sollten und auf Abkürzungen und Ähnliches verzichtet werden sollte.

Prä- und Suffixe

In der Vergangenheit wurden an Bezeichner teilweise Prä- und Suffixe mit angetragen. Begründet war dies mit den damaligen Compilern und der fehlenden Unterstützung in der Entwicklungsumgebung. Durch die Nutzung eines Präfixes konnte so z. B. der Typ einer Variable aus dem Namen ermittelt werden.

Die sicherlich bekannteste Notation ist die Ungarische Notation. Hier werden die Bezeichner aus einem Präfix für die Funktion, einem Kürzel für den Datentyp und einem Bezeichner zusammengesetzt.

Ein Beispiel für einen solchen Namen wäre die Variable idValue, welche anzeigt, dass es sich um einen Index vom Typ Double handelt, welcher den Namen Value trägt.

Mittlerweile wird diese Notation in der Praxis nur noch selten genutzt. Auch Linus Torvalds hatte sich dazu geäußert:

Encoding the type of a function into the name (so-called Hungarian notation) is brain damaged – the compiler knows the types anyway and can check those, and it only confuses the programmer.

Neben der besseren Unterstützung der IDEs gibt es andere Gründe, welche gegen eine Nutzung der ungarischen Notation sprechen. So kann z. B. bestehender Quelltext schlechter migriert werden, wenn sie die Namen nicht ändern dürfen, aber die Typen dies tun. Dies war z. B. der Fall bei der Umstellung der WinAPI auf eine 64-Bit fähige API, bei dem Namen nun nicht mehr auf den korrekten Datentyp hinweisen.

Einrückungen

Neben der Benennung von Bezeichnern ist auch die Einrückung ein unter Umständen recht emotionales Thema.

Dabei geht es hauptsächlich darum, ob Leerzeichen oder Tabulatoren für die Einrückungen genutzt werden. Aus pragmatischer Sicht sollte hier insbesondere die Mischung dieser beiden Varianten verhindert werden.

Für Leerzeichen spricht, dass die Einrückungen bei allen Nutzern identisch aussehen. Im Gegensatz zu Tabulatoren benötigen Leerzeichen, mehr Speicher. Vier Leerzeichen belegen 4 Byte, ein Tabulator nur ein Byte.

Bei Tabulatoren kann der Einzug in der Entwicklungsumgebung individuell konfiguriert werden, was aber gleichzeitig den Nachteil ergibt, dass der Quelltext bei unterschiedlichen Mitarbeitenden anders aussehen kann.

Persönlich würde der Autor an dieser Stelle immer Leerzeichen empfehlen. Damit ist ein Quelltext gewährleistet, welcher bei jedem Entwickler identisch aussieht. Der zusätzliche Speicherbedarf kann hierbei vernachlässigt werden.

Einrückungstiefen

Bei der Frage der Leerzeichen stellt sich auch die Frage, mit wie vielen Leerzeichen soll ein Block eingerückt werden. Hier ergibt sich die Möglichkeit, dies mit zwei Leerzeichen je Block zu machen:

void main() {
  doSomething();
}

Der Standard bei vielen Projekten sind hingegen vier Leerzeichen:

void main() {
    doSomething();
}

Allerdings sind auch acht Leerzeichen gebräuchlich, z. B. beim Linux-Kernel. Wirklich bemerkbar wird dies allerdings erst dann, wenn mehrere Blöcke ineinander verschachtelt werden:

void main() {

    for(int i = 0; i < 10; i++) {

        for(int j = 0; j < 10; j++) {

            doSomething();
        }
    }
}

Je nach Ausgabeformat, z. B. beim Ausdruck oder in Präsentationen ist es sinnvoll auf zwei Leerzeichen zu setzen, aber im Allgemeinen sollten vier Leerzeichen genutzt werden.

Whitespaces und Leerzeilen

Neben der Einrückung sind auch die Whitespaces im Quelltext selbst, sowie Leerzeilen ein Element zur Strukturierung des Quelltextes.

Leerzeilen stellen ein wichtiges Element zur Strukturierung dar. Natürlich kann ein Quelltext ohne Leerzeilen geschrieben werden und leider ist dies in der Praxis oft zu sehen. Sinnvoll ist es aber, den Quelltext etwas weiträumiger zu gestalten:

int getResult(int a, int b) {

    int sum = getSum();
    int ret = 0;

    for(int i = 0; i < 10; i++) {
        ret += sum;
    }

    return ret;
}

Die Trennung einzelner Bestandteile des Quelltextes durch Leerzeilen sollte anhand der funktionalen Blöcke bzw. nach der Fachlichkeit vorgenommen werden.

Neben den Leerzeilen, sind auch Whitespaces ein essenzieller Teil der Formatierung eines Quelltextes. Whitespaces definieren sich allgemein als Leerstellen in Text, Code oder Schrift, die zwischen Zeichen, Wörtern, Zeilen oder Absätzen liegen. In der Programmierung werden Whitespaces auch als Formatierung verwendet, um den Quelltext leserlicher zu machen und den Code übersichtlicher zu strukturieren.

Whitespaces verbessern die Sichtbarkeit und das Verständnis der Syntax:

int sum=a+b;

for(int i=0;i<10;i++) {
    doSomething();
}

Bei diesem Beispiel wäre es wesentlich sinnvoller, Leerzeichen zum Strukturieren zu nutzen und dem Quelltext eine gewissen Luftigkeit zu geben:

int sum = a + b;

for(int i = 0; i < 10; i++) {
    doSomething();
}

Dies erhöht die Lesbarkeit und sorgt letztlich für ein besseres Verständnis. Natürlich kann auch an dieser Stelle übertrieben werden:

for ( int i = 0; i < 10; i++ ) {
    doSomething ( ) ;
}

So werden hier auch Leerzeichen rund um die Klammern gesetzt, was im Normalfall nicht sonderlich hilfreich ist und deshalb unterlassen werden sollte.

Blockklammern

In vielen Programmiersprachen wird mit Blöcken gearbeitet. Ein Block definiert sich als eine Gruppe von Anweisungen, die als eine Einheit behandelt werden. So wird über den Block z. B. der Gültigkeitsbereich von Variablen definiert. Ein Block beginnt normalerweise mit einer öffnenden geschweiften Klammer und endet mit einer schließenden Klammer gleichen Typs.

Beispielsweise kann ein Block zu einer if-Anweisung gehören, in der eine Reihe von Anweisungen ausgeführt werden, wenn die Bedingung wahr ist. Hier kann natürlich die Frage nach der Notwendigkeit gestellt werden, wie in diesem Stück Java-Code:

if(something == true)
    doFooBar();

So würde dieses Beispiel ohne Probleme kompilieren und wenn die Bedingung zutrifft, die Methode doFooBar aufgerufen werden. Problematisch wird dieses Konstrukt allerdings dann, wenn der Quelltext an dieser Stelle erweitert wird:

if(something == true)
    doAnotherThing();
    doFooBar();

Nun würde nur noch die Methode doAnotherThing ausgeführt werden. Die andere Methode hingegen nicht mehr. Aus dem Quelltext ist dies allerdings nicht ohne Weiteres ersichtlich. Aus diesem Grund sollte immer mit Blockklammern gearbeitet werden, auch wenn nur eine einzelne Anweisung folgt:

if(something == true) {
    doFooBar();
}

Dadurch werden Fehler vermieden und die Intention des Quelltextes wird sofort ersichtlich.

Position der Klammern

Für die Positionierung der geschweiften Blockklammern gibt es in der Praxis zwei verbreitete Varianten, diese zu setzen. Bei der ersten Variante sind sie beide auf der gleichen Ebene zu finden:

boolean get()
{
    int a = 7;
    int b = 42;

    int result = doFooBar(7, 42);

    if(result == 23) 
    {
        return false;
    }

    return true;
}

Der Vorteil an dieser Variante ist, dass sofort zu sehen ist, wo ein Block beginnt und wo sich die entsprechende schließende Klammer des jeweiligen Blockes befindet. Als Nachteil wird bei dieser Variante oft aufgeführt, dass damit etwas Platz verschwendet wird.

Bei der anderen gebräuchlichen Variante wird die öffnende Klammer eines Blockes direkt hinter die Anweisung gesetzt, welche zum öffnenden Block gehört:

boolean get() {

    int a = 7;
    int b = 42;

    int result = doFooBar(7, 42);

    if(result == 23) {
        return false;
    }

    return true;
}

Dies erschwert zwar die Zuordnung zwischen dem Beginn des Blockes und dem Ende, allerdings zeigen die meisten modernen IDEs diese Zuordnung prominent an.

In der Theorie wird bei dieser Variante eine Zeile eingespart, allerdings ist es sinnvoll nach der öffnenden Blockklammer eine Leerzeile zu setzen, um die Übersichtlichkeit zu erhöhen.

Häufig wird noch ein Unterschied zwischen einzeiligen und mehrzeiligen Blöcken gemacht:

if(something == true) {
    doFooBar();
}

Dort wird die Leerzeile weggelassen, während sie bei mehrzeiligen Blöcken immer eingefügt wird:

if(something == true) {

    doFooBar();
    doSomething();

    for(int i = 0; i < 10; i++) {
        doThings();
    }    
}

Blöcke per Einrückung

Neben Sprachen mit solchen Blockklammern existieren auch Sprachen wie Python, welche andere Wege zur Strukturierung von Blöcken nutzen:

import sys
import random

running = True

while running:

    randomNumber = random.randint(0,8)

    if randomNumber == 0:
        break;
        
    else:
        print(randomNumber)

Hier wird die Zuordnung zu einem Block über die entsprechende Einrückung vorgenommen. Damit entfällt die Frage nach der Position der Blockklammern.

Reihenfolgen

In vielen Programmiersprachen gibt es Schlüsselwörter, wie Modifikatoren für die Sichtbarkeit. Für diese empfiehlt es sich auch eine entsprechende Reihenfolge zu definieren und diese einzuhalten.

Am Beispiel von Java wäre dies die Sichtbarkeit, dann eine eventuelle static-Definition gefolgt von einer final-Definition und am Ende der eigentlichen Definition:

public int a = 7;
public final int b = 24;
public static final int c = 42;

Auch bei Systemen zur statischen Codeanalyse, wie Sonarlint, sind solche Regeln hinterlegt.

Reihenfolge im Quelltext

Neben den Namen der Bezeichnern sind je nach Sprache auch bestimmte Reihenfolgen der einzelnen Elemente gewünscht. Unter Java ist dies vornehmlich folgende Reihenfolge: Konstanten, private Variablen, private Methoden, Getter und Setter und anschließend öffentliche Methoden.

Allerdings kann es valide sein, Public-Methoden und Private-Methoden zusammenzuhalten, wenn diese z. B. nach Funktionalität gruppiert sind.

Zeilenlänge und Umbrüche

Früher gab es relativ strenge Regeln, was die maximale Zeilenlänge innerhalb eines Quelltextes anging. Meist waren dies 80 Zeichen pro Zeile, bedingt durch die 80 Spalten in der Hollerith-Lochkarte von IBM. Daneben haben sich mittlerweile Zeilenlängen von 80 über 100 bis zu 120 Zeichen pro Zeile eingebürgert.

Auch in Zeiten größerer Bildschirme und höherer Auflösungen, sollten Zeilen trotzdem nicht unendlich lang gestaltet werden, sondern mit Zeilenumbrüchen gearbeitet werden. Für solche Umbrüche existieren unterschiedliche Variante, welche in gewisser Hinsicht Geschmacksache sind.

public int calculate(int valueA,
                     int valueB,
                     int valueC,
                     int valueD,
                     int valueE,
                     int valueF,
                     int valueG) {
    return 0;
}

Grundsätzlich sollten keine Umbrüche mitten in einer Parameterliste vorgenommen werden, sondern die Parameter einzeln umgebrochen werden. Auch bei Fluent Interfaces wird mit Zeilenumbrüchen gearbeitet:

CarBuilder carBuilder = new CarBuilder()
        .withWheels(4)
        .withEngine(400, Fuel.DIESEL)
        .withWindows(5)
        .build();

Die Umbrüche verbessern, richtig eingesetzt, die Lesbarkeit und Verständlichkeit des Quelltextes.

Kommentare

Für einen verständlichen Quelltext sind in vielen Fällen Kommentare in diesem nötig und wichtig.

Je nach Sprache werden unterschiedliche Möglichkeiten für Kommentare bereitgestellt. Vorwiegend sind dies Zeilenkommentare und Blockkommentare.

Blockkommentare sind eine Reihe von Kommentaren, die durch ein vorangestelltes /* und ein abschließendes */ angezeigt werden, sodass mehrere Zeilen Text zusammen kommentiert werden können. Zeilenkommentare sind Kommentare, die nur eine einzelne Zeile betreffen und mit // beginnen. Sie können am Ende einer Codezeile oder auf einer eigenen Zeile platziert werden. Beide Kommentartypen sind nützlich, um das Verständnis des Codes zu erleichtern, indem sie Erklärungen zu bestimmten Codeabschnitten bereitstellen.

In den meisten Fällen sollten innerhalb eines Quelltextes den Zeilenkommentaren der Vorrang eingeräumt werden, entweder zum Auskommentieren von Quellcode oder zum Dokumentieren innerhalb des Codes:

// Create system temporary directory
Path tmpdir = null;

// log.error(tmpdir);

Block-Kommentare werden oft für die Dokumentation von Methoden, z. B. mittels JavaDoc genutzt:

/**
 * This method returns an Optional that holds a String containing
 * the file extension of the passed filename.
 * @param filename The file name whose extension is to be determined.
 * @return Optional filled with a String with the file extension if 
 * the file has an extension, or an empty optional if it has no extension.
 */

Grundsätzlich gilt bei Kommentaren, dass sie fachlicher Natur sein sollten und dass nicht unnötig kommentiert wird. Als Sprache bietet sich hier wie bei der Benamung von Bezeichnern Englisch als kleinster gemeinsamer Nenner an. Unnötige Kommentare sollten vermieden werden:

// Calculate sum and store in sum
int sum = getSum(a, b);

Der Inhalt des Kommentars ergibt sich bereits aus der sprechenden Bezeichnung der Variablen und der dazugehörigen Methode, sodass dies nicht noch einmal mit einem Kommentar untermauert werden muss.

Interessanter wäre es hier, wenn der Kommentar noch etwas zur Fachlichkeit beiträgt:

// Calculate sums of base articles
int sum = getSum(a, b);

Auch das beliebte Auskommentieren von Code wird mittels der Sprachmittel des Kommentars ermöglicht. Im Normalfall sollte auskommentierter Quellcode am Ende immer entfernt werden und nicht im Quelltext verbleiben.

Allgemeine Regeln

Neben Regeln für spezielle Konstrukte existieren eine Reihe von allgemeinen Regeln, welche auch in Coding Conventions Einzug gefunden haben.

So gilt, dass pro Zeile genau eine Anweisung bzw. ein Statement kodiert wird, eine Funktion bzw. eine Methode genau eine Aufgabe erledigen und Klassen und Methoden eine gewisse Größe nicht überschreiten sollten.

Bei Klassen definiert sich hier meist eine maximale Größe von 2000 Zeilen, während bei Methoden gerne gesagt wird, dass eine Methode als Ganzes auf den Bildschirm passen sollte.

Aufgaben für Methoden

Auch die Beschränkung von Methoden auf eine Aufgabe ist eine sinnvolle Regel. So verheißt eine Methode mit dem Namen doItAll() schon wenig Gutes. Hingegen definiert folgende Methode:

int getSum(int a, int b)

schon anhand ihres Namens klar, welche Aufgabe sie wahrnimmt und mit welchem Ergebnis zu rechnen ist.

Dadurch, dass Funktionen bzw. Methoden sich nur auf eine Aufgabe beschränken, sind sie besser wiederverwendbar und verhindern in vielen Fällen doppelten Quelltext. Auch das Review solcher fachlich eng abgestimmten Methoden ist einfacher möglich, da die Komplexität verringert ist.

Coding Conventions

Während bis hierhin viele einzelne Elemente beschrieben wurden, sollen diese nun zu einer Coding Convention zusammengeführt werden. Solche Coding Conventions sind relativ umfangreiche Werke. In vielen Fällen ist es nicht nötig das Rad neu zu erfinden, da für viele Sprachen Standard-Konventionen existieren, welche genutzt werden können.

Alternativ sollte sich zumindest an diesen Konventionen orientiert werden. Auch die jeweiligen Entwicklungsumgebungen, setzten über die Code-Formatierung gewisse Teile von Coding Conventions direkt um.

Sprachspezifische Konventionen

Wer sich umschaut, wird feststellen, dass eine Reihe von Coding Conventions für unterschiedliche Sprachen existieren. Dies sind unter anderem die .NET: Microsoft Coding Conventions, für Java die Code Conventions for the Java Programming Language und für PHP: PSR-1 und PSR-2.

Allerdings werden manche dieser Konventionen wie für Java mittlerweile als veraltet angesehen und büßen damit auch an Verbindlichkeit ein. Bei anderen Styles wie PSR-2 werden diese direkt für die Entwicklung des Frameworks genutzt und sind somit verbindlich.

Übergreifende Konventionen

Daneben existieren noch andere Coding Conventions wie die Apple Coding Convention und der Google Style Guide.

Der Google Style Guide deckt Konventionen für unterschiedlichste Sprachen, wie C++, Objective-C, Java, Python, R, HTML, JavaScript und weitere ab und kann online eingesehen werden.

Lizenziert ist der Google Style Guide unter der Creative-Commons-Lizenz CC-BY. Neben der eigentlichen Beschreibung werden auch Dateien mit den entsprechenden Konfigurationen für die Entwicklungsumgebung mitgeliefert.

Dokumentation

Auch wenn ein Hauptaugenmerk bei Coding Conventions auf dem Quelltext liegt, sollte die Dokumentation ebenfalls beachtet werden. So sollte nur das notwendige dokumentiert und tote und inkorrekte Dokumentation gelöscht werden.

Auch hat es sich eingebürgert, eine entsprechende Datei ins Wurzelverzeichnis des Projektes zu legen. Diese trägt meist den Namen README bzw. README.md

In diesem Dokument wird erklärt, um welches Projekt es sich handelt und ein kurzer Überblick über das Projekt gegeben. Daneben werden weiterführende Links bereitgestellt und erklärt, wie das Projekt gebaut werden kann.

# WordPress2Markdown

WordPress2Markdown is a tool that convert WordPress eXtended RSS (WXR) into markdown. Export the WordPress site via
backend and use the WordPress eXtended RSS (WXR) with this tool.

## Usage

WordPress2Markdown is a command line tool.

> java -jar WordPress2Markdown.jar -i wordpress-export.xml -s DATETIME -o /home/seeseekey/MarkdownExport

### Parameter

The options available are:

    [--author -f value] : Filter export by author
    [--authors -a] : Export authors
    [--help -h] : Show help
    [--input -i value] : Input path
    [--output -o value] : Output path
    [--scheme -s /POST_ID|DATETIME/] : Scheme of filenames

## Conversion

WordPress2Markdown converted the following html and other tags:

* \<em\>
* \<b\>
* \<blockquote\>
* \<pre\>
* \<img\>
* \<a\>
* Lists
* WordPress caption blocks ()

All other tags are striped.

## Developing

Project can be compiled with:

> mvn clean compile

Package can be created with:

> mvn clean package

## Authors

* seeseekey - https://seeseekey.net

## License

WordPress2Markdown is licensed under AGPL3.

Eine weitere wichtige Datei ist das Changelog, bzw. die Datei CHANGELOG.md. Diese Datei dokumentiert Änderungen am Projekt und informiert den Leser somit über Änderungen, neue Funktionalität und Ähnliches.

# Changelog

This changelog goes through all the changes that have been made in each release.

## [1.2.0-SNAPSHOT]() - 2022-03-04

### Added

* Implement simple conversion for CSV files

### Changed

* Update project to Java 17
* Rework changelog
* Update dependencies
* Change license from GPL3 to AGPL3

### Fixed

* Fix some SonarLint code smells
* Small optimizations

## [1.1.0](https://github.com/seeseekey/Convert2Markdown/releases/tag/v1.1) - 2019-10-13

### Added

* Implement conversion of MediaWiki dump files
* Add statistical information for export
* Add support for exporting author (#1)
* Add filter to export only a specific author

### Changed

* Rename tool to Convert2Markdown
* Update documentation
* Rebuild HTML to markdown conversion with HTML parser

## [1.0.0](https://github.com/seeseekey/Convert2Markdown/releases/tag/v1.0) - 2019-03-26

* Initial release

Versionierung

Im weiteren Sinne gehört auch die Versionierung des Projektes zu den Coding Conventions. Gerne genutzt wird hierbei die semantische Versionierung. Dabei liegen den Zahlen der Versionnummer z. B. 2.4.7 eine bestimmte Bedeutung zugrunde.

So handelt es sich bei der ersten Zahl um die Major-Version, welche nur dann erhöht wird, wenn zur Vorgängerversion inkompatible Änderungen oder andere signifikanten Änderungen vorgenommen wurden.

Die zweite Zahl ist die sogenannte Minor-Version, welche meist bei neuer Funktionalität hochgezählt wird. Die letzte Zahl bezeichnet die Bugfix-Version und wird bei entsprechenden Fehlerbereinigungen hochgezählt.

Daneben existieren auch andere Versionierungen, wie das relativ beliebte Schema Jahr.Monat z. B. 2023.04 als Versionnummer, welche bei neuen Releases basierend auf der Version gerne um eine dritte Nummer erweitert werden z. B. 2023.04.1.

Umsetzung

Neben der eigentlichen Definition einer Coding Conventions ist es wichtig, dass diese im Entwicklungsalltag berücksichtigt und genutzt wird. Hier stellt sich dann die Frage nach der organisatorischen Umsetzung.

Grundlegend sind es einige Schritte auf dem Weg bis zu Nutzung und Umsetzung. So sollte sich im ersten Schritt auf eine Coding Convention geeinigt werden. Nachdem dies geschehen ist, mitsamt aller diesbezüglicher Regeln, wie zur Benennung oder der gewünschten Komplexität, müssen diese Coding Conventions entsprechend kommuniziert werden.

So ist es problemlos möglich, entsprechende Templates für die Einstellungen der jeweiligen IDEs zur Verfügung zu stellen. Auch sollte die Überprüfung der Coding Conventions beim Review kontrolliert werden.

Daneben können die entsprechenden Coding Conventions auch per Software überprüft und z. B. das Pushen in ein entferntes Git-Repository nur erlaubt werden, wenn die Coding Conventions eingehalten wurden. Allerdings sollte auch nicht versucht werden, soziale Probleme, welche sich bei der Einführung der Konventionen ergeben, durch rein technische Ansätze zu lösen.

Umstellung

Eine weitere Frage ist die Umstellung der bestehenden Projekte auf neue Coding Conventions. Bestimmte Dinge wie die Formatierung des Quelltextes können meist automatisch auch für größere Projekte bewerkstelligt werden.

Daneben sollte bestehender Code Stück für Stück auf die Konventionen angepasst werden, z. B. bezüglich der Benamung. Dies kann immer dann geschehen, wenn an einer entsprechenden Stelle im Rahmen einer Anforderung gearbeitet wird.

Probleme

Natürlich kann es bei der Nutzung und Einführung von Coding Conventions Probleme geben. Dies kann sich in Widerstand aus der Entwicklerschaft oder in Problemen mit der technischen Seite wie unterschiedlicher IDEs ausdrücken.

Vor allem bei einer Neueinführung kann es schwierig sein, sich an entsprechende Konventionen und Regeln zu gewöhnen, wenn vorher ohne gearbeitet wurde. Das Gleiche gilt, wenn eine Konvention nicht den eigenen Präferenzen entspricht.

Es kann passieren, dass einige Zeit dafür aufgebracht werden muss, die Konventionen zu verinnerlichen und umzusetzen. Deshalb ist es wichtig, die Konventionen klar zu kommunizieren, ihre Nutzung verpflichtend zu machen und dies im Entwicklungsprozess wie beim Review auch zu beachten.

Fazit

Coding Conventions sind ein wesentlicher Bestandteil der Softwareentwicklung und bieten viele Vorteile. Sie helfen dabei, Quelltexte einfacher lesbar, verständlich und wiederverwendbar zu machen. Dadurch wird die Wartbarkeit verbessert und die Qualität der Software erhöht. Dies kann zu einer höheren Produktivität und einem schnelleren Entwicklungsprozess führen.

Dieser Artikel erschien ursprünglich auf Golem.de und ist hier in einer alternativen Variante zu finden.

Selfhosting in fünfter Auflage erschienen

Vor einigen Tagen ist mein Buch Selfhosting: Server aufsetzen und betreiben in der fünften Auflage erschien. Die erste Auflage des Buches erschien im Jahr 2015. Im Gegensatz zu den Vorgängerausgaben, erscheint diese Ausgabe, neben der E-Book-Variante, auch gedruckt als Soft- und Hardcover-Ausgabe. Behandelt wird im Buch der Betrieb eines eigenen Servers mitsamt entsprechender Dienste unter der Nutzung der Linux-Distribution Ubuntu. Neben den bisherigen Inhalten wurde viele Kapitel um entsprechende Hintergründe erweitert.

Selfhosting

Nach einer kurzen Einleitung behandelt das Buch die Beschaffung eines Servers, die anschließende Installation und Grundeinrichtung. Dabei wird auch das Setup von verschlüsselten Servern behandelt. Nach der Vermittlung von Linux– und Netzwerk-Grundlagen, wird anschließend die Einrichtung unterschiedlichster Servertypen, wie Mail-, Game- oder Webserver behandelt. Neben diesen werden Dienste wie Git und XMPP besprochen. In weiteren Abschnitten des Buches wird auf Themen wie das Backup von Servern, die Sicherheit, Wartung und Verwaltung derselben eingegangen.

Die Hard- und Softcover-Ausgaben

Erhältlich ist das Buch als E-Book, sowie in einer Soft- und Hardcover-Variante. Es kann es unter anderem bei Amazon, Beam, Google Play, eBook.de und iTunes bezogen werden. Direkt bestellt werden kann das Buch über den Shop von Tredition.