How do I get "previous executed command" in a bash script? - bash

I use multiple bash sessions, and I want to keep track of history from all of them in one file (I don't care that it is multiplexed from multiple sessions, I can always put a session identifier in front of it.) I have tried doing
shopt -s histappend
and also adding
history -a
to the $PROMPT_COMMAND variable. But none of them really work for me, and I don't understand why they don't work (they behave very non-deterministically as far as I can tell... sometimes they multiplex commands from multiple sessions, sometimes they don't).
The goal of this question is to explore an alternate way to keep history from all sessions, where I can control what I write to the history. The idea is to store "previous command" in a shell variable and then echo that variable to a history-log file inside the definition of PS1 variable.
The question is: How do I get the "previous executed command" in a shell variable. I know I can execute echo !! >> logfile.txt in interactive bash session to record it to a log file. But how do I do this in a script file (or .bashrc file)?
I have tried
PROMPT_COMMAND="PC=$_;"
PREVIOUS_COMMAND=$(echo $PC) # $_ only gives the last argument of previous command
export PS1="[\u#\h \w] [$PREVIOUS_COMMAND $(echo $_) $_] $ "
But none of this works.
Thanks for your time,
~yogi

Something like
fc -ln -1
should work. That said, you're probably running into concurrent access issues (read: multiple shells overwriting each others' history) and you may not be able to do any better by hand.

Related

How to write a bash script or function that removes itself from history?

I would like to write a bash script or function that is able to remove the entry from the caller's bash history that corresponds to its own invocation. Is this possible?
The use case is a wrapper around writing secrets on a team-owned cluster, and I want to add some convenience around making sure all team members don't inadvertently leave the secrets in plain text in bash history. At first I was going to require the script use read -rs to prevent the command from containing the secret. Then, realizing that some will inevitably type in the secret name in the command invocation anyway, I wondered if I could support that but delete the invocation bash history.
Essentially, something like adding history -d $HISTCMD in the script; however, the history item isn't written until after the script completes. So no dice.
Is there a way to tell bash "do not record this command in history" or otherwise delete it upon completion?
Check out HISTIGNORE
A colon-separated list of patterns used to decide which command lines should be saved on the history list.
$ HISTIGNORE='echo *'
$ echo secret
secret
$ history 3
9921 man bash
9922 HISTIGNORE='echo *'
9923 history 3

Use a variable on a script command line when its value isn't set until after the script starts

How to correctly pass to the script and substitute a variable that is already defined there?
My script test.sh:
#!/bin/bash
TARGETARCH=amd64
echo $1
When I enter:
bash test.sh https://example/$TARGETARCH
I want to see
https://example/amd64
but I actually see
https://example/
What am I doing wrong?
The first problem with the original approach is that the $TARGETARCH is removed by your calling shell before your script is ever invoked. To prevent that, you need to use quotes:
./yourscript 'https://example.com/$TARGETARCH'
The second problem is that parameter expansions only happen in code, not in data. This is, from a security perspective, a Very Good Thing -- if data were silently treated as code it would be impossible to write secure scripts handling untrusted data -- but it does mean you need to do some more work. The easy thing, in this case, is to export your variable and use GNU envsubst, as long as your operating system provides it:
#!/bin/bash
export TARGETARCH=amd64
substitutedValue=$(envsubst <<<"$1")
echo "Original value was: $1"
echo "Substituted value is: $substitutedValue"
See the above running in an online sandbox at https://replit.com/#CharlesDuffy2/EcstaticAfraidComputeranimation#replit.nix
Note the use of yourscript instead of test.sh here -- using .sh file extensions, especially for bash scripts as opposed to sh scripts, is an antipattern; the essay at https://www.talisman.org/~erlkonig/documents/commandname-extensions-considered-harmful/ has been linked by the #bash IRC channel on this topic for over a decade.
For similar reasons, changing bash yourscript to ./yourscript lets the #!/usr/bin/env bash line select an interpreter, so you aren't repeating the "bash" name in multiple places, leading to the risk of those places getting out of sync with each other.

getting last executed command from script

I'm trying to get last executed command from command line from a script to be saved for a later reference:
Example:
# echo "Hello World!!!"
> Hello World!!!
# my_script.sh
> echo "Hello World!!!"
and the content of the script would be :
#!/usr/bin/ksh
fc -nl -1 | sed -n 1p
Now as you notices using here ksh and fc is a built in command which if understood correctly should be implemented by any POSIX compatible shells. [I understand that this feature is interactive and that calling same fc again will give different result but this is not the concern do discuss about]
Above works so far so good only if my_script.sh is being called from the shell which is as well ksh, or if calling from bash and changing 1st line of script as #!/bin/bash then it works too and it doesn't if shells are different.
I would like to know if there is any good way to achieve above without being constrained by the shell your script is called from. I understand that fc is a built in command and it works per shell thus most probably my approach is not good at all from what I want to achieve. Any better suggestions?
I actually attempted this, but it cannot be done between different shells consistently.
While
fc -l`,
is the POSIX standard command for showing $SHELL history, implementation details may be different.
At least bash and ksh93 both will report the last command with
fc -n -l -1 -1
However, POSIX does not guarantee that shell history will be carried over to a new instance of the shell, as this requires the presence of a $HISTFILE. If none is
present, the shell may default to $HOME/.sh_history.
However, this history file or Command History List is not portable between different shells.
The POSIX Shell description of the
Command History List says:
When the sh utility is being used interactively, it shall maintain a list of commands
previously entered from the terminal in the file named by the HISTFILE environment
variable. The type, size, and internal format of this file are unspecified.
Emphasis mine
What this means is that for your script needs to know which shell wrote that history.
I tried to use $SHELL -c 'fc -nl -1 -1', but this did not appear to work when $SHELL refers to bash. Calling ksh -c ... from bash actually worked.
The only way I could get this to work is by creating a function.
last_command() { (fc -n -l -1 -1); }
In both ksh and bash, this will give the expected result. Variations of this function can be used to write the result elsewhere. However, it will break whenever it's called
from a different process than the current.
The best you can do is to create these functions and source them into your
interactive shell.
fc is designed to be used interactively. I tested your example on cygwin/bash and the result was different. Even with bash everywhere the fc command didn't work in my case.
I think fc displays the last command of the current shell (here I don't speak about the shell interpretor, but shell as the "process box". So the question is more why it works for you.
I don't think there is a clean way to achieve what you want because (maybe I miss something) you want two different process (bash and your magic command [my_script.sh]) and by default OS ensure isolation between them.
You can rely on what you observe (not portable, depends on the shell interpretor etc.)
You cannot rely on BASH historic because it's in-memory (the file is updated only on exit).
You can use an alias or a function (edited: #Charles Duffy is right). In this case you won't be able to use your "magic command" from another terminal, but for an interactive use it does the job.
Edited:
Or you can provide two commands: one to save and another to look for. In this case you manage your own historic but you have to save explicitly each command that is painful...
So I look for a hook. And I found this other thread : https://superuser.com/questions/175799/does-bash-have-a-hook-that-is-run-before-executing-a-command
# At the beginning of the Shell (.bashrc for example)
save(){ history 1 >>"$HOME"/myHistory ; }
trap 'save' DEBUG
# An example of use
rm -f "$HOME"/myHistory
echo "1 2 3"
cat "$HOME"/myHistory
14 echo "1 2 3"
15 cat "$HOME"/myHistory
But I observe it slows down the interpretor...
Little convoluted, but I was able to use this command to get the most recent command in zsh, bash, ksh, and tcsh on Linux:
history | tail -2 | head -1 | sed -r 's/^[ \t]*[0-9]*[ \t]+([^ \t].*$)/\1/'
Caveats: this uses GNU sed, so you'll need to install that if you're using BSD, OS X, etc; and also, tcsh will display the time of the command before the command itself. Regular csh doesn't seem to having a functioning history command when I tried it.

saving bash functions

First of all, I'd like to be corrected on my vocabulary. I assume Terminal provides an environment for bash, which is a type/version of shell. Is this correct?
I'm trying to utilize bash and shell more in my development processes to speed up deployment. However, I'm only beginning to understand the basics outside the commands I've learned from growing up.
I've started making functions in Terminal to help automate some of my more repetitious tasks. This is all find and dandy until I exit terminal.
I assume that shell runs in an instance, so that instance is lost when I exit terminal. I notice that shell leaves a .bash_history, also accessible using 'history', where I can see my old functions from old sessions. However, of course, they no longer appear to execute.
I recognize that I could create a shell script, but this introduces compiling issues as well as having to pay more attention to where the scripts are stored relatively.
Question: When I create bash scripts using command(){}, they do not persist after closing the terminal. Can I make them do that so on new terminal sessions I have access to my old functions without resorting to shell scripts?
Edit: I also wanted to mention that I tried extensively to find an issue to this using traditional means, but "save" and "exit" in the search term often will direct to other aspects of shell.
Your first statement is correct. A terminal instance runs a type of shell (bash, sh, csh)
You can add them to your ~/.bashrc file or add the saved script path (no compiling needed) to your PATH variable.
You could also just copy scripts to /usr/local/bin for quick access anytime. You would have no need to keep track of where they are relatively. This is quite handy and makes your scripts available to other users (or not if permissions are set correctly)
See the Using History Interactively section of the Bash Reference Manual for ways you can execute commands from your history.
For example, typing !?foo and pressing Enter will execute the most recent command containing "foo". I like to have shopt -s histverify histreedit in my ~/.bashrc so I can edit and confirm the command, if necessary rather than executing it immediately.
Also see the Commands For Manipulating The History section for keystrokes you can use to search for history entries to recall and execute.
For example, pressing Ctrl-r and typing foo will recall the most recent command containing "foo". You can press Ctrl-r additional times to continue searching in reverse for additional matching commands. Press Enter when you're ready to execute the one currently shown or Ctrl-g to abort the search.
If you add stty -ixon to your ~/.bashrc, then you can use Ctrl-s to search forward through history after you've begun searching backward.
Of course, you can save your functions by editing ~/.bashrc and adding them to it. I prefer to keep my functions in a file I created called ~/bin/functions and then add a line to ~/.bashrc to source that file. The line looks like . ~/bin/functions.
I save larger scripts in /usr/local/bin or ~/bin. The former should already be in your path and you can add the latter to your path by editing ~/.bashrc.
After you type in the functions on the command-line you could recall them using command-line editing (as #Dennis Williamson mentioned), but there is an easier method: declare -f. This command lists all current functions, and you can redirect them to a file:
home/user1> function myfunc {
> echo "Hollow world!"
> }
/home/user1> declare -f > myfuncs
/home/user1> more myfuncs
myfunc ()
{
echo "Hollow world"
}
Note how Bash changes the function syntax from Korn shell to Bourne shell! Fortunately there is no difference between the two in Bash (unlike ksh93).
When you need to load the function it is a simple matter:
/home/user1> source myfuncs
/home/user1> myfunc
Hollow world!
You don't need execute permissions by the way, only read.
You could (as others have said) add this to one of your start-up files, like .bashrc.
You can create a simple library which would contain all your functions. This would basically solve your problem of storing functions.
yeshwantnaik$ cat my_functions.lib
function do_something(){
#your code goes here
}
Save it wherever you want. Preferably to your $HOME directory.
Load the library. Don't miss the dot in the beginning.
yeshwantnaik$ . $HOME/my_functions.lib
Now you can run your function directly.
yeshwantnaik$ do_something
Let Linux do stuff for you
You can skip the step of manually loading the library by letting Linux do it for you when you log in automatically.
Run below commands
echo ". $HOME/my_functions.lib" >> ~/.bashrc
echo ". $HOME/my_functions.lib" >> ~/.bash_profile
source ~/.bashrc
source ~/.bash_profile
That's it. You can directly execute your function from the command line without doing anything.

How do I prevent commands from showing up in Bash history?

Sometimes, when I run commands like rm -rf XYZ, I don't want this to be recorded in Bash history, because I might accidentally run the same command again by reverse-i-search. Is there a good way to prevent this from happening?
If you've set the HISTCONTROL environment variable to ignoreboth (which is usually set by default), commands with a leading space character will not be stored in the history (as well as duplicates).
For example:
$ HISTCONTROL=ignoreboth
$ echo test1
$ echo test2
$ history | tail -n2
1015 echo test1
1016 history | tail -n2
Here is what man bash says:
HISTCONTROL
A colon-separated list of values controlling how commands are saved on the history list. If the list of values includes ignorespace, lines which begin with a space character are not saved in the history list. A value of ignoredups causes lines matching the previous history entry to not be saved. A value of ignoreboth is shorthand for ignorespace and ignoredups. A value of erasedups causes all previous lines matching the current line to be removed from the history list before that line is saved. Any value not in the above list is ignored. If HISTCONTROL is unset, or does not include a valid value, all lines read by the shell parser are saved on the history list, subject to the value of HISTIGNORE. The second and subsequent lines of a multi-line compound command are not tested, and are added to the history regardless of the value of HISTCONTROL.
See also:
Why is bash not storing commands that start with spaces? at unix SE
Why does bash have a HISTCONTROL=ignorespace option? at unix SE
In your .bashrc/.bash_profile/wherever you want, put export HISTIGNORE=' *'. Then just begin any command you want to ignore with one space.
$ ls # goes in history
$ ls # does not
Even better use HISTIGNORE. This allows you to specify a set of patterns to be ignored (such as rm). It is better (I think) than just piping all history to /dev/null.
kill -9 $$
I know that is not as best as the previous answers, but this will kill the current Bash shell without saving anything, useful when HISTCONTROL is not set by default, you forgot to set it, or pure and simple you forgot to put a leading space and you just typed in some passwords and don't want them to remain permanently in history.
This is the quick way, and something like erasing the history file is not as good because you need to do it outside a history saving shell (log in as different user and use su/sudo, creating a background job, etc.)
You can do one of two things:
export HISTFILE=/dev/null
Or, begin the command with a space.
Or
unset HISTFILE
(similar to the previous answer only shorter: export HISTFILE=/dev/null)
I added an "Incognito" functionality to my .bashrc for when I want to run some commands without being saved without having to add spaces before each one.
Do note though that the in-memory history of the current terminal session will still be saved, but when I open a new terminal the commands issued in a past terminal's incognito session will never be seen because they were never written to the HISTFILE.
To your .bashrc:
ignoreHistory="false"
DEFAULT_HISTFILE=~/.bash_history
HISTFILE="$DEFAULT_HISTFILE"
# Toggle incognito mode
incognito() {
if [[ "$ignoreHistory" == "true" ]]; then
echo -e "\e[33mExited incognito mode\e[39m"
ignoreHistory="false"
HISTFILE="$DEFAULT_HISTFILE"
else
echo -e "\e[33mEntered incognito mode\e[39m"
ignoreHistory="true"
HISTFILE=/dev/null
fi
}
Nice little utility I think some people may find use in, you can even change the prompt to reflect whether you're in incognito mode or not.
At shell startup, I explicitly cleanup the history from the entries that I don't want to be there. For example, I don't want any rm -rf in the history (it's trauma after removing a directory full of results processed overnight, just with a single Arrow-Up + Enter :)
I put the following snippet in my init file (works with .zshrc, should also work with .bashrc)
# ...
HISTFILE=~/.zshhistory
# ...
# remove dangerous entries from the shell history
temp_histfile="/tmp/$$.temp_histfile"
grep -v -P '^rm .*-rf' $HISTFILE > $temp_histfile
mv $temp_histfile $HISTFILE

Resources