zsh ignores SAVEHIST size - zsh

Today I made the switch from bash to zsh (+ oh-my-zsh) on my Mac. In bash I kept an .bash_eternal_history with over 30k entries. I migrated that into the .zsh_history file and format.
Now I set these vars to keep an eternal history in zsh (taken from [1]):
HISTFILE="$HOME/.zsh_history"
HISTSIZE=10000000
SAVEHIST=10000000
setopt BANG_HIST # Treat the '!' character specially during expansion.
setopt EXTENDED_HISTORY # Write the history file in the ":start:elapsed;command" format.
setopt INC_APPEND_HISTORY # Write to the history file immediately, not when the shell exits.
setopt SHARE_HISTORY # Share history between all sessions.
setopt HIST_EXPIRE_DUPS_FIRST # Expire duplicate entries first when trimming history.
setopt HIST_IGNORE_DUPS # Don't record an entry that was just recorded again.
setopt HIST_IGNORE_ALL_DUPS # Delete old recorded entry if new entry is a duplicate.
setopt HIST_FIND_NO_DUPS # Do not display a line previously found.
setopt HIST_IGNORE_SPACE # Don't record an entry starting with a space.
setopt HIST_SAVE_NO_DUPS # Don't write duplicate entries in the history file.
setopt HIST_REDUCE_BLANKS # Remove superfluous blanks before recording entry.
setopt HIST_VERIFY # Don't execute immediately upon history expansion.
setopt HIST_BEEP # Beep when accessing nonexistent history.
[1] https://unix.stackexchange.com/a/273863
But the number in SAVEHIST get's ignored. No matter how high I set it, when I type echo $SAVEHIST in my shell, I get 10,000 and my .zsh_history file with over 30k entries gets capped at 10,000
What am I missing?

add $SAVEHIST and other hist parameters at the end of .zshrc.
(some oh-my-zsh bug)
it helps https://www.reddit.com/r/zsh/comments/f29hn4/omz_is_this_a_bug_in_historyzsh/fhf822j

I moved this section from .zshrc to ~/opts/args.sh, and in .zshrc I added the line source ~/opts/args.sh. That solved it

Related

How does this snippet of zsh code keep track of time? Doesn't seem to have date or anything

