Computer >> 컴퓨터 >  >> 프로그래밍 >> Bash 프로그래밍

대용량 파일 시스템 작업에 유용한 Bash 스크립트 활용법

디렉터리 안의 파일만 깔끔하게 목록으로 보고 싶은 적이 있으신가요? 아니면 반대로 디렉터리만 보고 싶으셨나요? 그렇다면 아래 소개하는 GPLv3 라이선스의 오픈 소스 스크립트가 바로 여러분이 찾던 도구일지도 모릅니다.

물론 find 명령어를 사용할 수도 있습니다:

find . -maxdepth 1 -type f -print

하지만 이 방식은 매번 입력하기 번거롭고, 출력 결과도 직관적이지 않으며, ls 명령어가 제공하는 편리한 기능들을 활용할 수 없다는 단점이 있습니다. lsgrep을 조합해 비슷한 결과를 얻는 방법도 있습니다:

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+"$@"}