LINUX.ORG.RU

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

 


2

4

Для себя уже навалял кучку полезных скриптиков и походу изучения 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

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

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

UPD2

Спасибо анону, подкинул идею отфильтровать хешем в один проход: Хочу говорить про Bash (комментарий)

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

UPD3

Продолжаю благодарить анона, что замотивировал таки раскурить sort. Итак, самый шустрый вариант:

file=$1
list=$(< $file \
    grep -vn '^$' |
    tac |
    sort -t: -k2 -u |
    sort -t: -k1,1n |
    cut -d: -f2-)
echo "$list" > $file

На моём компе меньше трёх сотых секунды!

★★★★★

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

Вроде как, да, pipe должен быть бысрее, особенно, если временные файлы не на tmpfs.

У меня как раз не в tmpfs.

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

для read конвеер не получится, так что только для grep. Пару раз дёрнул — разница в пользу конвеера где-то 50-60 тысячных секунды. Вряд ли стоит внимания.

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

необходимость бережно пердолиться со строками, помня о разделителях

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

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

То есть для bash быстрее развернуть {0..1000000} в большую строку (кучу слов) и «бежать» по ней, чем делать инкремент и арифметическое сравнение переменной i.

Ого, проверил, прикольно! Что ж, это ещё раз доказывает, что bash — это прежде всего про строки, по сути единственный вид данных.

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

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

$ cat test
         j

        ii
         j

       hhh
        ii
      gggg

       hhh
     fffff
      gggg

    eeeeee
     fffff
   ddddddd

    eeeeee
  cccccccc
   ddddddd

 bbbbbbbbb
  cccccccc
aaaaaaaaaa

 bbbbbbbbb
aaaaaaaaaa

Убираем пробелы, добавляем номера строк, сортируем по второму полю

$ grep -vn '^$' test | sort -t':' -k2
1:         j
4:         j
3:        ii
7:        ii
10:       hhh
6:       hhh
12:      gggg
8:      gggg
11:     fffff
15:     fffff
14:    eeeeee
18:    eeeeee
16:   ddddddd
20:   ddddddd
19:  cccccccc
23:  cccccccc
22: bbbbbbbbb
26: bbbbbbbbb
24:aaaaaaaaaa
27:aaaaaaaaaa

Неудача! Он сортирует учитывая ведущие пробелы! Добавляем ключ -b

$ grep -vn '^$' test | sort -t':' -k2b
24:aaaaaaaaaa
27:aaaaaaaaaa
22: bbbbbbbbb
26: bbbbbbbbb
19:  cccccccc
23:  cccccccc
16:   ddddddd
20:   ddddddd
14:    eeeeee
18:    eeeeee
11:     fffff
15:     fffff
12:      gggg
8:      gggg
10:       hhh
6:       hhh
3:        ii
7:        ii
1:         j
4:         j

Лучше, но есть проблема с номерами строк — ключом -u или uniq будут оставлены первые уникальные строки. Кстати, это не проблема, но надо следовать задуманному! Теперь придётся ещё отсортировать по первому полю в обратном порядке

$ grep -vn '^$' test | sort -t':' -k2b -k1,1r
27:aaaaaaaaaa
24:aaaaaaaaaa
26: bbbbbbbbb
22: bbbbbbbbb
23:  cccccccc
19:  cccccccc
20:   ddddddd
16:   ddddddd
18:    eeeeee
14:    eeeeee
15:     fffff
11:     fffff
8:      gggg
12:      gggg
6:       hhh
10:       hhh
7:        ii
3:        ii
4:         j
1:         j

Да блин, лексикографический порядок! Добавляем ключ -n

$ grep -vn '^$' test | sort -t':' -k2b -k1,1rn
27:aaaaaaaaaa
24:aaaaaaaaaa
26: bbbbbbbbb
22: bbbbbbbbb
23:  cccccccc
19:  cccccccc
20:   ddddddd
16:   ddddddd
18:    eeeeee
14:    eeeeee
15:     fffff
11:     fffff
12:      gggg
8:      gggg
10:       hhh
6:       hhh
7:        ii
3:        ii
4:         j
1:         j

Есть же! Вот теперь можно удалять дубликаты, но нужен ещё конвеер, увы

$ grep -vn '^$' test | sort -t':' -k2b -k1,1rn | sort -t':' -k2b -u
27:aaaaaaaaaa
26: bbbbbbbbb
23:  cccccccc
20:   ddddddd
18:    eeeeee
15:     fffff
12:      gggg
10:       hhh
7:        ii
4:         j

