0

So I have a few directories and I want to copy only the name of the directories that start with 'k' and end with 'g' to a file. How do I do that?

strugee
  • 14,951

3 Answers3

2

With zsh:

print -rC1 -- k*g(N/) > dirnames.txt

Note however that since newline is as valid a character as any in a file name, that means you won't be able to distinguish two files called k1g and k2g from one file called k1g<newline>k2g.

Instead of printing those dirnames one per line, you could print them NUL-delimited by adding the -N option to print.

Then, you can post-process the resulting file with things like dirs=( ${(0)"$(<dirnames.nul-delimited)"} ) or xargs -r0a dirnames.nul-delimited some-command.

To also include the name (base name / tail) of the k*g files found in subdirectories, you'd do:

print -rNC1 -- **/k*g(ND/:t) > dirnames.nul-delimited

Note that it could have duplicates if there's both a a/king and .git/king files for instance. You could remove duplicates with:

() { print -rNC1 -- ${(u)argv} > dirnames.nul-delimited; } **/k*g(ND/:t)

An approach to have a file list that is both human readable and post-processable would be to use shell quoting around the file names:

print -rC1 -- k*g(N/:q) > dirnames.txt

Here using a zsh-specific variant of quoting. Or using a portable (in sh-like shells) and more reliable quoting syntax:

() { print -rC1 -- ${(qq)argv} > dirnames.txt; } k*g(N/)

Or using GNU ls:

ls -Ud --quoting-style=shell-always -- k*g(N/) > dirnames.txt

That list can then later be retrieved with:

dirs=( ${(Q)${(zZ[n])"$(<dirnames.txt)"}} )

In zsh.

Or in both zsh or bash (or ksh93 if you declared dirs as array first):

eval "dirs=( $(<dirnames.txt) )"

If you don't have or can't use zsh for some reason, but are on a GNU system, and your filenames are made on valid text in the locale, you could do something approaching with:

  • print one per line (assuming file names don't contain newline characters):

    find . -mindepth 1 -maxdepth 1 -name 'k*g' -type d -printf '%f\n' |
      sort > dirnames.txt
    
  • same NUL-delimited:

    find . -mindepth 1 -maxdepth 1 -name 'k*g' -type d -printf '%f\0' |
      sort -z > dirnames.txt
    
  • recursive, nul-delimited:

    find . -name 'k*g' -type d -printf '%f\0' |
      sort -z > dirnames.txt
    
  • same deduplicated:

    find . -name 'k*g' -type d -printf '%f\0' |
      LC_ALL=C sort -zu |
      sort -z > dirnames.txt
    
  • quoted (though sorted in batches)

    find . -mindepth 1 -maxdepth 1 -name 'k*g' -type d -printf '%f\0' |
      xargs -r0 ls -d --quoting-style=shell-always --
    
  • quoted and properly sorted:

    find . -mindepth 1 -maxdepth 1 -name 'k*g' -type d -printf '%f\0' |
      sort -z |
      LC_ALL=C gawk -v 'RS=\0' -v q="'" '
        shquote(s) {
          gsub(q, q "\\" q q, s)
          return q s q
        }
        {print shquote($0)}' |
      xargs -r0 ls -d --quoting-style=shell-always --
    
1

Probably best not to use ls, because of this.

Try:

find * -maxdepth 0 -type d -name 'k*g' > filename.txt

(Without -maxdepth 0 it'll also do subfolders of folders. Use -iname instead of -name if uppercase K and G should also count.)

If there's any possibility that you'll have folders or files that begin with hyphens (but who does that?) you can use this instead:

find . -maxdepth 1 -type d -name 'k*g' | sed 's/^\.\///' > filename.txt

Notice the switch from -maxdepth 0 to -maxdepth 1.

frabjous
  • 8,691
  • 1
    I suspect this isn't actually better than my answer, although I would encourage folks to use this answer instead because it isn't worse and it might be better. why wouldn't it be better? the core issue is that filenames can contain newlines, and both this answer and mine will dump those newlines into filename.txt in a very confusing way that breaks the "one-record-per-line" convention. you could use -print0, but now whatever's consuming filename.txt has to expect NUL-delimited filenames... it's a nasty problem. – strugee Apr 04 '22 at 21:38
  • 1
    That fails if there are filenames that start with - or are find predicates (with potentially severe consequences if one is called -delete for instance). – Stéphane Chazelas Apr 05 '22 at 05:16
  • @StéphaneChazelas Although part of me feels that anyone who names a file -delete deserves whatever happens to them, I have edited the answer with an alternative suggestion for anyone worried about it. – frabjous Apr 05 '22 at 14:25
  • Another reason not to expand * is that it may expand to too many names, which would trigger an error when the shell tries to run the find command. Note that the sed expression could be shortened to s/..//, but that it would be better to do that from find in a short script called from -exec, to avoid issues with names containing newlines. – Kusalananda Apr 05 '22 at 17:38
  • @Kusalananda, here sed 's/^\.\///' is fine wrt newline as with -maxdepth 1,/ can't occur anywhere else in the file path. sed 's/..//' would be a problem though. One could shorten it to sed 's|./||' or sed s:./:: if they felt so inclined. – Stéphane Chazelas Apr 05 '22 at 19:06
  • @StéphaneChazelas Ah, with -maxdepth it's safe. So it is. I think I would personally prefer doing it with a parameter substitution in a shell called from -exec though (or with a -printf format if it was GNU find), which would make it future-proof for when the code is reused for something else. – Kusalananda Apr 05 '22 at 23:05
0

You can use shell globbing along with redirection:

$ ls -d k*g/ > filename.txt

This will break if you have directories whose names contain newline characters. (I think it will only pick up directories and not files, but I'm not in front of a computer to check.)

strugee
  • 14,951