Как удалить дубликаты в моем .bash_history, сохраняя порядок?

Вопрос или проблема

Мне очень нравится использовать control+r для рекурсивного поиска в истории команд. Я нашел несколько хороших параметров, которые мне нравится использовать с ним:

# игнорировать повторяющиеся команды, игнорировать команды, начинающиеся с пробела
export HISTCONTROL=erasedups:ignorespace

# сохранять последние 5000 записей
export HISTSIZE=5000

# добавлять в историю, вместо перезаписи (удобно для нескольких соединений)
shopt -s histappend

Единственная проблема для меня заключается в том, что erasedups удаляет только последовательные дубликаты – таким образом, с этой строкой команд:

ls
cd ~
ls

Команда ls будет записана дважды. Я думал о периодическом запуске с помощью cron:

cat .bash_history | sort | uniq > temp.txt
mv temp.txt .bash_history

Это бы удалило дубликаты, но, к сожалению, порядок не будет сохранен. Если я не прибегну к sort, я не думаю, что uniq сможет правильно работать.

Как можно удалить дубликаты в моей .bash_history, сохраняя порядок?

Дополнительно:

Есть ли проблемы с перезаписью файла .bash_history через скрипт? Например, если вы удаляете файл журнала apache, думаю, вам нужно отправить сигнал nohup / reset с помощью kill, чтобы он завершил соединение с файлом. Если это касается файла .bash_history, возможно, я мог бы каким-то образом использовать ps, чтобы проверить и убедиться, что нет активных сессий перед запуском скрипта фильтрации?

Я искал то же самое после того, как надоели дубликаты, и обнаружил, что если отредактировать мой ~/.bash_profile или ~/.bashrc, добавив:

export HISTCONTROL=ignoreboth:erasedups

Это делает почти то, что вы хотели (оно удаляет только подряд идущие дубликаты), оно сохраняет только последнюю из любой команды в одной сессии. ignoreboth фактически аналогичен ignorespace:ignoredups, и вместе с erasedups это работает. По крайней мере у меня в терминале Mac с bash это работает отлично. Нашел здесь на askubuntu.com.

Обратите внимание, что ignoredups не поможет с удалением непоследовательных дубликатов из существующего .bash_history. Дубликаты все еще будут появляться в файле при использовании shopt -s histappend.

Сортировка истории

Эта команда работает как sort|uniq, но сохраняет строки на месте

nl|sort -k 2|uniq -f 1|sort -n|cut -f 2

По сути, добавляет к каждой строке ее номер. После выполнения sort|uniq все строки снова сортируются в соответствии с их первоначальным порядком (используя поле номера строки), и поле номера строки удаляется из строк.

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

nl|sort -k2 -k 1,1nr|uniq -f1|sort -n|cut -f2

Управление .bash_history

Для повторного считывания и записи истории можно использовать history -a и history -w соответственно.

Нашел это решение и протестировал:

awk '!x[$0]++'

В первый раз, когда встречается определенная строка ($0), значение x[$0] равно нулю.
Обращение нуля с помощью ! становится единицей.
Утверждение, оцениваемое в единицу, приводит к выполнению действия по умолчанию, то есть печати.

Таким образом, в первый раз, когда встречается конкретный $0, он печатается.

Каждый следующий раз (повторы) значение x[$0] уже увеличено,
и его обратное значение равно нулю, соответственно, утверждение оценивается в ноль и не печатается.

Чтобы сохранить последнее повторенное значение, разверните историю и используйте тот же awk:

awk '!x[$0]++' ~/.bash_history                 # сохранить первое повторенное значение.

tac ~/.bash_history | awk '!x[$0]++' | tac     # сохранить последнее.

Расширяю ответ Клейтона:

tac $HISTFILE | awk '!x[$0]++' | tac | sponge $HISTFILE

tac переворачивает файл.

Убедитесь, что у вас установлен moreutils, чтобы у вас был доступен sponge, в противном случае используйте временный файл. В отличие от перенаправления оболочки, sponge впитывает весь ввод перед записью выходного файла. Это позволяет создавать конвейеры, которые читают и записывают в один и тот же файл.

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

Мое решение в .bashrc:

shopt -s histappend
export HISTCONTROL=ignoreboth:erasedups
export PROMPT_COMMAND="history -n; history -w; history -c; history -r"
tac "$HISTFILE" | awk '!x[$0]++' > /tmp/tmpfile  &&
                tac /tmp/tmpfile > "$HISTFILE"
