8
\$\begingroup\$

I want to replace rm by a more informative variant: I would like to see which files will be deleted, along with their size, and I would like this information to use the same coloring as ls. As an example (please imagine the coloring):

# remove -r foo/ bar/*
4K   bar/file1
1.2M foo/
6.1M bar/file2
remove -r 3 files, 7.3M [yn]? _

I built on suggestions found in another question, and now have a bash script that I begin to like. I have been using it for a few days without noticing any obvious errors. However, I would appreciate any help with making sure that the script will not misbehave in cases I did not foresee.

Here is my code for the coloring:

du_colored (){
    # read ls --color output into ls_colored_array
    # use \13 as a trick to handle names with spaces 
    read -d '\n' -r -a ls_colored_array <<< $(ls -Ad --color "$@" | tr " " "\13")

    if [[ ${#ls_colored_array[@]} = 0 ]]; then return 1; fi

    # - loop over the array and issue du -sh for every element (without coloring)
    # - exchange du's ouput with ls's
    # - finally sort the output
    for i in "${ls_colored_array[@]}"; do
        i=`echo $i | tr "\13" " "`
        printf '%s' "${i}" | sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g" | \
            xargs -n1 -0 du -sh | awk -v i="$i" '{ printf "%-4s ", $1; print i }'
    done | sort -h
}

and the removal script:

# Separate options to rm from 'real' arguments
for i in "$@"; do
    case $i in
        -*) options+=( "$i" ) ;;
        *)  toRemove+=( "$i" ) ;;
    esac
done

# Print file list
# Abort if no files match
du_colored "${toRemove[@]}"
if [ "_$?" != "_0" ]; then exit 1; fi

# Print summary
count=$(find "${toRemove[@]}" 2>/dev/null | wc -l 2>/dev/null)
size=$(du -sch "${toRemove[@]}" 2>/dev/null | tail -1 | tr '\t' ' ')
plural="s"
if [ $count -eq 1 ]; then plural=""; fi
printf "remove ${options[@]} ($count file$plural, $size) [ny]? " ; read
if [ "_$REPLY" = "_y" ]; then
    /bin/rm ${options[@]} ${toRemove[@]}
else
    echo '(cancelled)'
fi
  • Does this look safe in the sense that what du_colored presents is exactly what will be deleted? Both files and directories seem to be treated correctly, even if they include spaces or wildcards. Are there scenarios that I missed? An obvious one are file names that contain a \13 character.
  • The initial ls in du_colored throws errors for non-existing files, which I like because it mimicks the behavior of regular rm. However, I currently discard all error messages of the final find and grep commands, such that non-existing files do not produce multiple errors. This seems like a dirty trick. Is there a cleaner way?
  • Bonus question: Is there an easy way to make the output use multiple columns, just as a regular ls would? I tried several ways to pipe the output of du_colored to other tools, but either the colors were lost completely or the control characters for the coloring broke the alignment, as they do in ls --color | pr -2 -t.
\$\endgroup\$
0

2 Answers 2

3
\$\begingroup\$

Bonus question: Does ls -C help?

-C     list entries by columns

You need -r to remove directories, so do you intend to impose this check as well when directories are specified?

plural="s"
if [ $count -eq 1 ]; then plural=""; fi

That can be rewritten as

[ $count -eq 1 ] && plural="" || plural="s"

Is the use of _ really necessary here?

if [ "_$?" != "_0" ]; then exit 1; fi
if [ "_$REPLY" = "_y" ]; then

edit: Bonus question, round 2...

You could try this for du_colored:

du_colored() {
    du -sh "$@" | awk -F'\t' -v COL=4 \
        '{if(++c>COL){c=1;print""}("ls -Ad --color \""$2"\"")|getline entry;
        printf "%6s %s\t",$1,entry}END{print""}' | \
    column -s $'\t' -t
}

Instead of parsing ls output carefully to feed into du, I started from the output of du and then replacing each entry with its ls --Ad --color output. COL is used to fix the number of 'columns' we want column to pick up afterwards, which is in turn used to print the appropriate newlines.

Finally, I feed the output to column as a final attempt at pretty-printing. \t is used as the delimiter throughout.

It still doesn't look as nice as the default non-piped ls output, but at least it's something I suppose...

edit 2: Bonus question, round 3...

I hope this is what's required... :)

