Bash For-Loop on Directories - bash

Quick Background:
$ ls src
file1 file2 dir1 dir2 dir3
Script:
#!/bin/bash
for i in src/* ; do
if [ -d "$i" ]; then
echo "$i"
fi
done
Output:
src/dir1
src/dir2
src/dir3
However, I want it to read:
dir1
dir2
dir3
Now I realize I could sed/awk the output to remove "src/" however I am curious to know if there is a better way of going about this. Perhaps using a find + while-loop instead.

Do this instead for the echo line:
echo $(basename "$i")

No need for forking an external process:
echo "${i##*/}"
It uses the “remove the longest matching prefix” parameter expansion.
The */ is the pattern, so it will delete everything from the beginning of the string up to and including the last slash. If there is no slash in the value of $i, then it is the same as "$i".
This particular parameter expansion is specified in POSIX and is part of the legacy of the original Bourne shell. It is supported in all Bourne-like shells (sh, ash, dash, ksh, bash, zsh, etc.). Many of the feature-rich shells (e.g. ksh, bash, and zsh) have other expansions that can handle even more without involving external processes.

If you do a cd at the start of the script, it should be reverted when the script exits.
#!/bin/bash
cd src
for i in * ; do
if [ -d "$i" ]; then
echo "$i"
fi
done

Use basename as:
if [ -d "$i" ]; then
basename "$i"
fi

Related

For files in directory Bash [duplicate]

