«Я все равно собираюсь повторить, почему бы не использовать ls?»

«Я все равно собираюсь повторить, почему бы не использовать ls?»

Я постоянно вижу ответы, цитирующиеэта ссылказаявляя определенно«Не разбирай lsМеня это беспокоит по нескольким причинам:

  1. Похоже, информация по этой ссылке была принята без каких-либо вопросов, хотя при беглом прочтении я заметил по крайней мере несколько ошибок.

  2. Также создается впечатление, что проблемы, указанные в этой ссылке, не вызвали желания найти решение.

Из первого абзаца:

...когда вы запрашиваете [ls]список файлов, возникает огромная проблема: Unix допускает почти любой символ в имени файла, включая пробелы, символы новой строки, запятые, символы вертикальной черты и практически все, что вы когда-либо попытаетесь использовать в качестве разделителя, кроме NUL. ... lsразделяет имена файлов символами новой строки. Это нормально, пока у вас нет файла с символом новой строки в имени. И поскольку я не знаю ни одной реализации, которая lsпозволяла бы вам завершать имена файлов символами NUL вместо символов новой строки, это не позволяет нам безопасно получить список имен файлов с помощью ls.

Облом, да? Каквсегдаможем ли мы обрабатывать перечисленные наборы данных, завершающиеся новой строкой, для данных, которые могут содержать новые строки? Ну, если бы люди, отвечающие на вопросы на этом сайте, не делали этого ежедневно, я бы подумал, что у нас были проблемы.

Правда в том, что большинство lsреализаций на самом деле предоставляют очень простой API для разбора их вывода, и мы все это делали, даже не осознавая этого. Вы можете не только закончить имя файла null, вы можете также начать его с null или с любой другой произвольной строки, которую захотите. Более того, вы можете назначить эти произвольные строкипо типу файла. Пожалуйста примите к сведению:

LS_COLORS='lc=\0:rc=:ec=\0\0\0:fi=:di=:' ls -l --color=always | cat -A
total 4$
drwxr-xr-x 1 mikeserv mikeserv 0 Jul 10 01:05 ^@^@^@^@dir^@^@^@/$
-rw-r--r-- 1 mikeserv mikeserv 4 Jul 10 02:18 ^@file1^@^@^@$
-rw-r--r-- 1 mikeserv mikeserv 0 Jul 10 01:08 ^@file2^@^@^@$
-rw-r--r-- 1 mikeserv mikeserv 0 Jul 10 02:27 ^@new$
line$
file^@^@^@$
^@

Видетьэтотдля большего.

А вот следующая часть статьи меня действительно зацепила:

$ ls -l
total 8
-rw-r-----  1 lhunath  lhunath  19 Mar 27 10:47 a
-rw-r-----  1 lhunath  lhunath   0 Mar 27 10:47 a?newline
-rw-r-----  1 lhunath  lhunath   0 Mar 27 10:47 a space

Проблема в том, что из вывода lsни вы, ни компьютер не можете определить, какие части составляют имя файла. Каждое слово? Нет. Каждая строка? Нет. На этот вопрос нет правильного ответа, кроме: вы не можете определить.

