I have pattern matching problem in case command in bash - linux

I usually use pattern matching in [[ command,
but I wanted to use it in case command.
#!/bin/bash
bash -version|head -1
# Test 1:
[[ apple79 == apple#(14|38|79|11) ]] && echo ok 1
# Test2:
case apple79 in apple#(14|38|79|11)) echo ok 2;; *) ;; esac
When I run the above test.sh at terminal with . command everything is normal.
. test.sh
GNU bash, version 4.4.20(1)-release (x86_64-pc-linux-gnu)
ok 1
ok 2
But when I try to run it so:
./t.sh
GNU bash, version 4.4.20(1)-release (x86_64-pc-linux-gnu)
ok 1
./t.sh: line 6: syntax error near unexpected token `('
./t.sh: line 6: ` case apple79 in apple#(14|38|79|11)) echo ok 2;; *) ;; esac'
I have received an error messages for the case command.
I have seen some bash pattern matching doc eg.:
https://www.linuxjournal.com/content/pattern-matching-bash
But I have no any idea for this problem.

The extglob option is implicitly enabled inside [[ ... ]] for pattern matching, but you need to enable it explicitly to use it with the case statement.
#!/bin/bash
shopt -s extglob
[[ apple79 == apple#(14|38|79|11) ]] && echo ok 1
case apple79 in apple#(14|38|79|11)) echo ok 2;; *) ;; esac

The error is most likely because you have turned on extglob option in your current shell. Because sourcing the script takes the current shell's options and extended options, it works when sourcing the script.
But when doing the ./t.sh you are launching an explicit shell which does not have the option turned on by default. Since [[ operator with == turns on extglob by default, it works for the first test but fails for the case statement. To enable the option explicitly in scripts do shopt -s extglob at the top of your script.
As you can see below the pattern works with case only if the option is enabled. Try removing -O extglob from below command and you can see it doesn't work.
bash -O extglob -c 'case apple79 in apple#(14|38|79|11)) echo ok 2;; *) ;; esac'
As far why your attempt didn't work, try adding a line shopt extglob to your t.sh and repeat your tests. You'll notice that when the script is sourced you'll see extglob on and for the executed case get extglob off

The difference is between interactive and non-interactive shell options.
By default extglob are turned off for non-interactive shells, so you can do:
bash -i t.sh
or add
shopt -s extglob
at the beginning of your script

Related

How to detect shell options enabled

set allows tweaking shell execution, by enabling some features. Some of the most popular are:
set -e # exit on error
set -x # print executed commands
Is there a way within a script to detect currently enabled features?
In my particular case I would like to know if the set -x was called.
Use the $- variable:
$ echo $-
himBCHs
$ set -e
$ set -x
$ echo $-
ehimxBCHs
^ ^
So:
if [[ $- == *x* ]]; then
echo "xtrace is set"
fi
The command set -o prints all the configured bash options.
The builtin shopt can probably show what you wanted.
In man bash
errexit Same as -e
Now
help shopt
Output
shopt: shopt [-pqsu] [-o] [optname ...]
Set and unset shell options.
Change the setting of each shell option OPTNAME. Without any option
arguments, list each supplied OPTNAME, or all shell options if no
OPTNAMEs are given, with an indication of whether or not each is set.
Options:
-o restrict OPTNAMEs to those defined for use with `set -o'
-p print each shell option with an indication of its status
-q suppress output
-s enable (set) each OPTNAME
-u disable (unset) each OPTNAME
Exit Status:
Returns success if OPTNAME is enabled; fails if an invalid option is
given or OPTNAME is disabled.
shopt -qo errexit
echo $?
It returns 1 if disabled or 0 if enabled.
Now to have an actual test.
if shopt -qo errexit; then
printf 'errexit is enabled!\n'
fi
Or if you want to negate use the bang !
if ! shopt -qo errexit; then
printf 'errexit is disabled!\n'
fi

Bash return code error handling when using heredoc input

Motivation
I'm in a situation where I have to run multiple bash commands with a single bash invocation without the possibility to write a full script file (use case: Passing multiple commands to a container in Kubernetes). A common solution is to combine commands with ; or &&, for instance:
bash -c " \
echo \"Hello World\" ; \
ls -la ; \
run_some_command "
In practice writing bash scripts like that turns out to be error prone, because I often forget the semicolon leading to subtle bugs.
Inspired by this question, I was experiment with writing scripts in a more standard style by using a heredoc:
bash <<EOF
echo "Hello World"
ls -la
run_some_command
EOF
Unfortunately, I noticed that there is a difference in exit code error handling when using a heredoc. For instance:
bash -c " \
run_non_existing_command ; \
echo $? "
outputs (note that $? properly captures the exit code):
bash: run_non_existing_command: command not found
127
whereas
bash <<EOF
run_non_existing_command
echo $?
EOF
outputs (note that $? fails to capture the exit code compared to standard script execution):
bash: line 1: run_non_existing_command: command not found
0
Why is the heredoc version behaving differently? Is it possible to write the script in the heredoc style and maintaining normal exit code handling?
Why is the heredoc version behaving differently?
Because $? is expanded before running the command.
The following will output 1, that is the exit status of false command:
false
bash <<EOF
run_non_existing_command
echo $?
EOF
It's the same in principle as the following, which will print 5:
variable=5
bash <<EOF
variable="This is ignored"
echo $variable
EOF
Is it possible to write the script in the heredoc style and maintaining normal exit code handling?
If you want to have the $? expanded inside the subshell, then:
bash <<EOF
run_non_existing_command
echo \$?
EOF
or
bash <<'EOF'
run_non_existing_command
echo $?
EOF
Also note that:
bash -c \
run_non_existing_command ;
echo $? ;
is just equal to:
bash -c run_non_existing_command
echo $?
The echo $? is not executed inside bash -c.

Parameter Substitution replace with Alternation (OR) |

I'm trying to replace a extension of a filename considering case but without success.
#!/bin/bash
pdf_file="/root/users/test.pdf"
jpg_file="${pdf_file/.pdf/.jpg}"
echo $jpg_file
I tried it, but it doesn't work:
jpg_file="${pdf_file/(.pdf|.PDF)/.jpg}"
You could use a glob pattern like this:
$ echo "${pdf_file/.[Pp][Dd][Ff]/.jpg}"
/root/users/test.jpg
If you use extended glob patterns (shopt -s extglob), you could use this instead:
$ echo "${pdf_file/.#(PDF|pdf)/.jpg}"
/root/users/test.jpg
Or you could use the shell option to ignore case when matching:
$ shopt -s nocasematch
$ pdf_file="/root/users/test.PDF"
$ echo "${pdf_file/.pdf/.jpg}"
/root/users/test.jpg
Remark valid for all three solutions: ${parameter/pattern/string} replaces the pattern wherever it occurs, but the extension is likely at the end – we can make sure we'll only replace it at the end:
echo "${pdf_file%.[Pp][Dd][Ff]}.jpg"
which works in any POSIX shell, or
shopt -s extglob
echo "${pdf_file%.#(PDF|pdf)}.jpg"
or
shopt -s nocasematch
pdf_file="/root/users/test.PDF"
echo "${pdf_file%.pdf}.jpg"

Bash - Update terminal title by running a second command

On my terminal in Ubuntu, I often run programs which keep running for a long time. And since there are a lot of these programs, I keep forgetting which terminal is for which program, unless I tab through all of those. So I wanted to find a way to update my terminal title to the program name, whenever I run a command. I don't want to do it manually.
I use gnome-terminal, but answer shouldn't really depend on that. Basically, If I'm able to run a second command, then I can simply use gconftool command to update the title. So I was hoping to find a way to capture the command in bash and update the title after every command. How do I do that?
I have some answers for you :) You're right that it shouldn't matter that you're using gnome-terminal, but it does matter what command shell you're using. This is a lot easier in zsh, but in what follows I'm going to assume you're using bash, and that it's a fairly recent version (> 3.1).
First of all:
Which environment variable would
contain the current 'command'?
There is an environment variable which has more-or-less what you want - $BASH_COMMAND. There's only one small hitch, which is that it will only show you the last command in a pipe. I'm not 100% sure what it will do with combinations of subshells, either :)
So I was hoping to find a way to
capture the command in bash and update
the title after every command.
I've been thinking about this, and now that I understand what you want to do, I realized the real problem is that you need to update the title before every command. This means that the $PROMPT_COMMAND and $PS1 environment variables are out as possible solutions, since they're only executed after the command returns.
In bash, the only way I can think of to achieve what you want is to (ab)use the DEBUG SIGNAL. So here's a solution -- stick this at the end of your .bashrc:
trap 'printf "\033]0;%s\007" "${BASH_COMMAND//[^[:print:]]/}"' DEBUG
To get around the problem with pipes, I've been messing around with this:
function settitle () {
export PREV_COMMAND=${PREV_COMMAND}${#}
printf "\033]0;%s\007" "${BASH_COMMAND//[^[:print:]]/}"
export PREV_COMMAND=${PREV_COMMAND}' | '
}
export PROMPT_COMMAND=${PROMPT_COMMAND}';export PREV_COMMAND=""'
trap 'settitle "$BASH_COMMAND"' DEBUG
but I don't promise it's perfect!
Try this:
trap 'echo -ne "\033]2;$(history 1 | sed "s/^[ ]*[0-9]*[ ]*//g")\007"' DEBUG
Thanks to the history 1 it works even with complicated expressions like:
true && (false); echo $? | cat
For which approaches relying on $BASH_COMMAND or $# fail. For example simon's displays:
true | echo $? | cat
Thanks to Gilles and simon for providing inspiration.
I see what stoutie is trying to do, except it's a lot more work than needed. And doesn't cause all sorts of other potentially bad things that can occur as a result of redefining 'cd' and putting in all of that testing just to change directories. Bash has built in support for most of this.
You can put this in your .bashrc anywhere after you set your current PS1 prompt (this way it just prepends it)
# If this is an xterm set the titlebar to user#host:dir
case "$TERM" in
xterm*|rxvt*)
PS1="\[\e]0;\u#\h: \w\a\]$PS1"
;;
*)
;;
esac
The OP asked for bash, but others might be interested to learn that (as mentioned above) this is indeed a lot easier using the zsh shell. Example:
# Set window title to command just before running it.
preexec() { printf "\x1b]0;%s\x07" "$1"; }
# Set window title to current working directory after returning from a command.
precmd() { printf "\x1b]0;%s\x07" "$PWD" }
In preexec, $1 contains the command as typed (requires shell history to be enabled, which seems to be a fair assumption), $2 the expanded command (shell aliases etc.) and $3 the "very expanded" command (shell function bodies). (more)
I'm doing something like this, to show my pwd in the title, which could be modified to do whatever you want to do with the title:
function title { echo -en "\033]2;$1\007"; }
function cd { dir=$1; if [ -z "$dir" ]; then dir=~; fi; builtin cd "$dir" && title `pwd`; }
I just threw this in my ~/.bash_aliases.
Update
I ran into strange bugs with my original answer. I ended up picking apart the default Ubuntu PS1 and breaking it into parts only to realize one of the parts was the title:
# simple prompt
COLOR_YELLOW_BOLD="\[\033[1;33m\]"
COLOR_DEFAULT="\[\033[0m\]"
TITLE="\[\e]0;\u#\h:\w\a\]"
PROMPT="\w\n$ "
HUH="${debian_chroot:+($debian_chroot)}"
PS1="${COLOR_YELLOW_BOLD}${TITLE}${HUH}${PROMPT}${COLOR_DEFAULT}"
Without breaking into variables, it would look like this:
PS1="\[\033[1;33m\]\[\e]0;\u#\h:\w\a\]${debian_chroot:+($debian_chroot)}\w\n$ \[\033[0m\]"
I have tested three method, all is OK, use any one for your pleasure.
export PROMPT_COMMAND='echo -ne "\033]2;$(history 1 | sed "s/^[ ]*[0-9]*[ ]*//g")\007"'
trap 'echo -ne "\033]2;$(history 1 | sed "s/^[ ]*[0-9]*[ ]*//g")\007"' DEBUG
trap 'echo -ne "\e]0;"; echo -n $BASH_COMMAND; echo -ne "\a"' DEBUG
please note if use $BASH_COMMAND, it don't recognize bash alias, and use PROMPT_COMMAND show finished command, but use trap show running command.
Based on the the need to auto position putty windows I have modified my /etc/bash.bashrc file on a Debian/Ubuntu system. I have posted the full contents for completeness but the relevant bit to starts on the # Display command ... comment line.
# System-wide .bashrc file for interactive bash(1) shells.
# To enable the settings / commands in this file for login shells as well,
# this file has to be sourced in /etc/profile.
# If not running interactively, don't do anything
[ -z "$PS1" ] && return
# check the window size after each command and, if necessary,
# update the values of LINES and COLUMNS.
shopt -s checkwinsize
# set variable identifying the chroot you work in (used in the prompt below)
if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then
debian_chroot=$(cat /etc/debian_chroot)
fi
# set a fancy prompt (non-color, overwrite the one in /etc/profile)
PS1='${debian_chroot:+($debian_chroot)}\u#\h:\w\$ '
# Display command run in title which allows us to distinguish Kitty/Putty
# windows and re-position easily using AutoSizer window utility. Based on a
# post here: http://mg.pov.lt/blog/bash-prompt.html
case "$TERM" in
xterm*|rxvt*)
# Show the currently running command in the terminal title:
# http://www.davidpashley.com/articles/xterm-titles-with-bash.html
show_command_in_title_bar()
{
case "$BASH_COMMAND" in
*\033]0*)
# The command is trying to set the title bar as well;
# this is most likely the execution of $PROMPT_COMMAND.
# In any case nested escapes confuse the terminal, so don't
# output them.
;;
*)
echo -ne "\033]0;${USER}#${HOSTNAME}: ${BASH_COMMAND}\007"
;;
esac
}
trap show_command_in_title_bar DEBUG
;;
*)
;;
esac
# Commented out, don't overwrite xterm -T "title" -n "icontitle" by default.
# If this is an xterm set the title to user#host:dir
#case "$TERM" in
#xterm*|rxvt*)
# PROMPT_COMMAND='echo -ne "\033]0;${USER}#${HOSTNAME}: ${PWD}\007"'
# ;;
#*)
# ;;
#esac
# enable bash completion in interactive shells
if ! shopt -oq posix; then
if [ -f /usr/share/bash-completion/bash_completion ]; then
. /usr/share/bash-completion/bash_completion
elif [ -f /etc/bash_completion ]; then
. /etc/bash_completion
fi
fi
# if the command-not-found package is installed, use it
if [ -x /usr/lib/command-not-found -o -x /usr/share/command-not-found/command-not-found ]; then
function command_not_found_handle {
# check because c-n-f could've been removed in the meantime
if [ -x /usr/lib/command-not-found ]; then
/usr/bin/python /usr/lib/command-not-found -- "$1"
return $?
elif [ -x /usr/share/command-not-found/command-not-found ]; then
/usr/bin/python /usr/share/command-not-found/command-not-found -- "$1"
return $?
else
printf "%s: command not found\n" "$1" >&2
return 127
fi
}
fi
You can set up bash such that it sends a certain escape sequence to the terminal every time it starts an external program. If you use the escape sequence that terminals use to update their titles, your problem should be solved.
I have used that before, so I know it is possible. but I cannot remember it off the top of my head and do not have time to research the details right now, though.
Some of the old methods were removed from gnome-terminal 3.14 due to these two bugs (724110 and 740188).
In Ubuntu 20.04
PS1=$PS1"\[\e]0;New_Terminal_Name\a\]"
\[ begin a sequence of non-printing characters
\e]0; is the char sequence for setting the terminal title. Bash identifies this sequence and set the tile with the following characters. Number 0 turns out to be the value to reference the title property.
New_Terminal_Name is the tile we gave
\a is the ASCII bell character, also in this case, it marks the end of the tile to read from Bash.
\] end a sequence of non-printing characters
We can create a function for future use
function set_title(){
if [ -z "$PS1_BACK" ]; # set backup if it is empty
then
PS1_BACK="$PS1"
fi
TITLE="\[\e]0;$*\a\]"
PS1="${PS1_BACK}${TITLE}"
}
Open the ~/.bashrc file in your home directory with a text editor and append the above function at the end of it. Save and close.
To use it immediately source it to the current terminal.
source ~/.bashrc
We can use it then like this
set_title <New terminal tab title>
My terminal window titler script
This dynamic backgrounded script show all running command with pid number and elapsed time in seconds, like if I run du -h | less, this will build title looking like:
204640 6 du -h | 204641 6 less
Then when no command (other than himself) are running, don't change the terminal title, so standard behaviours works normaly.
First run start backgroud task. Second run in same terminal ask for kill previous backgrounded task.
Save this into a file, set execute flag then run it without argument:
cat <<"EOF" >titleWin.sh
#!/bin/bash
## Ask for kill process if already started
mapfile -t pids < <(ps -C ${0##*/} ho pid)
for pid in ${pids[#]} ;do
if [[ $pid != $$ ]] && [ -d /proc/$pid ]; then
echo -n "STARTED: [$pid]: ${0##*/}. Kill them (Y/n)? "
read -rsn 1 act
case $act in
n|N ) echo No;;
* ) echo Yes;kill $pid ;;
esac
exit
fi
done
## Title win for xterm or screen (or tmux).
case $TERM in
xterm*|rxvt* ) titleFmt='\e];%s\a';;
screen* ) titleFmt='\ek%s\e\\';;
* ) echo "Unable to title window.";exit 1;;
esac
tty=$(tty)
## Date to epochseconds converter
exec {dateout}<> <(:)
exec {datein}> >(exec stdbuf -o0 date -f - +%s >&$dateout)
DPID=$!
trap "echo TRAP;kill $DPID" 1 2 3 6 9 15
# Main loop
while :;do
string=""
while read -r pid wday mon day time year cmd; do
if [[ $pid != $$ ]] && [[ $pid != $PPID ]] && [[ $pid != $BASHPID ]] &&
[[ $pid != $DPID ]] && [ "${cmd#*pid,lstart,cmd}" ] &&
[ -d /proc/$pid ] ;then
echo >&${datein} $wday $mon $day $time $year
read -ru $dateout date
string+="$pid $((EPOCHSECONDS-date)) $cmd | "
fi
done < <(exec ps --tty ${tty#*/dev/} ho pid,lstart,cmd)
[[ "$string" ]] && printf "$titleFmt" "${string% | }"
sleep .333
done &
EOF
chmod +x titleWin.sh
./titleWin.sh

Using getopt in unix shell

I have to use a Unix script to pass arguments:
./Script.sh -c "abc" -d "def" -k "abc -d -c"
where the argument for:
-c = "abc"
-d = "def"
-k = "abc -d -c"
How can I handle options in a Uunix shell script?
Here is some option handling using getopts:
# -F Final version (do not append date to version)
# -s suffix Add '-suffix' after version number
# -V Print version and exit
# -h Print help and exit
# -j jdcfile JDC file for project - required
# -q Quiet operation
# -v Verbose operation
arg0=$(basename $0 .sh)
usage()
{
echo "Usage: $arg0 [-hqvFV] [-s suffix] -j jdcfile file.msd" 1>&2
exit 1
}
error()
{
echo "$0: $*" 1>&2
exit 1
}
Fflag=
suffix=
jdcfile=
qflag=
vflag=no
while getopts FVhj:qs:v opt
do
case "$opt" in
(F) Fflag="-F";;
(V) echo "Version information";;
(h) echo "Help information";;
(j) jdcfile="$OPTARG";;
(q) qflag="-q";;
(s) suffix="$OPTARG";;
(v) vflag=yes;;
(*) usage;;
esac
done
shift $(($OPTIND - 1))
case $# in
(1) : OK;;
(*) usage;;
esac
if [ -z "$jdcfile" ]
then error "you did not specify which jdcfile to use (-j option)"
fi
The script then continues and does its task based on the options it was given. The shift removes the 'consumed' options, leaving just the file name arguments.
The argument can contain whitespace, so either use the getopts built-in shell command or the GNU enhanced version of the external getopt program.
The getopts option is more portable, because not all systems have the GNU enhanced version of getopt. For example, Linux has the GNU enhanced version, but Mac OS X does not. The original version of getopt does not support whitespaces. Despite this limitation, there is a reason why you might want to use toe the GNU enhanced version: it supports long option names, which getopts does not.
This is how to use the GNU enhanced getopt with whitespaces. It is important to use "$#" (use $# instead of $* and make sure the double quotes are around it) and to eval the whole set command so that whitespaces are handled properly.
eval set -- `getopt --long currdir:,dir:,argval:,verbose -o c:d:k:v -- "$#"`
while [ $# -gt 0 ]
do
case "$1" in
-c | --currdir) CURRDIR="$2"; shift;;
-d | --dir) MYDIR="$2"; shift;;
-k | --argval) ARGVAL="$2"; shift;;
-v | --verbose) VERBOSE=yes;;
esac
shift
done
There is a command getopts, and a program getopt, though I'm of the opinion that by the time you need to handle arguments, you've outgrown shell scripting.
I'm not actually sure how getopts work, having never actually used it; but here, and check your shell's docs.
getopt splits your arguments into flags -- rest as far as I can tell.

Resources