Read string with ZSH completions - shell

I want to read a string from a user (using read builtin or something similar), with all completions enabled for normal shell usage. In other words, I want ZSH to complete all commands but after pressing ENTER I don't want ZSH to execute the command, but pass the string to my script. How can I achieve this?

To read a line with edition and completion, call the vared builtin.
foo='default text'
vared foo
Completion will work as if you were in the value part of a parameter assignment (because that's what vared does). If you want completion like a normal command line, I think you need to fiddle with _complete to make it forget about being inside vared.
zmodload zsh/parameter
autoload +X _complete
functions[_original_complete]=$functions[_complete]
_complete () {
unset 'compstate[vared]'
_original_complete "$#"
}
foo='default text'
vared foo

Related

Override of "select" builtin

Recently I tried to override three common commands:
sleep
wait
select
The first one (sleep) is commonly an external bin (/bin/sleep in my Debian 10).
The second one (wait) is a builtin (You can check it with command -v wait).
The third one (select) is also a builtin.
I will write some examples in order to reproduce what works and what not, please be patient.
There are no problems in overriding sleep and wait, I just added somewhere in my code the following:
sleep() {
echo using custom sleep
}
wait() {
echo using custom wait
}
Things change when I try to override select.
In particular, if I try to create a simple function as above, I get errors as the parser (?) thinks I am trying to use the command rather than create a new function.
You can reproduce the error with the following:
#!/bin/bash
select() {
echo using custom select
}
This is avoidable using the notation function select() {.
If I'm using an interactive shell I can solve the issue with an alias; steps to reproduce:
_select() { echo using custom select;}
alias select='_select'
select
BUT this solution doesn't work if I use files.
Let's try something like:
#!/bin/bash
# this is the actual script
. lib.sh
select
... and:
#!/bin/bash
# this is where I declare the function select
_select() {
echo using custom select
}
alias select='_select'
If I run script which in turn source lib.sh I will get an error when I try to call my alias.
This is the first time, for me, that an alias is a "second choice" after a builtin.
Is there something I am doing wrong or is this an actual "bug"?
Obviusly, a workaround would be rename the function in something else.
From the bash man page in section ALIASES:
Aliases are not expanded when the shell is not interactive, unless the expand_aliases shell option is set
using shopt (see the description of shopt under SHELL BUILTIN COMMANDS below).
Either add
shopt -s expand_aliases
before the first use of select or add -i to your shebang
#!/bin/bash -i
for an interactive shell.

How do you load bash_profile for bash commands run from perl script?

I wrote simple command that lets me run the last N commands from terminal history. It looks like this: $> r 3 which will replay the last 3 commands.
I have the following alias in my bash profile:
alias r="history -w; runlast $1"
And then the following simple perl script for the runlast command:
#!/usr/bin/env perl
use strict;
use warnings;
my $lines = $ARGV[0] || exit;
my #last_commands = split /\n/,
`bash -ic 'set -o history; history' | tail -$lines`;
#last_commands =
grep { $_ !~ /(^r |^history |^rm )/ }
map { local $_ = $_; s/^\s+\d+\s+//; $_ }
#last_commands;
foreach my $cmd (#last_commands) {
system("$cmd");
}
This works but my bash profile has aliases and other features (e.g. color output) I want the perl script to have access to. How do I load the bash profile for perl so it runs the bash commands with my bash profile? I read somewhere that if you "source the bash profile" for perl you can get it to work. So I tried adding source ~/.bash_profile; to my r command alias but that didn't have an effect. I'm not sure if I was doing that correctly, though.
The system forks a process in which it runs a shell, which is non-login and non-interactive; so no initialization is done and you get no aliases. Also note that the shell used is /bin/sh, which is generally a link to another shell. This is often bash but not always, so run bash explicitly.
To circumvent this you need to source the file with aliases, but as bash man page says
Aliases are not expanded when the shell is not interactive, unless the expand_aliases shell option is set using shopt (see the description of shopt under SHELL BUILTIN COMMANDS below).
Thus you need shopt -s expand_aliases, as mentioned. But there is another screw: on that same physical line aliases are not yet available; so it won't work like this in a one-liner.
I'd also recommend to put aliases in .bashrc, or in a separate file that is sourced.
Solutions
Add shopt -s expand_aliases to your ~/.bashrc, and before the aliases are defined (or the file with them sourced), and run bash as a login shell
system('/bin/bash', '-cl', 'source ~/.bashrc; command');
where -l is short for --login.
In my tests the source ~/.bashrc wasn't needed; however, the man page says
When bash is invoked as an interactive login shell, or as a non-interactive shell with the --login option, it first reads and executes commands from the file /etc/profile, if that file exists. After reading that file, it looks for ~/.bash_profile, ~/.bash_login, and ~/.profile, in that order, and reads and executes commands from the first one that exists and is readable.
and goes on to specify that ~/.bashrc is read when an interactive shel that is not login runs. So I added explicit sourcing.
In my tests sourcing .bashrc (with shopt added) while not running as a login shell didn't work, and I am not sure why.
This is a little heavy-handed. Also, initialization may be undesirable to run from a script.
Source ~/.bashrc and issue shopt command, and then a newline before the command
system('/bin/bash', '-c',
'source ~/.bashrc; shopt -s expand_aliases\ncommand');
Really. It works.
Finally, is this necessary? It asks for trouble, and there is probably a better design.
Other comments
The backticks (qx) is context-aware. If it's used in list context – its return assigned to an array, for example – then the command's output is returned as a list of lines. When you use it as the argument for split then it is in the scalar context though, when all output is returned in one string. Just drop split
my #last_commands = `bash -ic 'set -o history; history $lines`;
where I also use history N to get last N lines. In this case the newlines stay.
history N returns last N lines of history so there is no need to pipe to last
Regex substitution in a map can be done without changing the original
map { s/^\s+\d+\s+//r } #last_commands;
With /r modifier the s/// operator returns the new string, not changing the original. This "non-destructive substitution" has been available since v5.14
No need to explicitly use $_ in the last grep, and no need for parenthesis in regex
grep { not /^r |^history |^rm ?/ } ...
or
grep { not /^(?:r|history|rm)[ ]?/ } ...
where parens are now needed, but as it is only for grouping the ?: makes it not capture the match. I use [ ] to emphasize that that space is intended; this is not necessary.
I also added ? to make space optional since history (and r?) may have no space.
The proper solution is to have your Perl script just print the commands, and make your current interactive shell eval the string printed from your history. (I would probably get rid of Perl entirely but that's beside the point here.)
If the commands get evaluated in the current shell, you avoid many contextual problems which would be very hard or even intractable with system() or generally anything involving a new process. For example, a subprocess cannot have access to non-exported variables in the current shell. var="foo", echo "$var"; r 1 is going to be very hard to solve correctly with your current approach. Using the current interactive shell will also naturally and easily solve the problems you were having with trying to get a noninteractive subshell act like an interactive one.
Aliases suck anyway, so let's redefine r as a function:
r(){
history -w
eval $(printlast "$1")
}
... where refactoring runlast into a different script printlast is a trivial additional requirement. Or maybe just turn it into a (much simpler!) shell function:
printlast () {
history "$1" |
perl -ne 's/^\s*\d+\s+\*?//; print unless m/^(history|rm?)($|\s)'
}
With this, you can also get rid of history -w from the r definition.
Notice how we are using Perl where it is useful; but the main functionality makes sense to keep in the shell when you're dealing with the shell.
You can't source in a Bash script into a Perl script. The bash_profile has to be sourced in by the shell that executes the command. When Perl runs system, it forks a new shell each time.
You have to source in the bash_profile for each command that you run through system:
system('source ~/.bash_profile; ' + $cmd);
One more thing, system invokes a non-interactive shell. So, your Bash aliases defined in .bash_profile won't work unless you invoke:
shopt -s expand_aliases
inside that script

Including process execution time into shell prompt

Is it possible to include the execution time of the just finished process - the output of the 'time' command - into the command prompt? We use zsh on linux and bash (msys) on windows machines so it would be nice to know how to do it in both.
For zsh you can try $REPORTTIME variable (search man zshall for it). It won't put time into the prompt, but it will echo time after each executed command (does not work in some cases). You can use preexec and precmd hooks and and $SECONDS variable to get execution time (but it is not too precise):
function preexec() {
typeset -gi CALCTIME=1
typeset -gi CMDSTARTTIME=SECONDS
}
function precmd() {
if (( CALCTIME )) ; then
typeset -gi ETIME=SECONDS-CMDSTARTTIME
fi
typeset -gi CALCTIME=0
}
PS1='${ETIME} %'
(Note single quotes used in PS1 definition.)
Third approach will either force you to loose ability to change environment variables or will work only for single commands. It will also force you to write something hacky to view command output. This approach is “redefine accept-line zle widget to add time to the command executed”. I am not writing code here because it is has problems described above.
By the way, $SECONDS variable is also available in bash, though I do not know how to implement hooks there.

Get name of last run program in Bash

I have a bash script where I trap errors using the trap command, and I would like to be able to print the name of the last command (the one that failed)
#!/bin/bash
function error
{
# echo program name
}
trap error ERR
# Some commands ...
/bin/false foo # For testing
I'm not sure what to put in the error function. I tried echo $_ but that only works if the command has no arguments. I also tried with !! but that gives me "!!: command not found". At an interactive prompt (also bash) I get:
$ /bin/false foo
$ !!
/bin/false foo
which seems to be pretty much what I want. Why the difference?
What is the easiest way to get the name of the previous command inside a script?
Try echo $BASH_COMMAND in your trap function.
From man bash:
BASH_COMMAND
The command currently being executed or about to be executed,
unless the shell is executing a command as the result of a trap,
in which case it is the command executing at the time of the
trap.
You need to set
set -o history
to quote bash manual page:
When the -o history option to the set builtin is enabled, the shell provides access to the command history, the list of commands previously typed. The value of the HISTSIZE variable is used
as the number of commands to save in a history list. The text of the last HISTSIZE commands (default 500) is saved. The shell stores each command in the history list prior to parameter and
variable expansion (see EXPANSION above) but after history expansion is performed, subject to the values of the shell variables HISTIGNORE and HISTCONTROL.
In general, read the HISTORY and HISTORY EXPANSION sections in bash man page.

Is there a hook in Bash to find out when the cwd changes?

I am usually using zsh, which provides the chpwd() hook. That is: If the cwd is changed by the cd builtin, zsh automatically calls the method chpwd() if it exists. This allows to set up variables and aliases which depend on the cwd.
Now I want to port this bit of my .zshrc to bash, but found that chpwd() is not recognized by bash. Is a similar functionality already existing in bash? I'm aware that redefining cd works (see below), yet I'm aiming for a more elegant solution.
function cd()
{
builtin cd $#
chpwd
}
You would have to use a DEBUG trap or PROMPT_COMMAND.
Examples:
trap chpwd DEBUG # calls the function before each command
PROMPT_COMMAND=chpwd # calls the function after each command
Note that the function defined in PROMPT_COMMAND is run before each prompt, though, even empty ones.
A better solution could be defining a custom chpwd hook.
There's not a complete hook system designed in Bash when compared with other modern shells. PROMPT_COMMAND variable is used as a hook function, which is equivalent to precmd hook in ZSH, fish_prompt in Fish. For the time being, ZSH is the only shell I've known that has a chpwd hook builtin.
PROMPT_COMMAND
If set, the value is interpreted as a command to execute before the printing of each primary prompt ($PS1).
https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#Bash-Variables
chpwd Hook in Bash
A trick is provided to setup a chpwd equivalent hook in Bash based on PROMPT_COMMAND.
# create a PROPMT_COMMAND equivalent to store chpwd functions
typeset -g CHPWD_COMMAND=""
_chpwd_hook() {
shopt -s nullglob
local f
# run commands in CHPWD_COMMAND variable on dir change
if [[ "$PREVPWD" != "$PWD" ]]; then
local IFS=$';'
for f in $CHPWD_COMMAND; do
"$f"
done
unset IFS
fi
# refresh last working dir record
export PREVPWD="$PWD"
}
# add `;` after _chpwd_hook if PROMPT_COMMAND is not empty
PROMPT_COMMAND="_chpwd_hook${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
Usage
# example 1: `ls` list directory once dir is changed
_ls_on_cwd_change() {
ls
}
# append the command into CHPWD_COMMAND
CHPWD_COMMAND="${CHPWD_COMMAND:+$CHPWD_COMMAND;}_ls_on_cwd_change"
# or just use `ls` directly
CHPWD_COMMAND="${CHPWD_COMMAND:+$CHPWD_COMMAND;}ls"
Source: Create chpwd Equivalent Hook in Bash from my gist.

Resources