Также обратите внимание, как lsиногда искажаются данные имени файла (в нашем случае он превратил \nсимвол между словами"а"и "новая линия"в?вопросительный знак...

...

Если вы просто хотите перебрать все файлы в текущем каталоге, используйте forцикл и глоб:

for f in *; do
    [[ -e $f ]] || continue
    ...
done

Автор называет этоискажение имен файловкогда lsвозвращает список имен файлов, содержащих шаблоны оболочкиа потомрекомендует использовать shell-глобал для получения списка файлов!

Примите во внимание следующее:

printf 'touch ./"%b"\n' "file\nname" "f i l e n a m e" |
    . /dev/stdin
ls -1q

f i l e n a m e  
file?name

IFS="
" ; printf "'%s'\n" $(ls -1q)

'f i l e n a m e'
'file
name'

POSIX определяетоперанды -1и -q lsтак:

-q- Заставить каждый экземпляр непечатаемых символов имени файла и <tab>s быть записанным как вопросительный знак ( '?'). Реализации могут предоставлять эту опцию по умолчанию, если вывод осуществляется на терминальное устройство.

-1-(Цифра один.)Принудительно выводить по одной записи на строку.

Глоббинг не лишен своих проблем - ?совпадениялюбойсимвол, поэтому несколько ?результатов совпадения в списке будут соответствовать одному и тому же файлу несколько раз. Это легко обрабатывается.

Хотя, как это сделать, это не суть важно - в конце концов, это не так уж и много и показано ниже - мне было интереснопочему нет. Как я считаю, лучший ответ на этот вопрос был принят. Я бы посоветовал вам попытаться чаще сосредотачиваться на том, чтобы говорить людям то, что ониможетделать, чем на то, что онине мочь.По крайней мере, я думаю, гораздо меньше вероятность того, что вы ошибетесь.

Но зачем вообще пытаться? По общему признанию, моей главной мотивацией было то, что другие постоянно говорили мне, что я не смогу. Я прекрасно знаю, что lsвывод настолько регулярен и предсказуем, насколько вы можете этого желать, пока вы знаете, что искать. Дезинформация беспокоит меня больше, чем большинство вещей.

Правда, однако, за заметным исключением ответов Патрика и Вампуса Кью. Уомбли,(несмотря на потрясающий стиль последнего), я считаю большую часть информации в ответах здесь в основном правильной - shell glob и проще в использовании, и в целом более эффективен, когда дело доходит до поиска в текущем каталоге, чем parsing ls. Однако, по крайней мере, с моей точки зрения, они не являются достаточной причиной, чтобы оправдать распространение дезинформации, цитируемой в статье выше, и не являются приемлемым оправданием для "никогда не анализировать ls."

Обратите внимание, что противоречивые результаты ответа Патрика в основном являются результатом того, что он использовал zshthen bash. zsh- по умолчанию - не $(подставляет результаты команды word-split )в переносимом виде. Поэтому, когда он спрашиваетКуда делись остальные файлы?ответ на этот вопрос:Твоя раковина их съела.Вот почему вам нужно задать SH_WORD_SPLITпеременную при использовании zshи работе с переносимым кодом оболочки. Я считаю, что его неспособность отметить это в своем ответе является ужасно вводящим в заблуждение.

Ответ Вампуса для меня не подходит - в контексте списка ?символявляетсякомок ракушки. Я не знаю, как еще это сказать.

Чтобы обработать случай с несколькими результатами, вам нужно ограничить жадность глобуса. Следующий код просто создаст тестовую базу ужасных имен файлов и отобразит ее для вас:

{ printf %b $(printf \\%04o `seq 0 127`) |
sed "/[^[-b]*/s///g
        s/\(.\)\(.\)/touch '?\v\2' '\1\t\2' '\1\n\2'\n/g" |
. /dev/stdin

echo '`ls` ?QUOTED `-m` COMMA,SEP'
ls -qm
echo ; echo 'NOW LITERAL - COMMA,SEP'
ls -m | cat
( set -- * ; printf "\nFILE COUNT: %s\n" $# )
}

ВЫХОД

`ls` ?QUOTED `-m` COMMA,SEP
??\, ??^, ??`, ??b, [?\, [?\, ]?^, ]?^, _?`, _?`, a?b, a?b

NOW LITERAL - COMMA,SEP
?
 \, ?
     ^, ?
         `, ?
             b, [       \, [
\, ]    ^, ]
^, _    `, _
`, a    b, a
b

FILE COUNT: 12

Теперь я сохраню каждый символ, который не является /slash, -dash, :colonили буквенно-цифровым символом в глобе оболочки, а затем sort -uв списке для уникальных результатов. Это безопасно, потому что lsуже сохранно для нас все непечатаемые символы. Смотрите:

for f in $(
        ls -1q |
        sed 's|[^-:/[:alnum:]]|[!-\\:[:alnum:]]|g' |
        sort -u | {
                echo 'PRE-GLOB:' >&2
                tee /dev/fd/2
                printf '\nPOST-GLOB:\n' >&2
        }
) ; do
        printf "FILE #$((i=i+1)): '%s'\n" "$f"
done

ВЫХОД:

PRE-GLOB:
[!-\:[:alnum:]][!-\:[:alnum:]][!-\:[:alnum:]]
[!-\:[:alnum:]][!-\:[:alnum:]]b
a[!-\:[:alnum:]]b

POST-GLOB:
FILE #1: '?
           \'
FILE #2: '?
           ^'
FILE #3: '?
           `'
FILE #4: '[     \'
FILE #5: '[
\'
FILE #6: ']     ^'
FILE #7: ']
^'
FILE #8: '_     `'
FILE #9: '_
`'
FILE #10: '?
            b'
FILE #11: 'a    b'
FILE #12: 'a
b'

Ниже я снова подхожу к проблеме, но использую другую методологию. Помните, что - помимо \0null - /символ ASCII является единственным байтом, запрещенным в имени пути. Я отложил здесь подстановки и вместо этого объединил указанный POSIX -dпараметр for lsи также указанную POSIX -exec $cmd {} +конструкцию for find. Поскольку findестественным образом будет выдаваться только один /в последовательности, следующее легко обеспечивает рекурсивный и надежно разделенный список файлов, включающий всю информацию dentry для каждой записи. Просто представьте, что вы могли бы сделать с чем-то вроде этого:

#v#note: to do this fully portably substitute an actual newline \#v#
#v#for 'n' for the first sed invocation#v#
cd ..
find ././ -exec ls -1ldin {} + |
sed -e '\| *\./\./|{s||\n.///|;i///' -e \} |
sed 'N;s|\(\n\)///|///\1|;$s|$|///|;P;D'

###OUTPUT

152398 drwxr-xr-x 1 1000 1000        72 Jun 24 14:49
.///testls///

152399 -rw-r--r-- 1 1000 1000         0 Jun 24 14:49
.///testls/?
            \///

152402 -rw-r--r-- 1 1000 1000         0 Jun 24 14:49
.///testls/?
            ^///

152405 -rw-r--r-- 1 1000 1000         0 Jun 24 14:49
.///testls/?
        `///
...

ls -iможет быть очень полезным, особенно когда уникальность результата находится под вопросом.

ls -1iq | 
sed '/ .*/s///;s/^/-inum /;$!s/$/ -o /' | 
tr -d '\n' | 
xargs find

Это просто самые портативные средства, которые я могу придумать. С GNU lsвы могли бы сделать:

ls --quoting-style=WORD

И последнее, вот гораздо более простой методразборlsкоторый я использую довольно часто, когда мне нужны номера инодов:

ls -1iq | grep -o '^ *[0-9]*'

Это просто возвращает номера инодов, что является еще одной удобной опцией, указанной в POSIX.

решение1

Я совсем не убежден в этом, но давайте предположим ради аргумента, что вымог, если вы готовы приложить достаточно усилий, lsнадежно проанализируйте вывод, даже столкнувшись с «противником» — кем-то, кто знает написанный вами код и намеренно выбирает имена файлов, предназначенные для его взлома.

Даже если бы вы могли это сделать,это все равно будет плохой идеей.

Bourne shell 1 — плохой язык. Его не следует использовать для чего-либо сложного, если только экстремальная переносимость не важнее любого другого фактора (например, autoconf).

Я утверждаю, что если вы столкнулись с проблемой, когда анализ выходных данных lsкажется путем наименьшего сопротивления для скрипта оболочки, это весомый признак того, что все, что вы делаете,слишком сложен, чтобы быть скриптом оболочкии вам следует переписать все это на Perl, Python, Julia или любом другом языкехорошийЯзыки сценариев, которые легко доступны. В качестве демонстрации, вот ваша последняя программа на Python:

import os, sys
for subdir, dirs, files in os.walk("."):
    for f in dirs + files:
      ino = os.lstat(os.path.join(subdir, f)).st_ino
      sys.stdout.write("%d %s %s\n" % (ino, subdir, f))

Никаких проблем с необычными символами в именах файлов не возникает —выходнеоднозначен так же, как неоднозначен вывод ls, но это не имело бы значения в «реальной» программе (в отличие от такой демонстрационной программы), которая использовала бы результат os.path.join(subdir, f)напрямую.

Не менее важно, и в резком контрасте с тем, что вы написали, это все еще будет иметь смысл через шесть месяцев, и это будет легко изменить, когда вам понадобится, чтобы сделать что-то немного другое. В качестве иллюстрации предположим, что вы обнаружили необходимость исключить dotfiles и резервные копии редактора, а также обрабатывать все в алфавитном порядке по базовому имени:

import os, sys
filelist = []
for subdir, dirs, files in os.walk("."):
    for f in dirs + files:
        if f[0] == '.' or f[-1] == '~': continue
        lstat = os.lstat(os.path.join(subdir, f))
        filelist.append((f, subdir, lstat.st_ino))

filelist.sort(key = lambda x: x[0])
for f, subdir, ino in filelist: 
   sys.stdout.write("%d %s %s\n" % (ino, subdir, f))

1 Да, расширенные версии оболочки Bourne доступны в настоящее время: bashи zshобе значительно лучше оригинала. Расширения GNU для основных "утилит оболочки" (find, grep и т. д.) также очень помогают. Но даже со всеми расширениями среда оболочки не улучшаетсядостаточноконкурировать с действительно хорошими языками сценариев, поэтому мой совет остается прежним: «не используйте оболочку для чего-то сложного», независимо от того, о какой оболочке вы говорите.

«Как бы выглядела хорошая интерактивная оболочка, которая также была бы хорошим языком сценариев?» — это актуальный исследовательский вопрос, поскольку существует внутреннее противоречие между удобствами, необходимыми для интерактивного CLI (например, возможность печатать cc -c -g -O2 -o foo.o foo.cвместо subprocess.run(["cc", "-c", "-g", "-O2", "-o", "foo.o", "foo.c"])), и ограничениями, необходимыми для предотвращения незначительных ошибок в сложных сценариях (например,нетинтерпретируя не заключенные в кавычки слова в случайных местах как строковые литералы). Если бы я попытался разработать что-то подобное, я бы, вероятно, начал с того, что поместил IPython, PowerShell и Lua в блендер, но я понятия не имею, как будет выглядеть результат.

решение2

На эту ссылку ссылаются очень часто, поскольку информация в ней абсолютно точная и существует уже очень давно.


lsзаменяет непечатаемые символы на символы glob да, но эти символы не находятся в фактическом имени файла. Почему это важно? 2 причины:

  1. Если вы передадите это имя файла в программу, то это имя файла на самом деле не существует. Ей пришлось бы расширить глоб, чтобы получить настоящее имя файла.
  2. Файловый шаблон может соответствовать нескольким файлам.

Например:

$ touch a$'\t'b
$ touch a$'\n'b
$ ls -1
a?b
a?b

Обратите внимание, что у нас есть 2 файла, которые выглядят совершенно одинаково. Как вы собираетесь их различать, если они оба представлены как a?b?


Автор называет это искажением имен файлов, когда ls возвращает список имен файлов, содержащих шаблоны оболочки, а затем рекомендует использовать шаблон оболочки для получения списка файлов!

Здесь есть разница. Когда вы получаете обратно глобус, как показано, этот глобус может соответствовать более чем одному файлу. Однако, когда вы перебираете результаты, соответствующие глобу, вы получаете обратно точный файл, а не глобус.

Например:

$ for file in *; do printf '%s' "$file" | xxd; done
0000000: 6109 62                                  a.b
0000000: 610a 62                                  a.b

Обратите внимание, что xxdвывод показывает, что $fileсодержал необработанные символы \tи \n, а не ?.

Если вы используете ls, то вместо этого получите следующее:

for file in $(ls -1q); do printf '%s' "$file" | xxd; done
0000000: 613f 62                                  a?b
0000000: 613f 62                                  a?b

«Я все равно собираюсь повторить, почему бы не использовать ls

Ваш пример, который вы привели, на самом деле не работает. Он выглядит так, как будто работает, но на самом деле нет.

Я имею в виду вот это:

 for f in $(ls -1q | tr " " "?") ; do [ -f "$f" ] && echo "./$f" ; done

Я создал каталог с кучей имен файлов:

$ for file in *; do printf '%s' "$file" | xxd; done
0000000: 6120 62                                  a b
0000000: 6120 2062                                a  b
0000000: 61e2 8082 62                             a...b
0000000: 61e2 8083 62                             a...b
0000000: 6109 62                                  a.b
0000000: 610a 62                                  a.b

Когда я запускаю ваш код, я получаю это:

$ for f in $(ls -1q | tr " " "?") ; do [ -f "$f" ] && echo "./$f" ; done
./a b
./a b

Куда делись остальные файлы?

Давайте попробуем это:

$ for f in $(ls -1q | tr " " "?") ; do stat --format='%n' "./$f"; done
stat: cannot stat ‘./a?b’: No such file or directory
stat: cannot stat ‘./a??b’: No such file or directory
./a b
./a b
stat: cannot stat ‘./a?b’: No such file or directory
stat: cannot stat ‘./a?b’: No such file or directory

Теперь давайте используем настоящий глоб:

$ for f in *; do stat --format='%n' "./$f"; done
./a b
./a  b
./a b
./a b
./a b
./a
b

С Башем

Вышеприведенный пример был с моей обычной оболочкой, zsh. Когда я повторяю процедуру с bash, я получаю совершенно другой набор результатов с вашим примером:

Тот же набор файлов:

$ for file in *; do printf '%s' "$file" | xxd; done
0000000: 6120 62                                  a b
0000000: 6120 2062                                a  b
0000000: 61e2 8082 62                             a...b
0000000: 61e2 8083 62                             a...b
0000000: 6109 62                                  a.b
0000000: 610a 62                                  a.b

Радикально отличающиеся результаты с вашим кодом:

for f in $(ls -1q | tr " " "?") ; do stat --format='%n' "./$f"; done
./a b
./a b
./a b
./a b
./a
b
./a  b
./a b
./a b
./a b
./a b
./a b
./a b
./a
b
./a b
./a b
./a b
./a b
./a
b

С шаром-ракушкой все работает отлично:

$ for f in *; do stat --format='%n' "./$f"; done
./a b
./a  b
./a b
./a b
./a b
./a
b

Причина, по которой bash ведет себя таким образом, восходит к одному из пунктов, которые я указал в начале ответа: «Файловый глобальный шаблон может соответствовать более чем одному файлу».

lsвозвращает один и тот же глоб ( a?b) для нескольких файлов, поэтому каждый раз, когда мы расширяем этот глоб, мы получаем каждый файл, который ему соответствует.


Как воссоздать список файлов, которые я использовал:

touch 'a b' 'a  b' a$'\xe2\x80\x82'b a$'\xe2\x80\x83'b a$'\t'b a$'\n'b

Шестнадцатеричные коды — это символы UTF-8 NBSP.

решение3

Вывод — ls -qэто вообще не глоб. Он используется ?для обозначения «Здесь есть символ, который не может быть отображен напрямую». Глобы используются ?для обозначения «Здесь разрешен любой символ».

У глобов есть и другие специальные символы ( *и []по крайней мере, а внутри []пары их больше). Ни один из них не экранируется ls -q.

$ touch x '[x]'
$ ls -1q
[x]
x

Если вы обрабатываете ls -1qвывод, есть набор глобусов и расширяете их, вы не только получите xдважды, вы пропустите [x]полностью. Как глобус, он не соответствует себе как строке.

ls -qпредназначен для защиты ваших глаз и/или терминала от безумных персонажей, а не для создания чего-то, что можно будет вернуть оболочке.

решение4

Ответ прост: особые случаи, которые lsвам приходится обрабатывать, перевешивают любые возможные выгоды. Этих особых случаев можно избежать, если не анализировать lsвывод.

Мантра здесь такая:никогда не доверяйте файловой системе пользователя(эквивалентникогда не доверяйте пользовательскому вводу). Если есть метод, который будет работать всегда, со 100% уверенностью, это должен быть метод, который вы предпочитаете, даже если lsон делает то же самое, но с меньшей уверенностью. Я не буду вдаваться в технические подробности, так как они были рассмотренытердониПатрикшироко. Я знаю, что из-за рисков использования lsв важной (и, возможно, дорогостоящей) сделке, где на кону моя работа/престиж, я предпочту любое решение, которое не имеет степени неопределенности, если этого можно избежать.

Я знаю, что некоторые люди предпочитаютнекоторый риск вместо уверенности, ноЯ отправил(а) сообщение об ошибке.

Связанный контент