It is frequently possible to highlight different types of files with different colors, instead of with characters as -F would. This is an area where the two main ls versions differ:
GNU ls uses the –color option; checks the Unix file type, the file permissions and the file extension and uses its own database to control colors maintained using dircolors.
FreeBSD ls uses the -G option; checks only the Unix file type and file permissions. and uses the termcap database.
번역하자면 다음과 같다.
대부분의 ls에서는 다른 파일 타입에 대해 다른 색상으로 하이라이트 하는 것이 가능하다. (-F 옵션을 사용하여 파일 타입에 따른 글자를 추가로 표현하는 방식도 있다.) 두가지 주류 ls 버전은 색상 하이라이트 부분에서 차이가 난다.
GNU ls는 --color 옵션을 사용한다. 유닉스 파일 타입, 파일 권한 및 파일 확장자를 확인하며, dircolors의 데이터베이스를 통해 색상이 관리된다.
FreeBSD ls는 -G 옵션을 사용한다. 유닉스 파일 타입과 파일 권한만 확인하며, termcap의 데이터베이스를 통해 색상이 관리된다.
macOS는 내부적으로 Darwin을 사용하며, 내부적으로 BSD 커널과 Mach 커널을 사용한다.
리눅스에서는 GNU ls를 사용하고, macOS에서는 FreeBSD ls를 사용했다고 볼 수 있다. (정확히는 BSD계열이란 표현이 옳지만, 위키피디아 설명을 기준으로 구분할 경우를 말한다.) 터미널에서 man ls로 매뉴얼을 확인해 보면 macOS는 “BSD General Commands Manual"이라고 표현되어 있는 것을 볼 수 있다.
해결법
내가 생각하는 해결법은 2가지다.
macOS에서 GNU ls를 사용한다.
macOS의 BSD ls를 GNU ls와 비슷하게 설정한다.
나는 1번 해결법이 더 좋은 해결법이라 생각한다. 일단 리눅스에서 사용하던 환경을 일관되게 사용하려는 것이 목적이기도 했고, GNU ls는 파일 확장자에 따른 하이라이트도 제공하기 때문이다.
homebrew가 설치되었다면 brew install coreutils 명령으로 간단하게 설치할 수 있다.
GNU coreutils를 통해 설치된 명령어는 앞에 g가 붙는다. 즉, GNU coreutils의 ls는 gls로 호출해야 한다. ls 명령을 호출해도 gls가 호출될 수 있도록, 색상 옵션을 기본으로 넣게 설정하려면 쉘 rc파일에 아래 내용을 추가하자. (아마 최근 macOS는 zsh를 채용했기 때문에 .zshrc를 수정해야 할 것이다.)
alias를 활용하여 기존 BSD 버전 ls 대신 gls를, --color=auto 옵션까지 기본으로 추가해서 실행하도록 설정해주자.
여기까지 수행하면 일반파일, 실행파일, 디렉토리, 심볼릭 링크까지는 같은 색상으로 출력하지만, 파일 확장자별 구분이나, 깨진 심볼릭 링크까지는 확실히 구분해 주지 못한다.
현재 상태는 GNU ls의 기본 색 구분만 사용하고 있기 때문이다. 이전 원인 분석과정에서 설명했듯 GNU ls의 색상 관리에는 dircolors를 사용한다. GNU coreutils를 설치하면 ls 뿐만 아니라 dircolors도 같이 설치된다. dircolors의 설정을 반영하도록 rc파일을 수정해주자.
환경 변수 중 LSCOLORS를 ExGxFxdxCxdadahbadacec로 설정하면 된다.
해당 값은 기존 GNU ls와 최대한 비슷하도록 값을 설정한 것이다.
rc파일에서 자동으로 설정되게 하려면 아래와 같이 수정한다.
exportLSCOLORS="ExGxFxdxCxdadahbadacec"
이전에 언급했듯 GNU ls에 비해 기능이 부족하기 때문에 다양하게 색 구분은 할 수 없지만, 따로 프로그램 설치 없이 리눅스에서 사용하던 색 배치를 최대한 일관되게 볼 수 있다.
내부 원리
ls가 두 종류로 나뉘는 것을 설명하면서 각각 dircolors나 termcap의 데이터베이스를 사용한다는 식의 설명이 있었다. 결과적으로 두 ls 모두 shell 환경변수에서 값을 읽어온다. GNU ls는 LS_COLORS 환경변수를 참조하며, BSD ls는 LSCOLORS 환경변수를 참조한다. (중간에 _ 존재 여부로 차이난다.)
LS_COLORS (GNU ls의 환경 변수)
dircolors의 설정이 반영되도록 eval $(dircolors -b)를 rc파일에 삽입했었다. 그렇다면 dircolors -b는 무슨 일을 하는지 확인해 보자.
$ dircolors --help
Usage: dircolors [OPTION]... [FILE]
Output commands to set the LS_COLORS environment variable.
Determine format of output:
-b, --sh, --bourne-shell output Bourne shell code to set LS_COLORS
-c, --csh, --c-shell output C shell code to set LS_COLORS
-p, --print-database output defaults
--help display this help and exit
--version output version information and exit
If FILE is specified, read it to determine which colors to use for which
file types and extensions. Otherwise, a precompiled database is used.
For details on the format of these files, run 'dircolors --print-database'.
GNU coreutils online help: <https://www.gnu.org/software/coreutils/>
Full documentation at: <https://www.gnu.org/software/coreutils/dircolors>
or available locally via: info '(coreutils) dircolors invocation'
$ dircolors -b
LS_COLORS='rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:';
export LS_COLORS
설명에 나와있듯 dircolors -b는 bourne shell 호환 문법으로 LS_COLORS의 값을 환경변수로 설정한다. 현재 LS_COLORS의 값 부분이 길기 때문에 의미를 기준으로 좀 더 쪼개서 다시 보도록 하자.
보면 하나의 설정 당 :로 분리하여 설명하고 있으며, 여러 색 설정을 조합하여 쓰기 위해 ;를 사용하고 있음을 확인할 수 있다.
12번 라인은 파일의 타입에 따라 색 설정을 하도록 하고 있다. 39번 라인은 주로 압축 파일 확장자들을 같은 색으로 출력하도록 설정하고 있다. (tar등의 archive 파일이나, deb, rpm, jar 같이 내부적으로는 archive 압축 파일로 구성된 패키지 파일 등) 1012번 라인은 주로 이미지 파일 확장자들을 같은 색으로 출력하도록 설정하고 있다. 1317번 라인은 주로 동영상 파일 확장자들을 같은 색으로 출력하도록 설정하고 있다. 18~20번 라인은 주로 오디오 파일 확장자들을 같은 색으로 출력하도록 설정하고 있다.
여러 확장자들을 특성에 따라 같은 색으로 표현했으니, 파일 타입은 각각 어떤 방식으로 구분하는지 확인해보자.
staticstructbin_strcolor_indicator[]={{LEN_STR_PAIR("\033[")},/* lc: Left of color sequence */{LEN_STR_PAIR("m")},/* rc: Right of color sequence */{0,NULL},/* ec: End color (replaces lc+rs+rc) */{LEN_STR_PAIR("0")},/* rs: Reset to ordinary colors */{0,NULL},/* no: Normal */{0,NULL},/* fi: File: default */{LEN_STR_PAIR("01;34")},/* di: Directory: bright blue */{LEN_STR_PAIR("01;36")},/* ln: Symlink: bright cyan */{LEN_STR_PAIR("33")},/* pi: Pipe: yellow/brown */{LEN_STR_PAIR("01;35")},/* so: Socket: bright magenta */{LEN_STR_PAIR("01;33")},/* bd: Block device: bright yellow */{LEN_STR_PAIR("01;33")},/* cd: Char device: bright yellow */{0,NULL},/* mi: Missing file: undefined */{0,NULL},/* or: Orphaned symlink: undefined */{LEN_STR_PAIR("01;32")},/* ex: Executable: bright green */{LEN_STR_PAIR("01;35")},/* do: Door: bright magenta */{LEN_STR_PAIR("37;41")},/* su: setuid: white on red */{LEN_STR_PAIR("30;43")},/* sg: setgid: black on yellow */{LEN_STR_PAIR("37;44")},/* st: sticky: black on blue */{LEN_STR_PAIR("34;42")},/* ow: other-writable: blue on green */{LEN_STR_PAIR("30;42")},/* tw: ow w/ sticky: black on green */{LEN_STR_PAIR("30;41")},/* ca: black on red */{0,NULL},/* mh: disabled by default */{LEN_STR_PAIR("\033[K")},/* cl: clear to end of line */};
staticvoidparse_ls_color(void){charconst*p;/* Pointer to character being parsed */char*buf;/* color_buf buffer pointer */intind_no;/* Indicator number */charlabel[3];/* Indicator label */structcolor_ext_type*ext;/* Extension we are working on */if((p=getenv("LS_COLORS"))==NULL||*p=='\0'){/* LS_COLORS takes precedence, but if that's not set then
honor the COLORTERM and TERM env variables so that
we only go with the internal ANSI color codes if the
former is non empty or the latter is set to a known value. */charconst*colorterm=getenv("COLORTERM");if(!(colorterm&&*colorterm)&&!known_term_type())print_with_color=false;return;}ext=NULL;strcpy(label,"??");/* This is an overly conservative estimate, but any possible
LS_COLORS string will *not* generate a color_buf longer than
itself, so it is a safe way of allocating a buffer in
advance. */buf=color_buf=xstrdup(p);enumparse_statestate=PS_START;while(true){switch(state){casePS_START:/* First label character */switch(*p){case':':++p;break;case'*':/* Allocate new extension block and add to head of
linked list (this way a later definition will
override an earlier one, which can be useful for
having terminal-specific defs override global). */ext=xmalloc(sizeof*ext);ext->next=color_ext_list;color_ext_list=ext;++p;ext->ext.string=buf;state=(get_funky_string(&buf,&p,true,&ext->ext.len)?PS_4:PS_FAIL);break;case'\0':state=PS_DONE;/* Done! */gotodone;default:/* Assume it is file type label */label[0]=*(p++);state=PS_2;break;}break;casePS_2:/* Second label character */if(*p){label[1]=*(p++);state=PS_3;}elsestate=PS_FAIL;/* Error */break;casePS_3:/* Equal sign after indicator label */state=PS_FAIL;/* Assume failure... */if(*(p++)=='=')/* It *should* be... */{for(ind_no=0;indicator_name[ind_no]!=NULL;++ind_no){if(STREQ(label,indicator_name[ind_no])){color_indicator[ind_no].string=buf;state=(get_funky_string(&buf,&p,false,&color_indicator[ind_no].len)?PS_START:PS_FAIL);break;}}if(state==PS_FAIL)error(0,0,_("unrecognized prefix: %s"),quote(label));}break;casePS_4:/* Equal sign after *.ext */if(*(p++)=='='){ext->seq.string=buf;state=(get_funky_string(&buf,&p,false,&ext->seq.len)?PS_START:PS_FAIL);}elsestate=PS_FAIL;break;casePS_FAIL:gotodone;default:abort();}}done:if(state==PS_FAIL){structcolor_ext_type*e;structcolor_ext_type*e2;error(0,0,_("unparsable value for LS_COLORS environment variable"));free(color_buf);for(e=color_ext_list;e!=NULL;/* empty */){e2=e;e=e->next;free(e2);}print_with_color=false;}if(color_indicator[C_LINK].len==6&&!STRNCMP_LIT(color_indicator[C_LINK].string,"target"))color_symlink_as_referent=true;}
#ifdef COLORLS
if(f_color){/*
* We can't put tabs and color sequences together:
* column number will be incremented incorrectly
* for "stty oxtabs" mode.
*/f_notabs=1;(void)signal(SIGINT,colorquit);(void)signal(SIGQUIT,colorquit);parsecolors(getenv("LSCOLORS"));}#endif
#ifdef COLORLS
/* Most of these are taken from <sys/stat.h> */typedefenumColors{C_DIR,/* directory */C_LNK,/* symbolic link */C_SOCK,/* socket */C_FIFO,/* pipe */C_EXEC,/* executable */C_BLK,/* block special */C_CHR,/* character special */C_SUID,/* setuid executable */C_SGID,/* setgid executable */C_WSDIR,/* directory writeble to others, with sticky
* bit */C_WDIR,/* directory writeble to others, without
* sticky bit */C_NUMCOLORS/* just a place-holder */}Colors;staticconstchar*defcolors="exfxcxdxbxegedabagacad";
voidparsecolors(constchar*cs){inti;intj;size_tlen;charc[2];shortlegacy_warn=0;if(cs==NULL)cs="";/* LSCOLORS not set */len=strlen(cs);for(i=0;i<(int)C_NUMCOLORS;i++){colors[i].bold=0;if(len<=2*(size_t)i){c[0]=defcolors[2*i];c[1]=defcolors[2*i+1];}else{c[0]=cs[2*i];c[1]=cs[2*i+1];}for(j=0;j<2;j++){/* Legacy colours used 0-7 */if(c[j]>='0'&&c[j]<='7'){colors[i].num[j]=c[j]-'0';if(!legacy_warn){warnx("LSCOLORS should use ""characters a-h instead of 0-9 (""see the manual page)");}legacy_warn=1;}elseif(c[j]>='a'&&c[j]<='h')colors[i].num[j]=c[j]-'a';elseif(c[j]>='A'&&c[j]<='H'){colors[i].num[j]=c[j]-'A';colors[i].bold=1;}elseif(tolower((unsignedchar)c[j])=='x')colors[i].num[j]=-1;else{warnx("invalid character '%c' in LSCOLORS"" env var",c[j]);colors[i].num[j]=-1;}}}}