cd using bash rematch and results of pwd - bash

I'm looking to run pwd, and if the contents include "/Volumes/Storage/Users/testuser/", then redirect to ~/, followed by whatever the rest of the contents of pwd were. So for example if pwd returned:
/Volumes/Storage/Users/testuser/Desktop/folder1/folder2/
This bash script would cd into:
~/Desktop/folder1/folder2/
What I have so far is this, but I can't seem to get the match group to display properly:
[[ $(pwd) =~ \/Volumes\/Storage\/Users\/testuser\/(.*) ]] && cd "~/${BASH_REMATCH[1]}"

The tilde can't be quoted, otherwise cd will see a literal ~ character.
cd ~/"${BASH_REMATCH[1]}"
You also do not need to escape the slashes, for what it's worth.

Maybe you are interested in an alternative solution.
I suggest the following using shell parameter expansion.
PREFIX="/Volumes/Storage/Users/testuser/"
PWD=`pwd`
[[ ${PWD} =~ ${PREFIX}* ]] && cd ~/"${PWD#${PREFIX}}"
The form ${PWD#${PREFIX}} removes ${PREFIX} at the beginning of ${PWD}.

You should not be esacping the slashes in your regex.
Also, the ~ will not work correctly in a string, as John Kugelman mentioned.
if [[ $(pwd) =~ /Volumes/Storage/Users/testuser/(.*) ]]; then
cd "$HOME/${BASH_REMATCH[1]}"
fi

What are you trying to do?
If a user tries to cd to /Volumes/Storage/Users/testuser/Desktop/folder1/folder2/ and their HOME directory is /home/bob, they should cd to /home/bob/Desktop/folder1/folder2 instead?
What if /home/bob/Desktop/folder1/folder2 doesn't exist?
Here's a function to replace cd with one that munges the directory you're CD'ing to. Alias _cd to cd and you're all set.
The same basic thing could be done in other shell scripts if you're not munging the cd command.
I'm using the ${parameter#word} syntax to remove the $BAD_DIR prefix. I use glob matching to see if the directory has the bad directory as a prefix.
And then I use HOME_DIR=~ to set my true HOME directory. I don't know what happens if a user munges $HOME if it changes ~ or not. However, this allows me to use quotes in my directory name.
I probably should check $PWD to make sure they're not already there, and verify if this is a relative cd vs one where a full path is given. However, that's easy enough to add in. This should be enough to get you going.
function _cd {
cd_to_dir="$1"
BAD_DIR="/Volumes/Storage/Users/testuser"
if [[ $cd_to_dir = $BAD_DIR* ]]
then
HOME_DIR=~
cd_to_dir="$HOME_DIR/${cd_to_dir#${BAD_DIR}}"
fi
\cd "$cd_to_dir"
}
alias cd=_cd
This works on my computer. I have BASH, but I also set extglob and a few other things. This SHOULD work without extglob set, but if it doesn't add these lines:
function _cd {
cd_to_dir="$1"
BAD_DIR="/Volumes/Storage/Users/testuser"
is_extglob_set=$(shopt -q extglob)
[ $is_extglob_set ] || shopt -s extglob
if [[ $cd_to_dir = $BAD_DIR* ]]
then
HOME_DIR=~
cd_to_dir="$HOME_DIR/${cd_to_dir#${BAD_DIR}}"
fi
[ $is_extglob_set ] || shopt -u extglob
\cd $cd_to_dir
}
alias cd=_cd

Related

Is it possible to CD into a file?

I find a list of files that I need to cd to (obviously to the parent directory).
If I do cd ./src/components/10-atoms/fieldset/package.json I get the error cd: not a directory:, which makes sense.
But isn't there a way to allow for that? Because manipulating the path-string is pretty cumbersome and to me that would make total sense to have an option for that, since cd is a directory function and it would be cool that if the path would not end up in a file, it would recursively jump higher and find the "first dir" from the given path.
So cd ./src/components/10-atoms/fieldset/package.json would put me into ./src/components/10-atoms/fieldset/ without going on my nerves, telling me that I have chosen a file rather than a dir.
You could write a shell function to do it.
cd() {
local args=() arg
for arg in "$#"; do
if [[ $arg != -* && -e $arg && ! -d $arg ]]; then
args+=("$(dirname "$arg")")
else
args+=("$arg")
fi
done
builtin cd ${args[0]+"${args[#]}"}
}
Put it in your ~/.bashrc if you want it to be the default behavior. It won't be inherited by shell scripts or other programs so they won't be affected.
It modifies cd's arguments, replacing any file names with the parent directory. Options with a leading dash are left alone. command cd calls the underlying cd builtin so we don't get trapped in a recursive loop.
(What is this unholy beast: ${args[0]+"${args[#]}"}? It's like "${args[#]}", which expands the array of arguments, but it avoids triggering a bash bug with empty arrays on the off chance that your bash version is 4.0-4.3 and you have set -u enabled.)
This function should do what you need:
cdd() { test -d "$1" && cd "$1" || cd $(dirname "$1") ; }
If its first argument "$1" is a directory, just cd into it,
otherwise cd into the directory containing it.
This function should be improved to take into account special files such as devices or symbolic links.
You can if you enter a bit longer line (or create dedicated shell script)
cd $(dirname ./src/components/10-atoms/fieldset/package.json)
If you add it in script it can be :
cd $(dirname $1)
but you need to execute it on this way:
. script_name ./src/components/10-atoms/fieldset/package.json
You can put this function in your ~/.bashrc:
function ccd() {
TP=$1 # destination you're trying to reach
while [ ! -d $TP ]; do # if $TP is not a directory:
TP=$(dirname $TP) # remove the last part from the path
done # you finally got a directory
cd $TP # and jump into it
}
Usage: ccd /etc/postfix/strangedir/anotherdir/file.txt will get you to /etc/postfix.

How-To get root directory of given path in bash?

My script:
#!/usr/bin/env bash
PATH=/home/user/example/foo/bar
mkdir -p /tmp/backup$PATH
And now I want to get first folder of "$PATH": /home/
cd /tmp/backup
rm -rf ./home/
cd - > /dev/null
How can I always detect the first folder like the example above? "dirname $PATH" just returns "/home/user/example/foo/".
Thanks in advance! :)
I've found a solution:
#/usr/bin/env bash
DIRECTORY="/home/user/example/foo/bar"
BASE_DIRECTORY=$(echo "$DIRECTORY" | cut -d "/" -f2)
echo "#$BASE_DIRECTORY#";
This returns always the first directory. In this example it would return following:
#home#
Thanks to #condorwasabi for his idea with awk! :)
You can try this awk command:
basedirectory=$(echo "$PATH" | awk -F "/" '{print $2}')
At this point basedirectory will be the string home
Then you write:
rm -rf ./"$basedirectory"/
If PATH always has an absolute form you can do tricks like
ROOT=${PATH#/} ROOT=/${ROOT%%/*}
Or
IFS=/ read -ra T <<< "$PATH"
ROOT=/${T[1]}
However I should also add to that that it's better to use other variables and not to use PATH as it would alter your search directories for binary files, unless you really intend to.
Also you can opt to convert your path to absolute form through readlink -f or readlink -m:
ABS=$(readlink -m "$PATH")
You can also refer to my function getabspath.
To get the first directory component of VAR:
echo ${VAR%${VAR#/*/}}
So, if VAR="/path/to/foo", this returns /path/.
Explanation:
${VAR#X} strips off the prefix X and returns the remainder. So if VAR=/path/to/foo, then /*/ matches the prefix /path/ and the expression returns the suffix to/foo.
${VAR%X} strips off the suffix X. By inserting the output of ${VAR#X}, it strips off the suffix and returns the prefix.
If you can guarantee that your paths are well formed this is a convenient method. It won't work well for some paths, such as //path/to/foo or path/to/foo, but you can handle such cases by breaking down the strings further.
To get the first firectory:
path=/home/user/example/foo/bar
mkdir -p "/tmp/backup$path"
cd /tmp/backup
arr=( */ )
echo "${arr[0]}"
PS: Never use PATH variable in your script as it will overrider default PATH and you script won't be able to execute many system utilities
EDIT: Probably this should work for you:
IFS=/ && set -- $path; echo "$2"
home
Pure bash:
DIR="/home/user/example/foo/bar"
[[ "$DIR" =~ ^[/][^/]+ ]] && printf "$BASH_REMATCH"
Easy to tweak the regex.
You can use dirname...
#/usr/bin/env bash
DIRECTORY="/home/user/example/foo/bar"
BASE_DIRECTORY=$(dirname "${DIRECTORY}")
echo "#$BASE_DIRECTORY#";
Outputs the following...
/home/user/example/foo

bash - recursive script can't see files in sub directory

I got a recursive script which iterates a list of names, some of which are files and some are directories.
If it's a (non-empty) directory, I should call the script again with all of the files in the directory and check if they are legal.
The part of the code making the recursive call:
if [[ -d $var ]] ; then
if [ "$(ls -A $var)" ]; then
./validate `ls $var`
fi
fi
The part of code checking if the files are legal:
if [[ -f $var ]]; then
some code
fi
But, after making the recursive calls, I can no longer check any of the files inside that directory, because they are not in the same directory as the main script, the -f $var if cannot see them.
Any suggestion how can I still see them and use them?
Why not use find? Simple and easy solution to the problem.
Always quote variables, you never known when you will find a file or directory name with spaces
shopt -s nullglob
if [[ -d "$path" ]] ; then
contents=( "$path"/* )
if (( ${#contents[#]} > 0 )); then
"$0" "${contents[#]}"
fi
fi
you're re-inventing find
of course, var is a lousy variable name
if you're recursively calling the script, you don't need to hard-code the script name.
you should consider putting the logic into a function in the script, and the function can recursively call itself, instead of having to spawn an new process to invoke the shell script each time. If you do this, use $FUNCNAME instead of "$0"
A few people have mentioned how find might solve this problem, I just wanted to show how that might be done:
find /yourdirectory -type f -exec ./validate {} +;
This will find all regular files in yourdirectory and recursively in all its sub-directories, and return their paths as arguments to ./validate. The {} is expanded to the paths of the files that find locates within yourdirectory. The + at the end means that each call to validate will be on a large number of files, instead of calling it individually on each file (wherein the + is replaced with a \), this provides a huge speedup sometimes.
One option is to change directory (carefully) into the sub-directory:
if [[ -d "$var" ]] ; then
if [ "$(ls -A $var)" ]; then
(cd "$var"; exec ./validate $(ls))
fi
fi
The outer parentheses start a new shell so the cd command does not affect the main shell. The exec replaces the original shell with (a new copy of) the validate script. Using $(...) instead of back-ticks is sensible. In general, it is sensible to enclose variable names in double quotes when they refer to file names that might contain spaces (but see below). The $(ls) will list the files in the directory.
Heaven help you with the ls commands if any file names or directory names contain spaces; you should probably be using * glob expansion instead. Note that a directory containing a single file with a name such as -n would trigger a syntax error in your script.
Corrigendum
As Jens noted in a comment, the location of the shell script (validate) has to be adjusted as you descend the directory hierarchy. The simplest mechanism is to have the script on your PATH, so you can write exec validate or even exec $0 instead of exec ./validate. Failing that, you need to adjust the value of $0 — assuming your shell leaves $0 as a relative path and doesn't mess around with converting it to an absolute path. So, a revised version of the code fragment might be:
# For validate on PATH or absolute name in $0
if [[ -d "$var" ]] ; then
if [ "$(ls -A $var)" ]; then
(cd "$var"; exec $0 $(ls))
fi
fi
or:
# For validate not on PATH and relative name in $0
if [[ -d "$var" ]] ; then
if [ "$(ls -A $var)" ]; then
(cd "$var"; exec ../$0 $(ls))
fi
fi

Shell Script and spaces in path

I have larger shell script which handles different things.
It will get it's own location by the following...
BASEDIR=`dirname $0`/..
BASEDIR=`(cd "$BASEDIR"; pwd)`
then BASEDIR will be used create other variables like
REPO="$BASEDIR"/repo
But the problem is that this shell script does not work if the path contains spaces where it is currently executed.
So the question is: Does exist a good solution to solve that problem ?
Be sure to double-quote anything that may contain spaces:
BASEDIR="`dirname $0`"
BASEDIR="`(cd \"$BASEDIR\"; pwd)`"
The answer is "Quotes everywhere."
If the path you pass in has a space in it then dirname $0 will fail.
$ cat quote-test.sh
#!/bin/sh
test_dirname_noquote () {
printf 'no quotes: '
dirname $1
}
test_dirname_quote () {
printf 'quotes: '
dirname "$1"
}
test_dirname_noquote '/path/to/file with spaces/in.it'
test_dirname_quote '/path/to/file with spaces/in.it'
$ sh quote-test.sh
no quotes: usage: dirname string
quotes: /path/to/file with spaces
Also, try this fun example
#!/bin/sh
mkdir -p /tmp/foo/bar/baz
cd /tmp/foo
ln -s bar quux
cd quux
cat >>find-me.sh<<"."
#!/bin/sh
self_dir="$(dirname $0)"
base_dir="$( (cd "$self_dir/.." ; pwd -P) )"
repo="$base_dir/repo"
printf 'self: %s\n' "$self_dir"
printf 'base: %s\n' "$base_dir"
printf 'repo: %s\n' "$repo"
.
sh find-me.sh
rm -rf /tmp/foo
Result when you run it:
$ sh example.sh
self: .
base: /tmp/foo
repo: /tmp/foo/repo
Quote your full variable like this:
REPO="$BASEDIR/repo"
There is no reliable and/or portable way to do this correctly.
See How do I determine the location of my script? as to why
The best answer is the following, which is still OS dependent
BASEDIR=$(readlink -f $0)
Then you can do things like REPO="$BASEDIR"/repo , just be sure to quote your variables as you did.
Works perfectly fine for me. How are you using REPO? What specifically "doesn't work" for you?
I tested
#!/bin/sh
BASEDIR=`dirname $0`/..
BASEDIR=`(cd "$BASEDIR"; pwd)`
REPO="$BASEDIR"/repo
echo $REPO
in a ".../a b/c d" directory. It outputs ".../a b/repo", as expected.
Please give the specific error that you are receiving... A "doesn't work" bug report is the least useful bug report, and every programmer absolutely hates it.
Using spaces in directory names in unix is always an issue so if they can be avoided by using underscores, this prevents lots of strange scripting behaviour.
I'm unclear why you are setting BASEDIR to be the parent directory of the directory containing the current script (..) and then resetting it after changing into that directory
The path to the directory should still work if it has ..
e.g. /home/trevor/data/../repo
BASEDIR=`dirname $0`/..
I think if you echo out $REPO it should have the path correctly assigned because you used quotes when assigning it but if you then try to use $REPO somewhere else in the script, you will need to use double quotes around that too.
e.g.
#!/bin/ksh
BASEDIR=`dirname $0`/..
$REPO="$BASEDIR"/repo
if [ ! -d ["$REPO"]
then
echo "$REPO does not exist!"
fi
Use speech marks as below:
BASEDIR=`dirname "${0}"`/..

Is there a Bash shortcut for traversing similar directory structures?

The KornShell (ksh) used to have a very useful option to cd for traversing similar directory structures; e.g., given the following directories:
/home/sweet/dev/projects/trunk/projecta/app/models
/home/andy/dev/projects/trunk/projecta/app/models
Then if you were in the /home/sweet... directory then you could change to the equivalent directory in andy's structure by typing
cd sweet andy
So if ksh saw 2 arguments then it would scan the current directory path for the first value, replace it with the second and cd there. Is anyone aware of similar functionality built into Bash? Or if not, a hack to make Bash work in the same way?
Other solutions offered so far suffer from one or more of the following problems:
Archaic forms of tests - as pointed out by Michał Górny
Incomplete protection from directory names containing white space
Failure to handle directory structures which have the same name used more than once or with substrings that match: /canis/lupus/lupus/ or /nicknames/Robert/Rob/
This version handles all the issues listed above.
cd ()
{
local pwd="${PWD}/"; # we need a slash at the end so we can check for it, too
if [[ "$1" == "-e" ]]
then
shift
# start from the end
[[ "$2" ]] && builtin cd "${pwd%/$1/*}/${2:-$1}/${pwd##*/$1/}" || builtin cd "$#"
else
# start from the beginning
[[ "$2" ]] && builtin cd "${pwd/\/$1\///$2/}" || builtin cd "$#"
fi
}
Issuing any of the other versions, which I'll call cdX, from a directory such as this one:
/canis/lupus/lupus/specimen $ cdX lupus familiaris
bash: cd: /canis/familiaris/lupus/specimen: No such file or directory
fails if the second instance of "lupus" is the one intended. In order to accommodate this, you can use the "-e" option to start from the end of the directory structure.
/canis/lupus/lupus/specimen $ cd -e lupus familiaris
/canis/lupus/familiaris/specimen $
Or issuing one of them from this one:
/nicknames/Robert/Rob $ cdX Rob Bob
bash: cd: /nicknames/Bobert/Rob: No such file or directory
would substitute part of a string unintentionally. My function handles this by including the slashes in the match.
/nicknames/Robert/Rob $ cd Rob Bob
/nicknames/Robert/Bob $
You can also designate a directory unambiguously like this:
/fish/fish/fins $ cd fish/fins robot/fins
/fish/robot/fins $
By the way, I used the control operators && and || in my function instead of if...then...else...fi just for the sake of variety.
cd "${PWD/sweet/andy}"
No, but...
Michał Górny's substitution expression works nicely. To redefine the built-in cd command, do this:
cd () {
if [ "x$2" != x ]; then
builtin cd ${PWD/$1/$2}
else
builtin cd "$#"
fi
}

Resources