Наблюдение № 1

Первое наблюдение будет как раз о шаблонах, но не мышления, а о шаблонах имён файлов и различиях в их трактовке командными интерпретаторами DOS/Windows и Unix/Linux.

Допустим, в дереве файловой системы есть такая ветвь:

…\folder\
…\folder\file1.txt
…\folder\subfolder\
…\folder\subfolder\file2.txt

Для эксперимента в Windows её можно создать командами:

> MKDIR folder\subfolder
> ECHO 1 > folder\file1.txt
> ECHO 2 > folder\subfolder\file2.txt

а в Linux - командами:

$ mkdir -p folder/subfolder
$ echo 1 > folder/file1.txt
$ echo 2 > folder/subfolder/file2.txt

Предположим, возникла необходимость просмотреть содержимое каталога folder, для чего в Windows была набрана команда:

> DIR /B folder\*

Ключ /B требует отображать только имена файлов, без дополнительных сведений о дате, типе и тому подобных деталях.
Вот, что эта команда выведет:

file1.txt
subfolder

То, что требовалось и ожидалось. Теперь выполним аналогичную команду в командной оболочке BASH под Linux:

$ ls -1 folder/*

Ключ -1 (цифра «один», а не строчная латинская буква «эль») требует осуществлять вывод в одну колонку, но дело не в этом, а в самом содержимом этого вывода:

folder/file1.txt
folder/subfolder:
file2.txt

Ладно бы ещё только имена файлов и каталогов предварялись родительским folder — с этим ещё можно мириться. Но зачем команда отобразила содержимое подкаталога folder/subfolder?

Оказывается, командные оболочки Windows (COMMAND.COM и CMD.EXE) и Linux (SH и BASH, в других оболочках дело может обстоять иначе) по-разному обрабатывают параметры командной строки, которые содержат подстановочные символы * и ?. Windows поступает просто — передаёт параметры командам/программам в том виде, в каком они записаны в командной строке. Раз написано folder/*, то команда DIR и получит folder/*, а дальше сама попробует понять, чего от неё хотят. Раз *, значит надо показать все элементы каталога folder. Всё просто и логично.

В Linux всё сложнее. Командная оболочка сначала анализирует параметры командной строки и, если в них нет подстановочных символов или параметры заключены в кавычки, то поступает как Windows. Если же параметры содержат подстановочные символы и в кавычки не заключены, то командная оболочка сама проверяет соответствие содержимого каталога указанному шаблону и заменяет параметр с шаблоном на список параметров с подходящими именами файлов. В приведенном примере после такой обработки командная строка

$ ls -l folder/*

превратится в:

$ ls -l folder/file1.txt folder/subfolder

Программа ls выводит информацию о своих параметрах, причём если это каталог, то по умолчанию отображает его содержимое. Между прочим, Windows ведёт себя схожим образом, если в команде DIR указать несколько параметров:

> DIR /B folder\file1.txt folder\subfolder
file1.txt
file2.txt

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

Причина различий теперь ясна, но как в Linux получить список содержимого каталога без разворачивания содержимого подкаталогов? Можно просто указать имя каталога, без маски:

$ ls -1 folder
file1.txt
subfolder

Отлично, но как быть, если к именам элементов каталога (файлам и подкаталогам) всё-таки надо применить маску, например, «*.abc»? Идея заключить параметр командной строки в кавычки "folder/*" не работает: программа ls (в Linux это внешняя утилита, а не внутренняя команда оболочки, как DIR в Windows) ожидает, что шаблон будет обработан командной оболочкой, и просто сообщает об отсутствии в каталоге folder элемента с именем "*". Так что остаётся воспользоваться специальным ключом -d программы ls:

$ ls -1d folder/*
folder/file1.txt
folder/subfolder

Бытует мнение, что вариант работы командных оболочек Linux более прогрессивный, чем Windows. Ведь оболочка берёт на себя «общую» часть работы по разбору путей к файлам, в результате чего упрощается код программ и стандартизируется их поведение. Это действительно так, пока шаблону удовлетворяют несколько файлов. Но что, если таких файлов 100500? Командная оболочка попытается единовременно создать командную строку, в которой перечислены пути ко всем этим файлам. Обычно это заканчивается нехваткой памяти для командной строки. В то время, как подход, принятый в Windows, позволяет программе спокойно перебирать все файлы по одному.

Наблюдение № 2

Второе наблюдение связано с именованием каталогов в Linux. Имя subfolder может означать как файл, так и каталог. Несмотря на то, что в Linux почти все сущности являются файлами, различия иногда бывают важны. Допустим, требуется создать копию файла file1.txt в подкаталоге subfolder. Это можно сделать такой командой:

$ cp folder/file1.txt folder/subfolder

Она работает, и всё вроде бы прекрасно. Но предположим, что в путь назначения вкралась опечатка: вместо subfolder набрано sunfolder. Что произойдёт в этом случае? Операция завершится успешно, но содержимое файла file1.txt будет записано в файл folder/sunfolder. Это, очевидно, не то, что ожидалось. Как можно избежать такой неприятности?

Один из вариантов — для указания целевого каталога использовать автодополнение по клавише Tab. Но в этом случае надо быть особенно внимательным, если в каталоге folder есть другие элементы с именами, похожими на subfolder. Автоматика может выбрать не ту цель, которая была запланирована, и промах будет не менее досадным.

Чтобы подчеркнуть в команде, что в качестве целевого элемента ожидается именно каталог, а не файл, достаточно его имя завершить наклонной чертой:

$ cp folder/file1.txt folder/subfolder/

В таком варианте опечатка уже не пройдёт незамеченной:

$ cp folder/file1.txt folder/sunfolder/
cp: cannot create regular file 'folder/sunfolder': Not a directory

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

Наблюдение № 3

Третье наблюдение касается «особых каталогов». Все знают, что родительский каталог обозначается двумя точками: «..». Многим известно, что одной точкой «.» обозначается текущий каталог. Но если на каталоги верхнего уровня приходится ссылаться регулярно, то польза от ссылки на текущий каталог не столь очевидна: он ведь и так должен подразумеваться по умолчанию.

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

$ sudo apt install moria.deb
...
E: Unable to locate package moria.deb
E: Couldn't find any package by glob 'moria.deb'
E: Couldn't find any package by regex 'moria.deb'

Можно возразить, что для работы с локальными пакетами следует использовать программу dpkg, а не apt, которая задумывалась для работы с пакетами в репозиториях и ожидает в командной строке название пакета, а не имя файла. Но, с другой стороны, небольшое изменение командной строки позволяет раскрыть и программе apt глаза и на локальные файлы:

$ sudo apt install ./moria.deb

В отличие от dpkg, такой вариант автоматически подхватит из репозиториев недостающие зависимости, если такие вдруг обнаружатся в ходе установки.

Ещё одна польза от «точки» обнаруживается, когда надо выполнить копирование содержимого каталога, в котором есть скрытые файлы или подкаталоги (в Linux таковыми являются файлы, первым символом в имени которых идёт точка). Примером может служить файловая система дистрибутивов Ubuntu, в корне которой которой присутствует каталог .disk со служебной информацией. Если образ дистрибутивного носителя примонтирован в точке /mnt/ISO, то следующая команда копирования разместит в целевом каталоге ~/ubuntu копии всех файлов и каталогов, кроме скрытого:

$ cp -a /mnt/ISO/* ~/ubuntu/

А скрытый каталог потребуется переписать явным образом:

$ cp -a /mnt/ISO/.disk ~/ubuntu/

Но можно воспользоваться «точкой» и выполнить всю необходимую работу одной командой:

$ cp -a /mnt/ISO/. ~/ubuntu/

Этот вариант выполнит полное копирование, включая скрытый каталог.

Приведенные в статье наблюдения лишний раз доказывают общеизвестный факт о том, что мелочей в формальных языках общения с компьютером не бывает. И от точки может зависеть результат операции, как от запятой — судьба человека.

Комментарии (20)


  1. engine9
    07.01.2024 08:29
    -2

    Будьте добры, поясните назначение ссылки с точкой. Мне в своё время знакомый линуксоид показал, что из терминала запустить программу можно вызвав её с припиской точки и слэша.
    Вот так ругается:

    $ krita-5.1.5-x86_64.appimage
    krita-5.1.5-x86_64.appimage: command not found

    А вот так запускается:

    $ ./krita-5.1.5-x86_64.appimage

    Использую как заклинание, но не понимаю в чем задумка.


    1. R0bur Автор
      07.01.2024 08:29
      +5

      В Linux поиск исполняемых файлов осуществляется по путям, прописанным в переменной окружения PATH. Текущий каталог в список этих путей не входит. Поэтому если Вам надо выполнить программу/сценарий из текущего каталога, следует явным образом указать к путь к исполняемому файлу, а не полагаться на умолчание. Это Вы и делаете через "точку".


      1. engine9
        07.01.2024 08:29

        Вот спасибо! Теперь понял. Баш ведь воспринимает команду как команду, типа "ls".


        1. R0bur Автор
          07.01.2024 08:29
          +1

          Возможно, Вам будет удобнее поместить программу в один из подкаталогов домашнего каталога пользователя, которые входят в список путей поиска PATH. Это может быть ~/bin или ~/.local/bin. Тогда можно будет запускать её без префикса. Как посмотреть список путей, прописанных в PATH, написано ниже. Пользовательские каталоги начинаются с "/home/имя_пользователя/...".


      1. R0bur Автор
        07.01.2024 08:29
        +2

        Дополню свой ответ практическим упражнением.
        Чтобы посмотреть список путей в переменной PATH, можно выполнить команду:
        $ echo $PATH
        (обратите внимание, что пути разделяются двоеточием, а не точкой с запятой, как в Windows)
        Вы можете временно (в текущем сеансе командной оболочки) добавить путь к текущему каталогу в переменную PATH так:
        $ export PATH=$PATH:.
        После этого запуск программы упростится:
        $ krita-5.1.5-x86_64.appimage
        Но добавлять текущий каталог в переменную PATH на постоянной основе не рекомендуется по соображениям безопасности.


      1. Xexa
        07.01.2024 08:29
        -3

        Что не логично по сути своей.

        Что мешает искать начиная с текущего и далее по прописанным в path?

        А не вот этот костыль...


        1. R0bur Автор
          07.01.2024 08:29
          +5

          Так сделано для защиты от опечаток. "Злодей" может поместить в текущем каталоге исполняемый файл "ks". И тогда, если Вы промахнётесь по букве "l" в безобидной команде "ls", выполнится то, что выполняться не должно.


          1. Xexa
            07.01.2024 08:29
            -3

            И в чём проблема "описки"? Описка одна на тысячу, а вводить каждый раз лишние символы, когда запускаешь программы из текущей директории.

            Так сделано из-за косячного подхода изначального. И "линуксстайл" - пользователь должен страдать.

            Есть ядро ОС Линукс (и иже с ним). Само по себе оно бессмысленное. Для любой ОС нужен набор базовых инструментов как то "операции над файловой системой", "редактирование/чтение файлов" и тп.

            В линуксе это набор "утилит". Они базовые, без них смысла нет в операционке ибо ничего не сможешь сделать.

            В чём проблема эти утилиты сделать вшитыми в командную оболочку? И не будет проблем с "описками" и "кто-нибудь плохой подсунул программу с тем же именем, что ls"(это вообще бред ибо в линуксе запуск по умолчанию несколько админскими правами у приложений и ограничение на лазанье в системные директории, т.е ничего страшного от запуска приложения не будет)... Лежит в директории файл с именем ls, запустил пользователь в ls в командной строке - так пусть и крикнет командная строка "чувак, у тебя коллизия с системной командой... Проверь что за фигня у тебя".

            Dos и командная строка в винде вполне адекватно это отрабатывают.

            Зы: ах да.. Зоопарк команд в линуксе. Что-то в программах висит, что-то в bash вшито как команда и пересечение имён норм живёт и bash адекватно кричит... А вводить ./ - бред. И хоть заминусите линуксойды, но это бред и костыль вводить лишний раз символы которые один фиг ты введёшь и заставишь выполнить что хочешь, но с оверхедом повводимым символам.


            1. martin_wanderer
              07.01.2024 08:29

              Как пользователь линукса полностью разделяю ваше негодование. Развели зоопарк, понимаешь. С другой стороны - ну если какая-то утилита так часто используется - может просто добавить ее в PATH?


            1. storoj
              07.01.2024 08:29
              +1

              Лежит в директории файл с именем ls, запустил пользователь в ls в командной строке - так пусть и крикнет командная строка "чувак, у тебя коллизия с системной командой... Проверь что за фигня у тебя".

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


    1. RumataEstora
      07.01.2024 08:29
      +5

      Есть переменная $PATH, которая содержит список путей, где искать файлы на исполнение. Например:

      PATH=/bin:/usr/bin:/usr/local/bin

      Когда в командой строке пишете что-то вроде:

      $ какая-то-команда с какими-то ключами

      то оболочка (bash, sh, zsh или, прости господи, cmd) ищет файл, перебирая все пути из переменной $PATH. То есть оболочка ищет исполнимый файл, один из:

      /bin/какая-то-команда
      /usr/bin/какая-то-команда
      /usr/local/bin/какая-то-команда

      Как только будет найден файл для запуска (исполнения), поиск прекращается и вызывается команда с параметрами, например:

      /какой/то/путь/какая-то-команда с какими-то ключами

      Если вы явно описываете путь

      ./какая-то-команда с какими-то ключами

      то поиск не производится, а делается попытка запустить такую команду.

      Здесь описана упрощенная схема. Здесь не описаны особые случаи для псевдонимов (alias), функций, которые обрабатываются в первую очередь. Также не сказано про очень особый случай с cmd.exe, который а) дополнительно смотрит на расширение файла (сравнивает с переменной %PATHEXT%, ищет ассоциации по расширению, выполняет еще какие-то магические действия), б) игнорирует переменную %PATH%, если в текущем каталоге имеется исполнимый файл с таким именем и в) некоторые его подводные камни.


    1. Eteveto
      07.01.2024 08:29
      +3

      Как минимум задумка в том, чтобы запуск программ (исполнимых файлов любого типа, которые вносят изменение в системе) был чётко регламентирован и подконтролен пользователю.
      В системах подобных юникс есть право на исполнение. Как минимум - право владельца файла, группы и всех остальных. Даже без упоминания других возможностей, как исполнение от имени другого пользователя (суидность).

      Большинство исполнимых файлов (с правом исполнения "для всех") размещены в конкретных местах. Опять волшебное понятие - регламент. Эти места описываются в переменной пути. Причём значение этой переменной может быть своё не только для конкретного пользователя, но вообще для каждой отдельной сессии, и даже для конкретной копии запущенного процесса от имени пользователя. Эта переменная - часть контекста.

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

      Но главное - вообще не нужно запускать исполнимые файлы откуда попало. И вот тут главное - в системе может храниться несколько файлов с одним именем. Поэтому логично, безопасно и правильно запускать что-либо только из предназначенных для этого мест. Смотри переменную пути. И каждый исполнимы файл в безопасной системе должен быть с указанием владельца, группы и чётко определёнными правами права исполнения и изменения.
      Это гарантирует, что что-то левое (и скорее всего зловредное) не проникнет в систему и даже с минимальными правами не наплодит в каждом доступном для изменения месте кучу исполнимых файлов, которые будут по имени совпадать с прочими программами, включая системные.

      Именно поэтому для запуска чего-то нестандартного нужно указать полный путь. А полный путь к исполнимому фалу, который мы видим находясь в конкретной директории и есть точка-слеш - ./имя.исполнимого.файла.запустить_один.раз
      И право исполнимости нужно устанавливать на файлы осознанно.
      И разговор не только о разработчиках программ, у которых в процессе их деятельности может оказаться десяток различных версий под одним именем. Необходимость что-то выполнить может возникнуть у кого угодно.
      Главное - это полный контроль пользователя за тем, что он запускает. И понимание, что он запускает, откуда и зачем. И ответственность за такой запуск лежит уже на пользователе. Вероятность обмана пользователя уменьшена на порядок...

      Но если хочется сделать дырку в своей собственной системе... ну, вы всегда можете добавить "текущий каталог" в путь. Делов-то добавить в профиль PATH=,/:$PATH


      1. mpa4b
        07.01.2024 08:29

        Но если хочется сделать дырку в своей собственной системе... ну, вы всегда можете добавить "текущий каталог" в путь. Делов-то добавить в профиль PATH=,/:$PATH >

        Вот правда потом такая шняга может сломать сборку gcc, например


  1. Batalmv
    07.01.2024 08:29
    +4

    Наблюдение первое

    На самом деле в *nix системах правил разыменования намного больше, чем в cmd виндовом. И именно в этом вся соль, о которой вы написали, что оно происходит до выполнения команды.

    В этом суть идеологии реализации команд, как кирпичиков, которые умеют делать что-то одно в явном виде. Банально конструкция for f in *; do smth; done. Все понятно и очевидно. Разыменование подставит имена найденных файлов по маске, а потом можно с ними делать всякое. В винде же банально научили команду dir неочевидной функции, и она перестала быть "простым" кирпичиком. В *nix я всегда знаю, что происходит не потому что это "фича" конкретной команды, а свойста языка оболочки

    Упасть по памяти из-за большого числа файлов в каталоге - ну не знаю, интересно попробовать, а сколько это надо файлов создать?

    Быстро попробовал, сделал 1000 файлов с длиной имени каждого 200+ символов. Ожидаемо ничего не упало.

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

    ---------

    Файлик кстати можно создать командой touch :)

    Как по мне, командная строка в win - это просто какая-то заготовка, которую как-то слепили и выдали, чтобы было :)


    1. RumataEstora
      07.01.2024 08:29
      +3

      Команда ls по умолчанию сортирует результат своего выполнения. Вероятно, сортировка производится в памяти. Однажды я столкнулся с проблемой падения ls * на нескольких десятках тысяч файлов. В таких случаях ls -U или find спасают ситуацию.


    1. R0bur Автор
      07.01.2024 08:29
      +2

      Упасть по памяти из-за большого числа файлов в каталоге - ну не знаю, интересно попробовать, а сколько это надо файлов создать?

      Быстро попробовал, сделал 1000 файлов с длиной имени каждого 200+ символов. Ожидаемо ничего не упало.

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

      Насколько мне удалось выяснить, максимальная длина командной строки зависит от значения параметра ARG_MAX, установленного при сборке ядра Linux, и может быть получена командой:
      $ getconf ARG_MAX
      На доступной мне системе это значение - 2,097,152 (байта).


      1. saboteur_kiev
        07.01.2024 08:29
        +1

        Давным давно ARG_MAX было в районе килобайта, потом очень долгое время 4 кбайта. Потом, с всеобщим внедрением UTF-8, меньше 32 кбайт уже вроде бы и нет.
        Но в любом случае, если есть подозрение, что после всех expansions, длина командной строки будет больше 1-4 кбайт, уже стоит подумать о пайпе


      1. truthseeker
        07.01.2024 08:29
        +3

        Ну, хоть кто-то про эту написал. Спасибо, что просвещаете несмышлёнышей.

        А то

        Обычно это заканчивается нехваткой памяти для командной строки.

        у автора статьи - это какое-то дилетантство.

        Упирался в ограничение на практике не раз. На серверах с кучей памяти делал ls с * в каталоге с миллионами мелких файлов(сессии php, если кому интересно). До определённого момента очень тупит, но потом показывает(тупит на этапе чтения инфы о всех файлах и сортировке полученной инфы), затем отображает инфу о файлах. После определённого количества айнодов отображается ошибка о превышении максимального количества аргументов, вывод инфы о файла в таком случае вы не получите совсем.

        Собственно, по этой причине если вам нужно удалить много файлов сессий, юзайте find /ou/sessions/dir/ -type f -delete сразу, не пытайтесь удалять файлы с помощью rm в каталоге с миллионами файлов.

        То же касается ls, у find есть замечательный ключ -ls (делает примерно то же, что ls - отображает инфу о файла с подробностями).


    1. R0bur Автор
      07.01.2024 08:29

      Как по мне, командная строка в win - это просто какая-то заготовка, которую как-то слепили и выдали, чтобы было :)

      Справедливости ради, COMMAND.COM (в Windows 9x) и CMD.EXE (в WinNT и его последователях до настоящего времени), видимо, связаны требованием совместимости с командным интерпретатором MS DOS. Возможности современной оболочки PowerShell гораздо шире. Но и для старых оболочек можно писать довольно сложные и полезные сценарии.


  1. saboteur_kiev
    07.01.2024 08:29
    +1

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

    Шелл в линукс имеет свои способы решения, и их более чем достаточно.
    То же обращение к файлу - нет такого как "все есть файл", это абстракция. Это может быть файл, директория, ссылка, жесткая ссылка, пайп, file special или device special, и это очень круто!

    Генерирование списка файлов в командной строке имеет свой глубокий смысл.
    Оболочка берет на себя работу с wildcard по генерации списка, что позволяет очень быстро и легко писать свои скрипты и утилиты для работы с этим списком.
    Проблема длины командной строки давно решена благодаря утилите xargs.
    Если же не хватает wildcard, есть find с богатым функционалом.

    wildcard expansion и variable expansion это два мощнейших инструменты линукс шелла, и не нужно их сравнивать с cmd, который заменили на powershell и там совсем другая идеология.

    $ sudo apt install moria.deb ... E: Unable to locate package moria.deb

    Этот пример вообще неадекватный. Вы пытаетесь установить пакет не из файла, а из репозитория. Чтобы сказать что moria.deb это именно файл, нужно явно указать путь к файлу. Иначе будет выполняться поиск по репозиториям, а не в текущем каталоге.

    Ну а наблюдение два я вообще не понял. Во всех операционных системах есть понятие абсолютного и относительного пути. И что самое важно, следует не забывать что работа с файловой системой это не столько особенность Линукс, сколько стандарт POSIX, который поддерживает и Unix и MacOs и Linux (и его производные типа Андроид), и другие, например WebOS.