1

I have a script which pulls something out of my shell history and saves it to a file, but if there is a newline in the history entry it gets returned to me with a literal \n instead of the newline character. I'd like to replace the \n with a newline before saving it to the file.

I'm on OS X, and figured the problem might be the BSD-based sed that ships with OS X (that always seems to be the problem when sed isn't behaving!) so I tried with gsed, and what was failing with sed worked on the first try with gsed:

history -1 | awk '{$1=""; print}' | gsed -e 's/^[[:space:]]*//' -e s/'\\n'/"\n"/

But if at all possible I'd like something that'll work with the default utilities (whether sed or something else) on both Linux or OS X, since I can't count on gsed being installed on OS X, and since GNU sed is the normal sed for Linux there is obviously no need for gsed.

Mat
  • 52,586
iconoclast
  • 9,198
  • 13
  • 57
  • 97
  • To be sure I'm clear on this: you run this same 3-part pipeline using sed, and you get \n? Or do you mean that the \n is coming from awk? – Tom Zych Nov 21 '15 at 02:34
  • I would guess it has nothing to do with sed or gsed ... it has to do with the shell you are using and the expansion of "\n". Try the final -e option to sed as -e 's/\\n/\n/g' (the g will do all in the line rather than just the first, btw) (not confident this will work though). – Murray Jensen Nov 21 '15 at 02:50
  • @TomZych: it's really just the last -e expression that's failing. The rest is just for context. Instead of a newline, I'm getting an n as the replacement with the code as shown if I use sed instead of gsed. But I tried lots of different things and none of them worked except using gsed. – iconoclast Nov 21 '15 at 03:12
  • yeah - it is implementation defined how backslash escapes should be interpreted on the right-hand-side of a s///ubstitution. the portable way to do it is to backslash-escape a literal \newline character into the substitution. – mikeserv Nov 21 '15 at 03:14
  • Or, awk which you are using anyway portably (POSIXly) supports \n and friends on both pattern and substitute of gsub (and other places too). But note $1="" (with default FS) squashes whitespaces in the command; sub(/^ [0-9]+ /,"") works better on bash history and I expect something not very different should on zsh. – dave_thompson_085 Nov 21 '15 at 07:35

1 Answers1

1
sed 's/\\\\/& /g;s/\\n/\
/g;  s/\\\\ /\\\\/g' <in >out

...should handle the \newline replacements without misinterpreting backslash escapes - so you can still have a literal \\n in input. I'm a little fuzzy on how you can ever get a \n out of your history commands, though.

I tried w/ zsh to find out how a \newline might be read out from the history file without being escaped like that:

echo '
\n'
history -1

11355 echo '\n\n'

That won't do. So, I tried this instead:

hist(){ ! cat "$1"; }
echo '
\n'
FCEDIT=hist fc -1

echo '
\n'

...so that's probably what you should be doing instead

For some reason zsh escapes \newlines in history or fc -l output in an ambiguous way, but when it hands a history command over to some editor, it gives it the real thing.

zsh's primary means of history manipulation is fc, and the history command is not much more than an alias for fc -l. When fc is called with the -l option it will list the matching history lines to stdout (after ambiguously escaping any non-printable characters), but fc's default behavior is to invoke another utility with one or more temp file arguments into which it has written all history matches for its args.

fc derives the name of the utility it invokes from the $FCEDIT environment variable, or, if it is unset or null, from $EDITOR, or, if likewise unsuccessful, defaults to vi. If the invoked utility returns true, fc will afterward attempt to run any commands it finds in the (presumably) edited temp files before removing them.

And so the above command sequence substitutes cat for any editor command, and inverts its return so that a successful readout of fc's tempfiles will return false - to keep fc from trying to run the commands again.

A more complete, sort-of drop-in solution which doesn't need to call out to an external utility could look like:

history()
        case    $1      in
        (*/*) ! while   [ -r "$1" ]
                do      while   read -rE RTN
                        do :;   done    <$1
                shift;  done;;
        (*)     local   RTN=1  IFS=
                FCEDIT=history fc "$@"
                return  $((RTN*$?))
        esac

...which ought to handle almost transparently any argument list you might expect zsh's history command to do; except that its output is always literal and it never includes history timestamps or event numbers in the output (unless you call it like history -l [args], in which case it will behave as the builtin history).

mikeserv
  • 58,310
  • I'm on zsh, so maybe it's the zsh history command that does that. – iconoclast Nov 21 '15 at 03:13
  • @iconoclast - yeah, that doesn't work. i tried w/ a literal newline followed by a literal backslash then n in a zsh shell and it came out echo '\n\n'. there's no way to tell them apart. there's gotta be a way. seems silly like that. – mikeserv Nov 21 '15 at 03:19
  • @iconoclast - please see the edit. – mikeserv Nov 21 '15 at 03:32
  • cool.... I don't entirely understand your new solution, but I'll try to pick it apart to understand it... – iconoclast Nov 21 '15 at 03:36
  • @iconoclast - fc -l is what history aliases to. by default fc doesn't just print history commands, it hands them to some editor in a temporary file. the editor is $FCEDIT, or, if it is null or unset, $EDITOR, or, if it is null or unset, vi. when you pop into the editor to edit your command if the editor returns 0 when you're through then the newly edited command is newly executed all over again. that's why we cat our first argument - (the shell's temporary file with our last history command) - and ! invert its return - so it doesn't return 0 and get reexecuted. – mikeserv Nov 21 '15 at 03:40
  • 1
    @iconoclast - i fleshed it out a little more, in case you're interested. – mikeserv Nov 22 '15 at 11:03