Introduction to JLine 3
1. 서론
JLine은 콘솔 입력을 처리하기 위한 라이브러리로, GNU readline 라이브러리나 ZSH 라인 에디터와 유사한 기능을 제공합니다.
이번 튜토리얼에서는 JLine 3에 대해 살펴보며, JLine이 무엇인지, 무엇을 할 수 있는지, 그리고 어떻게 사용하는지를 배우겠습니다.
2. 의존성
JLine을 사용하기 전에, 현재 작성 시점에서의 최신 버전인 3.28.0을 빌드에 포함해야 합니다.
Maven을 사용하는 경우, 다음과 같이 pom.xml 파일에 의존성을 포함할 수 있습니다:
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline</artifactId>
<version>3.28.0</version>
</dependency>
또한, Windows에서 실행하려면 JLine의 터미널 제공 라이브러리도 필요하며, 추천되는 것은 JANSI입니다.
참고로, macOS 및 Linux 시스템에서는 JLine이 외부 함수 및 메모리 API(FFM) 또는 Java 네이티브 인터페이스(JNI) 바인딩을 적절히 사용할 수 있기 때문에 이 라이브러리가 필요하지 않습니다.
이제 JLine을 애플리케이션에서 사용할 준비가 되었습니다.
3. 터미널
JLine 내에서 중심이 되는 추상화는 Terminal입니다. 이는 입력을 읽고 출력을 쓸 수 있는 터미널 또는 명령줄 인터페이스를 나타냅니다. TerminalBuilder를 사용하여 이들을 구성할 수 있습니다:
try(Terminal terminal = TerminalBuilder.builder().build()) {
// 여기서 터미널을 사용합니다.
}
터미널은 Closeable을 구현합니다. 이는 코드가 완료된 후 터미널을 올바르게 닫기 위해 try-with-resources 패턴을 사용할 수 있음을 의미합니다.
빌더를 커스터마이즈하지 않으면 애플리케이션이 실행 중인 시스템 터미널의 래퍼가 생성됩니다. 이를 통해 지원되는 터미널 유형 세트에서 자동으로 터미널 유형을 감지할 수 있습니다. 이는 더 고급 입력 및 출력 구조를 사용하는 데 도움을 줍니다. 지원되는 터미널 유형을 감지할 수 없는 경우에는 JLine 기능 지원이 제한된 덤(단순한) 터미널로 대체됩니다.
Terminal 인스턴스를 얻은 후에는 직접 상호작용할 수 있습니다. JLine은 터미널에 대한 입력 및 출력을 InputStream 및 OutputStream 인스턴스 또는 Reader 및 Writer 인스턴스로 노출합니다:
InputStream inputStream = terminal.input();
OutputStream outputStream = terminal.output();
Reader reader = terminal.reader();
PrintWriter writer = terminal.writer();
이들은 우리가 예상하는 대로 사용할 수 있습니다. 일반적으로 출력 스트림에 쓰는 경우 주기적으로 플러시해야 출력이 표시됩니다:
terminal.flush();
JLine은 또한 터미널 크기, 커서 위치 등과 같은 정보를 확인할 수 있는 메커니즘을 제공합니다:
Size size = terminal.getSize();
이 기능은 터미널 래퍼가 이를 지원하는 경우에만 작동하며, 덤 터미널에서는 지원되지 않을 수 있습니다.
4. 라인 리더
Terminal이 JLine 사용의 중심이라면, LineReader가 이 라이브러리를 사용하는 진짜 이유입니다. 사용자로부터 입력을 받아들이고 여러 복잡한 입력 및 라인 편집을 지원합니다.
라인 리더를 사용하기 위해서는 먼저 생성해야 합니다. 이전과 유사하게 LineReaderBuilder를 사용하여 이것을 수행합니다:
LineReader lineReader = LineReaderBuilder.builder()
.terminal(terminal)
.build();
최소한 읽고자 하는 터미널을 제공해야 하며, 나중에 여러 가지 다른 옵션도 제공할 수 있습니다.
LineReader를 얻은 후에는 이를 사용하여 터미널에서 입력을 읽을 수 있습니다:
try {
String line = lineReader.readLine("> ");
terminal.writer().println("읽은 내용: " + line);
} catch (Exception e) {
// 예외 처리
}
LineReader.readLine() 메소드는 여러 오버로드가 있으며, 가장 간단한 형태는 사용자 입력을 위한 프롬프트로 사용할 String을 받습니다. 이는 사용자가 입력한 문자열을 반환하여 프로그램에서 사용할 수 있습니다:
입력하는 문자열을 삭제하고 다시 입력하는 것 외에도, 화살표 키를 사용하여 문자열 중간에서 편집하는 기능도 포함되어 있습니다.
readLine() 메소드는 사용자 입력이 중단되면 예외를 발생시킵니다. UserInterruptException은 인터럽트가 수신되면 발생하며, 일반적으로 Ctrl + C로 인해 발생합니다. EndOfFileException은 기본 입력 스트림이 닫히거나 EOF 신호를 수신할 경우 발생하며, 이는 Ctrl + D를 눌러 발생할 수 있습니다. 그런 다음 이러한 예외를 코드에서 처리할 수 있습니다.
readLine() 메소드의 더 복잡한 버전은 왼쪽 프롬프트, 오른쪽 프롬프트, 초기 값 및 문자 마스크를 지정할 수 있습니다. 예를 들어, 비밀번호 입력을 받을 때 사용됩니다:
String line = lineReader.readLine("> ", " <", '#', "비밀번호");
이는 왼쪽 프롬프트로 “> ”를 사용하고 오른쪽 프롬프트로 ” <”를 사용합니다. 모든 입력 문자는 “#” 기호로 마스킹되고, 입력에 대한 기본값은 “비밀번호”로 설정됩니다.
모든 매개변수는 선택적이며 필요하지 않은 경우 null로 대체할 수 있습니다. readLine()의 다른 대안은 궁극적으로 사용을 용이하게 하기 위해 이 기능을 감싸는 것입니다.
5. 히스토리
사용자 입력 수용과 함께 표준 LineReader는 자동으로 히스토리를 제공합니다. 이는 화살표 키를 사용하여 히스토리를 스크롤하고, Ctrl + R 및 Ctrl + S를 사용하여 검색할 수 있습니다. 이러한 제어 방식은 Bash 히스토리와 동일합니다.
히스토리는 History 인터페이스의 인스턴스를 사용하여 관리하며, 이를 LineReader와 연결하여 생성합니다. 기본적으로 이는 입력이 수신됨에 따라 자동으로 히스토리를 기록하고 라인 리더에서 접근할 수 있도록 해주는 DefaultHistory 클래스의 인스턴스가 됩니다. 사용자 정의 구현을 작성하여 LineReader에 연결할 수도 있습니다:
LineReader lineReader = LineReaderBuilder.builder()
.terminal(terminal)
.history(new DefaultHistory())
.build();
DefaultHistory 구현은 일부 옵션 플래그 및 변수를 제공하여 LineReader의 작동 방식에 영향을 줄 수 있는 설정을 허용합니다:
LineReader lineReader = LineReaderBuilder.builder()
.terminal(terminal)
.option(LineReader.Option.HISTORY_IGNORE_DUPS, false)
.variable(LineReader.HISTORY_FILE, Path.of("target/jline-history"))
.variable(LineReader.HISTORY_SIZE, 5)
.build();
이 설정은 히스토리가 중복 항목을 기록하도록 허용하고, 실행 간 공유를 위해 모든 히스토리를 “target/jline-history” 파일에 저장하며, 이전 다섯 항목만 다시 호출하도록 제한합니다.
6. 완성
입력을 타이핑할 뿐만 아니라, JLine은 Tab 키로 명령 완성을 지원합니다. 이를 위해 LineReader 생성 시 Completer 인스턴스를 제공합니다:
LineReader lineReader = LineReaderBuilder.builder()
.terminal(terminal)
.completer(completer)
.build();
우리는 이 인터페이스의 사용자 정의 구현을 작성할 수도 있습니다. 그러나 JLine에는 사용할 수 있는 몇 가지 표준 구현이 포함되어 있습니다.
가장 간단하게 사용할 수 있는 구현은 StringsCompleter입니다:
Completer completer = new StringsCompleter("foo", "bar", "baz");
이는 완성 후보로 작용하는 문자열 목록을 받습니다. Tab 키를 누르면 이 목록에 따라 후보가 나타납니다.
완성 후보가 단일 항목만 일치하면 자동으로 삽입됩니다. 그렇지 않으면 여러 가능한 일치 항목 중에서 선택합니다.
우리는 또한 사용할 수 있는 다른 간단한 완성기를 가지고 있습니다. 여기에는 Java enum의 항목에서 선택하는 EnumCompleter와 주어진 경로 아래의 파일 및 디렉토리에 따라 완성을 제공하는 FilesCompleter, DirectoriesCompleter, FileNameCompleter가 포함됩니다.
원하는 경우, 여러 완성기를 결합하여 AggregateCompleter를 사용할 수도 있습니다:
Completer completer = new AggregateCompleter(
new StringsCompleter("foo", "bar", "baz"),
new Completers.FilesCompleter(Path.of("/baseDir")),
new Completers.DirectoriesCompleter(Path.of("/baseDir"))
);
이제 고정 문자열 세트와 주어진 경로 아래의 모든 파일 및 디렉토리가 가능한 완성 후보로 제공됩니다.
추가로, 우리는 복잡한 명령 구조를 정의할 수 있는 몇 가지 복잡한 완성기를 사용할 수 있습니다:
- ArgumentCompleter는 명령의 각 단어에 대한 완성 후보를 제공합니다.
- RegexCompleter는 현재 명령을 정규 표현식과 비교하여 다음에 제공할 완성을 결정합니다.
- TreeCompleter는 가능한 명령 구조를 트리 형태로 구성할 수 있습니다.
우리는 이러한 모든 것을 요구 사항에 맞는 구조로 결합할 수 있습니다.
7. 요약
이번에는 JLine에 대한 간단한 소개였습니다. 이 라이브러리로 할 수 있는 일은 훨씬 더 많습니다. 텍스트 기반 애플리케이션에서 사용자 입력을 받아야 할 경우, 한 번 시도해 보시는 것은 어떨까요?
이 기사에 대한 모든 예제는 GitHub에서 확인할 수 있습니다.