Shell – Filetype and dereference when listing directory with LS command

command linelsshellsymlink

I'm trying to combine arguments to the ls command to list a directory content. What I'm basically trying to achieve is listing the directory dereferencing all links, but showing explicitly that the item listed is a link. I've tried combining the --dereference and --classify options, but instead of showing the link symbol (@) I get a * since the link targets an executable file.

Any thoughts on how I can get such result? I'm open to alternatives other than ls command.

EDIT:
I actually use other options from the ls command. My current command along with their options is the one bellow:

ls -ogq -LB --group-directories-first --time-style=long-iso

I intend to parse the output inside an application I'm building and can't use the default link output with the arrow (->). Other items on the directory (e.g. folders and files) must also be listed.

EDIT (2):

Just to clarify, I'm developing a Java apllication that uses a SSH2 API to connect to servers and list directories. The result from the listing is then used to populate a tree using the jsTree jQuery plugin . Currently The command I cited above give's me the following output:

felipe@simba:/mnt/drive$ ls -ogq --group-directories-first --time-style=long-iso 
total 12
drwxr-sr-x 2 4096 2014-06-11 18:04 folder1
drwxr-sr-x 6 4096 2014-06-27 19:35 folder2
dr-Sr-s-wt 2 4096 2014-06-27 13:51 folderWithPermissions
-rw-r--r-- 1    0 2014-06-30 10:42 file.txt
lrwxrwxrwx 1   49 2014-06-30 11:36 linkTeste -> folder2/dir/otherfile.txt

By applying a regular expression I can identify what is a folder, file or link by looking at the permissions. But when I have a link I need to list only the link name, instead of name -> destination. If I use the -L option for the ls command only the link name is outputed and I get the referenced destination's permissions (what is a good thing), since the -L option dereferences the link, but this way I can't know that the link is actually a link.

felipe@simba:/mnt/drive$ ls -ogq -L --group-directories-first --time-style=long-iso 
total 12
drwxr-sr-x 2 4096 2014-06-11 18:04 folder1
drwxr-sr-x 6 4096 2014-06-27 19:35 folder2
dr-Sr-s-wt 2 4096 2014-06-27 13:51 folderWithPermissions
-rw-r--r-- 1    0 2014-06-30 10:42 file.txt
-rwxr-xr-x 1    0 2014-06-27 18:40 linkTeste

I need to list only the link name, know it is a link and know what the destination is (file, folder or link). I can handle different kinds of output since I'm going to apply a regular expression anyway.

Alternatives using find or stat are also welcome.

Best Answer

Edit in response to updated question

Since you only care about links, directories and regular files, and don't need to deal with the other filetypes that ls can identify (FIFOs, sockets etc), you could do something like stat. For the examples below, I have created the following test environment:

$ ls -l
total 4.0K
-rw-r--r-- 1 terdon terdon    0 Jun 30 23:12 a new?line 
-rw-r--r-- 1 terdon terdon    0 Jun 30 23:12 a space
-rw-r--r-- 1 terdon terdon    0 Jun 30 23:12 a?tab
drwxr-xr-x 2 terdon terdon 4.0K Jun 30 23:11 dir1
lrwxrwxrwx 1 terdon terdon    4 Jun 30 23:13 linktodir1 -> dir1
lrwxrwxrwx 1 terdon terdon    7 Jun 30 23:13 sh -> /bin/sh

As you can see, these include links, links to executables, a file name with a space, one with a tab (\t) and one with a newline (\n). Most of these files would break your ls approach, but stat can deal with them correctly:

$ stat --printf "%A\t%N\t%F\n" * 
-rw-r--r--  ‘a new\nline’   regular file
-rw-r--r--  ‘a space’   regular file
-rw-r--r--  ‘a\ttab’    regular file
drwxr-xr-x  ‘dir1’  directory
lrwxrwxrwx  ‘linktodir1’ -> ‘dir1’  symbolic link
lrwxrwxrwx  ‘sh’ -> ‘/bin/sh’   symbolic link

The relevant sections of man stat:

--printf=FORMAT

