Performance profiling tools for shell scripts - performance

I'm attempting to speed up a collection of scripts that invoke subshells and do all sorts of things. I was wonder if there are any tools available to time the execution of a shell script and its nested shells and report on which parts of the script are the most expensive.
For example, if I had a script like the following.
#!/bin/bash
echo "hello"
echo $(date)
echo "goodbye"
I would like to know how long each of the three lines took. time will only only give me total time for the script. bash -x is interesting but does not include timestamps or other timing information.

You can set PS4 to show the time and line number. Doing this doesn't require installing any utilities and works without redirecting stderr to stdout.
For this script:
#!/bin/bash -x
# Note the -x flag above, it is required for this to work
PS4='+ $(date "+%s.%N ($LINENO) ")'
for i in {0..2}
do
echo $i
done
sleep 1
echo done
The output looks like:
+ PS4='+ $(date "+%s.%N ($LINENO) ")'
+ 1291311776.108610290 (3) for i in '{0..2}'
+ 1291311776.120680354 (5) echo 0
0
+ 1291311776.133917546 (3) for i in '{0..2}'
+ 1291311776.146386339 (5) echo 1
1
+ 1291311776.158646585 (3) for i in '{0..2}'
+ 1291311776.171003138 (5) echo 2
2
+ 1291311776.183450114 (7) sleep 1
+ 1291311777.203053652 (8) echo done
done
This assumes GNU date, but you can change the output specification to anything you like or whatever matches the version of date that you use.
Note: If you have an existing script that you want to do this with without modifying it, you can do this:
PS4='+ $(date "+%s.%N ($LINENO) ")' bash -x scriptname
In the upcoming Bash 5, you will be able to save forking date (but you get microseconds instead of nanoseconds):
PS4='+ $EPOCHREALTIME ($LINENO) '

You could pipe the output of running under -x through to something that timestamps each line when it is received. For example, tai64n from djb's daemontools.
At a basic example,
sh -x slow.sh 2>&1 | tai64n | tai64nlocal
This conflates stdout and stderr but it does give everything a timestamp.
You'd have to then analyze the output to find expensive lines and correlate that back to your source.
You might also conceivably find using strace helpful. For example,
strace -f -ttt -T -o /tmp/analysis.txt slow.sh
This will produce a very detailed report, with lots of timing information in /tmp/analysis.txt, but at a per-system call level, which might be too detailed.

Sounds like you want to time each echo. If echo is all that you're doing this is easy
alias echo='time echo'
If you're running other command this obviously won't be sufficient.

