텍스트 편집기와 텍스트 파일을 활용해 복잡한 명령줄 도구에 간단하고 강력한 사용자 인터페이스를 만드는 방법을 살펴봅니다.
업데이트: 몇 가지 피드백을 받은 뒤, 이 방법이 다른 방법들에 비해 내게 무엇을 주는지 더 잘 설명해야겠다는 생각이 들었다. 약간 미묘하고 전달하기가 어려워서, 아마 목록 형태가 나을 것 같다.
첫째, 이 카드는 원래 A text editor as a user interface라는 제목이었는데, 그건 틀렸고 완전히 잘못된 인상을 주었다. 전적으로 내 잘못이었다. 미안하다!
이건 긴 명령을 편집하는 이야기가 아니다. 셸에는 그런 일을 위한 내장 방법이 있다. 예를 들면 Bash에는 Ctrl-x Ctrl-e 시퀀스가 있다.
나는 Emacs의 ELisp 확장처럼 텍스트 편집기가 제어하는 편집기 내부 인터페이스를 만들고 싶은 것이 아니다.
나는 가능한 한 Vim script를 더 쓰고 싶지 않다. 정말로. 이미 해봤다. 됐다.
나는 이것을 어떤 특정한 편집기로도 제한하고 싶지 않다.
이것은 보통 개인용 생태계로서의 명령줄 도구 모음 의 일부다.
이 방법의 가장 놀라운 장점 가운데 하나는 텍스트 파일을 그대로 남겨 두어서 이전 입력이 계속 남아 있다는 점이다.
덤으로, 지속되는 편집기 실행 취소 기록은 최근에 사용한 값을 다시 제공해 주기도 한다.
새 중간 섹션인 My image gallery tool example을 추가했는데, 이게 요점을 훨씬 더 잘 전달해 줄 것이라고 생각한다.
좋다, 이제 카드로 돌아가자.
몇 개의 명령줄 인자를 받는 유틸리티를 만드는 것과 완전한 텍스트 기반 사용자 인터페이스(TUI)를 만드는 것 사이에는 엄청난 노력 차이가 있다. 좋은 사용자 인터페이스는 만들기가 정말 어렵다.
그런데도 가끔은 그 중간쯤 되는 무언가를 원할 때가 있다. 내가 최근에 실험해 보고 있는 것은 내 프로그램의 사용자 인터페이스로 텍스트 편집기를 사용하는 것이다.
프로그램이 사용자를 대신해 텍스트 편집기(환경 변수 $EDITOR에 지정된 것)를 실행하게 하는 것은 잘 알려진 아이디어다. 지금 당장 떠오르는 기존 예시는 세 가지가 아니라 네 가지가 있다.
crontab -e로 크론 테이블 편집하기
git commit으로 커밋 메시지 편집하기
visudo로 /etc/sudoers 편집하기
vipw로 /etc/passwd 편집하기
이 네 가지는 모두 임시 파일을 열고, 당신이 좋아하는 텍스트 편집기의 따뜻하고 편안한 환경에서 마음껏 편집하게 한 다음, 그 입력을 가져다 사용한다.
vipw는 좋은 예시인데, 그 전체 목적이 적용하기 전에 편집 내용이 괜찮은지 확인하는 데 있기 때문이다. man 페이지에서 인용하면 다음과 같다.
"vipw edits the password file after setting the appropriate locks, and does any necessary processing after the password file is unlocked. vipw performs a number of consistency checks on the password entries, and will not allow a password file with a "mangled" entry to be installed."
당신도 텍스트 편집의 힘을 사용해 임의의 복잡성을 가진 프로그램을 제어할 수 있다.
억지로 만든 예시로, 내가 어떤 텍스트를 써서 rev 유틸리티로 뒤집고 싶다고 상상해 보자.
이 세 줄짜리 셸 스크립트는 당신이 좋아하는 텍스트 편집기로 임시 파일을 열고, 그 내용을 rev에 통과시켜 뒤집힌 결과를 출력한다.
FILE=$(mktemp) $EDITOR $FILE rev < $FILE
억지스러울 거라고 미리 말했다. 하지만 개념이 얼마나 단순한지는 알 수 있다. 단 한 줄만으로 나는 내 텍스트 편집기의 완전한 표현력을 손에 넣었다. 비슷한 편집 능력을 가진 내 전용 임시 TUI를 직접 작성하려면 몇 년은 걸릴 것이다. 그리고 임시방편 인터페이스와 달리, 이 인터페이스가 어떻게 작동하는지 설명할 필요도 없다!
수년 동안 나는 우리 가족의 디지털 사진 "앨범"을 관리하는 데 정말 애를 먹었다. 이건 시간이 지나도 버텨야 하므로, 독점 소프트웨어는 애초에 고려 대상이 아니다.
나는 천천히 Ruby 스크립트 모음(그리고 집 안 인트라넷 웹 서버에서 제공되는 PHP 웹사이트)을 만들어서 다음을 기반으로 갤러리를 생성하고 관리하게 했다.
디렉터리(연도와 행사별로 나뉨).
텍스트 파일.
가져오는 과정에서 생성되는 썸네일.
"가져오기" 단계는 gallery라는 스크립트이며, 내가 여기서 초점을 맞추고 싶은 부분이 바로 이것이다. 아무 인자 없이 실행했을 때의 도움말 텍스트는 다음과 같다.
Usage: gallery <dir> [dry]
Options: dry do a dry run to see what would happen
Input:
<dir>/
inbox/ <-- incoming images
inbox.txt <-- description, first line will be <out> in output
Output:
<dir>/
<out>/
set<#>/
thumbs/ (all thumbs with md5 names)
description.txt (copy of inbox.txt)
thumb_map.txt
(all images moved here)
장황하다, 맞다. 하지만 나는 이걸 거의 보지 않는다. 이 스크립트는 보통 단축키에서 호출된다. 내 갤러리 디렉터리는 거의 바뀌지 않기 때문에, 바뀌는 경우에는 이 도움말 메시지가 철저하길 바란다!
"Input" 섹션이 바로 이 카드의 핵심이다.
추가할 이미지들은 inbox 디렉터리에 들어 있다.
그 디렉터리 바깥에는 이미지 전체를 설명하는 inbox.txt라는 파일이 있다.
inbox.txt의 형식이 진짜 사용자 인터페이스다. 텍스트 파일의 각 줄은 세트 이름 지정, 태그 달기 같은 서로 다른 기능을 제공한다. 정확한 세부 사항은 그다지 중요하지 않고, 텍스트 파일을 편집하는 일이 엄청나게 직관적이고 빠르게 되었다는 사실이 중요하다.
gallery 스크립트는 썸네일 만들기, 디렉터리 생성, 파일 이동 같은 일을 처리한다.
전통적인 "UI"가 그냥 비켜서서 그냥 텍스트나 편집하게 해주면 좋겠다고 바란 적이 있는가?
이걸 사용하는 모습은 다음과 같다.
한쪽에는 inbox 디렉터리를 연 그래픽 디렉터리 창(대개 썸네일을 보여 주는 Thunar 파일 관리자)이 있다.
다른 쪽에는 inbox.txt를 연 Vim이 있다.
카메라나 어딘가의 파일 공유에서 이미지를 inbox 디렉터리로 옮긴다.
inbox.txt를 편집한다.
Vim의 키 입력 시퀀스로 gallery 스크립트를 실행한다.
gallery는 썸네일을 만들면서 작은 점 … 을 출력한다.
이미지들은 inbox 디렉터리 밖으로 이동된다.
작업이 끝나면 다시 Vim으로 돌아오고, inbox는 비어 있으며 다음 세트를 받을 준비가 되어 있다.
간단히 말해, 전통적인 UI가 없기 때문에 이것은 내가 이제까지 써 본 최고의 이미지 목록화 인터페이스다. 텍스트를 편집하는 것만큼 빠르다.
(참고로 나는 이 스크립트를 Vim 안에서 호출하긴 하지만, 이건 어떤 의미에서도 편집기 내부 플러그인이 아니다. 반대로 스크립트가 텍스트 편집기를 호출할 수도 있다. 다음 예시는 그쪽에 더 가깝다.)
아, 그리고 텍스트 파일의 또 다른 멋진 점이 있다. 도움말 텍스트의 "Output" 섹션을 아주 주의 깊게 봤다면, 나는 매번 사용 후에 inbox.txt 내용을 버리지 않는다는 점을 알아챘을지도 모른다. gallery 스크립트는 그것을 최종 갤러리에 세트용 description.txt로 그대로 복사해서, 내 다른 도구들도 계속 사용할 수 있게 한다! 멋지지 않은가?
find, ffmpeg, rsync 같은 도구의 명령줄 인터페이스는 믿기 어려울 정도로 복잡해질 수 있다. 원하는 만큼 장황한 주석과 설명이 달린 텍스트 파일로 그것들을 제어한다고 상상해 보라.
이건 마치 자신만의 단계별 마법사를 갖는 것과 비슷하지만, UI 프로그래밍은 전혀 필요 없고 새 인터페이스를 배울 필요도 없다.
이 예시에서는 인기 있는 Python 비디오 다운로더 yt-dlp를 위한 텍스트 인터페이스를 내가 어떻게 만들었는지 보여 주겠다.
이건 셸 스크립트로도 만들 수 있지만, 나는 자기 돌봄 차원에서 대신 Ruby로 하고 있다(게다가 Ruby is better for "shell" scripting라고도 생각한다).
먼저 스크립트를 보고, 그다음 설명하겠다.
#!/usr/bin/env ruby
$destination="/media/video" $settings_file = "#{$destination}/download.txt"
if !Dir.exist?($destination) puts "Error: #{$destination} doesn't exist." exit 1 end
dirs = Dir.glob "#{$destination}/*/" subdirs = dirs.map{|d| d.split('/').last }
$settings = { subdir: subdirs[0], fname: 'foo', url: 'https://example.com/foo/', };
def read_settings File.readlines($settings_file).each_with_index do |line, idx| if(idx==1) then $settings[:subdir] = line.chomp end if(idx==2) then $settings[:fname] = line.chomp end if(idx==3) then $settings[:url] = line.chomp end end end
if File.exist? $settings_file read_settings() end
File.open($settings_file, 'w') do |f| f.write '# ' f.write subdirs.join(' ') f.write "\n" f.puts $settings[:subdir] f.puts $settings[:fname] f.puts $settings[:url] end
system("vim #{$settings_file}")
if !File.exist? $settings_file puts "Error: #{$settings_file} was not written!" exit 1 end
read_settings()
outpath = "#{$destination}/#{$settings[:subdir]}" if !Dir.exist?(outpath) puts "ERROR: #{outpath} doesn't exist!" exit 1 end
timestamp = Time.now.to_i outfname = "#{outpath}/#{$settings[:fname]}_#{timestamp}"
cmd = "yt-dlp_linux -o '#{outfname}.%(ext)s' '#{$settings[:url]}'" puts "Running: #{cmd}..." system(cmd)
이건 꽤 긴 예시지만, 오류 검사 등을 포함한 완전한 애플리케이션이고 지금까지 꽤 견고하게 잘 작동해 왔다. (오류 검사 등이 없는 버전이라면 이보다 훨씬 짧을 수도 있다.)
이 텍스트 파일은 대상 하위 디렉터리, 기본 파일 이름, 그리고 비디오가 호스팅된 페이지 URL을 지정한다.
내가 꽤 멋지다고 생각하는 기능 하나는, 스크립트가 현재 다운로드 가능한 하위 디렉터리 목록을 텍스트 파일 안의 주석으로 채워 넣어 주기 때문에 내가 그것들을 기억할 필요가 없다는 점이다.
예시는 다음과 같다.
howto fix_toilet https://example.com/how-to/fix-a-toilet.html
이렇게 하면 비디오는 /media/video/howto/fix_toilet_1777324491.mp4 같은 형태로 저장된다.
기본 파일 이름 뒤에는 타임스탬프가 붙기 때문에, 강의 시리즈 같은 연속 비디오를 가져오면서 다음 번호가 무엇인지 알아내는 로직(1,2,3 등)을 추가하지 않고도 정렬을 위해 순서를 유지할 수 있다.
나는 무작위 이름의 임시 텍스트 파일을 사용하지 않는다. 마지막 설정(있다면)을 다시 사용하고 싶기 때문이다. 하지만 파일이 없으면 예시 기본값으로 채워 넣는다.
이보다 더 많은 매개변수가 있다면, 텍스트 파일 형식에 더 많은 주석이나 간단한 .ini, .conf, .toml 스타일 입력 "필드"를 추가하고 싶은 유혹을 받을지도 모르겠다. 단순하게 유지하면 라이브러리 없이도 읽어들이기가 정말 아주 쉽다.
나는 적어도 여섯 가지가 넘는 서로 다른 유틸리티에서 이 요령의 변형을 사용해 왔고, 그중 일부는 매일 쓴다. 이 기법이 정말 점점 마음에 들게 되었다.
이게 비슷한 용도를 위한 몇 가지 아이디어를 주었기를 바란다.