ls dir 만, 또는 ls directory 만. 이걸 하다가, 큰 산을 또 하나 넘었다. 거 참.. 정말 리눅스 세상은 넓고도 넓고, 할 수만 있다면, 내 하고 싶은 만큼 뭐든지 할 수도 있다. (실력만 있다면..)
이 작업을 위해 필수로 공부해야할 사항들을 적어보았다.
- getopt (외부 프로그램; bash shell 내부 명령어는 getopts)
- eval
- set
getopt 는 이번에 처음 봤고, 나머지는 본 적은 있으나 제대로 사용해보진 못했었다.
그리고, 이번에 아주 큰 도움을 주신 두 분의 빛나는 가르침에 고마움을 전한다.
(알고보니 뻘짓…) wrapper 가 아닌 간단한 방법!
ls 로 dir 만, 또는 file 만 표시하게 하려면, grep 의 힘을 빌려야 한다. 이렇기 때문에, alias 로 만들면 ls 에 다른 선택사항을 줄 수가 없다.
아아아!!! 이게 뭔 개뻘짓???
이 글을 쓰고 있던 도중, 훨씬 간단하게 내가 원하는 바를 얻을 수 있는 방법이 갑자기 눈에 보였다. 그야말로 유레카!? 아.. 거 참..
허나, getopt 등을 공부한 시간이 헛되지는 않았겠지..?
물론 이를 위해선 alias 가 아니고 function 이나 script 를 사용해야 한다. 이유는 인수(Arguments) 때문이다. 이들의 차이는 여기에.
아주 간단하므로, 스크립트보단 함수가 낫겠다.
# 파일만 function lsf() { ls -l "$@"| grep '^[-|l]' } # 디렉토리만 function lsd() { ls -l "$@"| grep '^d' }
이렇게 해놓고, 그냥 ls 사용하듯 선택사항과 디렉토리를 지정해주면 된다. 순서는 관계없다.
즉, 아래 명령 결과는 모두 같다.
lsf ~/다운로드 -tr -G --human-readable
lsf -tr -G ~/다운로드 --human-readable
lsf -tr ~/다운로드 -G --human-readable
lsf -tr -G ~ --human-readable ~/다운로드
이걸 미리 생각해냈다면, getopt 를 파는 수고를 안해도 됐을 텐데… 물론, 그 덕에 지식은 조금 늘었으니 후회는 없다.
oh my zsh 를 사용하고 있다면, ~/.oh-my-zsh/custom
내에 적당한 파일을 만들고 확장자 zsh 를 붙인 뒤 실행권한을 준다. 그리고 저 내용을 넣으면, Zsh 가 실행될 때 자동으로 명령을 쓸 수 있게 된다.
bash 는, 아마도 .bashrc 에 넣으면 될 듯??
** 간단하게 실험해봤을 땐 저 함수로 잘 돌아갔는데, 혹시라도 이상현상이 보이면 수정하도록!!
본론으로 돌아가서, getopt 란?
getopt 는 외부 프로그램이라 설치를 해야 하는데, 아마도 어느 배포판에나 기본 장착돼 있는 듯 하다. 아치리눅스나 우분투 모두 getopt 가 들어있는 꾸러미 이름은 util-linux 이다.
#!/bin/bash # test_options.sh SHORT=a,b,c:,d:,h LONG=area,city1:,city2:,help OPTS=$(getopt --alternative --name weather --options $SHORT --longoptions $LONG -- "$@") echo "Options:$OPTS"
얘가 뭘 하는 친구인가 하면… (아.. 너무나 허무하고 간단하게 답을 찾은 뒤에 뻘짓을 기록하려니 좀 짜증나긴 한다.)
Shell script 에서 받은 Arguments 를 분석해주는 역할을 한다. 위에 언급한 StackAbuse 에 올라온 내용을 살짝 변경해서 getopt, set, eval, shift 등의 쓰임새를 정리해본다.
(여기에 쓰인 –alternative, –name 등은 man page 참고.)
일단, 기본 사용 형식은 이렇다. (man page)
$ getopt [options] -o|--options optstring [options] [--] parameters
기억해야할 것은 명령 끝 쪽에 붙은 --
이다. 이것이 의미하는 바는, ‘이 뒤로 더 이상 선택사항은 없다’. 가 된다. — 뒤에 나오는 문구는 옵션이 아닌, 처리해야할 대상이다. 그냥 ‘파일’이라고 생각하면 무난하다.
예를 들어, grep 로 –help 를 찾아야 한다고 생각해보자.echo '--try --nice --mark --help --only-you' | grep -P "--
help
"
이렇게 명령을 내리면 뭔 소린지 못 알아먹겠다고 화를 낸다.
이럴 때 ‘–‘ 를 붙인다.echo '--try --nice --mark --help --only-you' | grep -P -- "--
help
"
위 예에서처럼, 복잡한 명령에서 오류를 방지하고자할 때 — 를 붙이는 경우가 많다.
예제에선, -- "$@"
라고 썼는데, 옵션은 끝나고, $@(script 가 받은 인수 전체) 가 결국 getopt 가 받아야할 인수가 된다는 의미다. (– 를 빼버리면 getopt 에서 오류가 발생한다.)
이제, 이런 명령을 내려보면..
$ test_options.sh --city1 Paris --city2 NewYork --city1 'Paris' --city2 'NewYork' --
$OPTS 에는 getopt
의 결과값이 들어간다. getopt 에는 몇몇 옵션들이 있는데,
- –options 또는 -o : -k -l -d 등, 줄표와 이어지는 문자 1개만으로 이뤄진 선택사항
- –longoptions 또는 -l : –help 등, 줄표 2개와 이어지는 문자열로 이뤄진 선택사항
- — : 옵션이 아닌 인수. ls 라면 파일이나 디렉토리.
크게 정리하면 위와 같다.
그리하여, SHORT 변수에는, 스크립트가 받을 수 있는 모든 단문자 선택사항(one-character)을 넣어준다. 각 선택사항간 구분은 쉼표로 한다.
그게 SHORT=c:,d:,h
이다. 이때, 줄표는 뺀다.
LONG 에는 말 그대로 Long Options(multi-character), 즉 문자열로 이뤄진 선택사항을 넣어준다. 마찬가지로 선행되는 줄표 2개는 제외한다.LONG=city1:,city2:,help
그런데, 자세히 보면 뒤에 콜론(:)이 붙어있기도 하다. c, city1 등엔 있고, help 에는 없다.
콜론이 있으면 뒤에 값이 있음을 의미하고, 없으면 옵션 단독으로 쓰임을 뜻한다.
예를 들어, ls 라면 이런 다양한 옵션들이 붙을 수 있다.
ls -tl ~/.config --hide-control-chars -I 'a*' --block-size=M
- -t, -l : 단문자 선택사항
- –hide-control-chars : 문자열 선택사항
- -I ‘a*’ : 값이 있는 단문자 선택사항
- –block-size=M : 값이 있는 문자열 선택사항
크게는 두가지, 값 유무에 따른 하부 선택사항 두가지를 포함해 모두 네가지 종류가 있다.
위와 같이 단문자/문자열로 구분하고, 값이 있으면 뒤에 콜론을 붙여주면 된다.
getopt 는 이것들을 받아서 단(短)/장(長) 선택사항과, 일반 인수(– 뒤에 나오는)로 구분해주는 역할을 한다.
** 만약, 옵션에 없는 인수를 받았다면?
예를 들어, $ test_options.sh --city1 Paris --city2 NewYork --province B.C
라고 명령을 내리면, --province
는 ‘인식할 수 없는 옵션’이라고 표시된다.
이 때, 위에서 getopt --name weather
라고 명령을 줬었기 때문에, 오류의 주체는 ‘weather’ 가 된다. 만약, 이 선택사항이 없었다면, 그냥 getopt 오류로 표시된다.
# --name weather $ test_options.sh --city1=Paris --city2=NewY --area --province weather: 인식할 수 없는 옵션 '--province' # --name weather 없을 경우 $ test_options.sh --city1=Paris --city2=NewY --area --province getopt: 인식할 수 없는 옵션 '--province'
이제, 명령에 옵션이 아닌 인수를 붙여본다.
$ test_options.sh --city1=Paris -b --city2=NewY --area ~/Download/abcd.txt
뭘 하려는지는 모르지만, 옵션을 줬고, abcd.txt 파일도 필요하다고 알렸다.
이 결과를 보자면,
options: --city1 'Paris' -b --city2 'NewY' --area -- '/home/userB/Download/abcd.txt'
값이 있는 옵션명은 ‘이름과 값’이 공백으로 나뉘어 표시가 돼 있고, 옵션만 있는 것들은 단독으로 존재하고 있다. 그리고, 옵션이 아닌 인수(이해하기 쉽게 파일이라고 가정한다면)는, — 과 짝을 이뤘다.
그래서 어쩌라고?
getopt
getopt 에 여러 인수를 붙여주면 선택사항(Options)과 일반 인수(non-option parameter)를 나눠서 표시해준다.
여기선 위의 예제가 아닌, 내 (헛)고생의 산물로 얘기를 풀어가야겠다.
위에서 말한 ‘ls 디렉토리만’ 등을 사용하려면 grep 가 필요하고, 그러려면 함수나 스크립트가 필요하다.
이를 위해선 아무튼 인수를 받아 처리해야 하는데..? 그래서 아래와 같은 스크립트를 만들었다.
#!/bin/bash # ls-wrapper.sh # getopt 를 불러 인수 정리 SHORT="a,A,b,B,c,C,d,D,f,F,g,G,h,H,i,k,l,L,m,n,N,o,p,q,Q,r,R,s,S,t,u,U,v,x,X,Z,1,I:,T:" LONG="all,almost-all,author,escape,color,directory,dired,classify,file-type,full-time,\ group-directories-first,sort,no-group,human-readable,si,dereference-command-line,\ dereference-command-line-symlink-to-dir,hyperlink,inode,kibibytes,dereference,\ numeric-uid-gid,literal,hide-control-chars,show-control-chars,quote-name,reverse,\ recursive,size,context,zero,help,version,\ block-size:,format:,hide:,ignore:,indicator-style:,quoting-style:,sort:,time:,time-style:,tabsize:,width:" options=$(getopt --options $SHORT --longoptions $LONG -- "$@") echo "Options:$options" eval set -- "$options" exit 1
여기에 쓰인 SHORT, LONG 은 모두 실제로 ls 에 있는 옵션들이다. 장,단 선택사항과 값유무를 구분해서 정리했다. (뭔가 빠진게 있을 지도 모른다.)
getopt 는 SHORT, LONG 과, 입력받은 인수에 따라 결과를 반환한다. 예를 들어, 이런 명령을 내렸다고 해보자면,
$ ls-wrapper.sh -tl ~/다운로드/OS\ 설치\ 이미지 --hide-control-chars --show-control-chars -I 'neon*' -T 10 --block-size=M Options: -t -l --hide-control-chars --show-control-chars -I 'neon*' -T '10' --block-size 'M' -- '/home/nemoarch/다운로드/OS 설치 이미지'
단일 옵션과, 문자열 옵션이 구분되어 정리가 돼 출력됐다. 보기 좋게, 예쁘게.
문제는 이제부터다. 넘을 산이 하나 남았다.
이걸 어떻게 ls 로 넘겨줄건데??
set
이제 set 이 힘을 쓸 차례가 됐다. set 은 여러가지 일을 해주는데, 기본 설명은 아래와 같다.
set — set or unset options and positional parameters
터미널에서 set 을 그냥 치면, 환경변수를 출력해준다. 여기서 필요한 기능은 set -- [argument...]' 이다.
는, $options 를 현재 스크립트의 parameter 로 변경 시켜준다.
set -- $options
- 사용자가 입력한 인수 : -tl ~/다운로드/OS\ 설치\ 이미지 –hide-control-chars –show-control-chars -I ‘neon*’ -T 10 –block-size=M
- getopt 가 정리한 인수 : -t -l –hide-control-chars –show-control-chars -I ‘neon*’ -T ’10’ –block-size ‘M’ — ‘/home/userb/다운로드/OS 설치 이미지’
set 이전 $1 은 ‘-tl’ 이었고, getopt/set 이후 $1 은 ‘-t’ 가 된다.
이렇게 getopt 가 차분히, 정확하게 말하면 종류별로 정리를 해줬기 때문에 다음 작업을 용이하게 할 수 있게 된다.
그런데 eval 은?
set 까지 이해를 했는데, 그 앞에 보니 eval 이 붙어있다. 왜 여기에 eval 이 붙어야하는지는 확 이해가 되진 않는다. eval 은, 변수에 외부 명령어가 담겨 있을때, 이걸 스크립트 내에서 실행하려고 할 때 쓰는 명령이다.
아무튼, 몇가지 실험을 해보니, 어떤 건 되고, 어떤 건 또 안된다.
- eval set — “$options” : 성공
- set — “$options” : 실패
- set — $options : 성공..? 또는 실패.
이런 식으로 좀 결과가 다르다. 뭔가 따옴표와 연관이 있는데.. 나름대로 충실히 설명된 글은 찾았지만, 확 와닿질 않는다.
그냥, eval set — “$변수” 를 기본값으로 외우는게 속편하겠다.
** set — $options 는 성공? 실패??set -- $options
로, 따옴표를 없앤다. (물론 eval 도 없다.)ls-wrapper.sh --block-size=1G
라고 명령을 내린다.
이때 생성된 $options 결과는, --block-size '1G'
로, 홑따옴표가 붙어져 있다.
이걸, set --$options
에 넣으면, 결과값에도 ‘1G’ 가 붙어, 최종 옵션으로 줄 때 오류가 발생한다.
case 와 shift
결국 case 를 위해서 getopt, set 을 했다.
다시 정리해본다.
- getopt : 사용자가 (지저분하게) 입력한 인수들을, 단/장 선택사항, 값 유무등으로 구분하여 잘 정리해준다.
- set — : getopt 가 정리한 값을 받아, 마치 그게 사용자가 실제 입력한 인수인 듯 위장한다.
이렇게 정리된 인수로, case 문을 사용해 분기해나간다.
while : do case "$1" in -a | -A | -b | -B | -c | -C | -d | -D | -f | -F | -g | -G | -h | -H | -i | -k | -l | -L | -m | -n | -N | -o | -p | -q | -Q | -r | -R | -s | -S | -t | -u | -U | -v | -x | -X | -Z | -1 ) S_OPTS=$(echo "$1" | sed "s/-//g") SHORT_OPT="$SHORT_OPT$S_OPTS" shift 1 ;; -I | -T ) S_OPTS2=$(echo "$1" | sed "s/-//g") S_OPTS2_VAL="$2" SHORT_OPT_WITH_VAL="$SHORT_OPT_WITH_VAL -$S_OPTS2 $S_OPTS2_VAL" shift 2 ;; --all | --almost-all | --author | --escape | --color | --directory | --dired | --classify | --file-type | --full-time | --group-directories-first | --sort | --no-group | --human-readable | --si | --dereference-command-line | --dereference-command-line-symlink-to-dir | --hyperlink | --inode | --kibibytes | --dereference | --numeric-uid-gid | --literal | --hide-control-chars | --show-control-chars | --quote-name | --reverse | --recursive | --size | --context | --zero | --help | --version ) L_OPTS=$(echo "$1" | sed "s/--//g") LONG_OPT="$LONG_OPT --$L_OPTS" shift 1 ;; --block-size | --format | --hide | --ignore | --indicator-style | --quoting-style | --sort | --time | --time-style | --tabsize | --width ) L_OPTS2_VAL=$2 LONG_OPT_WITH_VAL="$LONG_OPT_WITH_VAL "$1=$L_OPTS2_VAL shift 2 ;; --) FILES="$2" shift 1 break ;; *) echo "$1?? 이건 뭐야? 똑바로 안해?" ;; esac done
case "$1" in
이므로, 첫번째 인수부터 검사해나가기 시작한다.
첫째 인수가 -a 등등으로 시작한다면, ‘-‘ 를 빼고(sed), SHORT_OPTS 에 추가한다.
여기까진 알겠는데..
shift 는??
위에서 이미 언급한 ‘빛나는‘ 글에, ‘그림’이 첨부돼 있다. 그림을 보면 나같은 놈도 쉽게 알아먹을 수 있다.
shift 는 인수를 전진시켜주는 역할을 한다.
첫째 인수가 ‘-t’ 였고, 이미 처리가 끝났다면, 다음 인수로 넘어가서 case 작업을 계속 해줘야 한다. case $1 in
이기에 계속 첫째 인수만 조사를 하기에, 다음 인수로 넘겨주는 명령이 필요하다. 그게 shift 다.
근데 shift 1 은 뭐고 shift 2 는?
-t 와 같이 단일 선택사항일 때는 한칸만 앞으로 넘긴다.
-I ‘neon*’ 이나, –block-size=1G 처럼 값이 있는 경우는 2칸을 넘겨야 그 다음 항목이 나온다.
이런 식으로 해서 밀고 (당기며) 끝까지 인수를 탐색한 후 원하는 작업을 완성하면 된다!!!!!!!!!!!!!!!!!!!!!!!!!!
최종 명령은?
ls-wrapper 스크립트 마지막은 이런 식이다.
ls -$SHORT_OPT $LONG_OPT $SHORT_OPT_WITH_VAL $LONG_OPT_WITH_VAL "$FILES"
여기서도 eval 때문에 좀 시행착오가 있었는데..
아무튼, 저런 식으로 따옴표를 주지 않고 이어 쓰면 문제가 없다. 다만, 파일에 해당하는 변수에는 겹따옴표를 넣어줘야 한다.
eval 을 쓰면 ls 이후 전체를 따옴표로 묶어도 잘 작동하긴 했는데, 다만, 파일명 변수는 따로 따옴표를 넣어줘야 했다.
eval ls -al "-$SHORT_OPT $LONG_OPT $SHORT_OPT_WITH_VAL $LONG_OPT \"$FILES\""
eval 개념이 아직도 좀 헷갈리고, 확실하질 않으니 그냥 eval 없이 쓰는 위의 명령 체계를 쓰는게 좋겠다. (어쨌든 결과는 나오니..)
용두사미식 끝맺음이겠으나..
기운이 없어서 더는 못쓰겠네.
이 정도면 나중에 봐도 알아먹겠지.
님 그러다 나중에 c 언어도 공부하실 기세네요 ㅎㅎhttps://man7.org/linux/man-pages/man3/getopt.3.html 이런 c 함수가 있고, glib 에는 https://docs.gtk.org/glib/struct.OptionContext.html 이러한 함수가 있조. 건승하세요
안녕하세요.
왜인지 알 수는 없으나, 스팸으로 분류돼 있어서 모르고 있었습니다.
Nimf 는 잘 사용하고 있습니다. 고맙습니다. 아직 구판을 쓰고 있긴 합니다만.
Qt6 때문에 머지 않은 시기에 갈아타긴 해야할 듯 한데..
암튼, 건강 잘 챙기시고, 또 뵙겠습니다!