지난 수십 년 동안 받아들여진 견해는 구문 분석 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
이러한 옵션을 사용하면 전통적으로 해로웠던 많은 작업을 수행할 수 있습니다. 예를 들어:
가장 최근에 수정된 파일 이름을 변수에 입력합니다.
$ 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"
이 질문을 제기하는 예입니다. Ask Ubuntu의 또 다른 질문은 OP가 파일 이름과 수정 날짜를 인쇄하려고 합니다. 누군가 게시했습니다답변and 를 사용하는 것은 다음에 추가하면 매우 강력해 보이는
ls
영리한 트릭입니다 .awk
--zero
ls
$ 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"
두 가지 예를 모두 깨뜨리는 이름을 찾을 수 없습니다. 그래서 내 질문은 다음과 같습니다.
- 위의 두 가지 예 중 하나가 실패하는 상황이 있습니까? 뭔가 이상한 게 있는 게 아닐까?
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
예를 들어 출력에서 사용할 수 없기 때문에 구문 분석하기가 여전히 가장 쉬운 것은 아닙니다. 이는 --zero
UID 및 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-iso
UTC 오프셋은 포함되지 않으므로 날짜가 모호할 수 있습니다. 최악의 경우 일광 절약 시간이 끝날 때 생성된 두 개의 파일이 날짜를 잘못된 순서로 표시할 수 있습니다. ( 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 --zero
NUL로 끝나는 원시 파일 이름만 제공되어야 출력이 안전하고 구문 분석하기 쉬워야 합니다.
가장 오래된 파일 의 경우 $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의 기능 ) @
로 사용하여 보간한 다음 코드를 사용하여 필요한 형식으로 시간을 가져옵니다.-d
date
date
strftime
불행히도 날짜 형식을 지정하는 기본 제공 방법 은 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] }'