ah — лучше, чем history

    Так получается, что я провожу в консоли (терминале) достаточно времени, порой даже больше, чем хотелось бы. Порой даже исполняю там какие-то команды и внимательно изучаю их вывод. Часто случается, что к выводу той или иной команды приходится возвращаться, а он постоянно теряется: то терминалы захлопываются, то в tmux окно закрываешь, то выводы прочих команд уже давным-давно забили и похоронили ту самую полезную строчку.

    Для того, чтобы сохранить вывод какой-либо утилиты я, как и многие, пользовался tee. Это работало, но постоянная суета среди бесконечных error.log, out.log, output.log, err.log log.log, lll.txt и тп если не сводила с ума, то безумно раздражала; вместо того, чтобы вести какой-то порядок, постоянно подмывало создать Новую Папку (1), где и похоронить эти самые логи, периодически бэкапя могильничек: порядок предполагал какую-то систематизацию, а в разгаре работы вспоминать как назвать свой файл крайне не хотелось.

    Тогда я написал ah, крохотную утилитку, которая сильно-сильно улучшила мою жизнь.

    ah предполагалась небольшим дополнением к встроенной команде history, которая есть практически в каждом шеле; однако ah умеет работать лишь с двумя: zsh (поскольку я им сам пользуюсь) и bash (поскольку им пользуются практически все, кто не пользуется zsh). Это ни в коем случае не замена history, скорее это дополнение к ней. ah, в основном, умеет 4 вещи: показывать history, сохранять вывод потоков команд, привязывать его к номеру в истории, и показывать по запросу. Кроме того, есть еще такая мелочь, как закладки (любой записи из истории можно дать имя).

    Сохранение вывода команды


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

    ➜ ah t -- find ./app -name "*.go" -type f
    ./app/historyentries/get_commands.go
    ./app/historyentries/parser.go
    ./app/historyentries/keeper.go
    ./app/historyentries/history_entry.go
    ./app/historyentries/history_processor.go
    ./app/environments/environments.go
    ./app/utils/re.go
    ./app/utils/logging.go
    ./app/utils/synchronized_writer.go
    ./app/utils/exec.go
    ./app/utils/utils.go
    ./app/commands/bookmark.go
    ./app/commands/remove_bookmarks.go
    ./app/commands/gc.go
    ./app/commands/list_trace.go
    ./app/commands/tee.go
    ./app/commands/execute.go
    ./app/commands/show.go
    ./app/commands/list_bookmarks.go
    ./app/slices/slices.go
    


    Вывод сохраняется (причем объединенный, как с stdout, так и с stderr), и его можно запросить в дальнейшем. В чем же отличие от, скажем, tee? С tee можно тоже написать подобное

    ➜ find ./app -name "*.go" -type f |& tee output.log
    


    На самом деле, эти команды не равнозначные. Дело в том, что для tee мы перенаправляем stderr в stdout, тем самым теряя возможность их фильтрации после tee. ah же сохраняет это разделение. Иными словами, мы можем написать

    ➜ ah t -- find ./app -name "*.go" -type f > /dev/null
    


    И получить на экране только вывод stderr. А что сохранит ah? Он сохранит оба потока. И код возврата. Да, ah завершается с тем же кодом, с которым отработала предыдущая команда. Еще ah нормально работает с ssh, причем даже если запускать там ncurses-приложения. Если нужно, есть поддержка псевдо-TTY и возможность запуска в реальном интерактивном шеле.

    Показ истории



    ➜ ah s 10
    ...
    !10109  (02.11.14 18:05:14)    nvim main.go
    !10110  (02.11.14 21:48:12) *  ah t -- find ./app -name "*.go" -type f
    


    Да, ah умеет показывать содержимое вашего HISTFILE и знает про HISTTIMEFORMAT. Угадайте, зачем рядом с номером стоят восклицательные знаки. А вот что значит звезда перед ah t...? Это значит, что ah хранит вывод этой команды. Посмотреть вывод можно с помощью субкоманды l.

    ➜ ah l 10110
    ./app/historyentries/get_commands.go
    ./app/historyentries/parser.go
    ./app/historyentries/keeper.go
    ./app/historyentries/history_entry.go
    ./app/historyentries/history_processor.go
    ./app/environments/environments.go
    ./app/utils/re.go
    ./app/utils/logging.go
    ./app/utils/synchronized_writer.go
    ./app/utils/exec.go
    ./app/utils/utils.go
    ./app/commands/bookmark.go
    ./app/commands/remove_bookmarks.go
    ./app/commands/gc.go
    ./app/commands/list_trace.go
    ./app/commands/tee.go
    ./app/commands/execute.go
    ./app/commands/show.go
    ./app/commands/list_bookmarks.go
    ./app/slices/slices.go
    


    Кстати, 10 в ah s 10 означает буквально «показать последние 10 команд». В то же время поддерживается синтаксис слайсов (1:1 как в Python): ah s 10 20 покажет все команды с 11 по 20, ah s 10 _20 — с 11 по 20 с конца (_, но не -). Еще можно искать по регулярным выражениям + есть примитивный fuzzy matching.

    Кроме того, можно делать закладки с субкомандой b, листать их с lb, удалять с rb, чистить старые выводы с gt, но это уже мелочи.

    Надеюсь, что кому-нибудь еще жить станет легче.

    So there.
    github.com/9seconds/ah
    Поделиться публикацией

    Комментарии 21

      +8
      Осталось чтобы выод сохранялся автоматически, и не приходилось помнить о том, что в начало каждой второй команды надо допихать вызов ah.
        +7
        Я как раз сейчас с этим экспериментирую (preexec в zsh и его эмуляция в bash), и на самом деле это изначальная цель. Но пока не все так гладко, особенно с bash'ем. В основном, все работает, но когда что-то отваливается, приходится мучительно все отключать и чесать голову, как подебажить. Так что пока без этого.
          0
          Я только что тоже пытался сделать это через preexec в zsh но ни чего не вышло. Я тупо не понял как повлиять на саму команду, а не просто выполнить что-то рядом с ней.
          Зато для zsh я смог получить в общем-то желаемый результат… Вот пример:
          add-time() { [[ $BUFFER = time* ]] || BUFFER="time $BUFFER"; zle .$WIDGET "$@"; }
          zle -N accept-line add-time
          
            +1
            Примерно вот так можно в zsh:

            function execute_with_ah {
                BUFFER="ah t -- $BUFFER"
                zle accept-line
            }
            
            zle -N execute_with_ah_widget execute_with_ah
            bindkey '^J' execute_with_ah_widget
            bindkey '^M' execute_with_ah_widget
            


            Единственное, что останавливает: пока очень плохо работают приложения с ncurses. Ищу сейчас, каким образом можно их правильно запускать, но прогресса мало (в частности ah t -- htop лучше не делать).
              0
              Ну можно сделать локальный костыль в виде исключения их в функции execute_with_ah по маске…
                +1
                А тем временем htop, vim сотоварищи заработали.
            • НЛО прилетело и опубликовало эту надпись здесь
              • НЛО прилетело и опубликовало эту надпись здесь
            +2
            Спасибо! Как и с другими хорошими идеями, я до вашего поста даже не задумывался о проблеме, а после удивляюсь, как же сам не додумался до такой штуки. :-)
              0
              есть утилитка script. Она забавнее вашей.
                +4
                А где на неё можно посмотреть, ибо гуглить «script» – занятие бесполезное.
                0
                Ещё есть script на стероидах. Он лучше. LiLaLo.
                0
                Ох, меня тоже давно мучала эта проблема (и её производные), но помимо хранения истории, мне хочется иметь к ней доступ с разных машин.
                Я даже начал что-то делать именно с этой стороны.
                flint не думал в сторону синхронизации?
                  0
                  Ох нет, и вот почему: дело в том, что все шелы работают с историей в предположении, что никто, кроме них в файл с историей лазить не будет, поэтому ah поступает несколько хитрее, чем просто привязывается к номеру (к номеру из HISTFILE привязываться, конечно, можно, но это работает только с несколькими довольно жесткими предположениями). Поэтому синхронизация истории в отрыве от синхронизации HISTFILE — занятие бессмысленное. С тем же успехом можно просто rsync'ать ~/.ah :)

                  В общем, я пока просто не представляю, как это должно выглядить и работать.
                  0
                  Прекрасно, спасибо.
                  Подскажите как у вас работает garbage collecting? Оно удаляет все или что-то определенное? Просто сборка муссора ассоциируется с чем-то, что работает само в фоне и удаляет мусор по каким-то критериям :)
                    0
                    Самым простым и предсказуемым образом, никакой магии :) Есть 3 флага для очистки. --all удаляет все-все, --keepLatest оставляет последние n выводов (хронологически), --olderThan удаляет все записи старше n дней.
                      0
                      Спасибо. Не нашел этого в документации на github-е.
                        0
                        Согласен, допишу немного позже. Она пока куцая, не спорю.
                    0
                    Круто, мысли людей сходятся, только кто-то думает и делает быстрее!

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое