One .bashrc to rule them all

Making my programming experience in Windows as nice as programming in Linux.

Windows 11 is my daily driver. I have a Dell Latitude E6440 that runs Fedora wonderfully, but I just haven't been using it very much recently. So I spent some time minimising the friction of working in Windows. I ended up with a .bashrc file that works in both Windows and Linux.

One day, I'll get around to trying out WSL [1], but for now, I'm working with Git Bash and ConEmu [2].

Installing make #

I found using a Makefile convenient for storing Pelican invocations (build for dev, host locally and rebuild for dev on save, build for production). Lucky for me, Windows 10 and Windows 11 come with the Windows Package Manager winget [3], so installing make is as simple as:

winget install gnuwin32.make

Getting ssh-agent to play nicely with ConEmu #

I found that ConEmu was hanging after I exited the last shell. It turned out that invoking ssh-agent from .bashrc caused ConEmu to hang around—I guess it was waiting for ssh-agent to exit.

The trick is to get ConEmu to start ssh-agent as a task alongside Git Bash [4]. ssh-add looks for the variables SSH_AUTH_SOCK and SSH_AGENT_PID, but I couldn't get ConEmuC to pass them on to Git Bash, so I just modified the batch script to generate a bash script that exports the variables...

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@echo off

rem Delayed expansion causes !variable! to be re-interpreted each time it is encountered
setlocal EnableDelayedExpansion

rem Is ssh-agent.exe currently running?
tasklist /fi "IMAGENAME eq ssh-agent.exe" | find /i /n "ssh-agent.exe" >NUL

if errorlevel 1 (
    rem No - need to run it

    rem Set the location of ssh-agent.exe here
    rem Run ssh-agent.exe and pipe the output to the file .ssh_agent_info
    "%USERPROFILE%\AppData\Local\Programs\Git\usr\bin\ssh-agent.exe" -c >".ssh_agent_info"

    if errorlevel 1 (
        echo "Couldn't start ssh-agent"
        exit /b 1
    )

    rem Set environment variables so that SSH applications such as ssh-add.exe
    rem know how to to connect to the SSH agent
    for /f "eol=; tokens=2,3*" %%i in (.ssh_agent_info) do (
        if "%%i" == "SSH_AUTH_SOCK" (
            set "SSH_AUTH_SOCK=%%j"
        )

        if "%%i" == "SSH_AGENT_PID" (
            set "SSH_AGENT_PID=%%j"
        )
    )

    rem Remove semicolon at the end
    set "SSH_AUTH_SOCK=!SSH_AUTH_SOCK:~0,-1!"
    set "SSH_AGENT_PID=!SSH_AGENT_PID:~0,-1!"

    echo SSH_AUTH_SOCK=!SSH_AUTH_SOCK! >.ssh_agent_info
    echo SSH_AGENT_PID=!SSH_AGENT_PID! >>.ssh_agent_info
    echo export SSH_AUTH_SOCK >>.ssh_agent_info
    echo export SSH_AGENT_PID >>.ssh_agent_info
)

endlocal

...and then .bashrc looks for the script and runs it. Now I can call ssh-add and any keys I add will be available from all sessions within ConEmu.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Start ssh-agent
if command -v "ssh-agent" &>/dev/null; then
    # Is ssh-agent running? Count ps aux lines that contain ssh-agent
    # (This code should work on both Windows and *nix)
    if [ `ps aux | grep ssh-agent | grep -v grep | wc -l` = "0" ]; then
        # ssh-agent is not running; run it
        ssh-agent >.ssh_agent_info 2>/dev/null
    fi

    if [ "$SSH_AGENT_PID" == "" ] && [ -f .ssh_agent_info ]; then
        # Set $SSH_AGENT_PID and $SSH_AUTH_SOCK
        source .ssh_agent_info
    fi
fi

At the time of writing, 1Password has an SSH agent [5], but it requires Windows Hello, which I don't want to use. The 1Password team is working on a solution that doesn't require Windows Hello, though, so I might have to tweak my setup again in the future...

scp woes #

When attempting to transfer files via scp, I received the error message below:

scp: Received message too long 1097295214
scp: Ensure the remote shell produces no output for non-interactive sessions.

It turns out that the .bashrc installed on the remote host was causing scp to fail because the script generated by ssh-agent includes a line to print the process ID of ssh-agent [6].

10
11
12
13
    if [ "$SSH_AGENT_PID" == "" ] && [ -f .ssh_agent_info ]; then
        # Set $SSH_AGENT_PID and $SSH_AUTH_SOCK
        source .ssh_agent_info
    fi

So I silenced the script:

12
        source .ssh_agent_info >/dev/null 2>&1

Symlinking on Windows #

My project files are on the D: drive. I got tired of typing cd /d/projects, so I created a symlink using mklink in an Command Prompt as Administrator. I didn't know that you could do that!

C:\Users\user\> mklink /d projects D:\projects

My new .bashrc file #

At the end of this all, I think my .bashrc file has come out a little more robust. I'm very happy with the results.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# Bash options
HISTCONTROL=ignoredups  # Ignore duplicate entries
HISTSIZE=1000           # 1000 lines
shopt -s histappend     # Append to history file
shopt -s checkwinsize   # Tell Bash to check the window size after each command

