Unter Windows kann man Ordner auch per Skript erstellen lassen. Das ist nützlich, wenn man standardisierte Ordnernamen möchte. Mit PowerShell lassen sich auch Formulare erzeugen, die Informationen abfragen. Hier ist ein Beispiel.
Warum standardisierte Ordnername helfen
In diesem Blog empfehlen wir, die Ablage nach Vorgängen und Prozessen zu strukturieren. So wird die Ablage zur sich selbst-pflegenden Aufgabenliste.
In manchen Prozessen setzt sich der Vorgangsname immer aus den gleichen Bausteinen zusammen, z. B. aus einem Datum, einer Abteilung, einem Stichwort und dem Bearbeiternamen. Wenn wir die Bestandteile aus fertigen Listen auswählen, gibt es weniger Fehler beim Anlegen.
Wenn das Anlegen eines Ordners einfacher ist, nutzen die Anwender:innen auch diese Möglichkeit.
PowerShell-Formulare haben zwei Vorteile:
- Man braucht keine zusätzliche Software auf dem Rechner zu installieren.
- Der Anwender kann das Formular anpassen, wenn sich eine Liste geändert hat.
Schauen wir uns an, wie diese Skript aufgebaut sind.
Aufbau von Skripten
Mit Batch-Dateien könnte man das Anlegen der Ordner ebenfalls beschleunigen. Auf der Kommandozeile werden die verschiedenen Optionen abgefragt. So richtig schön ist diese Möglichkeit aber nicht.
Unter Windows gibt es bereits die Möglichkeit, Formulare zu erzeugen und in PowerShell-Skripte zu integrieren:
- Zu Beginn des Skripts werden alle Variablen definiert.
- Dann bauen wir das Formular auf.
- Am Ende werden die Befehle auf Basis der gewählten Einstellungen ausgeführt.
Brian Posey hat bei Computerweekly einen Beitrag veröffentlicht, in dem er die Funktionsweise der Formular erläutert. /1/
Wir können in der PowerShell einfache Felder (Arrays) oder Schlüssel-Wert-Tabellen (Hashmaps) anlegen.
Beispiel für einfache Arrays:
- $StandortOptionen = @("Hamburg", "Berlin", "München")
- $AbteilungsOptionen = @("Marketing", "Vertrieb", "IT", "HR")
Die ausgewählten Werte werden dann so übernommen. Manchmal will man lieber Abkürzungen übernehmen. Dann wären Hashmaps sinnvoller:
$StandortKonfiguration = @{
"Hamburg" = "HH"
"Berlin" = "B"
"München" = "M"
}
In diesem Beispiel wählen wir Hamburg im Formular aus und schreiben HH in den Ordnernamen.
 |
