Rust 텍스트 블록
텍스트 블록
Java에서는 Java 15부터 추가된 텍스트 블록을 사용해 여러 행으로 표현된 문자열을 다음과 같이 쉽게 정의할 수 있다.
void dummy() {
String json = """
{
"a": 10,
"b": 20
}
""";
System.out.println(json);
}
여러 줄의 문자열을 이어붙이지 않아도 되고, 문자열 안에 있는 쌍따옴표를 이스케이프 없이 그대로 쓸 수 있어 문자열의 최종 모양을 쉽게 파악할 수 있게 된다. 나는 이게 코드 가독성에 큰 영향을 미치기 때문에 중요하다고 생각한다.
Rust에서도 여러 행으로 된 문자열을 간단히 표현할 수 있다. 문자열에
쌍따옴표나 역슬래시(\
)가 없다면 그냥 문자열을 여러 줄로 써주면 된다.
let text = "hello
world";
문자열에 포함된 역슬래시를 이스케이프가 아닌 문자열의 일부로 사용하고 싶다면 raw string을 사용하면 된다.
let text = r"C:\Program Files\Neptune
C:\Program Files\Uranus";
문자열 안에서 이스케이프 없이 쌍따옴표를 사용하려면 어떻게 해야 할까? raw string에서는 이스케이프를 인식하지 않지만 쌍따옴표가 문자열을 끝내버리기 때문에 문자열의 일부로 쌍따옴표를 사용할 수 없게 된다.
해결책은 raw string의 시작과 끝에 #
를 붙여주는 것이다. 따라서 맨
처음 보았던 JSON 문자열 예제를 Rust에서는 다음과 같이 쓸 수 있다.
fn dump() {
let text = r#"
{
"a": 10,
"b": 20
}
"#;
println!("{text}");
}
이게 끝인가? 이게 끝이라면 이 글을 쓰지 않았을 것이다. 위 코드를 실행시켜보면 약간 실망하게 된다.
코드에서 문자열 모양을 예쁘게 나타내기 위해 r#"
를 쓴 다음 줄을
바꾸었는데, 이게 문자열에도 그대로 들어가있다. 또한 코드 들여쓰기
레벨을 맞추기 위해 문자열도 들여쓰기를 했는데 이것 역시 문자열에
그대로 남아 있다.
문자열에 불필요한 줄바꿈과 들여쓰기를 없애려면 다음과 같이 수정해야 한다.
fn dump() {
let text = r#"{
"a": 10,
"b": 20
}
"#;
println!("{text}");
}
이게 최선인가? Java의 텍스트 블록처럼 알아서 불필요한 공백을 제거하게 할 수는 없을까?
text! 매크로
crates.io에서 간단히 검색해 몇몇 크레잇을 찾았지만, 내가 원하는 형대가 아니었다. 직접 매크로를 만들어서 공개하면 좋겠다는 생각이 들었다.
잽싸게 text!
매크로를 만들어봤다. 아주 초보적인 구현이다. 매크로를
만드는 좀더 자세한 방법은 Rust 매개변수
테스트를 참조하기 바란다.
use proc_macro::TokenStream;
use regex::Regex;
use syn::{parse_macro_input, LitStr};
#[proc_macro]
pub fn text(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as LitStr).value();
let mut leading_whitespace = "".to_string();
for c in input.chars() {
if !c.is_whitespace() {
break;
}
leading_whitespace.push(c);
}
let input = input.replacen(&leading_whitespace, "", 1);
let input = input.replace(&leading_whitespace, "\n");
let trailing_whitespace = Regex::new(r"\n\s*$").unwrap();
let input = trailing_whitespace.replace_all(&input, "");
let output = format!("r#\"{input}\"#");
output.parse().unwrap()
}
이제 코드에서 다음과 같은 식으로 텍스트 블록을 표현할 수 있다.
fn main() {
let text = text_blocks::text!(r#"
{
"a": 10,
"b": 20
}
"#
);
println!("{text}");
}
프로그램을 실행해 원하는 결과가 나오는 것을 확인했다. 조금 다듬고 최적화한 다음 crates.io에 공개할 수 있을 것 같았다.
공개하기 전에 몇 가지를 확인해보고 싶었다. 위 구현은 너무 단순하다. 텍스트 블록의 첫 줄을 검사해 삭제할 공백의 길이를 정하는데 이게 과연 올바른 방법일까? 위 JSON의 뒷부분 반만 문자열로 표현하고 싶다면 어떻게 될까?
fn main() {
let text = text_blocks::text!(r#"
"b": 20
}
"#
);
println!("{text}");
}
위 프로그램을 실행해 보면 결과가...
"b": 20
}
이런 문제는 Java 텍스트 블록에서는 발생하지 않는다. 텍스트 블록의 첫 줄만 보고 판단하면 안 된다. 제대로 처리하려면 문자열 전체를 읽어 가장 왼쪽에 있는 문자가 1열에 표시되도록 문자열을 조작해야 한다.
indoc 크레잇
text!
매크로를 어떻게 아름답게 구현할 수 있을지 생각하며 이것
저것 검색하다가 indoc을
발견했다. 설명을 읽어보니 내가 의도했던 기능이 이미 모두 구현되어
있었다. 그리고 serde_json을
포함해 엄청나게 많은 다른 크레잇에서 이미 indoc 크레잇이 사용하고 있는
것도 보였다.
처음부터 이걸 발견했다면 직접 매크로를 구현한다며 삽질을 하지는
않았을텐데. text!
매크로를 수정해 공개하는 것은 무의미해졌다.
이렇게 해서 내 두 번째 공개 크레잇이 될 뻔했던 text_blocks
는
태어나자마자 빛도 보지 못하고 휴지통으로 직행하게 되었다.
참고
- indoc 구현하려 했던 모든 기능을 이미 구현했고, 다른 프로젝트에서도 많이 사용되고 있다.
- Rust 매개변수 테스트 매개변수 테스트를 작성할 때 유용하게 사용할 수 있는 속성형 매크로(attribute-like macro)를 구현하는 방법을 설명한다.