Java Shebang. Pisanie skryptów w Java?

Java Shebang. Pisanie skryptów w Java?

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

  1. Najpierw stworzymy plik skryptu. Nazwijmy go tasks.sh – aby nie było widać ze pod spodem jest Java 😉
  2. 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…

Postaw mi kawę na buycoffee.to

Zapisz się do Newsletter i odbierz bonus! Okładka e-booka

Komentarz na temat “Java Shebang. Pisanie skryptów w Java?

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *