this post was submitted on 15 Jul 2023
27 points (100.0% liked)

Shell Scripting

1367 readers
1 users here now

From Ash, Bash and Csh to Xonsh, Ysh and Zsh; all shell languages are welcome here!

Rules:
  1. Follow Lemmy rules!
  2. Posts must relate to shell scripting. (See bottom of sidebar for more information.)
  3. Only make helpful replies to questions. This is not the place for low effort joke answers.
  4. No discussion about piracy or hacking.
  5. If you find a solution to your problem by other means, please take your time to write down the steps you used to solve your problem in the original post. You can potentially help others having the same problem!
  6. These rules will change as the community grows.

Keep posts about shell scripting! Here are some guidelines to help:


In general, if your submission text is primarily shell code, then it is welcome here!

founded 2 years ago
MODERATORS
 

I'm sure some of you have absolute monstrosities of sigils (I know I do, in my .zshrc alone). Post them without context, and try and guess what other users's lines are. If you want to provide context or guess, use the markdown editor to spoiler-tag your guesses and explanations!

top 31 comments
sorted by: hot top controversial new old
[–] gamma 6 points 1 year ago (1 children)
ZDOTDIR="${${(%):-%x}:P:h}"

Literal explanation

  • ${(%)...} enable prompt sequences
  • %x a prompt sequence which expands to the current file being executed
  • :P resolve symlinks
  • :h the parent directory

Full contextThis line is in my ~/.config/zsh/.zshenv, which I symlink into my home directory. This resolves the symlink and sets $ZDOTDIR to the directory in which my zsh config files actually live, that way they aren't all in my home directory.

[–] varsock 3 points 1 year ago* (last edited 1 year ago) (1 children)

I'm having a lot of fun taking these inscrutable lines of shell and asking AI to decipher it. Check it out:

spoilerThe ZDOTDIR variable in Zsh specifies the directory where the configuration files (.zshrc, .zshenv, .zshlogin, .zshlogout, .zprofile, and .zlogin) are located. By default, this directory is the home directory of the current user. If ZDOTDIR is set, Zsh will use the configuration files from that directory instead of the home directory.

The script you've posted appears to be setting the ZDOTDIR variable in a complex way, using Zsh parameter expansion and modifiers. Here's the breakdown:

${(%):-%x}: This is using Zsh's %(x,y,z) prompt sequence expansion. This construct is used to insert conditional strings in the prompt. However, in this case, the construct is being abused to get the full path of the currently running script (%x), since the else branch (- modifier) of a prompt sequence expansion is always executed.

:P: This is a Zsh parameter expansion modifier that makes the resulting path absolute by resolving symbolic links. It effectively "canonicalizes" the path.

:h: Another Zsh parameter expansion modifier, which removes the trailing component from a path, effectively getting the directory portion of a file path. This is equivalent to dirname in a POSIX shell.

In summary, this script sets ZDOTDIR to the directory of the currently running script, resolving any symbolic links to get the absolute path. This would make Zsh look in that directory for its configuration files, instead of the home directory. It seems to be useful for a script that's bundled with its own Zsh configuration files, and wants to use those instead of the user's configuration

[–] gamma 2 points 1 year ago* (last edited 1 year ago) (1 children)

Nice! This one was probably the most likely to be found, I actually got this trick from somewhere else online before I understood what it was doing. If you're curious, I've got a few other lines of nested Zsh expansions I picked out but didn't post.

[–] varsock 1 points 1 year ago (1 children)

curious to see if AI can decipher? Certainly! bonus that we get so see potentially beneficial shell

[–] gamma 1 points 1 year ago* (last edited 1 year ago) (6 children)

Here's all the ones I was considering. I only posted the first two here. I'll add the explanations later.

