이제 GNU ls의 출력을 구문 분석하는 것이 안전합니까?

이제 GNU ls의 출력을 구문 분석하는 것이 안전합니까?

지난 수십 년 동안 받아들여진 견해는 구문 분석 ls([1],[2]). 예를 들어, 파일의 수정 날짜와 이름을 쉘 변수에 저장하려는 경우 이는 올바른 방법이 아닙니다.

$ ls -l file
-rw-r--r-- 1 terdon terdon 0 Aug 15 19:16 file
$ foo=$(ls -l file | awk '{print $9,$6,$7,$8}')
$ echo "$foo"
file Aug 15 19:16

이 방법은 파일 이름이 약간 다를 때마다 실패합니다.

$ ls -l file*
-rw-r--r-- 1 terdon terdon 0 Aug 15 19:16 'file with spaces'
$ foo=$(ls -l file* | awk '{print $9,$6,$7,$8}')
$ echo "$foo"
file Aug 15 19:16

파일의 수정 날짜가 오늘과 가깝지 않으면 시간 형식이 변경될 수 있으므로 상황은 더욱 악화됩니다.

$ ls -l
total 0
-rw-r--r-- 1 terdon terdon 0 Aug 15 19:21  file
-rw-r--r-- 1 terdon terdon 0 Aug 15  2018 'file with spaces'

그러나 최신 버전의 GNU coreutils에는 ls특정 시간 형식을 설정하고 NULL로 구분된 출력을 생성하기 위해 결합할 수 있는 두 가지 옵션이 있습니다.

      --time-style=TIME_STYLE
              time/date format with -l; see TIME_STYLE below
[...]
     --zero end each output line with NUL, not newline
[...]
       The TIME_STYLE argument can be full-iso,  long-iso,  iso,  locale,  or
       +FORMAT.   FORMAT  is  interpreted like in date(1).  If FORMAT is FOR‐
       MAT1<newline>FORMAT2, then FORMAT1 applies  to  non-recent  files  and
       FORMAT2  to recent files.  TIME_STYLE prefixed with 'posix-' takes ef‐
       fect only outside the POSIX locale.  Also the  TIME_STYLE  environment
       variable sets the default style to use.

