Date:        Sat, 8 Feb 2025 19:33:10 -0500
    From:        Greg Wooledge <g...@wooledge.org>
    Message-ID:  <20250209003310.gm29...@wooledge.org>

  | You really want bash's ${var//search/replace} feature for the
  | renaming step,

In some cases I might agree, not necessarily about the
mechanism, but about doing the change entirely using shell
primitives, to avoid the costs of forking.

But if we're going to stop and ask the user every time
a rename might need to be done, clearly that need is
gone, and we might as well just do

        newname=$( printf '%sX\n' "${oldname}" | tr ' ' _)
        newname=${newmame%X}

where the nonsense with X is just in case $oldname happens
to end in a \n.   But most likely, if we're replacing spaces
we'd want to be replacing \n as well, and we can just do

        newname=$( printf '%s' "${oldname}" | tr $' \n' __)

(whether visible easily or not, there are two underscores).

  | so as long as you're already using that,

So, I wouldn't be, so I can continue writing a portable
script.

  | For the problem as stated (no recursion), we can discard the "ls"
  | command,

Sure, that one I agree with, and makes it much easier, I was just
maintaining the original design as much as possible.

  | The only bash extensions we really want, then, are the parameter
  | expansion for the renaming step,

which, as above, isn't really needed at all.

  | and "read -p" for the prompt.

which could be just a printf without a trailing \n before the
read -- but doesn't need to be as read -p is in the standard.

  | We could use case instead of [[ for checking the user's
  | input, but why?

Because it works everywhere, and is more flexible.

  | We're already using two bash extensions,

You're using 1, and that one isn't needed for the task
(if we needed something more than tr, I'd be using sed).

  | and [[ is nicer to use.

seems so perhaps in this simple case, but is less so
when there are more possibilities - like if we wanted
a 'q' reply to read to mean "exit script here" (which
I did with an EOF reply to read - your versions ignore
the exit status from read .. naughty naughty), and we
might want an 'l' reply to mean "make the chars in this
file name visible, so I can see anything odd", and a
'r' command to allow the user to type the new file name
rather than just replacing spaces with underscores.

And ...

If we start out using case, adding more variants is
child's play - using [[ ]] (or anything else using "if")
and things get very messy and ugly very quickly (or
we just discard that code, and switch to using case, in
which case (no pun intended) using the if variant at all
was just a waste of time).

  | If I were going to write this in POSIX sh, the read -p could be
  | replaced with printf + read,

except we don't need to do that,

  | and the [[ with case,

doing that is better anyway, even in bash

  | but the name alteration becomes *ugly*.

a little yes.

  |                 new=$(
  |                     printf %s "$f" | tr '[:space:]' _
  |                     printf x
  |                 )

Hmm ... I typed my reply above before I read down this far.
Great minds....   The difference between X and x is irrelevant,
but there's no reason not to pass the 'x' (or X) through tr, it
won't alter it, but yes, I agree using [:space:] might be better
than the ' ' (literal space) I used above, though in the majority
of cases, probably doesn't make a difference (and might even be
doing the wrong thing).

  | Forking multiple processes just to replace some characters
  | in a string is sad.

Not really.   Sometimes inefficient.   As I (believe I) have said
before, I *never* write bash scripts (I use bash interactively)
except when testing bash (and usally a bunch of other shells too)
and have never failed to write useful scripts to do whatever I
need (the shell I use has a few POSIX extensions, but I rarely
use them - the most common would be $RANDOM I think, but most
shells support that now).

Fork/exec isn't sad, it is very neat, and extremely versatile,
but it can sometimes be slow.   What I do in cases where that
matters is to just use a shell loop, here:

        while case $f in
                (*' '*) f=${f%% *}_$f{#* };;
                (*)     break;;
              esac
        do :; done

or something similar, depending upon the actual needs.   In the shell
I use that's quite fast enough (and the shell itself being much smaller
and faster than bash, means unless there are a lot of spaces in a lot
of filenames, the script probably runs faster as well).    But, in this
case, as noted above, none of that is needed, script execution speed cannot
be relevant, so the simplest, clearest, most portable, method is best.
[aside: all the literal spaces in the matching above could become [[:space:]]
if desired, that would change the meaning slightly, but still work.]

  | P.S. Yeah, I could have skipped the "printf x" and ${new%x} steps here,
  | because it's not possible for tr's output to end with a newline (even
  | if the original filename does), because the newline is replaced with
  | a _ character.

Hmm, yes, so it is, I keep confusing [:space:] and [:blank:] (the latter
doesn't include \n, or a few others).   But if the original requirement
was just to replace spaces, then we shouldn't be using the char classes
at all (kind of depends whether unbreakable spaces, etc, are to be included)
And as I included the X trick as well, in general, I agree, it is the
right thing to do in cases like this.

kre

Reply via email to