I came across this snippet of code in the prezto repo, and im really stumped.
It appears to rebuild zsh comp every 20 hours, but I can't figure out how.
(Nm-20) seems to be calling into a subshell, but Nm-20 itself is not a program.
($BLAH/zcompdump) should be a file, but it shouldn't be executable, so why is it surrounded by ()?
Lastly, if (( $#_comp_files )) seems to be that the outer() is a test to see if ( $#_comp_files ) is true or not, but what is $#_comp_files?
# Load and initialize the completion system ignoring insecure directories with a
# cache time of 20 hours, so it should almost always regenerate the first time a
# shell is opened each day.
autoload -Uz compinit
_comp_files=(${ZDOTDIR:-$HOME}/.zcompdump(Nm-20))
if (( $#_comp_files )); then
compinit -i -C
else
compinit -i
fi
unset _comp_files
The magic is the use of Glob Qualifiers.
At the end of a filename, you can add parenthesis with filters.
.zcompdump(Nm-20)) will select this file only if modified less than 20 days ago.
The N says to not display an error (NULL_GLOB).
It's missing a h to have Nmh-20 to get it if modified less than 20 h ago.
man zshexpn then search for Glob Qualifiers.
Try it yourself with
cd /tmp
touch myfile
z=(myfile(Nmm-1)) ; echo $z
wait a minute and try again the last line, with or without N

make zsh prompt update each time a command is executed

TLDR;
I need a way for .zshrc to automatically be sourced each time a command is executed. PROMPT needs to be updated each time a command is executed in order to show relevant information in the prompt.
Reason
I use Watson cli for tracking time. On my previous bash setup, I prepended my prompt ($PS1) with a symbol that indicates whether the timer is running or not (red/green). I have mimicked this functionality with Oh My Zsh, as follows (in the theme file):
WATSON_DIR="$HOME/Library/Application Support/watson"
watson_status() {
local txtred="${fg_bold[red]}"
local txtgrn="${fg_bold[green]}"
local txtrst="${reset_color}"
# Started
local status_color="$txtgrn"
# Stopped
if [[ $(cat "$WATSON_DIR/state") == '{}' ]]; then
status_color="$txtred"
fi
echo -e "$status_color""◉""$txtrst"
}
PROMPT="╭── %{$(watson_status) $fg_bold[green]%}%~%{$reset_color%}$(git_prompt_info) ⌚ %{$FG[130]%}%*%{$reset_color%}
╰─➤ $ "
Current issue
The icon will indicate the color of the state at the time that .zshrc was executed. For example, if the timer is running and the icon is properly indicating green, stopping the timer will not cause the icon to turn red. In order to see the icon change color, I have to source .zshrc.
This indicates that the function watson_status() needs to be run each time a command is executed, to give the latest status at the time of the command
I recently ported some prompt code from bash to zsh - on OSX Big Sur - and these were the two big "things to know" for me:
add setopt PROMPT_SUBST to your .zshrc. This "allows for functions in the prompt"
use single quotes when defining your PS1 / PROMPT. If you use double quotes, then the whole string will be evaluated once when the terminal starts, and then that evaluated value is "re-executed" every time the command changes. But you want the functions to be re-evaluated, not the output of the function at the time when the terminal is created.
Easiest example I used to confirm I had it working:
# print_epoch() { date '+%s' }
# this is what you want
# export PS1='$(print_epoch) > '
# this is not what you want
# export PS1="$(print_epoch) > "
Also worth noting PS1 and PROMPT are interchangeable
Extra:
While we are sharing here is my fairly minimal echo my user, directory, and git branch PROMPT with some colors and a pretty leaf:
parse_git_branch() {
git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/'
}
setopt PROMPT_SUBST
autoload -U colors && colors
export PROMPT='%n %~ %F{blue}🌿$(parse_git_branch)%f > '
noting that:
%n prints name
%~ prints directory relative to home
%F{blue} changes text to blue
%f resets color
Documentation: http://zsh.sourceforge.net/Doc/Release/Prompt-Expansion.html#Visual-effects

How to remove an entry from the history in ZSH

Let's say I ran a command using a zsh
echo "mysecret" > file
I can easily print the history including the entry numbers using the command fc -l:
1 echo "mysecret" >| file
But how can I easily delete an entry from the history?
I cannot find a corresponding paragraph in man zshbuiltins.
*BSD/Darwin (macOS):
LC_ALL=C sed -i '' '/porn/d' $HISTFILE
Linux (GNU sed):
LC_ALL=C sed -i '/porn/d' $HISTFILE
This will remove all lines matching "porn" from your $HISTFILE.
With setopt HIST_IGNORE_SPACE, you can prepend the above command with a space character to prevent it from being written to $HISTFILE.
As Tim pointed out in his comment below, the prefix LC_ALL=C prevents 'illegal byte sequence' failure.
I don't know if there is some elegant method for doing this, but in similar situations I have logged out (allowing zsh to empty its buffer and write my history to file), then logged in, and finally manually edited ~/.zsh_history, deleting the "dangerous" line.
If you use the HIST_IGNORE_SPACE option in zsh you can prepend commands with a space " " and they will not be remembered in the history file. If you have secret commands you commonly use you can do something along the lines of: alias hiddencommand=' hiddencommand'.
If you only want to make an occasional deletion, I think that it's easier to manually edit your .zsh_history.
In a zsh terminal:
Close the terminal session with the command to delete.
open a new session,
open ~/.zsh_history with a text editor (pico, Emacs, vim...),
delete the faulty lines,
close the editor, close the terminal session and open a new one,
enter history and the unwanted history item will be gone.
(Make sure the editor hasn't backed up the previous .zsh_history instance.)
(Solution based on https://til.hashrocket.com/posts/zn87awopb4-delete-a-command-from-zsh-history-)
This function will remove any one line you want from your Zsh history, no questions asked:
# Accepts one history line number as argument.
# Use `dc -1` to remove the last line.
dc () {
# Prevent the specified history line from being
# saved.
local HISTORY_IGNORE="${(b)$(fc -ln $1 $1)}"
# Write out the history to file, excluding lines that
# match `$HISTORY_IGNORE`.
fc -W
# Dispose of the current history and read the new
# history from file.
fc -p $HISTFILE $HISTSIZE $SAVEHIST
# TA-DA!
print "Deleted '$HISTORY_IGNORE' from history."
}
If you want to additionally prevent all dc commands from being written to history, add the following in your ~/.zshrc file:
zshaddhistory() {
[[ $1 != 'dc '* ]]
}
Update
I've now published a more comprehensive solution as a plugin: https://github.com/marlonrichert/zsh-hist
tldr:
vi $HISTFILE
more details:
run vi $HISTFILE
SHIFT + g — to go to the end
dd — to remove line
:wq — to save and exit
reload session or open a new tab to see changes
remove your line
In BASH [Not ZSH]:
1- in bash terminal type
hsitory # This will list all commands in history .bash_history file with line numbers
ex:
...
987 cd
988 ssh x#127.0.0.1
990 exit
991 cd
2- pick the CMD line number you want to delete
history -d 988
Note: if you want to delete for example last 3 CMDs, just pick the third line number from bottom ex: 988 and repeat the CMD history -d 988 3 times in sequence.
Source

How do i read, modify and write to the same file without involving a temporary file in zsh?

I like keeping my history files uncluttered. Since zsh has excellent history searching features, there is no need to save all the commands that I repeatedly use (e.g., finger, pwd, ls, etc) multiple times. To strip the history file of all duplicate lines, I did sort .zhistory|uniq -du. Now, I'd like to write this back to the same file, so that if I simply put this in my .zshrc, everytime I login, my history is trimmed and clean. If I try sort .zhistory|uniq -du>.zhistory, the resulting file is empty! On the other hand, if I do sort .zhistory|uniq -du>tempfile, it writes to tempfile correctly. Any idea how I can write to the same file?
You might be able to use a variable:
file='.zhistory' && var=$(sort -u "$file") && echo "$var" > "$file"
The reason you can't write to the same file is that the redirection occurs first and truncates the file before the utility ever sees it.
You can prevent duplicate lines in the first place. Use setopt with one or more of the following settings (from man zshoptions):
HIST_EXPIRE_DUPS_FIRST
If the internal history needs to be trimmed to add the current
command line, setting this option will cause the oldest history
event that has a duplicate to be lost before losing a unique
event from the list. You should be sure to set the value of
HISTSIZE to a larger number than SAVEHIST in order to give you
some room for the duplicated events, otherwise this option will
behave just like HIST_IGNORE_ALL_DUPS once the history fills up
with unique events.
HIST_FIND_NO_DUPS
When searching for history entries in the line editor, do not
display duplicates of a line previously found, even if the
duplicates are not contiguous.
HIST_IGNORE_ALL_DUPS
If a new command line being added to the history list duplicates
an older one, the older command is removed from the list (even
if it is not the previous event).
HIST_IGNORE_DUPS (-h)
Do not enter command lines into the history list if they are
duplicates of the previous event.
HIST_SAVE_NO_DUPS
When writing out the history file, older commands that duplicate
newer ones are omitted.
The program sponge can be useful to write back in the same file you read.
(For the example's sake, you don't know about sed -i)
echo "say what again" > file
sed s/what/woot/ file > file
So bad, file is now empty, you lost your file.
echo "say what again" > file
sed s/what/woot/ file | sponge file
does what you want
(Be careful not to write sponge > file or the file will be empty again.)
The fact that i didn't have an answer to this question annoyed me sufficiently that i wrote one - call this inplace and put it executably on your path:
#! /bin/bash
BACKUP_EXT=
while getopts "b:" flag
do
case "$flag" in
b) BACKUP_EXT="$OPTARG" ;;
esac
done
shift $((OPTIND - 1))
CMD="$1"
shift
for filename in "$#"
do
TMP_FILE="$(mktemp -t)"
bash -c "$CMD" <"$filename" >"$TMP_FILE"
if [[ -n "$BACKUP_EXT" ]]
then
mv "$filename" "$filename.$BACKUP_EXT"
fi
mv "$TMP_FILE" "$filename"
done
You may now say:
inplace 'sort | uniq -du' .zhistory
Incidentally, there's a way to do that uniqification without having to sort - but that's an answer for another question!

Why did my use of the read command not do what I expected?

I did some havoc on my computer, when I played with the commands suggested by vezult [1]. I expected the one-liner to ask file-names to be removed. However, it immediately removed my files in a folder:
> find ./ -type f | while read x; do rm "$x"; done
I expected it to wait for my typing of stdin:s [2]. I cannot understand its action. How does the read command work, and where do you use it?
What happened there is that read reads from stdin. When you put it at the end of a pipe, it read from that pipe.
So your find becomes
file1
file2
and so on; read reads that and replaces x successively with file1 then file2, and so your loop becomes
rm "file1"
rm "file2"
and sure enough, that rm's every file starting at the current directory ".".
A couple hints.
You didn't need the "/".
It's better and safer to say
find . -type f
because should you happen to type ". /" (ie, dot SPACE slash) find will start at the current directory and then go look starting at the root directory. That trick, given the right privileges, would delete every file in the computer. "." is already the name of a directory; you don't need to add the slash.
The find or rm commands will do this
It sounds like what you wanted to do was go through all the files in all the directories starting at the current directory ".", and have it ASK if you want to delete it. You could do that with
find . -type f -exec rm -i {} \;
or
find . -type f -ok rm {} \;
and not need a loop at all. You can also do
rm -r -i *
and get nearly the same effect, except that it will try to delete directories too. If the directory is empty, that'll even work.
Another thought
Come to think of it, unless you have a LOT of files, you could also do
rm -i `find . -type f`
Now the find in backquotes will become a bunch of file names on the command line, and the '-i' interactive flag on rm will ask the yes or no question.
Charlie Martin gives you a good dissection and explanation of what went wrong with your specific example, but doesn't address the general question of:
When should you use the read command?
The answer to that is - when you want to read successive lines from some file (quite possibly the standard output of some previous sequence of commands in a pipeline), possibly splitting the lines into several separate variables. The splitting is done using the current value of '$IFS', which normally means on blanks and tabs (newlines don't count in this context; they separate lines). If there are multiple variables in the read command, then the first word goes into the first variable, the second into the second, ..., and the residue of the line into the last variable. If there's only one variable, the whole line goes into that variable.
There are many uses. This is one of the simpler scripts I have that uses the split option:
#!/bin/ksh
#
# #(#)$Id: mkdbs.sh,v 1.4 2008/10/12 02:41:42 jleffler Exp $
#
# Create basic set of databases
MKDUAL=$HOME/bin/mkdual.sql
ELEMENTS=$HOME/src/sqltools/SQL/elements.sql
cat <<! |
mode_ansi with log mode ansi
logged with buffered log
unlogged
stores with buffered log
!
while read dbs logging
do
if [ "$dbs" = "unlogged" ]
then bw=""; cw=""
else bw="-ebegin"; cw="-ecommit"
fi
sqlcmd -xe "create database $dbs $logging" \
$bw -e "grant resource to public" -f $MKDUAL -f $ELEMENTS $cw
done
The cat command with a here-document has its output sent to a pipe, so the output goes into the while read dbs logging loop. The first word goes into $dbs and is the name of the (Informix) database I want to create. The remainder of the line is placed into $logging. The body of the loop deals with unlogged databases (where begin and commit do not work), then run a program sqlcmd (completely separate from the Microsoft new-comer of the same name; it's been around since about 1990) to create a database and populate it with some standard tables and data - a simulation of the Oracle 'dual' table, and a set of tables related to the 'table of elements'.
Other scripts that use the read command are bigger (by far), but generally read lines containing one or more file names and some other attributes of relevance, and then apply an appropriate transform to the files using the attributes.
Osiris JL: file * | grep 'sh.*script' | sed 's/:.*//' | xargs wgrep read
esqlcver:read version letter
jlss: while read directory
jlss: read x || exit
jlss: read x || exit
jlss: while read file type link owner group perms
jlss: read x || exit
jlss: while read file type link owner group perms
kb: while read size name
mkbod: while read directory
mkbod:while read dist comp
mkdbs:while read dbs logging
mkmsd:while read msdfile master
mknmd:while read gfile sfile version notes
publictimestamp:while read name type title
publictimestamp:while read name type title
Osiris JL:
'Osiris JL: ' is my command line prompt; I ran this in my 'bin' directory. 'wgrep' is a variant of grep that only matches entire words (to avoid words like 'already'). This gives some indication of how I've used it.
The 'read x || exit' lines are for an interactive script that reads a response from standard input, but exits if the command gets EOF (for example, if standard input comes from /dev/null).

Resources