| Abb. 1: Formular zum Anlegen eines neuen Vorgangs (Windows PowerShell) |
Das Aufbauen eines Formulars ist eine mühsame Arbeit. Man muss sich die Abstände und Größen der einzelnen Elemente auf dem Formular ansehen. Diese Arbeit kann man nun gut an die elektronischen Praktikanten geben.
Manchmal ist das Ausführen von PowerShell-Skripten deaktiviert. In dem Fall muss man einmalig die Ausführungsrichtlinie (Execution Policy) in der Eingabeaufforderung ändern: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Beispielformular zum Anlegen eines neuen Vorgangs
Sehen wir uns ein Beispiel für ein Formular an. Das folgende Formular fragt den Standort, die Abteilung und ein Datum ab. Daraufhin erstellt das Skript den Ordner Datum_Standortkürzel_Abteilungskürzel_Bearbeiter, z. B. "20251125_MUC_INF_janfi" oder "20251130_BER_INF_janfi". Dieses Skript habe ich mit Google Gemini erstellt.
# ==============================================================================
# 1. VORBEREITUNG & DATENHALTUNG
# ==============================================================================
# Definiert den Basisordner für die neuen Projekte.
# BITTE PRÜFEN: Dieser Pfad muss existieren!
$BasePath = "C:\Temp\Projekte"
# Definition der Dropdown-Werte über Hashtables
# Key (Schlüssel): Was der Benutzer sieht (z.B. "Hamburg (Nord)")
# Value (Wert): Was für den Ordnernamen verwendet wird (z.B. "HHN")
$StandortKonfiguration = @{
"Hamburg (Nord)" = "HHN"
"Berlin (Ost)" = "BER"
"München (Süd)" = "MUC"
"Zürich (CH)" = "ZRH"
}
$AbteilungsKonfiguration = @{
"Marketing & PR" = "MKT"
"Vertrieb (Sales)" = "VTB"
"IT-Infrastruktur" = "INF"
"Personalabteilung" = "HR"
}
# Auslesen des aktuellen Benutzernamens
$UserName = $env:USERNAME
# Assemblies für WinForms laden
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
# ==============================================================================
# 2. GUI DESIGN UND STEUERELEMENTE (MIT OPTIMIERTEN ABSTÄNDEN)
# ==============================================================================
# --- Hauptformular erstellen --------------------------------------------------
$form = New-Object System.Windows.Forms.Form
$form.Text = "Neuen Ordner anlegen"
$form.Size = New-Object System.Drawing.Size(400, 350)
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedSingle
$form.MaximizeBox = $false
$form.MinimizeBox = $false
# Startkoordinaten und Abstandskontrolle
$xOffset = 20
$yCurrent = 20 # Startposition des ersten Labels
# --- 1. Standort (ComboBox) ---------------------------------------------------
$lblStandort = New-Object System.Windows.Forms.Label
$lblStandort.Location = New-Object System.Drawing.Point($xOffset, $yCurrent)
$lblStandort.Size = New-Object System.Drawing.Size(350, 20)
$lblStandort.Text = "1. Standort auswählen (z.B. Hamburg):"
$form.Controls.Add($lblStandort)
# Position ComboBox: Labelhöhe (20) + 5px Abstand
$yCurrent += 25
$cbStandort = New-Object System.Windows.Forms.ComboBox
$cbStandort.Location = New-Object System.Drawing.Point($xOffset, $yCurrent)
$cbStandort.Size = New-Object System.Drawing.Size(340, 30) # Breite leicht reduziert (340)
$cbStandort.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList
# KORREKTUR: Klammerung für AddRange, um Fehler zu vermeiden.
$cbStandort.Items.AddRange(($StandortKonfiguration.Keys | Sort-Object))
$cbStandort.SelectedIndex = 0
$form.Controls.Add($cbStandort)
# Nächster Startpunkt: ComboBox-Höhe (30) + 20px Abstand
$yCurrent += 50
# --- 2. Abteilung (ComboBox) --------------------------------------------------
$lblAbteilung = New-Object System.Windows.Forms.Label
$lblAbteilung.Location = New-Object System.Drawing.Point($xOffset, $yCurrent)
$lblAbteilung.Size = New-Object System.Drawing.Size(350, 20)
$lblAbteilung.Text = "2. Abteilung auswählen (z.B. Marketing):"
$form.Controls.Add($lblAbteilung)
# Position ComboBox: Labelhöhe (20) + 5px Abstand
$yCurrent += 25
$cbAbteilung = New-Object System.Windows.Forms.ComboBox
$cbAbteilung.Location = New-Object System.Drawing.Point($xOffset, $yCurrent)
$cbAbteilung.Size = New-Object System.Drawing.Size(340, 30) # Breite leicht reduziert (340)
$cbAbteilung.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList
# KORREKTUR: Klammerung für AddRange, um Fehler zu vermeiden.
$cbAbteilung.Items.AddRange(($AbteilungsKonfiguration.Keys | Sort-Object))
$cbAbteilung.SelectedIndex = 0
$form.Controls.Add($cbAbteilung)
# Nächster Startpunkt: ComboBox-Höhe (30) + 20px Abstand
$yCurrent += 50
# --- 3. Datum (DateTimePicker) ------------------------------------------------
$lblDatum = New-Object System.Windows.Forms.Label
$lblDatum.Location = New-Object System.Drawing.Point($xOffset, $yCurrent)
$lblDatum.Size = New-Object System.Drawing.Size(350, 20)
$lblDatum.Text = "3. Startdatum festlegen:"
$form.Controls.Add($lblDatum)
# Position DateTimePicker: Labelhöhe (20) + 5px Abstand
$yCurrent += 25
$dtpDatum = New-Object System.Windows.Forms.DateTimePicker
$dtpDatum.Location = New-Object System.Drawing.Point($xOffset, $yCurrent)
$dtpDatum.Size = New-Object System.Drawing.Size(150, 25)
$dtpDatum.Format = [System.Windows.Forms.DateTimePickerFormat]::Short
$form.Controls.Add($dtpDatum)
# Nächster Startpunkt: DateTimePicker-Höhe (25) + 15px Abstand (löst "nfi"-Problem)
$yCurrent += 40
# --- 4. Bearbeiter-Information ------------------------------------------------
$lblBearbeiter = New-Object System.Windows.Forms.Label
$lblBearbeiter.Location = New-Object System.Drawing.Point($xOffset, $yCurrent)
$lblBearbeiter.Size = New-Object System.Drawing.Size(350, 20)
$lblBearbeiter.Text = "4. Bearbeiter (automatisch): $($UserName)"
$form.Controls.Add($lblBearbeiter)
# Nächster Startpunkt für den Button
$yCurrent += 30
# --- 5. Erstellen Button ------------------------------------------------------
$btnErstellen = New-Object System.Windows.Forms.Button
$btnErstellen.Location = New-Object System.Drawing.Point($xOffset, $yCurrent)
$btnErstellen.Size = New-Object System.Drawing.Size(350, 40)
$btnErstellen.Text = "Ordner erstellen und Formular schließen"
$form.Controls.Add($btnErstellen)
# ==============================================================================
# 3. LOGIK UND EREIGNISHANDLER
# ==============================================================================
$btnErstellen.Add_Click({
# 1. Werte aus den Steuerelementen auslesen (Anzeigename)
$SelectedStandortName = $cbStandort.Text
$SelectedAbteilungName = $cbAbteilung.Text
$SelectedDate = $dtpDatum.Value
# 2. Die Kürzel (Values) aus der Hashtable abrufen (Lookup)
$StandortKuerzel = $StandortKonfiguration[$SelectedStandortName]
$AbteilungsKuerzel = $AbteilungsKonfiguration[$SelectedAbteilungName]
# Datum für den Ordnernamen formatieren (z.B. 20251125)
$DatumFormatiert = $SelectedDate.ToString("yyyyMMdd")
# 3. Den vollständigen Ordnernamen konstruieren
# Ergebnis: C:\Temp\Projekte\20251125_HHN_MKT_mustermann
$NewFolderName = "${DatumFormatiert}_${StandortKuerzel}_${AbteilungsKuerzel}_${UserName}"
$FullFolderPath = Join-Path -Path $BasePath -ChildPath $NewFolderName
# 4. Prüfen und Ordner erstellen
if (Test-Path -Path $FullFolderPath) {
[System.Windows.Forms.MessageBox]::Show("Der Ordner existiert bereits:`n`n$FullFolderPath", "Fehler: Ordner vorhanden", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Warning)
} else {
try {
$NewFolder = New-Item -Path $FullFolderPath -ItemType Directory -Force -ErrorAction Stop
[System.Windows.Forms.MessageBox]::Show("Der Ordner wurde erfolgreich erstellt:`n`n$FullFolderPath", "Erfolg", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Information)
# NEU: Windows Explorer mit dem erstellten Ordner öffnen
Start-Process -FilePath $FullFolderPath
$form.Close()
} catch {
[System.Windows.Forms.MessageBox]::Show("Fehler beim Erstellen des Ordners:`n`n$($_.Exception.Message)", "Fehler", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error)
}
}
})
# --- Formular anzeigen --------------------------------------------------------
$form.ShowDialog() | Out-Null
Dieses Skript kann man nun lokal als neuer-vorgang.ps1 abspeichern und bei Bedarf aufrufen.
Beispielformular zum Anlegen eines offenen Punktes in Projekten
Offene Punkte sind Mini-Vorgänge in Projekten. Bei einfachen Projekten gibt es jeweils einen Ordner für offene und für geschlossene Punkte. Immer, wenn etwas getan werden muss, wird dafür ein neuer, offener Punkt angelegt. Auch dies kann man mit einem Formular automatisieren.
 |
