5

These are all the files

I want to use cp command using file globing to copy all those files that do not have a numeric value.

The copied files should not include 12a, ghf3 and s2d.

I tried using cp [!0-9] *[!0-9] *[!0-9] destination/

2
  • 3
    Please clarify your specific problem or provide additional details to highlight exactly what you need. As it's currently written, it's hard to tell exactly what you're asking. Commented Jun 16, 2023 at 8:51
  • 8
    What does the picture show? Please don't post images of text Commented Jun 16, 2023 at 12:06

4 Answers 4

8

With zsh:

set -o extendedglob cp -- ^*<->*(D^/) /path/to/destination/ 

Or cp -- ^*[0-9]*(D^/) destination/, <-> matching any sequence of one or more ASCII decimal digits (a form of <x-y> where the bounds are not set).

(^ being the negation operator, D to include hidden files, ^/ to also exclude the files of type directory which cp will refuse to copy without -r/-R/-a anyway).

Same with bash:

shopt -s extglob failglob dotglob cp -- !(*[0123456789]*) /path/to/destination/ 

!(x) (from ksh, requires extglob to be set at the time the code using it is read) as the equivalent of zsh's ^x, dotglob as the equivalent of the D qualifier, bash has no equivalent for ^/. Without failglob, that could copy the file called !(*[0123456789]*) literally if there was no matching file.

A POSIX version using globs is as always very painful to implement as there's no glob qualifier or equivalent of nullglob, failglob (default behaviour in zsh) or globdot / dotglob / D qualifier nor ksh-style or zsh-style negation operators:

set -- [*] * [ "$1$2" = '[*]*' ] && shift shift set -- .[*] .* "$@" [ "$1$2" = '.[*].*' ] && shift shift for file do case "$file" in (*[0123456789]* | . | ..) ;; (*) [ -d "$file" ] && [ ! -L "$file" ] || set -- "$@" "$file";; esac shift done [ "$#" -gt 0 ] && echo cp -- "$@" /path/to/destination/ 

But you could use find:

LC_ALL=C find . ! -name . -prune ! -name '*[0-9]*' ! -type d \ -exec sh -c 'exec cp "$@" /path/to/destination/' sh {} + 

LC_ALL=C to work around the fact that -name's * could fail to match on a sequence of bytes that cannot be decoded as text in the user's locale. In the C locale, [0-9] and [0123456789] are also equivalent. That means however that that could exclude file names that are encoded in charsets such as BIG5 or GB18030 where the encoding of some characters contain the byte values for the ASCII digits. Unlikely outside of China and dependencies.

zsh should be able to cope with locales and filenames using those charsets or file names that are not entirely made of text. bash as well though it switches to a byte-wise matching if the file names can't be fully decoded which can cause surprising results in some corner cases including the same as above with BIG5/GB18030.

zsh's [0-9] is always the same as [0123456789], bash's rarely so outside the C/POSIX locale.


Your:

cp [!0-9] *[!0-9] *[!0-9] destination

Does not work because that's:

  • [!0-9]: filename that is a single character other than those sorting between 0 and 9. And if there's no such single-letter character, in bash without failglob or nullglob enabled, that remains [!0-9] which is a file name that contains 2 digits.
  • *[!0-9]: a filename that ends in a character other than those sorting between 0 and 9. So 1234567X would match for instance.
  • *[!0-9] same again which I guess is a typo, but if you meant [!0-9]* that would match X12345 and if you meant *[!0-9]*, that would match 1234X5678.

If you meant the equivalent of regexp ^[^0-9]*$, or better ^[^0123456789]*$ where * in regexp means 0 or more of the preceding thing instead of 0 or more characters in globs, then that would be *([!0123456789]) in Korn-style extended globs (as also supported by bash with extglob) or [^0-9]# in zsh extended globs. See also +([!0123456789]) (ksh) and [^0-9]## (zsh) for the equivalent of ERE + (won't make a difference here as a filename cannot be empty anyway).

Also note that as you forgot the -- to mark the end of options, any filename starting with - would have been treated as an option by cp. For instance, with a file named -rt.., with GNU cp, that would have ended up copy all other files and /path/to/destination recursively to the parent directory!