Переворачиваем поле с номерами строк (ещё конвеер, мда…)

$ grep -vn '^$' test | sort -t':' -k2b -k1,1rn | sort -t':' -k2b -u | sort -t':' -k1,1
10:       hhh
12:      gggg
15:     fffff
18:    eeeeee
20:   ddddddd
23:  cccccccc
26: bbbbbbbbb
27:aaaaaaaaaa
4:         j
7:        ii

Да блииин! Лексикографический, сцуко! Не забывай ключ -n!

$ grep -vn '^$' test | sort -t':' -k2b -k1,1rn | sort -t':' -k2b -u | sort -k1,1n
4:         j
7:        ii
10:       hhh
12:      gggg
15:     fffff
18:    eeeeee
20:   ddddddd
23:  cccccccc
26: bbbbbbbbb
27:aaaaaaaaaa

Фух, теперь можно убрать номера строк

$ grep -vn '^$' test | sort -t':' -k2b -k1,1rn | sort -t':' -k2b -u | sort -t':' -k1,1n | sed -E 's/^[0-9]+://'
         j
        ii
       hhh
      gggg
     fffff
    eeeeee
   ddddddd
  cccccccc
 bbbbbbbbb
aaaaaaaaaa

Ну, вот так вроде должно работать нормально.

Вот он, кровавый Unix Way!

file=$1
list=$(grep -vn '^$' $file |
       sort -t':' -k2b -k1,1rn |
       sort -t':' -k2b -u |
       sort -t':' -k1,1n |
       sed -E 's/^[0-9]+://')
echo "$list" > $file

С виду работает как надо, как ещё точнее проверить уже не знаю 😀

UPD А нет, чё-то поломалось

[me bash_playground]$ cp history_full history
[me bash_playground]$ bash foobar history
[me bash_playground]$ diff <(sort -u history_full) <(sort history)
1d0
< 
5,6d3
<           some
<          some
9,10d5
<      text
<    text
4042d4036
< text
papin-aziat ★★★★★
() автор топика
Последнее исправление: papin-aziat (всего исправлений: 1)
Ответ на: комментарий от papin-aziat

Идеи? Вы там прямо задавали вопрос, что не так с сортировкой. Я вам сначала намекнул, что строки будут не в том порядке, потом прямо написал, что sort по k1 должен быть numeric. Что касается пробелов в начале строки, то в моей bash_history их нет, специально не сохраняются команды, начинающиеся с пробела, чтобы можно было вводить что-то «типа секретное».

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

Вы там прямо задавали вопрос, что не так с сортировкой. Я вам сначала намекнул…

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

Что касается пробелов в начале строки, то в моей bash_history их нет, специально не сохраняются команды, начинающиеся с пробела, чтобы можно было вводить что-то «типа секретное».

Кстати, надо проверить включена ли у меня эта функция и отключить её, мне не нужно 😊

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

Не совсем понял, на каком объёме данных получены 50-60 тысячных, там же 10 секунд время выполнения. Здесь вот https://unix.stackexchange.com/questions/219804/resource-usage-using-pipe-and... измеряли, получили, что сильно зависит от объёма, на коротких строках here string быстрее pipe, а на больших объёмах наоборот. У вас там как раз было затыкание с ростом объёма, поэтом про here string и написал.

Вобще, скорость записи во временный файл измерить сложно, так как sync() там не вызывается, то есть запись буферезируемая. Но сколько в данный момент у системы свободных буферов записи и не захочет ли система начать их освобождать сказать сложно.

А bash 5 добавил настройку, превращающую короткую here string в pipe, а не временный файл. И это ещё усложняет сравнение, что быстрее.

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

На коротком, 1000 строк, больше если дать — этот вариант скипта совсем долго работает, не дождёшься.

там же 10 секунд время выполнения

Не понял, наверное я сократил файл. 1000 строк что-то там, емнип, ~1.480 сек. с пайпом и ~1.430 со стрингом. Комп не занят ничем, но я на всякий случай ещё раз прогнал и результат примерно тот же. Такой вот я тестировщик 🤣

на коротких строках here string быстрее pipe, а на больших объёмах наоборот

Наверное речь идёт о каких-то очень длинных сроках, а в моём bash _history что там… эээ… наверное они считаются короткими для таких тестов, вот и получается, что here_string даже чутка быстрее.

А bash 5 добавил настройку, превращающую короткую here string в pipe, а не временный файл. И это ещё усложняет сравнение, что быстрее.

