Bash echo >> ... nur, wenn noch nicht vorhanden?

BeBur schrieb:
Mir ist generell aber auch ein Rätsel, wieso man überhaupt freiwillig Bash verwendet anstelle einer modernen, weniger qualvollen Sprache.
Vielleicht, weil ich nicht extra eine Anwendung schreiben will, die eine andere Anwendung installiert und konfiguriert?
Ergänzung ()

Folgende zwei Zeilen sind übrigens semantisch identisch, aber ich bin mir nicht sicher, ob erstere wirklich lesbarer ist:

Bash:
#!/bin/bash
set -e

if ( grep 'a' b.txt ); then echo 'a' >> b.txt ; echo >> b.txt; fi

grep 'a' b.txt || (echo 'a' >> b.txt && echo >> b.txt)

Vermutlich wird auch niemand eine kanonische Antwort darauf geben können ...
 
Zuletzt bearbeitet:
BeBur schrieb:
Mir ist generell aber auch ein Rätsel, wieso man überhaupt freiwillig Bash verwendet anstelle einer modernen, weniger qualvollen Sprache.
Ja. :-)
"Bash" hat viele Nachteile. das fängt schon allein damit an, das man keine vernünftigen Datentypen und Datenstrukturen hat und alles immer aufwendig parsen muss, wenn man von einem Programm etwas zurück bekommt (und dann noch hoffen muss, das die Ausgabe sich nicht in der nächsten Version ändert oder das man auch im Blick hat, wie LANG gesetzt ist usw.)
Mit Skriptsprachen a-la Python oder auch Ruby und wie sie alle heißen ist man i.d.R. besser dran.

Auf der Haben-Seite hat man allerdings, das man die Tools und auch auf die Weise benutzen kann, die man vom interaktiven Gebrauch der Shell gewohnt ist. Außerdem ist die Shell immer da und auf dem System muss nicht unbedingt ein Python sein und in manchen Fällen ist es doof, wenn man extra deswegen was installieren müsste.
Und insbesondere für kleine Sachen tut es ja oftmals auch ein Shell-Skript. Insbesondere bei diesen typischen, kleinen Wergwerf-Skripten, wo man nur mal schnell eben was machen will.

Insofern hängt es auch immer ein bisschen von der Situation und dem was man erreichen will ab.
btw.: Wenn ich Shellskripte schreibe, dann setze ich nicht mal die bash voraus, sondern nur eine POSIX-konforme Shell. Das ist dann noch mal eine Ecke "schärfer". :-)
 
  • Gefällt mir
Reaktionen: BeBur und CyborgBeta
Für größere Sachen stimme ich ja uneingeschränkt zu, zum Beispiel Python zu verwenden, aber nicht bei einem Dreizeiler (auch ... wenn man mit drei Zeilen natürlich auch schon viel bewerkstelligen kann). ;)
 
Ich würde die Inhalte in Variablen schreiben, dann spart man sich die Wiederholungen, wenn man was verändern will und das ganze wird deutlich kürzer:

Code:
LINE='eine zeile'
FILE='eine-datei.txt'
grep -qF -- "$LINE" "$FILE" || echo "$LINE" >> "$FILE"

Für exakte Treffer noch das x einfügen:

Code:
grep -qxF -- "$LINE" "$FILE" || echo "$LINE" >> "$FILE"

Und wenn man Fehler komplett ignorieren will, noch das s:

Code:
grep -qsxF -- "$LINE" "$FILE" || echo "$LINE" >> "$FILE"


Falls man sudo braucht, geht das mit dem echo nicht so einfach, da muss man tee nehmen:

Code:
sudo grep -qsxF "$LINE" "$FILE" ||  echo "$LINE" | sudo tee -a "$FILE"
 
  • Gefällt mir
Reaktionen: CyborgBeta
CyborgBeta schrieb:
Vermutlich wird auch niemand eine kanonische Antwort darauf geben können ...
Ist das überhaupt semantisch identisch? Bash ist schon eine Weile her, aber bei der zweiten Version wird doch der Rückgabewert von echo 'a' >> b.txt verwendet, bei der ersten nicht.