rm /tmp/tmpfile
  • опция histappend добавляет историю буфера в конец файла истории ($HISTFILE)
  • ignoreboth и erasedups предотвращают сохранение дублирующихся записей в $HISTFILE
  • Команда prompt обновляет кеш истории
    • history -n считывает все строки из $HISTFILE, которые могли появиться в другом терминале после последнего нажатия клавиши ввода
    • history -w записывает обновленный буфер в $HISTFILE
    • history -c очищает буфер, чтобы не возникало дублирования
    • history -r повторно считывает $HISTFILE, добавляя данные в новый чистый буфер
  • скрипт awk сохраняет первое вхождение каждой строки, которую он встречает. tac переворачивает файл, а затем переворачивает его обратно, чтобы он мог быть сохранен с самыми последними командами в истории
  • очистка временного файла

Каждый раз, когда вы открываете новый шелл, все дубликаты истории удаляются,
и каждый раз, когда вы нажимаете клавишу Enter
в другом окне/окне терминала,
она обновляет эту историю из файла.

Эти методы сохранят последние дублированные строки:

ruby -i -e 'puts readlines.reverse.uniq.reverse' ~/.bash_history
tac ~/.bash_history | awk '!a[$0]++' | tac > t; mv t ~/.bash_history

Почти все ответы в этой теме не учитывают файлы истории с временными метками или многострочные записи истории.

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

Я долго искал, но ничего подходящего так и не нашел. Поэтому я в конце концов сделал решение своими руками…

Вот мое решение… Объединить хранящуюся на диске “.bash_history” с историей в памяти оболочки. Сохранение порядка временных меток и порядка команд в пределах этих временных меток.

При необходимости удаляя повторяющиеся команды (даже если они многострочные), и/или удаляя (очищая) простые и/или чувствительные команды в соответствии с определенными регулярными выражениями perl. Настройте как вам удобно!

Вот результат… https://antofthy.gitlab.io/software/history_merge.bash.txt

Вы можете настроить его, как хотите, или сделать его функцией bash. Или же настроить команды, которые очищаются из истории…

Я запускаю это по требованию, используя алиас (например, ‘hm’ для слияния истории) или когда оболочка выходит из системы (из “.bash_logout”), если я не отключил историю оболочки (расстоянием “$HISTFILE”, используя ‘hd’ алиас)

Наслаждайтесь.

У меня включены временные метки, поэтому большинство решений для изменения файлов не работают. У меня также есть каталог для файлов истории, чтобы они были специфичны для хостов. Я использовал некоторые из решений, найденных здесь, чтобы удалить дубликаты и так далее из истории перед записью обратно в файл истории, но иногда у меня открыто несколько оболочек на одном и том же хосте, что затем оставляет эти дубликаты. Мое решение для очистки этого беспорядка время от времени – создать исполняемый файл с следующим содержимым:

#!/bin/sh

for file in ~/.bash_history/*
do
  tac "$file" | awk '!visited[$0]++' | tac | sed 'N;/^#.*\n#.*/!P;D' > tempfile;
  mv tempfile "$file"
done

Сохраните его и выполните.
По сути: переворачивает файл и использует awk для очистки дубликатов, сохраняя последний, снова переворачивает, затем использует sed для удаления последовательных временных меток, оставляя последнюю. Сохраняет файл во временный файл, перемещает временный файл в файл истории. Мой каталог истории уменьшился с 109M до 1008K 🙂

Я написал небольшую программу, которая позволяет чистить вашу историю bash/shell, также ретроактивно и сохраняя ее порядок:

https://gitlab.com/vn971/shell-history-cleaner

USAGE:
    shell-history-cleaner [OPTIONS] <TARGET_FILE>

ARGS:
    <TARGET_FILE>
            Целевой файл для очистки. Вы можете использовать "$HISTFILE", чтобы очистить историю оболочки.

OPTIONS:
    -d, --dedup
            Удаление дубликатов строк, чтобы оставить только одну последнюю встречу каждого дубликата. В отличие от встроенной функции удаления дубликатов bash, это также работает, если дубликаты не следуют друг за другом.

    -r, --remove <REMOVE>
            Строки для удаления. Например, 'yt-dlp.*' удалит строки, начинающиеся с 'yt-dlp'.
            Может быть указано несколько раз.

            Шаблоны - это регулярные выражения, предполагающие полное совпадение строки, как определено здесь: https://docs.rs/regex/latest/regex/#syntax

            Другой пример из реальной жизни:
            '(ps aux.*|git checkout .*|git branch .*| .*|yt-dlp .*|chmod .*|echo .*|man .*)'

    -h, --help
            Вывести справочную информацию