2
  • For the find version, what's wrong with the simpler find . -type f -not -name '*[0-9]*' -exec cp {} /path/to/dest/ \; (besides the LC_ALL thing)? Commented Jan 1 at 1:10
  • @Max, -type f is not the same as ! -type d as f (regular) and d (directory) are 2 of many types of files. -not is not standard, ! is the standard/portable equivalent. Outside the C locale, [0-9] is not guaranteed to be the same as [0123456789] and * could fail for filenames not encoded in the user's locale charmap; with -exec ... ';' you're spawning one process and running one cp per file. If you don't prune directories other than . you'll end up copying files in subdirectories as well (possibly overwriting each other in dest if they have the same name). Commented Jan 1 at 8:48
3

A POSIX approach could be to save off the files that do contain a character sorting between 0 and 9 (with [0-9]; or decimal digit with [[:digit:]] or only the 0123456789 ones with [0123456789]), copy the remainder, and then restore the saved set:

mkdir ".save.$$" && mv ./*[0-9]* ".save.$$/" cp ./* /target/directory/ && mv ".save.$$"/* . && rmdir ".save.$$" 

This will intentionally not copy dot files. If you want those included you would need a more complex solution.

1

Well I don't know if this is the best way, but this works:

cp $(ls --ignore="*[0-9]*") YOUR_DIR 

EDIT:

(read comment from @StéphaneChazelas why you should not use the first command)

This might work better:

find . -type f -maxdepth 1 -not -regex '.*[0-9].*' -exec cp {} move \; 
3
  • 3
    It only works with GNU ls and if $IFS contains the newline characters and none of the file names contain characters of $IFS (space tab and newline by default) or start with - or with POSIX-like shells other than zsh contain wildcard characters. It also won't copy files whose name starts with . and note that depending on the locale, [0-9] may match many more characters than 0123456789 Commented Jun 16, 2023 at 9:15
  • 2
    @StéphaneChazelas Every day I am here on unix stackexchange and I see you on almost every post^^ Where did you learn so much? It feels like you know every thing about linux xD No but for real I really like that you help out people so much. I also learned already so much from you. Just wanted to say thanks :) Commented Jun 16, 2023 at 9:21
  • 4
    Thanks. Beware that find is recursive and its -regex matches on the full path (and on GNU systems at least fails on file paths that are not entirely made of text, and again [0-9] may match more than [0123456789]). Commented Jun 16, 2023 at 9:32
0

Using Raku (formerly known as Perl_6)

raku -e 'for dir(test => none / \d /, ".", "..") { copy $_, $_ ~ ".bak" };' 

Raku is a language in the Perl-family of programming languages. The Raku code above creates a .bak backup file in the same directory. It uses an interesting operator class/type known as a Junction. There are four Junction types available in the Raku language: none, one, any and all.

~$ ls persons.txt places.txt thx1138.txt ~$ raku -e 'for dir(test => none / \d /, ".", "..") { copy $_, $_ ~ ".bak" };' ~$ ls persons.txt persons.txt.bak places.txt places.txt.bak thx1138.txt 

Above (specifically) a none Junction is given a list of 3 items that removes[*]:

  1. file-names containing a \d Unicode digit (using a Regex-matcher),
  2. the "." dot directory,
  3. the ".." dot-dot directory.

Once the correct file-names are selected, within the block $_ (these files) are copy copied into a backup file consisting of the file-name followed by.bak (single tilde ~ is used to concatenate strings in Raku).

Note that in Raku, \d denotes Unicode digits. To denote only Arabic digits, use <+[0..9]>. To denote a character-class other that Arabic digits, use <-[0..9]>.


Backup/Copy to a different directory:

Raku has multiple options for copying and/or backing-up files to a different directory. Know that a path consists of a dirname and basename. At some point you'll probably want to obtain absolute paths as opposed to relative paths (and resolve-ing them). Also, you can ascent through a path using parent, and add a directory to the path using add (which is a synonym for child). See the link below, "Input/Output the Definitive Guide" for tips.

Here's one approach (copy to backup /Tmp directory with original filenames intact):

~$ raku -e 'my $Cwd = $*CWD; \ mkdir(my $Tmp = $Cwd.parent.add: "/Tmp/"); \ for $Cwd.dir(test => none / \d /, ".", "..") { copy $_, IO::Path($Tmp ~ $_.basename) };' ~$ cd ../Tmp ~$ ls persons.txt places.txt 

[*]On platforms like MacOS you'll want to exclude copying of .DS_Store files as well.

https://docs.raku.org/type/IO/Path
https://docs.raku.org/language/io-guide#Input/Output_the_definitive_guide

2
  • 1
    Maybe worth noting that \d in raku does not only match on 0123456789 Commented Jan 1 at 13:07
  • @StéphaneChazelas done! Thx. Commented Jan 5 at 11:57

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.