I'm trying to loop through files in a directory, where the directory is passed through as an argument. I currently have the following script saved in test.sh:
#!/bin/bash
for filename in "$1"/*; do
echo "File:"
echo $filename
done
And I am running the above using:
sh test.sh path/to/loop/over
However, the above doesn't output the files at the directory path/to/loop/over, it instead outputs:
File:
path/to/loop/over/*
I'm guessing it's interpreting path/to/loop/over/* as a string and not a directory. My expected output is the following:
File:
foo.txt
File:
bar.txt
Where foo.txt and bar.txt are files in the path/to/loop/over/ directory. I found this answer which suggested to add a /* after the $1, however, this doesn't seem to help (neither do these suggestions)
Iterate over content of directory
Compatible answer (not only bash)
As this question is tagged shell, there is a POSIX compatible way:
#!/bin/sh
for file in "$1"/* ;do
[ -f "$file" ] && echo "Process '$file'."
done
Will be enough (work with filenames containing spaces):
$ myscript.sh /path/to/dir
Process '/path/to/dir/foo'.
Process '/path/to/dir/bar'.
Process '/path/to/dir/foo bar'.
This work well by using any posix shell. Tested with bash, ksh, dash, zsh and busybox sh.
#!/bin/sh
cd "$1" || exit 1
for file in * ;do
[ -f "$file" ] && echo "Process '$file'."
done
This version won't print path:
$ myscript.sh /path/to/dir
Process 'foo'.
Process 'bar'.
Process 'foo bar'.
Some bash ways
Introduction
I don't like to use shopt when not needed... (This change standard
bash behaviours and make script less readables).
There is an elegant way for doing this by using standard bash, without requirement of shopt.
Of course, previous answer work fine under bash, but. There are some
interresting way for making your script more powerfull, flexible, pretty, detailed...
Sample
#!/bin/bash
die() { echo >&2 "$0 ERROR: $#";exit 1;} # Emergency exit function
[ "$1" ] || die "Argument missing." # Exit unless argument submitted
[ -d "$1" ] || die "Arg '$1' is not a directory." # Exit if argument is not dir
cd "$1" || die "Can't access '$1'." # Exit unless access dir.
files=(*) # All files names in array $files
[ -f "$files" ] || die "No files found." # Exit if no files found
for file in "${files[#]}";do # foreach file:
echo Process "$file" # Process file
done
Explanation: considering globbing vs real files
When doing:
files=(/path/to/dir/*)
variable $files becomes an array containing all files contained under /path/to/dir/:
declare -p files
declare -a files=([0]="/path/to/dir/bar" [1]="/path/to/dir/baz" [2]="/path/to/dir/foo")
But if nothing match glob pattern, star won't be replaced and array become:
declare -p files
declare -a files=([0]="/path/to/dir/*")
From there. looking for $files is like looking for ${files[0]} ie: first field in array. So
[ -f "$files" ] || die "No files found."
will execute die function unless first field of array files is a file ([ -e "$files" ] to check for existing entry, [ -d "$files" ] to check for existing directory, ans so on... see man bash or help test).
But you could do replace this filesystem test by some string based test, like:
[ "$files" = "/path/to/dir/*" ] && die "No files found."
or, using array length:
((${#files[#]}==1)) && [ "${files##*/}" = "*" ] && die "No files found."
Dropping paths by using Parameter expansion:
For suppressing path from filenames, instead of cd $path you could do:
targetPath=/path/to/dir
files=($targetPath/*)
[ -f "$files" ] || die "No files found."
Then:
declare -p files
declare -a files=([0]="/path/to/dir/bar" [1]="/path/to/dir/baz" [2]="/path/to/dir/foo")
You could
printf 'File: %s\n' ${files[#]#$targetPath/}
File: bar
File: baz
File: foo
This would happen if the directory is empty, or misspelled. The shell (in its default configuration) simply doesn't expand a wildcard if it has no matches. (You can control this in Bash with shopt -s nullglob; with this option, wildcards which don't match anything are simply removed.)
You can verify this easily for yourself. In a directory with four files,
sh$ echo *
a file or two
sh$ echo [ot]*
or two
sh$ echo n*
n*
And in Bash,
bash$ echo n*
n*
bash$ shopt -s nullglob
bash$ echo n*
I'm guessing you are confused about how the current working directory affects the resolution of directory names; maybe read Difference between ./ and ~/

Shell scripting - if statement difference

This a question of an exercise:
What is the difference between the two "if" instructions?
#!/bin/bash
rm tmp
echo -n > tmp
for f in $*
do
if test ! -f $f
then
echo $f does not exist as a file
continue
fi
rm $f
if [ ! -f $f ]
then
echo $f has been deleted successfully
fi
ls $f >> tmp
done
x='cat tmp | grep -c ^.*$'
echo result: $x
The square brackets are a synonym for the test command, instead of if test ! -f $f we can use if [ ! -f $f ]. Note: test is a command which takes expression and test or evaluates.
No difference. test and [ are builtins in most (all?; definitely in dash, bash, yash, ksh, zsh, fish) shells now:
$ type [
[ is a shell builtin
$ type test
test is a shell builtin
There's also executable versions of them:
$ which [
/usr/bin/[
$ which test
/usr/bin/test
Unlike cd, test (or [) doesn't need to be a builtin (at least not for the common options -- some shells' extensions require it to be a builtin), but the fork+exec overhead of an external executable is too much for the little things that test tests.

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 to browse one or more directories passed as parameters

I made this script that should receive one or more parameter, and those parameter are all directories, and it has to browse those directories (one by one) and do some operations.
The operations work fine if the parameter is 1 (only one directory),
How should I modify my script to make it works if more than 1 parameter is passed
Example if I want it to do the same operations in 2 or 3 directories at the same time?
Thanks
#!/bin/sh
cd $1
for file in ./* # */
do
if [[ -d $file ]]
then
ext=dir
else
ext="${file##*.}"
fi
mv "${file}" "${file}.$ext"
done
First, if you are using bash use bash shebang (#! /bin/bash).
Then use
#! /bin/bash
for d in "$#"
do
echo "Do something with $d"
done
to iterate over the command line arguments (dirs in your case)
#!/bin/sh
for dir in "$#"; do
for file in "$dir"/*; do
echo "Doing something with '$file'"
done
done

Using $# properly

I am trying to write a tiny script that accepts any number of command line arguments that prints out the rwx permissions for a file (not directory)
What I have is
file=$#
if [ -f $file ] ; then
ls -l $file
fi
This accepts only one command line argument however. Thanks for any help.
Here is a demonstration of the some of the differences between $* and $#, with and without quotes:
#/bin/bash
for i in $*; do
echo "\$*: ..${i}.."
done; echo
for i in "$*"; do
echo "\"\$*\": ..${i}.."
done; echo
for i in $#; do
echo "\$#: ..${i}.."
done; echo
for i in "$#"; do
echo "\"\$#\": ..${i}.."
done; echo
Running it:
user#host$ ./paramtest abc "space here"
$*: ..abc..
$*: ..space..
$*: ..here..
"$*": ..abc space here..
$#: ..abc..
$#: ..space..
$#: ..here..
"$#": ..abc..
"$#": ..space here..
How about this one:
for file
do
test -f "$file" && ls -l "$file"
done
The for loop by default will work on $#, so you don't have to mention it. Note that you will need to quote "$file" in case if the file name has embedded space. For example, if you save your script to 'myll.sh':
$ myll.sh "My Report.txt" file1 file2
Then "My Report.txt" will be passed in as a whole token instead of 2 separate tokens: "My" and "Report.txt"
The variable you want is indeed $# - this contains all command-line arguments as separate words, each passed on intact (no expansion). ($* treats all of them as a single word - good luck sorting it out if you have spaces in filenames).
You can loop, if you like. This is easily expanded to more complex actions than ls.
for file in "$#"; do
if [ -f "$file" ]; then
ls -l "$file"
fi
done
Note: you should quote $# to protect any special characters inside! You should also quote $file for the same reason - especially inside the test. If there is an empty string in $#, file will also be empty, and without quotes, -f will attempt to act on the ']'. Errors ensue.
Also, if all you need to do is ls (skipping your if) you can just do this:
ls -l "$#"
You could usefully loop over any files specified like this:
for file in "$#"; do
ls -l "$file"
done
If you want to double-check that the name specified is not a directory, you could do this:
for file in "$#"; do
if [ ! -d "$file" ]; then
ls -l "$file"
fi
done
the bash variable for all arguments passed to a script is "$*". Try:
for file in $*; do
if [ -f $file ] ; then
ls -l $file
fi
done
(not tested)

Resources