7

This question already has an answer here:

I have the a series of markdown files in the working directory:

$ ls *.md
csv_reader.md  egrep.md  find.md  found_pdfs.md  osPathSep_help.md  readme.md  smtplib_help.md  today.md

I want to remove them except "today.md"

#!/usr/local/bin/bash
for i in ./*.md ; do
    if [[ $i != "today.md" ]]; then
        echo $i
    fi
done

Run it and get

$ bash bash/remove_files.sh
./csv_reader.md
./egrep.md
./find.md
./found_pdfs.md
./osPathSep_help.md
./readme.md
./smtplib_help.md
./today.md

Nonetheless, the structured commands are not handy in the command line, how could I accomplish such a task with shorter commands

marked as duplicate by muru, RalfFriedl, Isaac, Archemar, JigglyNaga Oct 31 '18 at 10:49

This question has been asked before and already has an answer. If those answers do not fully address your question, please ask a new question.

  • You can write that shorter on a single line: for f in *.md; do test "$f" = "today.md" || echo rm "$f"; done. (remove the echo to actually remove them). – mosvy Oct 31 '18 at 8:18
  • 1
    An easy-to-think way is mv today.md today.bak; rm *.md; mv today.bak today.md – jingyu9575 Oct 31 '18 at 9:23
  • "Nonetheless, the structured commands are not handy in the command line" Apart from the fact that this special case really does not need such a complex structure: You can always replace line breaks by a semicolon ;. – rexkogitans Oct 31 '18 at 9:29
19

Use a negative match (requires shopt -s extglob, but possibly already set):

rm !(today).md

(you can first use ls instead of rm to check the result).

Lots of power in extglob, you could also do

rm !(yesterday|today).md

if you wanted to spare two files.

12

find . -maxdepth 1 -name '*.md' ! -name today.md -type f -print

Should find all the files (-type f) in the current directory (. -- or explicitly put a directory name there) only (-maxdepth 1 prevents following subdirectories) that end in .md (-name '*.md'), excluding (!) the file today.md.

Be sure to include the single quotes around '*.md' so your shell doesn't try to expand that to the list of .md files in the current directory before it executes find.

It will print the list of files to be deleted. Change -print to -delete to delete them instead.

9

The above are better solutions, but the reason your code isn't working is because of the comparison.

"./today.md" is not equal to "today.md".

3

You may find it shorter to use bash's extended globbing feature to exclude the file you don't want:

shopt -s extglob
echo rm -- !(today).md

The above (after removing the echo for testing) says to match anything except today followed by .md.

Your example didn't match dot-files by default; you can change that behavior with shopt dotglob, if desired

Your script didn't work as written because the glob pattern you used (appropriately) prefixed the filenames with ./; therefore, your inner test should have compared against ./today.md.

-1

Unix is famous for having multiple ways to do the same simple tasks. Some people love to use sed, but I like grep, so if I were just typing on the command line, I would use:

rm -i `ls *.md | grep -v '^today.md$'`
  1. ls *.md lists all the files whose names end with .md.
  2. | pipes the results of ls into the next command, but as a side effect, causes ls to list the files on multiple lines, 1 per line.
  3. grep -v '^today.md$' echoes the lines that do not exactly match today.md
  4. rm -i then takes the results (because the commands are inside backticks) that asks you if you want to delete them, 1 by 1. I always do this when I am using a generated list of files, in case something goes wrong with the generator. You could use rm without the -i to delete the files without asking, but that is riskier and also does not give you a list of what files were deleted.
  • 1
    "Unix is famous for having multiple ways to do the same simple tasks" Yes, but parsing the output of ls is not one of them. At least, you should use ls -1 (the parameter is the number one, not the small letter L). – rexkogitans Oct 31 '18 at 9:32
  • 2
  • @DavidFoerster ls is not useless in this case, it is a convenient way of converting the space separated list of files produced by the glob into a newline separated list of files; The links you gave do not produce better solutions. The OP asked how to do something with shorter commands on the command line. It is true that this command line does not handle all possible filenames and is not suitable for use in a shell script, but it handles the OP's list of files perfectly. @rexkogitans the -1 flag is not needed, because it is the default behavior when the output is directed to a pipe. – Old Pro Nov 1 '18 at 20:00
  • @OldPro The link @DavidFoerster posted is exactly what I meant with "parsing the output of ls is not one of them": It is an anti-pattern with a lot of pitfalls. – rexkogitans Nov 5 '18 at 9:29
  • @rexkogitans I agree that parsing ls is a bad idea in a shell script, especially one meant to work on arbitrary input. It's different, though, when you are writing a one-time command to work on known input. In that case, parsing ls can be a quick, easy, and effective solution. Which is the OP's situation and what the OP asked for. – Old Pro Nov 5 '18 at 20:19

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