Here's the script I'm talking about
#!/bin/bash
_sv="77777777777777777a"
if [ -n "${!_sv}" ]; then
echo "the value is there"
${echo} "${!sv}"
elif [ -n "${!1}" ]; then
echo "${!1}"
else
echo "False"
fi
When I run it with sh test.sh or as bash test.sh in GNU bash in Windows10 (using Cmder) it give me a bad substitution error
dir_1/test.sh: line 2: 77777777777777777a: bad substitution
But the same script runs without any errors in macOS terminal shell and in Windows10, if and only if the variable is purely alphabetic or numeric.
IMPORTANT: If the _sv value is alphanumeric, it gives bad substitution error.
Please help!
This is expected behavior. You get this error because your name you are trying to expand is not a valid variable name.
This check was introduced in Bash 4.4 commit a0c0a00fc419b7:
## -5926,6 +6661,16 ## parameter_brace_expand_indir (name, var_is_special, quoted, quoted_dollar_atp, c
if (t == 0)
return (WORD_DESC *)NULL;
+ if (valid_brace_expansion_word (t, SPECIAL_VAR (t, 0)) == 0)
+ {
+ report_error (_("%s: bad substitution"), t);
+ free (t);
+ w = alloc_word_desc ();
+ w->word = &expand_param_error;
+ w->flags = 0;
+ return (w);
+ }
+
w = parameter_brace_expand_word (t, SPECIAL_VAR(t, 0), quoted, 0, 0);
free (t);
Previous versions instead expanded this to an empty string. This includes macOS's Bash 3.2.
Related
In the bash man page, it states:
Exit immediately if a pipeline (which may consist of a single simple command),
a subshell command enclosed in parentheses, or one of the commands executed as
part of a command list enclosed by braces...
So I assumed that a function should be considered a command list enclosed by braces. However, if you apply a conditional to the function call, errexit no longer persists inside the function body and it executes the entire command list before returning. Even if you explicitly create a subshell inside the function with errexit enabled for that subshell, all commands in the command list are executed. Here is a simple example that demonstrates the issue:
function a() { b ; c ; d ; e ; }
function ap() { { b ; c ; d ; e ; } ; }
function as() { ( set -e ; b ; c ; d ; e ) ; }
function b() { false ; }
function c() { false ; }
function d() { false ; }
function e() { false ; }
( set -Eex ; a )
+ a
+ b
+ false
( set -Eex ; ap )
+ ap
+ b
+ false
( set -Eex ; as )
+ as
+ set -e
+ b
+ false
Now if I apply a conditional to each of them...
( set -Eex ; a || false )
+ a
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false
( set -Eex ; ap || false )
+ ap
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false
( set -Eex ; as )
+ as
+ set -e
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false
You started to quote the manual but then you cut the bit that explained this behaviour, which was in the very next sentence:
-e Exit immediately if a pipeline, which may consist of a single simple command, a subshell command enclosed in parentheses, or one of the commands executed as part of a command list enclosed by braces returns a non-zero status. The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test in an if statement, part of any command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command’s return status is being inverted with !.
bug-bash mailing list has an explanation by Eric Blake more explicit about functions:
Short answer: historical compatibility.
...
Indeed, the correct behavior mandated by POSIX (namely, that 'set -e' is
completely ignored for the duration of the entire body of f(), because f
was invoked in a context that ignores 'set -e') is not intuitive. But
it is standardized, so we have to live with it.
And some words about whether set -e can be exploited to achieve the wanted behavior:
Because once you are in a context that ignores 'set -e', the historical
behavior is that there is no further way to turn it back on, for that
entire body of code in the ignored context. That's how it was done 30
years ago, before shell functions were really thought about, and we are
stuck with that poor design decision.
Not an answer to the original question, but a work-around for the underlying problem: set up a trap on errors:
function on_error() {
echo "error happened!"
}
trap on_error ERR
echo "OK so far"
false
echo "this line should not execute"
The reason for the behavior itself is properly explained in other answers (basically it's expected bash behavior as per the manual and POSIX): https://stackoverflow.com/a/19789651/1091436
not an answer but you might fix this counter intuitive behaviur by defining this helper function:
fixerrexit() { ( eval "expr '$-' : '.*e' >/dev/null && set -e; $*"; ); }
then call your functions via fixerrexit.
example:
f1()
{
mv unimportant-0.txt important.txt
rm unimportant-*.txt
}
set -e
if fixerrexit f1
then
echo here is the important file: important.txt
echo unimportant files are deleted
fi
if outer context has errexit on, then fixerrexit turns on errexit inside f1() as well, so you dont need to worry about commands being executed after a failure occurs.
the only downside is you can not set variables since it runs f1 inside a subshell.
Using Mac Terminal and Shell Script
Trying to do Shortcut of add and assignment operator like
SumVar=1
$(( SumVar += 5 ))
echo $SumVar
Getting error :
Error: line 3: 5: command not found
What is the correct syntax of this ?
Your syntax for incrementing the variable is correct, but you are using it in a context where Bash wants a command, and it complains that the result of the increment (4) is not a recognized command.
The let keyword is your friend.
let SumVar+=4
Or better yet just leave out the dollar sign (thanks #chepner);
(( SumVar += 4 ))
When you use a substitution like this, bash will try to execute a command of the substituted name/value. You can use this instead:
(( Sumvar += 5 ))
Or if you really insist on using substitution here, you can use it as an argument to the : command:
: $(( Sumvar += 5 ))
See the discussion at Is `command -v` option required in a POSIX shell? Is posh compliant with POSIX?. It describes that type as well as command -v option is optional in POSIX.1-2004.
The answer marked correct at Check if a program exists from a Bash script doesn't help either. Just like type, hash is also marked as XSI in POSIX.1-2004. See http://pubs.opengroup.org/onlinepubs/009695399/utilities/hash.html.
Then what would be a POSIX compliant way to write a shell script to find if a command exists on the system or not?
How do you want to go about it? You can look for the command on directories in the current value of $PATH; you could look in the directories specified by default for the system PATH (getconf PATH as long as getconf
exists on PATH).
Which implementation language are you going to use? (For example: I have a Perl implementation that does a decent job finding executables on $PATH — but Perl is not part of POSIX; is it remotely relevant to you?)
Why not simply try running it? If you're going to deal with Busybox-based systems, lots of the executables can't be found by searching — they're built into the shell. The major caveat is if a command does something dangerous when run with no arguments — but very few POSIX commands, if any, do that. You might also need to determine what command exit statuses indicate that the command is not found versus the command objecting to not being called with appropriate arguments. And there's little guarantee that all systems will be consistent on that. It's a fraught process, in case you hadn't gathered.
Perl implementation pathfile
#!/usr/bin/env perl
#
# #(#)$Id: pathfile.pl,v 3.4 2015/10/16 19:39:23 jleffler Exp $
#
# Which command is executed
# Loosely based on 'which' from Kernighan & Pike "The UNIX Programming Environment"
#use v5.10.0; # Uses // defined-or operator; not in Perl 5.8.x
use strict;
use warnings;
use Getopt::Std;
use Cwd 'realpath';
use File::Basename;
my $arg0 = basename($0, '.pl');
my $usestr = "Usage: $arg0 [-AafhqrsVwx] [-p path] command ...\n";
my $hlpstr = <<EOS;
-A Absolute pathname (determined by realpath)
-a Print all possible matches
-f Print names of files (as opposed to symlinks, directories, etc)
-h Print this help message and exit
-q Quiet mode (don't print messages about files not found)
-r Print names of files that are readable
-s Print names of files that are not empty
-V Print version information and exit
-w Print names of files that are writable
-x Print names of files that are executable
-p path Use PATH
EOS
sub usage
{
print STDERR $usestr;
exit 1;
}
sub help
{
print $usestr;
print $hlpstr;
exit 0;
}
sub version
{
my $version = 'PATHFILE Version $Revision: 3.4 $ ($Date: 2015/10/16 19:39:23 $)';
# Beware of RCS hacking at RCS keywords!
# Convert date field to ISO 8601 (ISO 9075) notation
$version =~ s%\$(Date:) (\d\d\d\d)/(\d\d)/(\d\d) (\d\d:\d\d:\d\d) \$%\$$1 $2-$3-$4 $5 \$%go;
# Remove keywords
$version =~ s/\$([A-Z][a-z]+|RCSfile): ([^\$]+) \$/$2/go;
print "$version\n";
exit 0;
}
my %opts;
usage unless getopts('AafhqrsVwxp:', \%opts);
version if ($opts{V});
help if ($opts{h});
usage unless scalar(#ARGV);
# Establish test and generate test subroutine.
my $chk = 0;
my $test = "-x";
my $optlist = "";
foreach my $opt ('f', 'r', 's', 'w', 'x')
{
if ($opts{$opt})
{
$chk++;
$test = "-$opt";
$optlist .= " -$opt";
}
}
if ($chk > 1)
{
$optlist =~ s/^ //;
$optlist =~ s/ /, /g;
print STDERR "$arg0: mutually exclusive arguments ($optlist) given\n";
usage;
}
my $chk_ref = eval "sub { my(\$cmd) = \#_; return -f \$cmd && $test \$cmd; }";
my #PATHDIRS;
my %pathdirs;
my $path = defined($opts{p}) ? $opts{p} : $ENV{PATH};
#foreach my $element (split /:/, $opts{p} // $ENV{PATH})
foreach my $element (split /:/, $path)
{
$element = "." if $element eq "";
push #PATHDIRS, $element if $pathdirs{$element}++ == 0;
}
my $estat = 0;
CMD:
foreach my $cmd (#ARGV)
{
if ($cmd =~ m%/%)
{
if (&$chk_ref($cmd))
{
print "$cmd\n" unless $opts{q};
next CMD;
}
print STDERR "$arg0: $cmd: not found\n" unless $opts{q};
$estat = 1;
}
else
{
my $found = 0;
foreach my $directory (#PATHDIRS)
{
my $file = "$directory/$cmd";
if (&$chk_ref($file))
{
$file = realpath($file) if $opts{A};
print "$file\n" unless $opts{q};
next CMD unless defined($opts{a});
$found = 1;
}
}
print STDERR "$arg0: $cmd: not found\n" unless $found || $opts{q};
$estat = 1;
}
}
exit $estat;
A recent vulnerability, CVE-2014-6271, in how Bash interprets environment variables was disclosed. The exploit relies on Bash parsing some environment variable declarations as function definitions, but then continuing to execute code following the definition:
$ x='() { echo i do nothing; }; echo vulnerable' bash -c ':'
vulnerable
But I don't get it. There's nothing I've been able to find in the Bash manual about interpreting environment variables as functions at all (except for inheriting functions, which is different). Indeed, a proper named function definition is just treated as a value:
$ x='y() { :; }' bash -c 'echo $x'
y() { :; }
But a corrupt one prints nothing:
$ x='() { :; }' bash -c 'echo $x'
$ # Nothing but newline
The corrupt function is unnamed, and so I can't just call it. Is this vulnerability a pure implementation bug, or is there an intended feature here, that I just can't see?
Update
Per Barmar's comment, I hypothesized the name of the function was the parameter name:
$ n='() { echo wat; }' bash -c 'n'
wat
Which I could swear I tried before, but I guess I didn't try hard enough. It's repeatable now. Here's a little more testing:
$ env n='() { echo wat; }; echo vuln' bash -c 'n'
vuln
wat
$ env n='() { echo wat; }; echo $1' bash -c 'n 2' 3 -- 4
wat
…so apparently the args are not set at the time the exploit executes.
Anyway, the basic answer to my question is, yes, this is how Bash implements inherited functions.
This seems like an implementation bug.
Apparently, the way exported functions work in bash is that they use specially-formatted environment variables. If you export a function:
f() { ... }
it defines an environment variable like:
f='() { ... }'
What's probably happening is that when the new shell sees an environment variable whose value begins with (), it prepends the variable name and executes the resulting string. The bug is that this includes executing anything after the function definition as well.
The fix described is apparently to parse the result to see if it's a valid function definition. If not, it prints the warning about the invalid function definition attempt.
This article confirms my explanation of the cause of the bug. It also goes into a little more detail about how the fix resolves it: not only do they parse the values more carefully, but variables that are used to pass exported functions follow a special naming convention. This naming convention is different from that used for the environment variables created for CGI scripts, so an HTTP client should never be able to get its foot into this door.
The following:
x='() { echo I do nothing; }; echo vulnerable' bash -c 'typeset -f'
prints
vulnerable
x ()
{
echo I do nothing
}
declare -fx x
seems, than Bash, after having parsed the x=..., discovered it as a function, exported it, saw the declare -fx x and allowed the execution of the command after the declaration.
echo vulnerable
x='() { x; }; echo vulnerable' bash -c 'typeset -f'
prints:
vulnerable
x ()
{
echo I do nothing
}
and running the x
x='() { x; }; echo Vulnerable' bash -c 'x'
prints
Vulnerable
Segmentation fault: 11
segfaults - infinite recursive calls
It doesn't overrides already defined function
$ x() { echo Something; }
$ declare -fx x
$ x='() { x; }; echo Vulnerable' bash -c 'typeset -f'
prints:
x ()
{
echo Something
}
declare -fx x
e.g. the x remains the previously (correctly) defined function.
For the Bash 4.3.25(1)-release the vulnerability is closed, so
x='() { echo I do nothing; }; echo Vulnerable' bash -c ':'
prints
bash: warning: x: ignoring function definition attempt
bash: error importing function definition for `x'
but - what is strange (at least for me)
x='() { x; };' bash -c 'typeset -f'
STILL PRINTS
x ()
{
x
}
declare -fx x
and the
x='() { x; };' bash -c 'x'
segmentation faults too, so it STILL accept the strange function definition...
I think it's worth looking at the Bash code itself. The patch gives a bit of insight as to the problem. In particular,
*** ../bash-4.3-patched/variables.c 2014-05-15 08:26:50.000000000 -0400
--- variables.c 2014-09-14 14:23:35.000000000 -0400
***************
*** 359,369 ****
strcpy (temp_string + char_index + 1, string);
! if (posixly_correct == 0 || legal_identifier (name))
! parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST);
!
! /* Ancient backwards compatibility. Old versions of bash exported
! functions like name()=() {...} */
! if (name[char_index - 1] == ')' && name[char_index - 2] == '(')
! name[char_index - 2] = '\0';
if (temp_var = find_function (name))
--- 364,372 ----
strcpy (temp_string + char_index + 1, string);
! /* Don't import function names that are invalid identifiers from the
! environment, though we still allow them to be defined as shell
! variables. */
! if (legal_identifier (name))
! parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST|SEVAL_FUNCDEF|SEVAL_ONECMD);
if (temp_var = find_function (name))
When Bash exports a function, it shows up as an environment variable, for example:
$ foo() { echo 'hello world'; }
$ export -f foo
$ cat /proc/self/environ | tr '\0' '\n' | grep -A1 foo
foo=() { echo 'hello world'
}
When a new Bash process finds a function defined this way in its environment, it evalutes the code in the variable using parse_and_execute(). For normal, non-malicious code, executing it simply defines the function in Bash and moves on. However, because it's passed to a generic execution function, Bash will correctly parse and execute additional code defined in that variable after the function definition.
You can see that in the new code, a flag called SEVAL_ONECMD has been added that tells Bash to only evaluate the first command (that is, the function definition) and SEVAL_FUNCDEF to only allow functio0n definitions.
In regard to your question about documentation, notice here in the commandline documentation for the env command, that a study of the syntax shows that env is working as documented.
There are, optionally, 4 possible options
An optional hyphen as a synonym for -i (for backward compatibility I assume)
Zero or more NAME=VALUE pairs. These are the variable assignment(s) which could include function definitions.
Note that no semicolon (;) is required between or following the assignments.
The last argument(s) can be a single command followed by its argument(s). It will run with whatever permissions have been granted to the login being used. Security is controlled by restricting permissions on the login user and setting permissions on user-accessible executables such that users other than the executable's owner can only read and execute the program, not alter it.
[ spot#LX03:~ ] env --help
Usage: env [OPTION]... [-] [NAME=VALUE]... [COMMAND [ARG]...]
Set each NAME to VALUE in the environment and run COMMAND.
-i, --ignore-environment start with an empty environment
-u, --unset=NAME remove variable from the environment
--help display this help and exit
--version output version information and exit
A mere - implies -i. If no COMMAND, print the resulting environment.
Report env bugs to bug-coreutils#gnu.org
GNU coreutils home page: <http://www.gnu.org/software/coreutils/>
General help using GNU software: <http://www.gnu.org/gethelp/>
Report env translation bugs to <http://translationproject.org/team/>
Is there a way to embed the last command's elapsed wall time in a Bash prompt? I'm hoping for something that would look like this:
[last: 0s][/my/dir]$ sleep 10
[last: 10s][/my/dir]$
Background
I often run long data-crunching jobs and it's useful to know how long they've taken so I can estimate how long it will take for future jobs. For very regular tasks, I go ahead and record this information rigorously using appropriate logging techniques. For less-formal tasks, I'll just prepend the command with time.
It would be nice to automatically time every single interactive command and have the timing information printed in a few characters rather than 3 lines.
This is minimal stand-alone code to achieve what you want:
function timer_start {
timer=${timer:-$SECONDS}
}
function timer_stop {
timer_show=$(($SECONDS - $timer))
unset timer
}
trap 'timer_start' DEBUG
PROMPT_COMMAND=timer_stop
PS1='[last: ${timer_show}s][\w]$ '
Using your replies and some other threads, I wrote this prompt which I want to share with you. I took a screenshot in wich you can see :
White : Last return code
Green and tick mark means success (return code was 0)
Red and cross mark means error (return code was >0)
(Green or Red) : Last command execution time in parenthesis
(Green or Red) : Current date time (\t)
(Green if not root, Red if root) : the logged username
(Green) : the server name
(Blue) : the pwd directory and the usual $
Here is the code to put in your ~/.bashrc file :
function timer_now {
date +%s%N
}
function timer_start {
timer_start=${timer_start:-$(timer_now)}
}
function timer_stop {
local delta_us=$((($(timer_now) - $timer_start) / 1000))
local us=$((delta_us % 1000))
local ms=$(((delta_us / 1000) % 1000))
local s=$(((delta_us / 1000000) % 60))
local m=$(((delta_us / 60000000) % 60))
local h=$((delta_us / 3600000000))
# Goal: always show around 3 digits of accuracy
if ((h > 0)); then timer_show=${h}h${m}m
elif ((m > 0)); then timer_show=${m}m${s}s
elif ((s >= 10)); then timer_show=${s}.$((ms / 100))s
elif ((s > 0)); then timer_show=${s}.$(printf %03d $ms)s
elif ((ms >= 100)); then timer_show=${ms}ms
elif ((ms > 0)); then timer_show=${ms}.$((us / 100))ms
else timer_show=${us}us
fi
unset timer_start
}
set_prompt () {
Last_Command=$? # Must come first!
Blue='\[\e[01;34m\]'
White='\[\e[01;37m\]'
Red='\[\e[01;31m\]'
Green='\[\e[01;32m\]'
Reset='\[\e[00m\]'
FancyX='\342\234\227'
Checkmark='\342\234\223'
# Add a bright white exit status for the last command
PS1="$White\$? "
# If it was successful, print a green check mark. Otherwise, print
# a red X.
if [[ $Last_Command == 0 ]]; then
PS1+="$Green$Checkmark "
else
PS1+="$Red$FancyX "
fi
# Add the ellapsed time and current date
timer_stop
PS1+="($timer_show) \t "
# If root, just print the host in red. Otherwise, print the current user
# and host in green.
if [[ $EUID == 0 ]]; then
PS1+="$Red\\u$Green#\\h "
else
PS1+="$Green\\u#\\h "
fi
# Print the working directory and prompt marker in blue, and reset
# the text color to the default.
PS1+="$Blue\\w \\\$$Reset "
}
trap 'timer_start' DEBUG
PROMPT_COMMAND='set_prompt'
Another very minimal approach is:
trap 'SECONDS=0' DEBUG
export PS1='your_normal_prompt_here ($SECONDS) # '
This shows the number of seconds since the last simple command was started. The counter is not reset if you simply hit Enter without entering a command -- which can be handy when you just want to see how long the terminal has been up since you last did anything in it. It works fine for me in Red Hat and Ubuntu. It did NOT work for me under Cygwin, but I'm not sure if that's a bug or just a limitation of trying to run Bash under Windows.
One possible drawback to this approach is that you keep resetting SECONDS, but if you truly need to preserve SECONDS as the number of seconds since initial shell invocation, you can create your own variable for the PS1 counter instead of using SECONDS directly. Another possible drawback is that a large seconds value such as "999999" might be be better displayed as days+hours+minutes+seconds, but it's easy to add a simple filter such as:
seconds2days() { # convert integer seconds to Ddays,HH:MM:SS
printf "%ddays,%02d:%02d:%02d" $(((($1/60)/60)/24)) \
$(((($1/60)/60)%24)) $((($1/60)%60)) $(($1%60)) |
sed 's/^1days/1day/;s/^0days,\(00:\)*//;s/^0//' ; }
trap 'SECONDS=0' DEBUG
PS1='other_prompt_stuff_here ($(seconds2days $SECONDS)) # '
This translates "999999" into "11days,13:46:39". The sed at the end changes "1days" to "1day", and trims off empty leading values such as "0days,00:". Adjust to taste.
You could utilize this zsh-borrowed hook for bash: http://www.twistedmatrix.com/users/glyph/preexec.bash.txt
Timing done with this hook (Mac OS X): Use Growl to monitor long-running shell commands
If you hadn't set up any of the other answers before you kicked off your long-running job and you just want to know how long the job took, you can do the simple
$ HISTTIMEFORMAT="%s " history 2
and it will reply with something like
654 1278611022 gvn up
655 1278611714 HISTTIMEFORMAT="%s " history 2
and you can then just visually subtract the two timestamps (anybody know how to capture the output of the shell builtin history command?)
I took the answer from Ville Laurikari and improved it using the time command to show sub-second accuracy:
function timer_now {
date +%s%N
}
function timer_start {
timer_start=${timer_start:-$(timer_now)}
}
function timer_stop {
local delta_us=$((($(timer_now) - $timer_start) / 1000))
local us=$((delta_us % 1000))
local ms=$(((delta_us / 1000) % 1000))
local s=$(((delta_us / 1000000) % 60))
local m=$(((delta_us / 60000000) % 60))
local h=$((delta_us / 3600000000))
# Goal: always show around 3 digits of accuracy
if ((h > 0)); then timer_show=${h}h${m}m
elif ((m > 0)); then timer_show=${m}m${s}s
elif ((s >= 10)); then timer_show=${s}.$((ms / 100))s
elif ((s > 0)); then timer_show=${s}.$(printf %03d $ms)s
elif ((ms >= 100)); then timer_show=${ms}ms
elif ((ms > 0)); then timer_show=${ms}.$((us / 100))ms
else timer_show=${us}us
fi
unset timer_start
}
trap 'timer_start' DEBUG
PROMPT_COMMAND=timer_stop
PS1='[last: ${timer_show}][\w]$ '
Of course this requires a process to be started, so it's less efficient, but still fast enough that you wouldn't notice.
I found that trap ... DEBUG was running every time $PROMPT_COMMAND was called, resetting the timer, and therefore always returning 0.
However, I found that history records times, and I tapped into these to get my answer:
HISTTIMEFORMAT='%s '
PROMPT_COMMAND="
START=\$(history 1 | cut -f5 -d' ');
NOW=\$(date +%s);
ELAPSED=\$[NOW-START];
$PROMPT_COMMAND"
PS1="\$ELAPSED $PS1"
It's not perfect though:
If history doesn't register the command (e.g. repeated or ignored commands), the start time will be wrong.
Multi-line commands don't get the date extracted properly from history.
Here's my take on Thomas'
uses date +%s%3N to get milliseconds as base unit,
simplified following code (less zeros)
function t_now {
date +%s%3N
}
function t_start {
t_start=${t_start:-$(t_now)}
}
function t_stop {
local d_ms=$(($(t_now) - $t_start))
local d_s=$((d_ms / 1000))
local ms=$((d_ms % 1000))
local s=$((d_s % 60))
local m=$(((d_s / 60) % 60))
local h=$((d_s / 3600))
if ((h > 0)); then t_show=${h}h${m}m
elif ((m > 0)); then t_show=${m}m${s}s
elif ((s >= 10)); then t_show=${s}.$((ms / 100))s
elif ((s > 0)); then t_show=${s}.$((ms / 10))s
else t_show=${ms}ms
fi
unset t_start
}
set_prompt () {
t_stop
}
trap 't_start' DEBUG
PROMPT_COMMAND='set_prompt'
Then add $t_show to your PS1
Another approach for bash 4.x and above would be to use coproc with PS0 and PS1 like below:
cmd_timer()
{
echo $(( SECONDS - $(head -n1 <&"${CMD_TIMER[0]}") ))
}
coproc CMD_TIMER ( while read; do echo $SECONDS; done )
echo '' >&"${CMD_TIMER[1]}" # For value to be ready on first PS1 expansion
export PS0="\$(echo '' >&${CMD_TIMER[1]})"
export PS1="[ \$(cmd_timer) ] \$"
This is a .bashrc ready snippet.
It is especially useful for everyone that uses undistract-me which overwrites trap DEBUG for its own purposes.
If somone just wants to see the time of execution,
add this line to bash_profile
trap 'printf "t=%s\n" $(date +%T.%3N)' DEBUG
Translated version for zsh.
Append to your ~/.zshrc file
function preexec() {
timer=$(date +%s%3N)
}
function precmd() {
if [ $timer ]; then
local now=$(date +%s%3N)
local d_ms=$(($now-$timer))
local d_s=$((d_ms / 1000))
local ms=$((d_ms % 1000))
local s=$((d_s % 60))
local m=$(((d_s / 60) % 60))
local h=$((d_s / 3600))
if ((h > 0)); then elapsed=${h}h${m}m
elif ((m > 0)); then elapsed=${m}m${s}s
elif ((s >= 10)); then elapsed=${s}.$((ms / 100))s
elif ((s > 0)); then elapsed=${s}.$((ms / 10))s
else elapsed=${ms}ms
fi
export RPROMPT="%F{cyan}${elapsed} %{$reset_color%}"
unset timer
fi
}
A version with split hours, minutes and seconds inspired by the zsh spaceship prompt, based on Ville's answer and this time conversion function by perreal.
I also added a threshold variable so that the timer only displays for long running commands.
time_threshold=5;
function convert_secs {
((h=${1}/3600))
((m=(${1}%3600)/60))
((s=${1}%60))
if [ $h -gt 0 ]; then printf "${h}h "; fi
if [ $h -gt 0 ] || [ $m -gt 0 ]; then printf "${m}m "; fi
if [ $s -gt 0 ]; then printf "${s}s "; fi
}
function timer_start {
timer=${timer:-$SECONDS}
}
function timer_stop {
timer_time=$(($SECONDS - $timer))
if [ ! -z $timer_time ] && [ $timer_time -ge ${time_threshold} ]; then
timer_show="took $(convert_secs $timer_time)"
else
timer_show=""
fi
unset timer
}
trap 'timer_start' DEBUG
PROMPT_COMMAND=timer_stop
PS1='\n\w ${timer_show}\n\\$ '
For the coloured output in my screenshot:
bold=$(tput bold)
reset=$(tput sgr0)
yellow=$(tput setaf 3)
cyan=$(tput setaf 6)
PS1='\n${bold}${cyan}\w ${yellow}${timer_show}${reset}\n\\$ '
Will putting a \t in PS1 work for you?
It does not give the elapsed time but it should be easy enough to subtract the times when necessary.
$ export PS1='[\t] [\w]\$ '
[14:22:30] [/bin]$ sleep 10
[14:22:42] [/bin]$
Following the OP's comment that he is already using \t.
If you can use tcsh instead of bash, you can set the time variable.
/bin 1 > set time = 0
/bin 2 > sleep 10
0.015u 0.046s 0:10.09 0.4% 0+0k 0+0io 2570pf+0w
/bin 3 >
You can change the format of the printing to be less ugly (se the tcsh man page).
/bin 4 > set time = ( 0 "last: %E" )
/bin 5 > sleep 10
last: 0:10.09
/bin 6 >
I do not know of a similar facility in bash
this is my version
use date to format time, only calc days
set terminal title
use \$ in PS1 for user $ + root #
show return code / exit code
use date -u to disable DST
use hidden names like _foo
_x_dt_min=1 # minimum running time to show delta T
function _x_before {
_x_t1=${_x_t1:-$(date -u '+%s.%N')} # float seconds
}
function _x_after {
_x_rc=$? # return code
_x_dt=$(echo $(date -u '+%s.%N') $_x_t1 | awk '{printf "%f", $1 - $2}')
unset _x_t1
#_x_dt=$(echo $_x_dt | awk '{printf "%f", $1 + 86400 * 1001}') # test
# only show dT for long-running commands
# ${f%.*} = int(floor(f))
(( ${_x_dt%.*} >= $_x_dt_min )) && {
_x_dt_d=$((${_x_dt%.*} / 86400))
_x_dt_s='' # init delta T string
(( $_x_dt_d > 0 )) && \
_x_dt_s="${_x_dt_s}${_x_dt_d} days + "
# format time
# %4N = four digits of nsec
_x_dt_s="${_x_dt_s}$(date -u -d0+${_x_dt}sec '+%T.%4N')"
PS1='rc = ${_x_rc}\ndT = ${_x_dt_s}\n\$ '
} || {
PS1='rc = ${_x_rc}\n\$ '
}
# set terminal title to terminal number
printf "\033]0;%s\007" $(tty | sed 's|^/dev/\(pts/\)\?||')
}
trap '_x_before' DEBUG
PROMPT_COMMAND='_x_after'
PS1='\$ '
sample output:
$ sleep 0.5
rc = 0
$ sleep 1
rc = 0
dT = 00:00:01.0040
$ sleep 1001d
rc = 0
dT = 1001 days + 00:00:00.0713
$ false
rc = 1
$