Чтобы уникально записывать каждую новую команду, нужно добавить в
~/.profile или аналогичный файл:

HISTCONTROL=erasedups
PROMPT_COMMAND='history -w'

Затем вам нужно добавить в ~/.bash_logout:

history -a
history -w

Расширение ответа Али.

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

tac $HISTFILE | awk '/^#/{if(l){print l;print;l=""}next} l{print l;l=""} !seen[$0]++{l=$0}' | tac

который затем можно sponge записать обратно в $HISTFILE.

Я использую этот код в .bash_profile:

remove_history_duplicates () {
    local i login_flag
    [ -z "$(history 1)" ] && login_flag=1 && history -r
    for i in $(history | awk '
        $1 ~ "[0-9]+" {
            id = $1
            $1=""
            if (uniq[$0]) {n++; print uniq[$0]}
            uniq[$0] = id
        }
        END {if (n) print "найдено",n,"дубликатов" > "/dev/stderr"}
    ' | sort -nr); do history -d $i; done

    if [[ -n $login_flag ]]; then
        [[ -n $i ]] && history -w && echo "история записана"
        history -c
    fi
}
remove_history_duplicates

Он не редактирует файл истории напрямую, а с помощью команд bash. Последняя повторяющаяся команда сохраняется, все предыдущие удаляются.

Если вы используете временные метки в истории (например, HISTTIMEFORMAT='%F %T '), замените строку $1="" строкой $1=""; $2=""; $3="" в коде awk.

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

Используя Perl

Сохранение первого появления дубликата (сравните с awk):

~$ perl -ne 'print unless $hash{$_}++' ~/.bash_history  > outfile

Сохранение последнего появления дубликата (сравните с awk):

~$ tac  ~/.bash_history | perl -ne 'print unless $hash{$_}++' | tac  > outfile

Используя Raku (ранее известный как Perl_6)

Сохранение первого появления дубликата (сравните с awk):

~$ raku -ne 'state %hash; .put unless %hash{$_}++' ~/.bash_history  > outfile

Сохранение последнего появления дубликата (сравните с awk):

~$ tac  ~/.bash_history | raku -ne 'state %hash; .put unless %hash{$_}++' | tac  > outfile

Используя Raku (ранее известный как Perl_6)

Сохранение первого появления дубликата (сравните с ruby):

~$ raku -e '.put for lines.unique;'  ~/.bash_history  > outfile

Сохранение последнего появления дубликата (сравните с ruby):

~$ tac  ~/.bash_history | raku -e '.put for lines.reverse.unique.reverse;' | tac  > outfile

https://stackoverflow.com/q/1444406/7270649
https://stackoverflow.com/a/32513573/7270649 https://unix.stackexchange.com/a/11941/227738
https://perldoc.perl.org
https://docs.raku.org

Я новичок в bash и не понимаю этих ответов, так как они плохо документированы. Вот как я решил эту проблему:

Проблема в том, что command-1 command-2 command-1 command-2 (… 25 раз больше …) the-command-i-actually-want.

Чтобы решить это, сначала я создаю скрипт Python, который очищает .bash_history:

# cleanup_bash_history.py

from pathlib import Path

hispath = (Path.home() / ".bash_history").resolve(strict=True)
history: dict[str, int] = {}

with hispath.open("r", encoding="utf-8") as hisfile:
    for linenum, line in enumerate(hisfile):
        line = line.strip()
        history[line] = linenum

with hispath.open("w", encoding="utf-8") as hisfile:
    for _, line in sorted(
        (linenum, line) for line, linenum in history.items()
    ):
        hisfile.write(f"{line}\n")

Этот скрипт предполагает, что .bash_history представляет собой просто список команд без временных меток.

Сначала все строки из .bash_history читаются в дикт, где строка является ключом, а номер строки значением. Таким образом, в конце, у меня есть наибольший номер строки для строки и ни одного дубликата строк.

Затем я создаю генератор из этого дикта, (linenum, line) for line, linenum in history.items(), который ведет себя как список кортежей (<linenum>, <line>). Я передаю это в sorted(), чтобы кортежи были отсортированы по номеру строки.

Затем я записываю строку из каждого кортежа в этом порядке обратно в .bash_history.

Далее, в .bashrc в конце, я добавляю:

PROMPT_COMMAND="history -a; python ~/Sys/scripts/cleanup_bash_history.py; history -c; history -r; $PROMPT_COMMAND"

Я скопировал это из этого ответа, это будет работать даже если открыто несколько окон bash, и общая история будет обновляться каждый раз, когда выполняется команда.

И это все, дубликатов больше нет.

Дополнение: если у вас возникают проблемы с элементами истории, записываемыми VS Code, удалите их вручную в скрипте Python, либо оберните команду bash в следующую конструкцию

if [ "$TERM_PROGRAM" != "vscode" ]; then
    ...
fi

Другие способы сделать это здесь также:
https://stackoverflow.com/questions/338285/prevent-duplicates-from-being-saved-in-bash-history/7449399#7449399

Отличный ответ. Если вы предпочитаете сохранить хронологический
порядок (вместо порядка ввода) для своих команд, измените функцию dedup(),
заменив awk '! x[$0]++' $@ на tac $@ | awk '! x[$0]++' | tac
trusktr

Мы можем устранить повторяющиеся строки, не сортируя файл, используя команду awk в следующем синтаксисе.

awk '!seen[$0]++' source.txt > target.txt

https://superuser.com/questions/722461/how-can-you-remove-duplicates-from-bash-history

Также
в nano используя регулярные выражения:

Нажмите Ctrl + \ Введите строку поиска ввод Введите
строку замены ввод Нажмите A для замены всех вхождений

в vim
:sort u

Если некоторые из предложений, включая приведенные выше, не работают сразу. После выполнения кода я делаю следующее:

history -c

чтобы сначала очистить историю, затем восстанавливаю версию без дубликатов поверх нее:

cp temp.txt ~/.bash_history

Ответ или решение

Для решения проблемы с дублирующимися записями в файле .bash_history необходимо использовать метод, который позволит удалять дубликаты, сохраняя при этом порядок команд. Важно отметить, что переменная HISTCONTROL=erasedups удаляет только последовательные дубликаты, то есть, если команда присутствует в истории несколько раз на разных строках, они сохранятся. Этот аспект нас, очевидно, не устраивает. Попробуем решить проблему с помощью нескольких инструментальных команд и скриптов.

Теория

В Unix-подобных системах файл .bash_history, как правило, хранит историю команд, которые пользователь вводил в командной оболочке bash. Проблема, описанная в запросе, определяется необходимостью удаления всех повторяющихся строк из этого файла, оставляя лишь их последнее вхождение, и при этом сохраняя порядок следования команд.

Стандартные Unix-утилиты, такие как sort и uniq, не подходят для этого сценария, поскольку они требуют предварительной сортировки, что приводит к потере изначального порядка команд. Однако, мы можем использовать инструменты, такие как awk и tac, которые позволяют работать с файлами построчно и обрабатывать их без нарушения порядка.

Пример

Рассмотрим пример на базе awk и других утилит. Представьте, что у вас есть следующая история команд:

ls
cd ~
ls
mkdir new_folder
cd new_folder
ls

Для удаления повторяющихся команд можно использовать следующую команду:

tac ~/.bash_history | awk '!seen[$0]++' | tac > ~/.bash_history.temp && mv ~/.bash_history.temp ~/.bash_history

Здесь tac реверсирует файл, чтобы сохранить последнее вхождение команды, awk '!seen[$0]++' удаляет дублирующиеся строки, и снова tac возвращает изначальный порядок. Это позволяет сохранять историю так, как она вводилась, но с убранными дубликатами.

Применение

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

# Добавьте следующую строку в ваш .bash_logout
tac ~/.bash_history | awk '!seen[$0]++' | tac > ~/.bash_history.temp && mv ~/.bash_history.temp ~/.bash_history

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

export PROMPT_COMMAND="history -a; history -w; tac $HISTFILE | awk '!seen[$0]++' | tac > $HISTFILE"

Это позволит перед сохранением команды в файл истории применять фильтрацию дублей.

Экстра кредит:

Перезаписывание файла .bash_history может быть потенциально рискованным, так как оно может возникнуть в процессе работы нескольких открытых терминалов. Однако, использование команды sponge из пакета moreutils поможет избежать проблем, связанных с несвоевременной обработкой входных данных:

tac ~/.bash_history | awk '!seen[$0]++' | tac | sponge ~/.bash_history

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

Заключение

Описание такого подхода позволит вам элегантно управлять историей команд в bash, чистить её от дублей, не теряя при этом важного для продуктивности порядка команд. Теперь в вашей истории останутся только уникальные команды с учетом их последнего появления, что упростит процесс поиска команд с помощью Ctrl+R и позволит быстрее находить нужную информацию.

Оцените материал
Добавить комментарий

Капча загружается...