ZDOTDIR="${${(%):-%x}:P:h}"                                              # posted 
reply=( ${(M)dirs:#*/$~pattern}(Noe['REPLY=${(l[3][0])#REPLY:t}'][1]) )  # posted
typeset -a $1=("${(@ps[$2])"${2:-"$(<&0)"}"}")
local d=("${(@s[/])${(%):-%~}}")
print -rn $termcap[sc]${termcap[ch]//(\%i|\%p1|\%d)}$termcap[ct]${(j::)s}$termcap[rc]
[ "${(Oa@)argv[1,-2]}" "${argv[-1]//(#m)[\[\]]/${(#)$((6 ^ #MATCH))}}"   # this one is definitely not useful
set -- "${@[0,eoo]:/(#m)^-*/${MATCH:a}}" "${@[eoo+1,-1]:a}"
[–] varsock 1 points 1 year ago (1 children)

[ "${(Oa@)argv[1,-2]}" "${argv[-1]//(#m)[[]]/${(#)$((6 ^ #MATCH))}}" # this one is definitely not useful

spoiler

The given shell script appears to be written in the Zsh shell syntax. Let's break it down step by step:

  1. [ "${(Oa@)argv[1,-2]}" "${argv[-1]//(#m)[\[\]]/${(#)$((6 ^ #MATCH))}}" ]: This line encloses the entire script within square brackets [ ]. In Zsh, square brackets are commonly used for conditional expressions.

  2. "${(Oa@)argv[1,-2]}": This part refers to an expansion of the argv array, which typically holds command-line arguments passed to the script. Here's what the individual components mean:

    • ${(Oa@)}: This is a parameter expansion flag in Zsh that sorts the elements of the array in ascending order and expands each element as separate words. The @ symbol is used to indicate the array variable argv.
    • argv[1,-2]: This is array slicing syntax that extracts a sub-array of elements from index 1 to the second-to-last element (-2). It excludes the last element, which is assumed to be the final argument.

    In summary, this part expands and sorts the elements of the argv array, excluding the last argument.

  3. "${argv[-1]//(#m)[\[\]]/${(#)$((6 ^ #MATCH))}}": This part refers to another expansion of the argv array, specifically targeting the last element (argv[-1]). Here's what the individual components mean:

    • "${argv[-1]//pattern/replacement}": This is a parameter expansion that performs pattern substitution within the last element of the argv array.
    • (#m): This is an extended globbing flag in Zsh that enables multiline mode for pattern matching. It allows patterns to match across multiple lines.
    • [\[\]]: This is the pattern to match. It matches any occurrence of square brackets ([ or ]).
    • /${(#)$((6 ^ #MATCH))}}: This is the replacement part of the substitution. It calculates the bitwise XOR (^) of 6 and the matched pattern (#MATCH), and uses (()) to perform arithmetic expansion. The (#) flag is used to indicate that the result should be expanded.

    In summary, this part performs a substitution on the last element of the argv array, replacing any occurrence of square brackets with the result of a bitwise XOR operation between 6 and the matched pattern.

Overall, the script appears to process command-line arguments, sort and manipulate them, and then enclose the result in a conditional expression for further evaluation or use. The exact purpose or context of the script can only be determined by understanding its broader context and usage.

[–] gamma 1 points 1 year ago

Corrections

ascending order

O is descending order. o is ascending order. In particular (Oa) keeps the array order, but flips it.

The @ symbol is used to indicate the array vriable argv.

Not here. As a PE flag, @ puts array elements in separate words, even if the parameter is enclosed in quotes.

The #MATCH gives the codepoint of the (first) character in $MATCH. The (#) flag turns codepoints into characters.

Full contextThis is in a joke function called ], which is like [ but you have to specify the elements in reverse order and end it with a [. This uses bit-fiddling to swap [ and ] in the last parameter because I'm a masochist.

[–] varsock 1 points 1 year ago (1 children)

set -- "${@[0,eoo]:/(#m)^-*/${MATCH:a}}" "${@[eoo+1,-1]:a}"

spoiler

The given script appears to be written in the Zsh shell syntax. Let's break it down step by step:

  1. set --: This command sets the positional parameters of the script. In Zsh, the -- option is used to signify the end of options and the beginning of positional parameters.

  2. "${@[0,eoo]:/(#m)^-*/${MATCH:a}}": This part refers to the expansion of the positional parameters, specifically targeting the elements from index 0 to the end of options offset (eoo). Here's what the individual components mean:

    • ${@[0,eoo]}: This refers to a sub-array of positional parameters from index 0 to eoo. It extracts the elements corresponding to command-line options.
    • /(#m)^-*: This is a pattern match within a substitution. It matches any occurrences of hyphens (-) at the beginning of each element. The (#m) flag enables multiline mode for pattern matching.
    • ${MATCH:a}: This is the replacement part of the substitution. It expands the matched pattern (${MATCH}) and applies the :a modifier, which removes leading hyphens from the matched string.

    In summary, this part removes leading hyphens from the command-line options.

  3. "${@[eoo+1,-1]:a}": This part refers to the expansion of the positional parameters, specifically targeting the elements from the end of options offset (eoo + 1) to the last element. Here's what the individual components mean:

    • ${@[eoo+1,-1]}: This refers to a sub-array of positional parameters from the end of options offset (eoo + 1) to the last element. It extracts the arguments that come after the command-line options.
    • :a: This applies the :a modifier, which removes leading hyphens from each element.

    In summary, this part removes leading hyphens from the command-line arguments that come after the options.

Overall, the script modifies the command-line options and arguments by removing leading hyphens. The modified values are then set as the new positional parameters using the set command. The exact purpose or context of the script can only be determined by understanding its broader context and usage.

[–] gamma 1 points 1 year ago

CorrectionsIt impressively identified exactly what eoo meant, and handled the array splicing, but it tripped over Zsh's globbing syntax.

/(#m)^-*: This is a pattern match within a substitution. It matches any occurrences of hyphens (-) at the beginning of each element.

  1. The : in ${foo:/pat/repl} requires pat to match the whole element
  2. The ^ actually negates the match, rather than anchors it to the start. ^-* matches any element not beginning with a hyphen.
  3. The (#m) flag captures the whole match in the parameter $MATCH.
  4. ${MATCH:a} transforms relative paths to absolute paths.

Full contextI use this as a wrapper around nvim --remote to make sure any non-options (i.e.: files) I pass have absolute paths rather than relative paths.

[–] varsock 1 points 1 year ago* (last edited 1 year ago)

I posted interpretations to the remaining ones. I did not have time to massage the prompts or verify the answers. I assume you know what they do and can comment on the accuracy. I am rate limited and had to use a lower tier model, As free up more queries I can re-do some or have it expand on what it's purpose and usefulness might be.

[–] varsock 1 points 1 year ago (1 children)

typeset -a $1=("${(@ps[$2])"${2:-"$(<&0)"}"}")

spoilerSo, overall, this script reads input from $2 or stdin if $2 is not provided, splits it into an array based on the delimiter $2, and then assigns the array to the variable named $1. Note that this script only works in Zsh, not in Bash. Zsh has a more advanced parameter and array system than Bash, so this script can't be directly translated into Bash.

  1. typeset -a: This part declares an array variable.
    • -a flag specifies that the variable is an array.
  2. $1=("${(@ps[$2])"${2:-"$(<&0)"}"}"):
    • $1 refers to the first argument passed to the script.
    • "${2:-"$(<&0)"}" is an expansion that evaluates to the second argument passed to the script. If the second argument is not provided, it reads input from standard input (<&0 means read from stdin).
    • "${(@ps[$2])" is an expansion that performs parameter splitting.
    • @ specifies that the expansion should be split into separate array elements.
    • (ps[$2]) is a parameter expansion that performs word splitting on the second argument.
  3. The entire expression "${(@ps[$2])"${2:-"$(<&0)"}"}" is wrapped in parentheses and assigned to the variable specified by $1.

In summary, this script takes two arguments: $1 represents the name of the array variable, and $2 represents the values to be assigned to that array. It then assigns the array $2 to the variable named by $1 after performing parameter and word splitting on the second argument. If the second argument is not provided, it reads input from standard input.

[–] gamma 1 points 1 year ago

This one was impressively spot-on.

[–] varsock 1 points 1 year ago (1 children)

local d=("${(@s[/])${(%):-%~}}")

spoilerThe given shell script is written in the Zsh shell syntax. It initializes a local array variable d with the expansion of a string. Let's break it down step by step:

  1. local d=: This line declares a local variable d. The local keyword is used to define variables with local scope, meaning they are only accessible within the current scope, such as within a function or a block of code.

  2. "${(%):-%~}": This part of the script performs string expansion and substitution to populate the array d.

    • %~ is a special parameter expansion in Zsh that expands to the current working directory (tilde expansion).

    • (%): is another parameter expansion flag that performs splitting and globbing on the value obtained from %~. It splits the resulting string into separate elements based on the / delimiter and performs globbing (filename expansion) on each element.

      For example, if the current working directory is /path/to/some/directory, then %~ expands to /path/to/some/directory, and (%): splits it into individual components: path, to, some, and directory. If there are any glob patterns present, such as * or ?, they would be expanded as well.

  3. ("${(@s[/])${(%):-%~}}"): This part surrounds the expanded string with parentheses to create an array and stores it in the variable d. The (@s[/]) syntax is an array flag in Zsh that splits the resulting string into separate elements based on the / delimiter. Each element represents a directory component obtained from the %~ expansion.

In summary, this script initializes the local array variable d with the directories present in the current working directory's path. Each directory is stored as a separate element in the array d.

[–] gamma 1 points 1 year ago

It may have gotten some details wrong, but the summary is spot-on.

Corrections(%) enables prompt expansion. %~ is a string, which when prompt expanded, is $PWD with tilde expansion.

[–] varsock 1 points 1 year ago (1 children)

print -rn $termcap[sc]${termcap[ch]//(%i|%p1|%d)}$termcap[ct]${(j::)s}$termcap[rc]

spoilerThe given shell script appears to be written in Zsh, a popular Unix shell. Let's break down the script step by step:

  1. print -rn: This command is used to print the output without a newline character (-n) and without interpreting any escape sequences (-r).

  2. $termcap[sc]: It refers to a termcap variable sc, which stands for "save cursor position." Termcap is a database and library used to control the behavior of terminals. $termcap[sc] saves the current cursor position.

  3. ${termcap[ch]//(\%i|\%p1|\%d)}: This part performs a substitution on the value of the termcap variable ch. The double slashes (//) indicate a global substitution, and the pattern (\%i|\%p1|\%d) matches any of the three percent sequences: %i, %p1, or %d. It replaces all occurrences of these sequences with an empty string.

  4. $termcap[ct]: It refers to the termcap variable ct, which stands for "clear to end of line." It clears everything from the cursor position to the end of the current line.

  5. ${(j::)s}: This uses parameter expansion to join the array variable s using colons (:) as separators. The (j::) flag indicates that colons should be used as separators.

  6. $termcap[rc]: It refers to the termcap variable rc, which stands for "restore cursor position." It restores the saved cursor position.

In summary, this shell script saves the cursor position, performs a substitution on the value of ch termcap variable, clears the current line from the cursor position to the end, prints the value of array variable s joined by colons, and finally restores the cursor position. The purpose and context of this script can only be determined based on the larger context or the values of the termcap variables sc, ch, ct, s, and rc.

[–] gamma 1 points 1 year ago

CorrectionsFirst, a look at man termcap:

       ch   Move cursor horizontally only to column %1
       ct   Clear tabs

(j::) actually joins with no seperator. The character following the j is used to capture the separator. Nearly any character or paired bracket can be used, but colons are common. Other ways to write this which might have been more obvious: (j''), (j[]), (j<>). To actually join with colons, something like (j.:.) would have been used.

Full contextZsh agressively sets tabstops to every 8 characters. This is in a function which I trap to the WINCH signal, to set the termstops to 4 using the $termcap sequences rather than the external command tput:

.on_winch(){
	# tabs
	local -a s
	repeat COLUMNS/$1 s+=(${termcap[RI]/\%p1\%d/$1}$termcap[st])

	# final
	print -rn $termcap[sc]${termcap[ch]//(\%i|\%p1|\%d)}$termcap[ct]${(j::)s}$termcap[rc]
}
.on_winch 4
trap '.on_winch 4' WINCH

[–] gamma 6 points 1 year ago

Here's one with a bit more context (and actually in Bash, rather than Zsh:

typeset -A any
while ((${#anys})); do
	any[${anys:0:1}]="/${anys:0:1}/!d;"
	anys=${anys:1}
done
anys=${any[*]}

Literal explanation

  • Iterate through the characters in $anys
  • For each character $c, define any[$c] to be the string /$c/!d (e.g.: any[x]='/x/!d')
  • Concatenate the values in ${any[@]}

Full contextThis is building a sed command to delete all lines which don't contain every character of the string $anys. The associative array and concatenation is to ensure I don't repeat myself when building the command. This is used in a program called dewordle, which prints all remaining words in a dictionary given the current known information in a game of wordle.

[–] xcjs 3 points 1 year ago* (last edited 1 year ago) (1 children)

It's not bash itself that was the complex part exactly, but I have a CI/CD pipeline that generates epub files from markdown. In some cases I have custom designed covers, but where a cover doesn't exist I have a bash script generate one using Imagemagick.

I wanted to generate the cover in one command to lessen performance impacts and disk I/O, but it took me a few weeks to figure out how to do it all in a single Imagemagick command:

convert \
    -size 960x1536 \
    -background "${backgroundColor}" \
    -fill "${textColor}" \
    -font "Liberation-Serif" \
    -pointsize 96 \
    -gravity north \
    caption:"${title}" \
    -bordercolor "rgb(0, 0, 0)" \
    -border 2 \
    -bordercolor "${borderColor}" \
    -border 40 \
    -background none \
    -fill "${textColor}" \
    -font "Liberation-Serif" \
    -pointsize 48 \
    -gravity south \
    -geometry +0-800 \
    -annotate +0+40 "${author}" \
    "${destination}cover.jpg"

Eventually it made an abstract sense to me, and I was able to bring it down to two commands and then finally one. This generates a cover with a selected background color (based on content type) and contains title text that will wrap to an inner border.

I think I had to give up on the author being wrapped, but it's much smaller than the title anyway.

[–] gamma 2 points 1 year ago (1 children)

Nice work! I've definitely had some fun with convert recently, as you might tell from the community banner.

spoilerIt is an image showing the script I used to create it 😛.

[–] xcjs 1 points 1 year ago* (last edited 1 year ago) (1 children)

That's certainly a script! 😅

[–] gamma 2 points 1 year ago* (last edited 1 year ago) (1 children)

It's a little more complex than needed, for the pixelated background I used xcolor names and then modulated them darker, but then relented and used hexcodes for the text in the foreground.

[–] xcjs 1 points 1 year ago

It's very impressive! I try to write legible code first, and if my shell scripts get too complex, I move on to another tool typically:

  • C# scripting
  • Python
  • PowerShell
  • Node.js

That might be why I don't have many cryptic examples.

[–] [email protected] 2 points 1 year ago (1 children)

I don't have a riddle and I can't read Bashisms. In posix shell the most unreadable things are parameter expansion, but compared to the lines OP posted they seems straight forward.

[–] gamma 2 points 1 year ago* (last edited 1 year ago)

I've written multiple Zsh plugins, so I've gotten familiar with a lot of Zsh's features. I've probably read every line in man zshexpn twice by now having referenced it so much.

Zsh expansions can look gnarly because parameter expansion forms can be nested. Where Bash has to do:

tmp=${foo%$suffix}
len=${#tmp}

Zsh can do it in one:

len=${#${foo%$suffix}}

Once you add PE forms which don't exist in Bash, parameter expansion flags, array subscript flags, globbing qualifiers and modifiers, you can get something which looks absolutely unreadable.

[–] gamma 2 points 1 year ago
reply=( ${(M)dirs:#*/$~pattern}(Noe['REPLY=${(l[3][0])#REPLY:t}'][1]) )

Literal explanation

  • $~pattern interpret globbing characters (like *, ?, or [a-z]) in the parameter $pattern,
  • ${(M)dirs:#*/$~pattern} filter the array $dirs to those matching */$~pattern.
  • ${...}(...) everything in (...) are globbing qualifiers
  • (N) enable nullglob
  • (oe[...]) sort by the result of the shell snippet, where $REPLY is used as input and output
  • ${...REPLY:t} remove all leading path components from $REPLY
  • ${...#REPLY:t} substitute the length of that string
  • ${(l[3][0])#REPLY:t} left-pad with zeroes to a length of 3
  • reply=( ... ) save as $reply

Full contextThis line is in a function which dynamically expands ~[g:abc] to one of my git repos, choosing the shortest directory matching path/to/abc*, or if no match is found, the shortest matching *abc*. So between /long/path/to/abcdef and /short/abcdefg, it would choose the first one (provided both of those were in the search paths I configured).

[–] Reptorian 2 points 1 year ago* (last edited 1 year ago) (1 children)

Feel free to delete this post if you don't feel that this fits though I think the language I use is very similar to bash in some ways (G'MIC). Here's a real-world example of my own code in G'MIC that's pretty much painful to parse:

command "out2display : skip ${""1=},${""2=},${""3=},${""4=1},${""5=1},${""6=},${""7=},${""8=},${""9=},${""10=} if narg($""1) if $""1 $__bg rv blend alpha fi fi xalp if narg($""6) if narg($""2)&&narg($""3)&&narg($""4)&&narg($""5) {$""4},{$""5},1,{s#0},i(#-1,$""2+x,$""3+y) f. begin(ww=w-1;hh=h-1;);(x<(2+narg($""7))||x>ww-2)||(y<(2+narg($""7))||y>hh-2)?(xor($""6,i)>128?0:255):i j[0] [-1],$""2,$""3 rm. if narg($""8)&&narg($""9)&&narg($""10) if $""8==0||$""8>2 {$""4},{$""5},1,{s#0},i(#0,$__nw+($__min_tile*$""9)-$""2+x-$""4,$""3+y) f. begin(ww=w-1;hh=h-1;);(x<(2+narg($""7))||x>ww-2)||(y<(2+narg($""7))||y>hh-2)?(xor($""6,i)>128?0:255):i j[0] [-1],{$__nw+($__min_tile*$""9)-$""2-$""4},$""3 rm. fi if $""8==1||$""8>2 {$""4},{$""5},1,{s#0},i(#0,$""2+x,$__nh+($__min_tile*$""10)-$""3+y-$""5) f. begin(ww=w-1;hh=h-1;);(x<(2+narg($""7))||x>ww-2)||(y<(2+narg($""7))||y>hh-2)?(xor($""6,i)>128?0:255):i j[0] [-1],$""2,{$__nh+($__min_tile*$""10)-$""3-$""5} rm. fi if $""8==2||$""8>2 {$""4},{$""5},1,{s#0},i(#0,$__nw+($__min_tile*$""9)-$""2+x-$""4,$__nh+($__min_tile*$""10)-$""3+y-$""5) f. begin(ww=w-1;hh=h-1;);(x<(2+narg($""7))||x>ww-2)||(y<(2+narg($""7))||y>hh-2)?(xor($""6,i)>128?0:255):i j[0] [-1],{$__nw+($__min_tile*$""9)-$""2-$""4},{$__nh+($__min_tile*$""10)-$""3-$""5} rm. fi fi fi fi if narg($""7) f[0] begin(ww=w-1;hh=h-1;);(x%$__min_tile==0||y%$__min_tile==0)||(x==ww||y==hh)?$""7:i fi"

That was before I figured out how to properly code in local commands.

[–] gamma 2 points 1 year ago

This is disgusting.

It's perfect!

[–] kartoffelsaft 1 points 1 year ago (1 children)

I have this in my laptop's .bashrc

PS1='\e[0m\n\e[40m[\e[32m\u\e[37m] [\e[31m\A \d\e[31m] [\e[33m`pwd`\e[37m]\e[K\n\e[K\n\e[1A'
PS0='\e[0m\n'

hintsome of the escape sequences move the cursor

full explanationgenerates the prompt:


[username] [00:01 Thu Jan 1] [/home/username]
█

with a slightly brighter/darker background (depending on terminal colors), while also resetting it to not effect the appearance of command outputs

  • \e[0m\n: new blank line
  • \e[40m: sets the background color for the prompt
  • [: literal text
  • \e[32m\u\e37m: username in green, reset color for brackets
  • ] [: literal text
  • \e[31m\A \d\e[31m: time/date in red, reset color
  • ] [: literal text
  • \e[33mpwd\e[37m: calls pwd, prints it in orange
  • ]: literal text
  • \e[K\n: fill the rest of the prompt line with the background
  • \e[K\n: fill the line where commands are typed with the background
  • \e[1A: move the cursor up so that it's in the background-filled area

I am colorblind so I may have gotten colors wrong, but that's hardly where the interesting bit is.

[–] o11c 3 points 1 year ago* (last edited 1 year ago) (1 children)

That doesn't seem sensible. Moving the cursor will confuse bash and you can get the same effect by just omitting the last \n.

Note that bash 5.0, but not earlier or later versions, is buggy with multiline prompts even if they're correct.

Your colors should use 39 (or 49) for reset.

Avoid doing external commands in subshells when there's a perfectly good prompt-expansion string that works.

You seem to be generating several unnecessary blank lines, though I haven't analyzed them in depth; remember that doing them conditionally is an option, like I do:

#PS1 built up incrementally before this, including things like setting TTY title for appropriate terminals
PS0='vvv \D{%F %T%z}\n'
PS1='^^^ \D{%F %T%z}\n'"$PS1"
prompt-command-exit-nonzero()
{
    # TODO map signal names and <sysexits.h> and 126/127 errors?
    # (128 also appears in some weird job-control cases; there are also
    # numerous cases where $? is not in $PIPESTATUS)
    # This has to come first since $? will be invalidated.
    # It's also one of the few cases where `*` is non-buggy for an array.
    local e=$? pipestatus="${PIPESTATUS[*]}"
    # Fixup newline. Note that interactive shells specifically use stderr
    # for the prompt, not stdin, stdout, or /dev/tty
    printf '\e[93;41m%%\e[39;49m%'"$((COLUMNS-1))"'s\r' >&2
    # if e or any pipestatus is nonzero
    if [[ -n "${e/0}${pipestatus//[ 0]}" ]]
    then
        if [[ "$pipestatus" != "$e" ]]
        then
            local pipestatus_no_SIGPIPE="${pipestatus//141 /}"
            local color=41
            if [[ -z "${pipestatus_no_SIGPIPE//[ 0]}" ]]
            then
                color=43
            fi
            printf '\e[%smexit_status: %s (%s)\e[49m\n' "$color" "$e" "${pipestatus// / | }" >&2
        else
            printf '\e[41mexit_status: %s\e[49m\n' "$e" >&2
        fi
    fi
}
PROMPT_COMMAND='prompt-command-exit-nonzero'

[–] kartoffelsaft 2 points 1 year ago* (last edited 1 year ago) (1 children)

Moving the cursor will confuse bash and you can get the same effect by just omitting the last \n.

When I was testing it I did not get the same effect. Instead it would only put the background behind what I had typed and not the whole line. Doing it now it seems to be working with the omission. I would assume it's a terminal emulator bug because I believe I have changed emulators since I wrote it. I've now removed it, thanks for fixing a bug.

Avoid doing external commands in subshells when there’s a perfectly good prompt-expansion string that works.

I wanted my home directory to not get shortened to ~, and if there is some way to do that with \w it isn't easy to find out how.

Also, what's the reasoning for avoiding it (besides it being idiomatic)? I'm sure there is one, but I don't think I've run into it yet.

You seem to be generating several unnecessary blank lines

I just like the look of it, and I have the screen space to do it.

[–] o11c 3 points 1 year ago

Two of the most expensive things a shell does are call fork and call execve for an external program. pwd is a builtin (at least for bash) but the former still applies. $PWD exists even if you don't want that shortening; just like your backticks be sure to quote it once so it doesn't get expanded when assigning to PS1.

In general, for most things you might want to do, you can arrange for variables to be set ahead of time and simply expanded at use time, rather than recalculating them every time. For example, you can hook cd/pushd/popd to get an actually-fast git prompt. Rather than var=$(some_function) you should have some_function output directly to a variable (possibly hard-coded - REPLY is semi-common; you can move the value later); printf -v is often useful. Indirection should almost always be avoided (unless you do the indirect-unset bash-specific hack or don't have any locals) due to shadowing problems (you have to hard-code variable name assumptions anyway so you might as well be explicit).