Maximum nested function level reached - bash

I have directory hierarchy where the "root" directory has a file called "text.txt". I want to find this "root" directory and then run the command 'foo' from within it. Here is what I currently have
# Locates the root directory
locateRoot () {
local root
#Findup looks up through the directory hierarchy. I'm sure this works.
root=$(findup text.txt 2>&/dev/null)
if [[ $? -ne 0 ]]
then
echo "Root not found"
return 1
fi
echo root
}
# Does foo from within the root directory
runFoo () {
local myDir
myDir=$(locateRoot)
pushd $myDir 1>&/dev/null
foo $#
popd 1>&/dev/null
}
However, whenever I run this program I get:
maximum nested function level reached
What's wrong with what I have? I'm positive that foo works as expected.

in you locateRoot function you just echo only root not content of it, which is wrong and your script seems to very lengthy to perform some simple task.i give you sample script which print path to directory which contain text.txt file.
#! /bin/bash
locateRoot ()
{
var=$(find / -iname "text.txt" -printf '%h\n' | sort -u)
printf "%s \n" "$var"
}
you can see absolute path to that directory which contain that file. You can modify above script to perform certain task as you want by just cd to that directory like
cd $var
//execute your command in $var directory

Related

check whether a directory contains all (and only) the listed files

I'm writing unit-tests to test file-IO functions.
There's no formalized test-framework in my target language, so my idea is to run a little test program that somehow manipulates files in a test-directory, and after that checks the results in a little shell script.
To evaluate the output, I want to check a given directory whether all expected files are there and no other files have been created during the test.
My first attempt goes like this:
set -e
test -e "${test_dir}/a.txt"
test -e "${test_dir}/b.txt"
test -d "${test_dir}/dir"
find "${test_dir}" -mindepth 1 \
-not -wholename "${test_dir}/a.txt" \
-not -wholename "${test_dir}/b.txt" \
-not -wholename "${test_dir}/dir" \
| grep . && exit 1 || true
This properly detects whether there are two files a.txt and b.txt, and a subdirectory dir/ in the ${test_dir}.
If there happens to be a file c.txt, the test should and will fail.
However, this doesn't scale well.
There are dozens of unit-tests and each has a different set of files/directories, so I find myself repeating lines very similar to the above again and again.
So I'd rather wrap the above into a function call like so:
if checkdirectory "${test_dir}" a.txt b.txt dir/ dir/subdir/ dir/.hidden.txt; then
echo "ok"
else
echo "ko"
fi
Unfortunately I have no clue how to implement checkdirectory (esp. the find invocation with multiple -not -wholename ... stanzas gives me headache).
To add a bit of fun, the constraints are:
support both (and differentiate between) files and directories
must (EDITed from should) run on Linux, macOS & MSYS2/MinGW, therefore:
POSIX if possible (in reality it will be bash, but probably bash<<4! so no fancy features)
EDIT
some more constraints (these didn't make it into original my late-night question; so just consider them "extra constraints for bonus points")
the test-directory may contain subdirectories and files in subdirectories (up to an arbitrary depth), so any check needs to operate on more than just the top-level directory
ideally, the paths may contain weirdo characters like spaces, linebreaks,... (this is really unit-testing. we do want to test for such cases)
the testdir is more often than not some randomly generated directory using mktemp -d, so it would be nice if we could avoid hardcoding it in the tests
no assumptions about the underlying filesystem can be made.
Assuming we have a directory tree as an example:
$test_dir/a.txt
$test_dir/b.txt
$test_dir/dir/c.txt
$test_dir/dir/"d e".txt
$test_dir/dir/subdir/
then would you please try:
#!/bin/sh
checkdirectory() {
local i
local count
local testdir=$1
shift
for i in "$#"; do
case "$i" in
*/) [ -d "$testdir/$i" ] || return 1 ;; # check if the directory exists
*) [ -f "$testdir/$i" ] || return 1 ;; # check if the file exists
esac
done
# convert each filename to just a newline, then count the lines
count=`find "$testdir" -mindepth 1 -printf "\n" | wc -l`
[ "$count" -eq "$#" ] || return 1
return 0
}
if checkdirectory "$test_dir" a.txt b.txt dir/ dir/c.txt "dir/d e.txt" dir/subdir/; then
echo "ok"
else
echo "ko"
fi
One easy fast way would be to compare the output of find with a reference string:
Lets start with an expected directory and files structure:
d/FolderA/filexx.csv
d/FolderA/filexx.doc
d/FolderA/Sub1
d/FolderA/Sub2
testassert
#!/usr/bin/env bash
assertDirContent() {
read -r -d '' s < <(find "$1" -printf '%y %p\n')
[ "$2" = "$s" ]
}
testref='d d/FolderA/
f d/FolderA/filexx.csv
f d/FolderA/filexx.doc
d d/FolderA/Sub1
d d/FolderA/Sub2'
if assertDirContent 'd/FolderA/' "$testref"; then
echo 'ok'
else
echo 'Directory content assertion failed'
fi
Testing it:
$ ./testassert
ok
$ touch d/FolderA/unwantedfile
$ ./testassert
Directory content assertion failed
$ rm d/FolderA/unwantedfile
$ ./testassert
ok
$ rmdir d/FolderA/Sub1
$ ./testassert
Directory content assertion failed
$ mkdir d/FolderA/Sub1
$ ./testassert
ok
$ rmdir d/FolderA/Sub2
# Replace with a file instead of a directory
touch d/FolderA/Sub2
$ ./testassert
Directory content assertion failed
Now if you add timestamps and other info like permissions, owner, group to the find -printf output, you can also check all these matches the asserted string output.
I don't know what you mean by differentiating between files and directories since your last if statement is somehow binary. Here's what worked for me:
#! /bin/bash
function checkdirectory()
{
test_dir="${1}"
shift
content="$#"
for file in ${content}
do
[[ -z "${test_dir}/${file}" ]] && return 1
done
# -I is meant to be appended to "ls" to ignore the files in order to check if other files exist.
matched=" -I ${content// / -I } -I ${test_dir}"
[[ -e `ls $matched` ]] && return 1
return 0
}
if checkdirectory /some/directory a.txt b.txt dir; then
echo "ok"
else
echo "ko"
fi
Here's a possible solution i dreamed up during the night.
It destroys the test-data, so might not be usable in many cases (though it might just work for paths generated on-the-fly during unit tests):
checkdirectory() {
local i
local testdir=$1
shift
# try to remove all the listed files
for i in "$#"; do
if [ "x${i}" = "x${i%/}" ]; then
rm "${testdir}/${i}" || return 1
fi
done
# the directories should now be empty,
# so try to remove those dirs that are listed
for i in "$#"; do
if [ "x${i}" != "x${i%/}" ]; then
rmdir "${testdir}/${i}" || return 1
fi
done
# finally ensure that no files are left
if find "${testdir}" -mindepth 1 | grep . >/dev/null ; then
return 1
fi
return 0
}
When invoking the checkdirectory function, deeper directories must come first (that is checkdirectory foo/bar/ foo/ rather than checkdirectory foo/ foo/bar/).