А какие-нибудь новые приятные удобства появились в пятом? Типа как в 4.4 (или где-то рядом) появился array[-1] вместо array[${#array[@]}-1] (в книжке прочитал).

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

Ну, особо не заметил. Запомнил, что появилась переменная $EPOCHSECONDS, но у меня в скриптах время через printf, как-бы без разницы.

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

Кстати, ваш скрипт с read/grep ломается, если встречается строка с пробелом в конце, так как баш обрезает последний сепаратор. Пример 47 https://mywiki.wooledge.org/BashPitfalls . А если у строки обрезали пробел в конце, то grep её не уберёт из $list, так как ″-x″ и скрип зациклит. Так что, возможно, у вас там такая строка встречалась где-то посередине, поэтому скрипт и не отрабатывал за любое время. Как-бы надо добавить перед while указание, что только новая строка является сепаратором: IFS=$'\n'. И заодно почитать Pitfalls по поводу того, как сложно сохранять/восстанавливть $IFS, если она может быть unset на входе в скрипт.

Ну, и опцию ″-e″ или ″--″ добавить бы в grep, иначе если $line будет начинаться с ″-″, будет неправильное поведение.

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

ваш скрипт с read/grep ломается, если встречается строка с пробелом в конце, так как баш обрезает последний сепаратор.

Точняк, оно! Теперь перемалывает полный файл за

real	0m15.692s
user	0m12.219s
sys	0m5.988s

Круто! Значит всё-таки ошибка, отлично. А начинающихся с - нет в файле. Мда… безобразие вообще-то, я рассчитывал, что опция -F как-то справляется с такими проблемами.

Вообще, этот read мутный какой-то, с ним ещё придётся разобраться.

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

если $line будет начинаться с ″-″, будет неправильное поведение.

Поменял на perlre — заработало! Натолкал в тестовый файл всяких вариантов с - в начале строк, всё пучком.

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

Как-бы надо добавить перед while указание, что только новая строка является сепаратором: IFS=$‘\n’. И заодно почитать Pitfalls по поводу того, как сложно сохранять/восстанавливть $IFS, если она может быть unset на входе в скрипт.

Я вроде для себя запомнил, что если IFS=‘бла-бла’ использовать «локально», то есть прямо перед командой, то и распространяться действие будет только на эту команду, и пока на этом остановился.

https://mywiki.wooledge.org/BashPitfalls

Ага, этот сайт мне уже подкинули, в закладках, надеюсь хватит энтузиазма его прочитать.

Ну, и опцию ″-e″ или ″–″ добавить бы в grep

-- — если правильно помню, то это говорит программе, что опции закончились и дальше идут аргументы. Стало быть это универсальный вариант, если обрабатываемые строки или файлы начинаются с -? Про -e почитал: This option can be used to protect a beginning with “-”., однако в конструкции grep -Fx -e "$line" не работает.

В результате два рабочих варианта:

list=$(grep -Fxv -- "$line" <<< $list)
list=$(grep -Pv "^\Q${line}\E$" <<< $list)

Квотирование, конечно, рулит в плане простоты применения, но вот в sed такого нет, как я понял.

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

IFS=‘бла-бла’ использовать «локально»

А как-же скорость кода? Лишнее присваивание внутри цикла :)

У моего grep опция -e работает. А вы привели пример не рабочей конструкции без -v, может в этом дело.

Касательно perlre, вы проверяли, если $line будет содержать \E?

Стало быть это универсальный вариант,

Для тех программ, которые понимают два минуса. echo, например, не понимает :)

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

Но чтобы понять реализовать то, чего ты хочешь, придётся как-то формулизовать, чем «фуфло» отличается от «не фуфла».

Не запоминать в историю команды, вернувшие errno!=0

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

Не запоминать в историю команды, вернувшие errno!=0

На мой взгляд, довольно бесполезно. Ну вот например, сделал ты опечатку, что в этом случае делаешь? Нажимаешь вверх, чтоб из истории достать эту команду, потом исправляешь? Ну вот а так не получится. Плюс иногда errno!=0 — это желаемый результат. При grep’е, например, если надо убедиться, что чего-то нет. К тому же в этом случае тоже придётся вносить команду в историю не при её вводе, а при её завершении (а это может быть хоть через неделю) — та же проблема, что и с проверкой вывода на «command not found».

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

Оффтоп.

nushell

В целом, хорош! Но…

  • собирается долго, почти как файрфокс (раст!);
  • весит много;
  • многословен;
  • многопробелен, от слова «пробел» (клавиша), в неожиданных местах;
  • слишком информативен при ошибках (синтаксиса), забивает экран ненужным шумом (как отключить?).
anonymous
()
Ответ на: комментарий от papin-aziat