Updated
See enable_profiler/disable_profiler in
https://github.com/vlovich/bashrc-wrangler/blob/master/bash.d/000-setup
which is what I use now. I haven't tested on all version of BASH & specifically but if you have the ts utility installed it works very well with low overhead.
Old
My preferred approach is below. Reason is that it supports OSX as well (which doesn't have high precision date) & runs even if you don't have bc installed.
#!/bin/bash
_profiler_check_precision() {
if [ -z "$PROFILE_HIGH_PRECISION" ]; then
#debug "Precision of timer is unknown"
if which bc > /dev/null 2>&1 && date '+%s.%N' | grep -vq '\.N$'; then
PROFILE_HIGH_PRECISION=y
else
PROFILE_HIGH_PRECISION=n
fi
fi
}
_profiler_ts() {
_profiler_check_precision
if [ "y" = "$PROFILE_HIGH_PRECISION" ]; then
date '+%s.%N'
else
date '+%s'
fi
}
profile_mark() {
_PROF_START="$(_profiler_ts)"
}
profile_elapsed() {
_profiler_check_precision
local NOW="$(_profiler_ts)"
local ELAPSED=
if [ "y" = "$PROFILE_HIGH_PRECISION" ]; then
ELAPSED="$(echo "scale=10; $NOW - $_PROF_START" | bc | sed 's/\(\.[0-9]\{0,3\}\)[0-9]*$/\1/')"
else
ELAPSED=$((NOW - _PROF_START))
fi
echo "$ELAPSED"
}
do_something() {
local _PROF_START
profile_mark
sleep 10
echo "Took $(profile_elapsed) seconds"
}

Here's a simple method that works on almost every Unix and needs no special software:
enable shell tracing, e.g. with set -x
pipe the output of the script through logger:
sh -x ./slow_script 2>&1 | logger
This will writes the output to syslog, which automatically adds a time stamp to every message. If you use Linux with journald, you can get high-precision time stamps using
journalctl -o short-monotonic _COMM=logger
Many traditional syslog daemons also offer high precision time stamps (milliseconds should be sufficient for shell scripts).
Here's an example from a script that I was just profiling in this manner:
[1940949.100362] bremer root[16404]: + zcat /boot/symvers-5.3.18-57-default.gz
[1940949.111138] bremer root[16404]: + '[' -e /var/tmp/weak-modules2.OmYvUn/symvers-5.3.18-57-default ']'
[1940949.111315] bremer root[16404]: + args=(-E $tmpdir/symvers-$krel)
[1940949.111484] bremer root[16404]: ++ /usr/sbin/depmod -b / -ae -E /var/tmp/weak-modules2.OmYvUn/symvers-5.3.18-57-default 5.3.18-57>
[1940952.455272] bremer root[16404]: + output=
[1940952.455738] bremer root[16404]: + status=0
where you can see that the "depmod" command is taking a lot of time.

Copied from here:
Since I've ended up here at least twice now, I implemented a solution:
https://github.com/walles/shellprof
It runs your script, transparently clocks all lines printed, and at the end prints a top 10 list of the lines that were on screen the longest:
~/s/shellprof (master|✔) $ ./shellprof ./testcase.sh
quick
slow
quick
Timings for printed lines:
1.01s: slow
0.00s: <<<PROGRAM START>>>
0.00s: quick
0.00s: quick
~/s/shellprof (master|✔) $

I'm not aware of any shell profiling tools.
Historically one just rewrites too-slow shell scripts in Perl, Python, Ruby, or even C.
A less drastic idea would be to use a faster shell than bash. Dash and ash are available for all Unix-style systems and are typically quite a bit smaller and faster.

Related

How to time a bunch of things in a bash script

I want to wrote a script that runs a script/service (curls from a service) with different parameters. Now, I want to time each query and store it in a file? How can I do this?
#! /bin/bash
input="/home/ubuntu/flowers"
while IFS= read -r line
do
time myservice 'get?flower="$line"'
done < "$input"
I also tried :
cat flowers | xargs -I {} time myservice "get?flower={}" | jq -c '.[] | {flower}'
My output looks something like
/usr/local/lib/python2.7/dist-packages/gevent/builtins.py:96: CryptographyDeprecationWarning: Python 2 is no longer supported by the Python core team. Support for it is now deprecated in cryptography, and will be removed in the next release.
result = _import(*args, **kwargs)
{"flower":"daffodil"}
{"flower":"daffodil"}
{"flower":"daffodil"}
{"flower":"daffodil"}
0.47user 0.07system 0:10.49elapsed 5%CPU (0avgtext+0avgdata 65432maxresident)k
OR contains stuff like
Myservice 10.#.#.#:7092 returned bad json for get?flower=lilly
Myservice 10.#.#.#:7092 returned bad json for get?flower=lilly
Myservice 10.#.#.#:7092 returned bad json for get?flower=lilly
Failed to connect (or too slow) on 10.#.#.#2:7092 timed out
Timeout Error: ('10.#.#.#', 7092)
Failed to connect (or too slow) on 10.#.#.#:7092
Timeout Error: ('10.#.#.#', 7092)
Failed to connect (or too slow) on 10.#.#.#:7092
Timeout Error: ('10.#.#.#', 7092)
which I would like to skip.
I know I can do a clean up later if there isn't a simple way to do this.
I want a file that is something like
lilly 0.91
hibiscus 0.93
Where the number is the time on the userside.
If all you are looking for is the amount of time each query takes, and you don't care about the output from myservice, then you can redirect it to /dev/null and ignore it.
Measuring the time is a little more tricky. You cannot redirect output from the time command to a different place than the command it is running. So it is better to use other approaches. Bash has an internal variable "SECONDS" that can be used to measure elapsed time, but I think you want more granularity than that. So you should use the 'date' command instead.
You will also need to use bc (or similar) to do floating point arithmetic.
Also, if the myservice command handles failures correctly (i.e. returns a non-zero value upon failure), then you can also handle failures cleanly.
#!/bin/bash
input_file="/home/ubuntu/flowers"
while IFS= read -r line; do
start_time=$(date +%s.%N) #
myservice 'get?flower="$line"' > /dev/null 2>&1
return_value=$?
end_time=$(date +%s.%N)
elapsed_time=$(echo "scale=3; ${end_time} - ${start_time}" | bc)
if [ ${return_value} -eq 0 ]; then
echo "${line}: ${elapsed_time}"
else
echo "${line}: Failed"
fi
done < "${input_file}"
The %s.%3N format string in the date commands means:
%s Seconds since 1 Jan 1970
. The '.' character
%N Nanoseconds
The scale=3 input to the bc command tells it to output 3 decimal places.

(again) What's wrong with this youtube-dl automatic download bash script

sometime ago I asked what was wrong about a bash script I was trying to do and I got a great solution: What's wrong with this youtube-dl automatic script?
I kept modifying the script to work with different youtube-dl command combinations and to deal with my very unstable Internet connection (that's the reason for the while/do loop) and it kept working flawlessly, but when I tried to use that same script structure to download Youtube playlists starting from a specific item in the list (e.g.: item number 15) that's when I get an error. I'm still pretty much a newbie in bash script (obviously), so I don't know what's wrong.
The script in question is this:
#!/bin/bash
function video {
youtube-dl --no-warnings -o '%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s' --socket-timeout 15 --hls-use-mpegts -R 64 --fragment-retries 64 --prefer-free-formats --all-subs --embed-subs -f 'bestvideo[height<=720]+bestaudio/best[height<=720]' "$#"
}
read -p "url: " url
video "$url"
while [ $? -ne 0 ]; do sleep 5 && video "$url" ; done
clear && echo completed!
So, for example, if I try to download a playlist, I just write in my Terminal:
printf https://www.youtube.com/playlist?list=PLS1QulWo1RIYmaxcEqw5JhK3b-6rgdWO_ | list720
("list720" is the name of the script, of course) The script runs without problems and does exactly what I expect it to do.
But if I run in the Terminal:
printf --playlist-start=15 https://www.youtube.com/playlist?list=PLS1QulWo1RIYmaxcEqw5JhK3b-6rgdWO_ | list720
I get the following error:
bash: printf: --: invalid option
printf: usage: printf [-v var] format [arguments]
ERROR: '' is not a valid URL. Set --default-search "ytsearch" (or run youtube-dl "ytsearch:" ) to search YouTube
If I invert the order (1st the youtube URL and then the --playlist-start=15 command), the script downloads the whole playlist and omits the "--playlist-start" command.
I tried just running the youtube-dl command string directly in the terminal and added the "--playlist-start" and URL at the end and it runs perfectly:
youtube-dl --no-warnings -o '%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s' --socket-timeout 15 --hls-use-mpegts -R 64 --fragment-retries 64 --prefer-free-formats --all-subs --embed-subs -f 'bestvideo[height<=720]+bestaudio/best[height<=720]' --playlist-start=15 https://www.youtube.com/playlist?list=PLS1QulWo1RIYmaxcEqw5JhK3b-6rgdWO_
...so I assume the problem is with the script.
Any help is welcome, thanks!
A much better design is to accept any options and the URL as command-line arguments. Scripts which require interactive I/O are pesky to include in bigger scripts and generally harder to use (you lose the ability to use your shell's tab completion and command-line history etc).
#!/bin/bash
# Don't needlessly use Bash-only syntax for declaring a function
# Indent the code
video () {
youtube-dl --no-warnings \
-o '%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s' \
--socket-timeout 15 --hls-use-mpegts -R 64 --fragment-retries 64 \
--prefer-free-formats --all-subs --embed-subs \
-f 'bestvideo[height<=720]+bestaudio/best[height<=720]' "$#"
}
until video "$#"; do
sleep 5
done
Clearing the screen after finishing seems hostile so I took that out, too.
Now if you want to pass additional parameters to youtube-dl just include them as parameters to your script:
list720 --playlist-start=15 'https://www.youtube.com/playlist?list=PLS1QulWo1RIYmaxcEqw5JhK3b-6rgdWO_'
You should also usually quote any URLs in case they contain shell metacharacters. See also When to wrap quotes around a shell variable?
Notice how we always take care to use double quotes around "$#"; omitting them in this case is simply an error.
Notice also how inside the function, "$#" refers to the function's arguments, whereas in the main script, it refers to the script's command-line arguments.
Tangentially, using printf without a format string is problematic, too. If you pass in a string which contains a per-cent character, that will get interpreted as a format string.
bash$ printf 'http://example.com/%7Efnord'
http://example.com/0.000000E+00fnord
The proper solution is to always pass a format string as the first argument.
bash$ printf '%s\n' 'http://example.com/%7Efnord'
http://example.com/%7Efnord
But you don't need printf to pass something as standard input. Bash has "here strings":
list720 <<<'http://example.com/%7Efnord'
(This would of course only work with your old version which read the URL from standard input; the refactored script in this answer doesn't work that way.)
SOLVED!
My brother (a "retired" programmer) took some time to evaluate how Bash script works and we figured a way of making the script work in a simpler way by just adding the youtube-dl commands and the Youtube URL as arguments.
The script changed a little bit, now it looks like this:
#!/bin/bash
function video() {
youtube-dl --no-warnings -o '%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s' --socket-timeout 15 --hls-use-mpegts -R 64 --fragment-retries 64 --prefer-free-formats --all-subs --embed-subs -f 'bestvideo[height<=720]+bestaudio/best[height<=720]' "$#"
}
video $#
while [ $? -ne 0 ]; do sleep 5 && video $# ; done
clear && echo completed!
Now I just have to write in my terminal:
list720 --playlist-start=15 https://www.youtube.com/playlist?list=PLS1QulWo1RIYmaxcEqw5JhK3b-6rgdWO_
And it works exactly as I want it to.
Thank you very much for your help and suggestions!
In bash the character '-' at the begin of a command is used to set an option, if you wants to print --playlist... you should use the escape character '\'.
Try something like printf "\-\-playlist..."
correction : printf '%s' '--playlist...'

Use PS0 and PS1 to display execution time of each bash command

It seems that by executing code in PS0 and PS1 variables (which are eval'ed before and after a prompt command is run, as I understand) it should be possible to record time of each running command and display it in the prompt. Something like that:
user#machine ~/tmp
$ sleep 1
user#machine ~/tmp 1.01s
$
However, I quickly got stuck with recording time in PS0, since something like this doesn't work:
PS0='$(START=$(date +%s.%N))'
As I understand, START assignment happens in a sub-shell, so it is not visible in the outer shell. How would you approach this?
I was looking for a solution to a different problem and came upon this question, and decided that sounds like a cool feature to have. Using #Scheff's excellent answer as a base in addition to the solutions I developed for my other problem, I came up with a more elegant and full featured solution.
First, I created a few functions that read/write the time to/from memory. Writing to the shared memory folder prevents disk access and does not persist on reboot if the files are not cleaned for some reason
function roundseconds (){
# rounds a number to 3 decimal places
echo m=$1";h=0.5;scale=4;t=1000;if(m<0) h=-0.5;a=m*t+h;scale=3;a/t;" | bc
}
function bash_getstarttime (){
# places the epoch time in ns into shared memory
date +%s.%N >"/dev/shm/${USER}.bashtime.${1}"
}
function bash_getstoptime (){
# reads stored epoch time and subtracts from current
local endtime=$(date +%s.%N)
local starttime=$(cat /dev/shm/${USER}.bashtime.${1})
roundseconds $(echo $(eval echo "$endtime - $starttime") | bc)
}
The input to the bash_ functions is the bash PID
Those functions and the following are added to the ~/.bashrc file
ROOTPID=$BASHPID
bash_getstarttime $ROOTPID
These create the initial time value and store the bash PID as a different variable that can be passed to a function. Then you add the functions to PS0 and PS1
PS0='$(bash_getstarttime $ROOTPID) etc..'
PS1='\[\033[36m\] Execution time $(bash_getstoptime $ROOTPID)s\n'
PS1="$PS1"'and your normal PS1 here'
Now it will generate the time in PS0 prior to processing terminal input, and generate the time again in PS1 after processing terminal input, then calculate the difference and add to PS1. And finally, this code cleans up the stored time when the terminal exits:
function runonexit (){
rm /dev/shm/${USER}.bashtime.${ROOTPID}
}
trap runonexit EXIT
Putting it all together, plus some additional code being tested, and it looks like this:
The important parts are the execution time in ms, and the user.bashtime files for all active terminal PIDs stored in shared memory. The PID is also shown right after the terminal input, as I added display of it to PS0, and you can see the bashtime files added and removed.
PS0='$(bash_getstarttime $ROOTPID) $ROOTPID experiments \[\033[00m\]\n'
As #tc said, using arithmetic expansion allows you to assign variables during the expansion of PS0 and PS1. Newer bash versions also allow PS* style expansion so you don't even need a subshell to get the current time. With bash 4.4:
# PS0 extracts a substring of length 0 from PS1; as a side-effect it causes
# the current time as epoch seconds to PS0time (no visible output in this case)
PS0='\[${PS1:$((PS0time=\D{%s}, PS1calc=1, 0)):0}\]'
# PS1 uses the same trick to calculate the time elapsed since PS0 was output.
# It also expands the previous command's exit status ($?), the current time
# and directory ($PWD rather than \w, which shortens your home directory path
# prefix to "~") on the next line, and finally the actual prompt: 'user#host> '
PS1='\nSeconds: $((PS1calc ? \D{%s}-$PS0time : 0)) Status: $?\n\D{%T} ${PWD:PS1calc=0}\n\u#\h> '
(The %N date directive does not seem to be implemented as part of \D{...} expansion with bash 4.4. This is a pity since we only have a resolution in single second units.)
Since PS0 is only evaluated and printed if there is a command to execute, the PS1calc flag is set to 1 to do the time difference (following the command) in PS1 expansion or not (PS1calc being 0 means PS0 was not previously expanded and so didn't re-evaluate PS1time). PS1 then resets PS1calc to 0. In this way an empty line (just hitting return) doesn't accumulate seconds between return key presses.
One nice thing about this method is that there is no output when you have set -x active. No subshells or temporary files in sight: everything is done within the bash process itself.
I took this as puzzle and want to show the result of my puzzling:
First I fiddled with time measurement. The date +%s.%N (which I didn't realize before) was where I started from. Unfortunately, it seems that bashs arithmetic evaluation seems not to support floating points. Thus, I chosed something else:
$ START=$(date +%s.%N)
$ awk 'BEGIN { printf("%fs", '$(date +%s.%N)' - '$START') }' /dev/null
8.059526s
$
This is sufficient to compute the time difference.
Next, I confirmed what you already described: sub-shell invocation prevents usage of shell variables. Thus, I thought about where else I could store the start time which is global for sub-shells but local enough to be used in multiple interactive shells concurrently. My solution are temp. files (in /tmp). To provide a unique name I came up with this pattern: /tmp/$USER.START.$BASHPID.
$ date +%s.%N >/tmp/$USER.START.$BASHPID ; \
> awk 'BEGIN { printf("%fs", '$(date +%s.%N)' - '$(cat /tmp/$USER.START.$BASHPID)') }' /dev/null
cat: /tmp/ds32737.START.11756: No such file or directory
awk: cmd. line:1: BEGIN { printf("%fs", 1491297723.111219300 - ) }
awk: cmd. line:1: ^ syntax error
$
Damn! Again I'm trapped in the sub-shell issue. To come around this, I defined another variable:
$ INTERACTIVE_BASHPID=$BASHPID
$ date +%s.%N >/tmp/$USER.START.$INTERACTIVE_BASHPID ; \
> awk 'BEGIN { printf("%fs", '$(date +%s.%N)' - '$(cat /tmp/$USER.START.$INTERACTIVE_BASHPID)') }' /dev/null
0.075319s
$
Next step: fiddle this together with PS0 and PS1. In a similar puzzle (SO: How to change bash prompt color based on exit code of last command?), I already mastered the "quoting hell". Thus, I should be able to do it again:
$ PS0='$(date +%s.%N >"/tmp/${USER}.START.${INTERACTIVE_BASHPID}")'
$ PS1='$(awk "BEGIN { printf(\"%fs\", "$(date +%s.%N)" - "$(cat /tmp/$USER.START.$INTERACTIVE_BASHPID)") }" /dev/null)'"$PS1"
0.118550s
$
Ahh. It starts to work. Thus, there is only one issue - to find the right start-up script for the initialization of INTERACTIVE_BASHPID. I found ~/.bashrc which seems to be the right one for this, and which I already used in the past for some other personal customizations.
So, putting it all together - these are the lines I added to my ~/.bashrc:
# command duration puzzle
INTERACTIVE_BASHPID=$BASHPID
date +%s.%N >"/tmp/${USER}.START.${INTERACTIVE_BASHPID}"
PS0='$(date +%s.%N >"/tmp/${USER}.START.${INTERACTIVE_BASHPID}")'
PS1='$(awk "BEGIN { printf(\"%fs\", "$(date +%s.%N)" - "$(cat /tmp/$USER.START.$INTERACTIVE_BASHPID)") }" /dev/null)'"$PS1"
The 3rd line (the date command) has been added to solve another issue. Comment it out and start a new interactive bash to find out why.
A snapshot of my cygwin xterm with bash where I added the above lines to ./~bashrc:
Notes:
I consider this rather as solution to a puzzle than a "serious productive" solution. I'm sure that this kind of time measurement consumes itself a lot of time. The time command might provide a better solution: SE: How to get execution time of a script effectively?. However, this was a nice lecture for practicing the bash...
Don't forget that this code pollutes your /tmp directory with a growing number of small files. Either clean-up the /tmp from time to time or add the appropriate commands for clean-up (e.g. to ~/.bash_logout).
Arithmetic expansion runs in the current process and can assign to variables. It also produces output, which you can consume with something like \e[$((...,0))m (to output \e[0m) or ${t:0:$((...,0))} (to output nothing, which is presumably better). 64-bit integer support in Bash supports will count POSIX nanoseconds until the year 2262.
$ PS0='${t:0:$((t=$(date +%s%N),0))}'
$ PS1='$((( t )) && printf %d.%09ds $((t=$(date +%s%N)-t,t/1000000000)) $((t%1000000000)))${t:0:$((t=0))}\n$ '
0.053282161s
$ sleep 1
1.064178281s
$
$
PS0 is not evaluated for empty commands, which leaves a blank line (I'm not sure if you can conditionally print the \n without breaking things). You can work around that by switching to PROMPT_COMMAND instead (which also saves a fork):
$ PS0='${t:0:$((t=$(date +%s%N),0))}'
$ PROMPT_COMMAND='(( t )) && printf %d.%09ds\\n $((t=$(date +%s%N)-t,t/1000000000)) $((t%1000000000)); t=0'
0.041584565s
$ sleep 1
1.077152833s
$
$
That said, if you do not require sub-second precision, I would suggest using $SECONDS instead (which is also more likely to return a sensible answer if something sets the time).
As correctly stated in the question, PS0 runs inside a sub-shell which makes it unusable for this purpose of setting the start time.
Instead, one can use the history command with epoch seconds %s and the built-in variable $EPOCHSECONDS to calculate when the command finished by leveraging only $PROMPT_COMMAND.
# Save start time before executing command (does not work due to PS0 sub-shell)
# preexec() {
# STARTTIME=$EPOCHSECONDS
# }
# PS0=preexec
# Save end time, without duplicating commands when pressing Enter on an empty line
precmd() {
local st=$(HISTTIMEFORMAT='%s ' history 1 | awk '{print $2}');
if [[ -z "$STARTTIME" || (-n "$STARTTIME" && "$STARTTIME" -ne "$st") ]]; then
ENDTIME=$EPOCHSECONDS
STARTTIME=$st
else
ENDTIME=0
fi
}
__timeit() {
precmd;
if ((ENDTIME - STARTTIME >= 0)); then
printf 'Command took %d seconds.\n' "$((ENDTIME - STARTTIME))";
fi
# Do not forget your:
# - OSC 0 (set title)
# - OSC 777 (notification in gnome-terminal, urxvt; note, this one has preexec and precmd as OSC 777 features)
# - OSC 99 (notification in kitty)
# - OSC 7 (set url) - out of scope for this question
}
export PROMPT_COMMAND=__timeit
Note: If you have ignoredups in your $HISTCONTROL, then this will not report back for a command that is re-run.
Following #SherylHohman use of variables in PS0 I've come with this complete script. I've seen you don't need a PS0Time flag as PS0Calc doesn't exists on empty prompts so _elapsed funct just exit.
#!/bin/bash
# string preceding ms, use color code or ascii
_ELAPTXT=$'\E[1;33m \uf135 '
# extract time
_printtime () {
local _var=${EPOCHREALTIME/,/};
echo ${_var%???}
}
# get diff time, print it and end color codings if any
_elapsed () {
[[ -v "${1}" ]] || ( local _VAR=$(_printtime);
local _ELAPSED=$(( ${_VAR} - ${1} ));
echo "${_ELAPTXT}$(_formatms ${_ELAPSED})"$'\n\e[0m' )
}
# format _elapsed with simple string substitution
_formatms () {
local _n=$((${1})) && case ${_n} in
? | ?? | ???)
echo $_n"ms"
;;
????)
echo ${_n:0:1}${_n:0,-3}"ms"
;;
?????)
echo ${_n:0:2}","${_n:0,-3}"s"
;;
??????)
printf $((${_n:0:3}/60))m+$((${_n:0:3}%60)),${_n:0,-3}"s"
;;
???????)
printf $((${_n:0:4}/60))m$((${_n:0:4}%60))s${_n:0,-3}"ms"
;;
*)
printf "too much!"
;;
esac
}
# prompts
PS0='${PS1:(PS0time=$(_printtime)):0}'
PS1='$(_elapsed $PS0time)${PS0:(PS0time=0):0}\u#\h:\w\$ '
img of result
Save it as _prompt and source it to try:
source _prompt
Change text, ascii codes and colors in _ELAPTXT
_ELAPTXT='\e[33m Elapsed time: '

Detect whether script is being run by Bash

I have put together the following to detect if a script is being run by Bash or not:
################################################################################
# Checks whether execution is going through Bash, aborting if it isn't. TinoSino
current_shell="$(
ps `# Report a snapshot of the current processes` \
-p $$ `# select by PID` \
-o comm `# output column: Executable namename` \
|\
paste `# Merge lines of files ` \
-s `# paste one file at a time instead of in parallel` \
- `# into standard output` \
|\
awk `# Pick from list of tokens` \
'{ print $NF }' `# print only last field of the command output`
)"
current_shell="${current_shell#-}" # Remove starting '-' character if present
if [ ! "${current_shell}" = 'bash' ]; then
echo "This script is meant to be executed by the Bash shell but it isn't."
echo 'Continuing from another shell may lead to unpredictable results.'
echo 'Execution will be aborted... now.'
return 0
fi
unset current_shell
################################################################################
I am not asking you specifically to code review it because you would be sending me to CodeReview; my question is:
how would you go about testing whether this "execution guard" put at the top of my script does indeed do its job reliably?
I am thinking about installing Virtual Machines and on each machine to install things like zsh, csh, etc. But it looks way too time-consuming to me. Better ways to do this?
Should you spot an immediate mistake do point it out to me though please. Just glaring bugs waving their legs waiting to be squashed should be squashed, I think.
This is better written as
if [ -z "$BASH_VERSION" ]
then
echo "Please run me in bash"
exit 1
fi
As for testing, get a list of non-bash shells from /etc/shells, and just run the script with each of them verifying that you get your error message.
getshver (recommended)
whatshell
which_interpreter (silly)
I would only recommend rolling your own if it isn't critical to "guarantee" correct results. I don't think such a guarantee is even possible, but most of the time you're at most targeting a few shells and only care about modern versions. Very few people should even care about version detection. Writing portable code while going outside of POSIX requires knowing what you're doing.
Don't bother detecting the shell just to abort. If people want to shoot themselves in the foot by ignoring the shebang that's their problem.

Shell script takes a list of commands as input, tries to execute them, and fails

I am, like many non-engineers or non-mathematicians who try writing algorithms, an intuitive. My exact psychological typology makes it quite difficult for me to learn anything serious like computers or math. Generally, I prefer audio, because I can engage my imagination more effectively in the learning process.
That said, I am trying to write a shell script that will help me master Linux. To that end, I copied and pasted a list of Linux commands from the O'Reilly website's index to the book Python In a Nutshell. I doubt they'll mind, and I thank them for providing it. These are the textfile `massivelistoflinuxcommands,' not included fully below in order to save space...
OK, now comes the fun part. How do I get this script to work?
#/bin/sh
read -d 'massivelistoflinuxcommands' commands <<EOF
accept
bison
bzcmp
bzdiff
bzgrep
bzip2
bzless
bzmore
c++
lastb
lastlog
strace
strfile
zmore
znew
EOF
for i in $commands
do
$i --help | less | cat > masterlinuxnow
text2wave masterlinuxnow -o ml.wav
done
It really helps when you include error messages or specific ways that something deviates from expected behavior.
However, your problem is here:
read -d 'massivelistoflinuxcommands' commands <<EOF
It should be:
read -d '' commands <<EOF
The delimiter to read causes it to stop at the first character it finds that matches the first character in the string, so it stops at "bzc" because the next character is "m" which matches the "m" at the beginning of "massive..."
Also, I have no idea what this is supposed to do:
$i --help | less | cat > masterlinuxnow
but it probably should be:
$i --help > masterlinuxnow
However, you should be able to pipe directly into text2wave and skip creating an intermediate file:
$i --help | text2wave -o ml.wav
Also, you may want to prevent each file from overwriting the previous one:
$i --help | text2wave -o ml-$i.wav
That will create files named like "ml-accept.wav" and "ml-bison.wav".
I would point out that if you're learning Linux commands, you should prioritize them by frequency of use and/or applicability to a beginner. For example, you probably won't be using bison right away`.
The first problem here is that not every command has a --help option!! In fact the very first command, accept, has no such option! A better approach might be executing man on each command since a manual page is more likely to exist for each of the commands. Thus change;
$i --help | less | cat > masterlinuxnow
to
man $i >> masterlinuxnow
note that it is essential you use the append output operator ">>" instead of the create output operator ">" in this loop. Using the create output operator will recreate the file "masterlinuxnow" on each iteration thus containing only the output of the last "man $i" processed.
you also need to worry about whether the command exists on your version of linux (many commands are not included in the standard distribution or may have different names). Thus you probably want something more like this where the -n in the head command should be replace by the number of lines you want, so if you want only the first 2 lines of the --help output you would replace -n with -2:
if [ $(which $i) ]
then
$i --help | head -n >> masterlinuxnow
fi
and instead of the read command, simply define the variable commands like so:
commands="
bison
bzcmp
bzdiff
bzgrep
bzip2
bzless
bzmore
c++
lastb
lastlog
strace
strfile
zmore
znew
"
Putting this all together, the following script works quite nicely:
commands="
bison
bzcmp
bzdiff
bzgrep
bzip2
bzless
bzmore
c++
lastb
lastlog
strace
strfile
zmore
znew
"
for i in $commands
do
if [ $(which $i) ]
then
$i --help | head -1 >> masterlinuxnow 2>/dev/null
fi
done
You're going to learn to use Linux by listening to help descriptions? I really think that's a bad idea.
Those help commands usually list every obscure option to a command, including many that you will never use-- especially as a beginner.
A guided tutorial or book would be much better. It would only present the commands and options that will be most useful. For example, that list of commands you gave has many that I don't know-- and I've been using Linux/Unix extensively for 10 years.

Resources