bash shell code confusion - bash

The following code recursively find the sub directory of arg1 (pwd by default),labeling each directory or file with a number. Then prompt user to enter a number, and cd that directory label with that number(if it is a directory).
But I do not understand where that number come from....
and how I can control the depth of subdirectory it reaches...
usage
source gd.sh
gd
#!/bin/bash
function gd ()
{
local dirname dirs dir
if [ $# -gt 0 ]
then
dirname=$1
else
dirname=$(pwd)
fi
dirs=$(find $dirname -type d)
PS3=`echo -e "\nPlease Select Directory Number: "`
select dir in $dirs
do
if [ $dir ]
then
cd $dir
break
else
echo 'Invalid Selection!'
fi
done
Thanks for help :)

The number comes from the select ... in ... instruction. It adds a number for each element of the list. Look at the man page of bash.
For your second question, use the option -maxdepth of find:
dirs=$(find $dirname -maxdepth 2 -type d)

Related

Bash: Check if a directory contains only files with a specific suffix

I am trying to write a script that will check if a directory contains only
a specific kind of file (and/or folder) and will return 1 for false, 0 for true.
IE: I want to check if /my/dir/ contains only *.gz files and nothing else.
This is what i have so far, but it doesn't seem to be working as intended:
# Basic vars
readonly THIS_JOB=${0##*/}
readonly ARGS_NBR=1
declare dir_in=$1
dir_in=$1"/*.gz"
#echo $dir_in
files=$(shopt -s nullglob dotglob; echo ! $dir_in)
echo $files
if (( ${#files} ))
then
echo "Success: Directory contains files."
exit 0
else
echo "Failure: Directory is empty (or does not exist or is a file)"
exit 1
fi
I want to check if /my/dir/ contains only *.gz files and nothing else.
Use find instead of globulation. It's really easier to use find and to parse find output. Globulation are simple for simple scripts, but once you want to parse "all files in a directory" and do some filtration and such, it's way easier (and safer) to use find:
find "$1" -mindepth 1 -maxdepth 1 \! -name '*.gz' -o \! -type f | wc -l | xargs test 0 -eq
This finds all "things" that are not named *.gz inside the directory or are not files (so mkdir a.gz is accounted for), counts them, and then tests if they're count is equal to 0. If the count is equal to 0, xargs test 0 -eq will return 0, if not, it will return status between 1 - 125. You can handle the nonzero return status with a simple || return 1 if you wish.
You can remove xargs with a simple bash substitution and use the method from this thread for a little speedup and get test return value, which is 0 or 1:
[ 0 -eq "$(find "$1" -mindepth 1 -maxdepth 1 \! -name '*.gz' -o \! -type f -print '.' | wc -c)" ]
Remember that the exit status of a script is the exit status of the last command executed. So you don't need anything else in your script if you wish, only a shebang and this oneliner will suffice.
Using Bash's extglob, !(*.gz) and grep:
$ if grep -qs . path/!(*.gz) ; then echo yes ; else echo nope ; fi
man grep:
-q, --quiet, --silent
Quiet; do not write anything to standard output. Exit
immediately with zero status if any match is found, even if an
error was detected. Also see the -s or --no-messages option.
-s, --no-messages
Suppress error messages about nonexistent or unreadable files.
Since you are using bash, there is another setting you can use: GLOBIGNORE
#!/bin/bash
containsonly(){
dir="$1"
glob="$2"
if [ ! -d "$dir" ]; then
echo 1>&2 "Failure: directory does not exist"
return 2
fi
local res=$(
cd "$dir"
GLOBIGNORE=$glob"
shopt -s nullglob dotglob
echo *
)
if [ ${#res} = 0 ]; then
echo 1>&2 "Success: directory contains no extra files"
return 0
else
echo 1>&2 "Failure: directory contains extra files"
return 1
fi
}
# ...
containsonly myfolder '*.gz'
Some have suggested to count all files which do not match the globbing pattern *.gz. This might be quite inefficient depending on the the number of files. For you job it is sufficient to find just one file, which does not match your globbing pattern. Use the -quite action of find to exit after the first match:
if [ -z "$(find /usr/share/man/man1/* -not -name '*.gz' -print -quit)" ]
then echo only gz
fi

bash script - how to check if any directory that starts with exists

I've been trying to write a script:
EFIDIR=/Volumes/EFI
KEXTDEST=$EFIDIR/EFI/CLOVER/kexts/Other
if [[ -d $EFIDIR/EFI/CLOVER/kexts/10.* ]]; then
echo "Directory found."
if [[ -d $EFIDIR/EFI/CLOVER/kexts/10.*/*.kext ]]; then
echo "Kext(s) found."
cp -R $EFIDIR/EFI/CLOVER/kexts/10.*/*.kext $KEXTDEST
fi
rm -R $EFIDIR/EFI/CLOVER/kexts/10.*
fi
I want to check whether any folder that starts with "10." (can be 10.10, 10.11... etc) exist then if any of those folders contains a folder that ends with (.kext) exists... copy to the destination folder.
How to write it the right way?
Thanks.
Try this:
EFIDIR=/Volumes/EFI
KEXTDEST=$EFIDIR/EFI/CLOVER/kexts/Other
for each in $(find $EFIDIR/EFI/CLOVER/kexts/ -name "10.*" -type d); do
echo "Directory found."
for innerdir in $(find $each -name "*.kext" -type d); do
echo "Kext(s) found."
cp -R $innerdir $KEXTDEST
done
rm -R $each
done
find $EFIDIR/EFI/CLOVER/kexts/ -name "10.*" -type d will look for directories (-type d) starting with name 10.* in $EFIDIR/EFI/CLOVER/kexts and if found will loop through every one in the for loop.
The inner for loop looks for directories starting with name *.kext .

BASH user drive selection

I am creating a simple script for mac os x to provide a user with a list of available drives to backup from based on the contents of /Volumes, but I am running into an issue with handling the output of the 'find' command if the drive name contains a space. The find command outputs each drive on a separate line, but the 'for each' breaks the name into parts. Example:
Script:
#!/bin/bash
find /Volumes -maxdepth 1 -type d
echo ""
i=1
for Output in $(find /Volumes -maxdepth 1 -type d)
do
DriveChoice[$i]=$Output
echo $i"="${DriveChoice[$i]}
i=$(( i+1 ))
done
Output:
/Volumes
/Volumes/backup
/Volumes/EZBACKUP DRIVE
/Volumes/Tech
1=/Volumes
2=/Volumes/backup
3=/Volumes/EZBACKUP
4=DRIVE
5=/Volumes/Tech
logout
[Process completed]
This seems like it should be fairly straight-forward. Is there a better way for me to accomplish this?
Update: Thank you chepner, that works perfectly. It is a simple script to generate a ditto command, but I will post it here anyway in case someone finds any part of it useful:
#!/bin/bash
#Get admin rights
sudo -l -U administrator bash
#Set the path to the backup drive
BackupPath="/Volumes/backup/"
#Generate a list of source drives, limiting out invalid options
i=1
while read -r Output; do
if [ "$Output" != "/Volumes" ] && [ "$Output" != "/Volumes/backup" ] && [ "$Output" != "/Volumes/Tech" ] ; then
DriveChoice[$i]=$Output
echo "$i=${DriveChoice[$i]}"
i=$(( i+1 ))
fi
done < <( find /Volumes -maxdepth 1 -type d)
#Have the user select from valid drives
echo "Source Drive Number?"
read DriveNumber
#Ensure the user input is in range
if [ $DriveNumber -lt $i ] && [ $DriveNumber -gt 0 ]; then
Source=${DriveChoice[$DriveNumber]}"/"
#Get the user's NetID for generating the folder structure
echo "User's NetID?"
read NetID
NetID=$NetID
#Grab today's date for generating folder structure
Today=$(date +"%m_%d_%Y")
#Destination for the Logfile
Destination=$BackupPath$NetID"_"$Today"/"
#Full path for the LogFile
LogFile=$Destination$NetID"_log.txt"
mkdir -p $Destination
touch $LogFile
#Destination for the backup
Destination=$Destination"ditto/"
#Execute the command
echo "Processing..."
sudo ditto "$Source" "$Destination" 2>&1 | tee "$LogFile"
else
#Fail if the drive selection was out of range
echo "Drive selection error!"
fi
You cannot safely iterate over the output of find using a for loop, because of the space issue you are seeing. Use a while loop with the read built-in instead:
#!/bin/bash
find /Volumes -maxdepth 1 -type d
echo ""
i=1
while read -r output; do
DriveChoice[$i]=$output
echo "$i=${DriveChoice[$i]}"
i=$(( i+1 ))
done < <( find /Volumes -maxdepth 1 -type d)

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!

How do I check if a directory exists or not in a Bash shell script?

What command checks if a directory exists or not within a Bash shell script?
To check if a directory exists:
if [ -d "$DIRECTORY" ]; then
echo "$DIRECTORY does exist."
fi
To check if a directory does not exist:
if [ ! -d "$DIRECTORY" ]; then
echo "$DIRECTORY does not exist."
fi
However, as Jon Ericson points out, subsequent commands may not work as intended if you do not take into account that a symbolic link to a directory will also pass this check.
E.g. running this:
ln -s "$ACTUAL_DIR" "$SYMLINK"
if [ -d "$SYMLINK" ]; then
rmdir "$SYMLINK"
fi
Will produce the error message:
rmdir: failed to remove `symlink': Not a directory
So symbolic links may have to be treated differently, if subsequent commands expect directories:
if [ -d "$LINK_OR_DIR" ]; then
if [ -L "$LINK_OR_DIR" ]; then
# It is a symlink!
# Symbolic link specific commands go here.
rm "$LINK_OR_DIR"
else
# It's a directory!
# Directory command goes here.
rmdir "$LINK_OR_DIR"
fi
fi
Take particular note of the double-quotes used to wrap the variables. The reason for this is explained by 8jean in another answer.
If the variables contain spaces or other unusual characters it will probably cause the script to fail.
Always wrap variables in double quotes when referencing them in a Bash script.
if [ -d "$DIRECTORY" ]; then
# Will enter here if $DIRECTORY exists, even if it contains spaces
fi
Kids these days put spaces and lots of other funny characters in their directory names. (Spaces! Back in my day, we didn't have no fancy spaces!)
One day, one of those kids will run your script with $DIRECTORY set to "My M0viez" and your script will blow up. You don't want that. So use double quotes.
Note the -d test can produce some surprising results:
$ ln -s tmp/ t
$ if [ -d t ]; then rmdir t; fi
rmdir: directory "t": Path component not a directory
File under: "When is a directory not a directory?" The answer: "When it's a symlink to a directory." A slightly more thorough test:
if [ -d t ]; then
if [ -L t ]; then
rm t
else
rmdir t
fi
fi
You can find more information in the Bash manual on Bash conditional expressions and the [ builtin command and the [[ compound commmand.
I find the double-bracket version of test makes writing logic tests more natural:
if [[ -d "${DIRECTORY}" && ! -L "${DIRECTORY}" ]] ; then
echo "It's a bona-fide directory"
fi
Shorter form:
# if $DIR is a directory, then print yes
[ -d "$DIR" ] && echo "Yes"
A simple script to test if a directory or file is present or not:
if [ -d /home/ram/dir ] # For file "if [ -f /home/rama/file ]"
then
echo "dir present"
else
echo "dir not present"
fi
A simple script to check whether the directory is present or not:
mkdir tempdir # If you want to check file use touch instead of mkdir
ret=$?
if [ "$ret" == "0" ]
then
echo "dir present"
else
echo "dir not present"
fi
The above scripts will check if the directory is present or not
$? if the last command is a success it returns "0", else a non-zero value.
Suppose tempdir is already present. Then mkdir tempdir will give an error like below:
mkdir: cannot create directory ‘tempdir’: File exists
To check if a directory exists you can use a simple if structure like this:
if [ -d directory/path to a directory ] ; then
# Things to do
else #if needed #also: elif [new condition]
# Things to do
fi
You can also do it in the negative:
if [ ! -d directory/path to a directory ] ; then
# Things to do when not an existing directory
Note: Be careful. Leave empty spaces on either side of both opening and closing braces.
With the same syntax you can use:
-e: any kind of archive
-f: file
-h: symbolic link
-r: readable file
-w: writable file
-x: executable file
-s: file size greater than zero
You can use test -d (see man test).
-d file True if file exists and is a directory.
For example:
test -d "/etc" && echo Exists || echo Does not exist
Note: The test command is same as conditional expression [ (see: man [), so it's portable across shell scripts.
[ - This is a synonym for the test builtin, but the last argument must, be a literal ], to match the opening [.
For possible options or further help, check:
help [
help test
man test or man [
Or for something completely useless:
[ -d . ] || echo "No"
Here's a very pragmatic idiom:
(cd $dir) || return # Is this a directory,
# and do we have access?
I typically wrap it in a function:
can_use_as_dir() {
(cd ${1:?pathname expected}) || return
}
Or:
assert_dir_access() {
(cd ${1:?pathname expected}) || exit
}
The nice thing about this approach is that I do not have to think of a good error message.
cd will give me a standard one line message to standard error already. It will also give more information than I will be able to provide. By performing the cd inside a subshell ( ... ), the command does not affect the current directory of the caller. If the directory exists, this subshell and the function are just a no-op.
Next is the argument that we pass to cd: ${1:?pathname expected}. This is a more elaborate form of parameter substitution which is explained in more detail below.
Tl;dr: If the string passed into this function is empty, we again exit from the subshell ( ... ) and return from the function with the given error message.
Quoting from the ksh93 man page:
${parameter:?word}
If parameter is set and is non-null then substitute its value;
otherwise, print word and exit from the shell (if not interactive).
If word is omitted then a standard message is printed.
and
If the colon : is omitted from the above expressions, then the
shell only checks whether parameter is set or not.
The phrasing here is peculiar to the shell documentation, as word may refer to any reasonable string, including whitespace.
In this particular case, I know that the standard error message 1: parameter not set is not sufficient, so I zoom in on the type of value that we expect here - the pathname of a directory.
A philosophical note:
The shell is not an object oriented language, so the message says pathname, not directory. At this level, I'd rather keep it simple - the arguments to a function are just strings.
if [ -d "$Directory" -a -w "$Directory" ]
then
#Statements
fi
The above code checks if the directory exists and if it is writable.
More features using find
Check existence of the folder within sub-directories:
found=`find -type d -name "myDirectory"`
if [ -n "$found" ]
then
# The variable 'found' contains the full path where "myDirectory" is.
# It may contain several lines if there are several folders named "myDirectory".
fi
Check existence of one or several folders based on a pattern within the current directory:
found=`find -maxdepth 1 -type d -name "my*"`
if [ -n "$found" ]
then
# The variable 'found' contains the full path where folders "my*" have been found.
fi
Both combinations. In the following example, it checks the existence of the folder in the current directory:
found=`find -maxdepth 1 -type d -name "myDirectory"`
if [ -n "$found" ]
then
# The variable 'found' is not empty => "myDirectory"` exists.
fi
DIRECTORY=/tmp
if [ -d "$DIRECTORY" ]; then
echo "Exists"
fi
Try online
Actually, you should use several tools to get a bulletproof approach:
DIR_PATH=`readlink -f "${the_stuff_you_test}"` # Get rid of symlinks and get abs path
if [[ -d "${DIR_PATH}" ]] ; Then # Now you're testing
echo "It's a dir";
fi
There isn't any need to worry about spaces and special characters as long as you use "${}".
Note that [[]] is not as portable as [], but since most people work with modern versions of Bash (since after all, most people don't even work with command line :-p), the benefit is greater than the trouble.
Have you considered just doing whatever you want to do in the if rather than looking before you leap?
I.e., if you want to check for the existence of a directory before you enter it, try just doing this:
if pushd /path/you/want/to/enter; then
# Commands you want to run in this directory
popd
fi
If the path you give to pushd exists, you'll enter it and it'll exit with 0, which means the then portion of the statement will execute. If it doesn't exist, nothing will happen (other than some output saying the directory doesn't exist, which is probably a helpful side-effect anyways for debugging).
It seems better than this, which requires repeating yourself:
if [ -d /path/you/want/to/enter ]; then
pushd /path/you/want/to/enter
# Commands you want to run in this directory
popd
fi
The same thing works with cd, mv, rm, etc... if you try them on files that don't exist, they'll exit with an error and print a message saying it doesn't exist, and your then block will be skipped. If you try them on files that do exist, the command will execute and exit with a status of 0, allowing your then block to execute.
[[ -d "$DIR" && ! -L "$DIR" ]] && echo "It's a directory and not a symbolic link"
N.B: Quoting variables is a good practice.
Explanation:
-d: check if it's a directory
-L: check if it's a symbolic link
To check more than one directory use this code:
if [ -d "$DIRECTORY1" ] && [ -d "$DIRECTORY2" ] then
# Things to do
fi
Check if the directory exists, else make one:
[ -d "$DIRECTORY" ] || mkdir $DIRECTORY
[ -d ~/Desktop/TEMPORAL/ ] && echo "DIRECTORY EXISTS" || echo "DIRECTORY DOES NOT EXIST"
Using the -e check will check for files and this includes directories.
if [ -e ${FILE_PATH_AND_NAME} ]
then
echo "The file or directory exists."
fi
This answer wrapped up as a shell script
Examples
$ is_dir ~
YES
$ is_dir /tmp
YES
$ is_dir ~/bin
YES
$ mkdir '/tmp/test me'
$ is_dir '/tmp/test me'
YES
$ is_dir /asdf/asdf
NO
# Example of calling it in another script
DIR=~/mydata
if [ $(is_dir $DIR) == "NO" ]
then
echo "Folder doesnt exist: $DIR";
exit;
fi
is_dir
function show_help()
{
IT=$(CAT <<EOF
usage: DIR
output: YES or NO, depending on whether or not the directory exists.
)
echo "$IT"
exit
}
if [ "$1" == "help" ]
then
show_help
fi
if [ -z "$1" ]
then
show_help
fi
DIR=$1
if [ -d $DIR ]; then
echo "YES";
exit;
fi
echo "NO";
As per Jonathan's comment:
If you want to create the directory and it does not exist yet, then the simplest technique is to use mkdir -p which creates the directory — and any missing directories up the path — and does not fail if the directory already exists, so you can do it all at once with:
mkdir -p /some/directory/you/want/to/exist || exit 1
if [ -d "$DIRECTORY" ]; then
# Will enter here if $DIRECTORY exists
fi
This is not completely true...
If you want to go to that directory, you also need to have the execute rights on the directory. Maybe you need to have write rights as well.
Therefore:
if [ -d "$DIRECTORY" ] && [ -x "$DIRECTORY" ] ; then
# ... to go to that directory (even if DIRECTORY is a link)
cd $DIRECTORY
pwd
fi
if [ -d "$DIRECTORY" ] && [ -w "$DIRECTORY" ] ; then
# ... to go to that directory and write something there (even if DIRECTORY is a link)
cd $DIRECTORY
touch foobar
fi
In kind of a ternary form,
[ -d "$directory" ] && echo "exist" || echo "not exist"
And with test:
test -d "$directory" && echo "exist" || echo "not exist"
file="foo"
if [[ -e "$file" ]]; then echo "File Exists"; fi;
The ls command in conjunction with -l (long listing) option returns attributes information about files and directories.
In particular the first character of ls -l output it is usually a d or a - (dash). In case of a d the one listed is a directory for sure.
The following command in just one line will tell you if the given ISDIR variable contains a path to a directory or not:
[[ $(ls -ld "$ISDIR" | cut -c1) == 'd' ]] &&
echo "YES, $ISDIR is a directory." ||
echo "Sorry, $ISDIR is not a directory"
Practical usage:
[claudio#nowhere ~]$ ISDIR="$HOME/Music"
[claudio#nowhere ~]$ ls -ld "$ISDIR"
drwxr-xr-x. 2 claudio claudio 4096 Aug 23 00:02 /home/claudio/Music
[claudio#nowhere ~]$ [[ $(ls -ld "$ISDIR" | cut -c1) == 'd' ]] &&
echo "YES, $ISDIR is a directory." ||
echo "Sorry, $ISDIR is not a directory"
YES, /home/claudio/Music is a directory.
[claudio#nowhere ~]$ touch "empty file.txt"
[claudio#nowhere ~]$ ISDIR="$HOME/empty file.txt"
[claudio#nowhere ~]$ [[ $(ls -ld "$ISDIR" | cut -c1) == 'd' ]] &&
echo "YES, $ISDIR is a directory." ||
echo "Sorry, $ISDIR is not a directoy"
Sorry, /home/claudio/empty file.txt is not a directory
There are great solutions out there, but ultimately every script will fail if you're not in the right directory. So code like this:
if [ -d "$LINK_OR_DIR" ]; then
if [ -L "$LINK_OR_DIR" ]; then
# It is a symlink!
# Symbolic link specific commands go here
rm "$LINK_OR_DIR"
else
# It's a directory!
# Directory command goes here
rmdir "$LINK_OR_DIR"
fi
fi
will execute successfully only if at the moment of execution you're in a directory that has a subdirectory that you happen to check for.
I understand the initial question like this: to verify if a directory exists irrespective of the user's position in the file system. So using the command 'find' might do the trick:
dir=" "
echo "Input directory name to search for:"
read dir
find $HOME -name $dir -type d
This solution is good because it allows the use of wildcards, a useful feature when searching for files/directories. The only problem is that, if the searched directory doesn't exist, the 'find' command will print nothing to standard output (not an elegant solution for my taste) and will have nonetheless a zero exit. Maybe someone could improve on this.
The below find can be used,
find . -type d -name dirname -prune -print
One Liner:
[[ -d $Directory ]] && echo true
(1)
[ -d Piyush_Drv1 ] && echo ""Exists"" || echo "Not Exists"
(2)
[ `find . -type d -name Piyush_Drv1 -print | wc -l` -eq 1 ] && echo Exists || echo "Not Exists"
(3)
[[ -d run_dir && ! -L run_dir ]] && echo Exists || echo "Not Exists"
If an issue is found with one of the approaches provided above:
With the ls command; the cases when a directory does not exists - an error message is shown
[[ `ls -ld SAMPLE_DIR| grep ^d | wc -l` -eq 1 ]] && echo exists || not exists
-ksh: not: not found [No such file or directory]

Resources