Vielleicht, weil ich nicht extra eine Anwendung schreiben will, die eine andere Anwendung installiert und konfiguriert?
Ein Interpreter wird in jedem Fall gestartet, entweder ein Bash Interpreter oder ein Python Interpreter. Ich finde, der Thread hier ist ein Indiz dafür, dass sich Ruby oder Python auch für kleinere Skripte lohnen.

"Easy to write, hard to read" ist gut für Dinge die nicht persistent sind, z.B. vim Befehle oder spontanes suchen und ersetzen mit regex oder einen Befehl über das cli ausführen. Code der abgespeichert ist sollte aber grundsätzlich "easy to read" sein.
 
BeBur schrieb:
Ich denke, du verwechselst grad interpretieren und kompilieren von Code. Ich habe jedenfalls noch nie gehört, dass sich jemand fragt, wie sich interpretierter Code auf CPU Instruktionen abbildet.
Nein, ich verwechsel gar nichts, und auch die bash wurde irgendwann mal als Binary übersetzt, oder? 😉
 
BeBur schrieb:
Ist das überhaupt semantisch identisch?
Ja, ich denke schon, auch wenn im ersteren Fall ; statt && verwendet wird... weil ja das set -e verwendet wird, und bei einem Fehler beides abbrechen würde (durch das set -e und das kurzschließende || und &&).

Man muss nur bei der Präzedenz aufpassen, weil sich diese von C unterscheidet... (alle Operatoren haben die gleiche Präzedenz) vermutlich, weil das ganze Teil älter als C ist. 😅
 
  • Gefällt mir
Reaktionen: nutrix und BeBur
nutrix schrieb:
Nein, ich verwechsel gar nichts, und auch die bash wurde irgendwann mal als Binary übersetzt, oder? 😉
Es hat halt genau Null Relevanz, welche CPU Instruktionen interpretierter Code generiert.
 
  • Gefällt mir
Reaktionen: CyborgBeta
Na ja, wenn man darin ein Optimierungsproblem sieht ... dann vielleicht schon. Sonst aber nicht, so etwas ist selten der Bottleneck.
 
  • Gefällt mir
Reaktionen: nutrix
BeBur schrieb:
Es hat halt genau Null Relevanz, welche CPU Instruktionen interpretierter Code generiert.
Wie kommst Du zu dieser Fehlannahme? Du kannst beispielsweise in C logisch die Reihenfolgen von & bzw. | vertauschen, was für Dich keinen Unterschied macht, aber es kann intern sehr wohl was ganz anderes und sogar nachteiligeres übersetzt werden.

Natürlich hat das Relevanz, wenn ein || wie if else im Endeffekt dann durch den Compiler intern gleich behandelt wird. Anders siehts es aus, wenn ein || logisch und in der bash interpretiert zwar kürzer erscheint, aber dann durch eine aufwendigere Konstruktion ersetzt wird als eine if else Anweisung. Dazu müßte man aber im Maschinencode debuggen, oder vermutlich einfacher, sich mal den Quellcode der Bash in Linux mal genauer anschauen und debuggen, wie das jeweils übersetzt und dann interpretiert wird. Oder bash selbst macht aus || auch intern nur ein schönes if else beim interpretieren.
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: CyborgBeta
nutrix schrieb:
Wie kommst Du zu dieser Fehlannahme? Du kannst beispielsweise in C logisch die Reihenfolgen von & bzw. | vertauschen, was für Dich keinen Unterschied macht, aber es kann intern sehr wohl was ganz anderes und sogar nachteiligeres übersetzt werden.
Ich spreche von interpretierten Code. C wird in aller Regel kompiliert, nicht interpretiert. Davon abgesehen macht man so gut wie gar nicht mehr solche Mikro-Optimierungen. Einmal, weil Compiler mitlerweile deutlich schlauer sind als noch vor 30 Jahren und zweitens weil der potentielle Performancegewinn in aller Regel keine Rolle spielt.

nutrix schrieb:
Anders siehts es aus, wenn ein || logisch und in der bash interpretiert zwar kürzer erscheint, aber dann durch eine aufwendigere Konstruktion ersetzt wird als eine if else Anweisung.
Aha und welche Relevanz könnte das rein theoretisch haben? Niemand lässt einen Interpreter über Code laufen, wenn es relevant ist, welche konkreten CPU Instruktionen letztendlich verwendet werden.
 