how to pass a variable through read in bash

I'm trying to make a script which asks for a directory path and then creates that directory. I'd like to be able to pass variables to the read builtin so pathnames so I don't have to type out full paths:
function make_dir {
echo "What is the path of the directory to be created?"
read directory
mkdir "$directory"
}
so I'd type:
make_dir
What is the path of the directory to be created?
$HOME/temp_dir
mkdir: cannot create directory `$HOME/temp_dir': No such file or directory
So I'd like to have $HOME expanded into /home/user/ and the script to make the directory /home/user/temp_dir, but I can't seem to get the expansion to work.
If I modify the make_dir function to show_dir below
function show_dir {
echo "What is the path of the directory to be created?"
read directory
echo "The directory is $directory"
}
and I type $HOME/temp_dir and get the following:
make_dir
What is the path of the directory to be created?
$HOME/temp_dir
The directory is $HOME/temp_dir
with no expansion. Any ideas on how to get this to work?
It's a little cumbersome, but one option is to use the -e flag to tell read to use Readline to get the input, then use Readline to expand the line after typing it, but before hitting Enter.
$ read -e directory
$HOME/dir
Now type Meta-Control-e, and Readline will expand the input just as if it were being processed prior to execution as a shell command. (Note that the Meta key is probably Alt or Esc, depending on your terminal setup.)
You are actually making things more difficult by attempting to get the directory with read. Unless you have an absolute requirement to use read, you are better off passing the directory to your function as an argument. For example:
function make_dir {
[ -n "$1" ] || {
printf "\n usage: make_dir <path_to_create>\n\n"
return 1
}
mkdir -p "$1" || {
printf "\n error: unable to create '$1', check permissions\n\n"
}
}
example:
$ function make_dir {
> [ -n "$1" ] || {
> printf "\n usage: make_dir <path_to_create>\n\n"
> return 1
> }
> mkdir -p "$1" || {
> printf "\n error: unable to create '$1', check permissions\n\n"
> }
> }
$ make_dir $HOME/temp_dir
$ ls -al temp_dir
total 8
drwxr-xr-x 2 david david 4096 Nov 26 15:34 .
drwxr-xr-x 76 david david 4096 Nov 26 15:34 ..
$ make_dir
usage: make_dir <path_to_create>
When you pass the directory to your function as an argument instead of using read, you can easily adjust your function to take/create multiple directories as well:
function make_dir {
[ -n "$1" ] || {
printf "\n usage: make_dir <path_to_create> [path2, ..]\n\n"
return 1
}
for i in "$#" ; do
mkdir -p "$i" || {
printf "\n error: unable to create '$i', check permissions\n\n"
}
done
}
example:
$ make_dir temp_dir_{1..3}
$ ls -1d temp_*
temp_dir_1
temp_dir_2
temp_dir_3
change the following line:
mkdir "$directory"
with
eval mkdir -p "$directory"

How to tell if a filename is a directory, not a file

I need to check if a parameter passed to a bash script is a folder or a file. It may or may not end with /
xpath=$(dirname "$1")
strips out the dirname if there was no trailing /
Thanks
Given the file a and dir t.
You can use the command file:
$ file t
t: directory
$ file a
a: ASCII text
Or also the -f (file) and -d (dir) flags.
$ [ -f a ] && echo "this is a file"
this is a file
$ [ -f t ] && echo "this is a file"
$
$ [ -d t ] && echo "this is a dir"
this is a dir
$ [ -d a ] && echo "this is a dir"
$
use "test -f" or "test -d" with the path. Though "dirname" always returns name of directory, never a filename, however it may return a directory in which file resides, or a directory in which directory resides, depending if argument is file or directory. "basename" returns filename or directory without preceeding path it resides in.

Check if directory is /Library or /Public

I am trying to write a bash script which takes a users home directory and cycles through the first level of subdirectories and performs some maintenance on those directories only if it is not the /Library or /Public folder. The code I have so far does not work as I get an error message saying that the directory name returned by $dir is a directory. Here is the code:
#!/bin/bash
user="short name"
source_root="/Users/"
source_use="$source_root$user"
cd "$source_use"
dirarr=( */ )
echo ${dirarr[#]}
for dir in "${dirarr[#]}"
do
if ( "$dir" -ne "/Library" -o "$dir" -ne "/Public")
then echo $dir.
# do something
fi
done
Can anyone help me get this working.
Many thanks
Your script has several problems:
You need to use [ ] or [[ ]] in your if statement, not ( ). In your example ( ) creates a subshell and tries to run a command "$dir", which is the reason you're getting the error message you see.
You're comparing against strings that you won't find - try "Library/" and "Public/" instead.
You probably want -a instead of -o.
-ne is used to compare numbers. You want !=.
Here's a corrected version of your script:
#!/bin/bash
user="short name"
source_root="/Users/"
source_use="$source_root$user"
cd "$source_use"
dirarr=( */ )
echo ${dirarr[#]}
for dir in "${dirarr[#]}"
do
if [ "$dir" != "Library/" -a "$dir" != "Public/" ]
then
echo $dir.
# do something
fi
done
Try this:
cd $source_root$user
for dir in `find . -maxdepth 1 -type d`
do
if [ $dir = ./Library ] || [ $dir = ./Public ]
then
continue
fi
(Perform actions)
done
Also, bash is backwards. != is string non-equality, -ne is integer non-equality. So, change to equals signs, too.
Good luck!

Find file by name up the directory tree, using bash

Using bash, how can I find a file with a specific name somewhere up the directory tree from the pwd?
To be more clear. I want to find a file that sits in my working directory's root, but I don't know where the root is, and my pwd might be anywhere below the root.
Find file.txt up to root
x=`pwd`
while [ "$x" != "/" ] ; do
x=`dirname "$x"`
find "$x" -maxdepth 1 -name file.txt
done
local DIR=$(pwd)
while [ ! -z "$DIR" ] && [ ! -f "$DIR/myFile.txt" ]; do
DIR="${DIR%\/*}"
done
echo $DIR/myFile.txt
I have the following function defined in my ~/.bashrc:
dnif () {
# Recursively list a file from PWD up the directory tree to root
[[ -n $1 ]] || { echo "dnif [ls-opts] name"; return 1; }
local THERE=$PWD RC=2
while [[ $THERE != / ]]
do [[ -e $THERE/${2:-$1} ]] && { ls ${2:+$1} $THERE/${2:-$1}; RC=0; }
THERE=$(dirname $THERE)
done
[[ -e $THERE/${2:-$1} ]] && { ls ${2:+$1} /${2:-$1}; RC=0; }
return $RC
}
which will search for a name you provide as a parameter in each directory upwards from the current to the root, and if found, list it with 'ls' and the optional ls -options that you provide. Example output:
me#host:~/dev/example
$ dnif; echo $?
dnif [ls-opts] name
1
me#host:~/dev/example
$ dnif -alp nonesuch; echo $?
2
me#host:~/dev/example
$ dnif -alp .bashrc; echo $?
-rw-r--r-- 1 me mine 3486 Apr 3 2012 /home/me/.bashrc
0
me#host:~/dev/example
$ dnif -d .
/home/me/dev/example/.
/home/me/dev/.
/home/me/.
/home/.
/.
Please note:
"dnif" is "find" backwards.
The function is a finite loop (not recursive), creates no subshells, and uses Bash built-ins as much as possible for speed.
All hits at each ascending directory level are listed.
The ls -opts are optional, but must precede the required search argument.
The search argument may be a file or directory.
If the search argument is a directory, include the ls -opt '-d' to restrict the results to directory names rather than contents.
The function returns exit code
0 if there is at least one hit,
1 if no parameters are provided for help, and
2 if nothing is found.

Resources