I'm trying to remove some numbers from end of a string using parameter expansion, like:
export ENV=dev12
echo ${ENV##[0-9]+}
But it doesn't work and I can't find anything on google on how to do this? Anyone know?
Parameter expansion uses glob syntax, not the more canonical regex syntax that grep and other tools use. You also have to use %% since ## is for prefixes.
There's no plain glob equivalent for what you want to do, but since you're using bash you can can enable extglob and use +([0-9]):
shopt -s extglob
ENV=dev12
echo ${ENV%%+([0-9])}
Using BASH regex you can do:
str='dev12'
[[ $str =~ ^(.*[^[:digit:]])[[:digit:]]+$ ]] && echo "${BASH_REMATCH[1]}"
dev
Related
I'm trying to port some code from bash 5.1 to 4.2.46. One function which tries to strip color codes from a specifically formatted string stopped working.
This is a sample string text in such format. I turn on extended globbing for this.
text="$(printf -- "%b%s%b" "\[\e[31m\]" "hello" "\[\e[0m\]")"
shopt -s extglob
In bash 5.1, this parameter expansion works to remove all the color codes and escape characters
bash-5.1$ echo "${text//$'\[\e'\[/}"
31m\]hello0m\]
bash-5.1$ echo "${text//$'\[\e'\[+([0-9])/}"
m\]hellom\]
bash-5.1$ echo "${text//$'\[\e'\[+([0-9])m$'\]'/}"
hello
In bash 4.2.46, I start getting a different behavior as I build up the parameter expansion.
bash-4.2.46$ echo "${text//$'\[\e'\[/}"
\31m\]hello\0m\]
bash-4.2.46$ echo "${text//$'\[\e'\[+([0-9])/}"
\[\]hello\[\] ## no longer matches because `+([0-9])` doesn't follow `\[`
The difference comes from this line: echo "${text//$'\[\e'\[/}"
bash-5.1: 31m\]hello0m\]
bash-4.2.46: \31m\]hello\0m\]
Here's what printf "%q" "${text//$'\[\e'\[/}" shows:
bash-5.1: 31m\\\]hello0m\\\]
bash-4.2.46: \\31m\\\]hello\\0m\\\]
Where is the extra \ coming from in 4.2.26?
Even when I try to remove it, the pattern stops matching:
bash-4.2.46$ echo "${text//$'\[\e'\[\\/}"
\[\]hello\[\] ## no longer matches because `\\` doesn't follow `\[`
I'm guessing there may be a bug related to parameter expansion, backslash escaping, and extended globbing.
I am aiming to write code that works on bash 4.0 onward, so I'm looking for a workaround primarily. An explanation (bug report, etc.) to why the behavior difference happens would be great, though.
Seems like a bug in bash. By bisecting the available versions, I found that 4.2.53(1)-release was the last version with this bug. Version 4.3.0(1)-release fixed the problem.
The list of changes mentions a few bug fixes in this direction. Maybe it was one of below bugfixes:
This document details the changes between this version, bash-4.3-alpha,
and the previous version, bash-4.2-release.
[...]
zz. When using the pattern substitution word expansion, bash now runs the
replacement string through quote removal, since it allows quotes in that
string to act as escape characters. This is not backwards compatible, so
it can be disabled by setting the bash compatibility mode to 4.2.
[...]
eee. Fixed a logic bug that caused extended globbing in a multibyte locale to
cause failures when using the pattern substititution word expansions.
Workaround
Instead of using parameter expansions with extglobs, use bash pattern matching with actual regexes (available in bash 3.0.0 and higher):
text=$'\[\e[31m\]hello\[\e[0m\]'
while [[ "$text" =~ (.*)$'\[\e['[0-9]*'m\]'(.*) ]]; do
text="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
done
echo "$text"
or rely on an external (but posix standarized) tool like sed:
text=$'\[\e[31m\]hello\[\e[0m\]'
text=$(sed $'s#\\\[\e[[0-9]*m\\\]##g' <<< "$text")
echo "$text"
The problem seems to be parsing $'...' inside ${test//<here>} when inside " quotes.
$ test='f() { "${text//\[$'\''\e'\''\[+([0-9])/}"; }; printf "%q\n" "$(declare -f f)"'; echo -n 'bash4.1 '; docker run bash:4.1 bash -c "$test" ; echo -n 'bash5.1 '; bash -c "$test"
bash4.1 $'f () \n{ \n "${text//\\[\E\\[+([0-9])/}"\n}'
bash5.1 $'f () \n{ \n "${text//\\[\'\E\'\\[+([0-9])/}"\n}'
Just use a variable.
esc=$'\e'
echo "${text//\\\[$esc\[+([0-9])/}"
I wonder whether there is a way to specify an one-or-more modifier for a character class in a parameter substitution pattern in bash, similar to a regex: i.e. [a-z]+. It seems like that for instance to remove all trailing whitespaces from a variable I would need to use ${fn##*[[:space:]]} (would only work for fn=" test" but not for fn=" test a") however I was hoping for something like ${fn##[[:space:]]+}. Is there a special syntax to specify regex instead of the patterns? Where is the pattern format specified?
Using extglob, you can do this:
shopt -s extglob
fn=" test a"
echo "${fn##+([[:space:]])}"
test a
Here glob expression +([[:space:]]) matches 1 or more whitespace characters.
You cannot use regular expressions in parameter expansions like that. However, the extended pattern syntax enabled by extglob is equivalent in power.
$ fn=" test a"
$ echo "$fn"
test a
$ shopt -s extglob
$ echo "${fn##*([[:space:]])}"
test a
I am trying to exclude a directory from a glob.
This works at the command line:
$ export exclude=BigDir
$ for d in ^$exclude/ ; do echo "$d" ; done
SmallDir/
SmallerDir/
$
But in a file it doesn't work at all
#!/bin/zsh
exclude=BigDir
for d in ^$exclude/ ; do echo "$d" ; done
Running ./test or however I saved it prints the literal string
^BigDir/
How do I get it to correctly expand in the script file?
You are incorrectly using the glob characters ? used by the shell and the regular expression constructs ^, $. The for loop in your example can not undergo a regex match to exclude the directory provided, since it undergoes only pathname expansion (aka. glob expansion)
Unless you let know the shell to treat ^ and $ as special by enabling extended glob options extglob in bash and extendedglob in zsh, you cannot achieve what you wanted to do.
So you probably just need
setopt extendedglob
print -rl ^BigDir*
meaning print anything except the the filenames matching with BigDir.
luajit -v
LuaJIT 2.1.0-beta3 -- Copyright (C) 2005-2017 Mike Pall. http://luajit.org/
I want to negate match the version part, and got LUAJIT_VERSION="2.1.0-beta3" at the beginning of bash script. I use:
if ! [[ "$(luajit -v)" =~ LuaJIT\s+"$LUAJIT_VERSION".* ]]; then
#rest of code
But it seems not working whether I put $LUAJIT_VERSION between "" or not:
Any part of the pattern may be quoted to force the quoted portion to be matched as a string ... If the pattern is stored in a shell variable, quoting the variable expansion forces the entire pattern to be matched as a string.
Bash docs
Can you tell me what's the correct way to do this task?
\s is not a recognized character class in bash; you need to use [[:blank:]] instead:
if ! [[ "$(luajit -v)" =~ LuaJIT[[:blank:]]+"$LUAJIT_VERSION" ]]; then
(The trailing .* isn't necessary, since regular expressions aren't anchored to the start or end of the string.)
However, it's not clear your regular expression needs to be that general. It looks like you can use a single, literal space
if ! [[ "$(luajit -v)" =~ LuaJIT\ "$LUAJIT_VERSION" ]];
or simply use pattern matching:
if [[ "$(luajit -v)" != LuaJIT\ "$LUAJIT_VERSION"* ]];
I'm trying to check if a url starts with http|https and ends with jpg|png. I have searched, but the answers don't work for me.
I have this currently:
if [[ $url = ^https?://.*jpg ]]
then
wget -O webcam.jpg $url
fi
But its fails to wget. What am I doing wrong?
You have to use the =~ operator for regular-expression matching; = only does pattern matching. The equivalent pattern would be http?(s)://*jpg*. (Recent versions of bash always use extended patterns inside [[ ... ]]; older versions may require they be turned on explicitly with shopt -s extglob.)
(I added the trailing * to the pattern because patterns are anchored to both ends of the string by default, while regular expressions require ^ and $ explicitly. Since you did not have $ at the end of your regular expression, I made the pattern open at the end as well.)