Zuletzt bearbeitet:
Auch interpretierter Code landet über den Interpreter wieder letztlich Maschinencode auf der CPU.
 
  • Gefällt mir
Reaktionen: CyborgBeta
@nutrix Ja und wie schon gesagt ist es komplett irrelevant, welche konkreten CPU Instruktionen dann ausgeführt werden und ob Ausdruck X einer interpretierten Sprache (z.B. Bash) eine leicht andere Sequenz erzeugt als Ausdruck Y.
 
Ist es nicht, und wenn Du es mir nicht glauben magst, bau einfach mal selbst einen solchen Interpreter, und dann wirst Du es hoffentlich beim Debuggen selbst sehen und verstehen. Wenn Du in der Interpretersprache ein IF verwendest, was sich dann entsprechend 1:1 in einen IF in der Sprache des geschriebenen Interpreters wie C oder anderes umsetzten läßt, was glaubst Du wohl, was ein Entwickler machen wird? Keiner erfindet das Rad neu, wenn es nicht zwingend ist. Programmierer sind doch grundsätzlich mal faul, oder nicht? 😉 Ein Interpreter ist, wenns entsprechend nahe programmiert wurde, auch "nur" ein Wrapper, der die Funktionen abbildet bzw. durchreicht oder (leicht) erweitert. Aber ja, wenn natürlich ein Entwickler hier ein eigenes IF in beispielsweise C bauen würde, dann würde es natürlich anders umgesetzt werden und wäre dann tatsächlich zur Laufzeit in der CPU als Vergleich irrelevant!

Egal wie oft Du es noch behauptest, Deine Annahme ist so nicht richtig. Am Ende führt eine CPU das alles aus, in Maschinencode, und nicht in Interpretersprache. Und das kann sehr wohl auch im Interpreter selbst (je nach Anspruch, Aufwand und Komplexität) machinenoptimiert vorbereitet bzw. abgebildet werden.

Aber wir sind viel zu OT und nützt für dieses Problem nichts weiter.
Ergänzung ()

BeBur schrieb:
Mir ist generell aber auch ein Rätsel, wieso man überhaupt freiwillig Bash verwendet anstelle einer modernen, weniger qualvollen Sprache.
Weil Bash an sich keine Sprache ist, sondern ein Batchprogramm. 😉 Damit löst man eben andere Probleme als mit einer Programmiersprache. Wer Server und Workstations im Unix/Linux Umfeld betreiben will, will sie effektiv verwalten können, und dafür sind Batchsprachen optimiert und gut dafür.
Und @CybogBeta möchte ja genau das, wenn ich es richtig verstehe.

Natürlich sind Batchsprachen (auch wenn sie es teilweise theoretisch könnten) nicht dafür geeignet, Anwendungen mit komplexen Problemlösungen zu ersetzen. Aber umgekehrt willst Du doch so schöne Sachen wie ein Cronjob mit Backup und Syncs über Nacht und anderes nicht aufwendig selbst "programmieren" wollen.
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: CyborgBeta
nutrix schrieb:
. Wenn Du in der Interpretersprache ein IF verwendest, was sich dann entsprechend 1:1 in einen IF in der Sprache des geschriebenen Interpreters wie C oder anderes umsetzten läßt, was glaubst Du wohl, was ein Entwickler machen wird?
Das kann man dennoch nicht vergleichen. In C kann der Compiler ggf. bestimmte Annahmen treffen. Zum Beispiel wenn Du da ein Zahl verarbeitest, dann weiß der Compiler das ist eine Zahl und braucht keine Typenwandlung zu machen. Wenns ein Integer ist kann er sogar spezialisierte (und damit schnellere) Instruktionen verwenden.
usw. usw.
Das konntest Du theoretisch in der Bash auch machen, aber deine Optimierungsversuche würden mehr Zeit kosten, als Du in den meisten Fällen gewinnen kannst (das kann man auch schön bei diesen ganzen Just-in-Time-Compilern bestaunen, die erst mal "warmlaufen" müssen um wirklich effektiv zu sein).

Das kommt schon deshalb nicht hin, weil die Bash das Skript auch parsen muss usw. (all das, was bei C der Compiler mit macht und deshalb gar nicht im Ergebniscode drin ist). Schon allein das kostet soviel Code (und damit Zeit), das die eigentliche Nutz-Operation gar nicht mehr ins Gewicht fällt.

Die Behauptung (so sie denn so gemeint war), das bei C und bei der Bash hinten letztlich ungefähr selbe Maschinencode rauskommt ist nicht haltbar.
 
  • Gefällt mir
Reaktionen: CyborgBeta und kieleich
Es geht direkt um die Funktion, die man direkt durchreichen kann, nicht um alles andere darum herum. Das der Code interpretiert dann durch die Parsierung langsamer und anders läuft ist klar.

Ich formuliere es anders: Es kann durchaus sein, daß ein || umständlicher und langsamer funktioniert als ein IF ELSE an der Stelle, oder auch schneller.
 
  • Gefällt mir
Reaktionen: CyborgBeta
andy_m4 schrieb:
Das kommt schon deshalb nicht hin, weil die Bash das Skript auch parsen muss usw. (all das, was bei C der Compiler mit macht und deshalb gar nicht im Ergebniscode drin ist). Schon allein das kostet soviel Code (und damit Zeit), das die eigentliche Nutz-Operation gar nicht mehr ins Gewicht fällt.

Die Behauptung (so sie denn so gemeint war), das bei C und bei der Bash hinten letztlich ungefähr selbe Maschinencode rauskommt ist nicht haltbar.
Ich glaube, das stimmt nicht ganz, da auch ein Bash- (oder Sh-) Script einmal ganz gelesen wird, bevor es interpretiert und ausgeführt wird. Also erfolgt die Ausführung nicht Zeile für Zeile (wie sollte dies bei if else Blöcken auch anders möglich sein?).

Insofern wäre es schon interessant, welcher Bytecode, also welche CPU-Instruktionen da letztlich herauspurzeln und ausgeführt werden - bzw., welche Optimierungen der Interpreter vornimmt.

---

Ich habe mal ein etwas komplexeres Beispiel für euch:

Java:
  private static final Predicate<Path> notSample =
      p -> !p.toFile().getPath().toLowerCase(Locale.ROOT).contains("sample");
  private static final Predicate<Path> isDirectory = p -> p.toFile().isDirectory();
  private static final Predicate<Path> isFile = p -> p.toFile().isFile();
  private static final Predicate<Path> isRar = p -> p.toFile().getName().endsWith(".rar");
  private static final Predicate<Path> isImg = p -> p.toFile().getName().endsWith(".jpg");

  private static void unrarAndMove(String fullSource, String fullDestination) throws IOException {
    File temp_sf = new File(fullSource);
    File temp_df = new File(fullDestination);
    if (!temp_sf.isDirectory() || !temp_df.isDirectory()) {
      System.out.println("Invalid source or destination");
      System.exit(0);
    }
    if (!temp_sf.isAbsolute()) {
      temp_sf = temp_sf.getAbsoluteFile();
    }
    if (!temp_df.isAbsolute()) {
      temp_df = temp_df.getAbsoluteFile();
    }
    File sf = temp_sf;
    File df = temp_df;
    System.out.println("Source: " + sf);
    System.out.println("Destination: " + df);
    try (Stream<Path> walk = Files.walk(sf.toPath())) {
      walk.sorted()
          .filter(notSample)
          .filter(isDirectory)
          .forEach(
              path -> {
                String middle = path.toString().replace(sf.getParent(), "");
                File newFolder = new File(df + middle);
                if (!newFolder.exists()) {
                  System.out.println("Creating folder: " + newFolder);
                  System.out.println(newFolder.mkdirs());
                }
              });
    }
    try (Stream<Path> walk = Files.walk(sf.toPath())) {
      walk.sorted()
          .filter(notSample)
          .filter(isFile)
          .filter(isRar)
          .forEach(path -> call(path.toFile().getParentFile(), "unar " + path.toFile().getName()));
    }
    try (Stream<Path> walk = Files.walk(sf.toPath())) {
      walk.sorted()
          .filter(notSample)
          .filter(isFile)
          .filter(isImg)
          .forEach(
              path -> {
                String middle = path.toString().replace(sf.getParent(), "");
                File newFile = new File(df + middle);
                call(
                    path.toFile().getParentFile(),
                    "mv -v " + path.toFile().getName() + " " + newFile);
              });
    }
  }

  private static void call(File dir, String cmd) {
    try {
      System.out.println("Executing: " + cmd + " in " + dir);
      ProcessBuilder pb = new ProcessBuilder("bash", "-c", cmd);
      pb.directory(dir);
      Process pro = pb.start();
      try (BufferedReader reader =
          new BufferedReader(
              new InputStreamReader(pro.getInputStream(), Charset.defaultCharset()))) {
        String line;
        while ((line = reader.readLine()) != null) {
          System.out.println(line);
        }
      }
      System.out.println("Exit code: " + pro.waitFor());
    } catch (Exception e) {
      e.printStackTrace();
      System.exit(0);
    }
  }