Вот он, кровавый Unix Way!

Охереть, три сортировки, вызов sed. И все равно криво работает. Быстрее было бы свой алгоритм закодить хоть даже и на баше, чем подбирать ключи к sort.

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

Касательно perlre, вы проверяли, если $line будет содержать \E?

Ну ты следователь! Добавил, не работает, да… да что ж такое-то!

У моего grep опция -e работает. А вы привели пример не рабочей конструкции без -v, может в этом дело.

Да, всё верно, опечатался, работает и так и эдак, но! Оба варианта теперь ломают результирующий файл, склеивают две строки из тестового исходного в одну, такую

$ diff <(sort -u test) <(sort history)
37a38
> dnf search gtk+ | grep ^gtk -- dnf search gtk+

Варианты с массивами на всю эту фигню проверять надо?

А как-же скорость кода? Лишнее присваивание внутри цикла :)

А… шуткуешь над стариком, одобряю! Не до жиру ведь, тут вона чё…

Для тех программ, которые понимают два минуса. echo, например, не понимает :)

А я думал, что это фича функции getops или типа того.

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

Ну ты следователь! Добавил, не работает, да… да что ж такое-то!

Пропал Калабуховский дом! Вы вкусили алгоритмов, структур данных и понеслася. Теперь вы в шаге от того, чтобы начать проверки писать. Потому как это проще для отладки.

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

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

Я правильно тебя понял, что надо себе завести тестовый файлик, собрать туда всё возможное дерьмо и прежде чем заявлять, что я написал гениальный код, таки проверить?

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

Пропал Калабуховский дом!

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

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

Оффтоп.

lines --skip-empty

Не работает, не убирает пустые строки!

Потому что файл открывается как байтовый стрим! Даже с раширением «.txt». Если имеет расширение «.json», то без дополнительных ключей парсит как json. При этом open имеет ключ --raw, который открывает как байтовый стрим.

Почему обидели текстовые файлы? Приходится дополнительно decodeировать.

> open t.txt | describe 
byte stream
> open t.json | describe 
record
> open --raw t.json | describe 
byte stream
> open t.txt | decode | describe 
string
> open t.txt | lines --skip-empty 
╭───┬───╮
│ 0 │   │
│ 1 │ a │
│ 2 │   │
│ 3 │ b │
│ 4 │   │
╰───┴───╯
> open t.txt | decode | lines --skip-empty 
╭───┬───╮
│ 0 │ a │
│ 1 │ b │
╰───┴───╯
anonymous
()
Ответ на: комментарий от papin-aziat

Да. Заодно помогает в ситуациях типа одну ошибку поправил, две другие внёс, на каждый чих нужно заново всё перепроверять. А тут можно быть уверенным, что ты ничего не испортил. Есть какая-то стабильная база, от которой можно плясать.

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

Не знаю, почему строки склеиваются, нужно исходный файл смотреть, в том числе и в hex, может там \r в конце.

С массивами засад меньше, там же нет вызова внешних команд.

Да, double dash фича getopt(), но ведь можно писать код и без неё. Особенно всякие скрипты. То есть, если использовать стандартные команды (rm, grep и т.д.), то два минуса работают. А если всякие спецтулзы, типа чтение датчика по i2c или 1wire, то там, запросто, два минуса не работает.

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

Анон, кажется я разобрался с проблемой в этом коде:

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

Классная идея, всё должно идти хорошо, но вот эта конструкция всё ломает:

[[ -v exists["$l"] ]]

в выхлопе ругань типа bad array subscript и даже знакомые мне вещи, которые явно указывают на подстановки.

Лечение оказалось простым:

[[ -v exists[\$l] ]]

