LINUX.ORG.RU

Хочу говорить про Bash

 


2

5

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

Решил попробовать написать программку, которая удаляет дубликаты строк из .bash_history (там более 7000 строк у меня). Мне это показалось достойной задачкой для начинающего башиста, хотя и (может быть) довольно бесполезной. Кстати, есть такая программа shell-history-cleaner (кажется на расте написана, давно себе собрал, работает, но видимо заброшена автором), я пользуюсь, но попробовать свои силы надо было.

В результате мне удалось сделать задуманное, но хочется поговорить и о «неудачных» вариантах, которые может быть были бы более удачными, если бы я больше знал о bash и linux вообще, так что категорически приветствуется критика и подсказки более правильных решений или каких-нибудь хитростей командной строки.

Так как-то сложилось, что я упорно откладывал изучение массивов на потом, и когда наконец более-менее разобрался с индексированным массивом, то сразу приступил к делу, чтобы заодно закрепить прочитанное в книжках, поэтому первый «неудачный» вариант будет об этом.

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

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

file=$1
mapfile -t list < <(grep -v '^$' $file)
while ((${#list[*]})); do
    line=${list[-1]}
    unset list[-1\]
    for i in ${!list[*]}; do
        [[ $line == "${list[i]}" ]] &&
            unset list[i\]
    done
    final+=("$line")
done
printf '%s\n' "${final[@]}" | tac > $file

Для таких же любителей как я поясню идею кода. На его краткость отлично повлиял тот факт, что в истории командной строки надо сохранять последние уникальные строки, то есть работать надо с конца файла, а у индексированных массивов как раз есть стабильная ссылка на последний индекс: array[-1]. То есть не надо ничего переворачивать.

Как я понимаю, самое тугое место в скрипте — сравнение строк, и что-то более быстрых вариантов походу нет. Далее надо думать о многопоточности, но я пока до этого не дозрел.

Поскольку на данном этапе я застрял и вроде бы ясно, что тема с массивами как-то всё усложняет, то решил написать скриптик в стиле unix-way, и искренне полагал, что это будет правильным решением задачки, ведь утилиты все написаны суровыми дядьками на Си, и там всё без дураков — быстро и надёжно. Пришла идея отбросить всю эту возню с поиском и удалением, а просто взять всё — и отфильтровать.

file=$1
list=$(tac $file | grep -v '^$')
while [[ $list ]]; do
    read -r line <<< $list
    list=$(grep -Fxv "$line" <<< $list)
    echo "$line"
done | tac > $file

Чтобы уважаемый лорчанин не хельпал ключи -F -x, напомню: F — читает regex буквально, а x — помещает выражение между ^$, иначе пришлось бы использовать ключ -P (perlre) и выражение выглядело бы как говно: "^\Q$line\E$", а работало бы ещё медленней, возможно, но это не точно. (кстати, я был весьма разочарован узнать, что с sed такое вообще не провернуть — никак не заставить подстановку читать буквально).

Итак, на короткой дистанции (разумеется, у меня был короткий вариант файла для тестов) этот скрипт почти в два раза обогнал предыдущий, но — что было для меня полнейшим разочарованием! — он совершенно заткнулся даже на средней дистанции (нагружая процессор на четверть, но размазано по потокам), то есть я его тупо прервал на какой-то там 10-ой минуте, так что о проверке на полном файле речи уже не шло. Как это понимать — не знаю, поясните. 1000 строк он смолол где-то за секунду с небольшим, а 3000 — уже застрял.

Ладно, пока не было новых идей, прочитал главу про ассоциативные массивы и сразу почуял, что это походу то, что мне надо, но я не ожидал, что настолько! Свойство хеша не дублировать индексы как будто специально создано для решения этой задачки. Быстро стало понятно, что надо просто переложить строки из обыкновенного массива в индексы хеша, а в значения хеша — номера индексов строк из обыкновенного массива.

file=$1
mapfile -t list < <(grep -v '^$' $file)
declare -A hash
for i in ${!list[*]}; do
    hash[${list[i]}]=$i
done
for i in "${!hash[@]}"; do
    final[${hash[$i]}]="$i"
done
printf '%s\n' "${final[@]}" > $file

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

Получается на bash таки можно что-то программировать и оно может работать быстро.


UPD

Наконец подсказали, что во втором скрипте у меня ошибка. Спасибо @mky: Хочу говорить про Bash (комментарий)
Теперь этот скрипт переваривает тот же файл за 15-16 секунд!

file=$1
list=$(tac $file | grep -v '^$')
while [[ $list ]]; do
	IFS=$'\n' read -r line <<< $list
	list=$(grep -Fxv -- "$line" <<< $list)
# 	list=$(grep -Pv "^\Q${line}\E$" <<< $list)
	echo "$line"
done | tac > $file
★★★★★

Последнее исправление: papin-aziat (всего исправлений: 5)

программку, которая удаляет дубликаты строк из .bash_history (там более 7000 строк у меня)

Не писать дубли в историю, настраивается в самом bash.


Установи себе zsh со всеми популярными модулями (Oh My Zsh) и получишь удобный шелл, который уже настроили за тебя и который к тому же не мешает писать и выполнять скрипты на bash.
В качестве вводной, статья Настраиваем и используем ZSH вместо Bash, или превращаем терминал в раскраску.

krasnh ★★★★
()
Последнее исправление: krasnh (всего исправлений: 2)
Ответ на: комментарий от krasnh

Не писать дубли в историю, настраивается в самом bash.

Это интересная, но какая-то запутанная тема — то пишет, то не пишет… Однако разговор не об этом, фильтровать можно не только этот файл.

Установи себе zsh

Не нужно.

papin-aziat ★★★★★
() автор топика

пока не было новых идей, прочитал главу про ассоциативные массивы

И ты ни разу не обратился к ChatGPT? Так мы Скайнет не построим. (


p.s. Надеюсь, про сервис проверки скриптов на ошибки shellcheck.net ты знаешь и сам.

krasnh ★★★★
()
Последнее исправление: krasnh (всего исправлений: 1)
Ответ на: комментарий от krasnh

И ты ни разу не обратился к ChatGPT?

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

Надеюсь, про сервис проверки скриптов на ошибки shellcheck.net ты знаешь и сам

Не знаю и не нужно. Стараюсь писать без ошибок 🤣

Так мы Скайнет не построим.

Какой там… Мне удалось таки раздобыть две приличные книжки и один маленький справочник, на том и стоим. А скайнеты ваши будут совсем про другое…

papin-aziat ★★★★★
() автор топика

Если не поленятся, то сейчас придут и объяснят, что bash — нинужен, скрипты писать только на sh, скорость для скриптов не важна.

менее двух десятых секунды

поздравляю, но это ухищрение:

Свойство хеша не дублировать индексы

Которое раньше (условно в версии 4.0) могло бы наоборот замедлить. ЕМНИП, когда в bash только добавили ассоциативные массивы, они были медленными.

Так что, если завтра, после обновления, вы получите, что ваш мегаоптимизированый скрипт сливает по скорости первому варианту «в лоб», то ничего удивительного. Поэтому и разбирать почему на маленьких файлах первый скрип сливает второму, а на больших наооборот неинтерестно. Каждое действие в bash вызывает такую кучу действий процессора, что страшно. А завтра кто-нибудь придёт и что-нибудь поправит в коде bash...

mky ★★★★★
()
Ответ на: комментарий от mky

поздравляю, но это ухищрение

Спасибо. У меня тоже возникло ощущение, что похоже на какой-то лайвхак, но в книжке «Идиомы Bash» авторы говорят, что хеши как раз удобный инструмент для «уникализации», подсчета и чего-то типа того, так что типа штатная тема, так и задумано.

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

Которое раньше (условно в версии 4.0) могло бы наоборот замедлить. ЕМНИП, когда в bash только добавили ассоциативные массивы, они были медленными.

Да, что-то читал про недопиленность темы тогда, но вот сейчас у меня bash-4.4 и, кстати, книжки тоже о 4+, — всё выглядит допиленным. Я ж специально взял историю шелла, ибо наверное трудно найти более всратый для манипуляций текст, ведь там всё — и групповые символы, и регулярки, да тьма просто, чего только нет.

Поэтому и разбирать почему на маленьких файлах первый скрип сливает второму, а на больших наооборот неинтерестно.

На мой поверхностный взгляд кажется понятным, что сравнение строк в [[ работает ровнее, чем в grep, хотя в последнем случае может быть ещё постоянная перезапись переменной добавляет.

Однако я пока подозреваю проверку, так как помню, что когда в одном случае взял пару тыщь строк с конца, то скрипт в стиле «юникс-вей» бодро отработал (там немного длинных срок), а потом взял столько же из середины, где было много длиннющих строк с yt-dlp (помянем!), то он резко просел, тогда как скрипт с индексированным массивом движется довольно ровно.

papin-aziat ★★★★★
() автор топика
Ответ на: комментарий от mky

Которое раньше (условно в версии 4.0) могло бы наоборот замедлить.

Да, забыл, как такое возможно? Даже если допустить, что он работает в 10 раз медленнее, то это всё равно быстро (даже в 100 раз — всё равно будет быстрее), простое копирование туда и назад, всё, ведь в данном случае нет никаких проверок с жуткой математической прогрессией, которой название я не знаю и как много получается не понимаю 😁

papin-aziat ★★★★★
() автор топика
Последнее исправление: papin-aziat (всего исправлений: 1)

пора узнать про алгоритмическую сложность

Как я понимаю, самое тугое место в скрипте — сравнение строк

или индексация в массиве, которая может быть O(n), а не O(1)

второй вариант – страх и ужас (чтение медленным read и форки в цикле)

Получается на bash таки можно что-то программировать и оно может работать быстро.

после выявления на ощупь медленных конструкций

если не получается написать скрипт на sh, то пора менять инструментарий. bash нинужен, хотя скорость скриптов не важна

anonymous
()
Ответ на: комментарий от anonymous

или индексация в массиве, которая может быть O(n), а не O(1)

А можно на литературном языке? И какие проблемы с индексацией, ведь ничего не передвигается в массивах — в первом удаляются индексы, а ко второму просто добавляются в конец?

Кстати, я всякое пробовал и вроде вариант с array[i]= вместо unset тоже, но разницы не заметил, хотя тогда либо приходилось оставить проверку пустых переменных, либо отсеивать if-ом, но первые «версии» мне быстро показали, что надо избегать лишних (хотя может и красивых) действий внутри цикла на тысячи итераций 😊

второй вариант – страх и ужас (чтение медленным read и форки в цикле)

read там берёт по одной строчке и отваливает, что он может замедлять? Про форки не понял.

papin-aziat ★★★★★
() автор топика
Ответ на: комментарий от papin-aziat

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

где было много длиннющих строк

Ну, here doс и here string — это ведь создание временного файла.

Ну, можете ещё скорость такого кода посмотреть:

file=$1
mapfile -t list < <(grep -v '^$' $file)
N=${#list[*]}
for ((i=N-1;i>0;i--)); do
    l=${list[i]}
    [ -z "$l" ] && continue
    for ((j=0;j<i;j++)); do
        [ "$l" = "${list[j]}" ] && list[j]=
    done
done
for ((i=0;i<N;i++)); do
    [ -n "${list[j]}" ] && printf '%s\n'
done

mky ★★★★★
()
Последнее исправление: mky (всего исправлений: 1)
Ответ на: комментарий от papin-aziat

А можно на литературном языке?

просматривается весь массив в поисках произвольного индекса

read там берёт по одной строчке и отваливает

read читает строчку посимвольно, а не сразу шматок байтов

Про форки не понял.

запуск $(prog …) неэффективен и антипаттерн. погугли

anonymous
()
Ответ на: комментарий от papin-aziat

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

Похожий опыт. Когда я только начинал его просить сгенерировать мне промпты для нейросетей, он неплохо справлялся. А потом стал отвечать односложно и невнятно.

tiinn ★★★★★
()

у меня там более 30.000 строк, дубликаты удаляются, мало я через sed снес все длинное… и зочем?

in_=open("")# впиши путь
seen=set()
with open("без дубликатов", "w") as out:
  for line in in_:
    _, cmd = line.split(" ", 1)
    if cmd not in seen:
      out.write(line)
      seen.add(cmd)

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

rtxtxtrx ★★
()
Последнее исправление: rtxtxtrx (всего исправлений: 1)

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

Такое намного проще на перле писать. И читать кстати тоже. В баше просто нет структур данных нормальных.

bread
()
Ответ на: комментарий от ox55ff

Лучше бы на изучение питона это время потратил

Так можно и совсем в программизм удариться (а потом стать геем). Для скриптов перл самое то если нужна какая-то обработка данных и от баша уже больно.

bread
()
Ответ на: комментарий от mky

Ладно уговорил, чистый баш

# читаем stdin
readarray -t input
declare -a output
declare -A exists

# реверс input
for (( i=${#input[*]}-1; i>=0; i-- )); do
    l="${input[$i]}"
    case "$l" in
        '') continue ;; # пропуск пустых строк
        ' '*) continue ;; # пропуск начинающихся с пробела
    esac
    [[ -v exists["$l"] ]] && continue
    exists["$l"]=1
    output+=("$l")
done

#реверс output
for (( i=${#output[*]}-1; i>=0; i-- )); do
    echo "${output[$i]}"
done
anonymous
()
Ответ на: комментарий от dataman

Каешна!

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

local lookup  = { }
for line in io.lines(assert(os.getenv('HOME'))..'/.bash_history') do
    if not lookup[line] then
       lookup[line]=true
       print(line)
    end
end

И всё :)

LINUX-ORG-RU ★★★★★
()

Решил попробовать написать программку, которая удаляет дубликаты строк из .bash_history (там более 7000 строк у меня). Мне это показалось достойной задачкой для начинающего башиста

Ага, самое то

HISTCONTROL=ignoredups:erasedups

Вот более полный фрагмент моих башовых предпочтений: https://github.com/annulen/misc-scripts/blob/master/bashrc

annulen ★★★★★
()
Ответ на: комментарий от ox55ff

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

Если такая аллергия на баш - лучше уж писать скрипты на с++, как бы извращённо это ни звучало.

anonymous
()

Ты крут.

Получается на bash таки можно что-то программировать и оно может работать быстро.

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

thesis ★★★★★
()

Получается на bash таки можно что-то программировать и оно может работать быстро.

Можно то можно, даже на CodeWars задачки есть, но писать на bash что-то хоть чуть-чуть сложное - это боль. Потому многие, после изучения основ, быстро переключаются на языки общего назначения, в основном на Python.

QsUPt7S ★★
()