Recursion in BASH - linux

My task is create script which lists all subdirs in user's directory and write them in a file.
I created a recursive function which should run through all directories from main and write the names of their subdirs in the file. But the script runs for first folders in my home dirs until reaching the folder without the subfolder. How do I do it correctly?
#!/bin/bash
touch "/home/neo/Desktop/exercise1/backup.txt"
writeFile="/home/neo/Desktop/exercise1/backup.txt"
baseDir="/home/neo"
print(){
echo $1
cd $1
echo "============">>$writeFile
pwd>>$writeFile
echo "============">>$writeFile
ls>>$writeFile
for f in $("ls -R")
do
if [ -d "$f" ]
then
print $1"/"$f
fi
done
}
print $baseDir

to get all folders within a path you can simply do:
find /home/neo -type d > /home/neo/Desktop/exercise1/backup.txt
done

Try this
fun(){
[[ -d $1 ]] && cd $1
echo $PWD
d=$(echo *)
[[ -d $d ]] && cd $d || return
fun
}

Related

is there a touch that can create parent directories like mkdir -p?

I have the following two functions defined in my .zshrc
newdir(){ # make a new dir and cd into it
if [ $# != 1 ]; then
printf "\nUsage: newdir <dir> \n"
else
/bin/mkdir -p $1 && cd $1
fi
}
newfile() { # make a new file, open it for editing, here specified where
if [ -z "$1" ]; then
printf "\nUsage: newfile FILENAME \n"
printf "touches a new file in the current working directory and opens with nano to edit \n\n"
printf "Alternate usage: newfile /path/to/file FILENAME \n"
printf "touches a new file in the specified directory, creating the diretory if needed, and opens to edit with nano \n"
elif [ -n "$2" ]; then
FILENAME="$2"
DIRNAME="$1"
if [ -d "$DIRNAME" ]; then
cd $DIRNAME
else
newdir $DIRNAME
fi
else
FILENAME="$1"
fi
touch ./"$FILENAME"
nano ./"$FILENAME"
}
but I am wondering, is there a version of touch that acts similar to mkdir -p, in that it can create parent dirs as needed in one line/command?
There is no touch that can create parent directory path, so write your own in standard POSIX-shell grammar that also works with zsh:
#!/usr/bin/env sh
touchp() {
for arg
do
# Get base directory
baseDir=${arg%/*}
# If whole path is not equal to the baseDire (sole element)
# AND baseDir is not a directory (or does not exist)
if ! { [ "$arg" = "$baseDir" ] || [ -d "$baseDir" ];}; then
# Creates leading directories
mkdir -p "${arg%/*}"
fi
# Touch file in-place without cd into dir
touch "$arg"
done
}
With zsh you can do:
mkdir -p -- $#:h && : >>| $#
mkdir is given the "head" of each argument to make the directories (man zshexpn says the :h expansion modifier works like the dirname tool). Then, assuming you have not unset the MUTLIOS option, the output of : (a command that produces no output) is appended to the files.

How to GREP a substring of a filename for renaming files?

I have a lot of files that were named with example1_partofEx1A_individualfile_name.fil.
I have since organized the files into a directory structure so the path is ./example1/partofEx1A/example1_partofEx1A_individualfile_name.fil.
I am trying to write a script that can take the unnecessary info out of the filename. I basically want to grep the 'individualfile' part so I can use a loop and mv.
I have no experience with scripting, just some with matlab. Using ubuntu or macOS. Any suggestions?
Maybe something like:
#!/bin/bash
cd /path/to/rootdir || exit
for file in *.fil;do
IFS=_ read dir1 dir2 individual <<<"$file"
if [ "$individual" ] ;then
test -d "$dir1/$dir2" || mkdir -p "$dir1/$dir2"
# mv -v "$file" "$dir1/$dir2/$individual"
mv -vt "$dir1/$dir2" "$file"
fi
done
As suggested by Charles Duffy's comment, I've added -p flag to mkdir in order to prevent race condition.
#!/usr/bin/env bash
for f in ./*/*/*; do
[[ $f =~ ^[.]/([^/]+)/([^/]+)/(.*) ]] || continue
dir1=${BASH_REMATCH[1]}
dir2=${BASH_REMATCH[2]}
filename=${BASH_REMATCH[3]}
[[ $filename = "${dir1}_${dir2}_"* ]] || continue
new_name=${filename#"${dir1}_${dir2}_"}
mv -- "$f" "${dir1}/${dir2}/$new_name"
done
Tested as follows:
testdir="$(mktemp -d)"
cd "$testdir" || exit
mkdir -p ./dirA/dirB
touch dirA/dirB/dirA_dirB_hello_world.txt
touch dirA/dirB/other_content_here.txt
for f in ./*/*/*; do
[[ $f =~ ^[.]/([^/]+)/([^/]+)/(.*) ]] || continue
dir1=${BASH_REMATCH[1]}
dir2=${BASH_REMATCH[2]}
filename=${BASH_REMATCH[3]}
[[ $filename = "${dir1}_${dir2}_"* ]] || continue
new_name=${filename#"${dir1}_${dir2}_"}
mv -- "$f" "${dir1}/${dir2}/$new_name"
done
...whereupon the directory contains:
./dirA
./dirA/dirB
./dirA/dirB/hello_world.txt
./dirA/dirB/other_content_here.txt
Assuming that the three parts (the first two to delete and the third to be kept) are always separated by underscores, this could be a possible solution:
for f in /path/to/rootdir/*; do
if [[ -f $f ]]; then
mv "$f" "$(echo "$f" | sed "s/\/.*_.*_/\//")"
fi
done
where /path/to/rootdir is the root directory of your structure.

Recursively iterate through directory - Linux

I want to recursively loop through all subdirectories within the current directory.
for i in *
this only iterates through all the files in the current directory. How do you make it so that if the file it is looking at is a directory, to enter that directory and recursively look through that directory too and so on and so forth.
EDIT: sorry should have been more specific. i cant use ls -R, as i want to display it in a tree structure with a certain format type. I have heard there are commands which can do this but i have to use a loop. cannot use "find" either...
Here is a function to do what you need. Call it with recp 0 in the current directory.
function recp
{
lvl=$(($1 + 1))
for f in *
do
j=0
while [[ $j -lt $lvl ]]
do
echo -n " "
j=$(($j+1))
done
echo $f
[[ -d $f ]] && ( cd $f && recp $lvl )
done
}
I'm not very good at bash scripting
but the example below goes through all the directories.
You can replace the echo command with whatever you like.
I'm sure the style of this example can be improved.
Also did you consider the command "tree"?
Good luck.
#!/bin/bash
function recursiveList
{
if [ -d $1 ]
then
cd $1
for x in *
do
recursiveList $x
done
cd ..
else
echo $1
fi
}
for i in *
do
recursiveList $i
done

Bash alias utilizing the command inputs

I want to create a bash alias to do the following:
Assume I am at the following path:
/dir1/dir2/dir3/...../dirN
I want to go up to dir3 directly without using cd ... I will just write cdd dir3 and it should go directly to /dir1/dir2/dir3. cdd is my alias name.
I wrote the following alias, but it doesn't work:
alias cdd='export newDir=$1; export myPath=`pwd | sed "s/\/$newDir\/.*/\/$newDir/"`; cd $myPath'
Simply it should get the current full path, then remove anything after the new destination directory, then cd to this new path
The problem with my command is that $1 doesn't get my input to the command cdd
This is a slightly simpler function that I think achieves what you're trying to do:
cdd() { cd ${PWD/$1*}$1; }
Explanation:
${PWD/$1*}$1 takes the current working directory and strips off everything after the string passed to it (the target directory), then adds that string back. This is then used as an argument for cd. I didn't bother adding any error handling as cdwill take care of that itself.
Example:
[atticus:pgl]:~/tmp/a/b/c/d/e/f $ cdd b
[atticus:pgl]:~/tmp/a/b $
It's a little ugly, but it works.
Here's a function - which you could place in your shell profile - which does what you want; note that in addition to directory names it also supports levels (e.g., cdd 2 to go up 2 levels in the hierarchy); just using cdd will move up to the parent directory.
Also note that matching is case-INsensitive.
The code is taken from "How can I replace a command line argument with tab completion?", where you'll also find a way to add complementary tab-completion for ancestral directory names.
cdd ()
{
local dir='../';
[[ "$1" == '-h' || "$1" == '--help' ]] && {
echo -e "usage:
$FUNCNAME [n]
$FUNCNAME dirname
Moves up N levels in the path to the current working directory, 1 by default.
If DIRNAME is given, it must be the full name of an ancestral directory (case does not matter).
If there are multiple matches, the one *lowest* in the hierarchy is changed to." && return 0
};
if [[ -n "$1" ]]; then
if [[ $1 =~ ^[0-9]+$ ]]; then
local strpath=$( printf "%${1}s" );
dir=${strpath// /$dir};
else
if [[ $1 =~ ^/ ]]; then
dir=$1;
else
local wdLower=$(echo -n "$PWD" | tr '[:upper:]' '[:lower:]');
local tokenLower=$(echo -n "$1" | tr '[:upper:]' '[:lower:]');
local newParentDirLower=${wdLower%/$tokenLower/*};
[[ "$newParentDirLower" == "$wdLower" ]] && {
echo "$FUNCNAME: No ancestral directory named '$1' found." 1>&2;
return 1
};
local targetDirPathLength=$(( ${#newParentDirLower} + 1 + ${#tokenLower} ));
dir=${PWD:0:$targetDirPathLength};
fi;
fi;
fi;
pushd "$dir" > /dev/null
}
I agree with mklement0, this should be a function. But a simpler one.
Add this to your .bashrc:
cdd () {
newDir="${PWD%%$1*}$1"
if [ ! -d "$newDir" ]; then
echo "cdd: $1: No such file or directory" >&2
return 1
fi
cd "${newDir}"
}
Note that if $1 (your search string) appears more than once in the path, this function will prefer the first one. Note also that if $1 is a substring of a path, it will not be found. For example:
[ghoti#pc ~]$ mkdir -p /tmp/foo/bar/baz/foo/one
[ghoti#pc ~]$ cd /tmp/foo/bar/baz/foo/one
[ghoti#pc /tmp/foo/bar/baz/foo/one]$ cdd foo
[ghoti#pc /tmp/foo]$ cd -
/tmp/foo/bar/baz/foo/one
[ghoti#pc /tmp/foo/bar/baz/foo/one]$ cdd fo
cdd: fo: No such file or directory
If you'd like to include the functionality of going up 2 levels by running cdd 2, this might work:
cdd () {
newDir="${PWD%%$1*}$1"
if [ "$1" -gt 0 -a "$1" = "${1%%.*}" -a ! -d "$1" ]; then
newDir=""
for _ in $(seq 1 $1); do
newDir="../${newDir}"
done
cd $newDir
return 0
elif [ ! -d "$newDir" ]; then
echo "cdd: $1: No such file or directory" >&2
return 1
fi
cd "${newDir}"
}
The long if statement verifies that you've supplied an integer that is not itself a directory. We build a new $newDir so that you can cd - to get back to your original location if you want.

Shell script for cloning a directory

I need to make a shell script that has 2 arguments, each one a valid directory. The script makes a new directory in the first directory specified by the first argument with the same name as the second one and copies the content(both subdirectories and files) of the second one in the newly created directory. But it only copies the files with .txt extension.
This is what I got so far:
#!/bin/bash
if [ ! $# -eq 2 ]
then echo usage: file.sh directory1 directory2
exit 1
fi
if [ ! -d $1 ]
then echo $1 is not a directory \!
exit 1
fi
if [ ! -d $2 ]
then echo $2 is not a directory \!
exit 1
fi
Leaving debugging to the student:
die () { echo >&2 "$*"; echo "Usage...."; exit 1; }
from="$1";
to="$2";
[ ."$from" = . ] && die "from dir name missing";
[ ."$to" = . ] && die "to dir name missing";
[ -d "$from" ] || die "from dir $from not a directory";
[ -d "$to" ] || die "to dir $to not a directory";
target="$to/$(basname "$from")"; #final target dir name, if I understand you correctly.
find "$from" -name '*.txt' -maxdepth=1 | cpio -pd "$to" ||
# (cd "$from" && find * -name '*.txt' -maxdepth=1 | cpio -o...) | ( cd "$to" && cpio -i...) ||
die "cpio failed"
Beware that cpio has many options and you should review them before using it.
The commented out technique allows you to more freely move to alternate target directories, which I do not think you need here.
Avoid grief: always quote file names.
Simply append this at the and of your script:
cp -dR $2 $1
You may have a better chance using rsync
For example:
rsync -avz /dir1/ /dir2
Depending on your preferences about conservation of file properties, many one-liner alternatives exist around cp, tar or rsync. Filtering can be obtained using the findcommand.

Resources