Bash: How to use an alias command in combination with watch - bash

I would like to use an alias command in bash in combination with the watch command. The watch command is multiple chained commands.
A very simple example how I would think it would work (but it does not):
alias foo=some_command # a more complicated command
watch -n 1 "$(foo) | grep bar" # foo is not interpreted as the alias :(

watch -n 1 "$(foo) | grep sh" is wrong for two reasons.
When watch "$(cmdA) | cmdB" is executed by the shell, $(cmdA) gets expanded before running watch. Then watch would execute the output of cmdA as a command (which should fail in most cases) and pipe the output of that to cmdB. You probably meant watch 'cmdA | cmdB'.
The alias foo is defined in the current shell only. watch is not a built-in command and therefore has to execute its command in another shell which does not know the alias foo. There is a small trick presented in this answer, however we have to make some adjustments to make it work with pipes and options
alias foo=some_command
alias watch='watch -n 1 ' # trailing space treats next word as an alias
watch foo '| grep sh'
Note that the options for watch have to be specified inside the watch alias. The trailing space causes only the next word to be treated as an alias. With watch -n 1 foo bash would try to expand -n as an alias, but not foo.

I made a function that uses the --color option and allows you to use -n to specify a refresh interval.
swatch_usage() {
cat <<EOF >&2
NAME
swatch - execute a program periodically with "watch". Supports aliases.
SYNOPSIS
swatch [options] command
OPTIONS
-n, --interval seconds (default: 1)
Specify update interval. The command will not allow quicker than 0.1 second interval.
EOF
}
swatch() {
if [ $# -eq 0 ]; then
swatch_usage
return 1
fi
seconds=1
case "$1" in
-n)
seconds="$2"
args=${*:3}
;;
-h)
swatch_usage
;;
*)
seconds=1
args=${*:1}
;;
esac
watch --color -n "$seconds" --exec bash -ic "$args || true"
}
I only needed color and timing support but i'm sure you could add more if you wanted.
The meat of the function is that it executes your command with bash directly in interactive mode and can thus use any aliases or commands that are normally available to you in bash.
I'm not that experienced with scripting so fair warning, your mileage may vary. Sometimes i have to press Ctrl+C a few times to get it to stop, but for what it's worth, i've been using it frequently for 6 months without issue.
Gist form: https://gist.github.com/ablacklama/550420c597f9599cf804d57dd6aad131

Related

Bash get the command that is piping into a script

Take the following example:
ls -l | grep -i readme | ./myscript.sh
What I am trying to do is get ls -l | grep -i readme as a string variable in myscript.sh. So essentially I am trying to get the whole command before the last pipe to use inside myscript.sh.
Is this possible?
No, it's not possible.
At the OS level, pipelines are implemented with the mkfifo(), dup2(), fork() and execve() syscalls. This doesn't provide a way to tell a program what the commands connected to its stdin are. Indeed, there's not guaranteed to be a string representing a pipeline of programs being used to generate stdin at all, even if your stdin really is a FIFO connected to another program's stdout; it could be that that pipeline was generated by programs calling execve() and friends directly.
The best available workaround is to invert your process flow.
It's not what you asked for, but it's what you can get.
#!/usr/bin/env bash
printf -v cmd_str '%q ' "$#" # generate a shell command representing our arguments
while IFS= read -r line; do
printf 'Output from %s: %s\n' "$cmd_str" "$line"
done < <("$#") # actually run those arguments as a command, and read from it
...and then have your script start the things it reads input from, rather than receiving them on stdin.
...thereafter, ./yourscript ls -l, or ./yourscript sh -c 'ls -l | grep -i readme'. (Of course, never use this except as an example; see ParsingLs).
It can't be done generally, but using the history command in bash it can maybe sort of be done, provided certain conditions are met:
history has to be turned on.
Only one shell has been running, or accepting new commands, (or failing that, running myscript.sh), since the start of myscript.sh.
Since command lines with leading spaces are, by default, not saved to the history, the invoking command for myscript.sh must have no leading spaces; or that default must be changed -- see Get bash history to remember only the commands run with space prefixed.
The invoking command needs to end with a &, because without it the new command line wouldn't be added to the history until after myscript.sh was completed.
The script needs to be a bash script, (it won't work with /bin/dash), and the calling shell needs a little prep work. Sometime before the script is run first do:
shopt -s histappend
PROMPT_COMMAND="history -a; history -n"
...this makes the bash history heritable. (Code swiped from unutbu's answer to a related question.)
Then myscript.sh might go:
#!/bin/bash
history -w
printf 'calling command was: %s\n' \
"$(history | rev |
grep "$0" ~/.bash_history | tail -1)"
Test run:
echo googa | ./myscript.sh &
Output, (minus the "&" associated cruft):
calling command was: echo googa | ./myscript.sh &
The cruft can be halved by changing "&" to "& fg", but the resulting output won't include the "fg" suffix.
I think you should pass it as one string parameter like this
./myscript.sh "$(ls -l | grep -i readme)"
I think that it is possible, have a look at this example:
#!/bin/bash
result=""
while read line; do
result=$result"${line}"
done
echo $result
Now run this script using a pipe, for example:
ls -l /etc | ./script.sh
I hope that will be helpful for you :)

