디렉터리 안의 파일만 깔끔하게 목록으로 보고 싶은 적이 있으신가요? 아니면 반대로 디렉터리만 보고 싶으셨나요? 그렇다면 아래 소개하는 GPLv3 라이선스의 오픈 소스 스크립트가 바로 여러분이 찾던 도구일지도 모릅니다.
물론 find 명령어를 사용할 수도 있습니다:
find . -maxdepth 1 -type f -print
하지만 이 방식은 매번 입력하기 번거롭고, 출력 결과도 직관적이지 않으며, ls 명령어가 제공하는 편리한 기능들을 활용할 수 없다는 단점이 있습니다. ls와 grep을 조합해 비슷한 결과를 얻는 방법도 있습니다:
ls -F . | grep -v /
그럼에도 불구하고 이 역시 번거로운 방식입니다. 이 스크립트는 이런 불편함을 해결해 주는 간단한 대안을 제공합니다.
사용법
이 스크립트는 호출하는 이름에 따라 네 가지 핵심 기능을 제공합니다. lsf는 파일 목록을, lsd는 디렉터리 목록을, lsx는 실행 파일 목록을, lsl은 심볼릭 링크 목록을 보여줍니다.
스크립트를 용도별로 여러 벌 설치할 필요는 없습니다. 심볼릭 링크를 활용하기 때문에 저장 공간을 절약할 수 있고, 스크립트 업데이트 관리도 한결 수월해집니다.
동작 원리는 의외로 단순합니다. 먼저 find 명령어로 항목을 검색한 뒤, 찾아낸 각 항목에 대해 ls 명령을 실행해 결과를 출력합니다. 여기서 장점은 스크립트에 전달한 인자들이 그대로 ls 명령에 넘겨진다는 점입니다. 예를 들어 다음 명령은 점(.)으로 시작하는 숨김 파일까지 포함해 모든 파일을 표시합니다:
lsf -a
디렉터리를 상세 정보와 함께 나열하려면 lsd 명령을 사용하세요:
lsd -l
여러 개의 인자를 함께 지정할 수도 있고, 파일이나 디렉터리 경로도 자유롭게 넘길 수 있습니다.
다음 명령은 현재 디렉터리의 상위 디렉터리(..)와 /usr/bin 디렉터리에 있는 모든 파일을 파일 유형 표시(-F)와 상세 목록(-l) 형태로 한꺼번에 보여줍니다:
lsf -F -l .. /usr/bin
다만 한 가지 아쉬운 점은 아직 재귀(recursion) 탐색을 지원하지 않는다는 것입니다. 아래 명령은 하위 디렉터리까지 내려가지 않고 현재 디렉터리의 파일만 나열합니다.
lsf -R
재귀 탐색 기능은 향후 버전에서 개선될 가능성이 있습니다.
내부 구조 살펴보기
스크립트는 탑다운(top-down) 방식으로 작성되어 있습니다. 초기화 관련 함수들이 스크립트 앞부분에 배치되어 있고, 실제 핵심 작업은 마지막 부분에서 수행됩니다. 실질적으로 중요한 역할을 하는 함수는 두 개뿐입니다.
첫 번째 parse_args() 함수는 명령행 인수를 분석해 옵션과 경로명을 분리하고, ls 명령의 일반 옵션과 이 스크립트 고유의 옵션을 구분해 줍니다.
두 번째 list_things_in_dir() 함수는 디렉터리 이름을 인자로 받아 해당 디렉터리에서 find 명령을 실행합니다. 그리고 발견된 항목 하나하나를 ls 명령으로 넘겨 화면에 표시합니다.
마치며
간단한 기능을 위해 만들어진 간단한 스크립트지만, 그만큼 활용 가치가 높습니다. 반복적인 명령 조합을 줄여 시간을 절약해 주며, 특히 방대한 규모의 파일 시스템을 다룰 때 의외로 큰 도움이 됩니다.
스크립트 전문
#!/bin/bash
# Script to list:
# directories (if called "lsd")
# files (if called "lsf")
# links (if called "lsl")
# or executables (if called "lsx")
# but not any other type of filesystem object.
# FIXME: add lsp (list pipes)
#
# Usage:
# <command_name> [switches valid for ls command] [dirname...]
#
# Works with names that includes spaces and that start with a hyphen.
#
# Created by Nick Clifton.
# Version 1.4
# Copyright (c) 2006, 2007 Red Hat.
#
# This is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published
# by the Free Software Foundation; either version 3, or (at your
# option) any later version.
# It is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# ToDo:
# Handle recursion, eg: lsl -R
# Handle switches that take arguments, eg --block-size
# Handle --almost-all, --ignore-backups, --format and --ignore
main ()
{
init
parse_args ${1+"$@"}
list_objects
exit 0
}
report ()
{
echo $prog": " ${1+"$@"}
}
fail ()
{
report " Internal error: " ${1+"$@"}
exit 1
}
# Initialise global variables.
init ()
{
# Default to listing things in the current directory.
dirs[0]=".";
# num_dirs is the number of directories to be listed minus one.
# This is because we are indexing the dirs[] array from zero.
num_dirs=0;
# Default to ignoring things that start with a period.
no_dots=1
# Note - the global variables 'type' and 'opts' are initialised in
# parse_args function.
}
# Parse our command line
parse_args ()
{
local no_more_args
no_more_args=0 ;
prog=`basename $0` ;
# Decide if we are listing files or directories.
case $prog in
lsf | lsf.sh)
type=f
opts="";
;;
lsd | lsd.sh)
type=d
# The -d switch to "ls" is presumed when listing directories.
opts="-d";
;;
lsl | lsl.sh)
type=l
# Use -d to prevent the listed links from being followed.
opts="-d";
;;
lsx | lsx.sh)
type=f
find_extras="-perm /111"
;;
*)
fail "Unrecognised program name: '$prog', expected either 'lsd', 'lsf', 'lsl' or 'lsx'"
;;
esac
# Locate any additional command line switches for ls and accumulate them.
# Likewise accumulate non-switches to the directories list.
while [ $# -gt 0 ]
do
case "$1" in
# FIXME: Handle switches that take arguments, eg --block-size
# FIXME: Properly handle --almost-all, --ignore-backups, --format
# FIXME: and --ignore
# FIXME: Properly handle --recursive
-a | -A | --all | --almost-all)
no_dots=0;
;;
--version)
report "version 1.2"
exit 0
;;
--help)
case $type in
d) report "a version of 'ls' that lists only directories" ;;
l) report "a version of 'ls' that lists only links" ;;
f) if [ "x$find_extras" = "x" ] ; then
report "a version of 'ls' that lists only files" ;
else
report "a version of 'ls' that lists only executables";
fi ;;
esac
exit 0
;;
--)
# A switch to say that all further items on the command line are
# arguments and not switches.
no_more_args=1 ;
;;
-*)
if [ "x$no_more_args" = "x1" ] ;
then
dirs[$num_dirs]="$1";
let "num_dirs++"
else
# Check for a switch that just uses a single dash, not a double
# dash. This could actually be multiple switches combined into
# one word, eg "lsd -alF". In this case, scan for the -a switch.
# XXX: FIXME: The use of =~ requires bash v3.0+.
if [[ "x${1:1:1}" != "x-" && "x$1" =~ "x-.*a.*" ]] ;
then
no_dots=0;
fi
opts="$opts $1";
fi
;;
*)
dirs[$num_dirs]="$1";
let "num_dirs++"
;;
esac
shift
done
# Remember that we are counting from zero not one.
if [ $num_dirs -gt 0 ] ;
then
let "num_dirs--"
fi
}
list_things_in_dir ()
{
local dir
# Paranoia checks - the user should never encounter these.
if test "x$1" = "x" ;
then
fail "list_things_in_dir called without an argument"
fi
if test "x$2" != "x" ;
then
fail "list_things_in_dir called with too many arguments"
fi
# Use quotes when accessing $dir in order to preserve
# any spaces that might be in the directory name.
dir="${dirs[$1]}";
# Catch directory names that start with a dash - they
# confuse pushd.
if test "x${dir:0:1}" = "x-" ;
then
dir="./$dir"
fi
if [ -d "$dir" ]
then
if [ $num_dirs -gt 0 ]
then
echo " $dir:"
fi
# Use pushd rather passing the directory name to find so that the
# names that find passes on to xargs do not have any paths prepended.
pushd "$dir" > /dev/null
if [ $no_dots -ne 0 ] ; then
find . -maxdepth 1 -type $type $find_extras -not -name ".*" -printf "%f\000" \
| xargs --null --no-run-if-empty ls $opts -- ;
else
find . -maxdepth 1 -type $type $find_extras -printf "%f\000" \
| xargs --null --no-run-if-empty ls $opts -- ;
fi
popd > /dev/null
else
report "directory '$dir' could not be found"
fi
}
list_objects ()
{
local i
i=0;
while [ $i -le $num_dirs ]
do
list_things_in_dir i
let "i++"
done
}
# Invoke main
main ${1+"$@"}