Использование bash completion в командной строке, собственных скриптах и приложениях. Часть 2

    Про bash completion на хабре я уже писал тут, и даже конце пообещал рассказать про настройку автодополнения для собственных скриптов.

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



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



    Есть скрипт, у которого есть три подкоманды. Одна из этих команд — work — в примере не рассматривается, у оставшихся есть следующие подкоманды. history делает import или export, каждой из этих команд надо передать имя проекта и пару значений под флагами. help может рассказать про work, history и help.

    Теперь о том, как, собственно, работает автодополнение. Для баша пишется функция, которой передаются уже введенные аргументы, и на основе их она генерирует возможные варианты дополнения. Эта функция (назовём её _my_command) регистрируется для конкретной команды (в данном случае — мы исполняем скрипт названный script, поэтому регистрация идет для script) волшебной командой complete:

    complete -F _my_command script

    Теперь самое интересное — написание этой функции, обрабатывающей аргументы, и выдающей список доступных параметров.
    Для начала, из man bash можно узнать о существовании специальных переменных для работы с автодополнением. Мы будем использовать следующие:

    COMPREPLY
    Это массив, из которого bash получает возможные дополнения.

    COMP_WORDS
    Это массив, содержащий уже введённые аргументы. Анализируя их, мы можем понять, какие варианты дополнения надо предлагать.

    COMP_CWORD
    Это — индекс в предыдущем массиве, который показывает позицию в нем аргумента, редактируемого в данный момент.

    Теперь попробуем на основе этих переменных проанализировать ввод, и если вводится первый аргумент — попробовать его дополнить
        _my_command(){ #объявляем функцию, которую будем привязывать к анализу
        
        COMPREPLY=() #пока что мы не знаем, что предложить пользователю, поэтому создадим пустой список.
        cur="${COMP_WORDS[COMP_CWORD]}" #получаем текущий вводимый аргумент
        subcommands_1="work history help" #массив подкоманд первого уровня - см. синтаксическое дерево в начале поста.
    
        if [[ ${COMP_CWORD} == 1 ]] ; then #если вводится первый аргумент, то попробуем его дополнить
            COMPREPLY=( $(compgen -W "${subcommands_1}" -- ${cur}) ) #some magic
            return 0 #COMPREPLY заполнен, можно выходить
        fi
        }
    


    если теперь мы запишем эту функцию вместе с приведенным выше вызовом complete в какой-нибудь скрипт, например ./complete.sh, выполним его в текущей консоли (лучше, конечно, для экспериментов запускать новый баш, а потом его убивать) как. ./complete sh, и, введя «script », нажмем Tab 2 раза, bash предложит нам варианты дополнения:
    $ script 
    help     history  work
    

    Соответственно, если начать вводить какую-то подкоманду, например wo и нажать Tab, то произойдёт автодополнение.

    Однако я ещё не объяснил, как именно работает использованная в скрипте магия, а именно

            COMPREPLY=( $(compgen -W "${subcommands_1}" -- ${cur}) ) #some magic
    


    Тут мы заполняем список возвращаемых вариантов с помощью встроенной утилиты bash compgen.

    Данная утилита принимает на вход список всех возможных значений аргумента, а так же текущую введенную часть аргумента, и выбирает те значения, до которых введенную часть можно дополнить. Введенная часть аргумента передается после --, а со списком возможных значений всё интереснее. В приведенном случае, возможные значения берутся (как указывает флаг -W) и данного скрипту списка слов (т.е. в приведенном выше примере — из subcommands_1=«work history help»). Однако там можно указывать и другие флаги — например -d — и тогда compgen будет дополнять исходя из существующих на машине директорий, или -f — тогда он будет дополнять до файлов.

    Можно посмотреть, что он выдаёт:
    $ compgen -W "qwerty qweasd asdfgh" -- qwe
    qwerty
    qweasd
    


    Соответственно можно генерировать различные списки кандидатов на автодополнение. Например, для решаемой задачи, нам (для импорта и экспорта истории) нужен список возможных проектов. В моём случае, каждому проекту соответствует директория в "${HOME}/projects", соответственно кандидатов можно подбирать как
    COMPREPLY=($(compgen -W "`ls ${HOME}/projects`" -- ${cur}))

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

    В завершение — моя топорная реализация автодополнения для модели, указанной в начале:

    _my_command()
    {
        COMPREPLY=()
        cur="${COMP_WORDS[COMP_CWORD]}"
        subcommands_1="work history help" #возможные подкоманды первого уровня
        subcommands_history="import export" #возможные подкоманды для history
        
        
        if [[ ${COMP_CWORD} == 1 ]] ; then # цикл определения автодополнения при вводе подкоманды первого уровня
            COMPREPLY=( $(compgen -W "${subcommands_1}" -- ${cur}) )
            return 0
        fi
        
        
        subcmd_1="${COMP_WORDS[1]}" #К данному моменту подкоманда первого уровня уже введена, и мы её выбираем в эту переменную
        case "${subcmd_1}" in #Дальше смотри, что она из себя представляет
        work)
            COMPREPLY=() #ничего дальше вводить не надо
            return 0
            ;;
        history)
    
            if [[ ${COMP_CWORD} == 2 ]] ; then #введены script history; надо подставить import или export
                COMPREPLY=( $(compgen -W "${subcommands_history}" -- ${cur}) )
                return 0
            fi
    
            #к данному моменту мы уже знаем, что делаем: импорт или экспорт
            subcmd_2="${COMP_WORDS[2]}"
    
            if [[ ${COMP_CWORD} == 3 ]] ; then #но в любом случае следующим аргументом идет имя проекта.
                COMPREPLY=($(compgen -W "`ls ${HOME}/projects`" -- ${cur}))
                return 0
            fi
    
            case "${subcmd_2}" in #а дальше у импорта и экспорта набор флагов разный. мы смотрим на предпоследний аргумент, и если он является флагом - подставляем соответствующие ему значения, иначе - выдаем на дополнение список флагов.
            import)
                case "${COMP_WORDS[COMP_CWORD-1]}" in
                -src) 
                    COMPREPLY=($(compgen -d -- ${cur})) #тут должна быть директория с исходниками
                    return 0
                    ;;
                -file)
                    COMPREPLY=($(compgen -f -- ${cur})) #тут должен быть импортируемый файл
                    return 0
                    ;;
                *)
                    COMPREPLY=($(compgen -W "-src -file" -- ${cur})) #список возможных флагов
                    return 0
                    ;;
                esac
                ;;
    
            export) #у экспорта только один флаг -o, если был он - то мы предлагаем дополнение до файла, куда экспортировать, иначе - предлагаем дополнение до флага
                if [[ ${COMP_WORDS[COMP_CWORD-1]} == "-o" ]] ; then 
                    COMPREPLY=($(compgen -f -- ${cur}))
                    return 0
                fi
                
                COMPREPLY=($(compgen -W "-o" -- ${cur}))
                return 0
                ;;
            *)
                ;;
            esac
            ;;
        help) #список возможных дополнений после help совпадает со списком подкоманд первого уровня, их и исследуем.
    	COMPREPLY=( $(compgen -W "${subcommands_1}" -- ${cur}))
    	return 0
            ;;
        esac
        return 0
        
    }
    
    complete -F _my_command script
    


    Ну и самое главное — подключение нашего скрипта к башу на постоянной основе. Это делается либо (топорно) прописыванием вызова нашего скрипта в .bashrc, либо (стандартно, если у вас есть такой файл: ) через /etc/bash_completion. Во втором случае мы должны положить наш скрипт в /etc/bash_completion.d, все скрипты откуда подцепляются из /etc/bash_completion.

    P.S.: Напомню, что положительный фидбек стоит оставлять в карме пользователя infthi

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 20

    • UFO just landed and posted this here
        0
        подсветил
        • UFO just landed and posted this here
        0
        COMPREPLY=($(compgen -W "`ls ${HOME}/projects`" — ${cur}))
        Можете подсказать, почему здесь не воспользовались опцией "-d"?
          0
          Потому что синтаксис предполагает имена проектов, а не пути к ним. Поскольку в модели каждому проекту соответствует директория в ~/projects, их имена невозбранно выдираются ls'ом. Подставление произвольной директории туда — неверно по идеологическим причинам.

          Ну и хотелось показать возможность подставления произвольных данных =)
          0
          холивар! может лучше просто zsh поставить? :)
          а вообще круто, конечно
            0
            чем zsh тут поможет?
              0
              ну, в нем автокоплит многое (в том числе большую часть описанного в статьях) умеет из коробки без дополнительных изощрений.
              + еще очень много всяких крутых штук, которые к башу прикручивать замучаешься, но это уже оффтоп
                0
                ну тут речь идет именно о кастмных комплишенах, которые zsh сам по себе уметь не может.
                  +1
                  если в script добавить --help который опишет все варианты, то zsh сможет сама сгенерировать автокомплит, например
                  • UFO just landed and posted this here
                      0
                      так кто бы спорил :)
            +4
            ИМХО, это топик для habrahabr.ru/blogs/shells
              0
              скорее в «линукс для всех»
              для shells всёже поосновательнее статью надо.
                0
                в shells стоит сделать выжимку из этих двух статей — будет самое то.
                А «linux для всех» — не совсем то, «домашним» пользователям нафиг не нужен комплишн.
              0
              При написании автодополнения я столкнулся с такой проблемой:
              Некоторые агументы могут иметь двоеточие в середине, например у меня есть второй аргумент «foo:bar».
              Когда я ввожу «fo», аргумент дополняется до «foo:» и при последующем нажатии на tab дополнение не проиходит так как COMP_CWORD равно не двум, а трём. То есть двоеточие выступает как разделитель.
              Можно ли как-то обойти эту пробелму?
                +2
                Если кому-то интересно, нашел вот такое решение
                COMP_WORDBREAKS=${COMP_WORDBREAKS//:}
                0
                Извиняюсь за вопрос не в тему. А можно ли сделать что-то вроде алиаса аргумента команды в bash?
                Например, в команде «du -h --max-depth 0 /home» заменить аргумент "--max-depth" на фряшный "-d"?
                Заранее, спасибо!
                  0
                  насколько я знаю — нет.
                  но можно сделать алиас на команду с аргументами, если приходится ее часто запускать.
                  0
                  Чем генерил картинку?

                  Only users with full accounts can post comments. Log in, please.