Confusion about bash parameter substitution - bash

I have a script to rename a file or a series of files that contain a specific string
for i in "${#:3}"; do
mv -- "$i" "${i/$1/$2}"
done
so when i do
bash script_name patter_1 pattern_2 string*
it does work but when i try:
mv -- "$i" "${i//$1/$2}"
it still works
${i/$1/$2}
and
${i//$1/$2}
so why is that happening, i search bash guide for beginner but still have no clue. Thank you

From the bash manual:
${parameter/pattern/string}
The pattern is expanded to produce a pattern just as in filename
expansion. Parameter is expanded and the longest match of pattern
against its value is replaced with string. If pattern begins with
‘/’, all matches of pattern are replaced with string
So if the replacement can be done just once, these are equivalent:
${parameter/pattern/string}
^
${parameter//pattern/string}
^^
See an example:
$ i="hello"
$ echo ${i/e/XX} #just one replacement
hXXllo # <----------------------
$ echo ${i//e/XX} # multiple replacements | they are the same
hXXllo # <----------------------
$ echo ${i/l/XX} #just one replacement
heXXlo # it just happens once!
$ echo ${i//l/XX} #multiple replacements
heXXXXo # it happens many times!

Related

extract path value substring using sed

Trying to extract text between a path variable which has the following value
path_value="path/to/value/src"
I want to extract just value from the above variable and use that later in my script. I know it can be done using grep or awk but I wanted to know how it can be done using sed
So I tried this
service_name=$(echo $path_value | sed -e 's/path/to/(.*\)/.*/\1/')
But I get this error bad flag in substitute command: '('
Could you please suggest what is the right regex to achieve what I am trying to do?
Using parameter substitution and eliminating the subprocess calls:
$ path_value="path/to/value/src"
$ tempx="${path_value%/*}"
$ echo "${tempx}"
path/to/value
$ service_name="${tempx##*/}"
$ echo "${service_name}"
value
Performing a bash/regex comparison and retrieving the desired item from the BASH_REMATCH[] array (also eliminates subprocess calls):
$ regex='.*/([^/]+)/([^/]+)$'
$ [[ "${path_value}" =~ $regex ]] && service_name="${BASH_REMATCH[1]}"
$ echo "${service_name}"
# fwiw, contents of the BASH_REMATCH[] array:
$ typeset -p BASH_REMATCH
declare -ar BASH_REMATCH=([0]="path/to/value/src" [1]="value" [2]="src")
You can use
#!/bin/bash
path_value="path/to/value/src"
service_name=$(echo "$path_value" | sed 's~path/to/\([^/]*\)/.*~\1~')
echo "$service_name"
# => value
See the online demo.
Note I replaced / regex delimiters with ~ so as to avoid escaping / chars inside the pattern.
The capturing parentheses must both be escaped in a POSIX BRE regex.
The [^/]* part only matches zero or more chars other than /.

glob pattern doesn't expand inside file in zsh

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.

How truncate the ../ characters from string in bash?

How can I truncate the ../ or .. characters from string in bash
So, If I have strings
str1=../lib
str2=/home/user/../dir1/../dir2/../dir3
then how I can get string without any .. characters in a string like after truncated result should be
str1=lib
str2=/home/user/dir1/dir2/dir3
Please note that I am not interesting in absolute path of string.
You don't really need to fork a sub-shell to call sed. Use bash parameter expansion:
echo ${var//..\/}
str1=../lib
str2=/home/user/../dir1/../dir2/../dir3
echo ${str1//..\/} # Outputs lib
echo ${str2//..\/} # Outputs /home/user/dir1/dir2/dir3
You could use:
pax> str3=$(echo $str2 | sed 's?\.\./??g') ; echo $str3
/home/user/dir1/dir2/dir3
Just be aware (as you seem to be) that's a different path to the one you started with.
If you're going to be doing this infrequently, forking an external process to do it is fine. If you want to use it many times per second, such as in a tight loop, the internal bash commands will be quicker:
pax> str3=${str2//..\/} ; echo $str3
/home/user/dir1/dir2/dir3
This uses bash pattern substitution as described in the man page (modified slightly to adapt to the question at hand):
${parameter/pattern/string}
The parameter is expanded and the longest match of pattern against its value is replaced with string. If pattern begins with /, all matches of pattern are replaced with string.
If string is null, matches of pattern are deleted and the / following pattern may be omitted.
You can use sed to achieve it
sed 's/\.\.\///g'
For example
echo $str2 | sed 's/\.\.\///g'
OP => /home/user/dir1/dir2/dir3

BASH: Path difference between two paths?

Say I have the paths
a/b/c/d/e/f
a/b/c/d
How do I get the below?
e/f
You can strip one string from the other with:
echo "${string1#"$string2"}"
See:
$ string1="a/b/c/d/e/f"
$ string2="a/b/c/d"
$ echo "${string1#"$string2"}"
/e/f
From man bash -> Shell parameter expansion:
${parameter#word}
${parameter##word}
The word is expanded to produce a pattern just as in filename
expansion. If the pattern matches the beginning of the expanded value
of parameter, then the result of the expansion is the expanded value
of parameter with the shortest matching pattern (the ‘#’ case) or the
longest matching pattern (the ‘##’ case) deleted.
With spaces:
$ string1="hello/i am here/foo/bar"
$ string2="hello/i am here/foo"
$ echo "${string1#"$string2"}"
/bar
To "clean" multiple slashes, you can follow Roberto Reale's suggestion and canonicalize the paths with readlink -m to allow comparison with strings with the same real path up:
$ string1="/a///b/c//d/e/f/"
$ readlink -m $string1
/a/b/c/d/e/f

bash: Storing a pattern in a variable without the pattern being evaluated

I'm reading in a string from the command line, and storing it into a variable. The problem I'm having is that when I'm trying to store a pattern (for example *.cpp), the variable will end up holding the first match to that pattern if there's a match in my current directory, and the pattern if there's no match. What do I need to do in order to store a pattern into a string?
Question as code:
pattern=$1
#say $1 is *.cpp
#what do I need to do to $1 for pattern to hold *.cpp rather than the 1st match to *.cpp
EDIT: I did wrap $1 in quotes, but my result is the same as using no quotes at all.
Issue as a reproductable case:
#$1 is *.cpp
#!/bin/bash
pattern = "$1"
echo $pattern
pattern does not echo *.cpp, but the first file found that matches the pattern.
Did some testing. It seems you need to quote both in the call and in the use in the script. I.e. in the script:
pattern=$1
echo "$pattern"
and then when calling the script:
./test.sh "*.cpp"
result:
*.cpp
You can use single quote when passing parameter, so the exact string is preserved.
myscript.sh:
#!/bin/bash
echo $1
Calling from cmdline:
# ./myscript.sh '*.cpp'
*cpp
Also the same result with backslash escaping:
# ./myscript.sh \*.cpp
*cpp

Resources