277

I'm trying to get the contents of a directory using shell script.

My script is:

for entry in `ls $search_dir`; do
    echo $entry
done

where $search_dir is a relative path. However, $search_dir contains many files with whitespaces in their names. In that case, this script does not run as expected.

I know I could use for entry in *, but that would only work for my current directory.

I know I can change to that directory, use for entry in * then change back, but my particular situation prevents me from doing that.

I have two relative paths $search_dir and $work_dir, and I have to work on both simultaneously, reading them creating/deleting files in them etc.

So what do I do now?

PS: I use bash.

David Gelhar
  • 27,873
  • 3
  • 67
  • 84
jrharshath
  • 25,975
  • 33
  • 97
  • 127

13 Answers13

462
search_dir=/the/path/to/base/dir
for entry in "$search_dir"/*
do
  echo "$entry"
done
Cedric Simon
  • 4,571
  • 4
  • 40
  • 52
Ignacio Vazquez-Abrams
  • 776,304
  • 153
  • 1,341
  • 1,358
  • 15
    Can you explain why `for entry in "$search_dir/*"` don't work? Why we need to place `/*` outside of quotes? – mrgloom Mar 03 '16 at 15:12
  • 20
    @mrgloom: Because you need to let the shell glob the wildcard. – Ignacio Vazquez-Abrams Mar 03 '16 at 20:19
  • wouldn't `find` be faster? – Alexej Magura Mar 14 '17 at 21:30
  • @AlexejMagura: `find` is a separate process. Globbing happens in the shell. – Ignacio Vazquez-Abrams Mar 14 '17 at 22:03
  • 7
    The solution gives the full path. What if I just wanted to list what was in the current directory? – ThatsRightJack Jul 23 '17 at 05:26
  • Just omit the path. – Ignacio Vazquez-Abrams Jul 23 '17 at 05:26
  • 3
    @mrgloom if you want to do so you can achieve that with `for entry in "${search_dir}/*"` – funilrys Aug 12 '17 at 14:53
  • it doesn't work if a file is space seprated(consider each as an entry) – CallMeLoki Jan 16 '18 at 18:14
  • on MacOs, you don't need the parentheses - just write: for entry in ~/gcloud_installs/* – Agile Bean Jun 16 '18 at 08:51
  • This does not work recursively. For a recursive solution `find` is better. See this answer https://stackoverflow.com/a/55817578/1343917 – Dmitry Gonchar May 08 '19 at 05:29
  • 4
    This doesn't work (in bash with the default settings) if the folder is empty or if some files start with a period. – Martin Jambon Jul 21 '20 at 20:40
  • Is there a way to implement the `| grep *.txt` from the answer below to this snippet? – Georodin Jul 28 '20 at 11:05
  • @Georodin Just use `"$search_dir"/*.txt` if that's what you want. – tripleee Dec 16 '20 at 07:40
  • 2
    @Anish B: Stop vandalizing the original post. The answer was right as it is written – Inian Dec 16 '20 at 11:44
  • 1
    @AnishB.: No the answer _as_ it was written should work fine for a directory name assigned to `search_dir` variable – Inian Dec 16 '20 at 11:46
  • 1
    @AnishB.: If you have a problem using the attempt, then something is not right at your end. Suggest asking a new question to resolve it, instead of modifying a highly rated answer incorrectly (Also check if your `*` is outside of double quotes) – Inian Dec 16 '20 at 11:49
  • 1
    There is nothing wrong with the quoting of the `search_dir` variable. See [When to wrap quotes around a shell variable?](https://stackoverflow.com/q/10067266/5291015), i.e. to protect against directory names containing spaces – Inian Dec 16 '20 at 11:57
  • @Inian I got the issue. Yes, you are right. – Anish B. Dec 16 '20 at 14:54
  • In addition to the comment by @MartinJambon, the "${search_dir}/*" doesn't seem to work at all for RedHat 7.9. If search_dir is empty (for any variation) or if using the latter variation, I confusingly get the contents of the current directory (assuming search_dir is not the current dir). – GG2 Dec 13 '21 at 22:43
  • @GG2 You may want to initially check that the variable is set correctly before proceeding: `if test -z "$search_dir"; then echo "No search directory provided"; exit 1; fi` – Nick Bull Dec 30 '21 at 20:17
  • @NickBull I can't remember exactly how I was trying this originally. If I revisit this now, I don't get exactly the same results. If I let an empty string past, it lists contents of root. For an empty dir or containing only files starting with ".", it just echoes the evaluated value of "${search_dir}/*", e.g. empty_dir/*. It only lists the current directory if search_dir="." We'll chalk it up to something I did wrong. – GG2 Jan 14 '22 at 01:15
  • [Here are some other approaches I just added](https://stackoverflow.com/a/71345102/4561887) with `find` and `ls` where I capture the output into some bash arrays with `mapfile` and then customize and print the contents of the arrays. – Gabriel Staples Mar 04 '22 at 00:04
  • This doesn't seem to work if the directory contains several files and their name contain spaces and I save the output of the command in a variable. – msoutopico Jul 04 '22 at 09:31
73

This is a way to do it where the syntax is simpler for me to understand:

yourfilenames=`ls ./*.txt`
for eachfile in $yourfilenames
do
   echo $eachfile
done

./ is the current working directory but could be replaced with any path
*.txt returns anything.txt
You can check what will be listed easily by typing the ls command straight into the terminal.

Basically, you create a variable yourfilenames containing everything the list command returns as a separate element, and then you loop through it. The loop creates a temporary variable eachfile that contains a single element of the variable it's looping through, in this case a filename. This isn't necessarily better than the other answers, but I find it intuitive because I'm already familiar with the ls command and the for loop syntax.

rrr
  • 1,914
  • 2
  • 21
  • 24
  • 1
    This works OK for a quick, informal script or one-liner, but it will break if a filename contains newlines, unlike the glob-based solutions. – Soren Bjornstad Sep 03 '18 at 17:24
  • @SorenBjornstad thanks for the advice! I didn't know newlines were permitted in filenames- what kind of files might have them? Like, is this something that occurs commonly? – rrr Sep 04 '18 at 02:28
  • 1
    Newlines in filenames are evil for this reason and as far as I know there's no legitimate reason to use them. I've never seen one in the wild myself. That said, it's totally possible to maliciously construct filenames with newlines in such a way as to exploit this. (For instance, imagine a directory containing files `A`, `B`, and `C`. You create files called `B\nC` and `D`, then choose to delete them. Software that doesn't handle this right could end up deleting preexisting files B and C instead even if you didn't have permission to do that.) – Soren Bjornstad Sep 04 '18 at 22:01
  • 2
    https://mywiki.wooledge.org/ParsingLs explains a large number of pitfalls with this approach. You should basically never use `ls` in scripts. It's silly anyway; the shell has already expanded the wildcard by the time `ls` runs. – tripleee Dec 16 '20 at 07:44
37

The other answers on here are great and answer your question, but this is the top google result for "bash get list of files in directory", (which I was looking for to save a list of files) so I thought I would post an answer to that problem:

ls $search_path > filename.txt

If you want only a certain type (e.g. any .txt files):

ls $search_path | grep *.txt > filename.txt

Note that $search_path is optional; ls > filename.txt will do the current directory.

tegan
  • 2,125
  • 2
  • 14
  • 17
  • 3
    No need to use grep to get only .txt files: `ls $search_path/*.txt > filename.txt'. But more importantly, one should not use the output of the ls command to parse file names. – Victor Zamanian Jun 07 '17 at 14:13
  • 1
    @VictorZamanian, can you elaborate why we should not use the output of `ls` to parse filenames? Haven't heard of this before. – samurai_jane Feb 09 '19 at 16:56
  • 3
    @samurai_jane There's a lot of links to provide regarding this topic, but here's one first search result: https://mywiki.wooledge.org/ParsingLs. I even saw a question here on SO claiming the reasons for not parsing the output of ls were BS and was very elaborative about it. But the replies/answers still claimed it was a bad idea. Have a look: https://unix.stackexchange.com/questions/128985/why-not-parse-ls-and-what-do-to-instead – Victor Zamanian Feb 09 '19 at 18:11
23
for entry in "$search_dir"/* "$work_dir"/*
do
  if [ -f "$entry" ];then
    echo "$entry"
  fi
done
l0b0
  • 55,365
  • 30
  • 138
  • 223
ghostdog74
  • 327,991
  • 56
  • 259
  • 343
19
$ pwd; ls -l
/home/victoria/test
total 12
-rw-r--r-- 1 victoria victoria    0 Apr 23 11:31  a
-rw-r--r-- 1 victoria victoria    0 Apr 23 11:31  b
-rw-r--r-- 1 victoria victoria    0 Apr 23 11:31  c
-rw-r--r-- 1 victoria victoria    0 Apr 23 11:32 'c d'
-rw-r--r-- 1 victoria victoria    0 Apr 23 11:31  d
drwxr-xr-x 2 victoria victoria 4096 Apr 23 11:32  dir_a
drwxr-xr-x 2 victoria victoria 4096 Apr 23 11:32  dir_b
-rw-r--r-- 1 victoria victoria    0 Apr 23 11:32 'e; f'

$ find . -type f
./c
./b
./a
./d
./c d
./e; f

$ find . -type f | sed 's/^\.\///g' | sort
a
b
c
c d
d
e; f

$ find . -type f | sed 's/^\.\///g' | sort > tmp

$ cat tmp
a
b
c
c d
d
e; f

Variations

$ pwd
/home/victoria

$ find $(pwd) -maxdepth 1 -type f -not -path '*/\.*' | sort
/home/victoria/new
/home/victoria/new1
/home/victoria/new2
/home/victoria/new3
/home/victoria/new3.md
/home/victoria/new.md
/home/victoria/package.json
/home/victoria/Untitled Document 1
/home/victoria/Untitled Document 2

