1

I need to list the sub directories of a certain path. The separation should be with spaces and not new lines.

I also do not want the absolute path to the sub directories, just their names.

# correct
dir1 dir2 dir3

# incorrect: separation with new lines
dir1
dir2
dir3

# incorrect: absolute paths
/home/x/y/dir1 /home/x/y/dir2 /home/x/y/dir3

I've seen a lot of other posts like this SO post, but they do not accomplish my request.

I've tried ls -d ~/y but it lists absolute paths and separates with new lines. I guess I could use sed to remove the irrelevant part of the path, and then remove all the new lines. But I couldn't get it to work, and it seems like there should be a better solution

1
  • 3
    Also, a bit of an XY problem, since you don't explain why this has to be done via ls (as your question title indicates). You seem to be presupposing that the solution needs to involve ls, but you don't explain why this can't or shouldn't be done via other means like find. Sep 27, 2021 at 19:30

4 Answers 4

5

Assuming that you are using GNU tools, you could use GNU basename to get the names of all subdirectories in a particular directory. You could then use paste to format this as a space-delimited list.

basename -a /some/path/*/ | paste -d ' ' -s -

The above command uses the fact that GNU basename has an -a option to return the filename portion of multiple pathnames given as operands on its command line.

We use a file-globbing pattern ending in / to generate the pathnames for GNU basename. Only directories can match such a pattern.

In the end, the paste creates the space-separated list from the newline-separated list produced by GNU basename.

Note that it would be difficult to parse the generated list of filenames if any of the original names of directories contain space characters.

Note that if the directory contains symbolic links, this method will try to follow those symbolic links.


Restricting us from using any external tools, we could use an array in the bash shell to store and manipulate the directory paths.

shopt -s nullglob

topdir=/some/path

dirpaths=( "$topdir"/*/ )
dirpaths=( "${dirpaths[@]#$topdir/}" )
dirpaths=( "${dirpaths[@]%/}" )

printf '%s\n' "${dirpaths[*]}"

The above shell code expands the same globbing pattern as we used in the first part of this answer but stores the resulting directory paths in the array dirpaths. It then deletes the known prefix $topdir/ from each element of the array and the trailing / before printing the array as a single string of space-delimited names. The delimiter used between the names on the last line will be the first character from $IFS, which by default is a space.


Using find, you could look for subdirectories in the particular top directory you're interested in while making sure not to return the top directory itself. You would also stop find from progressing into the subdirectories.

topdir=/some/path
find "${topdir%/}/." ! -name . -prune -type d -exec basename {} \; | paste -d ' ' -s -

The above command avoids the search starting point using a negated -name test, and it prunes the search tree with -prune so that find does not recurse down into any subdirectories. We call basename for each found directory which outputs the filename of the directories onto separate lines. As the last step, we're piping the result from find through paste to format the output into a space-separated list on a single line.

With GNU find, you could write this as

find /some/path -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | paste -d ' ' -s -

Using find like this will list directories with hidden names, and you will not see any symbolically linked directories.


In the zsh shell, you would be able to use a more advanced shell globbing pattern to pick out the filenames of only directories and print them in one go.

