Słowa „skrypt” i Java w jednym zdaniu? Czy to może się kojarzyć z czymś innym niż JavaScript? I co ciekawe nie jest to post na temat JavaScript. Chociaż o dziwo już dwa razy wymieniłem nazwę tego języka we wstępie. Dzięki wprowadzeniu „JEP 330: Launch Single-File Source-Code Programs„ w JDK 11 mamy możliwość napisania kodu Java w kilka sekund i odpalenia go tak jakbyśmy uruchamiali zwykły skrypt bash-owy – tak po prostu!
Jak często zdarzyło Ci się podczas pisania skomplikowanego skryptu w bash-u lub innym języku skryptowym myśleć:
„hmm, gdyby tak można było to wszystko zrobić w Javie”
Zaraz potem jednak wracasz na ziemię i przypominasz sobie o procesie kompilacji kodu i całej otoczce sprawiającej, że pisanie skryptów w Javie to jednak nie jest najlepszy pomysł.
Co to jest Shebang?
Shebang to termin używany w kontekście skryptów wykonywalnych na systemach operacyjnych typu Unix, Linux oraz MacOS, a oznacza on sekwencję znaków składającą się z krzyżyka (#
) i wykrzyknika (!
) umieszczoną na samym początku pliku skryptowego. Jest to tzw. magiczny numer, który informuje system o tym, jaki interpreter powinien zostać użyty do wykonania danego skryptu.
Na przykład, jeśli na początku pliku skryptowego umieścimy linijkę:
#!/bin/bash
informuje to system, że do wykonania skryptu powinien zostać użyty interpreter Bash.
Jeśli natomiast na początku pliku umieścimy:
#!/usr/bin/python3
oznacza to, że skrypt powinien zostać wykonany przez interpreter Pythona w wersji 3.
Shebang jest szczególnie przydatny w przypadku, gdy skrypt ma być wykonany bezpośrednio jako program (tj. bez wcześniejszego wywoływania interpretera), ponieważ umożliwia systemowi automatyczne rozpoznanie, za pomocą którego interpretera skrypt powinien zostać uruchomiony.
Hello World w kilka sekund.
No wiec sprawdźmy jak to działa w praktyce. Na początek coś najprostszego:
$ vim script.java
public class ScriptJava { public static void main(String... args) { System.out.println("Hello World"); } }
$ java script.java Hello World
Jak widać działa. Z ciekawszych rzeczy można zauważyć, że nazwa pliku nie musi być taka sama jak nazwa klasy publicznej.
Idźmy dalej i spróbujmy coś popsuć:
$ mv script.java script.zupa $ java script.zupa Error: Could not find or load main class script.zupa Caused by: java.lang.ClassNotFoundException: script.zupa
No niestety to nie zadziałało. Czyli jeśli wykonujemy kod przez podanie naszego skryptu do komendy java to plik musi mieć rozszerzenie .java. To może inaczej:
$ mv script.zupa script.sh $ chmod +x script.sh $ ./script.sh ./script.sh: line 1: public: command not found ./script.sh: line 3: syntax error near unexpected token `(' ./script.sh: line 3: ` public static void main(String[] args) {'
Też nie, ale tutaj możemy coś zaradzić zmieniając pierwszą linię naszego skryptowego pliku:
#!/usr/bin/java --source 11 public class ScriptJava { public static void main(String... args) { System.out.println("Hello World"); } }
Hmm, co ciekawe teraz tworząc ten wpis zacząłem się zastanawiać jaki „Language” wybrać w powyższym Code Block – eeh to przecież nadal Java 🙂
$ ./script.sh Hello World
Na zakończenie sprawdźmy jeszcze co się stanie jeśli jednak byśmy to zrobili w pliku .java:
$ mv script.sh script.java $ ./script.java ./script.java:1: error: illegal character: '#' #!/usr/bin/java --source 11 ^ ./script.java:1: error: class, interface, or enum expected #!/usr/bin/java --source 11 ^ 2 errors error: compilation failed
hmm to może:
$ java script.java script.java:1: error: illegal character: '#' #!/usr/bin/java --source 11 ^ script.java:1: error: class, interface, or enum expected #!/usr/bin/java --source 11 ^ 2 errors error: compilation failed
Bunt. Czyli jeśli chcemy aby nasza Java była skryptem to traktujmy ją jak skrypt.
Przekazywanie argumentów
Jeśli skrypt to przekazywanie argumentów. Jak trudne to może być? Sprawdźmy.
#!/usr/bin/java --source 11 public class ScriptJava { public static void main(String... args) { System.out.println("Hello World from " + args[0); } }
$ ./script.sh Moon Hello World from Moon
Coś ambitniejszego?
Co jeśli chcielibyśmy napisać skrypt, który rzeczywiście coś robi? W Javie prędzej czy póżniej przy pisaniu programu będziemy chcieli użyć jakiejś zależność. Na codzień pomaga nam w tym system do zarządzania zależnościami taki jak Maven lub Gradle. Jednak w tym przypadku chcemy aby nasz skrypt był jak najprostszy i nie wymagał ciężkiego arsenału narzędzi wspomagających.
Cel
Prosty skrypt do zarządzania zadaniami do wykonania.
Wymagania:
Skrypt ma umożliwiać cztery operacje:
- dodanie nowego zadania do wykonania
- wypisanie zadań do wykonania
- zmianę statusu zadania na DONE/NOT DONE
- usunięcie zadania do wykonania
Plan
Zadania wykonane będziemy zapisywać do bazy danych. W celu uproszczenia użyjemy plikowej bazy danych H2. Jednak nic nie stoi na przeszkodzie aby użyć jakiejkolwiek innej bazy danych. Równie dobrze można by zapisać zdania w pliku bezpośrednio jednak ponieważ chcę tutaj pokazać jak używać jakiejś zewnętrznej biblioteki więc pozostaję przy użyciu h2.jar.
Wykonanie
- Najpierw stworzymy plik skryptu. Nazwijmy go tasks.sh – aby nie było widać ze pod spodem jest Java 😉
- W drugim kroku ściągniemy bazę H2 i dla ułatwienia zapiszemy plik jako h2.jar bez wersji.
touch tasks.sh wget http://repo2.maven.org/maven2/com/h2database/h2/1.4.199/h2-1.4.199.jar -O h2.jar ls h2.jar tasks.sh
Shebang line
Dodajmy pierwsza najważniejszą lianijkę do naszego tasks.sh:
#!/usr/bin/java --class-path h2.jar --source 11
Następnie możemy już kodować w samej Javie:
#!/usr/bin/java --class-path h2.jar --source 11 import java.sql.*; public class Tasks { public static void main(String... args) throws SQLException { initDatabase(); validateParameters(args); String action = args[0]; switch (action) { case "list": case "l": printToDoList(); break; case "add": case "a": addNewTask(args[1]); break; case "rm": case "r": deleteTask(parseTaskId(args[1])); break; case "done": case "d": changeTaskStatus(parseTaskId(args[1]), true); break; case "undone": case "u": changeTaskStatus(parseTaskId(args[1]), false); break; default: throw new IllegalArgumentException(args[0]); } } private static void initDatabase() throws SQLException { try (Connection conn = DriverManager.getConnection(URL)) { Statement statement = conn.createStatement(); statement.executeUpdate("CREATE TABLE IF NOT EXISTS TODO (" + " id INTEGER NOT NULL auto_increment, " + " task VARCHAR(255), status BOOL default false, " + " primary key (id))"); statement.close(); } } private static void validateParameters(String[] args) { if (args.length == 0 || null == args[0]) { System.out.println("What would you like to do:" + "\n\tlist[l] - List all todo task" + "\n\tadd[a] {\"ToDo task\"}" + "\n\trm[r] {task id}" + "\n\tdone[d] {task id}" + "\n\tundone[u] {task id}"); System.exit(1); } } private static int parseTaskId(String arg) { return Integer.parseInt(arg); } private static void printToDoList() throws SQLException { try (Connection conn = DriverManager.getConnection(URL)) { printToDoList(conn); } } private static void printToDoList(Connection conn) throws SQLException { try (Statement statement = conn.createStatement()) { try (ResultSet rs = statement.executeQuery("SELECT * FROM TODO")) { System.out.println(String.format("|%4s.|%-40s|%1s|", "No", "Task", "Status")); while (rs.next()) { printTask(rs); } } } } private static void printTask(ResultSet rs) throws SQLException { int id = rs.getInt("id"); String task = rs.getString("task"); boolean status = rs.getBoolean("status"); System.out.println(String.format("|%4s.|%-40s|%1s|", id, task, status ? "[DONE]" : "[ ]")); } private static void addNewTask(String toDoTask) throws SQLException { try (Connection conn = DriverManager.getConnection(URL); Statement statement = conn.createStatement()) { statement.executeUpdate("INSERT INTO TODO (task) VALUES ('" + toDoTask + "');"); printToDoList(conn); } } private static void changeTaskStatus(Integer id, Boolean status) throws SQLException { try (Connection conn = DriverManager.getConnection(URL); Statement statement = conn.createStatement()) { statement.executeUpdate("UPDATE TODO set STATUS=" + status + " where ID=" + id + ";"); printToDoList(conn); } } private static void deleteTask(Integer id) throws SQLException { try (Connection conn = DriverManager.getConnection(URL); Statement statement = conn.createStatement()) { statement.executeUpdate("DELETE FROM TODO where ID=" + id + ";"); printToDoList(conn); } } private static final String URL = "jdbc:h2:./persistent_todo"; }
Sprawdzam
$ ls h2.jar tasks.sh $ chmod +x tasks.sh $ ./tasks.sh What would you like to do: list[l] - List all todo task add[a] {"ToDo task"} rm[r] {task id} done[d] {task id} undone[u] {task id} $ ./tasks.sh add "Bake a pizza" | No.|Task |Status| | 1.|Bake a pizza |[ ]| $ ./tasks.sh add "Write a blog post about this" | No.|Task |Status| | 1.|Bake a pizza |[ ]| | 2.|Write a blog post about this |[ ]| $ ./tasks.sh add "Go to sleep today" | No.|Task |Status| | 1.|Bake a pizza |[ ]| | 2.|Write a blog post about this |[ ]| | 3.|Go to sleep today |[ ]| $ ./tasks.sh done 1 | No.|Task |Status| | 1.|Bake a pizza |[DONE]| | 2.|Write a blog post about this |[ ]| | 3.|Go to sleep today |[ ]| $ ./tasks.sh done 2 | No.|Task |Status| | 1.|Bake a pizza |[DONE]| | 2.|Write a blog post about this |[DONE]| | 3.|Go to sleep today |[ ]| $ ./tasks.sh done 3 | No.|Task |Status| | 1.|Bake a pizza |[DONE]| | 2.|Write a blog post about this |[DONE]| | 3.|Go to sleep today |[DONE]| $ ./tasks.sh undone 3 | No.|Task |Status| | 1.|Bake a pizza |[DONE]| | 2.|Write a blog post about this |[DONE]| | 3.|Go to sleep today |[ ]| $ ls h2.jar persistent_todo.mv.db tasks.sh
Wykonując powyższe komendy możemy bardzo wyraźnie zauważyć opóźnienie jakie nam towarzyszy przy każdej operacji. Niestety tutaj uwydatnia się chyba największa wada stosowania Javy do pisania skryptów wiersza poleceń. Ten skrypt jednak musi być skompilowany i za każdym razem musi być wystartowana nasza maszyna wirtualna a wiemy, że boot JVM zajmuje czas.
Podsumowanie
Java uruchamiana jedną komendą – niby nic wielkiego a jednak fajne ułatwienie do szybszego uruchomienia kodu. Szkoda, że czekaliśmy na takie coś tak długo. Jest to dostępne w wersji JDK 11 i też od tej wersji Oracle wprowadza nową licencję. Jeśli interesuje cię czy Java jest nadal darmowa to przeczytaj mój artykuł: Czy Java jest nadal darmowa?
Po co nam to?
Cel wprowadzenia tego improvement-u do JDK jest sprawienie aby uczenie się języka Java było jeszcze prostsze i nie wymagało całej otoczki jaka nam towarzyszy przy tworzeniu bardziej rozbudowanych aplikacji. Dodatkowo dzięki temu, że wszystko możemy mieć teraz w jednym pliku i nie widzimy skompilowanych plików class (normalnie z jednego pliku źródłowego kompilator może nam wygenerować kilka plików .class) możemy tworzyć łatwiej małe narzędzia pomocnicze przy użyciu Javy.
Czy pisanie skryptów w Java jest użyteczne?
Nie wiem. Ty mi powiedz i daj znać w komentarzu…
cudo, gdzie mozna o tym wiecej ppoczytac?