Как я понял, от команды [[ -v надо спрятать переменную, иначе сначала она подставит, а потом результат (если там переменная) ещё раз подставит конструкция array[$var].

$ l=\$foo
$ foo=bar
$ declare -A hash
$ hash=([\$foo]=1)
$ [[ -v hash[$l] ]] && echo true || echo false
false
$ eval echo $l
bar
$ eval echo \$l
$foo
$ [[ -v hash[\$l] ]] && echo true
true

Это было интересно! Но, пожалуй, проще было проверить наличие строки

[[ ${exists[$l]} ]] && continue

или число (у тебя там как раз единица)

(( ${exists[$l]} )) && continue

у этих вариантов нет двойной подстановки.

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

Если это не академический интерес, bash ради bash, то я бы создал эталонный файл истории, только с действительно нужными, часто используемыми командами, чтобы они автодополнялись, и время от времени им замещал бы оригинал. Потому что вот эти все cd, pushd, cp, mv и прочие clear там не нужны. Но я bash не пользуюсь, просто по аналогии.

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

вот эта конструкция всё ломает: [[ -v exists["$l"] ]]

У меня всё работает. Какая версия баш?

$ bash --version
GNU bash, version 5.2.37(1)-release (x86_64-pc-linux-gnu)

Это было интересно! Но, пожалуй, проще было проверить наличие строки [[ ${exists[$l]} ]] && continue

Можно еще попробовать более универсальный вариант

       ${parameter:+word}
              Use Alternate Value.  If parameter is null or unset, nothing is substituted, other‐
              wise the expansion of word is substituted.

Можно проверять на существание ключа даже с пустым значением

[[ ${exists[$l]:+exists} ]] && continue

«Антибаш» с сортировами хорош тем, что не сжирает ОЗУ. sort использует внешнюю сортировку и пишет промежуточные таблицы на диск в $TMP

Вот рабочий вариант (вроде)

$ cat antibash.sh 
#!/bin/sh

grep -vE '^$|^ ' | \
    nl -w1 -s: | \
    tac | \
    sort -t: -k2 -u | \
    sort -t: -k1,1 -n | \
    cut -d: -f2-

$ dd if=/dev/urandom bs=64k count=10000 status=none| base64 -w32 | time sh antibash.sh > /dev/null
69.76user 5.88system 1:08.02elapsed 111%CPU (0avgtext+0avgdata 10804maxresident)k
0inputs+0outputs (0major+7457minor)pagefaults 0swaps

10 мегабайт

Другие варианты (читый баш, awk) уходят в своп, если повезет выбивает oom-киллер

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

И как их объединить в один идеальный?

Что трудно запомнить, но это возможно пригодится и что часто используется, но лень вводить. На самом деле, это короткий список. Я вот долго не мог запомнить pacman -Qqdt, сохранил и все. С другой стороны одно время часто использовал блок кода и решил его тоже оставить в виде команды, а не скрипта или функции. Блок выглядит так:

& {
    несколько
    строк
    кода
}
dmitry237 ★★★★
()
Ответ на: комментарий от dmitry237
  • история - это хронологическая запись (прошлого), чтобы потом помотреть, возможно, повторить простым копированием и редактированием последовательности команд.

  • часто используемые - это часто используемые, обычно сортируются по числу использований.

  • нужные - это команды, которые скоро (в будущем) понадобятся.

Как совместить прошлое и будущее в идеальном настоящем?

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

Как совместить прошлое и будущее в идеальном настоящем?

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

Не понял зачем сортировать? Если в консоли вводишь начало, а потом выбираешь нужное продолжение, а-ля fzf

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

Не помню, не знаю, что вводил я или кто-то другой раньше

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

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

Есть разные списки … Их не надо смешивать

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

function Compact-History
{
    [Alias('ch')]
    param ( [Parameter()] [switch] $Optimize )

    $histfile = (Get-PSReadLineOption).HistorySavePath

    if ($Optimize)
    {
        Copy-Item $HOME/.local/share/powershell/PSReadLine/Base.txt $histfile
        return
    }

    $history  = [Microsoft.PowerShell.PSConsoleReadLine]::GetHistoryItems()
    $check = @($history[0])
    $null > $histfile

    for ($i = 0; $i -lt $history.Count; $i++)
    {
        if (-not ($history[$i].Commandline -in $check.Commandline))
        {
            $check += $history[$i]
            [Microsoft.PowerShell.PSConsoleReadLine]::AddToHistory($history[$i].Commandline)
        }
    }
}

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

Запускается:

ch
# или
ch -o
dmitry237 ★★★★
()
Ответ на: комментарий от dmitry237

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

Можно просто чистить историю скриптом, к чему ТС и подбирается. Не просто удалять дубликаты, а ещё и мусор по регекспам выносить.

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

а ещё и мусор по регекспам выносить.

Это он удаляет пустые строки или строки с одним пробелом (кстати почему их не может быть больше?). Странно, что они вообще попадают в историю, у меня такого нет.

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

Потому что вот эти все cd, pushd, cp, mv и прочие clear там не нужны.

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

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

Это он удаляет пустые строки или строки с одним пробелом (кстати почему их не может быть больше?).

$ printf "0\n 1\n  2\n   3\n"
0
 1
  2
   3

$ printf "0\n 1\n  2\n   3\n" | grep -vE '^ '
0

Странно, что они вообще попадают в историю, у меня такого нет.

До настройки истории туда попадает много чего.

anonymous
()