| Abb. 2: Formular zum Anlegen eines neuen offenen Punktes (Windows PowerShell) |
Das Skript vergibt eine laufende Nummer und fragt nach Anlass und Quelle für den offenen Punkt. Zusätzlich kann man ein Stichwort vergeben.
Mit dem folgenden Prompt habe ich das Skript bei Claude.ai erzeugt:
Bitte erstelle mir ein PowerShell-Skript mit einem WPF-Formular, das ein paar Variablen abfragt und danach einen Ordner im gleichen Verzeichnis anlegt. Die Variablen sollen in einem HashMap gespeichert werden. Ich möchte mit dem Skript offene Punkte in meinem Projekt schnell erfassen. Der Ordnername für offene Punkte setzt sich aus folgenden Bestandteilen zusammen: P + laufende Nummer (vierstellig mit führenden Nullen) + Anlass + Quelle + Stichwort. Die Bestandteile sind durch Unterstrich "_" getrennt. Die laufende Nummer kann im gleichen Verzeichnis als Textdatei gespeichert werden. Hier sind die Anlässe: Anfrage (ANF), Plan ändern (PLN), Sitzung vorbereiten (BES), Dokument ändern oder erstellen (DOK). Hier sind die Quellen: Kunde (KD), Behörde (BH), Feuerwehr (FW), Arbeitssicherheit (AS), Intern (IN). Das Stichwort kann frei vergeben werden, darf aber nicht länger als 30 Zeichen sein.
Hier ist der Quelltext zum Skript. In diesem Beispiel definieren wir das Formular über eine XAML-Datei.
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName WindowsBase
# XAML für das WPF-Formular
[xml]$xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Offene Punkte erfassen"
Height="400"
Width="500"
ResizeMode="NoResize"
WindowStartupLocation="CenterScreen">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Überschrift -->
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Text="Neuen offenen Punkt erfassen"
FontSize="16"
FontWeight="Bold"
Margin="0,0,0,15"
HorizontalAlignment="Center"/>
<!-- Laufende Nummer (Anzeige) -->
<Label Grid.Row="1" Grid.Column="0" Content="Laufende Nr.:"/>
<TextBox Grid.Row="1" Grid.Column="1"
Name="txtLaufendeNummer"
IsReadOnly="True"
Background="LightGray"
Margin="0,2"/>
<!-- Anlass -->
<Label Grid.Row="2" Grid.Column="0" Content="Anlass:"/>
<ComboBox Grid.Row="2" Grid.Column="1"
Name="cboAnlass"
Margin="0,2">
<ComboBoxItem Content="ANF - Anfrage" Tag="ANF"/>
<ComboBoxItem Content="PLN - Plan ändern" Tag="PLN"/>
<ComboBoxItem Content="BES - Sitzung vorbereiten" Tag="BES"/>
<ComboBoxItem Content="DOK - Dokument ändern oder erstellen" Tag="DOK"/>
</ComboBox>
<!-- Quelle -->
<Label Grid.Row="3" Grid.Column="0" Content="Quelle:"/>
<ComboBox Grid.Row="3" Grid.Column="1"
Name="cboQuelle"
Margin="0,2">
<ComboBoxItem Content="KD - Kunde" Tag="KD"/>
<ComboBoxItem Content="BH - Behörde" Tag="BH"/>
<ComboBoxItem Content="FW - Feuerwehr" Tag="FW"/>
<ComboBoxItem Content="AS - Arbeitssicherheit" Tag="AS"/>
<ComboBoxItem Content="IN - Intern" Tag="IN"/>
</ComboBox>
<!-- Stichwort -->
<Label Grid.Row="4" Grid.Column="0" Content="Stichwort:"/>
<TextBox Grid.Row="4" Grid.Column="1"
Name="txtStichwort"
MaxLength="30"
Margin="0,2"/>
<!-- Zeichenzähler -->
<TextBlock Grid.Row="5" Grid.Column="1"
Name="txtZeichenzaehler"
Text="0 / 30 Zeichen"
FontSize="10"
Foreground="Gray"
Margin="2,0,0,5"/>
<!-- Vorschau -->
<Label Grid.Row="6" Grid.Column="0" Content="Ordnername:"/>
<TextBox Grid.Row="6" Grid.Column="1"
Name="txtVorschau"
IsReadOnly="True"
Background="LightGray"
Margin="0,2"/>
<!-- Status/Fehler -->
<TextBlock Grid.Row="7" Grid.Column="0" Grid.ColumnSpan="2"
Name="txtStatus"
Foreground="Red"
TextWrapping="Wrap"
Margin="0,10"/>
<!-- Buttons -->
<StackPanel Grid.Row="9" Grid.Column="0" Grid.ColumnSpan="2"
Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0,10,0,0">
<Button Name="btnErstellen" Content="Ordner erstellen"
Width="120" Height="30"
Margin="0,0,10,0"
IsEnabled="False"/>
<Button Name="btnAbbrechen" Content="Abbrechen"
Width="100" Height="30"/>
</StackPanel>
</Grid>
</Window>
"@
# XAML laden
$reader = (New-Object System.Xml.XmlNodeReader $xaml)
$window = [Windows.Markup.XamlReader]::Load($reader)
# Kontrollen abrufen
$txtLaufendeNummer = $window.FindName("txtLaufendeNummer")
$cboAnlass = $window.FindName("cboAnlass")
$cboQuelle = $window.FindName("cboQuelle")
$txtStichwort = $window.FindName("txtStichwort")
$txtZeichenzaehler = $window.FindName("txtZeichenzaehler")
$txtVorschau = $window.FindName("txtVorschau")
$txtStatus = $window.FindName("txtStatus")
$btnErstellen = $window.FindName("btnErstellen")
$btnAbbrechen = $window.FindName("btnAbbrechen")
# Globale Variablen
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
$counterFile = Join-Path $scriptPath "laufende_nummer.txt"
$currentNumber = 0
# Funktion: Laufende Nummer laden oder initialisieren
function Get-LaufendeNummer {
if (Test-Path $counterFile) {
$nummer = Get-Content $counterFile
if ($nummer -match '^\d+$') {
return [int]$nummer
}
}
return 0
}
# Funktion: Laufende Nummer speichern
function Set-LaufendeNummer($nummer) {
$nummer | Out-File -FilePath $counterFile -NoNewline
}
# Funktion: Sonderzeichen für Dateinamen bereinigen
function Clean-Filename($text) {
$invalidChars = [IO.Path]::GetInvalidFileNameChars() -join ''
$pattern = "[{0}]" -f [RegEx]::Escape($invalidChars)
return $text -replace $pattern, ''
}
# Funktion: Vorschau aktualisieren
function Update-Vorschau {
$anlass = if ($cboAnlass.SelectedItem) { $cboAnlass.SelectedItem.Tag } else { "" }
$quelle = if ($cboQuelle.SelectedItem) { $cboQuelle.SelectedItem.Tag } else { "" }
$stichwort = Clean-Filename $txtStichwort.Text.Trim()
if ($anlass -and $quelle -and $stichwort) {
$nummer = "{0:D4}" -f ($currentNumber + 1)
$txtVorschau.Text = "P{0}_{1}_{2}_{3}" -f $nummer, $anlass, $quelle, $stichwort
$btnErstellen.IsEnabled = $true
$txtStatus.Text = ""
} else {
$txtVorschau.Text = ""
$btnErstellen.IsEnabled = $false
if (-not $stichwort -and $txtStichwort.Text.Length -gt 0) {
$txtStatus.Text = "Stichwort enthält ungültige Zeichen"
} else {
$txtStatus.Text = ""
}
}
}
# Initialisierung
$currentNumber = Get-LaufendeNummer
$txtLaufendeNummer.Text = "P{0:D4}" -f ($currentNumber + 1)
# Event-Handler
$txtStichwort.Add_TextChanged({
$txtZeichenzaehler.Text = "$($txtStichwort.Text.Length) / 30 Zeichen"
Update-Vorschau
})
$cboAnlass.Add_SelectionChanged({ Update-Vorschau })
$cboQuelle.Add_SelectionChanged({ Update-Vorschau })
$btnErstellen.Add_Click({
try {
# HashMap mit allen Daten erstellen
$daten = @{
LaufendeNummer = $currentNumber + 1
Anlass = $cboAnlass.SelectedItem.Tag
AnlassText = $cboAnlass.SelectedItem.Content
Quelle = $cboQuelle.SelectedItem.Tag
QuelleText = $cboQuelle.SelectedItem.Content
Stichwort = $txtStichwort.Text.Trim()
Ordnername = $txtVorschau.Text
ErstelltAm = Get-Date
ErstelltVon = $env:USERNAME
}
# Ordner erstellen
$folderPath = Join-Path $scriptPath $daten.Ordnername
if (Test-Path $folderPath) {
$txtStatus.Text = "Ordner existiert bereits!"
return
}
New-Item -Path $folderPath -ItemType Directory -Force | Out-Null
# Info-Datei im Ordner erstellen
$infoFile = Join-Path $folderPath "_info.txt"
$infoContent = @"
Offener Punkt: $($daten.Ordnername)
========================
Erstellt am: $($daten.ErstelltAm.ToString("dd.MM.yyyy HH:mm:ss"))
Erstellt von: $($daten.ErstelltVon)
Laufende Nummer: P$('{0:D4}' -f $daten.LaufendeNummer)
Anlass: $($daten.AnlassText)
Quelle: $($daten.QuelleText)
Stichwort: $($daten.Stichwort)
"@
$infoContent | Out-File -FilePath $infoFile -Encoding UTF8
# Laufende Nummer erhöhen und speichern
$currentNumber++
Set-LaufendeNummer $currentNumber
# Erfolgsmeldung
[System.Windows.MessageBox]::Show(
"Ordner wurde erfolgreich erstellt:`n$folderPath",
"Erfolg",
[System.Windows.MessageBoxButton]::OK,
[System.Windows.MessageBoxImage]::Information
)
# Formular zurücksetzen
$txtLaufendeNummer.Text = "P{0:D4}" -f ($currentNumber + 1)
$cboAnlass.SelectedIndex = -1
$cboQuelle.SelectedIndex = -1
$txtStichwort.Text = ""
$txtVorschau.Text = ""
$btnErstellen.IsEnabled = $false
# Optional: Explorer öffnen
Start-Process explorer.exe -ArgumentList $folderPath
} catch {
[System.Windows.MessageBox]::Show(
"Fehler beim Erstellen des Ordners:`n$($_.Exception.Message)",
"Fehler",
[System.Windows.MessageBoxButton]::OK,
[System.Windows.MessageBoxImage]::Error
)
}
})
$btnAbbrechen.Add_Click({
$window.Close()
})
# Fenster anzeigen
$window.ShowDialog() | Out-Null
Dieses Formular wird als Skript mit dem Namen neuer-punkt.ps1 abgespeichert.
Verweise
Kommentare
Kommentar veröffentlichen