2

My system:

  • OS: MacOS / Mac OS X (Mojave 10.14.5)
  • OS core: Darwin (18.6.0)
  • Kernel: Darwin Kernel / XNU (18.6.0 / xnu-4903.261.4~2/RELEASE_X86_64)
  • ls: version unknown, but man ls gives a page from the BSD General Commands Manual
  • Shells:
    • Bash: GNU bash, version 5.0.7(1)-release (x86_64-apple-darwin18.5.0)
    • Zsh: zsh 5.7.1 (x86_64-apple-darwin18.2.0)

In MacOS, in a terminal CLI using a shell such as bash or zsh, I'd like to use the (BSD) command ls (or perhaps a similarly common and useful tool) to list the contents of a directory other than the current working directory, where all files except those ending with a tilde (~) are shown.

Excluding the last stipulation, ls naturally accomplishes this task when the non-current directory is used as an argument to ls: ls arg where arg is an absolute or relative path to the non-current directory (such as /absolute/path/to/directory, ~/path/from/home/to/directory, or path/from/current/dir/to/directory).

I know how to list non-backup contents in the current directory, using filename expansion (aka "globbing") and the -d option (to list directories and not their contents), like so: ls -d *[^~] (or ls -d *[!~]). I want the same sort of results, but for a non-current directory.

I can almost achieve what I want by using ls -d arg/*[^~], where arg is the same as described above, but the results show the path to each content element (ie, each file and directory in the directory of interest). I want ls to display each element without the path to it, like is done with ls arg.

In Linux, using the GNU command ls, I can achieve exactly what I want using the -B option to not list backup files: ls -B arg. Although this is what I want, I'd like to achieve this using tools native to MacOS, preferably the BSD ls.

Note: I do not want to use grep (eg, ls arg | grep '.*[^~]$'), because grep changes the formatting and coloring of the output.

Question recap: On a Mac, how can I list the contents of a non-current directory but not the backup files, preferably using ls?

  • Isn't ls arg | grep --color=always '.*[^~]$' good enough? – ChatterOne Jun 4 at 7:48
  • @ChatterOne: Using grep with --color-always is not appropriate for my purposes because it changes the formatting and coloring of the output. I want it to behave just like ls. – zeroparallax Jun 4 at 9:16
4

You could execute ls in a subshell:

(cd arg; ls -d *[^~])
2

Using bash and its GLOBIGNORE shell variable, together with macOS's basename:

$ mkdir dir
$ touch dir/file{1,2}{,~}
$ ls -R
dir

./dir:
file1   file1~  file2   file2~
$ GLOBIGNORE=*~
$ ls -d dir/*
dir/file1       dir/file2
$ basename -a dir/*
file1
file2

Setting GLOBIGNORE to a list of :-delimited patterns will make filename completion ignore those patterns.

The basename utility in macOS accepts more than one pathname if you use its -a option, and will return a list consisting of only the filename portions of those pathnames.

Instead of using GLOBIGNORE (which provides a more generic way to ignore certain filename pattern expansions) you could obviously use your pattern *[!~] (note that ! negates a character class in the shell, while ^ negates a character class in regular expressions):

unset GLOBIGNORE
basename -a dir/*[!~]

... or, you could just install GNU coreutils from Homebrew and use gls -B as you may be used to on Linux systems.

  • Unfortunately, basename changes the format and coloring of the output, so it does not meet my requirements. – zeroparallax Jun 4 at 7:18
  • I'm having trouble with your solution. After executing GLOBIGNORE=*~, I can use set | grep GLOBIGNORE to see that I have affected the variable: GLOBIGNORE='*~'. And executing echo "$GLOBIGNORE" shows *~. But when I use your suggested commands above, they do not leave out the files ending with a tilde (even when using /bin/ls rather than ls). – zeroparallax Jun 4 at 18:12
  • Also, is it a bad idea or "bad habit" to use ^ instead of ! in the globbing pattern? Maybe I should change the ^ characters to ! characters in my question (and answer). – zeroparallax Jun 4 at 18:25
  • @zeroparallax I just tested my solution with the default tools on macOS Mojave 10.14.5, and it behaves as I have described. The bash shell understands [^...] but the standard syntax is [!...] for filename globbing patterns. – Kusalananda Jun 4 at 18:27
1

The concise answer is to use a subshell to execute the command in the non-current directory:

(cd arg; ls -d *[^~])

But to create a general function that works for the current directory as well as any non-current directory, we can add the following to .bashrc (or to bashrc_aliases_lsBSD, sourced by .bashrc):

alias ls='/bin/ls'
alias   l=''
unalias l
function l () { ( if [[ -n "$@" ]]; then cd "$@"; fi ; /bin/ls -d *[^~] ) }

If we defy the preference mentioned above for using BSD, we can use GNU ls without any problems by installing the GNU core utilities (eg, via brew install coreutils) and creating the desired aliases (instead of using the code above) in .bashrc (or in bashrc_aliases_lsGNU, sourced by .bashrc):

alias ls='/usr/local/bin/gls'
alias l='/usr/local/bin/gls -B'

My own current solution is to use both of these constructions and switch between them when desired using a variable I call LS_TYPE and a function I call ls-switch (defined in bashrc_aliases, sourced by .bashrc), which sources one of two config files containing these two constructions:

function ls-switch () {
    if [[ $LS_TYPE = "GNU" ]] ; then
        LS_TYPE="BSD"
        source ~/.config/bash/bashrc_aliases_lsBSD
    elif [[ $LS_TYPE = "BSD" ]] ; then
        LS_TYPE="GNU"
        source ~/.config/bash/bashrc_aliases_lsGNU
    fi
}

With this arrangement, the default behaviors of ls and l are set in .bashrc with the first definition of LS_TYPE and choosing which config file to source, for example:

LS_TYPE="BSD"
source ~/.config/bash/bashrc_aliases_lsBSD
  • 1
    It would be more convenient to detect which ls is available automatically. Something like: LS_TYPE=unknown; case $(uname -s) in Darwin*|*BSD*) LS_TYPE=BSD; esac; if type gls >/dev/null 2>/dev/null; then alias ls=gls; LS_TYPE=GNU; else case "$(ls --version -1 -d / 2>&1)" in *GNU*) LS_TYPE=GNU;; esac (untested code, just to give an idea of how to do it) – Gilles Jun 4 at 7:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.