# Aliases
alias ls='ls -hF --color=auto'

if uname -a | grep MINGW >/dev/null 2>&1; then
    # Allow Python to run in Git Bash
    alias python='winpty python'
    alias venv_activate='source .venv/Scripts/activate'
else
    alias venv_activate='source .venv/bin/activate'
fi

if command -v bc &>/dev/null; then
    alias bc='bc -l'
fi

if command -v xclip &>/dev/null; then
    alias xclip='xclip -selection clipboard'
fi

# Start ssh-agent
if command -v "ssh-agent" &>/dev/null; then
    # Is ssh-agent running? Count ps aux lines that contain ssh-agent
    # (This code should work on both Windows and *nix)
    if [ `ps aux | grep ssh-agent | grep -v grep | wc -l` = "0" ]; then
        # ssh-agent is not running; run it
        ssh-agent >.ssh_agent_info 2>/dev/null
    fi

    if [ "$SSH_AGENT_PID" == "" ] && [ -f .ssh_agent_info ]; then
        # Set $SSH_AGENT_PID and $SSH_AUTH_SOCK
        source .ssh_agent_info >/dev/null 2>&1
    fi
fi

# https://www.ditig.com/256-colors-cheat-sheet

_set_ps1() {
    if [[ "$TERM" =~ 256color ]]; then
        local c1='\[\033[38;5;044m\]' # DarkTurquoise
        local c2='\[\033[38;5;160m\]' # Red3
        local c3='\[\033[38;5;034m\]' # Green3
        local c4='\[\033[38;5;247m\]' # Grey62
    else
        local c1='\[\033[36m\]' # Cyan
        local c2='\[\033[31m\]' # Red
        local c3='\[\033[32m\]' # Green
        local c4='\[\033[37m\]' # Grey
    fi

    local c0='\[\033[0m\]' # Default colour

    export PS1="\
$c1\u@\h \
$c0\w \
$c1\$(_get_git_branch)\
$c4\$(_get_git_no_untracked)\
$c2\$(_get_git_untracked)\
$c4\$(_get_git_no_uncommitted)\
$c2\$(_get_git_uncommitted)\
$c4\$(_get_git_no_committed)\
$c3\$(_get_git_committed)\
$c0\\$ "
}

_get_git_branch() {
    # Print branch name, if any
    local branch=`git branch 2>/dev/null | grep '^*' | cut -d' ' -f2-`
    [[ -n "$branch" ]] && echo -n "$branch"
}

_get_git_no_untracked() {
    # Empty circle
    local character="\xE2\x97\x8B"

    # Ensure we are in a git repo
    if git b &>/dev/null; then
        # Print the character if there AREN'T any untracked files
        if ! git status 2>/dev/null | grep -q 'Untracked files' 2>/dev/null; then
            # Space in front to separate it from the branch name
            echo -e " $character"
        fi
    fi
}

_get_git_untracked() {
    # Empty circle
    local character="\xE2\x97\x8B"

    # Print the character if there ARE untracked files
    if git status 2>/dev/null | grep -q 'Untracked files' 2>/dev/null; then
        # Space in front to separate it from the branch name
        echo -e " $character"
    fi
}

_get_git_no_uncommitted() {
    # Filled circle
    local character="\xE2\x97\x8F"

    # Ensure we are in a git repo
    if git b &>/dev/null; then
        # Print the character if there AREN'T any unstaged changes
        if ! git status 2>/dev/null | grep -q 'Changes not staged for commit' &>/dev/null; then
            echo -e "$character"
        fi
    fi
}

_get_git_uncommitted() {
    # Filled circle
    local character="\xE2\x97\x8F"

    # Print the character if there ARE unstaged changes
    if git status 2>/dev/null | grep -q 'Changes not staged for commit' 2>/dev/null; then
        echo -e "$character"
    fi
}

_get_git_no_committed(){
    # Filled circle
    local character="\xE2\x97\x8F"

    # Ensure we are in a git repo
    if git b &>/dev/null; then
        # Print the character if there AREN'T any changes that have been added for commit
        if ! git status 2>/dev/null | grep -q 'Changes to be committed' &>/dev/null; then
            echo -e "$character"
        fi
    fi
}

_get_git_committed() {
    # Filled circle
    local character="\xE2\x97\x8F"

    # Print the character if there ARE changes that have been added for commit
    if git status 2>/dev/null | grep -q 'Changes to be committed' 2>/dev/null; then
        echo -e "$character"
    fi
}

_set_ps1

Footnotes #

[1] What is the Windows Subsystem for Linux? (docs.microsoft.com) ^

[2] ConEmu - Handy Windows Terminal (conemu.github.io) ^

[3] Use the winget tool to install and manage applications (docs.microsoft.com) ^

[4] ConEmu, cygwin/msys and ssh-agent (conemu.github.io) ^

[5] 1Password SSH agent (developer.1password.com) ^

[6] "Put simply, .bashrc, .bash_profile, .cshrc, .profile, etc., have to be silent for non-interactive sessions or they interfere with the sftp / scp connection protocol." (Stack Overflow) (unix.stackexchange.com) ^