다음은 이러한 옵션이 설정된 파일입니다(가독성을 약간 향상시키기 위해 각 출력 줄 끝에 있는 0이 줄 바꿈으로 대체됨 #).

$ ls -l --zero --time-style=long-iso -- *
-rw-r--r--+ 1 terdon terdon 0 2023-08-16 21:35 a file with a
newline#
-rw-r--r--+ 1 terdon terdon 0 2023-08-15 19:16 file#
-rw-r--r--+ 1 terdon terdon 0 2018-08-15 12:00 file with spaces#

ls이러한 옵션을 사용하면 전통적으로 해로웠던 많은 작업을 수행할 수 있습니다. 예를 들어:

  1. 가장 최근에 수정된 파일 이름을 변수에 입력합니다.

    $ touch 'a file with a'$'\n''newline'
    $ last=$(ls -tr --zero | tail -z -n1)
    bash: warning: command substitution: ignored null byte in input
    $ printf -- 'LAST: "%s"\n' "$last"
    LAST: "a file with a 
    newline"
    
  2. 이 질문을 제기하는 예입니다. Ask Ubuntu의 또 다른 질문은 OP가 파일 이름과 수정 날짜를 인쇄하려고 합니다. 누군가 게시했습니다답변and 를 사용하는 것은 다음에 추가하면 매우 강력해 보이는 ls영리한 트릭입니다 .awk--zerols

    $ output=$(ls -l --zero --time-style=long-iso -- * | 
               awk 'BEGIN{RS="\0"}{ t=index($0,$7); print substr($0,t+6), $6 }')
    $ printf 'Output: "%s"\n' "$output"
    Output: "a file with a
    newline 2023-08-16"
    

두 가지 예를 모두 깨뜨리는 이름을 찾을 수 없습니다. 그래서 내 질문은 다음과 같습니다.

  1. 위의 두 가지 예 중 하나가 실패하는 상황이 있습니까? 뭔가 이상한 게 있는 게 아닐까?
  2. ls그렇지 않다면 최신 버전의 GNU가 실제로 임의의 파일 이름을 사용해도 안전하다는 의미입니까 ?

답변1

이제 GNU ls의 출력을 구문 분석하는 것이 안전합니까? (그리고 --zero)

--zero많은 도움이 되지만 여기서 사용된 방식은 여전히 ​​안전하지 않습니다. 출력 형식 ls자체와 질문의 출력을 구문 분석하는 데 사용되는 명령 모두에 문제가 있습니다.--zero실제로 언급된ParsingLs 위키 페이지에 있지만 예제에서는 긴 형식을 사용하지 않습니다(아마도 여기서 문제 때문일 것입니다!). 이 답변에 있는 많은 질문은 Stéphane Chazelas가 댓글에서 질문한 것입니다.


우선, 공백이 ls -l포함된 사용자/그룹 이름을 있는 그대로 인쇄하고 열 수를 엉망으로 만들기 때문에 문제가 됩니다( --zero여기서는 중요하지 않음).

$ ls -l --time-style=long-iso foo.txt
-rw-rw-r-- 1 foo bar users 0 2023-08-16 16:45 foo.txt

최소한 UID 및 GID를 숫자로 인쇄하거나 완전히 무시하는 --numeric-uid-gid/ 가 필요합니다 . 둘 다 다른 긴 형식 필드도 포함합니다.-n-go

ls인수에 나타나는 모든 디렉토리의 내용도 나열되므로 이를 원할 수도 있습니다 -d.

다른 열에는 공백이나 NUL이 포함될 수 없다고 생각합니다.

ls -dgo --time-style=long-iso --zero -- *

아마 안전할 거예요. 아마도.

여러 파일이 있는 경우 하나를 필드 구분 기호로 사용하는 대신 열을 공백으로 채우므로 cut예를 들어 출력에서 ​​사용할 수 없기 때문에 구문 분석하기가 여전히 가장 쉬운 것은 아닙니다. 이는 --zeroUID 및 GID를 사용하거나 생략하여 파이프 로 출력하는 경우에도 발생합니다. 파일 크기와 링크 수가 너비에 따라 다를 수 있기 때문입니다.

$ ls -dgo --zero --time-style=long-iso -- *.txt |tr '\0' '\n'
-rw-rw-r-- 21    0 2023-08-16 17:24 bar.txt
-rw-rw-r--  1 1234 2023-08-16 17:30  leading space.txt

파일 이름은 오른쪽에 추가되지 않으므로(이상할 수 있음) 타임스탬프와 파일 이름 사이에 공백만 있다고 가정하는 것이 안전합니다.

--time-style=long-isoUTC 오프셋은 포함되지 않으므로 날짜가 모호할 수 있습니다. 최악의 경우 일광 절약 시간이 끝날 때 생성된 두 개의 파일이 날짜를 잘못된 순서로 표시할 수 있습니다. ( ls요청하면 여전히 올바르게 정렬되지만 출력은 혼란스러울 것입니다.) 이 점에서는 --full-time/ --time-style=full-iso(또는 사용자 정의 형식)이 더 좋을 것이며 명시적으로 설정하면 TZ=UTC0날짜를 문자열로 비교하기가 더 쉬워집니다.

$ TZ=Europe/Helsinki ls -dgo --time-style=long-iso -- *
-rw-rw-r-- 1 0 2023-10-29 03:30 first
-rw-rw-r-- 1 0 2023-10-29 03:20 second

$ TZ=UTC0 ls -dgo --full-time -- *
-rw-rw-r-- 1 0 2023-10-29 00:30:00.000000000 +0000 first
-rw-rw-r-- 1 0 2023-10-29 01:20:00.000000000 +0000 second

$ TZ=UTC0 ls -dgo --time-style=+%FT%T.%NZ -- *
-rw-rw-r-- 1 0 2023-10-29T00:30:00.000000000Z first
-rw-rw-r-- 1 0 2023-10-29T01:20:00.000000000Z second

일반 파일 외에 다른 것이 있으면 상황은 더욱 악화됩니다. 많은 경우에는 문제가 되지 않을 수도 있지만 어쨌든 다음과 같습니다.

장치 파일의 경우 ls크기는 인쇄되지 않지만 주/부 장치 번호는 인쇄됩니다. 다른 파일과 열 개수를 다르게 하려면 쉼표와 공백으로 구분하세요. 쉼표를 사용하여 두 변형을 구별할 수 있지만 이로 인해 구문 분석이 더 어려워집니다.

$ ls -dgo --zero --time-style=long-iso -- /dev/null somefile.txt |tr '\0' '\n'
crw-rw-rw- 1  1, 3 2023-07-16 15:37 /dev/null
-rw-rw-r-- 1 12345 2023-08-17 06:14 somefile.txt

그런 다음 긴 형식으로 인쇄되는 심볼릭 링크가 있지만 link name -> link target링크나 대상 이름 자체에 무엇을 포함할 수 있는지에 대해서는 말할 것도 없습니다 ->.

$ ls -dgo --zero --time-style=long-iso -- how* what* |tr '\0' '\n'
lrwxrwxrwx 1 14 2023-08-17 06:05 how -> about -> this?
lrwxrwxrwx 1  5 2023-08-17 05:54 what -> is -> this?

글쎄, 기술적으로 크기 필드는 링크 이름의 길이(문자가 아닌 바이트 단위)를 알려주는 것 같습니다.

이 경우 --quoting-style=shell-escape-always실제로 는 다음보다 낫습니다 --zero.$''

$ ls -dgo --quoting-style=shell-escape-always --time-style=long-iso -- how* what*  |cat
lrwxrwxrwx 1 14 2023-08-17 06:05 'how' -> 'about -> this?'
lrwxrwxrwx 1  5 2023-08-17 05:54 'what -> is' -> 'this?'

쉘을 사용하더라도 파싱하는 것은 별로 재미가 없습니다.


원하는 필드를 명시적으로 선택할 수 있으면 더 좋겠지만 그런 옵션이 보이지 않습니다 ls. GNU find에는 -printf안전한 출력을 생성하는 기능이 있습니다. 시간별로 정렬하려면 ls타임스탬프를 인쇄할 필요 없이 //만 ls --zero사용하면 됩니다 -t. 아래를 참조하세요. (zsh 자체는 이것을 할 수 있지만 Bash는 그다지 좋지 않습니다.)-u-c

타임스탬프와 파일 이름을 원하면 비슷한 작업을 find ./* -printf '%TY-%Tm-%Td %TT %p\0'수행해야 하지만 기본적으로 하위 디렉터리로 반복되므로 원하지 않는 경우 조치를 취해야 합니다. 어쩌면 -prune끝에 추가할 수도 있습니다 . 둘 중 하나도 --도움이 되지 않으므로 접두사가 find필요합니다 ./.

어쩌면 stat --printf더 쉬울 수도 있습니다.


위의 두 가지 예 중 하나가 실패하는 상황이 있습니까? 뭔가 이상한 게 있는 게 아닐까?

질문에 사용된 명령은 last=$(ls -tr --zero | tail -z -n1)명령 대체가 최종 NL을 무시한 후 후행 줄 바꿈을 제거하기 때문에 본질적으로 Bash에서 안전하지 않습니다. 그리고에드 모튼이 지적했다.ls, 출력이 아무리 안전하더라도 적어도 특정 AWK 명령이 손상되었습니다 .

내 생각에 AWK는 마지막 필드 자체에 필드 구분 기호가 포함될 수 있는 고정된 수의 필드가 있는 입력에는 적합하지 않다고 생각합니다. Perl split()에는 생성할 필드 수를 제한하는 추가 매개변수가 있지만 일부(전부는 아님) 필드 구분 기호가 여러 공백일 수 있는 경우 사용하기가 쉽지 않습니다. 순진한 사람들은 split/ +/, $_, 6파일 이름의 선행 공백을 먹습니다. 이 문제와 장치 노드 문제를 처리하기 위해 정규 표현식을 작성할 수 있지만 이는 둥근 못을 사각형 구멍에 밀어넣는 것처럼 시작되며 심볼릭 링크 출력 문제를 해결하지 못합니다.


긴 형식의 출력이 없는 경우 ls --zeroNUL로 끝나는 원시 파일 이름만 제공되어야 출력이 안전하고 구문 분석하기 쉬워야 합니다.

가장 오래된 파일 의 경우 $n위키 페이지에는 다음이 있습니다.

readarray -t -d '' -n 5 sorted < <(ls --zero -tr)
# check the number of elements you got

read -rd ''단 하나의 경우에는 댓글에서 언급한 대로 would do를 사용할 수 있습니다 .

IFS= read -rd '' newest < <(ls -t --zero)
# check the exit status or make sure "$newest" is not empty

답변2

GNU의 출력에만 의존 하고 있다면 ls이는 GNU Coreutils 패키지에 의존하고 있다는 의미입니다. 이는 stat원하는 방식으로 개체에 대한 정보를 가져오기 위한 형식 문자열이 있는 다른 Coreutils 유틸리티, 즉 .Stat를 사용할 수 있음을 의미합니다 .

예를 들어 현재 디렉터리의 수정 시간을 다음 형식으로 인쇄합니다 MMM DD HH:MM.

$ echo $(date -d @$(stat --format="%Y" .) +"%b %m %H:%M")
Aug 08 07:57

이 명령은 객체의 수정 시간을 10진수 정수로 stat --format=%Y .가져옵니다 .. 이는 에포크 이후 친숙한 초 수를 나타냅니다.

접두사를 인수 (GNU Coreutils의 기능 ) @로 사용하여 보간한 다음 코드를 사용하여 필요한 형식으로 시간을 가져옵니다.-ddatedatestrftime

불행히도 날짜 형식을 지정하는 기본 제공 방법 은 stat없습니다 . strftime여러 번의 호출 없이 수정 시간을 포함한 여러 필드의 정보를 얻으려면 stat다중 필드 라인을 인쇄한 다음 해당 라인을 구문 분석해야 합니다. 이는 긁힌 출력보다 여전히 더 나은 측정값입니다 ls. 최대 효율성이 중요하지 않다면(만약 그렇다면 왜 Bash로 코딩하겠습니까?) 여러 호출로 인해 어려움을 겪을 수 있습니다 stat.

stat수정 시간이 가장 오래된 파일을 검색하는 데 사용할 수 없다는 설명이 주석에 작성되었습니다 . stat단독으로는 할 수 없는 것이 사실이지만 stat실제로는 ls -1t.

$ for x in *.txt ; do stat --format="%Y %n" "$x" ; done | sort -n | head -1
1328379315 readme-mt.txt

이 문서는 꽤 오래 전으로 거슬러 올라갑니다.

$ date -d @1328379315
Sat Feb  4 10:15:15 PST 2012

이제 우리가 가진 문제는 이름에 개행 문자가 포함되어 있으면 정렬이 엉망이 된다는 것입니다. 우리는 그것을 사용할 수 있습니다 ls.

예를 들어, 이름을 Bash 배열로 읽은 다음 이름 대신 배열 인덱스와 함께 타임스탬프를 인쇄할 수 있습니다. 출력에서 sort -n | head -1우리는 두 번째 필드가 가장 최근에 수정된 파일 이름의 배열 인덱스를 제공하는 항목을 얻습니다.

ls우리 는 어떻게든 구문 분석해야 하는 인코딩된 공백과 개행 문자로 출력을 처리하는 문제를 완전히 피할 수 있습니다 .

$ array=(*.txt)
$ for x in ${!array[@]}; do 
>   printf "%s %s\n" $(stat --format="%Y" "${array[$x]}") $x 
> done | sort -n | head -1
1328379315 29
$ echo "${array[29]}"
readme-mt.txt

array[29]*.txt이름이 어떤 문자로 구성되어 있는지에 관계없이 발견된 30번째 파일이 저장됩니다 . 우리 sort작업은 이름을 볼 수 없기 때문에 이로 인해 영향을 받지 않습니다.

따라서 질문에 답하기 위해 GNU ls에는 출력을 보다 안전하게 구문 분석할 수 있는 몇 가지 기능이 있지만, 쉘 언어에서는 출력을 안전하게 구문 분석하는 것이 여전히 쉽지 않습니다.

popen("ls ...", "r")GNU ls는 올바른 옵션 과 올바른 구문 분석 논리를 사용하는 C 프로그램에서 안전하게 사용할 수 있습니다 ls.

"크롤링 안 함" 규칙 의 출력은 ls스크립팅 컨텍스트에 있습니다.

답변3

질문의 마지막 예제에 대한 코드를 보면 다음과 같습니다.

ls -l --zero --time-style=long-iso -- * | 
    awk 'BEGIN{RS="\0"}{ t=index($0,$7); print substr($0,t+6), $6 }'

ls명령의 샘플 출력을 게시했습니다 ( #<newline>더 나은 가시성을 위해 NUL 대체).

$ ls -l --zero --time-style=long-iso -- *
-rw-r--r--+ 1 terdon terdon 0 2023-08-16 21:35 a file with a
newline#
-rw-r--r--+ 1 terdon terdon 0 2023-08-15 19:16 file#
-rw-r--r--+ 1 terdon terdon 0 2018-08-15 12:00 file with spaces#

$7타임스탬프처럼 보여야 합니다 . 그렇다면 t=index($0,$7)1단어보다 긴 사용자 이름/그룹에 대해서는 실패합니다. 예:

-rw-r--r--+ 1 terdon Domain Users 0 2023-08-15 19:16 file#

그 시점부터 타임스탬프는 $8대신 (또는 사용자 이름 및/또는 그룹에 포함된 단어 수에 따라 더 높은 숫자)이 됩니다 $7.

사용자 이름/그룹을 포함할 수 없는 경우 특정 필드를 찾는 대신 행의 첫 번째 항목 :만 찾아 문제를 해결할 수 있습니다 .:

ls -l --zero --time-style=long-iso -- * | 
    awk -v RS='\0' 'p=index($0,":") { print substr($0,p+4), substr($0,p-13,10) }'

또는 GNU awk(아마도 사용하고 있음 RS='\0')를 사용하여 세 번째 인수를 다음과 같이 설정합니다 match().

ls -l --zero --time-style=long-iso -- * | 
    awk -v RS='\0' 'match($0,/(.{10}) ..:.. (.*)/,a) { print a[2], a[1] }'

관련 정보