$ find . -maxdepth 1 -type f -not -path '*/\.*' | sed 's/^\.\///g' | sort
new
new1
new2
new3
new3.md
new.md
package.json
Untitled Document 1
Untitled Document 2

Notes:

  • . : current folder
  • remove -maxdepth 1 to search recursively
  • -type f : find files, not directories (d)
  • -not -path '*/\.*' : do not return .hidden_files
  • sed 's/^\.\///g' : remove the prepended ./ from the result list
Victoria Stuart
  • 4,610
  • 2
  • 44
  • 37
12
find "${search_dir}" "${work_dir}" -mindepth 1 -maxdepth 1 -type f -print0 | xargs -0 -I {} echo "{}"
Noel Yap
  • 18,822
  • 21
  • 92
  • 144
  • 2
    I know this is pretty old but I can't seem to get the last `xargs -0 -i echo "{}"` command, care to explain me a bit? In particular what is the `-i echo "{}"` part do? Also I read from the `man` page that `-i` is deprecated now and we should use `-I` insted. – drkg4b Oct 05 '15 at 17:27
  • 1
    `-i` substitutes `{}` with the arg. – Noel Yap Oct 05 '15 at 17:45
  • Thanks! This is useful, also for the slow minded like me I think that the `{}` is the string that is replaced with the matches by the `find` command. – drkg4b Oct 06 '15 at 11:03
  • It's what's replaced by `xargs`, not `find`. – Noel Yap Oct 06 '15 at 13:02
  • I've updated the answer to use `-I`. Thanks for the heads up about the deprecation of `-i`. – Noel Yap Oct 06 '15 at 16:49
  • 3
    why do you use `xargs`? by default, `find` prints what it finds... you could delete everything from `-print0`. – gniourf_gniourf Oct 06 '15 at 16:56
  • 1
    Doing that wouldn't handle file entries with spaces well. – Noel Yap Sep 06 '18 at 21:43
