My goals are to:

  1. Do it automatically (without adding a line to my script every time I add or change the location of a config file)
  2. The files in the git repo folder should not be hidden, but the symlinks in my home directory should
  3. Existing files of the same name should be overwritten, but not directories. For example if I have config/conf.txt in my dotfiles repo, it shouldn't overwrite the entire ~/.config folder, but simply add a link to conf.txt inside ~/.config.

My current solution does these, but it could probably be done better:


echo -n Creating symlinks...

shopt -s extglob nocaseglob

# Create hidden symlinks, hard link them to $HOME, rm old symlinks
for dotfile in $DOTFILES_DIR/!(readme*|${0##*/}); do
    cp -asf "$dotfile" "$hidden"
    cp -alf "$hidden" "$HOME"
    rm -r "$hidden"

echo done

I think I should be able to copy the files directly to the home directory, but I had some trouble getting that to work right if a destination folder already existed.

Edit: I was able to eliminate the second for loop. Much better. There may still be a better way, though.

  • \$\begingroup\$ Do not that cp is for copying, not symlinking (look at ln) \$\endgroup\$ Commented Oct 11, 2017 at 4:02
  • \$\begingroup\$ cp -s creates symlinks. I use cp because the -a option preserves the directory structure (point 3 above) \$\endgroup\$
    – pouellet
    Commented Oct 11, 2017 at 4:32
  • \$\begingroup\$ Interesting... need to go read my man pages again! \$\endgroup\$ Commented Oct 11, 2017 at 4:48

1 Answer 1


Instead of symlinks I would probably arrange for

$ make -C $HOME/dotfiles  refresh

to perform some name mangling and rsync -av copying, or perhaps create simpler symlinks without a "hidden" layer. But you've chosen your favorite approach, so we'll go down that path.

Habitually add these suggested double quotes, please, to control globbing:

for dotfile in "$DOTFILES_DIR"/!(readme*|${0##*/}); do

(Yes, I know, the source makes it a constant with no SPACE if $HOME lacks whitespace. It's just a good habit to have.)

There seems to be more than one file-copying case covered by this code. The cp; cp; rm approach you've chosen is non-intuitive. I would not be willing to maintain this codebase as presented. An alternate design would use find $DIR -type f plus a pipeline which mangles filenames and performs any needed copying.

More # comments on the cp flag behavior might help. Automated unit test(s) that demonstrate the code's behavior would definitely help. For example, it could show what happens when conf.txt was present for a while, and then is removed from the dotfiles repo.

  • \$\begingroup\$ Shouldn't ${0##*/} also be double-quoted? I don't think we want word-splitting there - or does the extglob option somehow prevent that? \$\endgroup\$ Commented Jun 13, 2023 at 4:29
  • 1
    \$\begingroup\$ I think likely you're right. In the end, I trust unit tests; they reveal actual behavior. Initially I tried what I knew was dumb, just blindly quoting the whole thing. And shellcheck told me I was being dumb, observing that the for loop would iterate over just a single item. I feel that defining a $DIR directory variable would help with clarity, and that a find pipeline which selects or discards certain patterns would also be an improvement. Possibly defined as a brief function. Cramming everything into a one-liner doesn't do future maintainers any favors, as evidenced by our doubts. \$\endgroup\$
    – J_H
    Commented Jun 13, 2023 at 4:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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