du_colored() {
    awk -F'/' 'NR==FNR{k=$0;gsub(/\x1B\[[;[:digit:]]*m/,"",k);s[k]=$0}
        NR!=FNR{l=$0;for(i=3;i<=NF;i+=2){v=$i;gsub(/[ ]+$/,"",v);sub(v,s[v],l)};
            gsub("/"," ",l);print l}' \
    <(ls -Ad --color "$@") \
    <(du -sh "$@" | awk -F'\t' -v C=4 '{if(++c>C){c=1;print""}printf "/%6s/%s\t",$1,$2}
        END{print""}' | column -s $'\t' -t)
}
  1. Get both the output of ls -Ad --color "$*" and du -sh "$@" ... using bash process substitution to pass into awk.

    • The du -sh command is in turn piped to an 'internal' awk command, similar to my earlier suggestion, for column to render properly. What's crucial to note here is that the start, the size and the file/directory entry are delimited using the / character, which is the safest delimiter as files and directories cannot have that (nested directories yes though).
  2. The 'main' awk command uses / to split each line into 1 + C * 2 columns (C=4 used above). By using the combination of NR==FNR/NR!=FNR conditions, we can specify different handling for the two 'files'.

    • For the ls output, i.e. NR==FNR, we construct a map from the file/directory (by stripping away the color codes - gsub(/\x1B\[[;[:digit:]]*m/,"",k)) to its colored output.
    • For the du output, we then substitute the file/directory in the appropriate fields (after trimming the extra spaces behind - gsub(/[ ]+$/,"",v)) with the colored output from the mapping.
    • After substituting each / character back to a character, we reprint the entire line, which now contains the colored output.

This is arguably more efficient as both ls and du are performed once on the arguments. The lesson learned here is that column doesn't play ball with most control characters (seemingly \t is the only exception here). For some reason, sort-ing on du's output is still introducing minor quirks in my own testing, so I'm leaving that out for now.

\$\endgroup\$
6
  • 1
    \$\begingroup\$ ls -C was also my first instinct, but I do not think that it helps in the current setup: I use ls only to populate the file list. The coloring and size information is then provided by du_colored on each item of the list. This formatted output is what I would like to print in multiple columns. \$\endgroup\$ Commented Jul 9, 2015 at 7:54
  • \$\begingroup\$ The second solution to the bonus question is quite elegant and probably also way more portable than mine, thanks a lot! My column still hickups on the varying number of coloring escape codes used for different file types, misaligning the entries slightly. Is this a known bug and/or something that is easy to fix? \$\endgroup\$ Commented Jul 9, 2015 at 13:05
  • \$\begingroup\$ @klimpergeist seems to be related: stackoverflow.com/questions/20151601/… \$\endgroup\$
    – h.j.k.
    Commented Jul 9, 2015 at 13:24
  • 1
    \$\begingroup\$ Indeed. Here is a version with column first. Only works if column has a way to access the terminal width. du_colored() { du -sh "$@" | sort -h | column | sed -e 's_\(\t\t*\)_\v\1\v_g' | awk -F'\v' '{ for(i=1;i<=NF;i+=4){ ("ls -Ad --color \""$(i+2)"\"")|getline entry; printf "%6s%s%s%s",$(i),$(i+1),entry,$(i+3)}; print ""}' } \$\endgroup\$ Commented Jul 9, 2015 at 15:16
  • 1
    \$\begingroup\$ The version from edit2 does not work for me. column complains about an overlong line. I fixed that by appending END {print ""} to du's awk, but now the part following NR!=FNR is not executed any more. I'll try later to solve this. If I can't, I might just stick with the version from my comment above. \$\endgroup\$ Commented Jul 10, 2015 at 20:46
0
\$\begingroup\$

Does this cause problems for utilities calling rm?

If so, this:

if [ -t 1 ] ; then ..your code..; fi

will make sure you don't send unwanted output to programs using rm. This solution only works for POSIX-compatible shells.

See this and other answers for more info on detecting whether you are in a shell or not.

\$\endgroup\$
5
  • \$\begingroup\$ I think this is a misunderstanding because the question was migrated from Unix to Code Review. @NicolaiS this answer style doesn't fit the CR model well, probably best to delete it. Sorry about the mix-up. \$\endgroup\$
    – Phrancis
    Commented Jul 9, 2015 at 4:28
  • 2
    \$\begingroup\$ It is alright to post answers pointing out bugs. \$\endgroup\$
    – user34073
    Commented Jul 9, 2015 at 4:31
  • \$\begingroup\$ It would be better to explain this in more detail in your answer, however. Feel free to drop by chat to discuss this: chat.stackexchange.com/rooms/8595/the-2nd-monitor \$\endgroup\$
    – user34073
    Commented Jul 9, 2015 at 4:32
  • \$\begingroup\$ Interesting information, but irrelevant to this code, since the entire point of the program is to display information to the terminal. \$\endgroup\$ Commented Jul 9, 2015 at 5:41
  • \$\begingroup\$ I would not even want to replace rm: Once you work on another system, bad things will happen. Instead, I called my script rem and added an alias to my bashrc: alias rm='echo "Refusing to delete! Use \"rem\"!" || ' \$\endgroup\$ Commented Jul 9, 2015 at 7:46

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.