5

Similar to Accepted answer - but lists only file names instead of full paths:

This seems to have been answered a while ago, but I guess I want to also contribute an answer that just lists the files in the desired directory, as opposed to the full paths.

    #search_dir=/the/path/to/base/dir/
    IFS=$'\n' #for in $() splits based on IFS
    search_dir="$(pwd)"
    for entry in $(ls $search_dir)
    do
        echo $entry
    done

If you also wanted to filter for a specific file you would add a grep -q statement.

    #search_dir=/the/path/to/base/dir/
    IFS=$'\n' #for in $() splits based on IFS
    search_dir="$(pwd)"
    for entry in $(ls $search_dir)
    do
        if grep -q "File should contain this entire string" <<< $entry; then
        echo "$entry"
        fi
    done

References:

More information about IFS can be found here.

More information about finding substrings in shell can be found here.

Kolyan1
  • 151
  • 1
  • 7
4

The accepted answer will not return files prefix with a '.' To do that use

for entry in "$search_dir"/* "$search_dir"/.[!.]* "$search_dir"/..?*
do
  echo "$entry"
done
Claudio
  • 7,474
  • 3
  • 18
  • 48
TrevTheDev
  • 2,616
  • 2
  • 18
  • 36
2

How to get the list of files in a directory in a shell script?

In addition to the most-upvoted answer by @Ignacio Vazquez-Abrams, consider the following solutions which also all work, depending on what you are trying to do. Note that you can replace "path/to/some/dir" with . in order to search in the current directory.

1. List different types of files using find and ls

References:

  1. For find, see this answer. See also my comment here.
  2. For ls, see linuxhandbook.com: How to List Only Directories in Linux

Tip: for any of the find examples below, you can pipe the output to sort -V if you'd like it sorted.

Example:

find . -maxdepth 1 -type f | sort -V

List only regular files (-type f) 1 level deep:

# General form
find "path/to/some/dir" -maxdepth 1 -type f

# In current directory
find . -maxdepth 1 -type f

List only symbolic links (-type l) 1 level deep:

# General form
find "path/to/some/dir" -maxdepth 1 -type l

# In current directory
find . -maxdepth 1 -type l

List only directories (-type d) 1 level deep:

Note that for the find example here, we also add -mindepth 1 in order to exclude the current directory, ., which would be printed as . at the top of the directory list otherwise. See here: How to exclude this / current / dot folder from find "type d"

# General form
find "path/to/some/dir" -mindepth 1 -maxdepth 1 -type d

# In current directory
find . -mindepth 1 -maxdepth 1 -type d

# OR, using `ls`:
ls -d

Combine some of the above: list only regular files and symbolic links (-type f,l) 1 level deep:

Use a comma (,) to separate arguments to -type:

# General form
find "path/to/some/dir" -maxdepth 1 -type f,l

# In current directory
find . -maxdepth 1 -type f,l

2. Capture the output of any command into a bash indexed array, with elements separated by the newline char (\n)

However, $search_dir contains many files with whitespaces in their names. In that case, this script does not run as expected.

This is solved by telling bash to separate elements in the string based on the newline char \n instead of the space char--which is the default IFS (Internal Field Separator--see The Meaning of IFS in Bash Scripting) variable used by bash. To do this, I recommend using the mapfile command.

The bash script static code analyzer tool named shellscript recommends using mapfile or read -r whenever you want to read in a string into a bash array, separating elements based on the newline char (\n). See: https://github.com/koalaman/shellcheck/wiki/SC2206.

Update: to see examples of how to do this with both mapfile and read -r see my answer here: How to read a multi-line string into a regular bash "indexed" array. I now prefer to use read -r instead of mapfile, because mapfile will KEEP any empty lines as elements in the array, if any exist, which I do NOT want, whereas read -r [again, my preference now] will NOT keep empty lines as elements in the array.

(Back to my original answer:)

Here is how to convert a newline-separated string into a regular bash "indexed" array with the mapfile command.

# Capture the output of `ls -1` into a regular bash "indexed" array.
# - includes both files AND directories!
mapfile -t allfilenames_array <<< "$(ls -1)"
# Capture the output of `find` into a regular bash "indexed" array
# - includes directories ONLY!
# Note: for other `-type` options, see `man find`.
mapfile -t dirnames_array \
    <<< "$(find . -mindepth 1 -maxdepth 1 -type d | sort -V)"

Notes:

  1. We use ls -1 (that's a "dash numeral_one") in order to put each filename on its own line, thereby separating them all by the newline \n char.
  2. If you'd like to Google it, <<< is called a "here string" in bash.
  3. See mapfile --help, or help mapfile, for help.

Full code example:

From file array_list_all_files_and_directories.sh in my eRCaGuy_hello_world repo:

echo "Output of 'ls -1'"
echo "-----------------"
ls -1
echo ""

# Capture the output of `ls -1` into a regular bash "indexed" array.
# - includes both files AND directories!
mapfile -t allfilenames_array <<< "$(ls -1)"
# Capture the output of `find` into a regular bash "indexed" array
# - includes directories ONLY!
# Note: for other `-type` options, see `man find` and see my answer here:
# https://stackoverflow.com/a/71345102/4561887
mapfile -t dirnames_array \
    <<< "$(find . -mindepth 1 -maxdepth 1 -type d | sort -V)"

# Get the number of elements in each array
allfilenames_array_len="${#allfilenames_array[@]}"
dirnames_array_len="${#dirnames_array[@]}"

# 1. Now manually print all elements in each array

echo "All filenames (files AND dirs) (count = $allfilenames_array_len):"
for filename in "${allfilenames_array[@]}"; do
    echo "    $filename"
done
echo "Dirnames ONLY (count = $dirnames_array_len):"
for dirname in "${dirnames_array[@]}"; do
    # remove the `./` from the beginning of each dirname
    dirname="$(basename "$dirname")"
    echo "    $dirname"
done
echo ""

# OR, 2. manually print the index number followed by all elements in the array

echo "All filenames (files AND dirs) (count = $allfilenames_array_len):"
for i in "${!allfilenames_array[@]}"; do
    printf "  %3i: %s\n" "$i" "${allfilenames_array["$i"]}"
done
echo "Dirnames ONLY (count = $dirnames_array_len):"
for i in "${!dirnames_array[@]}"; do
    # remove the `./` from the beginning of each dirname
    dirname="$(basename "${dirnames_array["$i"]}")"
    printf "  %3i: %s\n" "$i" "$dirname"
done
echo ""

Here is the example output of the code block just above being run inside the eRCaGuy_hello_world/python dir of my eRCaGuy_hello_world repo:

eRCaGuy_hello_world/python$ ../bash/array_list_all_files_and_directories.sh
Output of 'ls -1'
-----------------
autogenerate_c_or_cpp_code.py
autogenerated
auto_white_balance_img.py
enum_practice.py
raw_bytes_practice.py
slots_practice
socket_talk_to_ethernet_device.py
textwrap_practice_1.py
yaml_import

All filenames (files AND dirs) (count = 9):
    autogenerate_c_or_cpp_code.py
    autogenerated
    auto_white_balance_img.py
    enum_practice.py
    raw_bytes_practice.py
    slots_practice
    socket_talk_to_ethernet_device.py
    textwrap_practice_1.py
    yaml_import
Dirnames ONLY (count = 3):
    autogenerated
    slots_practice
    yaml_import

All filenames (files AND dirs) (count = 9):
    0: autogenerate_c_or_cpp_code.py
    1: autogenerated
    2: auto_white_balance_img.py
    3: enum_practice.py
    4: raw_bytes_practice.py
    5: slots_practice
    6: socket_talk_to_ethernet_device.py
    7: textwrap_practice_1.py
    8: yaml_import
Dirnames ONLY (count = 3):
    0: autogenerated
    1: slots_practice
    2: yaml_import

Gabriel Staples
  • 36,492
  • 15
  • 194
  • 265
0

Here's another way of listing files inside a directory (using a different tool, not as efficient as some of the other answers).

cd "search_dir"
for [ z in `echo *` ]; do
    echo "$z"
done

echo * Outputs all files of the current directory. The for loop iterates over each file name and prints to stdout.

Additionally, If looking for directories inside the directory then place this inside the for loop:

if [ test -d $z ]; then
    echo "$z is a directory"
fi

test -d checks if the file is a directory.

SnoopDogg
  • 441
  • 1
  • 5
  • 8
  • `for [ z` is a syntax error. `if [ test` is silly and wrong. Not quoting `"$z"` is a quoting error. – tripleee Dec 16 '20 at 07:43
  • Please explain. I successfully ran the code back when I wrote the answer. If you’re going to necropost at least be useful. – SnoopDogg Dec 17 '20 at 15:21
  • What's to explain? If you tested this back then, you were definitely not using a standard shell. I already pointed out the errors, and it should not be hard to look up what the correct syntax looks like, from some of the other answers here even. But check out this demo: https://ideone.com/ERcu2c and fork it to your heart's content. – tripleee Dec 17 '20 at 16:43
  • If you think this is irrelevant because it's old, you don't understand how Stack Overflow works. This is regularly - probably daily - linked as the canonical answer for new users who post duplicates of this common question, but also gets probably 100x that traffic from Google searches. – tripleee Dec 17 '20 at 16:51
0

There is simpler answer with using mapfile!

mapfile -t file_list < <(ls "${some_path}")

Then you can use file_list list with all contents in some_path! If you want just file list, use find instead of ls

Ex)


// check count.
echo "${#file_list[@]}"

// check list.
for file_name in "${file_list[@]}"; do
  echo "$file_name"
done

Heedo Lee
  • 59
  • 3
0

Get list of files in a directory in shell and in bash, 4 tips

Some remarks

  • Seaching for files is not searching for entries, under Un*x, Everything is a file, so under a path, you could find sockets, devices, fifos, and lot of other things which are not files.
  • Correct accepted answer from Ignacio Vazquez-Abrams will fail if $search_dir is empty or if $search_dir doesn't exist.
  • Under , you could use arrays,
  • and use 's globstar for recursivity instead of find.

Posix shell first:

If you

search_dir="/the/path to/base/dir"
for entry in "$search_dir"/* ; do

(note the use of quote, for keeping space in path name)

From there, script execution will continue even if

  • there's nothing in directory
  • the directory doesn't exist (wrong path)
  • things presents in directory are not files (named fifo, unix socket, audio device, hard disk, ...).

So next step have to be:

    if [ -f "$entry" ] ;then
        printf 'Now, you could quietly do something with "%s".\n' "$entry"
        # ...
    fi
done

Now bash.

bash array

I often use this:

entries=("$search_dir"/*)

But, same remark: this will produce an array containing ("/the/path to/base/dir/*") in case of no file or wrong path.

You could clean 1st wrong field by using

[[ ${entries[0]} == "$search_dir/*" ]] && unset entries[0]

Or you could do a quick loop over the array, see at bottom of this answer...

Then

for entry in "${entries[@]}";do
    if [[ -f $entry ]] ;then  # ensuring entry is a file.
        'Now, you could quietly do something with "%s".\n' "$entry"
        # ...
    fi
done

With bash array, you could show the content of your array with full path, by:

printf ' - %s\n' "${entries[@]}"

And, for showing only file names:

printf ' - %s\n' "${entries[@]#$search_dir/}"

Recursion using globstar:

From man bash:

  • globstar
    If set, the pattern ** used in a pathname expansion context will match all files and zero or more directories and subdirectories. If the pattern is followed by a /, only directories and subdirectories match.

globstar is off by default:

shopt globstar 
globstar        off
shopt -s globstar 

Then

entries=("$search_dir"/**)
printf ' - %s\n' "${entries[@]#$search_dir/}"

will print every entries under $search_dir/.

Searching for files.

Quick loop over array for dropping entries which are not files.

for i in ${!entries[@]};do
    [[ -f ${entries[i]} ]] || unset entries[i]
done

Then

printf ' - %s\n' "${entries[@]#$search_dir/}"

will print every files under $search_dir/.

Building array while searching for files

If your $search_dir do hold a lot of non-file entries, you'd better build array from tested entries:

for entry in "$search_dir"/**;do
    entries+=("$entry")
done

printf ' - %s\n' "${entries[@]#$search_dir/}"
F. Hauri - Give Up GitHub
  • 64,122
  • 17
  • 116
  • 137
-1
ls $search_path ./* |grep ".txt"|
while IFS= read -r line
do 
   echo "$line"
done
Shivang Kakkar
  • 421
  • 3
  • 15
alubhate
  • 25
  • 3
  • 1
    As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Sep 24 '21 at 07:29
  • 1
    There are **eight existing answers** to this question, including a top-voted, accepted answer with over **three hundred votes**. Are you _certain_ your solution hasn't already been given? If not, why do you believe your approach improves upon the existing proposals, which have been validated by the community? Offering an explanation is _always_ useful on Stack Overflow, but it's _especially_ important where the question has been resolved to the satisfaction of both the OP and the community. Help readers out by explaining what your answer does different and when it might be preferred. – Jeremy Caney Feb 25 '22 at 00:36