How can I easily log some specific command line commands into a file?

I often perform configuration changes using single line commands on Mac OS, Linux or even Windows and I want to easily log them in a file, so I can replay if I have to reconfigure the machine again.
Please not that I want to do these only for some commands, so the shell history is of not use.
Ideally I would like to be able to use some kind of shell extension that logs some of the commands.
As you know if you start your bash command with a space, this command is not logged into the history.
What if I can have another prefix that would do the opposite? Is there something there that can be used for this? A solution for bash would be more than enough and if there is an already existing solution it would much better than me writing a new one.
You could do your logging in PROMPT_COMMAND, extracting the specific commands from shell history and writing them to a file.
Something like:
log () {
last_command="$(history -p \!\!)"
if [[ $last_command == " "* ]] # save commands starting with *two* spaces
then
printf "%s\n" "$last_command" >> ~/special.log
fi
}
PROMPT_COMMAND="log; $PROMPT_COMMAND"
This has problems:
PROMPT_COMMAND is run each time the prompt is printed. Just pressing Enter multiple times could cause a command to be logged multiple times.
Marking with two spaces would, of course, need you to remove ignorespace or ignoreboth from HISTCONTROL so that commands starting spaces are logged at all.
AFAICT, history is updated when the next command is read, so the command is logged after the next command returns to the prompt, since that's when the correct history is available in PROMPT_COMMAND.
All this would be easier in zsh, with a preexec hook:
preexec () {
if [[ $1 == " "* ]]
then
printf "%s\n" "$1" >> ~/special.log
fi
}
The preexec function automatically gets the command as the first argument if history is enabled, saving us a deal of trouble. It is run when the command has been read, but before it begins execution, so the timing is perfect. From the documentation:
preexec
Executed just after a command has been read and is about to be
executed. If the history mechanism is active (regardless of whether
the line was discarded from the history buffer), the string that the
user typed is passed as the first argument, otherwise it is an empty
string. The actual command that will be executed (including expanded
aliases) is passed in two different forms: the second argument is a
single-line, size-limited version of the command (with things like
function bodies elided); the third argument contains the full text
that is being executed.
$ ls
$ echo foo | echo bar
bar
$ cat ~/special.log
ls
echo foo | echo bar
A function in .bashrc can be used like a prefix:
log_this_command () {
echo "$#" >> ~/a_log_file # log the command to file
"$#" # and run the command itself
}
Caveat: this only logs expanded arguments, rather than the raw input.
Source function with the same name function screencapture {echo "used parms: $#"; command screencapture $#}
appending to log file function screencapture {echo "$(date) screencapture " $# >> ~/log.txt; command screencapture $#}
as one runs screencapture command, log entry is created and command executes as uninterfered
you could automate in creating these functions, if the list of them is like .... all of them

How to get last command run without using `!!`?

I'm trying to alias _! to sudo the last command, but I'm running into roadblocks. !! doesn't seem to work in my .zshrc file, and sed has given me repeated problems. I tried using the following command, and several variations of it, but to no avail.
history | tail -1 | sed -e 's/[^0-9\*\ ]+/\0/g'
However, this still interpreted the piped input as a file, instead of a string of text. I also tried a variation using awk:
history | tail -1 | awk '{ gsub("/[^0-9\*\ ]+", "") ; system( "echo" $1 ) }'
I'm sure I'm just having some trouble putting the commands in correctly, but some help would be appreciated.
You can use the fc built-in to access the history programmatically. For example, I believe this will behave as you wish:
alias _!='fc -e "sed -i -e \"s/^/sudo /\""'
With no arguments, fc (short for "fix command") fires up $EDITOR on your previous command and runs the result of your editing. You can specify a different editor with the -e option; here, I'm specifying a non-interactive one in the form of a sed command that will insert sudo in front of the command line.
The command assumes GNU sed. As written, it will also work with the version that ships on modern BSD/macOS, but by way of a hackcident: it treats the -e as an argument to -i instead of a new option. Since the -e is optional with only one expression, this works fine, but it means that sed makes a backup of the temp file with -e on the end, which will hang around after the command completes. You can make that cleaner by using this alternative version on those systems:
alias _!='fc -e "sed -i \"\" -e \"s/^/sudo /\""'
(That won't work with GNU sed, which sees the empty string argument as a filename to operate on...)
On older systems, a more portable solution could use ed:
alias _!="fc -e 'ed -s <<<$'\''s/^/sudo /\nw\nq'\'"
You can often get away with something simpler, like sudo $(fc -ln -1) (-l = list commands, -n = without numbers, -1 = only the last command), but in general you will run into quoting issues, since the command is output by fc the way it was typed:
% touch '/etc/foo bar'
touch: /etc/foo bar: Permission denied
% sudo $(fc -ln -1)
touch: '/etc/foo: No such file or directory
None of the above is limited to zsh, btw; fc dates to the original version of ksh, so it will also work in bash, etc.
This fc command will always give most recently executed command in zsh and in bash:
fc -ln -1
As per help fc:
-l (letter el) list lines instead of editing
-n omit line numbers when listing
-1 (minus one) gets the just executed command.
Found a amazing widget to sudo:
sudo-command-line() {
[[ -z $BUFFER ]] && zle up-history
[[ $BUFFER != sudo\ * ]] && {
typeset -a bufs
bufs=(${(z)BUFFER})
if (( $+aliases[$bufs[1]] )); then
bufs[1]=$aliases[$bufs[1]]
fi
bufs=(sudo $bufs)
BUFFER=$bufs
}
zle end-of-line
}
zle -N sudo-command-line
bindkey "\e\e" sudo-command-line
Author:lilydjwg
The following is the way to run the last command in command:
fc -ln -1 is the simplest way, but one problem, when run something with some spaces at the beginning of the command, this command won't shown up in history, anything based on history won't work properly.
So we need ZLE(Zsh Line Editor) to store the command manually.
Store_Your_Command () {
if [[ -z $BUFFER ]]
then
# If nothing input, just clear the screen
zle clear-screen
else
zle accept-line
# Remember the last command, useful in some alias
# Add space at the beginning of a command, this command wont
# show up in history, so use variables to store the command
LAST_COMMAND=$CURRENT_COMMAND
CURRENT_COMMAND=$BUFFER
fi
}
# Create a user-defined widget
zle -N Store_Your_Command
# Bind it to the **Enter** key
bindkey "^M" Store_Your_Command
Then whenever we press enter to run a command, this command will be stored in $CURRENT_COMMAND, and the last command will be stored in $LAST_COMMAND.
Want to run the last command? Just run eval $LAST_COMMAND, you can also put it to your alias.
When some alias in the last command, zsh wont run the last command correctly, so we need to expand our alias: when we input an alias, replace the alias to the original command/content, with help of the builtin zle: _expand_alias.
First, delete the widget we just added.
Add those to your .zshrc:
# When input space, expand alias -----------------------------------{{{
expand_alias_space () {
zle _expand_alias
zle self-insert
}
zle -N expand_alias_space
bindkey " " expand_alias_space
# }}}
# When input enter, expand alias -----------------------------------{{{
expand_alias_enter () {
if [[ -z $BUFFER ]]
then
zle clear-screen
else
zle _expand_alias
zle accept-line
# Remember the last command, useful in some alias
# Add space at the beginning of a command, this command won't
# show up in history, so use variables to store the command
LAST_COMMAND=$CURRENT_COMMAND
CURRENT_COMMAND=$BUFFER
fi
}
zle -N expand_alias_enter
bindkey "^M" expand_alias_enter
# }}}
Now we can expand alias to the original command/content by press Space key or just press Enter key to run the command, and use eval $LAST_COMMAND to run the last command without any problems.
But it will call another problem when run a command use eval $LAST_COMMAND twice:
zsh: job table full or recursion limit exceeded
We need to replace eval $LAST_COMMAND to the real command, because $LAST_COMMAND always change.
We write a function run the last command like this
# Echo the last command
fun()
{
# The command we need to run in this function
CURRENT_COMMAND="echo \[`echo $LAST_COMMAND`\]"
# run the command
eval $CURRENT_COMMAND
}
The command stored in $CURRENT_COMMAND wont change like eval $LAST_COMMAND does.
problem sloved.
No more problem I hope
If I want . to be the alias for last command,
lastcmd() {
# start climbing back in history, checking for alias
n=-1
lc=$(fc -ln $n $n)
# "." is checked because I have aliased lastcmd to "."
while [ "$lc" = "lastcmd" ] || [ "$lc" = "." ]
do
n=$(( $n - 1 ))
lc=$(fc -ln $n $n)
done
eval ${lc}
}
alias .=lastcmd
This is pretty simple and works well for me. Replace the second condition in while with whatever alias you end up using.
Add this to your .zshrc --
func preexec() {
export LAST_COMMAND="$1"
}

Shell Script Help--Accept Input and Run in BackGround?

I have a shell script in which in the first line I ask the user to input how many minutes they want the script to run for:
#!/usr/bin/ksh
echo "How long do you want the script to run for in minutes?:\c"
read scriptduration
loopcnt=0
interval=1
date2=$(date +%H:%M%S)
(( intervalsec = $interval * 1 ))
totalmin=${1:-$scriptduration}
(( loopmax = ${totalmin} * 60 ))
ofile=/home2/s499929/test.log
echo "$date2 total runtime is $totalmin minutes at 2 sec intervals"
while(( $loopmax > $loopcnt ))
do
date1=$(date +%H:%M:%S)
pid=`/usr/local/bin/lsof | grep 16752 | grep LISTEN |awk '{print $2}'` > /dev/null 2>&1
count=$(netstat -an|grep 16752|grep ESTABLISHED|wc -l| sed "s/ //g")
process=$(ps -ef | grep $pid | wc -l | sed "s/ //g")
port=$(netstat -an | grep 16752 | grep LISTEN | wc -l| sed "s/ //g")
echo "$date1 activeTCPcount:$count activePID:$pid activePIDcount=$process listen=$port" >> ${ofile}
sleep $intervalsec
(( loopcnt = loopcnt + 1 ))
done
It works great if I kick it off an input the values manually. But if I want to run this for 3 hours I need to kick off the script to run in the background.
I have tried just running ./scriptname & and I get this:
$ How long do you want the test to run for in minutes:360
ksh: 360: not found.
[2] + Stopped (SIGTTIN) ./test.sh &
And the script dies. Is this possible, any suggestions on how I can accept this one input and then run in the background?? Thanks!!!
You could do something like this:
test.sh arg1 arg2 &
Just refer to arg1 and arg2 as $1 and $2, respectively, in the bash script. ($0 is the name of the script)
So,
test.sh 360 &
will pass 360 as the first argument to the bash or ksh script which can be referred to as $1 in the script.
So the first few lines of your script would now be:
#!/usr/bin/ksh
scriptduration=$1
loopcnt=0
...
...
With bash you can start the script in the foreground and after you finished with the user input, interrupt it by hitting Ctrl-Z.
Then type
$ bg %
and the script will continue to run in the background.
Why You're Getting What You're Getting
When you run the script in the background, it can't take any user input. In fact, the program will freeze if it expects user input until its put back in the foreground. However, output has to go somewhere. Thus, the output goes to the screen (even though the program is running in the background. Thus, you see the prompt.
The prompt you see your program displaying is meaningless because you can't input at the prompt. Instead, you type in 360 and your shell is interpreting it as a command you want because you're not putting it in the program, you're putting it in the command prompt.
You want your program to be in the foreground for the input, but run in the background. You can't do both at once.
Solutions To Your Dilemma
You can have two programs. The first takes the input, and the second runs the actual program in the background.
Something like this:
#! /bin/ksh
read time?"How long in seconds do you want to run the job? "
my_actual_job.ksh $time &
In fact, you could even have a mechanism to run the job in the background if the time is over a certain limit, but otherwise run the job in the foreground.
#! /bin/ksh
readonly MAX_FOREGROUND_TIME=30
read time?"How long in seconds do you want to run the job? "
if [ $time -gt $MAX_FOREGROUND_TIME ]
then
my_actual_job.ksh $time &
else
my_actual_job.ksh $time
fi
Also remember if your job is in the background, it cannot print to the screen. You can redirect the output elsewhere, but if you don't, it'll print to the screen at inopportune times. For example, you could be in VI editing a file, and suddenly have the output appear smack in the middle of your VI session.
I believe there's an easy way to tell if your job is in the background, but I can't remember it offhand. You could find your current process ID by looking at $$, then looking at the output of jobs -p and see if that process ID is in the list. However, I'm sure someone will come up with an easy way to tell.
It is also possible that a program could throw itself into the background via the bg $$ command.
Some Hints
If you're running Kornshell, you might consider taking advantage of many of Kornshell's special features:
print: The print command is more flexible and robust than echo. Take a look at the manpage for Kornshell and see all of its features.
read: You notice that you can use the read var?"prompt" form of the read command.
readonly: Use readonly to declare constants. That way, you don't accidentally change the value of that variable later. Besides, it's good programming technique.
typeset: Take a look at typeset in the ksh manpage. The typeset command can help you declare particular variables as floating point vs. real, and can automatically do things like zero fill, right or left justify, etc.
Some things not specific to Kornshell:
The awk and sed commands can also do what grep does, so there's no reason to filter something through grep and then through awk or sed.
You can combine greps by using the -e parameter. grep foo | grep bar is the same as grep -e foo -e bar.
Hope this helps.
I've tested this with ksh and it worked. The trick is to let the script call itself with the time to wait as parameter:
if [ -z "$1" ]; then
echo "How long do you want the test to run for in minutes:\c"
read scriptduration
echo "running task in background"
$0 $scriptduration &
exit 0
else
scriptduration=$1
fi
loopcnt=0
interval=1
# ... and so on
So are you using bash or ksh? In bash, you can do this:
{ echo 360 | ./test.sh ; } &
It could work for ksh also.

BASH Variables with multiple commands and reentrant

I have a bash script that sources contents from another file. The contents of the other file are commands I would like to execute and compare the return value. Some of the commands are have multiple commands separated by either a semicolon (;) or by ampersands (&&) and I can't seem to make this work. To work on this, I created some test scripts as shown:
test.conf is the file being sourced by test
Example-1 (this works), My output is 2 seconds in difference
test.conf
CMD[1]="date"
test.sh
. test.conf
i=2
echo "$(${CMD[$i]})"
sleep 2
echo "$(${CMD[$i]})"
Example-2 (this does not work)
test.conf (same script as above)
CMD[1]="date;date"
Example-3 (tried this, it does not work either)
test.conf (same script as above)
CMD[1]="date && date"
I don't want my variable, CMD, to be inside tick marks because then, the commands would be executed at time of invocation of the source and I see no way of re-evaluating the variable.
This script essentially calls CMD on pass-1 to check something, if on pass-1 I get a false reading, I do some work in the script to correct the false reading and re-execute & re-evaluate the output of CMD; pass-2.
Here is an example. Here I'm checking to see if SSHD is running. If it's not running when I evaluate CMD[1] on pass-1, I will start it and re-evaluate CMD[1] again.
test.conf
CMD[1]=`pgrep -u root -d , sshd 1>/dev/null; echo $?`
So if I modify this for my test script, then test.conf becomes:
NOTE: Tick marks are not showing up but it's the key below the ~ mark on my keyboard.
CMD[1]=`date;date` or `date && date`
My script looks like this (to handle the tick marks)
. test.conf
i=2
echo "${CMD[$i]}"
sleep 2
echo "${CMD[$i]}"
I get the same date/time printed twice despite the 2 second delay. As such, CMD is not getting re-evaluate.
First of all, you should never use backticks unless you need to be compatible with an old shell that doesn't support $() - and only then.
Secondly, I don't understand why you're setting CMD[1] but then calling CMD[$i] with i set to 2.
Anyway, this is one way (and it's similar to part of Barry's answer):
CMD[1]='$(date;date)' # no backticks (remember - they carry Lime disease)
eval echo "${CMD[1]}" # or $i instead of 1
From the couple of lines of your question, I would have expected some approach like this:
#!/bin/bash
while read -r line; do
# munge $line
if eval "$line"; then
# success
else
# fail
fi
done
Where you have backticks in the source, you'll have to escape them to avoid evaluating them too early. Also, backticks aren't the only way to evaluate code - there is eval, as shown above. Maybe it's eval that you were looking for?
For example, this line:
CMD[1]=`pgrep -u root -d , sshd 1>/dev/null; echo $?`
Ought probably look more like this:
CMD[1]='`pgrep -u root -d , sshd 1>/dev/null; echo $?`'

Resources