print -r -- /some/path/*(/:t)

This command uses a glob qualifier, /:t, consisting of two parts, affecting the preceding globbing pattern /some/path/*. The / makes the pattern only match directories (not symbolically linked ones; for that use -/), while :t extracts the "tail" of each generated pathname, i.e., the filename component.

The print -r command prints its arguments with spaces as delimiters while avoiding expanding escape sequences like \n or \t in the data. Using -- to delimit the operands from the options (also works with - like in the ksh shell) makes sure directory names resulting from the glob expansion are not taken as options even if they start with -.

You could use this from within the bash shell to generate your list.

zsh -c 'print -r -- /some/path/*(/:t)'
1
  • @StéphaneChazelas Ouch again. Yes, what you are proposing is far better.
    – Kusalananda
    Sep 28, 2021 at 6:23
0

Since you already know the directory from which you are listing the subdirectories, you can use the following ...

find /home/x/y -maxdepth 1 -type d -printf '%P '
3
  • The command ls -d * will list files of all types, including directories. It will not distinguish between directories and non-directories.
    – Kusalananda
    Sep 27, 2021 at 21:28
  • I see. I have replaced the command with the find command.
    – asiby
    Sep 28, 2021 at 4:37
  • Note that -printf is only available in the GNU implementation of find. Sep 28, 2021 at 5:14
-1

Here is my suggestion using a for loop:

$ echo "$(for node in $(ls ~/); do if test -d "${node}"; then printf "${node} "; fi; done)"
Desktop Documents Downloads Music Pictures Public Templates Videos

Alternate using find:

$ echo "$(for node in $(find ~/ -mindepth 1 -maxdepth 1 -type d); do printf "$(basename ${node}) "; done)"
.mozilla Videos Pictures Music Documents Public Templates Downloads .gnupg .config .local .cache Desktop

Edit: In regards to they's comment, you would need to change the IFS variable for directories with spaces:

$ IFS_orig=${IFS}
$ IFS=$'\n'
$ echo "$(for node in $(ls ~/); do if test -d "${node}"; then printf "'${node}' "; fi; done)"
'Desktop' 'Documents' 'Downloads' 'Music' 'My Test' 'Pictures' 'Public' 'Templates' 'Videos'

$ IFS=${IFS_orig}

Edit: The simplest option seems to be to use ls -d *:

$ ls -d *
 Desktop   Documents   Downloads   Music  'My Test'   Pictures   Public   Templates   Videos

Also, forgot that in the for loops, should test using absolute or relative path:

$ echo "$(for node in $(ls "${HOME}"); do if test -d "${HOME}/${node}"; then printf "'${node}' "; fi; done)"
'Desktop' 'Documents' 'Downloads' 'Music' 'My Test' 'Pictures' 'Public' 'Templates' 'Videos'
4
  • 1
    Add a directory called My Documents and see what happens. The issue is that the shell will split the output from the command substitution around ls into words based on spaces, tabs, and newlines. The loop will iterate over each of the words that this generates. The shell would also apply filename globbing on any term that happened to look like a globbing pattern.
    – Kusalananda
    Sep 27, 2021 at 21:44
  • Thanks, I updated my answer in regards to whitespace. Not sure what to do about globbing patterns. Sep 27, 2021 at 21:59
  • I don't want to be a party pooper, but Unix filenames could theoretically contain newlines too. Also, I now see that you test with test -d on $node whether it's a directory in the current directory. Doing so will likely fail if there isn't a set of identically named directories in the current directory. You also have an issue with printf, which disqualifies some filenames containing the character % (introducing a printf formatting sequence). The printf utility's first argument should always be a static formatting string, so use printf '"%s" ' "$node" instead.
    – Kusalananda
    Sep 27, 2021 at 22:08
  • Your approach with find is saner but still suffers from issues with printf. With GNU find, you could use something like find ~/ -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | paste -d ' ' -s - instead. Note that we don't need those command substitutions. With non-GNU find, replace -printf '%f\n' with -exec basename {} \;.
    – Kusalananda
    Sep 27, 2021 at 22:13
-2

The final command I got to is: ls -d ~/y/*/ | awk -F/ '{print$5}' | sed 's/ /\\ /' | tr '\n' ' '

Origin was from a user who has now deleted their post, but they didn't account for not using ls on the current directory, so I adjusted for that. I also added the sed pipe so it adds backslashes for directory names' which have spaces in them.

It is pretty hardcoded with print$5 so it'll only work with directories right in the ~ directory.

2
  • 1
    You should use find ~/y/ -maxdepth 1 -type d for this, not ls. See Why not parse ls (and what to do instead)?. Spaces and newlines are valid characters in filenames, so spaces are not a reliable filename separator. In fact, he only reliable separator character is a NUL (because it isn't valid in a filename), so you should use -print0 with the find command, and pipe it into programs which can work with NUL-separated input. or use mapfile to read it into an array.
    – cas
    Sep 27, 2021 at 5:15
  • Your question is how to ls subdirectories this way, i.e. general use of ls. If this only works in directories in /~, it's not a general answer but for a specific case. Before you accept it, I'd suggest either editing the command so that it works also outside /~, or changing your question and title to also refer to this one specific use case. Sep 27, 2021 at 7:21

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .