Justin
Justin

Reputation: 45390

Search .netrc file in bash, and return username for given machine

I need a way (the most portable) in bash, to perform a search of the ~/.netrc file, for a particular machine api.mydomain.com and then on the next line, pull the username value.

The format is:

machine a.mydomain.com
  username foo
  passsword bar
machine api.mydomain.com
  username boo
  password far
machine b.mydomain.com
  username doo
  password car

So, it should matchin api.mydomain.com and return exactly boo from this example.

awk '/api.mydomain.com/{getline; print}' ~/.netrc

Get's me the line I want, but how do I find the username value?

Upvotes: 4

Views: 4345

Answers (8)

jrayhawk
jrayhawk

Reputation: 11

The "most portable way" to do Bash:

  • POSIX-only, Dash and Busybox sh compatible
  • only builtins, no external commands
  • errtrace-compatible

The "most portable way" to do .netrc machine/key matching involves at least:

  • Whitespace agnosticism
  • "default" fallback stanza support
  • port specification support

There is no formal specification for netrc, so measuring "portability" in terms of feature set compatibility becomes ambiguous much beyond the above list due to divergent evolution.

Several less portable syntactic and feature improvements that could be made with non-portable Bash are noted throughout.


netrc_match() { # <host list> [port list] [key list] [netrc file]
  [ -n "$1" ] && NETRC_MATCH_HOSTS="$1" || ! echo "ERROR: host list required as first argument." >&2 || return 1
  NETRC_MATCH_PORTS="${2-21 990}" # allow blank list
  NETRC_MATCH_KEYS="${3:-login password}"
  if [ -n "$4" ]; then
    NETRC="$4"
  elif [ -z "$NETRC" ]; then
    NETRC=~/.netrc
  fi

# Bash can avoid tainting the calling environment via:
#   scoping variable declarations using the "local" builtin
#   returning values via a Bash 4.3 "declare -n" named reference to a Bash 4.0 "declare -A" associative array
# We otherwise maintain something resembling namespace safety by verbosely prefixing variable names with the function name.

# Bash 3.1 can be set up to do case-insensitive string comparisons on NETRC_MATCH_HOSTS and NETRC_MATCH_KEYS:
  #trap "$(shopt -p nocasematch)" RETURN
  #shopt -s nocasematch

  # Let IFS take care of all whitespace deduplication and equivalence for us:
  #for NETRC_WORD in $(<"$NETRC"); do # bash 2.02 syntactic sugar
  for NETRC_WORD in $( while IFS= read -r line; do printf '%s\n' "$line"; done <"$NETRC" ); do
    if [ "$NETRC_MODE" = "value" ]; then
      NETRC_VALUE="$NETRC_WORD"
      NETRC_MODE="key" # next NETRC_WORD is a key
      #if [[ "$NETRC_KEY" == machine ]]; then # bash 3.1 with nocasematch
      if [ "$NETRC_KEY" = machine ]; then
# There is ambiguity about whether incomplete sets of NETRC_MATCHED_KEYS from previous matching machine stanzas should be discarded or retained.
# Here, we retain whatever we match, overwriting with whatever we find as we go, until we have a complete set.
        unset    NETRC_HOST_MATCHED
        unset    NETRC_PORT_MATCHED
        unset NETRC_MACHINE_MATCHED
        NETRC_HOST="${NETRC_VALUE%:*}" # filter everything after first :
        NETRC_PORT="${NETRC_VALUE#*:}" # filter everything before last :
        if [ "$NETRC_HOST" = "$NETRC_PORT" ]; then # if there was no :
# There is ambiguity here about whether port-free machine declarations should always match.
# Precedent is to match liberally, which we do here:
          NETRC_PORT_MATCHED=y
        else
          for NETRC_MATCH_PORT in $NETRC_MATCH_PORTS; do
            [ "$NETRC_MATCH_PORT" = "$NETRC_PORT" ] && NETRC_PORT_MATCHED=y && break
          done
        fi
        [ -n "$NETRC_PORT_MATCHED" ] || continue
        for NETRC_MATCH_HOST in $NETRC_MATCH_HOSTS; do
# netrc is ambiguous about relative-vs-absolute host matching.
# We are somewhat liberal about what we accept here (within the limitations of POSIX shell's simple string matching).
# Glob matching, DNS resolution, and libnss-style resolv.conf "search" domains are outside the scope of what POSIX shell can do.
          [ "${NETRC_MATCH_HOST#"${NETRC_MATCH_HOST%?}"}" = "." ] && NETRC_MATCH_HOST="${NETRC_MATCH_HOST%.*}" # normalize to relative host to avoid butchering ip addresses
          [ "${NETRC_HOST#"${NETRC_HOST%?}"}"             = "." ] &&       NETRC_HOST="${NETRC_HOST%.*}"
          # relative host comparison:
          #[[ "$NETRC_MATCH_HOST"    == "$NETRC_HOST"    ]] && NETRC_HOST_MATCHED=y && break # bash 3.1 with nocasematch
          [ "$NETRC_MATCH_HOST"     =  "$NETRC_HOST"    ]  && NETRC_HOST_MATCHED=y && break
        done
        [ -n "$NETRC_HOST_MATCHED" ] && [ -n "$NETRC_PORT_MATCHED" ] && NETRC_MACHINE_MATCHED=y
      elif [ -n "$NETRC_MACHINE_MATCHED" ]; then
        unset NETRC_TODO
        for NETRC_MATCH_KEY in $NETRC_MATCH_KEYS; do
          #if [[ "$NETRC_KEY" == "$NETRC_MATCH_KEY" ]]; then # bash 3.1 with nocasematch
          if [ "$NETRC_KEY" = "$NETRC_MATCH_KEY" ]; then
            #printf -v "NETRC_KEY_$NETRC_MATCH_KEY" "%s" "$NETRC_VALUE" # bash 3.1 eval avoidance
            eval "NETRC_KEY_$NETRC_MATCH_KEY=\"\$NETRC_VALUE\""
          fi
          #[ -z "${!NETRC_MATCH_KEY}" ] && NETRC_TODO=y # bash 2.0 eval avoidance
          [ -z "$( eval echo \"\$NETRC_KEY_"$NETRC_MATCH_KEY"\" )" ] && NETRC_TODO=y
        done
        [ -z $NETRC_TODO ] && return # No NETRC_MATCH_KEYS left unassigned. Return immediately to avoid processing "default" stanza.
      fi
    else # NETRC_MODE == "key" or unset due to being the first NETRC_WORD
      NETRC_KEY="$NETRC_WORD"
      case "$NETRC_KEY" in
# bash 3.1 nocasematch would apply to these:
        default) NETRC_MACHINE_MATCHED=y ;; # "default" key does not have a value, and intrinsically matches
        *) NETRC_MODE="value" ;; # next NETRC_WORD is a value
      esac
    fi
  done
  echo "WARNING: missing NETRC keys." >&2 # function should've already returned by this point
  return 2
}

netrc_match "api.mydomain.com" "" "username"

echo "$NETRC_KEY_username"

Upvotes: 1

laplasz
laplasz

Reputation: 3497

Another solution to get user of password:

grep -A2 armdocker.rnd.ericsson.se ~/.config/artifactory.netrc | awk '/login/ {print $2}'

grep -A2 gives back the following 2 lines of a requested machine

Upvotes: 0

engedics
engedics

Reputation: 63

I recently stumbled upon this issue and couldn't find an answer which covers the one-line format (i.e. machine x login y password z), and inline or intra-line comments.

What I ended up with is making the file into one line with xargs (so only single spaces remain), then using grep with lookbehind (for the keyword) and lookahead (for the next whitespace or end of line)

xargs < ~/.netrc | grep -oP '(?<=machine api\.domain\.com ).*?(?=( machine)|$)' | grep -oP '(?<=login ).*?(?=\s|$)'

This could be of course developed into a function of sorts with extending the dots in the remote host variable with backslashes, printing the password as well, etc.

get_netrc_entry() {
    machine_entry=$(xargs < ~/.netrc | grep -oP "(?<=machine ${1//./\\.} ).*?(?=( machine)|$)")
    grep -oP '(?<=login ).*?(?=\s|$)' <<<"${machine_entry}"
    grep -oP '(?<=password ).*?(?=\s|$)' <<<"${machine_entry}"
}

Upvotes: 1

glenn jackman
glenn jackman

Reputation: 247092

This sed is as portable as I can make it:

sed -n '
    /machine[   ]\{1,\}api.mydomain.com/ {      
        # we have matched the machine
        :a
        # next line
        n
        # print username, if matched
        s/^[    ]\{1,\}username[        ]\{1,\}//p
        # goto b if matched
        tb
        # else goto a
        ba
        :b
        q
    }
' ~/.netrc

The whitespace in the brackets is a space and a tab character.


Looking at this with fresh eyes, this is what I would write now:

awk -v machine=api.mydomain.com '
    $1 == "machine" {
        if (m)
            # we have already seen the requested domain but did not find a username
            exit 1
        if ($2 == machine) m=1
    }
    m && $1 == "username" {print $2; exit}
' ~/.netrc

or if you like unreadable oneliners

awk '$1=="machine"{if(m)exit 1;if($2==M)m=1} m&&$1=="username"{print $2;exit}' M=api.mydomain.com ~/.netrc

Upvotes: 1

Neale Pickett
Neale Pickett

Reputation: 306

This vanilla Bourne shell (which includes Bash, KSH, and more) function should parse any valid .netrc file: it handled everything I could think of. Invoke with netrc-fetch MACHINE [FIELD]. For your question, it would be netrc-fetch api.mydomain.com user.

netrc-fetch () {
  cat $HOME/.netrc | awk -v host=$1 -v field=${2:-password} '
    {
      for (i=1; i <= NF; i += 2) {
        j=i+1;
        if ($i == "machine") {
          found = ($j == host);
        } else if (found && ($i == field)) {
          print $j;
          exit;
        }
      }
    }
  '
}

Upvotes: 1

Jotne
Jotne

Reputation: 41460

If you like to avoid using the getline fuction use this:

awk '/api.mydomain.com/ {f=NR} f&&f+1==NR {print $2}' ~/.netrc
boo

As Ed write here: avoid using it.
http://awk.info/?tip/getline

This will find the line number of the pattern, and then
when line number is one more, print field #2

Can be shorten some to:

awk '/api.mydomain.com/ {f=NR} f&&f+1==NR&&$0=$2' ~/.netrc

or

awk 'f&&!--f&&$0=$2; /api.mydomain.com/ {f=1}' ~/.netrc

This may be the most robust way to do it.
If there are comments line or blank line after domain, other solution fails.

awk '/api.mydomain.com/ {f=1} f && /username/ {print $2;f=0}' ~/.netrc
boo

If domain is found, set flag f. If flag f is true and next line has username print field #2

Upvotes: 2

John1024
John1024

Reputation: 113934

$ awk '/api.mydomain.com/{getline; print $2}' ~/.netrc
boo

To capture it in a variable:

$ name=$(awk '/api.mydomain.com/{getline; print $2}' ~/.netrc)
$ echo "$name"
boo

By default, awk splits records (lines) into fields based on whitespace. Thus, on the line username boo, username is assigned to field 1, denoted $1, and boo is assigned to field 2, denoted $2.

Upvotes: 5

SMA
SMA

Reputation: 37083

Here's what you could do:

 awk '/api.mydomain.com/{getline;print $2}' ~/.netrc ##$2 will print username

Upvotes: 0

Related Questions