like --format, but interpret backslash escapes, and do not output a mandatory trailing newline. If you want a newline, include \n in FORMAT

%A     access rights in human readable form

%F     file type

%N     quoted file name with dereference if symbolic link

Note that fields are separated by \t, this means you will be able to deal with whitespace within fields (in file names for example) gracefully.

You mentioned that you can't deal with ->. I'm not entirely sure why, but you could either just remove that with sed

$ stat --printf "%A\t%N\t%F\n" * | sed 's/->//' 
lrwxrwxrwx  ‘linktodir1’  ‘dir1’    symbolic link

or substitute it with another string:

$ stat --printf "%A\t%N\t%F\n" * | sed 's/->/→/' | grep linktodir
lrwxrwxrwx  ‘linktodir1’ → ‘dir1’   symbolic link

or just parse the file type.


Depending on what you want to do, it might be useful to separate each of the three file types you are searching for and deal with each separately. If so, use find1 and its -printf option:

$ find ./ -maxdepth 1 -mindepth 1 -type f -printf '%M\t%P\t%l\n'  ## files
$ find ./ -maxdepth 1 -mindepth 1 -type d -printf '%M\t%P\t%l\n'  ## directories
$ find ./ -maxdepth 1 -mindepth 1 -type l -printf '%M\t%P\t%l\n'  ## links

In this case, the printf directives are

          %M     File's permissions (in symbolic form, as for  ls).   This
                 directive is supported in findutils 4.2.5 and later.
          %P     File's  name  with  the name of the command line argument
                 under which it was found removed.
          %l     Object of symbolic link (empty string if file  is  not  a
                 symbolic link).

You could also combine the above into a single command (using find's -o operator) but which lets you use -printf to print an arbitrary string depending on the file type. For example:

$ find ./ -maxdepth 1 -mindepth 1 \( -type l -printf 'link:\t%M\t%P\t%l\n' \) \
-o \( -type d -printf 'dir:\t%M\t%P\n' \) \
-o \( -type f -printf 'file:\t%M\t%P\n' \) 
file:   -rw-r--r--  a?tab
file:   -rw-r--r--  a space
link:   lrwxrwxrwx  linktodir1  dir1
file:   -rw-r--r--  a new?line
dir:    drwxr-xr-x  dir1
link:   lrwxrwxrwx  sh  /bin/sh

The command above will interpret \t and \n correctly if its output is not shown on a terminal. However, to deal with file names with newlines correctly you will need to be careful when parsing (make sure a "line" begins with [file|dir|link]:) or use \0 as a line terminator in each printf call instead of \n:

$ find ./ -maxdepth 1 -mindepth 1 \( -type l -printf 'link:\t%M\t%P\t%l\0' \) \
-o \( -type d -printf 'dir:\t%M\t%P\0' \) \
-o \( -type f -printf 'file:\t%M\t%P\0' \)

1 -maxdepth and -mindepth are GNU extensions, so this approach will only work for GNU find.


The following were posted as solution to the first, less specific version of the question. I am leaving them here since they may be useful to others.

  1. Shell and readlink

    for f in *; do 
     readlink "$f" >/dev/null && echo "$(readlink -f "$f") (link)" || echo "$f";
    done
    

    Example output:

    /etc (link)
    foo
    sample.R
    sample.R~
    

    The above iterates through all files and directories under the current one and if readlink returns successful (if $f is a link), it will dereference it (readlink -f, note that this will follow all links. If you only want the first level, remove the -f) and print the target along with (link). If it is not, it will just print $f.

  2. If this is just for you and not intended to be parsed, just use ls -l:

    $ ls -l
    total 512116
    -rw-r--r-- 1 terdon terdon    100641 Jun 30 19:10 er
    lrwxrwxrwx 1 terdon terdon         5 Jun 30 19:12 etc -> /etc/
    -rw-r--r-- 1 terdon terdon 524288000 Jun 30 19:10 foo
    -rwxr--r-- 1 terdon terdon       353 Jun 30 15:22 sample.R
    -rwxr--r-- 1 terdon terdon       249 Jun 30 14:51 sample.R~
    

    That will clearly indicate links with link -> target.

Related Question