Vereinfacht gesagt, sollen Archive entpackt und verschoben werden. Dabei soll die Quellverzeichnisstruktur auf die Zielverzeichnisstruktur abgebildet werden, und "Sample"-Verzeichnisse sollen ignoriert werden.

Vermutlich wird das mit einem Shell-Script nicht so einfach möglich sein.
 
  • Gefällt mir
Reaktionen: nutrix
CyborgBeta schrieb:
Ich glaube, das stimmt nicht ganz, da auch ein Bash- (oder Sh-) Script einmal ganz gelesen wird, bevor es interpretiert und ausgeführt wird.
Ja. Das schon. Ist aber im wesentlichen nur eine Syntaxprüfung und so ein Compiler macht deutlich mehr.

CyborgBeta schrieb:
Vermutlich wird das mit einem Shell-Script nicht so einfach möglich sein.
Na geht schon. Man lässt sich halt vom rar-Kommando den Archivinhalt anzeigen und parst und iteriert sich da irgendwie durch. Man kann schon in Shellskript einiges machen. Und es gibt durchaus auch umfangreichere Shellskripte. Zum Beispiel der FreeBSD-Installer oder auch der Updater ist im wesentlichen Shell-Skript.

Und ganz oft muss Shellskript ja auch gar nicht die eigentliche Arbeit leisten. Das ist ja oft eher glue-code um Programme zu verbinden.

nutrix schrieb:
Es geht direkt um die Funktion, die man direkt durchreichen kann, nicht um alles andere darum herum.
Ja. Aber selbst da.
Mal ein ganz triviales Beispiel:
CPUs haben häufig spezielle Instruktionen um gegen 0 zu vergleichen. Das kommt öfter mal vor und deshalb lohnt es sich auch, dafür eine eigene Instruktion zu haben die dann auch besonders schnell ausgeführt wird.
In der Bash wird es aber so bei der Ausführung von Shell-Skript nicht verwendet werden weil die Bash ja vorher (zur Compile-Zeit) nicht weiß, ob das Skript am Ende mit Null vergleicht oder nicht. Da wird dann auf der Maschinencode-Ebene also letztlich ein generischer Vergleich laufen. Theoretisch könnte das die Bash vorher prüfen und dementsprechend einen anderen Codepfad wählen. Macht aber keiner, weil allein die Prüfung mehr Zeit kostet, als das Du sie durch die optimierte Maschinencode-Instruktion rausholen kannst.
 
  • Gefällt mir
Reaktionen: CyborgBeta und nutrix
andy_m4 schrieb:
Mal ein ganz triviales Beispiel:
CPUs haben häufig spezielle Instruktionen um gegen 0 zu vergleichen. Das kommt öfter mal vor und deshalb lohnt es sich auch, dafür eine eigene Instruktion zu haben die dann auch besonders schnell ausgeführt wird.
In der Bash wird es aber so bei der Ausführung von Shell-Skript nicht verwendet werden weil die Bash ja vorher (zur Compile-Zeit) nicht weiß, ob das Skript am Ende mit Null vergleicht oder nicht. Da wird dann auf der Maschinencode-Ebene also letztlich ein generischer Vergleich laufen.
Stimmt, gutes Beispiel. @BeBur, dann hast Du doch recht. 🤝
 
  • Gefällt mir
Reaktionen: CyborgBeta
Zurück
Oben