I just found about zsh suffix aliases that allow one to associate a program with an extension in the shell:
alias -s {md,txt}=nano
Is there a way to do something like this but for file that do not have an extension?
I've tried:
alias -s {}=nano
But if I then try to use it, I get a command not found error:
> alias -s {}=nano
> touch file_without_extension
> file_without_extension
zsh: command not found: file_without_extension
Suffix aliases require a filename extension. You can use a command_not_found_handler function to work around that, though:
# Run this from a zsh prompt or put it in a file and source it
command_not_found_handler() {
# If just the name of an existing file is given, with no extra arguments
# open it in nano. Otherwise, print a message to stderr and return an error.
if [[ $# -eq 1 && -f $1 ]]; then
nano "$1"
else
print -u2 -f "command not found: %s\n" "$1"
return 127
fi
}
# Then with the function loaded:
$ file_without_extension # Opens in nano
$ file_that_doesnt_exist
command not found: file_that_doesnt_exist
$ file_without_extension blah
command not found: file_without_extension
base on the above answer:
with vim conceal:
command_not_found_handler() {
if [[ $# -eq 1 && -f $1 ]]; then
echo "leo test________________"
vim "$1"
else
print -u2 -f "command not found: %s\n" "$1"
# -u n Print the arguments to file descriptor n.
# 2 : stderr
return 127 # return an error.
fi
echo "aaaaaaaaaa leo test________________"
}
# 名字不能改
#
# (Nearly) All about this from man zshmisc (modified by me for easier understanding)
# If a command name contains no ✌/✌
# the shell attempts to locate it:
# If there exists a shell function by that name,
# the function is invoked as described in the section `Func‐ tions'.
# If there exists a shell ✌builtin✌ by that name,
# the builtin is invoked.
#
# contains ✌/✌
# the shell searches for a ✌directory✌ containing an executable file
# searche ✌by that name✌.
searches # in each element of $path
#
# if the search is successful
# if execution ✌fails✌ because the file is not in ✌executable format✌,
# and the file is ✌not a directory✌,
# it is assumed to be a shell script.
# /bin/sh is ✌spawned✌ to execute it.
# If the program is a file beginning with `#!',
# the remainder of the first line specifies an interpreter for the program.
# The shell will execute the specified interpreter,
# which do not handle this executable format in the ✌kernel✌ ,
# on ✌operating systems✌
#
# else (the search fails)
# the shell prints an error message and returns a nonzero exit status.
#
# If no external command is found
# but a function ✌command_not_found_handler✌ exists
# the shell executes this function with all command line arguments.
# The return status of the function becomes the status of the command.
#
# If the function wishes to mimic the behaviour of the shell when the command is not found,
# it should print the message `command not found: cmd'
# to standard error and return status 127.
#
# Note that the handler is executed in a subshell forked to execute an external command,
# hence changes to directories,
# shell parame‐ ters,
# have no effect on the main shel
#
# vim:ft=zsh
Related
I have written following shell script to execute given set of commands in the directory of each package. It is supposed to stop if any command fails. It should also show the command output on the console.
It takes 3 arguments with options
'-b' base directory of packages
'-c' comma separated list of commands
'-p' comma separated list of packages (individual
directories)
Shell Script
#!/bin/bash
#Function to execute a command
executeCommand(){
printf "\n**** Executing Command: '$1' *****\n\n"
$1
return $?
}
#Initialize the variables with command line arguments
while getopts "c:p:b:" options; do
case $options in
b ) set -f # disable glob
IFS=, # split on space characters
base_directory=$OPTARG ;; # use the split+glob operator
c ) set -f # disable glob
IFS=, # split on space characters
commands=($OPTARG) ;; # use the split+glob operator
p ) set -f # disable glob
IFS=, # split on space characters
packages=($OPTARG) ;; # use the split+glob operator
esac
done
#Iterate over all packages
for (( i = 0; i < ${#packages[#]} ; i++ )); do
#Go to each package directory
if executeCommand "cd ${base_directory}${packages[$i]}"; then
#Execute all the commands one by one for current package
for (( j = 0; j < ${#commands[#]} ; j++ )); do
if executeCommand "${commands[$j]}"; then
echo "Successfully Executed the Command"
else
break 2;
fi
done
else
break;
fi
done
If I execute it with following arguments it gives me an error that No Such File or directory however, if I manually execute cd /local/workplace/directory1 it goes to the directory1
Execution and Error
~/bb-slds.sh \
-c "build clean","build package" \
-p directory1,directory1 \
-b /local/workplace/
**** Executing Command: 'cd /local/workplace/directory1' *****
/home/jramay/bb-slds.sh: line 6: cd /local/workplace/directory1: No such file or directory
It works fine if I don't use getopts and instead initialize the variables as below.
base_directory="/local/workplace/"
declare -a commands=(
"build clean"
"build package"
)
declare -a packages=(
"directory1"
"directory2"
)
This was a tricky one. :)
You changed IFS during the getopts loop, and never changed it back to the old value. So when you write:
$1
in executeCommand, it doesn't treat the space as a separator between cd and its argument.
You need to set it back to the default value after the getopts is done:
IFS=$' \t\n'
The set -f changes persist after getopts as well. You should probably just do it once before the loop, then put
set +f
after it.
I am missing my bash aliases in fish, and don't want to manually convert all of them to fish functions.
How to get them all from bash to fish?
Bonus points if:
the solution supports an iterative process, as in: i can easily change the aliases in bash, and re-convert/re-import them into fish
the solution also imports bash functions
Convert bash aliases to bash scripts
I decided to do this instead of approach below, and putting the scripts into ~/bin/, which is in my PATH. This allows me to use them from whatever shell i am currently executing, and it prevents potential problems with quoting.
use like so:
# converts all bash aliases to script files
convert_bash_aliases_to_scripts
# removes all scripts previously converted by this script
convert_bash_aliases_to_scripts clean
bash conversion script:
#!/bin/bash
# Convert bash aliases to bash scripts.
#
# Copyright 2018 <hoijui.quaero#gmail.com>, licensed under the GPL-3.0+
#
# Usage:
# convert_bash_aliases_to_scripts # converts all bash aliases to script files
# convert_bash_aliases_to_scripts clean # removes all scripts previously converted by this script
COLOR_RED=$'\e[0;31m'
COLOR_ORANGE=$'\e[0;33m'
COLOR_BLUE=$'\e[0;34m'
COLOR_BLUE_LIGHT=$'\e[1;34m'
COLOR_GREEN=$'\e[0;32m'
COLOR_BROWN=$'\e[0;33m'
COLOR_YELLOW=$'\e[1;33m'
COLOR_WHITE=$'\e[1;37m'
COLOR_CYAN=$'\e[0;36m'
COLOR_PURPLE=$'\e[0;35m'
COLOR_GRAY=$'\e[1;30m'
COLOR_GRAY_LIGHT=$'\e[0;37m'
COLOR_NONE=$'\e[m' # No Color
OUTPUT_DIR=~/bin/converted/aliases
LINKS_DIR=~/bin
README_FILE_NAME="README.md"
README_FILE="$OUTPUT_DIR/$README_FILE_NAME"
if [ "$1" = "clean" ]
then
for script_file in $(find "$LINKS_DIR" -maxdepth 1 -type l)
do
conv_script_file="$OUTPUT_DIR/$(basename $script_file)"
if [ -e $conv_script_file ] && [ "$(readlink --canonicalize $script_file)" = "$(realpath $conv_script_file)" ]
then
script_name=$(basename $script_file)
echo "removing converted bash alias-script: $script_name"
rm $conv_script_file \
&& rm $script_file
fi
done
rm $README_FILE 2> /dev/null
rmdir $OUTPUT_DIR 2> /dev/null
exit 0
fi
SOURCE_FILES="${HOME}/.bashrc ${HOME}/.bash_aliases"
mkdir -p $OUTPUT_DIR
echo -e "# Bash alias conversion scripts\n\nsee $0\n\nWARNING: Do NOT manually edit files in this directory. instead, copy them to $LINKS_DIR (replacing the symbolic link that already exists there), and edit that new file.\nIf you edit the files in this dir, it will be replaced on the next (re)conversion from aliases." \
> $README_FILE
AUTO_IMPORT_WARNING="# WARNING Do NOT edit this file by hand, as it was auto-generated from a bash alias, and may be overwritten in the future. please read ${README_FILE}"
function _is_link_to {
local file_link=$1
local file_target=$2
test -e $file_target \
&& test "$(readlink --canonicalize $file_link)" = "$(realpath $file_target)"
return $?
}
for source_file in $SOURCE_FILES
do
IFS=$'\n'
for a in $(cat $source_file | grep "^alias")
do
a_name="$(echo "$a" | sed -e 's/alias \([^=]*\)=.*/\1/')"
a_command="$(echo "$a" | sed -e 's/alias \([^=]*\)=//' -e 's/[ \t]*#.*$//')"
if echo "${a_command:0:1}" | grep -q -e "[\'\"]"
then
# unquote
a_command_start=1
let a_command_end="${#a_command} - 2"
else
# leave as is
a_command_start=0
let a_command_end="${#a_command}"
fi
script_file="$LINKS_DIR/$a_name"
conv_script_file="$OUTPUT_DIR/$a_name"
# Check whether the script already exists.
# If so, we skip importing it, unless it is just a link to a previously imported script.
log_action="none"
log_action_color="${COLOR_NONE}"
log_content=""
if [ -e $script_file ] && ! $(_is_link_to $script_file $conv_script_file)
then
log_action="skipped (exists)"
log_action_color="${COLOR_ORANGE}"
log_content=""
else
if [ -e $script_file ]
then
log_action="reimporting"
log_action_color="${COLOR_BLUE}"
else
log_action="importing"
log_action_color="${COLOR_GREN}"
fi
# write the script file to a temporary location
conv_script_file_tmp="${conv_script_file}_BAK"
echo "#!/bin/bash" > $conv_script_file_tmp
echo -e "$AUTO_IMPORT_WARNING" >> $conv_script_file_tmp
echo -e "#\n# Imported bash alias '$a_name' from file '$source_file'" >> $conv_script_file_tmp
cat >> "${conv_script_file_tmp}" <<EOF
${a_command:${a_command_start}:${a_command_end}} \${#}
EOF
if diff -N ${conv_script_file_tmp} ${conv_script_file} > /dev/null
then
log_content="no change"
log_content_color="${COLOR_NONE}"
else
log_content="changed"
log_content_color="${COLOR_GREEN}"
fi
log_content=$(printf "%s %10s -> %s${COLOR_NONE}" "${log_content_color}" "${log_content}" "$a_command")
mv "${conv_script_file_tmp}" "${conv_script_file}"
# make the script executable
chmod +x $conv_script_file
# remove the link if it already exists (in case of reimport)
rm $script_file 2> /dev/null
# .. and re-create it as local symbolic link
# to the function in the imports dir
ln --symbolic --relative $conv_script_file $script_file
fi
printf "%s%20s: %-25s${COLOR_NONE}%s\n" "${log_action_color}" "${log_action}" "$a_name" "${log_content}"
done
done
Deprecated: Creating fish wrappers that execute bash code
Below is a script that creates fish script wrappers for the local bash aliases: For each bash alias, it takes the contents, and creates a fish alias/script that executes the code in bash sub-shell.
It is not optimal, but is sufficient for most of my aliases.
WARNING It might happen that the imported function acts differently then in bash. You may loose data or accidentally DDOS your coworkers when using them.
use like so:
# imports (or reimports) all bash aliases into fish functions, permanently
import_bash_aliases
# removes all fish functions previously imported by this script
import_bash_aliases clean
save this in ~/.config/fish/functions/import_bash_aliases.fish:
#!/usr/bin/fish
# Fish function to import bash aliases
#
# Copyright 2018 <hoijui.quaero#gmail.com>, licensed under the GPL-3.0+
#
# This script is based on a script from Malte Biermann,
# see: https://glot.io/snippets/efh1c4aec0
#
# WARNING: There is no guarantee that the imported aliases work the same way
# as they do in bash, so be cautious!
#
# Usage:
# import_bash_aliases # imports (or reimports) all bash aliases into fish functions, permanently
# import_bash_aliases clean # removes all fish functions previously imported by this script from bash aliases
function import_bash_aliases --description 'Converts bash aliases to .fish functions.\nThis might be called repeatedly, and will not override functions that are already defined in fish, except they are merely an older import from this script.'
set -l FISH_FUNCTIONS_DIR ~/.config/fish/functions
set -l BASH_IMPORTS_DIR_NAME bash-imports
set -l BASH_IMPORTS_DIR $FISH_FUNCTIONS_DIR/$BASH_IMPORTS_DIR_NAME
set -l README_FILE $BASH_IMPORTS_DIR/README.md
if test "$argv[1]" = "clean"
for fun_file in (find $FISH_FUNCTIONS_DIR -maxdepth 1 -name '*.fish')
set -l imp_fun_file $BASH_IMPORTS_DIR/(basename $fun_file)
if test -e $imp_fun_file ; and test (readlink --canonicalize $fun_file) = (realpath $imp_fun_file)
set -l fun_name (basename $fun_file '.fish')
echo "removing imported bash alias/function $fun_name"
rm $imp_fun_file
and rm $fun_file
and functions --erase $fun_name
end
end
rm $README_FILE ^ /dev/null
rmdir $BASH_IMPORTS_DIR ^ /dev/null
return 0
end
set -l SOURCE_FILES ~/.bashrc ~/.bash_aliases
mkdir -p $BASH_IMPORTS_DIR
echo -e "# Bash alias imports\n\nsee `$argv[0]`\n\nWARNING: Do NOT manually edit files in this directory. instead, copy them to $FISH_FUNCTIONS_DIR (replacing the symbolic link that already exists there), and edit that new file.\nIf you edit the files in this dir, it will be replaced on the next (re)import from bash aliases." \
> $README_FILE
set -l UNUSED_STUB_MSG "The bash alias corresponding to this function was NOT imported, because a corresponding function already exists at %s\n"
set -l AUTO_IMPORT_WARNING "# WARNING Do NOT edit this file by hand, as it was auto-generated from a bash alias, and may be overwritten in the future. please read {$README_FILE}"
function _fish_func_exists
set -l fun_name $argv[1]
# This also detects in-memory functions
functions --query $fun_name
# This also detects script files in the functions dir
# that do not contain a function wiht the same name
or test -e "$FISH_FUNCTIONS_DIR/$fun_name.fish"
return $status
end
function _is_link_to
set -l file_link $argv[1]
set -l file_target $argv[2]
test -e $file_target
and test (readlink --canonicalize $file_link) = (realpath $file_target)
return $status
end
for source_file in $SOURCE_FILES
for a in (cat $source_file | grep "^alias")
set -l a_name (echo $a | sed -e 's/alias \([^=]*\)=.*/\1/')
set -l a_command (echo $a | sed -e 's/alias \([^=]*\)=//' -e 's/[ \t]*#[^\'\"]\+$//')
set -l fun_file "$FISH_FUNCTIONS_DIR/$a_name.fish"
set -l imp_fun_file "$BASH_IMPORTS_DIR/$a_name.fish"
# Check whether the function already exists.
# If so, we skip importing it, unless it is just a link to a previously imported function.
if _fish_func_exists $a_name; and not _is_link_to $fun_file $imp_fun_file
set_color red
printf "%20s: %-25s\n" "skipping (exists)" $a_name
set_color normal
#printf $UNUSED_STUB_MSG $fun_file > $imp_fun_file
else
set_color green
printf "%20s: %-25s -> %s\n" "(re-)importing" $a_name $a_command
set_color normal
# remove the link, in case of re-importing
rm $fun_file ^ /dev/null
# write the function file
echo "#!/usr/bin/fish" > $imp_fun_file
echo "\
$AUTO_IMPORT_WARNING
function $a_name -d 'bash alias "$a_name" import'
bash -c $a_command' '\$argv''
end
" \
>> $imp_fun_file
# make the script executable
chmod +x $imp_fun_file
# .. and re-create it as local symbolic link
# to the function in the imports dir
ln --symbolic --relative $imp_fun_file $fun_file
end
end
end
# (re-)load all the functions we just defined
exec fish
end
I am getting the following error in my makefile despite of having no spaces on that line, I edited a part of the file by adding tabs. But on the current line " case "$UNAME" in " , I am not able to resolve it and adding a tab on that line is throwing ‘recipe commences before first target. Stop.’ error
Makefile:33: *** missing separator. Stop
#! /bin/sh Makefile
# On GNU/Linux you must: alias make "/bin/sh Makefile $1" for this to work!!
#
# PURPOSE: Chooses specific Makefile
#
# NORMAL USAGE: make [version-selector]
#
# SAMPLE USAGE: make
# make [mp][f][d] e.g.
# on Linux
# make mp (to create spider_linux_mp)
# make all (to create all current executables)
# make new (to update all current executables version)
TARGET="$1"
echo TARGET: $TARGET
DEST="DEST=_tmp"
DEBUG=
if expr "$TARGET" : '.*d' > /dev/null
then
DEBUG="QFFLAGS=-g QLFLAGS=-g SUFFIX=d"
# echo DEBUG: $DEBUG
TARGET=`echo $TARGET | tr -d 'd'`
fi
UNAME=`uname`
#echo UNAME: $UNAME
UNAMEm=`uname -m`
#echo UNAMEm: $UNAMEm
case "$UNAME" in
# Find current operating system
Linux)
PLATFORM=_linux
#VERSIONS=" _linux _linux_mp_opt64 _linux_mp_intel _linux_mpi_opt64 _linux_mp_intel64 "
VERSIONS=" _linux _linux_mp_opt64 _linux_mp_intel _linux_mp_intel64 "
if expr "$TARGET" : '.*mp' > /dev/null
then # Want mp with version
WHICH="mp"
else
if expr "$TARGET" : '.*f' > /dev/null
then # Want version
WHICH=""
fi
fi;;
esac
I am learning bash. And I would like to make a function which wrap another function in a temporal script file and execute it with sudo -u command in sub-shell.
The problem I encountered is the generated script cannot find the wrapped function although it is exported in the wrap function.
I append test cords below. Someone who finds problems, please let me know. Thank you very much.
main.sh
source "./display.sh"
source "./sudo_wrap.sh"
display_func "load success"
sudo_wrap_func '' 'display_func' '3' '4 5'
output, display.sh, sudo_wrap.sh and generated temporal file are appended below,
output
display_func : load success
export -f display_func
30481: line 5: display_func: command not found
display.sh
function display_func() {
echo "display_func : $#"
}
sudo_wrap.sh
function sudo_wrap_func() {
local sudo_user="${1:-root}"
local function_name="${2:?'function_name is null string.'}"
shift 2
local func_augs=( "$#" )
local script
# *** script : header ***
script="#!/bin/bash\n"
script="${script}\n"
# *** script : making augments for function ***
script="${script}augs=("
for aug in "${func_augs[#]}"
do
if [[ "${aug}" =~ [[:blank:]] ]]; then
script=" ${script} \"${aug}\""
else
script=" ${script} ${aug}"
fi
done
script="${script})\n"
local tmp_script_file="${RANDOM}"
echo -e "${script}" >> "${tmp_script_file}"
# *** script : calling function with augments ***
echo -e "${function_name} \"\${augs[#]}\"\n" >> "${tmp_script_file}"
echo "export -f "${function_name}"" >&2
export -f "${function_name}"
sudo -u"${sudo_user}" bash "${tmp_script_file}"
rm "${tmp_script_file}"
}
temporally generated file (in this case, file name is 30481)
#!/bin/bash
augs=( 3 "4 5")
display_func "${augs[#]}"
As I said in a comment, the basic problem is that sudo cleans its environment (including both variables and functions) before running the command (/script) as another user. This can be overridden with sudo -E, but only if it's explicitly allowed in /etc/sudoers.
But the problem is not insoluble; you just have to include the definition of the function in the script, so it gets recreated in that environment. bash even has a convenient command, declare -f display_func, that prints the function definition in the appropriate form (and declare -p variable does the same for variables). So you can use those to add the appropriate definitions to the script.
Here's a script I wrote to do this. I made a few other changes vs. your script: I take -u username to specify a different user to run as (so you don't have to pass '' as the first argument if you don't want to specify a different user). I also added -f functionname and -v variablename to "export" additional function and variable definitions into the script (in case the main function depends on them). I also create the temp script file in /tmp, and change ownership if necessary so it'll be readable by the other user.
#!/bin/bash
me="$(basename "$0")"
usage() {
echo "Usage: $me [-u user] [-f otherfunction] [-v variablename] function [args...]" >&2
}
tmp_script_file=$(mktemp "/tmp/${me}.XXXXXXXXXXXX") || {
echo "Error creating temporary script file" >&2
exit 1
}
echo "#!/bin/bash" > "$tmp_script_file" # Not actually needed, since we'll run it with "bash"
# Parse the command options; "-u" gets stored for later, but "-f" and "-v" write
# the relevant declarations to the script file as we go.
sudo_user=""
while getopts u:f:v: OPT; do
case "$OPT" in
u)
sudo_user="$OPTARG" ;;
f)
declare -f "$OPTARG" >>"$tmp_script_file" || {
echo "Error saving definition of function $OPTARG" >&2
exit 1
} ;;
v)
declare -p "$OPTARG" >>"$tmp_script_file" || {
echo "Error saving definition of variable $OPTARG" >&2
exit 1
} ;;
?) usage; exit 1 ;;
esac
done
shift $(($OPTIND-1))
if (( $# == 0 )); then # No actual command specified
usage
exit 1
fi
# Write the main function itself into the script
declare -f "$1" >>"$tmp_script_file" || {
echo "Error saving definition of function $1" >&2
exit 1
}
# Then the command to run it, with arguments quoted/escaped as
# necessary.
printf "%q " "$#" >>"$tmp_script_file"
# the printf above won't write a newline, so add it by hand
echo >>"$tmp_script_file"
# If the script will run as someone other than root, change ownership of the
# script so the target user can read it
if [[ -n "$sudo_user" ]]; then
sudo chown "$sudo_user" "$tmp_script_file"
fi
# Now launch the script, suitably sudo'ed
sudo ${sudo_user:+ -u "$sudo_user"} bash "$tmp_script_file"
# Clean up
sudo rm "$tmp_script_file"
Here's an example of using it:
$ foo() { echo "foo_variable is '$foo_variable'"; }
$ bar() { echo "Running the function bar as $(whoami)"; echo "Arguments: $*"; foo; }
$ export -f foo bar # need to export these so the script can see them
$ export foo_variable='Whee!!!' # ditto
$ # Run the function directly first, so see what it does
$ bar 1 2 3
Running the function bar as gordon
Arguments: 1 2 3
foo_variable is 'Whee!!!'
$ # Now run it as another user with the wrapper script
$ ./sudo_wrap.sh -f foo -v foo_variable -u deenovo bar 1 2 3
Running the function bar as deenovo
Arguments: 1 2 3
foo_variable is 'Whee!!!'
Note that you could remove the need to export the functions and variables by either running the script with source or making it a function, but doing that would require changes to how $me is defined, the usage function, replacing all those exits with returns, and maybe some other things I haven't thought of.
Is there any variable in bash that contains the name of the .sh file executed? The line number would be great too.
I want to use it in error messages such as:
echo "ERROR: [$FILE:L$LINE] $somefile not found"
#!/bin/bash
echo $LINENO
echo `basename $0`
$LINENO for the current line number
$0 for the current file. I used basename to ensure you only get the file name and not the path.
UPDATE:
#!/bin/bash
MY_NAME=`basename $0`
function ouch {
echo "Fail # [${MY_NAME}:${1}]"
exit 1
}
ouch $LINENO
You have to pass the line as a parameter if you use the function approach else you will get the line of the function definition.
I find the "BASH_SOURCE" and "BASH_LINENO" built-in arrays very useful:
$ cat xx
#!/bin/bash
_ERR_HDR_FMT="%.23s %s[%s]: "
_ERR_MSG_FMT="${_ERR_HDR_FMT}%s\n"
error_msg() {
printf "$_ERR_MSG_FMT" $(date +%F.%T.%N) ${BASH_SOURCE[1]##*/} ${BASH_LINENO[0]} "${#}"
}
error_msg "here"
error_msg "and here"
Invoking xx yields
2010-06-16.15:33:13.069 xx[11]: here
2010-06-16.15:33:13.073 xx[14]: and here
You just need to
echo $LINENO
echo $(basename $0)
Here's how to do it in a reusable function. if the following is in a file named script:
#!/bin/bash
debug() {
echo "${BASH_SOURCE[1]##*/}:${FUNCNAME[1]}[${BASH_LINENO[0]}]" > /dev/tty
}
debug
This produces the output:
script:main[5]
Which indicates the line on which debug was called.
The following will print out the filename, function, line and an optional message.
Also works in zsh for extra goodness.
# Say the file, line number and optional message for debugging
# Inspired by bash's `caller` builtin
# Thanks to https://unix.stackexchange.com/a/453153/143394
function yelp () {
# shellcheck disable=SC2154 # undeclared zsh variables in bash
if [[ $BASH_VERSION ]]; then
local file=${BASH_SOURCE[1]##*/} func=${FUNCNAME[1]} line=${BASH_LINENO[0]}
else # zsh
emulate -L zsh # because we may be sourced by zsh `emulate bash -c`
# $funcfiletrace has format: file:line
local file=${funcfiletrace[1]%:*} line=${funcfiletrace[1]##*:}
local func=${funcstack[2]}
[[ $func =~ / ]] && func=source # $func may be filename. Use bash behaviour
fi
echo "${file##*/}:$func:$line $*" > /dev/tty
}